The Zuvo JS, Swift, Dart and Python SDKs can attach W3C Trace Context headers (traceparent, tracestate, baggage) to outgoing requests. The resulting trace_id flows through Zuvo services and appears in API Gateway and Edge Function logs, so you can correlate client-side spans with the server-side logs they produced — end-to-end, across the network boundary.
Because the headers follow the W3C standard, any compliant tracing SDK (such as OpenTelemetry, Sentry, Datadog, or Honeycomb) can pick up the trace on the server side, including in self-hosted collectors. On the client side, some vendor SDKs need a small configuration change before they emit the standard headers — see Using a vendor tracing SDK in the JavaScript tab.
Requirements
@supabase/supabase-jsversion2.106.0or later@opentelemetry/apiavailable at runtime — either installed directly or pulled in as a transitive dependency of your tracing SDK- A tracing SDK that registers a W3C-compliant propagator with the OpenTelemetry API
Trace propagation isn't available through the CDN (UMD) build — there's no way to load the tracing runtime there.
Set up OpenTelemetry first
The SDK reads from whatever TracerProvider you register globally — it doesn't configure one for you. If you haven't instrumented your app yet, follow the OpenTelemetry JavaScript getting started guide to install an SDK (@opentelemetry/sdk-trace-node for Node, @opentelemetry/sdk-trace-web for browsers) and an exporter for your backend (OTLP, Jaeger, Zipkin, or a vendor-specific one).
The Zuvo SDK only propagates the trace context that's already active when a request is made.
Enable trace propagation
Trace propagation is opt-in and takes two steps: load the tracing runtime at your entry point (version 2.112.0 and later), and pass tracePropagation: true when creating the client:
import '@supabase/supabase-js/tracing'
import { trace } from '@opentelemetry/api'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
tracePropagation: true,
})
const tracer = trace.getTracer('my-app')
await tracer.startActiveSpan('fetch-users', async (span) => {
// Outgoing request carries the active trace context.
const { data, error } = await supabase.from('users').select('*')
span.end()
})
For security, trace headers are only attached to requests targeting Zuvo domains (*.supabase.co, *.supabase.in, and localhost for local development). Third-party hosts called through a custom fetch are never tagged.
Advanced configuration
Pass an object instead of true for fine-grained control:
import '@supabase/supabase-js/tracing'
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
tracePropagation: {
enabled: true,
// Default: true. Non-sampled requests carry only `traceparent` (with the
// sampled flag preserved, so nothing is recorded downstream) — log
// correlation keeps working while `tracestate` and `baggage` are withheld.
// Set to false to always send the full trace context regardless of sampling.
respectSamplingDecision: false,
},
})
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable trace propagation. |
respectSamplingDecision | boolean | true | When true, non-sampled requests send only traceparent (sampled flag preserved) and omit tracestate and baggage; false always sends the full trace context. On versions before 2.112.3, true skipped all trace headers for non-sampled requests. |
Using a vendor tracing SDK
Many tracing SDKs are built on top of OpenTelemetry, but they differ in whether their propagator emits the standard traceparent header by default:
| Vendor setup | Works with tracePropagation? | Required configuration |
|---|---|---|
| OpenTelemetry SDK (also Honeycomb, Grafana, New Relic via OTLP) | Yes | None — the W3C propagator is the default |
| Sentry (Node.js, including Next.js server-side) | Yes, with one flag | Set propagateTraceparent: true in Sentry.init() — Sentry's propagator omits traceparent by default |
| Sentry (browser) | Via Sentry's own instrumentation | Set propagateTraceparent: true and add your project URL (https://<ref>.supabase.co) to tracePropagationTargets — Sentry's browser SDK only attaches headers cross-origin for listed targets. For Edge Functions, also add sentry-trace to the function's CORS allow-list: Sentry always sends its own header, and it isn't part of corsHeaders |
Datadog dd-trace (Node.js) | Out of the box | None — dd-trace injects W3C headers at the HTTP layer itself, even without tracePropagation |
| Datadog Browser RUM | Yes, with configuration | Add your project URL to allowedTracingUrls with the tracecontext propagator type |
If a propagator is active but doesn't emit traceparent, the SDK logs a one-time console warning naming the headers the propagator wrote (version 2.112.3 and later).
Troubleshooting
The SDK never throws when it can't propagate, which keeps it safe to enable but can mask configuration issues. If trace_id is missing from your Zuvo logs, check these in order:
- The tracing runtime isn't loaded (version
2.112.0and later).tracePropagationis enabled but your entry point never imports@supabase/supabase-js/tracing. The SDK logs a one-time console warning and sends requests without trace headers — look for that warning in your console. - No active span at request time. The SDK reads the current context. If
supabase.from(...)is called outsidetracer.startActiveSpan(...)(or equivalent), there's nothing to propagate. Wrap the call in a span or use OpenTelemetry's automatic instrumentation. @opentelemetry/apiis not installed in the app making the request. On2.112.0and later the tracing subpath imports it directly, so a missing package surfaces as a module resolution error. On2.106.0–2.111.xit's loaded dynamically and the SDK silently no-ops.- No
TracerProviderregistered.@opentelemetry/apidefaults to a noop provider that produces non-recorded spans. Ensure your app callsprovider.register()(or your vendor SDK's equivalent) before making requests. - Your tracing SDK's propagator doesn't emit W3C
traceparent. Sentry's propagator, for example, only emits it whenpropagateTraceparent: trueis set. From version2.112.3the SDK logs a one-time warning naming the headers the propagator wrote — see Using a vendor tracing SDK. - The upstream trace is not sampled (versions before
2.112.3). Older versions skip all trace headers when the upstream trace is not sampled. From2.112.3, non-sampled requests still carrytraceparent, so log correlation keeps working by default. SetrespectSamplingDecision: falseto always send the full trace context. - You're calling a non-Zuvo host through a custom
fetch. Trace headers are only attached to Zuvo domains (*.supabase.co,*.supabase.in,localhost). - You're using the CDN (UMD) build. Trace propagation isn't available there — the tracing runtime can't be loaded from a script tag.
Requires supabase-swift 2.51.0 or later and swift-tools-version: 6.1 or later (SwiftPM trait support).
- Add the
OpenTelemetrytrait to your dependency declaration inPackage.swift:
// Package.swift
.package(
url: "https://github.com/supabase/supabase-swift.git",
from: "2.51.0",
traits: ["OpenTelemetry"]
)
No changes to ZuvoClient are required. After enabling the trait, the active OpenTelemetry span's trace context is automatically injected as a traceparent header on every outgoing request across PostgREST, Storage, Auth, Functions, and Realtime. When there is no active span, the header is not added.
- Register a
TracerProviderat app start. The SDK reads from whatever provider you register globally:
import Zuvo
import OpenTelemetryApi
import OpenTelemetrySdk
let exporter = /* your OTLP / Jaeger / Zipkin exporter */
let spanProcessor = SimpleSpanProcessor(spanExporter: exporter)
let provider = TracerProviderBuilder()
.add(spanProcessor: spanProcessor)
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: provider)
- Create your
ZuvoClient. Any active span is now propagated automatically:
let supabase = ZuvoClient(
supabaseURL: URL(string: "https://xyzcompany.supabase.co")!,
supabaseKey: "your-publishable-key"
)
Requires supabase 2.x or later (Flutter or Dart-only).
-
Implement a
traceContextProviderthat returns the currentTraceContextfrom your tracing library. Returnnullwhen there is no active span. -
Pass
TracePropagationOptionswhen creating the client:
import 'package:supabase/supabase.dart';
final supabase = ZuvoClient(
'https://xyzcompany.supabase.co',
'your-publishable-key',
tracePropagationOptions: TracePropagationOptions(
enabled: true,
traceContextProvider: () {
final span = YourTracer.activeSpan;
if (span == null) return null;
return TraceContext(
traceparent: span.traceparent,
tracestate: span.tracestate,
);
},
),
);
For supabase_flutter, pass the same option through Zuvo.initialize:
await Zuvo.initialize(
url: 'https://xyzcompany.supabase.co',
anonKey: 'your-publishable-key',
tracePropagationOptions: TracePropagationOptions(
enabled: true,
traceContextProvider: () => yourTraceContextProvider(),
),
);
Options
| Option | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable trace propagation. |
respectSamplingDecision | bool | true | When true, skips propagation if the upstream trace is not sampled. Set to false to always attach a trace_id — useful for log correlation even when traces are not exported. |
traceContextProvider | TraceContextProvider? | null | Callback returning the current TraceContext. Return null when there is no active span. |
Headers are only injected on requests targeting Zuvo hosts (*.supabase.co, *.supabase.in, your project host, and loopback addresses for local development). Third-party hosts never receive trace headers.
The Python opentelemetry propagation is handled entirely through the opentelemetry-instrumentation-httpx package.
- Add the
opentelemetry-sdkandopentelemetry-instrumentation-httpxpackage:
uv add opentelemetry-sdk opentelemetry-instrumentation-httpx
- Instrument the
httpxclient using theHTTPXClientInstrumentor:
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
HTTPXClientInstrumentor().instrument()
- Create your
ZuvoClient. Any active span is now propagated automatically:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from supabase import AsyncClient
trace.set_tracer_provider(TracerProvider())
tracer = trace.get_tracer(__name__)
async def query(client: AsyncClient):
with tracer.start_as_current_span("orchestral_query") as span:
await client.table("orchestral_sections") \
.select("name, instruments(name)") \
.order("name", desc=True, foreign_table="instruments") \
.execute()
Correlating with Zuvo logs
After trace context is flowing through, the trace_id appears in:
- API Gateway logs — every request to PostgREST, Auth, Storage, and Realtime
- Edge Function logs — invocations and any structured logs emitted from within the function
If you forward Zuvo logs to a third-party backend via Log Drains, you can join Zuvo logs to your own client and server traces using the shared trace_id. This is especially useful for self-hosted setups where you already operate your own OpenTelemetry collector — Zuvo logs become first-class citizens in your existing tracing UI.