Troubleshooting
The failure mode of any tracking integration is silence, and silence looks exactly like success.
Two tools exist to break that tie: php craft jarhead/explain <uri> and Utilities → Hotjar.
Start with whichever is closer to hand.
There is no tracking code in the page source
Ask directly:
php craft jarhead/explain # the homepage
php craft jarhead/explain blog/hello # any URI
It answers with the first rule that applied. In rough order of how often each one is the culprit:
| Reason | What it means |
|---|---|
dev-mode | devMode is on and trackInDevMode is off. The default. |
admin | You are signed in as an admin. Admins are excluded by default. |
environment | This environment is not in allowedEnvironments. |
no-site-id | No Site ID resolves for this Craft site in this environment. |
preview | A live preview, draft URL or share token. |
uri-excluded | The URI matched an exclusion rule — the message names which. |
uri-not-included | Inclusion rules exist and this URI matches none of them. |
logged-in | excludeLoggedIn is on. |
user-group | The user is in an excluded group. |
control-panel | The control panel is never tracked. Not configurable. |
disabled | The master switch is off. |
The order matters: you are told the first reason that applies, so fixing it may reveal another.
"No Site ID" when you are sure you set one
The value is probably an environment variable that is not set here. An unresolved $HOTJAR_SITE_ID
reads back as "not configured in this environment" rather than as a literal string, which is
correct behaviour and the whole reason the setting is env-aware.
Utilities → Hotjar distinguishes the two: a site whose ID comes from an env var shows the
variable name and unset here, rather than not configured.
The code is in the page but there are no recordings
Open the browser console on a front-end page:
window.jarhead.status()
blockedBy | Meaning |
|---|---|
null and booted: true | Jarhead did its job. The problem is on Hotjar's side — keep reading. |
'dnt' | The visitor sends Do Not Track and honourDnt is on. |
'gpc' | The visitor sends Global Privacy Control. |
'automated' | navigator.webdriver is set — you are in Playwright, Puppeteer or Selenium. |
'consent-timeout' | consentTimeout elapsed before consent arrived. |
null and booted: false | A consent gate is open and waiting. See below. |
If it booted and Hotjar still shows nothing, press Verify in Utilities → Hotjar. The two answers that catch most cases:
- "Session recording is switched off for this site in your Hotjar account." Nothing about the installation is wrong. Turn recording on in Hotjar.
- "Hotjar is sampling N% of sessions." You are recording a fraction of traffic by design.
An automated browser is the answer more often than you would think
If you are testing with Playwright or a headless Chrome, excludeAutomatedBrowsers is doing
exactly what it was asked to. Turn it off temporarily, or test in a real browser.
The consent gate never opens
window.jarhead.status() shows booted: false with blockedBy: null — the gate is waiting.
Cookie mode. Check the cookie is actually there and actually matches:
document.cookie // is the cookie present at all?
window.jarhead.status().consentMode
The most common cause is a value mismatch. A consent manager that stores every category in one
cookie needs substring: — substring:analytics rather than analytics. Leave
consentCookieValue empty and the cookie merely existing counts as consent.
dataLayer mode. Existing entries are checked and later pushes are watched. Google Consent Mode
pushes an arguments object rather than a plain one; Jarhead handles both. Confirm the value is
actually granted:
window.dataLayer.filter(e => JSON.stringify(e).includes('analytics_storage'))
Event mode. The event is listened for on both window and document. Fire it manually to
prove the wiring:
window.dispatchEvent(new Event('jarhead:consent'));
Manual mode. Nothing opens the gate but window.jarhead.consent().
A gate that can never open — cookie mode with no cookie name — is reported as an error on the settings screen and in Utilities → Hotjar, not left for you to discover.
Nothing loads and the console shows a CSP error
A Content Security Policy is the most common reason a correctly installed tracking code does nothing, because the browser reports the block somewhere nobody is looking. Utilities → Hotjar prints the exact directives; see Configuration.
If you cannot allow 'unsafe-inline' on script-src, set scriptNonce to whatever generates the
nonce for the request. Jarhead puts it on its own script tag and on the Hotjar script that tag
creates.
Events fired from a template never arrive
They are queued until the gate opens, then flushed in order — that is by design. Check the queue:
window.jarhead.status().queued
A non-zero queued with booted: false means the events are fine and the gate is the problem.
Every session count is double
Something is placing the tracking code twice. Either a template calls
{{ craft.jarhead.snippet }} and something else adds Hotjar independently — a tag manager, a
hard-coded snippet in a layout, another plugin — or Hotjar's own installer was left in place.
Jarhead itself cannot double up: calling the Twig tag stands automatic injection down for that page, and the browser gate refuses to run twice.
Search your templates for static.hotjar.com and check your tag manager.
The page is truncated at the point the snippet was added
That is a stale content-length header, and Jarhead restamps it — so if you are seeing this,
something after Jarhead is modifying the response body without restamping. Look at any other
plugin or module hooking Response::EVENT_AFTER_PREPARE.
Recordings are full of your own team
Turn on the exclusions. excludeAdmins is on by default, but excludeLoggedIn and
excludedGroups are not:
'excludeLoggedIn' => true,
'excludedGroups' => ['editors', 'staff'],
Then confirm with the impersonation the tester gives you:
php craft jarhead/explain blog/hello --user=14