PHPackages                             justinholtweb/craft-jarhead - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. justinholtweb/craft-jarhead

ActiveCraft-plugin

justinholtweb/craft-jarhead
===========================

Hotjar for Craft CMS — the tracking code on every page with no template change, plus the environment gating, consent gating and automatic attributes a real site needs.

5.0.0(yesterday)11↑2900%proprietaryPHPPHP ^8.2

Since Aug 23Pushed today1 watchersCompare

[ Source](https://github.com/justinholtweb/craft-jarhead)[ Packagist](https://packagist.org/packages/justinholtweb/craft-jarhead)[ RSS](/packages/justinholtweb-craft-jarhead/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (1)Versions (2)Used By (0)

Jarhead
=======

[](#jarhead)

**Hotjar for Craft CMS.** Paste the Site ID; every front-end page gets the tracking code, with no template change.

Reference point: the WordPress plugin [Hotjar](https://wordpress.org/plugins/hotjar/). That plugin is one field and one `wp_head` hook, and for a single-site WordPress blog it is genuinely enough. Craft sites are not that. Jarhead does the one field too — and then does the four things that go wrong on the day *after* you install it.

Free. Single edition, everything switched on, no licence key.

---

Install
-------

[](#install)

```
composer require justinholtweb/craft-jarhead
php craft plugin/install jarhead
```

Craft 5.3+, PHP 8.2+. No runtime dependencies beyond Craft's own. No database tables.

Then: **Settings → Plugins → Jarhead**, paste your Hotjar Site ID, done.

---

The four things
---------------

[](#the-four-things)

### 1. Half your recordings are your own team

[](#1-half-your-recordings-are-your-own-team)

Hotjar bills by session. An editor clicking through drafts, a designer refreshing a template, an admin previewing an entry — all of it records, all of it counts, none of it is a customer.

Jarhead excludes admins and previews by default, and will also exclude signed-in users, named user groups, and any URI you name.

```
'excludeAdmins' => true,
'excludePreviews' => true,
'excludedGroups' => ['editors', 'staff'],
'uriRules' => [
    ['pattern' => 'checkout/*', 'mode' => 'exclude'],
    ['pattern' => 'account/*', 'mode' => 'exclude'],
],
```

It also refuses to run under an automated browser — Playwright, Puppeteer, Selenium, most uptime checkers. That one 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.

### 2. The Site ID ships to every environment

[](#2-the-site-id-ships-to-every-environment)

One ID in one field, deployed to staging, to `.ddev.site`, to a client's UAT box. Every recording of a developer typing `asdf` into a form lands in the same account as the real ones, and nothing in the data tells them apart afterwards.

```
// config/jarhead.php
return [
    'siteIds' => ['default' => '$HOTJAR_SITE_ID'],
    'allowedEnvironments' => ['production'],
];
```

The Site ID is per Craft site and read through `App::parseEnv()`, so production and staging can have different Hotjar accounts — or staging can have none. An environment variable that does not exist in this environment is treated as "not configured here", not as a literal string, so the same project config is correct everywhere.

`php craft jarhead/status --strict` exits non-zero when a site that should be sending is not, which makes it a post-deploy check rather than something a stakeholder tells you about.

### 3. Consent is bolted on afterwards, if at all

[](#3-consent-is-bolted-on-afterwards-if-at-all)

Session recording is personal data by any reading of GDPR — it is a video of somebody using your site. 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, and it holds Hotjar's URL as a string it has not used.)

```
'consentMode' => 'cookie',
'consentCookie' => 'cookie_consent',
'consentCookieValue' => 'substring:analytics',
```

Four gates:

ModeBoots when`cookie`a named cookie exists, optionally holding a named value`event`a DOM event fires on `window` or `document``dataLayer``dataLayer` carries `analytics_storage: 'granted'` — Google Consent Mode v2`manual`your code calls `window.jarhead.consent()`Do Not Track and Global Privacy Control are honoured on top of whichever gate you pick, and both are read in the browser for the same caching reason.

Anything you call while the gate is shut is queued, not lost — an event written into a template is earlier than the visitor's decision by definition.

### 4. A recording with no tags is a video you have to watch

[](#4-a-recording-with-no-tags-is-a-video-you-have-to-watch)

Every Craft page already knows its section, its entry type, its template, its site, and whether the viewer was signed in. Jarhead sends the ones you switch on, as Hotjar attributes, so recordings and heatmaps can be filtered:

```
'autoAttributes' => ['craft_site', 'craft_section', 'craft_entry_type', 'craft_template'],
'customAttributes' => [
    ['name' => 'release', 'value' => '$RELEASE_TAG'],
],
```

Signed-in users can optionally be identified by their Craft user ID, HMAC'd with your security key by default. 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.

---

Twig
----

[](#twig)

Everything is safe to call on a page that is not being tracked; you never have to ask first.

```
{# Place the tracking code yourself. Doing this stands automatic injection down for the page,
   so you can leave the setting on. #}
{{ craft.jarhead.snippet }}

{# Is this page tracked, and if not, why not? #}
{{ craft.jarhead.enabled }}
{{ craft.jarhead.explain.reason }}     {# 'preview', 'uri-excluded', 'environment', … #}
{{ craft.jarhead.explain.message }}    {# a sentence you can put in a staging footer #}

{# Events and attributes. Queued if the consent gate is still shut. #}
{{ craft.jarhead.event('Newsletter signup') }}
{{ craft.jarhead.identify(null, { plan: 'pro', trial: 'no' }) }}
{{ craft.jarhead.tag(['checkout', 'guest']) }}

{# The raw snippet, for pasting into a tag manager. No gate, no exclusions, no attributes. #}
{{ craft.jarhead.trackingCode() }}
```

JavaScript
----------

[](#javascript)

```
window.jarhead.consent();                       // grant consent (the only way in under `manual`)
window.jarhead.event('Added to cart');
window.jarhead.identify('user-123', { plan: 'pro' });
window.jarhead.tag(['checkout']);
window.jarhead.stateChange('/checkout/step-2'); // a route change Jarhead did not see
window.jarhead.ready(function () { /* Hotjar is loaded */ });
window.jarhead.status();                        // why it has or has not booted
```

`status()` is the first thing to type into a console when a page is not recording:

```
{ booted: false, blockedBy: 'gpc', consentMode: 'cookie', hotjarSiteId: 1234567, queued: 2 }
```

---

The Hotjar utility
------------------

[](#the-hotjar-utility)

**Utilities → Hotjar.** Three questions, in the order they actually get asked:

1. **Is it configured?** Every Craft site, its resolved Site ID, whether that came from an environment variable, and the verdict for its homepage right now.
2. **Does Hotjar know this ID?** A button that asks Hotjar's CDN for the tracking script belonging to your Site ID, and reads the answer properly. Hotjar returns `200 application/javascript` for *every* numeric ID ever asked for — an ID that does not exist gets 200 with an empty body — so a check written against the status code says yes to `hotjar-1.js`. Jarhead reads the body.

    The script also carries your site's Hotjar settings, so this reports the two things that actually explain an empty account: **recording switched off** for the site, and **sampling**below 100%. Neither is an installation problem, and both look exactly like one.

    This is the only outbound request Jarhead ever makes, it happens only when somebody presses the button, and it goes to `static.hotjar.com` and nowhere else.
3. **Why is it not on *that* page?** Type a URI, optionally a user ID and a preview flag, and get back the first rule that applied — through the same code path a real request takes.

The utility also prints the Content Security Policy directives Hotjar needs, because a CSP is the single most common reason a correctly installed tracking code does nothing and reports it to a console nobody is looking at.

---

Console
-------

[](#console)

```
php craft jarhead/status                 # what each site would do right now
php craft jarhead/status --strict        # non-zero exit if a site is not sending — for CI
php craft jarhead/verify                 # ask Hotjar whether the Site IDs exist
php craft jarhead/explain checkout/cart  # why that URI would or would not be tracked
php craft jarhead/explain blog --user=14 # …as a specific user
php craft jarhead/snippet --site=default # the raw tracking code
```

---

What Jarhead never does
-----------------------

[](#what-jarhead-never-does)

- **It never tracks the control panel**, 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.
- **It never changes Hotjar's snippet.** The tracking code is emitted verbatim, because it is the documented integration surface — Hotjar's own support will ask you to compare it — and a plugin that "improves" it is a plugin whose bug reports all close as "not our code". Jarhead changes what runs *around* the snippet.
- **It never stores anything about a visitor.** No tables, no counters, no logs of who was tracked. Hotjar is the thing collecting the data; a shadow copy in Craft would be a second privacy problem added to solve the first.
- **It never sends an email address or a username.**

---

Settings reference
------------------

[](#settings-reference)

Every setting is overridable from `config/jarhead.php`.

SettingDefault`enabled``true`Master switch`siteIds``[]`Hotjar Site ID per Craft site handle; `$ENV_VAR` supported`snippetVersion``6`Hotjar's `hjsv``autoInject``true`Splice into front-end HTML automatically`injectionPoint``'head'``head` or `body`; falls back if the tag is absent`scriptNonce``''`CSP nonce, usually an environment variable`allowedEnvironments``[]`Empty means all of them`trackInDevMode``false``excludePreviews``true`Live preview, drafts, share tokens`excludeAdmins``true``excludeLoggedIn``false``excludedGroups``[]`User group handles`uriRules``[]``['pattern' => …, 'mode' => 'include'|'exclude']`; `re:` for regex`excludeAutomatedBrowsers``true``navigator.webdriver``consentMode``'off'``off`, `cookie`, `event`, `dataLayer`, `manual``consentCookie``''``consentCookieValue``''`Comma separated; `substring:` prefix supported`consentEvent``'jarhead:consent'``consentDataLayerKey``'analytics_storage'``consentDataLayerValue``'granted'``consentPollInterval``500`Milliseconds, cookie mode only`consentTimeout``0`Seconds; 0 watches for the life of the page`honourDnt``true``honourGpc``true``autoAttributes`site, section, entry type, environment`customAttributes``[]``['name' => …, 'value' => …]`; `$ENV_VAR` supported`identifyUsers``false``hashUserIds``true``spaSupport``false`URI patterns are globs by default (`blog/*`), regular expressions when prefixed with `re:`, and `__home__` matches the homepage. An exclusion always beats an inclusion regardless of order, because these are read as "everywhere, except there".

---

Licence
-------

[](#licence)

The Craft License. See `LICENSE.md`. Jarhead is free: no editions, no licence key, and no licensing code in the plugin.

Jarhead is not affiliated with, endorsed by, or sponsored by Hotjar Ltd. "Hotjar" is their trademark; this plugin only installs the tracking code they publish.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance100

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community7

Small or concentrated contributor base

Maturity45

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/035cb655c55af0e9e5b96754b80fd9703e195c32dbdfc49ae9a43ab9cf8db560?d=identicon)[justinholtweb](/maintainers/justinholtweb)

---

Top Contributors

[![justinholtweb](https://avatars.githubusercontent.com/u/295903?v=4)](https://github.com/justinholtweb "justinholtweb (1 commits)")

---

Tags

trackinganalyticscraftcmscraft-plugingdprconsenthotjarsession-recordingheatmaps

### Embed Badge

![Health badge](/badges/justinholtweb-craft-jarhead/health.svg)

```
[![Health](https://phpackages.com/badges/justinholtweb-craft-jarhead/health.svg)](https://phpackages.com/packages/justinholtweb-craft-jarhead)
```

###  Alternatives

[verbb/formie

The most user-friendly forms plugin for Craft.

101400.6k79](/packages/verbb-formie)[verbb/hyper

A user-friendly links field for Craft.

24153.5k15](/packages/verbb-hyper)[verbb/vizy

A flexible visual editor field for Craft.

4251.5k1](/packages/verbb-vizy)[wrav/oembed

A simple plugin to extract media information from websites, like youtube videos, twitter statuses or blog articles.

36209.2k3](/packages/wrav-oembed)[verbb/events

A full-featured plugin for event management and ticketing.

2312.1k](/packages/verbb-events)[verbb/icon-picker

A slick field to pick icons from. Supports SVGs, Sprites, Webfonts, Font Awesome and more.

16172.7k7](/packages/verbb-icon-picker)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
