Docs

Web SDK

One snippet for any website or single-page app; an npm package for bundlers. Autocapture, SPA routes, errors, vitals, forms, replay, flags and surveys.

Install with the snippet

Put this in the <head> of every page. The script reads data-key and data-host from its own tag, replays every call queued on the stub, and replaces window.clickiq with the real dispatcher. The core is about 21 KB minified, about 8.2 KB gzipped. Optional attributes: data-debug (log every event) and data-autocapture="false".

<script>
  !function(w,d,k,h){if(w.clickiq)return;var q=[],t=w.clickiq=function(){q.push(arguments)};t.q=q;
  var s=d.createElement("script");s.async=1;s.src=h+"/sdk/clickiq.js";s.setAttribute("data-key",k);
  s.setAttribute("data-host",h);d.head.appendChild(s)}(window,document,"ciq_YOUR_KEY","https://clickiq.example.com");
</script>

Install with npm

import { init, track, identify, page, reset, consent, set } from "@insight/clickiq";

init({ key: "ciq_…", host: "https://clickiq.example.com" });   // a no-op during SSR
track("Signed up", { plan: "pro" });

init options: key, host, autocapture, replay (false = never record), forms (false = no form analytics), flushInterval (5000 ms), batchSize (20), sessionTimeoutMinutes (30), cookieDomain, recorderUrl, flags (false = no feature flags), surveys (false = no surveys), surveysUrl, debug. Calls made before init are queued.

Calls

clickiq("track", "Order completed", { revenue: 129, items: 2 });   // custom event; numeric revenue/value also fills `value`
clickiq("identify", "user-123", { plan: "pro", team: "Growth" });   // user_id on every later event; traits become user.<trait>
clickiq("page");                     // a manual pageview (route changes are automatic)
clickiq("reset");                    // sign-out: forget the user, new anonymous id, new session
clickiq("consent", false);           // stop, clear ids, queue and recording; remembered. true resumes
clickiq("set", { replay: false });   // runtime options; any other key becomes a property on every event
clickiq("survey", "<survey id>");   // show a live survey now (targeting skipped; consent still applies)

What is captured automatically

EventWhenNotable fields
session_startFirst event of a sessionprops.reason: first, timeout or campaign
pageviewLoad, pushState/replaceState, popstate, hashchangeOn SPA navigations the referrer is the previous in-app URL
page_leave, scrollNext route, tab hidden or page hideduration_ms, maximum scroll depth %
clickButtons, links, inputs, [role=button], labels, [data-track], or the outermost pointer-cursor ancestorel {tag, text, selector, id, classes, href}, x as a fraction of page width, y in px
rage_click3+ clicks within 700 ms and 30 pxOnce per burst
dead_clickA click on an interactive element followed by no DOM change, URL change or request for 1 sInputs, labels and new-tab links excluded
form_field, form_submit, form_abandonBlur or change of a field; submit; leaving a touched formTimings, counts, a length bucket. Never values
errorwindow.onerror, unhandled rejections, console.error (if enabled)Message, stack, source, line; at most 5 per message and 50 per page
vitalLCP, INP, CLS, FCP, TTFB, once per hard loadValue in ms (CLS unitless)

The el.selector is a CSS path of at most 5 levels, anchored at the nearest data-testid or stable id (generated-looking ids are skipped), so it survives most re-renders.

HTML attributes

AttributeEffect
data-clickiq-name="Hero CTA"Names an element (or its children, up to 3 levels) for heatmap zones and reports
data-privateElement text is sent as **** and masked in replay
data-clickiq-blockNo click, rage, dead or form events from inside; not recorded in replay
data-clickiq-form="checkout"Treat an element as a form for form analytics (SPAs without a <form>)
data-trackMake a non-interactive element count as clickable

Identity and sessions

The anonymous id (a_…) lives in localStorage and a first-party cookie (clickiq_aid, one year, SameSite=Lax). The session id (s_…) is shared by all tabs and ends after 30 minutes of inactivity; arriving with a different utm_campaign starts a new one.

Delivery

Events queue in memory (up to 1,000) and are posted to /api/ingest/batch every 5 s or at 20 events, as text/plain JSON (no CORS preflight) with keepalive. Network errors, 429 and 5xx are retried with exponential backoff (5 s to 60 s, 6 attempts); other 4xx are dropped. When the page is hidden the queue goes out with navigator.sendBeacon in chunks of at most 60 KB.

Remote configuration

