Backend traces
A slow or failing request in a replay often only makes sense with what the server did. Backend traces put your server's spans — timings, database calls, the exception — under the request, in the same replay.
How it works
With traces on, the browser SDK adds a W3C traceparent header to your app's requests and records the trace id with each one. Your server continues that trace — any OpenTelemetry SDK does this out of the box, or use @spectosh/node — and sends its spans to Specto. In the replay, a request whose server reported spans gets a trace chip; open it for the waterfall.
1. Send trace headers from the browser
<script src="https://specto.sh/specto.js" data-key="pk_your_key" data-traces="true" defer></script>
startRecording({ key: 'pk_your_key', traces: true })
Only same-origin requests get the header by default. For an API on another origin, list it — and allow the header in that API's CORS (Access-Control-Allow-Headers: traceparent), or the browser blocks the request:
<script src="https://specto.sh/specto.js" data-key="pk_your_key" data-traces="true" data-trace-origins="https://api.example.com" defer></script>
A request that already carries a traceparent (OpenTelemetry in the browser, for example) keeps it, and Specto records that trace id. The header never goes to Specto's own endpoints.
2. Create a server key
Settings → Install → Backend traces → Create server key (project admins). It's shown once; Specto stores only its hash. It's a secret: keep it in your server's environment, never in browser code. Rotating it stops the old key within a minute.
3a. With OpenTelemetry
Point the OTLP HTTP/JSON exporter at Specto — the standard environment variables work with every OpenTelemetry SDK:
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://specto.sh/api/otlp/v1/traces
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_TRACES_HEADERS="authorization=Bearer sk_your_server_key"
OTEL_SERVICE_NAME=apiNode.js
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http// tracing.js — load it first: node --import ./tracing.js server.js import { NodeSDK } from '@opentelemetry/sdk-node' import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' new NodeSDK({ traceExporter: new OTLPTraceExporter(), // reads the OTEL_EXPORTER_OTLP_TRACES_* variables instrumentations: [getNodeAutoInstrumentations()], }).start()
Python, Go, Java, .NET and Ruby work the same way: their OTLP HTTP exporters read the same variables. Specto accepts OTLP JSON only — choose the http/json protocol (protobuf and gRPC answer 415).
3b. With @spectosh/node
No OpenTelemetry? This tracer has no dependencies: one span per request, child spans where you want them, errors recorded.
npm install @spectosh/nodeimport { createTracer } from '@spectosh/node' const tracer = createTracer({ endpoint: 'https://specto.sh/api/otlp/v1/traces', serverKey: process.env.SPECTO_SERVER_KEY, service: 'api', }) // Express, Connect or node:http app.use(tracer.middleware()) // … your routes … app.use(tracer.errorHandler()) // records the error on the request's span // fetch-style handlers: Next.js route handlers, Remix, Hono, TanStack Start… export const POST = tracer.wrap(async (request) => Response.json(await checkout(request))) // child spans around anything worth timing const user = await tracer.span('SELECT users', () => db.query(sql), { 'db.system': 'postgresql' })
Spans are sent in batches every two seconds. In serverless functions, await tracer.flush() before returning; on shutdown, await tracer.shutdown(). Calling another service? Pass tracer.current.traceparent as its traceparent header and its spans join the same trace.
What is stored, and limits
| Per span | name, service, kind, start, duration, status and message, attributes (up to 40, values up to 1,000 characters), exception type, message and stack |
| Redacted | attributes named like secrets (authorization, cookie, token, password, api_key, session_id…) become [redacted]; emails, tokens and card-like numbers in values are masked on arrival |
| Per request | 4 MB (gzip welcome), 2,000 spans; spans older than the retention window, or without valid ids, are rejected (partial success) |
| Rate | the plan's ingest rate, per organization (429 with Retry-After) |
| Retention | 30 days |
Spans count toward no quota. They are your server's data: send only what you'd want a teammate watching a replay to see — a database statement with literal values may carry personal data.
No trace chip on a request?
• In your browser's Network tab, the request should carry a traceparent header — if not, check data-traces and, for another origin, data-trace-origins.
• Your server must read it: OpenTelemetry's HTTP instrumentation and @spectosh/node do. A proxy that strips unknown headers breaks the link.
• The exporter's response says the rest: 401 a wrong or rotated key, 415 protobuf instead of JSON, 429 rate limited. See also troubleshooting.

