.NET SDK
Fire-and-forget batched event capture for .NET 8+.
Fire-and-forget event capture for .NET. Call ChurnWarn.Initialize once, then
ChurnWarn.CaptureEvent from your app; events are queued and sent in the
background in configurable batches to POST /api/events/batch.
Requirements
- .NET 8.0+
- A Gateway API key (
X-Api-Key) or Bearer JWT
Install
dotnet add path/to/App.csproj reference path/to/churn_warn_dotnet_sdk/ChurnWarn.Sdk.csproj
Usage
using ChurnWarn.Sdk;
// Once at startup (e.g. Program.cs)
ChurnWarn.Initialize(new ChurnWarnOptions
{
BaseUrl = new Uri("https://your-gateway.example.com"),
ApiKey = Environment.GetEnvironmentVariable("CHURNWARN_API_KEY"),
DefaultSource = "my_app",
BatchSize = 50,
FlushInterval = TimeSpan.FromSeconds(5),
OnSendError = ex => Console.Error.WriteLine(ex),
});
// Anywhere — sync, non-blocking
ChurnWarn.CaptureEvent("user-42", RawEvents.AppLogin);
ChurnWarn.CaptureEvent("user-42", Metrics.FeatureUsed, payload: new { feature = "reports" });
ChurnWarn.Shutdown(); // optional: drain + stop on shutdownCaptureEvent does not throw on network failures — configure OnSendError to
observe background errors. If the queue exceeds MaxQueueSize, new events are
dropped and OnSendError is invoked.
Options
| Property | Default | Description |
|---|---|---|
BaseUrl | required | Gateway root URL. |
ApiKey | — | X-Api-Key header (preferred for servers). |
ApiToken | — | Bearer JWT when ApiKey is not set. |
DefaultTenantId | — | Applied when events omit TenantId. |
DefaultSource | dotnet_sdk | Event source field. |
BatchSize | 50 | Max events per flush (≤ 500). |
FlushInterval | 5s | Max wait before sending a non-empty queue. |
MaxQueueSize | 10000 | 0 = unbounded. |
RedactPayload | true | Mask sensitive patterns before enqueue. |
OnBeforeEnqueue | — | Hook to transform events before enqueue. |
Payload & idempotency
Use CaptureEvent(RecordEventInput) for full control. Payload is serialized to
JSON; PayloadJson overrides when you already have a JSON string (advanced —
redacted when parseable). Omit IdempotencyKey to generate one per event.
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 ChurnWarn .NET SDK to this .NET 8 / ASP.NET app.
How it works: call ChurnWarn.Initialize(new ChurnWarnOptions { ... }) exactly once at
startup, then ChurnWarn.CaptureEvent(externalAccountId, eventType, payload) from anywhere.
It is fire-and-forget, non-blocking, and batches to POST /api/events/batch.
Please:
1. Add a project reference to ChurnWarn.Sdk and Initialize once in Program.cs. Read config
from the environment: ApiKey = CHURNWARN_API_KEY, BaseUrl = the gateway URL,
DefaultSource = "<this-service-name>".
2. Wire OnSendError to ILogger so dropped/failed sends are observable.
3. Register ChurnWarn.Shutdown() on IHostApplicationLifetime.ApplicationStopping so the
queue drains on graceful shutdown.
4. Use the account id from our own database as externalAccountId (the tenant/organization
id, NOT the per-user id), and keep payloads to a few small fields.
5. Use the Metrics.* and RawEvents.* constants instead of raw strings.
Leave RedactPayload = true. Do not await CaptureEvent or wrap it in try/catch — it never throws.Pick one value for externalAccountId 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 ChurnWarn.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 }.
- "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.OnboardingCompleted — where the onboarding/setup flow marks itself done (the final
step handler, or where you flip an "onboarded" flag). Fire once per account.
- Metrics.FeatureUsed — 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> }.
- "seat_used" and "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 the current counts. payload: { count = <int> } on each (utilization = used ÷ purchased).
- Metrics.SeatExpanded / Metrics.PlanUpgraded — 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.NpsResponse / Metrics.CsatResponse — in your survey-submission handler or survey
webhook. payload: { score = <0-10 for NPS / 1-5 for CSAT> }.
- Metrics.SupportTicketOpened / 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, method 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
ChurnWarn.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
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.