- contact@insightclickiq.com
- Every click, every session, explained by AI
One snippet for any website or single-page app; an npm package for bundlers. Autocapture, SPA routes, errors, vitals, forms, replay, flags and surveys.
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>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.
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)| Event | When | Notable fields |
|---|---|---|
session_start | First event of a session | props.reason: first, timeout or campaign |
pageview | Load, pushState/replaceState, popstate, hashchange | On SPA navigations the referrer is the previous in-app URL |
page_leave, scroll | Next route, tab hidden or page hide | duration_ms, maximum scroll depth % |
click | Buttons, links, inputs, [role=button], labels, [data-track], or the outermost pointer-cursor ancestor | el {tag, text, selector, id, classes, href}, x as a fraction of page width, y in px |
rage_click | 3+ clicks within 700 ms and 30 px | Once per burst |
dead_click | A click on an interactive element followed by no DOM change, URL change or request for 1 s | Inputs, labels and new-tab links excluded |
form_field, form_submit, form_abandon | Blur or change of a field; submit; leaving a touched form | Timings, counts, a length bucket. Never values |
error | window.onerror, unhandled rejections, console.error (if enabled) | Message, stack, source, line; at most 5 per message and 50 per page |
vital | LCP, INP, CLS, FCP, TTFB, once per hard load | Value 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.
| Attribute | Effect |
|---|---|
data-clickiq-name="Hero CTA" | Names an element (or its children, up to 3 levels) for heatmap zones and reports |
data-private | Element text is sent as **** and masked in replay |
data-clickiq-block | No 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-track | Make a non-interactive element count as clickable |
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.
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.
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.
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.
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 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.
| Event | props |
|---|---|
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.
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.
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-5These 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.
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.