Subchapter 93.10
references/python.mdMarkdown11 KBView on GitHub
AI agents: this is one page from PostHog’s docs. Full index of Markdown docs for LLMs: https://posthog.com/llms.txt (opens in a new tab)
Canonical URL: https://posthog.com/docs/distributed-tracing/installation/python (opens in a new tab)
Cloud region: This page uses US Cloud hosts. For an EU Cloud project, use
https://eu.i.posthog.cominstead ofhttps://us.i.posthog.com(ingestion host), and usehttps://eu.posthog.cominstead ofhttps://app.posthog.com(app host for links).
There are two ways to send spans from Python.
posthog | OpenTelemetry | |
|---|---|---|
| Packages | The SDK you already use for analytics | opentelemetry-sdk and an OTLP exporter |
| Instrumentation | Manual – you wrap the operations you care about | Manual, plus auto-instrumentation for Django, Flask, FastAPI, databases, and more |
| Person and session join | Automatic inside a PostHog context that identifies them | Optional PostHog context processor |
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.
Minimum version:
posthog7.58.0 or later.
1
Required
pip install "posthog>=7.58.0"2
Required
Tracing is off until you set the traces option. There’s no OpenTelemetry dependency to add.
from posthog import Posthog
posthog = Posthog(
"<ph_project_token>",
host="https://us.i.posthog.com",
traces={
"service_name": "checkout-api",
"environment": "production",
},
)| Option | Description |
|---|---|
service_name | Identifies the service in the Tracing UI. Maps to service.name |
service_version | Release version. Maps to service.version |
environment | Deployment environment, e.g. production. Maps to deployment.environment |
resource_attributes | Additional OpenTelemetry resource attributes |
Use your project token (the same one you use for capturing events), not a personal API key (opens in a new tab). Tracing works with the synchronous Posthog client and the module-level API, not AsyncPosthog.
See the Python SDK docs (opens in a new tab) for batching, queue, and span-limit options, and before_span_send (opens in a new tab) for scrubbing attributes or dropping spans before they’re exported.
3
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.
with posthog.start_span("POST /checkout", kind="server") as span:
span.set_attribute("plan", user.plan)
with posthog.start_span("create-order"):
order
4
Recommended
Spans created inside a PostHog context (opens in a new tab) 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.
from posthog import new_context, identify_context, set_context_session
with new_context():
identify_context(user.id)
set_context_session(session_id)
5
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.
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.
1
Required
For the complete SDK reference, see the OpenTelemetry Python docs (opens in a new tab).
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-httpopentelemetry-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
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 withphx_).
You can find your project token in Project settings (opens in a new tab).
3
Required
Set up the OpenTelemetry SDK to export spans to PostHog over OTLP HTTP.
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export
4
Required
Wrap the operations you want to measure in spans, and attach attributes for context.
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"
Checkpoint
Confirm spans are reaching PostHog
Whichever route you took:
Checkpoint
What you can do with your traces
| Action | Description |
|---|---|
| Why you need distributed tracing (opens in a new tab) | What a trace shows you that nothing else does |
| Explore traces | Read a trace as a waterfall to see where time goes |
| Filter spans | Narrow down by service, status, duration, and attributes |
| Propagate context | Pass trace context across services so spans join the same trace |
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 (opens in a new tab) for the full span API and for continuing a trace across services with W3C traceparent headers.
If you use Django, the contexts middleware (opens in a new tab) sets this up for every request, and reads the X-POSTHOG-DISTINCT-ID and X-POSTHOG-SESSION-ID headers that tracing_headers (opens in a new tab) sends from the browser.
The .http. segment in the import path is what selects the HTTP/protobuf exporter.
Alternatively, configure the exporter with environment variables:
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/tracespath to the traces endpoint. Don’t use the baseOTEL_EXPORTER_OTLP_ENDPOINTvariable, which appends its own/v1/traces.
To link spans to people and sessions, add the optional PostHog context processor to your existing provider before requests start:
from posthog.integrations.otel import PostHogContextSpanProcessor
provider.add_span_processor(PostHogContextSpanProcessor())Keep your existing provider, exporter, and sampler. This integration supports OpenTelemetry SDK 1.20.0 or later. The processor adds missing posthogDistinctId and sessionId attributes from the active PostHog context when each span starts. Explicit attributes take precedence. If the span has too little attribute capacity, enrichment is skipped to preserve its existing attributes.
The PostHog Django, Flask, and ASGI integrations also enrich an active request span after they read the request context. Put the OpenTelemetry request middleware outside the PostHog middleware. Configure tracing_headers (opens in a new tab) in the browser SDK. The PostHog framework integrations (opens in a new tab) capture these headers by default with Python SDK 7.71.0 or later. Set capture_tracing_headers=False, or POSTHOG_MW_CAPTURE_TRACING_HEADERS = False in Django, to disable extraction. These headers are untrusted analytics context.
For Celery, initialize the PostHog Celery integration before OpenTelemetry Celery instrumentation. This order also preserves task context for eager or nested execution.
For a custom integration, call enrich_current_span() from posthog.integrations.otel after you set the request IDs. The processor handles child spans started later in that context.
Missing IDs stay absent. Spans that already started keep their attributes after the context ends. New spans in detached tasks do not receive the ended context’s IDs. Set explicit IDs in a new new_context() scope for background work. The bridge does not transfer context to another process or thread. Error-response spans created outside the PostHog middleware scope, including outer FastAPI error handling, can omit request IDs.