At start the SDK reads the project’s settings from GET /api/ingest/config?wk= (4 s timeout). If that fails it runs with analytics on and replay off. Events captured before the config arrives are held, then filtered by it.

{"replay": {"enabled": true, "sample_rate": 1.0, "mask_all_inputs": true,
            "mask_text_selector": "[data-private]", "block_selector": "[data-clickiq-block]"},
 "autocapture": true, "capture_errors": true, "capture_vitals": true, "capture_console_errors": true,
 "respect_dnt": false, "heatmaps": true, "forms": true}

respect_dnt: true honours Do Not Track and Global Privacy Control.

Session replay

When replay is enabled and the session is sampled in (decided once per session), the core loads clickiq-recorder.js from the same host (rrweb, about 23 KB gzipped). It records with every input masked, your mask and block selectors applied, a full snapshot every 10 minutes, and posts chunks every 5 s or at about 512 KB (each under 2 MB) with a sequence number that survives reloads.

Feature flags

Flags are evaluated by the server (POST /api/flags/evaluate with the write key and the distinct id) on init and after identify (with its traits), cleared and fetched again on reset, and cached in localStorage per person, so the first read on the next page load is instant. The first time a flag is read in a session the SDK sends $flag_exposure {flag, variant}; experiment results are counted from it, so read the flag where the experience is decided.

// snippet: safe before the SDK has loaded
clickiq("onFlags", (flags) => {
  const f = clickiq("flags");              // { isEnabled, getVariant, getPayload, onFlags, all, reload }
  if (f.isEnabled("new-nav")) showNewNav();
  if (f.getVariant("checkout-redesign") === "new") renderOnePage(f.getPayload("checkout-redesign"));
});

// npm
import { isEnabled, getVariant, getPayload, onFlags, reloadFlags } from "@insight/clickiq";
onFlags(() => { if (getVariant("checkout-redesign") === "new") { /* … */ } });

isEnabled is true for an enabled boolean flag or any variant; getVariant answers the variant key, true/false for a boolean flag, and undefined for an unknown one. Targeting can use person traits, country, device type, browser, OS and language. From a server, call POST /api/flags/evaluate with the same distinct id to get the same answer.

Surveys

Surveys are built in the console and shown by the SDK with no extra code. After init the client asks GET /api/surveys/active for the live surveys this visitor qualifies for (the server applies trait, country and device filters), then applies the rest in the page: path globs (/checkout*, re-checked on every route change), a per-visitor sample drawn once, once per person, after_event and a delay. One survey at a time; never while the visitor has opted out.

The widget (about 3 KB gzipped) loads lazily only when a survey is about to appear. It renders in a shadow root, is a labelled dialog with real buttons, radios and checkboxes, closes on Esc, and the corner variant never takes focus from what the visitor is typing. Replay records the widget; the open-text box carries data-private.

Eventprops
Survey shown{survey_id}
Survey responded{survey_id, question_id, question_type, answer}, one per question; a number for NPS and rating, a string for choice and text, a list for multi
Survey dismissed{survey_id, question_id}: the question they closed it on

Answers are ordinary events, so funnels, segments and widgets use them: promoters per week is a trend on Survey responded filtered to question_id = nps and answer ≥ 9.

Sampling at very high volume

A project’s Sessions kept setting (Volume & email card; sample_rate in /api/ingest/config) keeps or drops whole sessions: a session is kept when a hash of its id (FNV-1a) falls under the rate, with the same function in every SDK. A dropped session sends nothing, replay included; kept events carry sample_rate. Counts and sums in charts are weighted back up and labelled “Sampled at 25%”. Errors and conversions are not exempt, so rates stay unbiased; keep 100% if you must see every error.

Your own AI features

clickiq.ai.generation({ agent: "support-copilot", model: "claude-sonnet-4-6", input_tokens: 812,
                        output_tokens: 240, latency_ms: 1830, status: "ok", trace_id });
clickiq.ai.feedback({ agent: "support-copilot", trace_id, rating: "down" });   // up | down | 1-5

These send $ai_generation, $ai_tool_call and $ai_feedback events for the AI agents dashboard. Prompt and response text is dropped unless you pass capture_text: true and the project allows it. Server-side agents use the Python SDK.

Verify the install

Open the project in Sites & apps: the install page shows the snippet, the npm and HTTP variants and a live check (last event, events in the last 24 hours). Add data-debug to the script tag to log every event in the browser console. The Install health agent playbook checks for missing custom events, identify, vitals, forms and replay coverage.

Need a hand with the integration?

Send us your event list or your app's screen map and we will review it before you ship.

Talk to an engineer