Spectodocs

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=api

Node.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/node
import { 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 spanname, service, kind, start, duration, status and message, attributes (up to 40, values up to 1,000 characters), exception type, message and stack
Redactedattributes 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 request4 MB (gzip welcome), 2,000 spans; spans older than the retention window, or without valid ids, are rejected (partial success)
Ratethe plan's ingest rate, per organization (429 with Retry-After)
Retention30 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.