Node.js SDK

Background batched capture for Node services. Zero dependencies.

Zero runtime dependencies, uses global fetch (Node 18+). Background capture: initializecaptureEvent (queued) → periodic / size-based flush to POST /api/events/batch. Call shutdown() to drain and stop.

Install

"dependencies": {
  "churn-warn-node-sdk": "file:../sdks/churn_warn_node_sdk"
}

Usage

import { initialize, captureEvent, shutdown, Metrics, RawEvents } from "churn-warn-node-sdk";

initialize({
baseUrl: "https://your-gateway.example.com",
apiToken: process.env.CHURNWARN_TOKEN, // or apiKey for X-Api-Key
defaultTenantId: null,
defaultSource: "node_sdk",
batchSize: 50,
flushIntervalMs: 5000,
maxQueueSize: 10000,
onSendError: (e) => console.error("send failed", e),
});

captureEvent("acct-1", Metrics.LOGIN, { payload: { path: "/" } });
captureEvent({ externalAccountId: "acct-2", eventType: RawEvents.APP_LOGIN, payload: { x: 1 } });

await shutdown();

Auth

  • apiKeyX-Api-Key (preferred on servers).
  • apiTokenAuthorization: Bearer … (a leading Bearer is stripped).

Options

OptionDefaultNotes
batchSize / flushIntervalMs50 / 5000Flush triggers; ≤ 500 events per request.
maxQueueSize10000When full, the event is dropped and onSendError fires.
redactPayloadtrueMask sensitive patterns before enqueue.
onBeforeEnqueueHook to transform a record before it's queued.
fetchglobalOverride for tests or polyfills.

Batching & tenant rules

  • Each flush sends up to 500 events; larger slices are split automatically.
  • If any event sets tenantId, every non-empty tenantId in that flush must be identical. It's sent once as the batch-level tenantId, or combined with defaultTenantId when all per-event values are omitted.

Errors

Non-success HTTP responses surface through onSendError as ChurnWarnApiError (statusCode, message, body). A 202 batch response can still carry per-row error strings when using the lower-level API.

Prompt starters

Hand these to an AI coding assistant (Claude Code, Cursor, Copilot Chat) to wire the SDK into your service. Edit the placeholders first, then paste.

Integrate the Node SDK
Add the churn-warn-node-sdk to this Node service.

How it works: call initialize({ ... }) once at boot, then captureEvent(externalAccountId,
eventType, { payload }) from anywhere. It is fire-and-forget, queued, and flushed in batches
to POST /api/events/batch. Zero dependencies; needs Node 18+ (global fetch).

Please:
1. initialize() once at startup. Read config from the environment: apiKey = CHURNWARN_API_KEY
 (X-Api-Key) or apiToken = CHURNWARN_TOKEN (Bearer), baseUrl = the gateway URL,
 defaultSource = "<this-service-name>".
2. Wire onSendError to our logger so dropped/failed sends are observable.
3. Call await shutdown() in the process's graceful-shutdown path (SIGTERM/SIGINT) so the
 queue drains before exit.
4. Use the account id from our own database as externalAccountId (the tenant/org id, NOT the
 per-user id), and keep payloads to a few small fields.
5. Import and use the Metrics and RawEvents constants instead of raw strings.

Leave redactPayload at its default. Do not await captureEvent or try/catch it — it never throws.

Pick one value for externalAccountId and use it everywhere — changing the id later splits an account's history in two.

Capture the default-dashboard signals
Instrument the business signals that feed ChurnWarn's default Product Health dashboard by
adding captureEvent(externalAccountId, <metric>, { payload: <object> }) calls in this codebase.
The browser SDK already auto-captures usage, sessions, clicks, frustration and JS errors —
do NOT re-send those from the server. Add only the signals below, each at the call site
described:

- Metrics.LOGIN — in the authentication success path, right after the session/token is issued
(the same place you already record a successful sign-in). payload: { external_user_id: the
signed-in user's id }.
- Metrics.ACTIVE_USER — once per user per day. Add it to auth middleware and guard it with a
cheap per-user-per-day check (cache/Redis/DB flag) so it fires at most once a day per user.
payload: { external_user_id }. Drives the distinct-active-users count.
- Metrics.ONBOARDING_COMPLETED — where the onboarding/setup flow marks itself done (the final
step handler, or where you flip an "onboarded" flag). Fire once per account.
- Metrics.FEATURE_USED — in the handler of a high-value action (report generated, export run,
integration connected), not on every request. payload: { feature: "<stable-short-name>" }.
- Metrics.SESSION — where a session ends / on logout, if you track session length. Skip it
entirely if you have no session concept. payload: { session_minutes: <number> }.
- Metrics.SEAT_USED and Metrics.SEAT_PURCHASED — in your billing/subscription code where seat
counts can change (a subscription-updated webhook, a member invited/removed, a plan change).
Emit BOTH with current counts. payload: { count: <int> } on each (utilization = used ÷ purchased).
- Metrics.SEAT_EXPANDED / Metrics.PLAN_UPGRADED — in that same billing path, when seats
increase or the plan moves to a higher tier.
- Metrics.REFERRAL — where a referral is recorded as successful (accepted/converted) in your
invite/referral flow.
- Metrics.NPS_RESPONSE / Metrics.CSAT_RESPONSE — in your survey-submission handler or survey
webhook. payload: { score: <0-10 for NPS / 1-5 for CSAT> }.
- Metrics.SUPPORT_TICKET_OPENED / RESOLVED / NEGATIVE — in your ticket lifecycle / support
webhook, ONLY if you do NOT use a Zendesk/Intercom/Freshdesk integration (those map
automatically). payload on resolved: { resolution_hours: <number> }.

Always pass our own DB account id (the tenant/organization id) as externalAccountId.

Make these changes non-breaking — they must be pure additive telemetry:
- Add each call AFTER the existing operation has fully succeeded (after the DB commit / after
the response is decided). Never change control flow, return values, function signatures, or
business logic.
- captureEvent is fire-and-forget and never throws — do NOT await it, do NOT wrap surrounding
code in new try/catch, and do NOT let it block the request path.
- Don't add new blocking I/O; the SDK buffers and sends in the background. Reuse the existing
initialize() — do not re-initialize.
- If you can't confidently find the correct call site for a signal, SKIP it and leave a
// TODO: ChurnWarn <metric> here comment instead of guessing. Omitting a signal is fine;
firing it from the wrong place is not.
- Keep each edit small and reviewable, and keep payloads to the documented fields only.

Treat the agent's output as a draft: review every inserted call site to confirm it fires only after success and with the right account id, run your tests (especially auth, billing and onboarding), and ship to a dev/staging tenant first — watch the events land on the account's score breakdown before going live. Contract value, days-to-renewal and tenure read the account's MRR, renewal date and created date — set those on the account record, not as events. See the default components reference.

Privacy

Exported helpers: maskSensitiveText, redactPayload, redactPayloadJson, safeUrlString.

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.