Python SDK
Background capture plus an optional async direct client.
Background capture API (initialize / capture_event / shutdown) plus an
optional async ChurnWarnClient for direct posts. Requires Python 3.9+
and httpx.
Install
cd sdks/churn_warn_python_sdk
pip install -e .
Capture API (recommended)
Call initialize once, then capture_event from anywhere; events are queued and
sent on a background thread to POST /api/events/batch.
from churn_warn_sdk import (
ChurnWarnOptions, Metrics, RawEvents, RecordEventInput,
capture_event, initialize, shutdown,
)
initialize(ChurnWarnOptions(
base_url="https://your-gateway.example.com",
api_token="your-jwt", # or api_key="..." for X-Api-Key
default_source="python_sdk",
batch_size=50,
flush_interval_seconds=5.0,
max_queue_size=10_000,
on_send_error=lambda e: print("send failed", e),
))
capture_event("acct-1", Metrics.LOGIN, payload={"path": "/"})
capture_event(RecordEventInput(
external_account_id="acct-2", event_type=RawEvents.APP_LOGIN, payload={"x": 1},
))
shutdown()Auth
api_key→X-Api-Keyheader.api_token→Authorization: Bearer …(aBearerprefix is stripped).
Options
| Option | Notes |
|---|---|
batch_size / flush_interval_seconds | Flush triggers, capped at 500 events per request. |
max_queue_size | When full, the event is dropped and on_send_error is invoked. |
redact_payload | Default True — masks sensitive patterns before enqueue. |
on_before_enqueue | Optional hook to transform events before queueing. |
Batching & tenant rules
At most 500 events (MAX_EVENTS_PER_BATCH) per request; larger lists split
automatically in the async client. If any event sets tenant_id, every non-null
tenant_id in the batch must match — it's sent once as the batch-level
tenantId.
Errors
- Capture path: failures go to
on_send_error; the sender raisesChurnWarnApiErroron non-success HTTP. - Async client: non-success HTTP raises
ChurnWarnApiError(status_code,body).
Prompt starters
Hand these to an AI coding assistant (Claude Code, Cursor, Copilot Chat) to wire the SDK into your app. Edit the placeholders first, then paste.
Add the churn_warn_sdk to this Python app (Django / Flask / FastAPI / worker).
How it works: call initialize(ChurnWarnOptions(...)) once at startup, then
capture_event(external_account_id, event_type, payload={...}) from anywhere. It queues and
flushes on a background thread to POST /api/events/batch. Requires Python 3.9+ and httpx.
Please:
1. initialize() once at startup. Read config from the environment: api_key = CHURNWARN_API_KEY
(X-Api-Key) or api_token = CHURNWARN_TOKEN (Bearer), base_url = the gateway URL,
default_source = "<this-service-name>".
2. Wire on_send_error to our logger so dropped/failed sends are observable.
3. Call shutdown() in the app's graceful-shutdown path so the queue drains before exit.
4. Use the account id from our own database as external_account_id (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 redact_payload at its default (True). capture_event does not block — don't try/except it.Pick one value for external_account_id and use it everywhere — changing the
id later splits an account's history in two.
Instrument the business signals that feed ChurnWarn's default Product Health dashboard by
adding capture_event(external_account_id, <metric>, payload=<dict>) 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 external_account_id.
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.
- capture_event is fire-and-forget and does not block — do NOT wrap surrounding code in new
try/except for it, and do NOT let it block the request path.
- Don't add new blocking I/O; the SDK buffers and sends on a background thread. 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
Helpers: redact_payload, safe_url_string, mask_sensitive_text.
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.