Configuration
Every setting is available on Settings → Plugins → Jarhead and overridable from
config/jarhead.php.
Site IDs
One Hotjar Site ID per Craft site, keyed by site handle:
// config/jarhead.php
return [
'siteIds' => [
'default' => '$HOTJAR_SITE_ID',
'german' => '$HOTJAR_SITE_ID_DE',
],
];
Values are read through Craft's environment parsing, so production and staging can point at different Hotjar accounts — or staging at none. An empty or missing value means Hotjar is off for that site. That is the off switch; there is no second one, because "configured but disabled" is a state nobody can see.
| Setting | Default | |
|---|---|---|
enabled | true | Master switch. Off, Jarhead emits nothing anywhere |
siteIds | [] | Site ID per Craft site handle |
snippetVersion | 6 | Hotjar's hjsv. Here so a version bump needs no release |
Where the code goes
| Setting | Default | |
|---|---|---|
autoInject | true | Splice into front-end HTML automatically |
injectionPoint | 'head' | head or body |
scriptNonce | '' | CSP nonce, usually an environment variable |
Hotjar asks to be in the head, and it matters more than it sounds: the tracking code is what
starts the recording, so anything that happens before it is part of the session nobody can watch.
A page with no <head> falls back to the body either way.
Turn autoInject off to place the code yourself:
{{ craft.jarhead.snippet }}
Calling that tag stands automatic injection down for the page, so it is safe to add to a layout while leaving the setting on.
Content Security Policy
A CSP is the single most common reason a correctly installed tracking code does nothing — the browser reports the block to a console nobody is looking at. Utilities → Hotjar prints the directives Hotjar needs:
script-src https://static.hotjar.com https://script.hotjar.com 'unsafe-inline';
connect-src https://*.hotjar.com https://*.hotjar.io wss://*.hotjar.com;
img-src https://*.hotjar.com data:;
font-src https://*.hotjar.com;
style-src 'unsafe-inline';
Set scriptNonce and you can drop 'unsafe-inline' from script-src — the nonce is put on
Jarhead's own script tag and on the Hotjar script it creates.
Environments
| Setting | Default | |
|---|---|---|
allowedEnvironments | [] | Empty means all of them |
trackInDevMode | false |
Matched against CRAFT_ENVIRONMENT. See Installation for why filling this in
matters more than anything else here.
Who not to track
| Setting | Default | |
|---|---|---|
excludePreviews | true | Live preview, drafts, share tokens |
excludeAdmins | true | |
excludeLoggedIn | false | Blunt; right for a marketing site |
excludedGroups | [] | User group handles |
uriRules | [] | See below |
excludeAutomatedBrowsers | true | navigator.webdriver |
The control panel is never tracked, and there is no setting for it. A session recorder pointed at the control panel records other people's addresses, order histories and account details. That is a data breach with a subscription.
URI rules
'uriRules' => [
['pattern' => 'checkout/*', 'mode' => 'exclude'],
['pattern' => 'account/*', 'mode' => 'exclude'],
],
Patterns are globs by default, regular expressions when prefixed with re:, and __home__
matches the homepage.
An exclusion always beats an inclusion, whatever the order — these are read as "everywhere, except there". Write only exclusions and everything else stays tracked; write a single inclusion and nothing outside the inclusions is tracked.
Automated browsers
excludeAutomatedBrowsers refuses to run when navigator.webdriver is set — Playwright,
Puppeteer, Selenium and most uptime checkers. It is decided in the visitor's browser rather than
from the user agent, because a user-agent test on the server would vary your page cache.
Consent
| Setting | Default | |
|---|---|---|
consentMode | 'off' | off, cookie, event, dataLayer, manual |
consentCookie | '' | Cookie mode: the cookie name |
consentCookieValue | '' | Comma separated; substring: prefix supported |
consentEvent | 'jarhead:consent' | Event mode: the DOM event |
consentDataLayerKey | 'analytics_storage' | Google Consent Mode v2 |
consentDataLayerValue | 'granted' | |
consentPollInterval | 500 | Milliseconds, cookie mode only |
consentTimeout | 0 | Seconds; 0 watches for the life of the page |
honourDnt | true | Do Not Track |
honourGpc | true | Global Privacy Control |
Under a consent gate, nothing reaches Hotjar until the visitor agrees: no script element, no
request to static.hotjar.com, no _hjSettings. Not a blocked script and not a deferred one —
the tracking code is created by the gate, in the browser, after consent. (The gate itself is in
the page, holding Hotjar's URL as a string it has not used.)
Consent is decided in the browser, not on the server, because a cookie read server-side would vary the page cache — and a cached "yes" served to the next visitor is worse than no gate at all.
'consentMode' => 'cookie',
'consentCookie' => 'cookie_consent',
'consentCookieValue' => 'substring:analytics',
substring: matches anywhere inside the value, which is how most consent managers behave — they
store every category in one cookie and expect you to look for yours inside it. Leave
consentCookieValue empty and the cookie simply existing counts as consent.
For manual, nothing happens until your code calls window.jarhead.consent().
Do Not Track and Global Privacy Control are honoured on top of whichever gate you pick, and a refusal beats consent: consenting does not override a visitor's DNT.
Attributes
| Setting | Default | |
|---|---|---|
autoAttributes | site, section, entry type, environment | |
customAttributes | [] | ['name' => …, 'value' => …] |
identifyUsers | false | Send the Craft user ID |
hashUserIds | true | HMAC it with your security key |
spaSupport | false | Report route changes |
Available automatic attributes: craft_site, craft_section, craft_entry_type,
craft_template, craft_logged_in, craft_user_group, craft_environment, craft_language.
The craft_ prefix is deliberate — attribute names are a flat namespace shared with every other
integration sending to the same Hotjar site.
'autoAttributes' => ['craft_site', 'craft_section', 'craft_entry_type', 'craft_template'],
'customAttributes' => [
['name' => 'release', 'value' => '$RELEASE_TAG'],
],
Custom values are read through the environment too, so a build number or release tag can be an attribute without being committed.
There is no setting anywhere that sends an email address or a username. A Hotjar account is
not the right home for either, and an identifier only has to be stable to be useful — which is
why identifyUsers sends the Craft user ID, HMAC'd with your security key by default.