Back to Supabase

Client-side tracing

apps/docs/content/guides/monitoring-and-debugging/client-side-tracing.mdx

1.26.0811.1 KB
Original Source

The Supabase JS, Swift, and Dart SDKs can attach W3C Trace Context headers (traceparent, tracestate, baggage) to outgoing requests. The resulting trace_id flows through Supabase 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.

<Tabs scrollable size="small" type="underlined" defaultActiveId="js" queryGroup="language"

<TabPanel id="js" label="JavaScript">

Requirements

  • @supabase/supabase-js version 2.106.0 or later
  • @opentelemetry/api available 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
<Admonition type="caution">

As of @supabase/supabase-js version 2.112.0, the OpenTelemetry integration lives in an opt-in subpath that you load once at your application entry point:

ts
import '@supabase/supabase-js/tracing'

The main bundle contains no OpenTelemetry code — this import is what wires it up. The subpath imports @opentelemetry/api directly, so your bundler includes it and module resolution fails loudly if it isn't installed. If tracePropagation is enabled without this import, the SDK logs a one-time warning and sends requests without trace headers.

On versions 2.106.02.111.x, the subpath doesn't exist — don't add the import there. Those versions load @opentelemetry/api dynamically and silently no-op when it's missing.

</Admonition>

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 Supabase 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:

ts
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 Supabase 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:

ts
import '@supabase/supabase-js/tracing'

const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, {
  tracePropagation: {
    enabled: true,
    // Default: true. When false, headers are attached even if the
    // upstream trace is not sampled — useful when you want every
    // Supabase request tagged with a trace_id for log correlation.
    respectSamplingDecision: false,
  },
})
OptionTypeDefaultDescription
enabledbooleanfalseEnable trace propagation.
respectSamplingDecisionbooleantrueIf true, skip propagation when the upstream trace is not sampled.

Using a vendor tracing SDK

Many tracing SDKs are built on top of OpenTelemetry. They work with this guide as long as a W3C-compliant propagator is registered. Some vendor SDKs inject only their proprietary headers by default and need extra configuration to also emit the standard traceparent header. Check your vendor's OTel integration docs for the exact setup.

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 Supabase logs, check these in order:

  • The tracing runtime isn't loaded (version 2.112.0 and later). tracePropagation is 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 outside tracer.startActiveSpan(...) (or equivalent), there's nothing to propagate. Wrap the call in a span or use OpenTelemetry's automatic instrumentation.
  • @opentelemetry/api is not installed in the app making the request. On 2.112.0 and later the tracing subpath imports it directly, so a missing package surfaces as a module resolution error. On 2.106.02.111.x it's loaded dynamically and the SDK silently no-ops.
  • No TracerProvider registered. @opentelemetry/api defaults to a noop provider that produces non-recorded spans. Ensure your app calls provider.register() (or your vendor SDK's equivalent) before making requests.
  • The upstream trace is not sampled. By default the SDK respects upstream sampling decisions. Set respectSamplingDecision: false to propagate every request regardless of sampling.
  • You're calling a non-Supabase host through a custom fetch. Trace headers are only attached to Supabase 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.
</TabPanel> <TabPanel id="swift" label="Swift">

Requires supabase-swift 2.51.0 or later and swift-tools-version: 6.1 or later (SwiftPM trait support).

  1. Add the OpenTelemetry trait to your dependency declaration in Package.swift:

    swift
    // Package.swift
    .package(
      url: "https://github.com/supabase/supabase-swift.git",
      from: "2.51.0",
      traits: ["OpenTelemetry"]
    )
    

    No changes to SupabaseClient 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.

  2. Register a TracerProvider at app start. The SDK reads from whatever provider you register globally:

    swift
    import Supabase
    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)
    
  3. Create your SupabaseClient. Any active span is now propagated automatically:

    swift
    let supabase = SupabaseClient(
      supabaseURL: URL(string: "https://xyzcompany.supabase.co")!,
      supabaseKey: "your-publishable-key"
    )
    
</TabPanel> <TabPanel id="dart" label="Dart">

Requires supabase 2.x or later (Flutter or Dart-only).

  1. Implement a traceContextProvider that returns the current TraceContext from your tracing library. Return null when there is no active span.

  2. Pass TracePropagationOptions when creating the client:

    dart
    import 'package:supabase/supabase.dart';
    
    final supabase = SupabaseClient(
      '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 Supabase.initialize:

    dart
    await Supabase.initialize(
      url: 'https://xyzcompany.supabase.co',
      anonKey: 'your-publishable-key',
      tracePropagationOptions: TracePropagationOptions(
        enabled: true,
        traceContextProvider: () => yourTraceContextProvider(),
      ),
    );
    

Options

OptionTypeDefaultDescription
enabledboolfalseEnable trace propagation.
respectSamplingDecisionbooltrueWhen 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.
traceContextProviderTraceContextProvider?nullCallback returning the current TraceContext. Return null when there is no active span.

Headers are only injected on requests targeting Supabase hosts (*.supabase.co, *.supabase.in, your project host, and loopback addresses for local development). Third-party hosts never receive trace headers.

</TabPanel> </Tabs>

Correlating with Supabase 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 Supabase logs to a third-party backend via Log Drains, you can join Supabase 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 — Supabase logs become first-class citizens in your existing tracing UI.