From c75022c199914910e861af6d2f9e4749f2900df6 Mon Sep 17 00:00:00 2001 From: Eli Reisman Date: Tue, 6 Oct 2026 20:49:12 -0700 Subject: [PATCH 1/4] docs: 8.0 migration guide, README and openfeature-provider pin --- .sampo/changesets/capture-v1-major.md | 2 +- .../openfeature-provider-posthog-8.md | 5 + AGENTS.md | 2 +- README.md | 4 +- docs/migration-7.x-to-8.0.md | 134 ++++++++++++++++++ openfeature-provider/pyproject.toml | 2 +- 6 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 .sampo/changesets/openfeature-provider-posthog-8.md create mode 100644 docs/migration-7.x-to-8.0.md diff --git a/.sampo/changesets/capture-v1-major.md b/.sampo/changesets/capture-v1-major.md index 9271aee97..b34e2a802 100644 --- a/.sampo/changesets/capture-v1-major.md +++ b/.sampo/changesets/capture-v1-major.md @@ -2,4 +2,4 @@ pypi/posthog: major --- -Capture v1 is the only capture path. Events and AI events send to the capture v1 endpoints, and the legacy v0 capture path is removed. See the migration guide for breaking changes. +Capture v1 is the only capture path. Events and AI events send to the capture v1 endpoints, and the legacy v0 capture path is removed. See the [migration guide](https://github.com/PostHog/posthog-python/blob/main/docs/migration-7.x-to-8.0.md) for breaking changes. diff --git a/.sampo/changesets/openfeature-provider-posthog-8.md b/.sampo/changesets/openfeature-provider-posthog-8.md new file mode 100644 index 000000000..5e9a804bf --- /dev/null +++ b/.sampo/changesets/openfeature-provider-posthog-8.md @@ -0,0 +1,5 @@ +--- +pypi/openfeature-provider-posthog: minor +--- + +Requires posthog 8.0 or later. Upgrade both packages together. diff --git a/AGENTS.md b/AGENTS.md index 4d67e9efc..0e40e181c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,7 +23,7 @@ Follow [Public API changes](./CONTRIBUTING.md#public-api-changes). As an agent, Before changing capture configuration, serialization, routing, or retries, read the relevant implementation and tests. -Capture v1 is the only capture protocol (`capture` posts to `/i/v1/analytics/events`, `capture_ai` to `/i/v1/ai/events`); strictly typed v1 options and `$set`/`$set_once` relocation; compression (gzip, zlib-wrapped deflate, optional zstd, default none), set per lane by `capture_compression` and `capture_ai_compression`; partial-only per-event retries with stable identity; accumulated drop reporting even on 2xx; terminal v1 `429`; `Retry-After` as a minimum bounded by the shared 30s ceiling; and inline blocking retries with `sync_mode=True`. +Capture v1 is the only capture protocol (`capture` posts to `/i/v1/analytics/events`, `capture_ai` to `/i/v1/ai/events`); per-event `options` sent as given, layered over `super_options` and context options, with legacy `$` option properties hoisted once after `before_send`; `$set`/`$set_once` relocation; compression (gzip, zlib-wrapped deflate, optional zstd, default none), set per lane by `capture_compression` and `capture_ai_compression`; partial-only per-event retries with stable identity; accumulated drop reporting even on 2xx; terminal v1 `429`; `Retry-After` as a minimum bounded by the shared 30s ceiling; and inline blocking retries with `sync_mode=True`. ## Mirror and build safety diff --git a/README.md b/README.md index 364101f01..f14f16be6 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,15 @@ SDK usage examples and code snippets live in the official documentation so they | SDK Version | Python Versions Supported | Notes | | ------------- | ---------------------------- | -------------------------- | -| 7.3.1+ | 3.10, 3.11, 3.12, 3.13, 3.14 | Added Python 3.14 support | +| 8.0.0+ | 3.10, 3.11, 3.12, 3.13, 3.14 | Capture v1 only, see the [migration guide](docs/migration-7.x-to-8.0.md) | +| 7.3.1 - 7.x | 3.10, 3.11, 3.12, 3.13, 3.14 | Added Python 3.14 support | | 7.0.0 - 7.0.1 | 3.10, 3.11, 3.12, 3.13 | Dropped Python 3.9 support | | 4.0.1 - 6.x | 3.9, 3.10, 3.11, 3.12, 3.13 | Python 3.9+ required | ## Documentation - [Python library docs](https://posthog.com/docs/libraries/python) +- [Migrating from 7.x to 8.0](docs/migration-7.x-to-8.0.md) - [Django framework docs](https://posthog.com/docs/libraries/django) - [Flask framework docs](https://posthog.com/docs/libraries/flask) - [OpenFeature provider docs](https://posthog.com/docs/feature-flags/installation/openfeature) — use PostHog flags through the [OpenFeature](https://openfeature.dev) Python SDK diff --git a/docs/migration-7.x-to-8.0.md b/docs/migration-7.x-to-8.0.md new file mode 100644 index 000000000..03d3806ba --- /dev/null +++ b/docs/migration-7.x-to-8.0.md @@ -0,0 +1,134 @@ +# Migrating from posthog 7.x to 8.0 + +posthog 8.0 sends every event through capture v1 and removes the legacy capture path. +Most apps upgrade without code changes. +Read the checklist first, then the sections that apply to you. + +## Checklist + +You need to change code if your app does any of these: + +- passes `Client` or `AsyncPosthog` constructor arguments by position after `host` +- sets `capture_mode`, `POSTHOG_CAPTURE_MODE` or `gzip` +- imports `CaptureV1Error`, `posthog.capture_v1`, `request.batch_post`, `EVENTS_ENDPOINT` or `AI_EVENTS_ENDPOINT` +- sends events to a self-hosted PostHog that does not serve the capture v1 endpoints +- reuses one event `uuid` for more than one event +- sets `$process_person_profile` to turn person processing on for events without a distinct ID +- passes strings such as `"true"` for `$cookieless_mode`, `$ignore_sent_at` or `$process_person_profile` +- expects `super_properties` to override properties passed to a single call +- tests the AI integrations with a mock client and asserts on `capture` +- passes its own client object to the AI integrations + +## Endpoints + +| Method | Endpoint | +| --- | --- | +| `capture`, `set`, `set_once`, `alias`, `group_identify`, `capture_exception` | `/i/v1/analytics/events` | +| `capture_ai`, `capture_ai_immediate`, and every built-in AI integration | `/i/v1/ai/events` | + +If you send events to a self-hosted PostHog, check that it serves both endpoints before you upgrade. +An endpoint that is not served drops every event sent to it. + +## Removed options and APIs + +| Removed | Use instead | +| --- | --- | +| `capture_mode`, `CaptureMode`, `POSTHOG_CAPTURE_MODE` | Nothing. Capture v1 is the only path. | +| `gzip=True` | `capture_compression=CaptureCompression.GZIP`. The default is no compression. `POSTHOG_CAPTURE_COMPRESSION` still works. | +| `CaptureV1Error` | `CaptureError`. There is no alias. | +| `posthog.capture_v1` | `posthog.capture_event` and `posthog.capture_send` | +| `request.batch_post`, `async_batch_post`, `EVENTS_ENDPOINT`, `AI_EVENTS_ENDPOINT` | Nothing. Send events through a client. | +| The `gzip` parameter of `request.post` and `request.flags` | Nothing. | +| The `backoff` dependency | Add it to your own requirements if your code imports it. | + +Constructor arguments after `host` are keyword-only: + +```python +# 7.x +Posthog("", "https://us.i.posthog.com", True) + +# 8.0 +Posthog("", host="https://us.i.posthog.com", debug=True) +``` + +## Errors and `on_error` + +Capture v1 returns a result for every event, even when the request succeeds. +An event can be dropped, for example by billing limits or quotas, inside a 2xx response. +8.0 reports those events as failures. + +- Every capture failure is a `CaptureError`. It has these fields: + - `status`: the HTTP status, or `0` when the request never got a response + - `endpoint`, `request_id` and `attempts` + - `drops` and `retry_exhausted`: the uuids that were dropped or ran out of retries + - `event_results`: a `CaptureEventResult(result, details)` for each uuid, from `posthog.capture_send` + - `verdict_summary()`: counts such as `drop/billing=1, retry/not_persisted=1` +- `on_error(error, batch)` receives every failure, including in `sync_mode` and from `AsyncPosthog.capture_immediate`. +- Without `on_error`, the SDK logs one line per failed batch, for example `2 event(s) not persisted by /i/v1/analytics/events: ...`. The line never contains event content or the server's response text. +- In `sync_mode`, a failed `capture()` calls `on_error` and returns `None`. +- If a capture inside `on_error` fails, the SDK logs the failure and does not call `on_error` again. +- A `429` response is not retried. +- Retries start at 100 ms and double each attempt, up to 30 seconds. The `Consumer` default is 3 retries, down from 10. + +## Event uuids + +- Each event needs its own uuid. Capture rejects a whole batch that contains the same uuid twice, so the other events in that batch are lost too. +- The SDK accepts a uuid with hyphens, 32 hex digits, `{...}` braces or a `urn:uuid:` prefix, in any case. It sends the lowercase hyphenated form and returns that form from `capture`. +- Any other value is replaced with a generated uuid, and the SDK logs an error. This applies to `AsyncPosthog` too. +- Generated uuids are UUIDv7. + +## Event options + +Capture v1 sends processing options in an `options` object, next to `properties`. + +- `capture`, `capture_ai`, `set`, `set_once`, `alias`, `group_identify` and `capture_exception` take an `options` argument: + + ```python + posthog.capture("signed_up", distinct_id="user-1", options={"cookieless_mode": True}) + ``` + +- `super_options` sets options on every event, like `super_properties`. +- `set_context_option(key, value)` sets an option for the current context, like `tag()`. +- Options are sent as given. PostHog validates them. +- The legacy properties `$cookieless_mode`, `$ignore_sent_at`, `$product_tour_id` and `$process_person_profile` still work. They move into options after `before_send`. Their values are no longer converted, so pass `True` or `False`, not `"true"`. +- An option wins over its legacy property. A `None` option counts as unset. + +Values apply in this order, and each layer overrides the ones before it: + +1. options the SDK sets, such as turning off person processing for events without a distinct ID +2. `super_options` and `super_properties` +3. context options and tags +4. the `options` and `properties` of the call +5. `before_send` + +Two changes follow from this order: + +- Properties passed to a call now override `super_properties`. `super_properties` can no longer change `$lib`, `$lib_version` or `$geoip_disable`. +- An event without a distinct ID gets `options.process_person_profile = false`. A `$process_person_profile: true` property no longer turns person processing back on. Set the option instead. + +## AI capture + +- `capture_ai` and every built-in AI integration send to `/i/v1/ai/events`. The AI endpoint accepts events up to 8 MiB. +- `enable_full_ai_capture` now controls only content: string truncation and media redaction. It no longer chooses the endpoint. `_use_ai_lane` and `_enable_multimodal_capture` still work as aliases. +- The AI integrations call your client's `capture_ai`, and pass `options=`. A client object without `capture_ai` gets `capture` calls instead. A custom client must accept the `options` keyword argument. +- Tests that pass a `Mock` client to an AI integration must assert on `mock.capture_ai`, not `mock.capture`. +- When an AI integration or MCP falls back to a trace, run or session ID, it turns person processing off with a per-event option. A `$process_person_profile: true` property no longer turns it back on. `before_send` can still change it. +- MCP's `PostHogCaptureEvent` has an `options` key. +- New `Client` and `AsyncPosthog` arguments for the AI lane: + - `capture_ai_compression`: default none. `POSTHOG_CAPTURE_COMPRESSION` does not apply to it. + - `capture_ai_max_queue_size`: default 1000 + - `capture_ai_timeout`: default 30 seconds + - `capture_ai_max_event_bytes`: default 8 MiB plus 64 KiB. You can only lower it. +- `AsyncPosthog` has `capture_ai` and `capture_ai_immediate`. + +## Batching and size limits + +- An analytics event over 900 KiB is dropped and logged. This now applies in `sync_mode` too. +- An AI event over `capture_ai_max_event_bytes` is dropped and logged. +- A batch stops before an event that would take it past 5 MiB. A larger event is sent alone. +- `flush()` and `shutdown()` drain the analytics and AI queues at the same time. + +## OpenFeature provider + +`openfeature-provider-posthog` 0.2.0 requires posthog 8.0 or later. +Upgrade both packages together. diff --git a/openfeature-provider/pyproject.toml b/openfeature-provider/pyproject.toml index 4987c5008..cb90119b1 100644 --- a/openfeature-provider/pyproject.toml +++ b/openfeature-provider/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ # single-call API. Pinned to the posthog major this provider is released and # tested against (both ship from this repo); bump the upper bound when moving # the provider to a new posthog major. - "posthog>=7.0.0,<8.0.0", + "posthog>=8.0.0,<9.0.0", "openfeature-sdk>=0.8.0", ] From 63a76e9d103415589179547b871e3250eae36886 Mon Sep 17 00:00:00 2001 From: Eli Reisman Date: Tue, 6 Oct 2026 21:54:42 -0700 Subject: [PATCH 2/4] chore: let openfeature provider accept posthog 7 and 8 --- .sampo/changesets/openfeature-provider-posthog-8.md | 2 +- docs/migration-7.x-to-8.0.md | 5 +++-- openfeature-provider/pyproject.toml | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.sampo/changesets/openfeature-provider-posthog-8.md b/.sampo/changesets/openfeature-provider-posthog-8.md index 5e9a804bf..02e3f2e00 100644 --- a/.sampo/changesets/openfeature-provider-posthog-8.md +++ b/.sampo/changesets/openfeature-provider-posthog-8.md @@ -2,4 +2,4 @@ pypi/openfeature-provider-posthog: minor --- -Requires posthog 8.0 or later. Upgrade both packages together. +Supports posthog 8.x as well as 7.x. diff --git a/docs/migration-7.x-to-8.0.md b/docs/migration-7.x-to-8.0.md index 03d3806ba..82961df8f 100644 --- a/docs/migration-7.x-to-8.0.md +++ b/docs/migration-7.x-to-8.0.md @@ -120,6 +120,7 @@ Two changes follow from this order: - `capture_ai_timeout`: default 30 seconds - `capture_ai_max_event_bytes`: default 8 MiB plus 64 KiB. You can only lower it. - `AsyncPosthog` has `capture_ai` and `capture_ai_immediate`. +- `AsyncPosthog` accepts `privacy_mode` and `enable_full_ai_capture`, with the same meaning as on `Client`. An AI integration given an `AsyncPosthog` client used to always truncate and redact media. ## Batching and size limits @@ -130,5 +131,5 @@ Two changes follow from this order: ## OpenFeature provider -`openfeature-provider-posthog` 0.2.0 requires posthog 8.0 or later. -Upgrade both packages together. +`openfeature-provider-posthog` 0.2.0 works with posthog 7.x and 8.x. +Older provider versions require posthog below 8.0, so upgrade the provider before or with posthog. diff --git a/openfeature-provider/pyproject.toml b/openfeature-provider/pyproject.toml index cb90119b1..2fb16aaac 100644 --- a/openfeature-provider/pyproject.toml +++ b/openfeature-provider/pyproject.toml @@ -27,7 +27,7 @@ dependencies = [ # single-call API. Pinned to the posthog major this provider is released and # tested against (both ship from this repo); bump the upper bound when moving # the provider to a new posthog major. - "posthog>=8.0.0,<9.0.0", + "posthog>=7.0.0,<9.0.0", "openfeature-sdk>=0.8.0", ] From 3b68a57c603ea71eeddf6f7852b6411245d97fb0 Mon Sep 17 00:00:00 2001 From: Eli Reisman Date: Wed, 7 Oct 2026 10:41:59 -0700 Subject: [PATCH 3/4] docs: say options beat legacy properties at every layer --- docs/migration-7.x-to-8.0.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/migration-7.x-to-8.0.md b/docs/migration-7.x-to-8.0.md index 82961df8f..0c9cb9130 100644 --- a/docs/migration-7.x-to-8.0.md +++ b/docs/migration-7.x-to-8.0.md @@ -91,7 +91,8 @@ Capture v1 sends processing options in an `options` object, next to `properties` - `set_context_option(key, value)` sets an option for the current context, like `tag()`. - Options are sent as given. PostHog validates them. - The legacy properties `$cookieless_mode`, `$ignore_sent_at`, `$product_tour_id` and `$process_person_profile` still work. They move into options after `before_send`. Their values are no longer converted, so pass `True` or `False`, not `"true"`. -- An option wins over its legacy property. A `None` option counts as unset. +- An option set at any layer wins over its legacy property set at any layer. For example, `super_options={"cookieless_mode": True}` wins over an event's `$cookieless_mode: False`. When you move a default to options, move the per-event overrides of that key to options too. +- A `None` option counts as unset. Values apply in this order, and each layer overrides the ones before it: From 4fd8597021dd0f35cb564f0dc9acc904737d4ad0 Mon Sep 17 00:00:00 2001 From: Eli Reisman Date: Wed, 7 Oct 2026 17:06:51 -0700 Subject: [PATCH 4/4] docs: name the CaptureError import and the posthoganalytics mirror --- docs/migration-7.x-to-8.0.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/migration-7.x-to-8.0.md b/docs/migration-7.x-to-8.0.md index 0c9cb9130..93516d128 100644 --- a/docs/migration-7.x-to-8.0.md +++ b/docs/migration-7.x-to-8.0.md @@ -35,12 +35,14 @@ An endpoint that is not served drops every event sent to it. | --- | --- | | `capture_mode`, `CaptureMode`, `POSTHOG_CAPTURE_MODE` | Nothing. Capture v1 is the only path. | | `gzip=True` | `capture_compression=CaptureCompression.GZIP`. The default is no compression. `POSTHOG_CAPTURE_COMPRESSION` still works. | -| `CaptureV1Error` | `CaptureError`. There is no alias. | +| `CaptureV1Error` (`from posthog.capture_v1 import CaptureV1Error`) | `CaptureError` (`from posthog import CaptureError`). There is no alias. | | `posthog.capture_v1` | `posthog.capture_event` and `posthog.capture_send` | | `request.batch_post`, `async_batch_post`, `EVENTS_ENDPOINT`, `AI_EVENTS_ENDPOINT` | Nothing. Send events through a client. | | The `gzip` parameter of `request.post` and `request.flags` | Nothing. | | The `backoff` dependency | Add it to your own requirements if your code imports it. | +The `posthoganalytics` package has the same changes. For example, import `CaptureError` from `posthoganalytics`. + Constructor arguments after `host` are keyword-only: ```python