Hotjar Technical Implementation Guide | OpsBlu Docs

Hotjar Technical Implementation Guide

Install the Hotjar tracking script, configure the hj() API for custom events, session recordings, heatmaps, surveys, and the Identify API.

How Hotjar Works

Hotjar collects behavioral data through a single JavaScript agent that loads asynchronously into the page. The agent operates three independent subsystems:

Session Recording captures DOM state through an initial full snapshot followed by incremental mutation observations. Hotjar serializes the initial DOM into a virtual representation, then uses a MutationObserver to record every node insertion, removal, attribute change, and text modification. Mouse coordinates, scroll positions, viewport resizes, and input events are captured as timestamped event streams and merged with the DOM mutations during playback. Recordings are not video; they are structured event logs replayed against a reconstructed DOM.

Heatmaps aggregate interaction coordinates across all sessions matching a URL pattern. Hotjar produces three heatmap types:

  • Click heatmaps record mousedown/touchstart coordinates mapped to CSS selectors
  • Move heatmaps sample cursor position at intervals (desktop only) and render density maps
  • Scroll heatmaps track the maximum scroll depth per session, reporting the percentage of visitors reaching each vertical threshold

Surveys and Feedback inject overlay widgets into the DOM. Surveys are served from Hotjar's CDN based on targeting rules evaluated client-side (URL match, device type, scroll depth, time on page). Response data is sent to Hotjar's ingestion endpoint, not stored locally.

All data transmits to https://in.hotjar.com via XHR POST requests. The agent compresses payloads with a custom binary protocol before transmission. Hotjar assigns each visitor a _hjid cookie (365-day expiry) for cross-session identity.


Installing the Tracking Script

Add the Hotjar snippet inside <head>. Replace SITE_ID with your numeric site ID from Hotjar settings:

