Browser JavaScript SDK
Dependency-free browser capture with optional auto-capture.
A small, dependency-free client for sending raw product events from the
browser to POST /api/events. Ships as a single ES5 IIFE exposing the global
ChurnWarn — drop it in with a <script> tag, no build step required.
Install
Settings → API keys generates this snippet with your own key, tenant and URL already filled in — copy it from there rather than editing the example below.
<script src="https://your-dashboard.example.com/sdk/v0.7/churn-warn.js"></script>
<script>
ChurnWarn.configure({
apiKey: "cw_pub_...", // website key, safe to publish
tenantId: "00000000-0000-0000-0000-000000000000",
serverUrl: "https://your-dashboard.example.com", // omit if same-origin
autoCapture: true
});
// After login, once you know the account id:
ChurnWarn.identify("account-external-id-123");
</script>The SDK is served version-pinned (/sdk/v0.7/…) and cached immutably, so a
snippet you pasted once keeps resolving to the build it was written against.
/sdk/latest/churn-warn.js always points at the newest release.
Call configure and identify as early as possible so the first session and
page view are not missed.
Sending events manually
captureEvent(externalAccountId, eventName, extraData?) is fire-and-forget: it
uses fetch, returns immediately, and never throws.
ChurnWarn.captureEvent("acct-1", ChurnWarn.Metrics.FEATURE_USED, { feature: "export_csv" });
ChurnWarn.captureEvent("acct-1", ChurnWarn.RawEvents.APP_LOGIN);Auto-capture
With autoCapture enabled, the SDK emits health-oriented events automatically
after identify() — no manual instrumentation needed:
| Event | When |
|---|---|
page_viewed | First identify + every URL change (includes a normalized route) |
session_started / session_ended | New session / tab hidden or unload |
session_heartbeat | Every 60s while the tab is visible and recently active |
element_clicked | Clicks on buttons, links, inputs — name in payload.label |
feature_used | Any click inside a data-feature="…" section |
rage_click | 3+ clicks on one element within 2s |
form_abandoned | Input typed into but never submitted |
scroll_depth | 25 / 50 / 75 / 100% milestones |
js_error | Uncaught errors and promise rejections |
Route normalization
page_viewed includes a route with high-cardinality id segments collapsed to
:id (/orders/9281 → /orders/:id). Use it in event-map conditions so a
single raw type fans out to several metrics:
page_viewed → feature_used condition: {"payload.route":"/billing*"}
page_viewed → plan_upgraded condition: {"payload.route":"/checkout/success"}
Prompt starters
Hand these to an AI coding assistant (Claude Code, Cursor, Copilot Chat) to wire the SDK into your front-end. Edit the placeholders first, then paste.
Add the ChurnWarn browser SDK to this web app.
How it works: load the hosted churn-warn.js, call ChurnWarn.configure({ ... }) once as early as
possible, and ChurnWarn.identify("<account-id>") right after login. With autoCapture on it
then emits page views, sessions, clicks, feature usage, rage clicks, form abandonment and JS
errors on its own. Manual events use ChurnWarn.captureEvent(accountId, name, extraData?) —
fire-and-forget, never throws.
Please:
1. Load the script and call configure({ apiKey: "<website-key from Settings -> API keys>",
tenantId: "<tenant-id>", serverUrl: "<dashboard-url or omit if same-origin>",
autoCapture: true }) as early in page load as possible.
2. Call identify("<account-id>") as soon as the account id is known after login, using the
account id from our own system (the org/tenant id, not the per-user id). Call it on every
page load for a logged-in user so the first session and page view aren't missed.
3. Tag the value-delivering UI sections with data-feature="<name>" so feature usage is
captured automatically, and put stable names on key buttons with
data-churn-warn-label="<name>".
4. Mark any sensitive UI with data-churn-warn-ignore.
Don't manually send page_viewed / session / click / error events — autoCapture covers those.Use a website key (cw_pub_…), never a server key — it ends up in
your page source. No CORS setup is needed. See
Auth & CORS notes.
Add the client-side business signals that feed ChurnWarn's default Product Health dashboard
but are NOT auto-captured, using ChurnWarn.captureEvent("<account-id>", name, extraData?).
autoCapture already handles page views, sessions, clicks, feature usage, frustration and JS
errors — don't duplicate those. Add only the signals below, each in the handler described:
- ChurnWarn.Metrics.ONBOARDING_COMPLETED — in the final step of your onboarding/setup flow
(the "Finish"/"Done" handler, or the effect that marks onboarding complete). Fire once.
- ChurnWarn.Metrics.PLAN_UPGRADED — on the upgrade-success / checkout-confirmation screen
(the component shown after a successful upgrade). Fire once.
- ChurnWarn.Metrics.NPS_RESPONSE — in your NPS survey widget's submit handler. { score: <0-10> }
- ChurnWarn.Metrics.CSAT_RESPONSE — in your CSAT survey widget's submit handler. { score: <1-5> }
- ChurnWarn.Metrics.REFERRAL — in the success handler of your referral/invite flow (a referral
sent or accepted).
- ChurnWarn.Metrics.FEATURE_USED — only for a feature that is NOT inside a data-feature
section; call it in that feature's own click/use handler. { feature: "<name>" }
Use the same account id you pass to ChurnWarn.identify().
Make these changes non-breaking — they must be pure additive telemetry:
- Add captureEvent only inside existing handlers, in the success branch (inside the .then /
after the await resolves), as a side-effect. Never change component logic, state, props, or
what a handler returns.
- captureEvent never throws and is non-blocking — do NOT await it and do NOT wrap surrounding
UI code in new try/catch for it.
- Guard one-time events (onboarding, upgrade) so React re-renders / StrictMode don't
double-fire them (use a ref, or an effect with correct dependencies).
- Don't re-send anything autoCapture already covers, and don't re-call configure or identify.
- If you can't confidently find the right handler for a signal, SKIP it and leave a
// TODO: ChurnWarn <metric> here comment instead of guessing.Treat the agent's output as a draft: review each call site to confirm it fires only on success and isn't double-firing, and test the onboarding, checkout and survey flows before shipping. Server-owned signals — seats used/purchased, support tickets, MRR and renewal date — should come from a backend SDK or integration, not the browser. See the default components reference.
Privacy
Auto-capture is built to avoid leaking sensitive data:
urlandreferrerinclude onlyorigin + pathname— query strings and hash fragments are never sent.element_clickeddoes not capture visibleinnerText. Usedata-churn-warn-label="submit_checkout"for stable names.- Mark sensitive UI with
data-churn-warn-ignoreto exclude it entirely. - Derived click labels (from
data-testid,id,aria-label,name) are masked for emails, tokens, phone numbers, and similar patterns.
redact_payload / redactPayload). Sensitive values are masked in-process before an event is queued, so unmasked data never leaves your application. Masking is bounded, never throws, and falls back to a safe *** on any error.Values detected and masked inside strings
| Pattern | Becomes |
|---|---|
| Email addresses | j***e@example.com (first/last char kept) |
| Credit-card numbers | ****-****-****-1234 (last 4 kept) |
| Phone numbers | 415****6789 |
| US SSN-like values | ***-**-**** |
| JWTs (eyJ…header.payload.sig) | eyJhbGciOi...*** |
| API keys / bearer tokens (key: value) | abcd*** (first 4 kept) |
| Passwords embedded in URLs | user:********@host |
| IBANs | DE89***6789 |
Keys always replaced with ***
passwordsecrettokenapi_keyauthorizationcookiessncredit_cardAny key containing one of these words has its value redacted. Keys named url / referrer (or ending in url) keep only their path — query strings and hash fragments are stripped.
- Send
pathorrouteinstead of full URLs in server-side payloads. - Never use an email or username as
external_account_id— use a stable non-PII id. - Masking is a safety net, not a license to send secrets. Keep payloads minimal.
Auth & CORS notes
- Use a website key (
cw_pub_…) from Settings → API keys, sent asX-Api-Key. It is scoped toPOST /api/eventsandPUT /api/accounts, so a visitor who reads it out of your page source can send events and nothing else. A server key (cw_sec_…) in browser code would expose your whole project. - No CORS setup is needed. The ingest endpoints accept any origin and carry no credentials, so your site works from any domain out of the box. Every other endpoint stays on the dashboard's own allow-list.
- Omit
tenantIdto resolve the tenant from the key; pass it to override. serverUrlomitted ⇒ requests go same-origin (window.location.origin).