Files
supabase/apps/docs/content/guides/observability/client-side-tracing.mdx
e0280cb650 docs: restructure observability navigation and overview (#49505)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Stack

Draft stack extracted from `docs/monitoring`. Merge bottom-up. The
troubleshooting *catalog* rewrite (`content/troubleshooting` and the
Diagnosing UI) stays out of scope.

1. #49503 move inspect and advisors
2. #49501 split Studio logs from ClickHouse queries
3. #49500 treat reports as signal dashboards
4. #49502 add Observe the data hub
5. #49506 add agent setup components
6. #49504 add hire-an-agent templates
7. **#49505** restructure observability nav, overview, Detecting, and
flatten Observe the data ← **this PR**

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Docs update. Top layer in the observability stack.

## What is the current behavior?

The section is still titled Monitoring and Debugging, with a Debugging /
Monitoring split that does not match the new pages. The debugging guide
is still the master layer-isolation + symptom table. Observe the data is
split into “what data” vs “where to observe it,” which duplicates the
source pages.

## What is the new behavior?

- Section title is Observability
- Overview groups Observe the data, Detect and resolve, Hire an agent,
and Export
- **Observe the data is flattened by source.** Logs, Metrics API,
Database, Advisors, and Reports each list where to read that source.
There is no separate MCP/API/CLI/Studio nav group.
- **Observe vs Detecting:** Observe is the catalog (what exists, how to
access it). Detecting is how to *use* those sources to pick up a Health
/ Security / Performance / Usage signal. Named errors skip to
Diagnosing.
- Studio Logs sits under Logs. Reports sits beside the other sources.
- Troubleshooting stays in the global menu and also appears as
Diagnosing under Detect and resolve

## Additional context

This is the last PR in the stack. Together the seven PRs reconstruct the
`docs/monitoring` observability IA and guide content, without shipping
the troubleshooting catalog overhaul.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-a3cb5ece-925b-4046-b58a-5d69e9a9d794?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-a3cb5ece-925b-4046-b58a-5d69e9a9d794&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Saxon Fletcher <SaxonF@users.noreply.github.com>
Co-authored-by: Nik Richers <nik@validmind.ai>
2026-09-04 13:38:39 +10:00

283 lines
18 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: 'client-side-tracing'
title: 'Client-side tracing'
description: 'Propagate W3C trace context from the Supabase JS, Swift, and Dart SDKs through Supabase services'
---
The Supabase JS, Swift, Dart and Python SDKs can attach [W3C Trace Context](https://www.w3.org/TR/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. On the client side, some vendor SDKs need a small configuration change before they emit the standard headers — see [Using a vendor tracing SDK](#using-a-vendor-tracing-sdk) in the JavaScript tab.
<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.0`–`2.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](https://opentelemetry.io/docs/languages/js/getting-started/) 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.
<Admonition type="note">
Calling Edge Functions from the browser with trace propagation enabled requires the function's CORS allow-list to include the trace headers. In the function, import `corsHeaders` from `npm:@supabase/supabase-js@^2.112.3/cors` or add `traceparent`, `tracestate`, and `baggage` to your own allow-list, then redeploy the function. See [CORS support for Edge Functions](/docs/guides/functions/cors).
</Admonition>
## 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. 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 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.0`–`2.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.
- **Your tracing SDK's propagator doesn't emit W3C `traceparent`.** Sentry's propagator, for example, only emits it when `propagateTraceparent: true` is set. From version `2.112.3` the SDK logs a one-time warning naming the headers the propagator wrote — see [Using a vendor tracing SDK](#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. From `2.112.3`, non-sampled requests still carry `traceparent`, so log correlation keeps working by default. Set `respectSamplingDecision: false` to always send the full trace context.
- **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
| 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 Supabase hosts (`*.supabase.co`, `*.supabase.in`, your project host, and loopback addresses for local development). Third-party hosts never receive trace headers.
</TabPanel>
<TabPanel id="python" label="Python">
The Python `opentelemetry` propagation is handled entirely through the `opentelemetry-instrumentation-httpx` package.
1. **Add** the `opentelemetry-sdk` and `opentelemetry-instrumentation-httpx` package:
```sh
uv add opentelemetry-sdk opentelemetry-instrumentation-httpx
```
2. **Instrument** the `httpx` client using the `HTTPXClientInstrumentor`:
```python
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
HTTPXClientInstrumentor().instrument()
```
<Admonition type="note">
This will instrument all `httpx` clients in your process. If you want to instrument only the Supabase client, you can use `HTTPXClientInstrumentor.instrument_client` in the specific sub-package client you want to trace.
</Admonition>
3. **Create** your `SupabaseClient`. Any active span is now propagated automatically:
```python
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()
```
</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](/docs/guides/observability/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.