Skip to Content
SDKSelf-diagnostics

Self-diagnostics

The Morse SDK absorbs its own internal errors so they can never break your application. When it does, it reports that error to us — so we find out our SDK is broken inside your process without you having to file a ticket.

This page is the complete list of what that report contains. It is checked against the wire schema by an automated test: if we add a field to the payload without adding it here, our build fails.

Everything we receive, you also see. Every diagnostic is written to your own morse_ai logger before it is transmitted — including when you have transmission switched off entirely. There is no category of self-reported information we get that your engineers cannot.

On by default

Self-diagnostics is on by default, at level error — the SDK transmits an event when it absorbs one of its own internal errors. You do not switch it on, and upgrading the SDK does not ask: if you are on a version that has this feature, it is already active. To switch it off, see Turning it off.

The level is off, error, or debug. It starts at error and we can lower or raise it from our side via a response header on traffic the SDK is already sending — used to mute a noisy release, or to ask for more detail while chasing a bug. debug relaxes how many reports may be sent; it never widens what may be sent, and it cannot override your opt-out.

When it sends

Only on failure. There is no heartbeat, no periodic health check, no startup ping. A process that runs without an internal SDK error transmits zero diagnostic events, and never even starts the background sender.

Two kinds of report exist:

  • Errors — one absorbed exception from inside the SDK.
  • Delivery counters — aggregate counts of telemetry the SDK had to discard (a full queue, an exhausted retry, a quota rejection). These are counts only, and they are how you and we both learn that telemetry is being silently lost.

What is never sent

This list is the point of the page, so it comes before the field table:

  • Your prompts and completions. No LLM input or output, ever, at any verbosity level.
  • Your span, trace, or log content. A diagnostic is about the SDK, not about your data.
  • Local variables. Stack frames are captured with traceback.extract_tb (Python) and by parsing Error.stack (TypeScript) — neither reads frame locals, so there is nothing to strip.
  • Your application’s stack frames. Frames outside the Morse package are replaced by a count. We learn “three of your frames were here”; we never learn which files or functions.
  • Your environment variables, config values, API keys, or credentials of any kind. Exception messages get a second, unconditional pass for credential assignments (password=, client_secret:, authorization: Bearer …, PGPASSWORD=) — even if your organization has switched that pattern off for its own telemetry. We would rather lose a word of our own error text than receive one of your secrets.
  • Your file paths. SDK frames are reported relative to the Morse package root, so no absolute path from your machine or container is transmitted.
  • Your users’ data, IDs, or identifiers of any kind.

Every field we transmit

FieldWhat it is
kinderror or counters
fingerprintStable identifier for the failing code path, e.g. morse_ai.tracing._send_trace
error_typeException class name, e.g. KeyError
messageException message, passed through the same PII redaction as your telemetry, then through the secret-assignment scrub a second time unconditionally, and byte-capped
stackMorse-scoped frames: our file/line/function, plus a count of your elided frames
countersDelivery-loss counts, {reason, category, quantity} — see below
sdk_languagepython or typescript
sdk_versionThe SDK version installed
runtime_versionPython or Node version
platformOS and architecture, e.g. Linux-x86_64
extraAn allow-listed set only: adapter, module, function, operation, endpoint_host (hostname only, never a path or query)

Your organization is identified by the API key the request is authenticated with. It is never read from the payload — the schema has no field for it.

Delivery-loss reasons

Each is a closed value, paired with a category (trace, span, log, record, diagnostic) and a count. No free text is ever sent.

ReasonWhat it means
queue_fullThe in-memory queue was full and the oldest events were evicted.
encode_failedThe batch could not be serialised to JSON. A payload problem, not a network one.
auth_rejectedThe server returned 401 or 403 — the API key is revoked, expired, or lacks the scope.
endpoint_rejectedAnother non-429 4xx (400, 404, 413, 422) — usually a misconfigured endpoint URL.
quota_rejectedThe server returned 429 and the batch was still being rate-limited after the SDK had waited out the server’s Retry-After and retried. Seeing this means you were over your ingest limit for long enough that redelivery could not catch up, not that a single 429 was discarded.
send_failedA send failed and no retry was possible — the flush budget had already elapsed.
retry_exhaustedA retryable failure (5xx or a network error) that used up all three attempts. A rate limit that outlasts our retries is reported as quota_rejected, not here.
shutdown_dropThe process was shutting down and the batch could not be flushed in time.

auth_rejected, endpoint_rejected and send_failed were added in 2026-08. Before that, the Python SDK reported every non-429 4xx as encode_failed and the TypeScript SDK reported every drop as retry_exhausted, so these failures were indistinguishable in the counters. The TypeScript SDK gained a retry loop shortly after, which is what finally made its retry_exhausted truthful.

Since 2026-08, these counters also cover the trace pipeline’s OTLP exporter, not only the legacy JSON transport. A batch the exporter gives up on — the common case being a burst that trips your plan’s ingest rate limit — previously produced a single line on your own stdout and nothing else: no exception, no counter, no signal here. It is now counted under span, and logged locally at WARNING with the count, whichever way you have diagnostics configured.

Retention

Diagnostic events are deleted after 30 days, on a fixed schedule that does not depend on your plan.

Turning it off

Set MORSE_SDK_DISABLE_TELEMETRY=1 in your environment. Nothing is transmitted.

Local logging continues. Opting out stops data leaving your process; it does not stop your own engineers from seeing what the SDK is doing. Delivery-loss counters in particular keep reaching your logger at WARNING, because silent telemetry loss is exactly the thing you want your on-call to know about.

⚠️

Turning this off means we cannot see SDK failures happening in your environment. If the SDK breaks for you, we will only find out when you tell us.

Rate limits and volume

Diagnostics are bounded on every axis so they can never become a load source:

  • At most 100 reports per failing code path per minute leave the SDK.
  • Local log lines are capped separately, so a tight failure loop cannot flood your logs.
  • Pending diagnostics are held in a small bounded buffer; if it fills, the oldest are dropped.
  • If our endpoint is unreachable, a circuit breaker stops attempts for five minutes rather than retrying. Diagnostics are never retried — a retry loop would amplify the exact failure being reported.
Last updated on