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

<script src="/static/churn-warn.umd.min.js"></script>
<script>
ChurnWarn.configure({
  apiKey: "<your-bearer-token>",
  serverUrl: "https://your-gateway.example.com", // omit if same-origin
  tenantId: "00000000-0000-0000-0000-000000000000", // optional
  autoCapture: true
});

// After login, once you know the account id:
ChurnWarn.identify("account-external-id-123");
</script>

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:

EventWhen
page_viewedFirst identify + every URL change (includes a normalized route)
session_started / session_endedNew session / tab hidden or unload
session_heartbeatEvery 60s while the tab is visible and recently active
element_clickedClicks on buttons, links, inputs — name in payload.label
feature_usedAny click inside a data-feature="…" section
rage_click3+ clicks on one element within 2s
form_abandonedInput typed into but never submitted
scroll_depth25 / 50 / 75 / 100% milestones
js_errorUncaught 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.

Integrate the browser SDK
Add the ChurnWarn browser SDK to this web app.

How it works: load churn-warn.umd.min.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: "<bearer-token>", serverUrl: "<gateway-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.

Set the Bearer token and your page origin must be allowed by the API's CORS config. See Auth & CORS notes.

Capture the default-dashboard signals
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:

  • url and referrer include only origin + pathname — query strings and hash fragments are never sent.
  • element_clicked does not capture visible innerText. Use data-churn-warn-label="submit_checkout" for stable names.
  • Mark sensitive UI with data-churn-warn-ignore to exclude it entirely.
  • Derived click labels (from data-testid, id, aria-label, name) are masked for emails, tokens, phone numbers, and similar patterns.
On by default
Every SDK ships with payload redaction enabled(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

PatternBecomes
Email addressesj***e@example.com (first/last char kept)
Credit-card numbers****-****-****-1234 (last 4 kept)
Phone numbers415****6789
US SSN-like values***-**-****
JWTs (eyJ…header.payload.sig)eyJhbGciOi...***
API keys / bearer tokens (key: value)abcd*** (first 4 kept)
Passwords embedded in URLsuser:********@host
IBANsDE89***6789

Keys always replaced with ***

passwordsecrettokenapi_keyauthorizationcookiessncredit_card

Any 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.

Practices that matter more than masking
  • Send path or route instead 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

  • The route is POST /api/events and requires Authorization: Bearer <token>. Your page origin must be allowed by the API's CORS config.
  • Omit tenantId to resolve the tenant from the token's tenant_id claim; pass it to override.
  • serverUrl omitted ⇒ requests go same-origin (window.location.origin).