Observability
Distributed tracing goes to Honeycomb, and the goal is one trace per user action across the two clouds: a portal request on Cloudflare and the API Lambda work it triggers on AWS appear as a single trace, not two headless fragments — because a cross-cloud incident is undebuggable when each side only sees its own half. Getting there took three deliberate pieces — a real root span in the API, an instrumented Worker entry for the portal, and manual (not automatic) trace propagation between them. The last one is a hard-won production lesson.
The trace path
- apps/api opens a proper root span per invocation and continues an incoming
traceparent, so portal-originated requests join the caller’s trace instead of starting headless ones. - The portal Worker is instrumented by wrapping its entry point:
apps/portal/src/otel-worker-entry.tswraps TanStack Start’s real server entry (@tanstack/react-start/server-entry) withinstrument()from@microlabs/otel-cf-workers, exporting tohttps://api.honeycomb.io/v1/traceswith thex-honeycomb-teamheader from theHONEYCOMB_API_KEYWorker secret (see Secrets), service nameopsdna-portal. - Propagation is opt-in per call site:
server/traceparent-header.tsreads the active span viaspanContext()and builds thetraceparentheader manually wherever the portal fetches the API.
Two portal footnotes worth knowing: src/ssr.tsx is dead code for both build targets (verified empirically with a log marker that never appeared) — the real entry is the TanStack virtual server-entry module, which is exactly why the otel wrapper lives in its own file. And a Cloudflare-native tracing approach was evaluated and dropped in favor of otel-cf-workers.
The instrumentGlobalFetch trap
The portal’s otel config sets:
instrumentation: { instrumentGlobalFetch: false }Do not turn instrumentGlobalFetch back on. The library’s automatic mode monkey-patches globalThis.fetch to wrap every outbound call — and in production that broke the WorkOS SDK’s session refresh with TypeError: Illegal invocation (the SDK’s internal fetch usage lost its required this binding once wrapped). Users couldn’t authenticate; the fix shipped same-day via wrangler rollback. The library’s wrap code even looks correct on inspection, so the exact mechanism was never fully pinned — which is precisely why the mitigation is categorical: with global instrumentation off, tracing only touches fetches we explicitly opt into via traceparentHeader(), never WorkOS’s or any other library’s.
The general lesson: on Workers, prefer explicit per-call-site propagation over global monkey-patching. It costs one helper call per outbound fetch and eliminates an entire class of third-party breakage.
Adding tracing to a new call path
When the portal grows a new server-side fetch to the API, propagate the trace explicitly:
import { traceparentHeader } from "../server/traceparent-header";
const res = await fetch(`${env.API_BASE_URL}/some/route`, {
headers: { ...traceparentHeader() },
});Nothing else is needed — instrument() already gives that code real spanContext() access, and apps/api continues whatever traceparent arrives.
Other signals
- Cloudflare Worker observability (
"observability": { "enabled": true }inwrangler.jsonc) provides Workers-native logs/analytics for both the prod and staging portal workers, independent of Honeycomb. - CloudWatch alarms ship with the CDK stacks:
DatabaseStackcarries RDS alarms with an SNS alert topic, and the per-route Lambda factory (infra/lib/constructs/route-lambda.ts) wires alarms for each API route — see the infrastructure overview. - Staging traces flow the same way as prod (the staging worker has its own
HONEYCOMB_API_KEYsecret), so trace regressions can be caught on staging before promotion — see Environments.