<script>
(function(h,o,t,j,a,r){
    h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
    h._hjSettings={hjid:SITE_ID,hjsv:6};
    a=o.getElementsByTagName('head')[0];
    r=o.createElement('script');r.async=1;
    r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
    a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
</script>

For Google Tag Manager deployment, create a Custom HTML tag with the same snippet and fire it on All Pages.

For single-page applications (React, Vue, Angular), Hotjar auto-detects URL changes via pushState/replaceState interception. If your router uses hash-based navigation, enable hash tracking:

hj('stateChange', window.location.pathname + window.location.hash);

Call this after each route transition if automatic detection fails.

Content Security Policy headers must allow:

script-src: https://static.hotjar.com https://script.hotjar.com
connect-src: https://*.hotjar.com https://*.hotjar.io wss://*.hotjar.com
frame-src: https://vars.hotjar.com
img-src: https://static.hotjar.com
style-src: 'unsafe-inline'

Event Tracking and Data Collection

Hotjar exposes the hj() global function for programmatic interaction. All calls are queued if the script has not loaded yet.

Custom Events

Trigger events that appear as tags on session recordings and can be used as heatmap/survey filters:

// Fire a custom event
hj('event', 'checkout_started');

// Events with context (the event name is the only parameter)
hj('event', 'plan_upgraded');
hj('event', 'form_error_email');

Events are string identifiers only. Hotjar does not support event properties or key-value payloads on custom events. Use events to mark moments in the session timeline for filtering recordings.

Heatmap Triggers

Heatmaps activate automatically on URLs matching your configured patterns. For dynamically rendered content, trigger a manual heatmap capture after the DOM is stable:

// Force heatmap snapshot on the current page state
hj('trigger', 'my_heatmap_name');

Survey Triggers

Programmatically display a survey by its trigger name:

// Show a survey configured with trigger name "post_purchase"
hj('trigger', 'post_purchase');

Recording Tags

Tag the current session recording with a label for easier filtering in the Hotjar dashboard:

hj('tagRecording', ['premium_user', 'onboarding_flow']);

Tags are arrays of strings. You can apply multiple tags to a single recording.

Form Analysis

Hotjar automatically tracks form interactions (focus, fill time, drop-off) on any <form> element. No additional configuration is required. To exclude a form from tracking:

<form data-hj-suppress>
  <!-- This form will not be tracked -->
</form>

Identity and User Tracking

The Hotjar Identify API associates session data with known user attributes. Call hj('identify') after authentication:

hj('identify', userId, {
  email: 'user@example.com',
  plan: 'enterprise',
  signup_date: '2025-03-15',
  company_size: 250
});

Parameters:

  • userId (string|null): Your internal user ID. Pass null to set attributes without a user ID.
  • Attributes object: Key-value pairs. Keys must be strings. Values can be strings, numbers, booleans, or dates (ISO 8601 strings).

Attribute limits:

  • Maximum 20 custom attributes per identify call
  • Key names: 255 characters max, alphanumeric and underscores only
  • String values: 255 characters max

The Identify API links the _hjid cookie to your user ID, enabling cross-session user lookup in the Hotjar dashboard. Calling identify multiple times in the same session updates attributes; it does not create duplicate records.

For anonymous users, you can still set attributes without an ID:

hj('identify', null, {
  ab_test_variant: 'B',
  referral_source: 'google'
});

API and Data Export

Hotjar does not expose a public REST API for querying raw session data or heatmap results. Data access works through:

Dashboard exports: Heatmap screenshots, survey responses, and recording lists can be exported as CSV or PNG from the Hotjar web interface.

Integrations: Hotjar pushes data to connected platforms:

Hotjar → Slack (survey responses, feedback notifications)
Hotjar → HubSpot (user attributes, survey responses as contact properties)
Hotjar → Zapier (webhook triggers on new survey response, new feedback)
Hotjar → Segment (Hotjar as a Segment destination for identify calls)

Webhooks via Zapier: Create automated workflows triggered by Hotjar events:

// Example Zapier webhook payload for a new survey response
{
  "survey_id": 123456,
  "response_id": "abc-def",
  "answers": [
    {"question": "How likely are you to recommend us?", "answer": "9"}
  ],
  "user": {
    "user_id": "usr_12345",
    "device": "desktop",
    "country": "US"
  },
  "timestamp": "2025-11-20T14:30:00Z"
}

Google Tag Manager integration: Hotjar fires a hjDoneWithSurvey event on the dataLayer when a user completes a survey, which you can capture with a GTM trigger.


Common Issues

Recordings show blank or partially rendered pages: The Hotjar script loaded before the DOM was ready, or critical CSS is loaded from a domain not allowlisted in your CSP. Check that all asset domains are permitted in style-src and font-src.

Heatmap data does not match the current page layout: Heatmaps are tied to a DOM snapshot from when data was collected. If the page layout changed, historical heatmap overlays will misalign. Create a new heatmap after significant layout changes.

SPA route changes not detected: Verify that your router calls history.pushState(). Hotjar intercepts pushState and replaceState. If you use a custom routing mechanism, call hj('stateChange', newPath) manually after each navigation.

Session recordings missing for some users: Check sampling rate configuration in Hotjar settings. On free and lower tiers, Hotjar caps daily recording sessions. Once the cap is hit, no additional sessions are recorded until the next day (UTC midnight reset).

Identify API attributes not appearing: Attribute keys containing spaces, special characters, or exceeding 255 characters are silently dropped. Verify key names are alphanumeric with underscores only. Check the browser console for [Hotjar] warnings.

Survey not displaying: Surveys require the trigger to fire on a page URL that matches the survey's URL targeting rule. Both conditions (trigger + URL match) must be satisfied. Test with the Hotjar browser extension in debug mode.


Platform-Specific Considerations

Sampling and session caps: Hotjar enforces daily session recording limits by plan tier. On high-traffic sites, this means a small percentage of total sessions are actually recorded. Hotjar does not support configurable sampling rates; the cap is hard-limited. Plan accordingly when using recordings for quantitative analysis.

Privacy and data residency: Hotjar processes data in EU data centers (Ireland). The _hjid cookie is a first-party cookie set on your domain. Hotjar supports cookie consent banners; suppress all tracking until consent is granted:

// Do not install the Hotjar snippet until consent is given
// Then dynamically inject it:
function loadHotjar() {
  (function(h,o,t,j,a,r){
    h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
    h._hjSettings={hjid:SITE_ID,hjsv:6};
    a=o.getElementsByTagName('head')[0];
    r=o.createElement('script');r.async=1;
    r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
    a.appendChild(r);
  })(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
}

Sensitive data suppression: Hotjar automatically masks all input fields with type="password". For additional suppression, use the data-hj-suppress attribute on any element:

<div data-hj-suppress>
  <!-- All text content and child inputs in this container are masked in recordings -->
</div>

Alternatively, suppress all text on the page by default and explicitly allow safe elements with data-hj-allow.

Recording storage: Hotjar stores recordings for 365 days on paid plans. Recordings cannot be downloaded as video files; they exist only as replayable event streams within the Hotjar dashboard.

Performance impact: The Hotjar agent adds approximately 40-80KB (compressed) to initial page load. It executes asynchronously and should not block rendering. The MutationObserver overhead is negligible on pages with moderate DOM complexity but can become measurable on pages with thousands of concurrent DOM mutations (virtualized lists, real-time data tables).