@clickroom/sdk-js
v0.9.0
Published
Browser SDK for Clickroom — product analytics, feature flags and experiments.
Maintainers
Readme
@clickroom/sdk-js
Browser SDK for clickroom — autocapture, batched event transport, and feature flags.
Ships two ways: a CDN <script> snippet for plain HTML pages and an npm ESM
package for bundled apps.
Install
Option A — CDN snippet (recommended for websites)
Paste this into your page <head>. It installs a queued stub immediately, loads the
real bundle asynchronously, and replays any calls made before load:
<script>
"use strict";(()=>{(function(i,s){var p="http://localhost:3001",_="/static/clickroom.js",l=["init","capture","identify","reset","isFeatureEnabled","getFeatureFlag","onFeatureFlags"];if(i.clickroom&&i.clickroom.__loaded__)return;var t=i.clickroom||{};t._q=t._q||[],t.__stub__=!0;function f(r){return function(){var e=Array.prototype.slice.call(arguments);t._q.push([r,e]),r==="init"&&e[1]&&e[1].api_host&&(p=e[1].api_host)}}var o;for(o=0;o<l.length;o++)t[l[o]]=f(l[o]);i.clickroom=t;function h(){var r=i.clickroom,e,n,u;if(!(!r||r.__stub__))for(e=0;e<t._q.length;e++)n=t._q[e][0],u=t._q[e][1],typeof r[n]=="function"&&r[n].apply(r,u)}var a=s.createElement("script");a.type="text/javascript",a.async=!0,a.src=p.replace(/\/$/,"")+_,a.onload=h;var c=s.getElementsByTagName("script")[0];c&&c.parentNode?c.parentNode.insertBefore(a,c):s.head.appendChild(a)})(window,document);})();
clickroom.init('ckw_live_xxx', { api_host: 'https://your-host', autocapture: true })
</script>The readable source of this snippet is loader.js.
Option B — npm
pnpm add @clickroom/sdk-jsimport clickroom, { init, capture } from '@clickroom/sdk-js'
init('ckw_live_xxx', { api_host: 'https://your-host', autocapture: true })
capture('signup_started', { plan: 'pro' })
// or via the default singleton:
clickroom.capture('signup_completed')Quickstart
import clickroom from '@clickroom/sdk-js'
clickroom.init('ckw_live_xxx', {
api_host: 'https://your-host',
autocapture: true, // auto-track clicks, submits, and SPA pageviews
capture_pageview: true,
})Events are batched (flushed by size/interval) and POSTed to {api_host}/v1/capture as
{ batch: EventPayload[] }. A beforeunload beacon flushes the remaining queue.
Error handling
If the server rejects a batch outright (HTTP 4xx — e.g. a schema-invalid event under strict
validation), the SDK does not retry: retrying a request the server will never accept would
just waste the retry budget and delay delivery of everything behind it. The batch is dropped and
the failure is surfaced via console.warn, including the response body (e.g. { accepted, errors
}) when the server returns one, so you can see which event/property failed validation. Network
errors and 5xx responses are unaffected — the SDK retries those with its existing bounded
exponential backoff.
Public API
| Method | Signature | Description |
| --- | --- | --- |
| init | init(writeKey, options?) | Initialise the SDK. options: api_host, autocapture, capture_pageview. |
| capture | capture(event, properties?) | Enqueue a custom event. |
| identify | identify(distinctId, properties?) | Associate the current session with a user id. |
| reset | reset() | Clear the current identity / anonymous id. |
| isFeatureEnabled | isFeatureEnabled(key): boolean | Whether a boolean flag is on. |
| getFeatureFlag | getFeatureFlag(key): string \| boolean \| undefined | Flag value (multivariate). |
| onFeatureFlags | onFeatureFlags(cb) | Subscribe to flag updates. |
All of the above are available both as named exports and as methods on the default
clickroom singleton (and on window.clickroom when loaded via the CDN bundle).
Feature flags
Flag methods (isFeatureEnabled / getFeatureFlag / onFeatureFlags) are fully
implemented: flags are fetched from {api_host}/v1/flags (with a cheap {api_host}/v1/flags/version
check ahead of it to skip the full fetch when nothing changed), evaluated, cached in
localStorage, and re-fetched on identify() or via the optional
feature_flag_poll_interval.
Support widget
Opt-in (support is off by default — an existing embed fetches zero extra bytes). When enabled,
init() fetches + caches the form config from GET /v1/support, gates each form on its linked
flag, and builds the support client. There are three shapes, and they differ only in where the
UI mounts — identity, event context, form analytics and realtime replies are wired identically in
every mode. The tracking story does not change between the FAB and an inline page.
1. Default FAB — unchanged. support: true behaves exactly as before: it auto-mounts the
floating launcher. This is byte-identical to prior releases; nothing about support: true changed.
clickroom.init('WRITE_KEY', { support: true })
// normalises to { enabled: true, mode: 'launcher' }2. Page-only (mode: 'manual'). Build the client but mount nothing — you place the support
surface yourself as a full page. Nothing auto-mounts; no FAB.
clickroom.init('WRITE_KEY', { support: { enabled: true, mode: 'manual' } })React — extend the existing <ClickroomSupport> with mode="inline":
import { ClickroomSupport } from '@clickroom/sdk-js/react'
// e.g. a /support route
<ClickroomSupport mode="inline" />Vanilla (Vue / Svelte / Angular / plain JS) — clickroom.support.mount() returns the same
SupportRenderHandle (open() / close() / refresh() / destroy()). Call destroy() on route
leave so a route change doesn't leak the host:
const handle = clickroom.support.mount({
el: '#support', // HTMLElement | selector string
mode: 'inline', // 'inline' (default) fills the container | 'launcher'
view: 'home', // 'home' (default) | 'search' | 'bot' | 'new' | 'tickets' | 'thread'
home: 'search', // 'search' (default) | 'conversations' — the home surface's posture
// formId, ticketId, onTicketOpen, onTicketCreated optional
})
// on unmount:
handle?.destroy()view picks the opening surface: 'home' is the search-first landing, 'search' the docs search,
'bot' the AI assistant, 'new' a fresh conversation, 'tickets' the conversation list, and
'thread' a single thread (implied when you pass ticketId without a view). home only affects
the 'home' surface, and falls back to 'conversations' automatically when the form has no docs
source connected. The same view / home props exist on <ClickroomSupport mode="inline" />.
clickroom.support.open() / .close() drive the support surface from a customer's own "Help" nav
item — the one you mounted via mount() if there is one, otherwise the auto-mounted FAB. Both are
no-ops when support is off or nothing is mounted.
3. Both (mode: 'both'). Auto-mount the FAB and allow manual mounts — a FAB on marketing
pages plus a full page at /support.
clickroom.init('WRITE_KEY', { support: { enabled: true, mode: 'both' } })mode gates exactly one thing — whether the FAB auto-mounts. 'launcher' (default) and 'both'
auto-mount it; 'manual' does not. Config fetch, flag gating, the ticket-token store, the
recentEvents context ring, the realtime subscription and every $support_* event are the same
across all three.
Build
pnpm --filter @clickroom/sdk-js buildOutputs to dist/:
clickroom.js— minified IIFE CDN bundle (entrysrc/global.ts, exposeswindow.clickroom)index.js— ESM entry for npm consumers (entrysrc/index.ts)index.d.ts— type declarations (tsc --emitDeclarationOnly)*.js.map— source maps
Build pipeline: esbuild.config.mjs then tsc for types.
Demo
demo/index.html is a manual / Playwright test page. Build first,
then open it (or serve the package dir) and watch batched 202s in the Network tab.
