NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #497 most downloaded on PyPI
Integrate PostHog into any python application.
Last release 2 days ago
02 Oct 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
314 releases · first in 2020
One column per quarter.
053e734 Keep streamed Anthropic tool call arguments when the response also contains server tool blocks, such as web search. — Thanks @breken-ai !
max_output_tokens is reached. — Thanks @breken-ai!3eb3861 Add conversation and session correlation to custom PostHogMCP dispatchers, matching @posthog/mcp . prepare_tool_list() adds an optional conver
PostHogMCP dispatchers, matching @posthog/mcp. prepare_tool_list() adds an optional conversation_id field to each compatible tool input schema and a compatible _mcp_instructions output field. prepare_tool_call() accepts a carried session_id. The new prepare_tool_result() delivers a minted handle without changing the original result. Tool and report capture methods accept conversation_id. Existing dispatchers must call prepare_tool_result() to deliver new handles. Set PostHogMCP(enable_conversation_id=False) to keep the previous behavior. — Thanks @gesh!bcf9d34 $mcp_tools_list events no longer copy the tool descriptors into $mcp_response , which keeps only the response envelope such as nextCursor . Th
$mcp_tools_list events no longer copy the tool descriptors into $mcp_response, which keeps only the response envelope such as nextCursor. The tool names stay in $mcp_listed_tool_names. — Thanks @lucasheriques!299906b Add optional MCP server build metadata. Set server_build to record an immutable deployment identifier as $mcp_server_build on MCP events from
3f94e80 Code variable masking now searches strings of up to 2,048 characters for known credential formats, not only strings of up to 200 characters. A
$$_posthog_redacted_key_<n>_$$ placeholder when it matches a mask pattern but is not a plain field name, when it looks like a secret, when a non-string key holds a part that masking redacts or can't check, when its text can't be read, or when it is too long to scan. URL credentials are removed from string keys, and keys that end up with the same text keep separate entries. — Thanks @ablaszkiewicz!b804ea1 Keep MCP conversation sessions consistent across user messages. — Thanks @gesh !
21dc4dc Read a feature flag's evaluation runtime from the SDK: get_feature_flag_evaluation_runtime(key) returns the FeatureFlagEvaluationRuntime on a
get_feature_flag_evaluation_runtime(key) returns the FeatureFlagEvaluationRuntime on a locally loaded flag definition, and get_feature_flag_keys_by_evaluation_runtime(runtime) lists the keys a given runtime can evaluate. The local-evaluation payload already carried the value, but only on the untyped client.feature_flags dicts, so a backend that serves flags to its own frontend had to call /api/feature_flag/local_evaluation itself to see it. — Thanks @PostHog[bot]!edd5997 Return None for malformed, empty, or whitespace-only serialized JSON feature flag payloads instead of returning the raw string or raising a JS
get_all_flags_and_payloads, get_feature_flags_and_payloads, and get_feature_payloads) validate payloads but preserve valid serialized JSON for compatibility with callers using json.loads(). Single-flag and evaluate_flags() snapshot payload getters continue returning decoded values, including JSON strings (such as ""), false, and zero. Reject non-JSON constants such as NaN and Infinity and isolate decoder-limit failures so flag values and healthy sibling payloads remain available. — Thanks @marandaneto!9c84daa Honor filters.holdout during local feature flag evaluation. A user in an experiment holdout now receives the holdout-<id> variant instead of b
filters.holdout during local feature flag evaluation. A user in an experiment holdout now receives the holdout-<id> variant instead of being bucketed into a regular variant, matching how the server evaluates the same flag. Holdout membership is resolved before release conditions, so a held-out user never reaches the flag's targeting. — Thanks @rubychilds for your first contribution 🎉!e6e44b9 Read the release id from the POSTHOG_RELEASE_ID environment variable and send it as $release_id on every event. On $exception events, error tr
POSTHOG_RELEASE_ID environment variable and send it as $release_id on every event. On $exception events, error tracking uses it to link the exception to its release by a direct id lookup. Create the release and get its id with posthog-cli release resolve. An explicit $release_id in the event properties or in super_properties wins over the environment variable. Minimal $feature_flag_called events keep their strict property allowlist and do not carry it. — Thanks @ablaszkiewicz!property_matching_version during local feature flag evaluation, including person, group, cohort, and flag dependency conditions. Version 2 uses explicit boolean equality; missing/1 retains legacy truthiness. Preserve the selector through definition caches and invalidate evaluated results on version-only refreshes. Bind Client-managed Redis results to their definitions snapshot so invalidated entries cannot revive after a worker restart. Older entries without snapshot metadata become cache misses for these clients. — Thanks @marandaneto!7abd976 Add distributed tracing (alpha): start_span() and get_active_span() record spans and export them to PostHog as OTLP, with no OpenTelemetry dep
start_span() and get_active_span() record spans and export them to PostHog as OTLP, with no OpenTelemetry dependency, when the new traces client option is set. — Thanks @turnipdabeets!f69e670 After a failed prompt refetch, the SDK now serves the stale cached prompt for a cooldown period (60 seconds by default) instead of retrying th
prompts.get() call. When the failure is a 429, the cooldown follows the Retry-After the server sends, capped at one hour. This keeps a rate-limited client from holding itself against the limit. Matches the behavior the JavaScript SDK already has. — Thanks @jurajmajerik!8ee7ad1 Enable MCP model capture and conversation correlation by default. Advertised tool schemas gain an llm_model argument (never enforced at dispat
8ee7ad1 Enable MCP model capture and conversation correlation by default. Advertised tool schemas gain an llm_model argument (never enforced at dispatch) and eligible tool results gain a conversation handle; MCPAnalyticsOptions(capture_model=False, enable_conversation_id=False) restores the previous shape. Fresh low-level instances now read the self-reported model instead of staying silent.
Standalone FastMCP on MCP SDK 1.x skips llm_model injection when application middleware can change tool listing or dispatch. Model metadata capture remains enabled; this prevents cold replicas from rejecting injected arguments and preserves replacement tools' own arguments. — Thanks @lucasheriques!
d0ecc9f prompts.get_all() now works without a label. It fetches the latest version of every prompt in one request and warms the cache for plain prompt
prompts.get_all() now works without a label. It fetches the latest version of every prompt in one request and warms the cache for plain prompts.get(name) calls. Previously the label was required, and passing label=None sent the literal string "None" as the label filter, returning an empty result. — Thanks @jurajmajerik!79fcb12 MCP virtual tools now use conversation IDs when enable_conversation_id is enabled. — Thanks @gesh !
ed02588 MCP analytics now adds its virtual tools ( get_more_tools , send_feedback ) to the first tools/list page only, rather than to every page and t
ed02588 MCP analytics now adds its virtual tools (get_more_tools, send_feedback) to the first tools/list page only, rather than to every page and to the last page respectively.
If one of your own tools already uses a virtual tool's name, PostHog warns and names the option that renames its own — missing_capability_tool_name, or collect_feedback's tool_name. Warnings also reach the posthog.mcp logger, so you see them without setting the logger option. — Thanks @gesh!
f07d67c Add LangChain v1 agent middleware for AI observability. — Thanks @gouveags for your first contribution 🎉!
e9f6b67 The Gemini adapter now covers two surfaces of genai.Client it previously lacked. Client.aio.models reaches the tracked async models adapter, s
genai.Client it previously lacked. Client.aio.models reaches the tracked async models adapter, so await client.aio.models.generate_content(...) works without swapping the class out for AsyncClient, and client.files (plus client.aio.files, and AsyncClient.files for the async Files API) passes through to the provider, so multimodal flows that upload a file before referencing it in contents no longer fail. Every surface of one client shares a single provider client instead of opening its own. — Thanks @DanielTobi0!9d965dd Fix missing MCP analytics events with standalone FastMCP 4 while preserving tool arguments and compatibility with MCP SDK v1. — Thanks @lucash
c3d3c96 Capture Gemini thought summaries as thinking content blocks. When a request enables thinking_config.include_thoughts , parts marked thought=Tr
thinking content blocks. When a request enables thinking_config.include_thoughts, parts marked thought=True in responses, inputs, and streaming chunks are now formatted as {"type": "thinking", "thinking": ...} (matching the Anthropic thinking-block shape PostHog renders as reasoning) instead of plain text blocks. — Thanks @fivestarspicy!collect_feedback option to MCP analytics. It injects a send_feedback virtual tool and captures every call as a $mcp_feedback event, so agents can report a missing capability, a tool problem, or praise.on_feedback handler that routes reports to a real backend. PostHogMCP gains the same option plus capture_feedback for custom dispatchers. — Thanks @gesh!56a4959 Prompts.get_all now fails loudly in two cases it previously papered over: a server that ignores the label filter but happens to have some labe
Prompts.get_all now fails loudly in two cases it previously papered over: a server that ignores the label filter but happens to have some labels on latest versions no longer produces a silently incomplete result, and a malformed row in the list response now raises the invalid-response error instead of being skipped. A rejected batch also no longer leaves partially cached prompts. — Thanks @jurajmajerik!1cc513f Follow async capture redirects without duplicating or incorrectly retaining a configured host path prefix. — Thanks @nickita-khylkouski !
97ea5fd Capture MCP resource discovery and reads from instrumented servers. URL credential redaction (userinfo, credential-named query and fragment pa
$mcp_tool_call parameters, responses and error messages, so URLs in existing tool-call data will show %5Bredacted%5D values after upgrading. — Thanks @lucasheriques!7db5105 Prompts.get_all(label="production") fetches every prompt that carries a label in one request and stores them in the prompt cache, so later get
Prompts.get_all(label="production") fetches every prompt that carries a label in one request and stores them in the prompt cache, so later get(name, label=...) calls are cache hits. Apps with many prompts no longer need one request per prompt per cache cycle. Against a PostHog server that does not support labels on the prompt list endpoint yet, the call fails with a clear error instead of caching wrong versions. — Thanks @jurajmajerik!e85647b OpenAI and LangChain generations now also emit the served service tier as the explicit $ai_service_tier event property, next to the copy insid
$ai_service_tier event property, next to the copy inside $ai_model_parameters. Cost processing prices tiered calls only from the explicit property, whose writers assert response-derived values. — Thanks @bernatixer!b8f8253 Capture MCP model identifiers from client metadata or an SDK-owned self-report field. Model capture remains opt-in and preserves application-o
b8f8253 Capture MCP model identifiers from client metadata or an SDK-owned self-report field.
Model capture remains opt-in and preserves application-owned fields across repeated tool listings.
MCP context and conversation-ID injection now preserve additionalProperties: false in tool schemas, including when model capture is disabled. Servers that validate these schemas now reject undeclared arguments that earlier SDK versions allowed. Declared analytics fields remain valid. — Thanks @lucasheriques!
09a8c4e Only terminal Responses API statuses become $ai_stop_reason : a queued or in-progress background run no longer records a lifecycle state as it
$ai_stop_reason: a queued or in-progress background run no longer records a lifecycle state as its stop reason, and an incomplete run is named by what cut it short (incomplete_details.reason, e.g. max_output_tokens). Streaming runs that end incomplete or failed now carry a stop reason too, and the LangChain callback reads stop reasons from response_metadata as well, covering Responses API and Anthropic runs that previously recorded none. — Thanks @bernatixer!80c541d Honor HTTP-date Retry-After headers in asynchronous requests while preserving the existing retry backoff and delay cap. — Thanks @Bortlesboat
db8ecb8 Report the Python package version in MCP event metadata and request headers so SDK Health can assess the installed package. — Thanks @marandan
d4a8f0a OpenAI generations now record the service tier the provider served ( service_tier inside $ai_model_parameters ), on non-streaming, streaming,
service_tier inside $ai_model_parameters), on non-streaming, streaming, and LangChain capture paths. LLM analytics uses it to price flex and priority calls at their real rates instead of standard; a requested tier can be refused, so the value always comes from the response. — Thanks @bernatixer!4ab555d Label MCP events and requests as posthog-python-mcp , and AI events as posthog-ai — Thanks @marandaneto !
posthog-python-mcp, and AI events as posthog-ai — Thanks @marandaneto!86ad9e7 Streaming generations interrupted before the provider reported any usage no longer send zero $ai_cache_read_input_tokens , $ai_cache_creation_
$ai_cache_read_input_tokens, $ai_cache_creation_input_tokens, or $ai_reasoning_tokens. A fabricated 0 reads as a report of nothing, so cost processing priced an unknown generation as a known $0.00 instead of leaving it unknown. Streams whose usage was reported keep the historical zero defaults. — Thanks @bernatixer!caf9030 MCP tool failures now report the exception the tool actually raised on $mcp_error_message and $mcp_error_type , stepping past the SDK's dispat
$mcp_error_message and $mcp_error_type, stepping past the SDK's dispatch ToolError wrapper. mcp 2.1 masks the original message out of that wrapper, which left the failures view with only Error executing tool <name>. The $exception sibling still carries the full chain. — Thanks @bernatixer!9444ec5 Omit $ai_input_tokens and $ai_output_tokens when the provider never reported usage, instead of sending 0 , so an interrupted stream no longer
$ai_input_tokens and $ai_output_tokens when the provider never reported usage, instead of sending 0, so an interrupted stream no longer looks like a free call. A zero reported by the provider is still sent, and zero keeps meaning a real report of nothing. Covers the OpenAI, Anthropic, Gemini, LangChain, OpenAI Agents and Claude Agent SDK integrations. — Thanks @bernatixer!131cc1a Match local feature flag string operators using the flags service's boolean coercion, JSON stringification, and casing rules. — Thanks @marand
f50f333 Add non-blocking feature flag evaluation and remote config APIs to AsyncPosthog — Thanks @marandaneto !
fc7e043 Honor default_cache_ttl_seconds=0 in AI prompts so callers can disable default prompt caching. — Thanks @ckarnell for your first contribution
0e70f0c Align local is_set and is_not_set evaluation with partial property context. — Thanks @marandaneto !
is_set and is_not_set evaluation with partial property context. — Thanks @marandaneto!9a1d137 Add an opt-in capture_trace_context client option. When enabled, and a valid OpenTelemetry span is active at capture time, its trace and span
capture_trace_context client option. When enabled, and a valid OpenTelemetry span is active at capture time, its trace and span IDs are attached to events captured with capture() and capture_ai() as $trace_id and $span_id, so they can be correlated with backend traces. Disabled by default, and explicit $trace_id/$span_id properties take precedence. — Thanks @DanielVisca!8046114 Return an empty feature flag snapshot without evaluation when feature flag keys are explicitly empty. — Thanks @marandaneto !
35220f3 Fall back to remote evaluation when a requested flag is missing from local definitions. This changes the previous behavior where the key was o
c55c9b2 MCP analytics now surfaces the previously-silent case where the stateless session mint middleware ( PostHogMcpStatelessSessionMiddleware ) nev
PostHogMcpStatelessSessionMiddleware) never attached — the trap where an ASGI app is built or mounted before instrument() runs, so autowiring can't retrofit it and every session falls back to a fragmented per-process id. instrument() warns when streamable_http_app() was already called before it ran, and a one-time warning fires the first time a tool call arrives over streamable HTTP and the session still has to come from process memory. Both go to the posthog.mcp standard-library logger as well as the MCPAnalyticsOptions(logger=...) sink, so they are visible without opting in — silence them with logging.getLogger("posthog.mcp").setLevel(logging.ERROR). Neither fires for stdio, a correctly-wired server, a conversation-anchored session, or the SSE transport (which the mint cannot fix). Documented in the new posthog/mcp/README.md. — Thanks @PostHog[bot]!f483bab feat(mcp): capture $mcp_client_user_agent and $mcp_vendor_client so MCP usage can be attributed to a product surface. clientInfo.name only say
$mcp_client_user_agent and $mcp_vendor_client so MCP usage can be attributed to a product surface. clientInfo.name only says which client library is calling — Anthropic reports claude-code from the CLI, the Agent SDK, the VS Code extension and the desktop app alike — so $mcp_client_name collapses every surface into one bucket and the harness breakdown reads 100% "Other" for Python-backed servers. The distinguishing detail lives in the User-Agent parenthetical (claude-code/2.1.0 (cli) vs (sdk-ts)) and in vendor headers like x-anthropic-client. Both are captured raw and classified at query time, so labels can improve without an SDK release. HTTP transports only: stdio and in-memory servers carry no headers and their events are unchanged. Custom dispatchers pass their own via new client_user_agent / vendor_client arguments on every PostHogMCP.capture_* method. Parity with @posthog/mcp. — Thanks @gesh!2863909 feat(mcp): emit $mcp_error_message and $mcp_error_type on failed MCP events. The reason a tool call failed previously lived only on the siblin
$mcp_error_message and $mcp_error_type on failed MCP events. The reason a tool call failed previously lived only on the sibling $exception event, so PostHog's failures view — which reads the scalars off the primary event — showed empty error rows for every Python-backed MCP server, and switching off enable_exception_autocapture removed the reason entirely. Both values are read from the same $exception_list the sibling carries, so the two surfaces can never disagree, and the message inherits the existing 2048-character cap. PostHogMCP.capture_tool_call() and capture_tools_list() take a new optional error_type for custom dispatchers that want a coarse category ("validation", "timeout") instead of the thrown class name. Exception messages are also redacted before they leave — previously nothing sanitized the error payload, so the $exception sibling had been shipping them raw. Credential-looking words go through the SDK's own detector (entropy, known key formats, PEM markers), per word, so a message like auth failed for sk-... keeps its diagnostic text and loses only the key. Parity with @posthog/mcp, which sanitizes exception values the same way. — Thanks @gesh!b0ab12c feat(mcp): support MCP Python SDK v2 and bring posthog.mcp to parity with the TypeScript SDK ( @posthog/mcp ). Most of this reaches SDK 1.x se
b0ab12c feat(mcp): support MCP Python SDK v2 and bring posthog.mcp to parity with the TypeScript SDK (@posthog/mcp). Most of this reaches SDK 1.x servers too — the parity work is not v2-only.
MCP SDK v2 / spec 2026-07-28. instrument() now wraps mcp.server.mcpserver.MCPServer (the renamed FastMCP) and the v2 low-level Server (constructor-injected handlers, string-keyed registry, late add_request_handler registrations included), capturing tool calls, tools/list, errors, intent, client identity, and $mcp_protocol_version on both protocol eras — the legacy handshake and the stateless 2026-07-28 envelope, decided per request. Previously instrument() raised ImportError on mcp>=2 and took the host application down with it; it now degrades to a logged no-op on any unsupported or unrecognized SDK.
Cross-SDK parity (SDK 1.x and 2.x alike). Conversation-anchored sessions land as the cross-pod correlation the stateless era needs: with enable_conversation_id, $session_id derives deterministically from the agent-echoed conversation_id (new export derive_session_id_from_conversation, byte-compatible with @posthog/mcp). Only a handle the SDK could have minted (a uuidv7) anchors a session, so two callers inventing the same id can no longer be merged. The handle is delivered over both channels a tool result has — a content text block carrying it as plain JSON data on the minting response (an imperative server sentence inside a tool result is prompt-injection-shaped, and a client that strips it silently breaks the feature), and an _mcp_instructions key declared on the tool's output schema and mirrored into structuredContent on every response. That second channel is what makes the feature work at all for tools with structured output: clients that read structuredContent never render content, so the agent had no handle to echo (0% echo rate measured against Claude Code before the mirror). The prompt-back now rides errored results too, so a failure on a conversation's first call doesn't split the retry into a new session. The session is resolved only once the handle's fate is known, so the call that mints a handle joins the same session as the calls that echo it — while a handle that could not be delivered anchors nothing, rather than stranding events in a conversation nobody holds. Host callbacks (identify, intent_fallback, event_properties) receive the SDK's own per-request context as extra["ctx"] identically on both majors, with a new exported get_request_headers(extra) to read HTTP headers off it — the underlying shape differs per major, and a hand-rolled read that works on one silently returns nothing on the other, sending every event out anonymous.
Fixes affecting existing SDK 1.x users. Analytics could break a tool call in three ways, each now fixed and regression-tested: the SDK's tool cache is rebuilt from an internal listing pass we skipped injecting on, so after any call to an unlisted tool name a strict schema rejected either the analytics parameters we advertise (Input validation error) or the conversation key we write (Output validation error); the conversation handle was written into the caller's result object in place, so a tool returning a shared or cached result served one conversation's handle to every later caller; and on jlowin's FastMCP the advertised schema marked context required while the adapter strips it before validation, failing every call under strict_input_validation=True. Two behavioural changes come with the parity work: an invented (non-uuidv7) conversation_id echo is replaced with a fresh handle rather than trusted, and minted prompt-backs are now appended to errored results. — Thanks @gesh!
1adf542 Drop events when before_send callbacks raise exceptions — Thanks @marandaneto !
6fc55b6 Normalize SDK event timestamps to UTC, including datetime values and parseable ISO timestamp strings, and correct UTC serialization for except
…flag (old private flags kept as deprecated aliases). — Thanks @carlos-marchal-ph!
capture_ai: AI events on the dedicated AI endpoint with the event UUID returned; new enable_full_ai_capture flag (old private flags kept as deprecated aliases). — Thanks @carlos-marchal-ph!9beed86 fix: preserve event delivery when gevent monkey-patches queue.Queue, including in preloaded gunicorn workers — Thanks @marandaneto!
queue.Queue, including in preloaded gunicorn workers — Thanks @marandaneto!9c4fd84 Fix async OpenAI streaming captures to include token usage and other generation properties emitted by synchronous streams. — Thanks @ckarnell
f38790c Fix local evaluation for negated, missing, and malformed cohort definitions — Thanks @marandaneto!
bd5cff4 fix: declare Gemini's cache accounting model on generations with cache reads, so ingestion prices cached tokens from $ai_cache_reporting_exclu
$ai_cache_reporting_exclusive instead of inferring it from the token counts. — Thanks @fivestarspicy!100f993 group_identify() now validates the group identity before enqueuing. Previously group_identify("company", None) (or an empty-string group_type
group_identify() now validates the group identity before enqueuing. Previously group_identify("company", None) (or an empty-string group_type / group_key) sent a $groupidentify event with a null/empty $group_type or $group_key, which cannot address a group profile and just adds an unusable event to the project. Missing values are now dropped with a warning instead, matching the sdk-specs group-identify contract. Valid values, including non-string group keys, are passed through unchanged. — Thanks @posthog[bot]!55370ee fix: prevent client lifecycle deadlocks when error callbacks, concurrent join()/shutdown() calls, or forked sync-mode clients interact with qu
join()/shutdown() calls, or forked sync-mode clients interact with queue and worker teardown. — Thanks @marandaneto!77821ce feat: FeatureFlagEvaluations.is_enabled() accepts a default_value returned when the flag has no value in the evaluation — the key was not part
FeatureFlagEvaluations.is_enabled() accepts a default_value returned when the flag has no value in the evaluation — the key was not part of the evaluated set, or the evaluation came back empty (failed /flags request, quota limit, no resolvable distinct_id). A flag that has a value still wins, so a disabled flag returns False even with default_value=True. The default is False, so existing calls behave exactly as before. — Thanks @posthog[bot]!evaluate_flags() now JSON-decodes payloads for locally-evaluated flags, the same way it already did for flags resolved remotely. Previously get_flag_payload() returned a parsed value ({"copy": "new"}) when the flag came back from /flags but the raw JSON string ('{"copy": "new"}') when the poller evaluated it locally, so the payload's type depended on where the flag happened to resolve. The $feature_flag_payload property on $feature_flag_called events is decoded for locally-evaluated flags too. Payload strings that aren't valid JSON are still passed through unchanged. — Thanks @posthog[bot]!$feature_flag_called dedupe tracker now evicts its oldest entry when it reaches capacity instead of clearing every entry. Previously, each time a client accumulated 50,000 distinct IDs the whole tracker was wiped, so the next flag read for every previously seen distinct ID re-emitted a $feature_flag_called event it had already deduped. — Thanks @posthog[bot]!c5f4e8f Normalize Gemini tool calls and tool responses in captured input so they render in traces and reach evaluations — Thanks @marco-g-pm!
ae26014 fix: flush() no longer waits out flush_interval before delivering a partial batch. A consumer holding fewer than flush_at events now sends the
flush() no longer waits out flush_interval before delivering a partial batch. A consumer holding fewer than flush_at events now sends them as soon as flush() (or shutdown()) asks it to, instead of blocking the caller for the rest of the batching window — which previously made flush() deliver nothing at all when flush_interval was longer than the flush timeout. Timer-based batching without an explicit flush is unchanged. — Thanks @posthog[bot]!6397d78 alias() now validates both identities before enqueuing. Previously alias(None, "user-123") (or an empty-string previous_id) sent a $create_ali
alias() now validates both identities before enqueuing. Previously alias(None, "user-123") (or an empty-string previous_id) sent a $create_alias event with a null/empty distinct_id, which cannot link anything and just adds an unusable event to the project. Missing identities are now dropped with a warning instead, matching the sdk-specs alias contract. The drop that already happened when no alias target could be resolved now logs a warning too, and a non-string previous_id such as 0 is stringified consistently in both distinct_id and properties.distinct_id. — Thanks @posthog[bot]!5ed7d0d Prevent stale feature flag definition publication — Thanks @marandaneto!
b16ec74 Isolate MCP pending capture tasks by owner and loop — Thanks @marandaneto!
Your coding agent can read these notes before it upgrades. Set up the MCP server →