Telemetry
Telemetry is how your App reports on its own health — capturing exceptions, sending log lines, and leaving a breadcrumb trail you can read back when something goes wrong. You reach it through sdk.telemetry, and your handler code is identical no matter which provider is behind it.
The default provider is Sentry. Your App can instead run on Datadog, Better Stack, or OpenTelemetry (OTLP) — that choice is a platform-level setting, not a code change. Whatever is configured, you call the same four methods: captureError, captureMessage, log, and addBreadcrumb. Never import a provider SDK or fetch a provider endpoint directly; go through sdk.telemetry so your code stays portable and the platform can swap providers under you.
Every Telemetry call is fire-and-forget. A missing secret, a provider rate-limit, or a network failure is swallowed — none of them ever surfaces as a handler error. Always call Telemetry with void, never await, so it can never add latency or break your request path.
Quick reference
Section titled “Quick reference”| Method | Shape | Returns |
|---|---|---|
captureError | captureError(error, context?) | Promise<void> |
captureMessage | captureMessage(message, context?) | Promise<void> |
log | log(input) | Promise<void> |
addBreadcrumb | addBreadcrumb(breadcrumb) | Promise<void> |
You reach these through the SDK inside a handler:
const sdk = c.get("sdk")void sdk.telemetry.captureError(err)All four methods return a resolved Promise<void> unconditionally. The void prefix tells TypeScript you are intentionally not awaiting.
Capturing errors
Section titled “Capturing errors”captureError is the workhorse. Pass the thrown value as the first argument; the provider records the full stack trace itself.
const sdk = c.get("sdk")
try { await doWork()} catch (err) { void sdk.telemetry.captureError(err) return c.json({ error: "internal" }, 500)}The optional second argument is a context object:
type CaptureErrorContext = { tags?: Record<string, string> extra?: Record<string, unknown> trace?: TraceContext fingerprint?: string[] release?: string user?: TelemetryUser}void sdk.telemetry.captureError(err, { tags: { route: "checkout", region: "ap" }, extra: { orderId },})Do not also write error.stack to console in the same catch block. The provider already captures the stack, and echoing it to Worker logs can surface runtime detail you would not otherwise expose.
Capturing a message
Section titled “Capturing a message”captureMessage records a notable event that is not an exception — a degraded-but-handled condition, an unexpected branch, anything you want visibility into without throwing.
type CaptureMessageContext = { level?: TelemetryLevel // "fatal" | "error" | "warning" | "info" | "debug" tags?: Record<string, string> extra?: Record<string, unknown> trace?: TraceContext fingerprint?: string[] release?: string user?: TelemetryUser}void sdk.telemetry.captureMessage("stripe webhook arrived out of order", { level: "warning", tags: { provider: "stripe" },})Structured logging
Section titled “Structured logging”log sends a single structured log line with a level, a message, and arbitrary attributes.
type LogInput = { level: TelemetryLevel message: string // 1–8192 chars attributes?: Record<string, unknown> trace?: TraceContext release?: string}void sdk.telemetry.log({ level: "info", message: "order fulfilled", attributes: { orderId, itemCount },})message is validated to between 1 and 8192 characters. An out-of-range message is silently dropped — the SDK does not throw; it tracks the validation failure internally and your handler continues.
Breadcrumbs
Section titled “Breadcrumbs”Breadcrumbs are a request-scoped trail. You drop them along the way; if an error is later captured in the same handler invocation, the breadcrumbs ride along with it so you can see what led up to the failure.
type Breadcrumb = { category: string // 1–200 chars message: string // 1–2000 chars level?: TelemetryLevel data?: Record<string, unknown>}const sdk = c.get("sdk")
void sdk.telemetry.addBreadcrumb({ category: "db", message: "loaded cart", data: { itemCount },})// ... later in the same request ...void sdk.telemetry.captureError(err) // breadcrumbs attach automaticallyThe breadcrumb buffer is scoped to one request, holds up to 50 entries (the oldest drop when it fills), and drains into the next captureError or captureMessage. Breadcrumbs are not attached to log or addBreadcrumb calls themselves.
Distributed tracing
Section titled “Distributed tracing”When a request arrives carrying a trace header from an upstream service, parse it and pass the resulting TraceContext to your Telemetry calls. This correlates your App’s errors with the wider request in the provider’s trace view.
type TraceContext = { traceId: string // 32 hex chars spanId: string // 16 hex chars sampled: boolean}Two inbound parsers and matching formatters are exported from @anyknown/app-server:
import { parseTraceparent, parseSentryTrace, formatTraceparent, formatSentryTrace, newSpanId,} from "@anyknown/app-server"const trace = parseTraceparent(c.req.header("traceparent") ?? null)
if (trace) { void sdk.telemetry.captureError(err, { trace })}parseTraceparent reads the W3C traceparent header; parseSentryTrace reads a sentry-trace header. Both return null on absent or malformed input, so the if (trace) guard above is all the validation you need. Use formatTraceparent / formatSentryTrace (and newSpanId for a fresh child span) when you propagate the trace onward to a downstream service.
When you pass a trace with sampled: true, the event bypasses the sample-rate filter and is always sent — the App honors the upstream’s sampling decision.
Attaching the signed-in user
Section titled “Attaching the signed-in user”To attribute an error or message to the Visitor who hit it, use the attachSessionUser helper. It reads the session through the SDK and returns a ready-to-spread user object, so your Telemetry code never reaches into auth internals.
import { attachSessionUser } from "@anyknown/app-server"const { user } = await attachSessionUser(sdk, c.req.raw)
void sdk.telemetry.captureError(err, { user })type TelemetryUser = { id?: string email?: string ip_address?: string}The helper populates user.id only. The id is sent as-is — it is not hashed — so this is appropriate for an opaque session id. Do not hand-fill email or ip_address unless you have confirmed your provider’s data handling is acceptable for that data; those fields exist for providers that expect them, not as a recommendation to send PII.
Error grouping with fingerprint
Section titled “Error grouping with fingerprint”fingerprint is an array of strings that tells the provider how to group related errors into a single issue. It is honored only by the Sentry adapter for server-side grouping; the other providers store it as a plain tag or attribute.
void sdk.telemetry.captureError(err, { fingerprint: ["checkout", "payment-declined"],})Configuration
Section titled “Configuration”There is nothing to enable — all four methods and all four providers are available on every App, with no plan flag. The only setup is the secret your active provider needs. You set these by their NAME through Secret Storage; the value is injected for you at runtime and never appears in your code.
| Secret name | Provider | Notes |
|---|---|---|
SENTRY_DSN | Sentry (default) | Full DSN URL: https://<key>@<host>/<project_id>. |
DD_API_KEY | Datadog | — |
BETTERSTACK_SOURCE_TOKEN | Better Stack | — |
OTEL_EXPORTER_OTLP_HEADERS_AUTH | OTLP | Optional; sent as the Authorization header if set. |
Which provider is active, and a few routing details, are platform settings rather than secrets — they are configured for your App, not set in code:
| Setting | Applies to | Notes |
|---|---|---|
TELEMETRY_PROVIDER | — | Selects the active provider. Absent or unrecognized → Sentry. |
TELEMETRY_SAMPLE_RATE | — | Float 0–1. Absent → 1 (send everything). |
TELEMETRY_LOG_DRAIN_URL | — | Optional secondary HTTP endpoint that also receives every event as a JSON POST. |
APP_RELEASE | Datadog / Better Stack / OTLP | Default release tag when you don’t pass context.release. |
DD_SITE | Datadog | Defaults to datadoghq.com; use datadoghq.eu for EU. |
BETTERSTACK_INGEST_HOST | Better Stack | Required for Better Stack — the ingest host, no trailing slash. |
OTEL_EXPORTER_OTLP_ENDPOINT | OTLP | Required for OTLP — the collector base URL. |
For Datadog, Better Stack, and OTLP, if you don’t pass context.release (or LogInput.release), the APP_RELEASE tag is used as the default. For Sentry, release is caller-supplied only.
Reading errors back
Section titled “Reading errors back”Under normal SDK wiring, none of the four methods ever rejects — so you almost never need to handle a TelemetryError. It exists for the rare case where you opt in with an explicit .catch(), or for adapter authors.
type TelemetryErrorKind = | "secret-missing" | "validation" | "auth" | "rate-limited" | "provider-error"
class TelemetryError extends Error { readonly kind: TelemetryErrorKind readonly status: number readonly retryAfterSec?: number}Because every public method already absorbs failures, treat TelemetryError as informational. Do not build your handler’s control flow around catching it.
Things to know
Section titled “Things to know”These are the details that most often trip people up.
-
Always
void, neverawait. Telemetry is fire-and-forget by design — awaiting it only adds latency, because failures are already absorbed and there is no result to act on. Keep it off your request’s critical path. -
PII is scrubbed on the way out. The
extrafields on a capture context, theattributeson a log, anduser.emailall pass through a configurable PII scrub before leaving the Worker. Pattern-matched values are redacted. This is a safety net, not a license to send personal data on purpose. -
Sampling can drop events. When
TELEMETRY_SAMPLE_RATEis below1, a fraction of events is dropped before sending. Atracewithsampled: truealways bypasses sampling, so traced errors are never sampled out. -
A rate-limited provider trips a brief circuit breaker. If the provider returns 429, further events are dropped silently for about 60 seconds, then sending resumes. The breaker fails open — a fault in the platform’s own limiter never blocks your telemetry.
-
Validation failures no-op silently. An over-long
logmessage, or a breadcrumb that breaks thecategory/messagelength limits, is dropped without throwing. Your handler is never interrupted by a malformed Telemetry call — but the event also never arrives, so keep messages within the documented limits. -
fingerprintis Sentry-only for grouping. On Datadog, Better Stack, and OTLP it is recorded as a plain attribute, not used to merge issues. -
OTLP sends to two signals when traced. With a
tracepresent, the OTLP adapter emits to both the logs and traces endpoints; without one, only logs. You don’t configure this — it follows from whether you passtrace. -
The log drain mirrors every sent event. If
TELEMETRY_LOG_DRAIN_URLis configured, each event that passes sampling and the circuit breaker is also POSTed there as JSON — independently of whether the primary provider call succeeds.