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

CaptureEvent 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

PropertyDefaultDescription
BaseUrlrequiredGateway root URL.
ApiKeyX-Api-Key header (preferred for servers).
ApiTokenBearer JWT when ApiKey is not set.
DefaultTenantIdApplied when events omit TenantId.
DefaultSourcedotnet_sdkEvent source field.
BatchSize50Max events per flush (≤ 500).
FlushInterval5sMax wait before sending a non-empty queue.
MaxQueueSize100000 = unbounded.
RedactPayloadtrueMask sensitive patterns before enqueue.
OnBeforeEnqueueHook 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.

Integrate the .NET SDK
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.

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

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.