NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #572 most downloaded on PyPI
Integrate PostHog into any python application.
Last release 4 days ago
25 Sep 2026
Ships on a steady schedule
a new release about every 8 days
Nearly every release is documented
notes for 60 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
7 years old
309 releases · first in 2020
One column per quarter.
0b353a7 Keep consumers alive after malformed before_send results — Thanks @marandaneto!
aa00432 Restore exception hooks safely — Thanks @marandaneto!
bfec2b1 Reset MCP background capture state after fork — Thanks @marandaneto!
4bf123e The OpenAI Agents SDK group_id now also maps to $ai_session_id on $ai_trace and span events, so grouped runs show up as sessions in PostHog AI
group_id now also maps to $ai_session_id on $ai_trace and span events, so grouped runs show up as sessions in PostHog AI observability. $ai_group_id is still emitted alongside it. — Thanks @marco-g-pm for your first contribution 🎉!3c9aa59 feat(ai): Prompts.get(..., with_metadata=True) results now include config, the JSON object of model parameters or agent configuration stored w
Prompts.get(..., with_metadata=True) results now include config, the JSON object of model parameters or agent configuration stored with the prompt version in PostHog prompt management (None when the version has none). Config is carried through the client-side cache and the stale-cache fallback. The hardcoded fallback string has no config, so use defensive access like (result.config or {}).get("temperature", 0). — Thanks @jurajmajerik!170f4e2 feat(mcp): emit $mcp_protocol_version on MCP analytics events — the MCP spec version, recovered from the session token across stateless pods (
$mcp_protocol_version on MCP analytics events — the MCP spec version, recovered from the session token across stateless pods (parity with the TypeScript SDK). PostHogMCP capture methods gain a protocol_version argument. — Thanks @gesh for your first contribution 🎉!cdc0825 Preserve Anthropic cache-write TTL breakdowns across Python SDK AI integrations. — Thanks @gouveags!
13de879 Fix module-level settings propagation to the default client — Thanks @marandaneto!
5535ecd fix(errors): emit $exception_list in canonical order — index 0 is the caught/outermost exception, causes follow in unwrap order, and the root
$exception_list in canonical order — index 0 is the caught/outermost exception, causes follow in unwrap order, and the root cause is last (previously the list was reversed with the root cause first). This aligns posthog-python with the cross-SDK exception ordering spec. Frame order within each stacktrace is unchanged. — Thanks @cat-ph!4c8a85a AI capture now records multimodal and structured content (thinking blocks, tool calls, media, and Responses API output items) faithfully acros
37aafd3 feat(mcp): stateless and multi-pod server support — carry $session_id and the client identity (harness) across pods via a self-encoded Mcp-Ses
$session_id and the client identity (harness) across pods via a self-encoded Mcp-Session-Id token minted at initialize and replayed on every request. Auto-wired on the instrument() FastMCP path (stateless_http=True); custom PostHogMCP dispatchers add PostHogMcpStatelessSessionMiddleware and read get_mcp_session(). — Thanks @gesh!f9a163c Refactored capture internals to support multiple delivery lanes per client. Added an internal test lane for heavy AI events.
f9a163c Refactored capture internals to support multiple delivery lanes per client. Added an internal test lane for heavy AI events.
Events captured after shutdown() are now dropped with a warning instead of being silently queued with no consumer to deliver them. — Thanks @carlos-marchal-ph!
2d7f8cc The client.metrics config can now be set through module-level settings: assign posthog.metrics = {"service_name": ..., ...} alongside posthog.
client.metrics config can now be set through module-level settings: assign posthog.metrics = {"service_name": ..., ...} alongside posthog.api_key and the dict is applied when setup() builds the global client. Previously module-configured apps had no way to pass the metrics config, so every series recorded through the global client shipped service.name='unknown_service'. Late assignment (e.g. a Django ready() hook running after an early setup()) still applies on the next setup() call, as long as the metrics API hasn't been used yet. — Thanks @DanielVisca!6766309 Harden the alpha posthog.metrics client based on review follow-ups.
count()/gauge()/histogram() can no longer rewrite an already-recorded series' attributes on the wire.metrics client config (non-dict config or resource_attributes, non-numeric flush_interval, non-integer max_series_per_flush, non-callable before_send) now degrades to defaults with a warning instead of raising from the first client.metrics.count() call, matching the client's no-throw contract. — Thanks @DanielVisca!ca5e883 Clarify the queue-full warning to say the event is being dropped, instead of only reporting that the queue is full. — Thanks @emmayusufu for y
5ef2c23 $feature_flag_called events are now minimized for non-experiment flags when the server enables it. When the /flags v2 response (minimalFlagCal
5ef2c23 $feature_flag_called events are now minimized for non-experiment flags when the server enables it. When the /flags v2 response (minimalFlagCalledEvents) or the local-evaluation payload (minimal_flag_called_events) reports the gate as enabled and the evaluated flag has no linked experiment (has_experiment is false), the event's properties are reduced to a strict allowlist ($feature_flag, $feature_flag_response, $feature_flag_has_experiment, the $feature_flag_* debug scalars, locally_evaluated, $groups, $process_person_profile, $session_id, $lib, $lib_version, $is_server, $geoip_disable, $os, $os_version, $os_distro, $python_runtime, $python_version). Everything else — including super properties and custom event properties — is stripped from those events.
If the server does not report the gate, if the flag's has_experiment signal is missing, or if the flag is linked to an experiment, the full property set is sent unchanged. There is no SDK-side configuration; the gate is controlled per-team by the server. For evaluate_flags() snapshots, the gate is pinned when the snapshot is created, so deferred flag accesses are shaped by the evaluation that produced them.
Custom flag_definition_cache providers now receive an additional minimal_flag_called_events key in the definitions payload, so the gate survives external cache round-trips.
When the server reports has_experiment for a flag, every $feature_flag_called event also carries a $feature_flag_has_experiment boolean property. — Thanks @haacked!
1653bcb Add a label option to Prompts.get() to fetch the prompt version a label (e.g. production) currently points to. Labeled fetches are cached sepa
label option to Prompts.get() to fetch the prompt version a label (e.g. production) currently points to. Labeled fetches are cached separately, and PromptResult carries the resolved label. Requires a PostHog version with prompt labels; older servers ignore the parameter and return the latest version. — Thanks @jurajmajerik!5ab6318 Add the active OpenTelemetry span's $trace_id and $span_id to events captured with capture_exception. — Thanks @hpouillot!
$trace_id and $span_id to events captured with capture_exception. — Thanks @hpouillot!556c134 $feature_flag_called events now carry a $feature_flag_has_experiment boolean property when the server reports whether the flag is linked to an
$feature_flag_called events now carry a $feature_flag_has_experiment boolean property when the server reports whether the flag is linked to an experiment. When the server does not report the signal (older deployments), the property is omitted. — Thanks @haacked!5e42b1e Add the posthog.metrics API (count, gauge, histogram) — alpha.
5e42b1e Add the posthog.metrics API (count, gauge, histogram) — alpha.
Backend services can now record metrics through the same statsd-style pre-aggregating client the browser SDK ships, with no OpenTelemetry setup:
client = Posthog("<ph_project_api_key>", metrics={"service_name": "billing-worker"})
client.metrics.count("invoices.processed", 1, attributes={"plan": "pro"})
client.metrics.gauge("queue.depth", 42)
client.metrics.histogram("job.duration", 187, unit="ms")
Samples aggregate in memory and flush as OTLP/JSON to /i/v1/metrics (one data point per series per window, delta temporality). Pending metrics are flushed on shutdown(); buffered windows are retried on transient failures and dropped loudly after 3 consecutive failed flushes. The metrics client option accepts service_name, service_version, environment, resource_attributes, flush_interval (seconds), max_series_per_flush (cardinality guardrail, default 1000), and a before_send hook. — Thanks @DanielVisca!
eb025c8 Django middleware also sends the request user agent as $raw_user_agent, the standardized property PostHog's server-side classification (e.g. b
$raw_user_agent, the standardized property PostHog's server-side classification (e.g. bot detection) reads — Thanks @lricoy!ae3c4e5 Malformed flag-dependency conditions (missing key, null value, or wrong operator) now evaluate locally as no-match (false), matching the serve
/flags endpoint on every evaluation. 7.22.1 made these conditions fall back to the server, which could massively increase billable /flags request volume for flag definitions containing legacy/malformed dependency conditions. — Thanks @patricio-posthog!4d61b18 Capture pre-calculated total cost from OpenAI Agents Responses API usage. — Thanks @fuchengwarrenzhu for your first contribution 🎉!
650d107 Fix local evaluation of flag dependencies with a flag_evaluates_to: false condition: such conditions never matched, forcing the dependent flag
flag_evaluates_to: false condition: such conditions never matched, forcing the dependent flag to false for every locally-evaluated user. — Thanks @matheus-vb!d459b57 Add an opt-in capture_mode for the Capture V1 ingestion protocol (POST /i/v1/analytics/events). Set capture_mode="v1" on the client (or the PO
d459b57 Add an opt-in capture_mode for the Capture V1 ingestion protocol (POST /i/v1/analytics/events). Set capture_mode="v1" on the client (or the POSTHOG_CAPTURE_MODE=v1 environment variable) to use Bearer auth, per-event results, and partial retry. Defaults to "v0" (the legacy /batch/ endpoint), so existing setups are unaffected.
When using capture_mode="v1", request bodies can be compressed via capture_compression (or POSTHOG_CAPTURE_COMPRESSION): "gzip", "deflate", "zstd" (requires the optional posthog[zstd] extra), or "none" (default). The legacy gzip=True flag is honored as a fallback.
Per-event server verdicts are surfaced through the existing on_error handler: events the backend explicitly drops, or fails to accept after retries, raise a CaptureV1Error carrying the affected event UUIDs — so a rejection is never silently lost, even when the HTTP request itself succeeded. — Thanks @eli-r-ph for your first contribution 🎉!
30c184f Stop duplicating distinct_id inside /flags person properties — Thanks @marandaneto!
c6350b6 Testing release workflow. — Thanks @marandaneto!
cd86110 Fall back to uncompressed uploads when gzip compression fails — Thanks @marandaneto!
888a725 Add posthog.mcp, a Python SDK for PostHog MCP analytics (just pip install posthog; the MCP SDK is a peer dependency of instrument(), not bundl
posthog.mcp, a Python SDK for PostHog MCP analytics (just pip install posthog; the MCP SDK is a peer dependency of instrument(), not bundled). instrument(server, posthog_client) wraps a FastMCP or low-level mcp.server.Server so every tool call, agent intent, tools/list, initialize, and failure is captured to PostHog as a $mcp_* event. Also adds PostHogMCP, a Client subclass for custom dispatchers (needs nothing beyond posthog), plus opt-in context intent capture, identify, report_missing (get_more_tools), and conversation_id. Beta. — Thanks @lucasheriques for your first contribution 🎉!cdd878c Clear feature flag called cache on shutdown — Thanks @marandaneto!
89adb2f Fix internal imports for posthoganalytics mirror — Thanks @hpouillot!
42ff4ca Detect and redact high-entropy secrets (API keys, tokens, passwords) in exception code variables. Adds the code_variables_detect_secrets optio
code_variables_detect_secrets option (default True). — Thanks @ablaszkiewicz!c359f93 Mask sensitive data held inside objects and in URL/DSN credentials when capturing exception code variables. Custom objects are now traversed s
password are redacted by attribute name instead of leaking via repr(), and credentials embedded in connection strings are scrubbed. Adds the code_variables_mask_url_credentials option (default True). — Thanks @ablaszkiewicz!09c8fba Warn on duplicate async PostHog clients and document client lifecycle guidance — Thanks @marandaneto!
bc8e531 Add a default timeout for flushing queued events. — Thanks @marandaneto!
98a305a Increase the default background flush interval to 5 seconds — Thanks @marandaneto!
8d416ae Add missing return type annotations to improve typing coverage without changing runtime behavior. — Thanks @miachillgood for your first contri
b9f3208 Add opt-in client-side rate limiting for exception autocapture, using the same token bucket algorithm as the posthog-js and posthog-node SDKs:
enable_exception_autocapture_rate_limiting client option and tune via exception_autocapture_bucket_size (default 50), exception_autocapture_refill_rate (default 10), and exception_autocapture_refill_interval_seconds (default 10). — Thanks @hpouillot!ee6a3c8 Warn when an AI wrapper's base_url points at the PostHog AI Gateway. The gateway emits its own $ai_generation, so each call would be captured
base_url points at the PostHog AI Gateway. The gateway emits its own $ai_generation, so each call would be captured (and billed) twice. The wrapper only warns and never drops the event. Detection covers the wrapper funnels (OpenAI, Anthropic, LangChain) and the OTel span path. — Thanks @richardsolomou!fe76fc9 Improve mypy coverage for core SDK modules without changing runtime behavior. — Thanks @Kshitijmishradev for your first contribution 🎉!
00b2091 Add internal-only routing of $ai_* events to a dedicated capture endpoint in their own batch, gated behind the unstable _dedicated_ai_endpoint
$ai_* events to a dedicated capture endpoint in their own batch, gated behind the unstable _dedicated_ai_endpoint client option (off by default, not for general use). — Thanks @carlos-marchal-ph!a2ce51e feat(feature-flags): support the early_exit condition option in local evaluation. When a flag enables early exit, evaluation now stops and ret
early_exit condition option in local evaluation. When a flag enables early exit, evaluation now stops and returns False as soon as a condition group's property filters match but the rollout percentage excludes the user, instead of falling through to later groups — matching the server-side evaluation behavior. — Thanks @gustavohstrassburger!3aed638 Add a configurable $is_server event property (default true) so PostHog can identify server-side events. Set is_server=False when using posthog
$is_server event property (default true) so PostHog can identify server-side events. Set is_server=False when using posthog-python as a client/CLI so the device OS is attributed normally. — Thanks @turnipdabeets for your first contribution 🎉!44e6b14 Fix async streaming responses from the AI wrappers (OpenAI, Anthropic, Gemini) so they support async with as well as async for. Previously, co
async with as well as async for. Previously, consuming a stream via async with (e.g. with pydantic-ai) raised TypeError: 'async_generator' object does not support the asynchronous context manager protocol. — Thanks @turnipdabeets for your first contribution 🎉!643a810 Return empty flag defaults from Client flag helpers when the flags API fails. — Thanks @marandaneto!
034dce2 Make module-level setup no-op when API key is blank — Thanks @marandaneto!
8f6d6c8 Include group context in the $feature_flag_called dedupe key so group-scoped flags fire a separate event for each group a user is evaluated un
$feature_flag_called dedupe key so group-scoped flags fire a separate event for each group a user is evaluated under, instead of being dedup-ed against the first group context the same (distinct_id, flag, response) was seen under. — Thanks @gustavohstrassburger!a44e0be Add async flag definition cache providers — Thanks @dustinbyrne!
0207088 Track OpenAI chat completions parse calls — Thanks @marandaneto!
be9b78b Reject semver values with leading zeros in local flag evaluation. Per semver 2.0.0 §2, numeric identifiers must not include leading zeros — va
1.07.3 are not valid semver and should not match targeting conditions. Both override values and flag values are now validated; invalid inputs raise InconclusiveMatchError so the condition does not match. — Thanks @dmarticus!1574b1b Fix OpenAI usage parsing when token detail fields are null — Thanks @michael-ciridae!
a098aa7 Fix Gemini web search extraction when response candidates are null. — Thanks @marandaneto!
52cd20e feat: add Celery integration and improve PostHog client fork safety — Thanks @parinporecha!
44c1261 Fix scoped context support for async functions — Thanks @marandaneto!
f6c8ede fix: type warning on new_context — Thanks @itsaphel for your first contribution 🎉!
chore: remove unsupported setup-uv input by @marandaneto in https://github.com/PostHog/posthog-python/pull/545
Full Changelog: https://github.com/PostHog/posthog-python/compare/7.13.2...7.14.0
69dc2a8 Add evaluate_flags() and a new flags option on capture() so a single /flags call can power both flag branching and event enrichment per request:
flags = posthog.evaluate_flags(distinct_id, person_properties={"plan": "enterprise"})
if flags.is_enabled("new-dashboard"):
render_new_dashboard()
posthog.capture("page_viewed", distinct_id=distinct_id, flags=flags)
The returned FeatureFlagEvaluations snapshot exposes is_enabled(), get_flag(), get_flag_payload() for branching and only_accessed() / only([keys]) filter helpers. Pass flag_keys=[...] to evaluate_flags() to scope the underlying /flags request itself.
Deprecates feature_enabled(), get_feature_flag(), get_feature_flag_payload(), and capture(send_feature_flags=...). They continue to work but now emit a DeprecationWarning pointing at evaluate_flags(). Removal is planned for the next major version. — Thanks @dmarticus!
chore(deps): bump litellm from 1.83.0 to 1.83.7 in /examples/example-ai-dspy by @dependabot[bot] in https://github.com/PostHog/posthog-python/pull/536
Full Changelog: https://github.com/PostHog/posthog-python/compare/7.13.1...7.13.2
chore(deps): bump python-dotenv from 1.1.1 to 1.2.2 by @dependabot[bot] in https://github.com/PostHog/posthog-python/pull/528
Full Changelog: https://github.com/PostHog/posthog-python/compare/7.13.0...7.13.1
chore: use engineering@posthog.com in package metadata by @marandaneto in https://github.com/PostHog/posthog-python/pull/514
Full Changelog: https://github.com/PostHog/posthog-python/compare/v7.12.0...7.13.0
capture_errors option to Prompts that reports prompt fetch failures to PostHog error tracking via capture_exception() when enabled. — Thanks @andrewm4894!chore: merge master into main by @marandaneto in https://github.com/PostHog/posthog-python/pull/513
Full Changelog: https://github.com/PostHog/posthog-python/compare/v7.11.2...v7.12.0
Prompts.get() now accepts with_metadata=True and returns a PromptResult dataclass containing source (api, cache, stale_cache, or code_fallback), name, and version alongside the prompt text. The previous plain-string return is deprecated and will be removed in a future major version. — Thanks @marandaneto!chore: add GitHub community health files by @marandaneto in https://github.com/PostHog/posthog-python/pull/505
Full Changelog: https://github.com/PostHog/posthog-python/compare/v7.11.1...v7.11.2
/api/feature_flag/local_evaluation to /flags/definitions — Thanks @patricio-posthog!Your coding agent can read these notes before it upgrades. Set up the MCP server →