I just launched gradients.sh. Come check it out →
Email SDK
Reference

Telemetry and privacy

Opt-out SDK and CLI analytics, redacted error reporting, storage, and privacy behavior.

Core SDK and CLI telemetry are enabled by default. The first enabled run prints an opt-out notice to stderr and stores a random, stable installation identifier plus the notice marker. The identifier is not tied to a person's identity.

Opt out

Turn it off for every project on a machine. The choice is saved in the local state file:

status reports whether telemetry is on and which setting decided it. email-sdk telemetry enable reverses the choice. The saved choice applies to processes started afterwards; restart long-running servers to pick it up. If the file exists but cannot be read, telemetry stays off and the file is left untouched until you run disable or enable again.

Environment variables take precedence over the saved choice. They fit CI, containers, and Workers, where there is no persistent home directory:

In code, disableTelemetry() switches it off for the whole process, including clients that already exist and clients that a dependency creates:

Or opt out a single client:

NODE_ENV=test also disables capture. Repository tests use a Bun preload that sets EMAIL_SDK_TELEMETRY=0 for every in-process run.

When disabled, telemetry does not print the notice or make network requests. Only email-sdk telemetry disable writes config while opting out.

Events

The current event names are client created, email sent, email adapter attempted, email batch sent, and cli command run. Event names such as email sent describe an operation completing, including failure; they are not delivery receipts.

Allowed facts include built-in adapter names, operation/command names, counts, success, durations, stable error codes, runtime and SDK versions, CI facts, and feature-presence booleans. Custom adapter names become custom.

Counting messages

Use measurement_schema_version = 2 when aggregating the current send measurements. Older events used delivered_count for a successful send outcome, not confirmed delivery, and cannot be converted into accurate historical delivery totals. New events omit that property.

Keep these event series separate:

EventUnitCounting rule
email sentLogical send operationOne event per send() or sendPersonalized() call, regardless of retries or fallback. Each sendMany() item emits one.
email adapter attemptedAdapter invocationOne event for each actual send or native sendPersonalized invocation, including retries and fallback attempts.
email batch sentBatch summaryDescribes batch size and item outcomes; never add it to operation or provider-volume totals.

On logical operations, message_count is the requested number of individual messages: one for send() regardless of to/cc/bcc count, or one per personalized recipient. This includes operations that fail validation before contacting a provider. Use attempt events to measure work that reached an adapter.

Attempt events count recipients on the message after middleware has run. Native personalization can represent many messages in one invocation; expanded personalization produces an invocation for each recipient, plus any retries. Validation and CLI dry runs produce no attempt events.

Acceptance and uncertainty

Provider acceptance is not inbox delivery. Acceptance is recorded at the adapter-call boundary, before after-send middleware, hooks, or a subsequent cancellation can fail the logical operation. A partial provider response and a failed application operation can therefore coexist.

Use these properties to build reports. Filter to measurement_schema_version = 2 and the event named in each row.

ReportEventSum
Logical operationsemail sentlogical_operation_count
Successful / failed operationsemail sentoperation_success_count / operation_failure_count
Provider attemptsemail adapter attemptedprovider_attempt_count
Submitted messagesemail adapter attemptedprovider_submitted_message_count
Submitted recipientsemail adapter attemptedprovider_submitted_recipient_count
Explicitly accepted messages / recipientsemail adapter attemptedprovider_accepted_message_count / provider_accepted_recipient_count
Uncertain provider failuresemail adapter attempted, with provider_volume_eligible = trueuncertain_attempt_count

Submitted volume includes explicit acceptance and successful submissions where the adapter returns no recipient lists, as Resend does. acceptance_basis distinguishes explicit, inferred_from_success, and unknown. An explicitly empty acceptance list never implies acceptance. Explicit acceptance is a subset of submitted volume; adding these counters together would double count.

The generic accepted_recipient_count, rejected_recipient_count, and unknown_recipient_count describe recipient-level evidence, including test and custom adapters. A successful submission without recipient lists can still have unknown recipient-level acceptance. A timeout is not a confirmed rejection. not_sent_attempt_count tracks failures known not to have sent, separately from uncertain failures.

Provider-volume counters include runtime submissions through built-in and custom adapters. Built-in memory and failing adapters remain excluded even when renamed, as do CI and injected telemetry transports. adapter_kind identifies provider, test, or custom; custom and renamed route names are reported only as custom, without losing their submission counts. Adapter invocations remain observable in the generic counters. The SDK cannot detect every user-written mock or provider sandbox behind an otherwise normal adapter.

Acceptance totals describe observed submissions, not unique emails across separate SDK calls. The SDK does not collect message identifiers or idempotency keys, so it cannot deduplicate application retries across processes or prove that a provider delivered only one copy. Opt-outs, blocked requests, and processes exiting before telemetry flushes also make these measurements incomplete. Label aggregate numbers as observed SDK usage, not all emails sent or delivered.

Flushing on serverless

Sends fire telemetry without awaiting it, so delivery never adds latency to an email. Serverless runtimes freeze the process the moment a response is returned, which drops those in-flight requests and undercounts exactly the platforms most transactional email ships from.

client.flush() waits for in-flight telemetry and never rejects. Await it before your handler returns, or hand it to the platform's background primitive:

Long-running servers do not need this — the process stays alive long enough for the request to finish on its own. When telemetry is disabled, flush() resolves immediately.

Error reports

Handled provider and unknown failures may produce a PostHog $exception event. Caller validation mistakes and unknown adapter names are excluded.

Error reporting limits cause depth, frames, message length, duplicate classes, and reports per process. It scrubs email addresses, URLs, quoted text, long tokens, home directories, and filesystem paths from messages and stacks.

Data that is not collected

Telemetry does not collect message bodies, subjects, addresses, headers, attachments, credentials, idempotency keys, provider response bodies, URLs, or filesystem paths.

Local state

The default state file is ~/.config/email-sdk/telemetry.json, or $XDG_CONFIG_HOME/email-sdk/telemetry.json when XDG_CONFIG_HOME is set.

Observability plugin

Configure your own application log, metric, and trace events.

On this page