Python tracing installation

Contents

There are two ways to send spans from Python.

posthogOpenTelemetry
PackagesThe SDK you already use for analyticsopentelemetry-sdk and an OTLP exporter
InstrumentationManual – you wrap the operations you care aboutManual, plus auto-instrumentation for Django, Flask, FastAPI, databases, and more
Person and session joinAutomatic inside a PostHog context that identifies themSet the attributes yourself

Pick OpenTelemetry if you already run it, if you use asyncio with AsyncPosthog, or if you want spans from your web framework and database driver without writing them yourself. Pick posthog 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

Minimum version: posthog 7.58.0 or later.

  1. Install posthog

    Required
    Terminal
    pip install "posthog>=7.58.0"
  2. Enable tracing

    Required

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

    Python
    from posthog import Posthog
    posthog = Posthog(
    "<ph_project_token>",
    host="https://us.i.posthog.com",
    traces={
    "service_name": "checkout-api",
    "environment": "production",
    },
    )

    OptionDescription
    service_nameIdentifies the service in the Tracing UI. Maps to service.name
    service_versionRelease version. Maps to service.version
    environmentDeployment environment, e.g. production. Maps to deployment.environment
    resource_attributesAdditional OpenTelemetry resource attributes

    Use your project token (the same one you use for capturing events), not a personal API key. Tracing works with the synchronous Posthog client and the module-level API, not AsyncPosthog.

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

  3. Create spans with posthog

    Required

    start_span used in a with block makes the span active for the block and ends it when the block exits. Spans started inside the block nest underneath it automatically.

    Python
    with posthog.start_span("POST /checkout", kind="server") as span:
    span.set_attribute("plan", user.plan)
    with posthog.start_span("create-order"):
    order = create_order(cart)
    with posthog.start_span("charge-card"):
    stripe.charge(order)

    If an exception escapes the block, the span records it, its status is set to error, and the exception 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 block, call start_span without with and call end() yourself. See the Python 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 context that has a distinct ID or session ID carry posthogDistinctId and sessionId attributes, which is what makes a trace reachable from a person or a Session Replay recording.

    Python
    from posthog import new_context, identify_context, set_context_session
    with new_context():
    identify_context(user.id)
    set_context_session(session_id)
    with posthog.start_span("POST /checkout"):
    process_order()

    If you use Django, the contexts 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, even with sync_mode on, so a short-lived process can exit before they're sent. Both flush() and shutdown() export spans that have already ended.

    Python
    def handler(event, context):
    with posthog.start_span("handler"):
    do_work()
    posthog.flush()

    In a serverless handler, call flush() before returning. Call shutdown() when the process is genuinely exiting.

With OpenTelemetry

  1. Install OpenTelemetry packages

    Required

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

    Terminal
    pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http

    opentelemetry-exporter-otlp-proto-http is the OTLP HTTP/protobuf trace exporter. The -proto-grpc variant sends gRPC, so pick -proto-http 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.

    Python
    from opentelemetry import trace
    from opentelemetry.sdk.resources import Resource, SERVICE_NAME
    from opentelemetry.sdk.trace import TracerProvider
    from opentelemetry.sdk.trace.export import BatchSpanProcessor
    from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
    resource = Resource.create({SERVICE_NAME: "my-service"})
    provider = TracerProvider(resource=resource)
    exporter = OTLPSpanExporter(
    endpoint="https://us.i.posthog.com/i/v1/traces",
    headers={"Authorization": "Bearer <ph_project_token>"},
    )
    provider.add_span_processor(BatchSpanProcessor(exporter))
    trace.set_tracer_provider(provider)

    The .http. segment in the import path is what selects the HTTP/protobuf exporter.

    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_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.

    Python
    tracer = trace.get_tracer("my-service")
    def checkout(order_id, amount):
    with tracer.start_as_current_span("checkout") as span:
    span.set_attribute("order.id", order_id)
    span.set_attribute("order.amount", amount)
    # ... do work ...
    return "confirmed"

    To join these spans to a person or a Session Replay recording, set posthogDistinctId and sessionId attributes yourself, from the X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID headers that tracing_headers sends from the browser.

  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?