Node.js tracing installation

Contents

There are two ways to send spans from Node.js.

posthog-nodeOpenTelemetry
PackagesThe SDK you already use for analyticsSix @opentelemetry/* packages
InstrumentationManual – you wrap the operations you care aboutManual, plus auto-instrumentation for HTTP, Express, databases, and more
Person and session joinAutomatic inside a PostHog request contextOptional PostHog context processor

Pick OpenTelemetry if you already run it, or if you want spans from your HTTP server and database driver without writing them yourself. Pick posthog-node if PostHog is your only tracing backend and you'd rather instrument a handful of operations by hand than add an exporter pipeline.

Both routes send OTLP spans to the same endpoint, so you can start with one and switch later without losing your traces.

With posthog-node

Minimum version: posthog-node@5.52.0 or later.

  1. Install posthog-node

    Required

    npm install posthog-node --save

  2. Enable tracing

    Required

    Tracing is off until you set the traces option. There's no OpenTelemetry dependency to add.

    JavaScript
    import { PostHog } from 'posthog-node'
    export const posthog = new PostHog('<ph_project_token>', {
    host: 'https://us.i.posthog.com',
    traces: {
    serviceName: 'checkout-api',
    environment: 'production',
    },
    })

    OptionDescription
    serviceNameIdentifies the service in the Tracing UI. Maps to service.name
    serviceVersionRelease version. Maps to service.version
    environmentDeployment environment, e.g. production. Maps to deployment.environment
    resourceAttributesAdditional OpenTelemetry resource attributes

    Use your project token (the same one you use for capturing events), not a personal API key.

    See the Node.js SDK docs for batching, queue, and span-limit options, and beforeSpanSend for scrubbing attributes or dropping spans before they're exported.

  3. Create spans

    Required

    withSpan runs a callback with a span active for its duration and ends the span for you. Spans created inside the callback nest underneath it automatically.

    JavaScript
    await posthog.withSpan('POST /checkout', { kind: 'server' }, async (span) => {
    span.setAttribute('plan', user.plan)
    const order = await posthog.withSpan('create-order', () => createOrder(cart))
    await posthog.withSpan('charge-card', () => stripe.charge(order))
    return order
    })

    If the callback throws or rejects, the span records the exception, its status is set to error, and your original error propagates unchanged.

    Span names should be low-cardinality operation names – GET /users/:id, not GET /users/123. Variable values belong in attributes.

    For work that can't wrap a callback, startSpan returns a span you end yourself. See the Node.js SDK docs for the full span API and for continuing a trace across services with W3C traceparent headers.

  4. Recommended

    Spans created inside a PostHog request context carry posthogDistinctId and sessionId attributes, which is what makes a trace reachable from a person or a Session Replay recording.

    JavaScript
    posthog.withContext({ distinctId: user.id, sessionId }, async () => {
    await posthog.withSpan('POST /checkout', () => processOrder())
    })

    If you use Express, the PostHog middleware sets this up for every request, and reads the X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID headers that tracing_headers sends from the browser.

  5. Flush before the process exits

    Recommended

    Queued spans are exported on an interval, so a short-lived process can exit before they're sent. Both flush() and shutdown() export spans that have already ended.

    JavaScript
    export const handler = async () => {
    await posthog.withSpan('handler', () => doWork())
    await posthog.flush()
    }

    In a serverless handler, call flush() rather than shutdown(): the container is reused across invocations, so shutdown() would throw away the connection pool and the flag cache. Call shutdown() when the process is genuinely exiting.

With OpenTelemetry

  1. Install OpenTelemetry packages

    Required

    For the complete SDK reference, see the OpenTelemetry JavaScript docs.

    Terminal
    npm install @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/sdk-trace-base @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/exporter-trace-otlp-proto

    @opentelemetry/exporter-trace-otlp-proto is the OTLP HTTP/protobuf trace exporter. The similarly named -otlp-http package sends HTTP/JSON and -otlp-grpc sends gRPC, so pick -proto to match this guide.

  2. Get your project token

    Required

    You'll need your PostHog project token to authenticate trace requests. This is the same token you use for capturing events with the PostHog SDK.

    Important: Use your project token which starts with phc_. Do not use a personal API key (which starts with phx_).

    You can find your project token in Project settings.

  3. Configure the SDK

    Required

    Set up the OpenTelemetry SDK to export spans to PostHog over OTLP HTTP.

    JavaScript
    import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'
    import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
    import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto'
    import { resourceFromAttributes } from '@opentelemetry/resources'
    import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'
    const exporter = new OTLPTraceExporter({
    url: 'https://us.i.posthog.com/i/v1/traces',
    headers: {
    Authorization: 'Bearer <ph_project_token>',
    },
    })
    const provider = new NodeTracerProvider({
    resource: resourceFromAttributes({
    [ATTR_SERVICE_NAME]: 'my-service',
    }),
    spanProcessors: [new BatchSpanProcessor(exporter)],
    })
    provider.register()

    Alternatively, configure the exporter with environment variables:

    Terminal
    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://us.i.posthog.com/i/v1/traces"
    OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer <ph_project_token>"
    OTEL_EXPORTER_OTLP_TRACES_PROTOCOL="http/protobuf"
    OTEL_SERVICE_NAME="my-service"

    Note: Pass the full /i/v1/traces path to the traces endpoint. Don't use the base OTEL_EXPORTER_OTLP_ENDPOINT variable, which appends its own /v1/traces.

  4. Create spans

    Required

    Wrap the operations you want to measure in spans, and attach attributes for context.

    JavaScript
    import { trace, SpanStatusCode } from '@opentelemetry/api'
    const tracer = trace.getTracer('my-service')
    function chargeCustomer(customerId) {
    return tracer.startActiveSpan('charge-customer', (span) => {
    try {
    span.setAttribute('customer.id', customerId)
    // ... do work ...
    span.setStatus({ code: SpanStatusCode.OK })
    } catch (err) {
    span.recordException(err)
    span.setStatus({ code: SpanStatusCode.ERROR })
    throw err
    } finally {
    span.end()
    }
    })
    }

    To link spans to people and sessions, add the optional PostHog context processor to your existing provider. Use your existing PostHog client as posthog:

    JavaScript
    import { PostHogContextSpanProcessor } from 'posthog-node/otel'
    const provider = new NodeTracerProvider({
    resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'my-service' }),
    spanProcessors: [
    new PostHogContextSpanProcessor(posthog),
    new BatchSpanProcessor(exporter),
    ],
    })

    Use this configuration in place of the provider configuration above. Keep your existing exporter and sampler. Register the processor before requests start.

    For Express, put setupExpressRequestContext before your routes. It reads the browser tracing headers and adds missing posthogDistinctId and sessionId attributes to the active request span. The processor adds the same attributes to child spans when they start. Configure tracing_headers for your backend hostname in the browser SDK.

    For a custom integration, call enrichCurrentSpan(posthog) from posthog-node/otel after you set the IDs inside posthog.withContext(). The helper is also exported from the main posthog-node entry without loading OpenTelemetry. It requires the registered processor. It does not manage another framework's request lifetime.

    At explicit request entry, the processor also fills missing IDs on the nearest recording server ancestor through parent span objects it has observed. This covers HTTP spans that start before framework middleware. If a sampler drops an intermediate span, that parent chain cannot be verified, so the earlier server span stays unchanged. New recording spans inside the request still receive context.

    Existing sessionId and posthogDistinctId span attributes take precedence. Set authenticated application identity with posthogDistinctId. Missing IDs stay absent. Express context ends when the response finishes or closes. Spans that already started keep their attributes; new spans in detached work do not receive the ended request's IDs. Background jobs need their own context transfer. The processor does not create a session, recording, trace provider, or exporter.

  1. Test your setup

    Checkpoint
    Confirm spans are reaching PostHog

    Whichever route you took:

    1. Run your application and trigger the instrumented code
    2. Open the PostHog Tracing interface
    3. Confirm your spans and traces appear
    View your traces in PostHog
  2. Next steps

    Checkpoint
    What you can do with your traces

    ActionDescription
    Why you need distributed tracingWhat a trace shows you that nothing else does
    Explore tracesRead a trace as a waterfall to see where time goes
    Filter spansNarrow down by service, status, duration, and attributes
    Propagate contextPass trace context across services so spans join the same trace

    View your traces in PostHog

Still have questions?

Was this page useful?