Skip to content

fix: only move the current screen for wrapper events built as $screen - #930

Merged
turnipdabeets merged 1 commit into
feat/v4-capture-v1from
fix/wrapper-screen-built-name
Nov 3, 2026
Merged

turnipdabeets merged 1 commit into
feat/v4-capture-v1from
fix/wrapper-screen-built-name

Conversation

@turnipdabeets

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Flutter builds a plain capture(eventName: r'$screen') under another name, because native applies screen() rules to $screen builds, and restores the real name before sending. sendBuiltEventForWrapper only looked at the name at send, so with beforeSend callbacks set, a plain capture moved the current screen and later events got its $screen_name. Without callbacks it doesn't.

Now the current screen only moves when the event was built and sent as $screen. The event itself is still sent as before.

Raised in review: PostHog/posthog-flutter#682 (comment)

💚 How did you test it?

Added a regression test to PostHogWrapperEventTest. It failed before the fix and passes now. ./gradlew :posthog:test :posthog:apiCheck spotlessCheck passes (JVM only, no device).

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

No changeset: sendBuiltEventForWrapper is unreleased, wrapper-only API on this branch.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Written with Claude Code. A wrapper that renames a plain event to $screen also no longer moves the current screen; only posthog-flutter uses this API today. The screen check is separate from the distinct id fix in the sibling PR, and the two don't overlap.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Nov 3, 2026
@turnipdabeets
turnipdabeets requested a review from a team as a code owner November 3, 2026 00:37
@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-11-03T00:50:25.722127+00:00
Duration: 474794ms

✅ All Tests Passed!

232/232 tests passed


Capture_V1 Tests

✅ 105/105 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 306ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 27ms
Required Headers.Has Authorization Bearer Header ✅ 23ms
Required Headers.Has Content Type Json ✅ 21ms
Required Headers.Has Posthog Sdk Info Format ✅ 22ms
Required Headers.Has Posthog Attempt Header ✅ 17ms
Required Headers.Has Posthog Request Id ✅ 18ms
Required Headers.Has Posthog Request Timestamp ✅ 21ms
Required Headers.Has User Agent ✅ 20ms
Body Format.Body Has Created At And Batch ✅ 19ms
Body Format.No Api Key In Body ✅ 19ms
Body Format.No Sent At In Body ✅ 18ms
Event Format.Event Has Required Root Fields ✅ 20ms
Event Format.Event Uuid Is Valid ✅ 17ms
Event Format.Event Timestamp Is Rfc3339 ✅ 19ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 18ms
Event Format.Distinct Id Is String ✅ 20ms
Event Format.Distinct Id At Root Not Properties ✅ 17ms
Event Format.Custom Properties Preserved ✅ 17ms
Event Format.Set Properties Preserved ✅ 18ms
Event Format.Set Once Properties Preserved ✅ 19ms
Event Format.Groups Properties Preserved ✅ 20ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 20ms
Event Format.Event Has Required Root Fields Batch ✅ 23ms
Event Format.Event Uuid Is Valid Batch ✅ 24ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 20ms
Event Format.Distinct Id Is String Batch ✅ 21ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 20ms
Event Format.Custom Properties Preserved Batch ✅ 23ms
Event Format.Set Properties Preserved Batch ✅ 23ms
Event Format.Set Once Properties Preserved Batch ✅ 26ms
Event Format.Groups Properties Preserved Batch ✅ 20ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 23ms
Batch Behavior.Multiple Events In Single Batch ✅ 45ms
Batch Behavior.Batch Envelope Smoke ✅ 25ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 12ms
Batch Behavior.Flush At Triggers Batch ✅ 1018ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 15ms
Deduplication.Generates Unique Uuids ✅ 38ms
Deduplication.Different Events Same Content Different Uuids ✅ 17ms
Deduplication.Preserves Uuid On Retry ✅ 7014ms
Deduplication.Preserves Timestamp On Retry ✅ 7017ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7021ms
Deduplication.No Duplicate Events In Batch ✅ 29ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 15ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 12017ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 7016ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 2024ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 7016ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 18ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 20ms
Response Format Validation.Success No Retry After When All Ok ✅ 17ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2010ms
Response Format Validation.Success No Retry After When Drop Only ✅ 17ms
Response Format Validation.Response Echoes Request Id ✅ 12ms
Retry Behavior.Retries On 408 ✅ 7014ms
Retry Behavior.Retries On 500 ✅ 7014ms
Retry Behavior.Retries On 503 ✅ 7018ms
Retry Behavior.Retries On 504 ✅ 7015ms
Retry Behavior.Retries On 404 ✅ 7014ms
Retry Behavior.Retries On 429 ✅ 6010ms
Retry Behavior.Retries On 502 ✅ 7014ms
Retry Behavior.Retryable Errors Have Retry After ✅ 4013ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10026ms
Retry Behavior.Does Not Retry On 400 ✅ 4018ms
Retry Behavior.Does Not Retry On 401 ✅ 4017ms
Retry Behavior.Does Not Retry On 402 ✅ 4015ms
Retry Behavior.Does Not Retry On 413 ✅ 4015ms
Retry Behavior.Does Not Retry On 415 ✅ 4019ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4017ms
Retry Behavior.Implements Backoff ✅ 14027ms
Partial Batch Handling.Handles 200 Full Success ✅ 2016ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3025ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3020ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 3024ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7014ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7015ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7014ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7014ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7013ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7016ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7018ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3019ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 3024ms
Compression.Sends Gzip Content Encoding ✅ 15ms
Compression.No Content Encoding When Disabled ✅ 13ms
Compression.Compressed Body Is Decompressible ✅ 13ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4014ms
Event Options.Cookieless Mode Override ✅ 13ms
Event Options.Disable Skew Correction Override ✅ 11ms
Event Options.Process Person Profile Override ✅ 12ms
Event Options.Product Tour Id Override ✅ 13ms
Event Options.Unset Options Omitted ✅ 16ms
Event Options.Options Override In Batch ✅ 24ms
Event Options.Unknown Option Passes Through ✅ 16ms
Event Options.Option Value Is Not Converted ✅ 19ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 18ms
Event Options.Option Wins Over Legacy Property ✅ 17ms
Event Options.Legacy Property Fills Unset Option ✅ 19ms
Event Options.Null Option Falls Back To Legacy Property ✅ 22ms
Property Layers.Caller Property Beats Registered Property ✅ 16ms
Property Layers.Before Send Runs Last ✅ 21ms
Fail Open.Empty Api Key Sends Nothing ✅ 3012ms
Fail Open.Personal Api Key Sends Nothing ✅ 3011ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 15ms

Capture_Ai_V1 Tests

✅ 110/110 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 16ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 14ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 12ms
Endpoint And Method.Capture Ai Does Not Reroute Non Ai Events ✅ 14ms
Lane Isolation.Failing Ai Lane Does Not Delay Analytics ✅ 7021ms
Required Headers.Has Authorization Bearer Header ✅ 14ms
Required Headers.Has Content Type Json ✅ 14ms
Required Headers.Has Posthog Sdk Info Format ✅ 13ms
Required Headers.Has Posthog Attempt Header ✅ 12ms
Required Headers.Has Posthog Request Id ✅ 12ms
Required Headers.Has Posthog Request Timestamp ✅ 13ms
Required Headers.Has User Agent ✅ 12ms
Body Format.Body Has Created At And Batch ✅ 13ms
Body Format.No Api Key In Body ✅ 14ms
Body Format.No Sent At In Body ✅ 14ms
Event Format.Event Has Required Root Fields ✅ 12ms
Event Format.Event Uuid Is Valid ✅ 19ms
Event Format.Event Timestamp Is Rfc3339 ✅ 23ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 21ms
Event Format.Distinct Id Is String ✅ 20ms
Event Format.Distinct Id At Root Not Properties ✅ 17ms
Event Format.Custom Properties Preserved ✅ 17ms
Event Format.Set Properties Preserved ✅ 19ms
Event Format.Set Once Properties Preserved ✅ 19ms
Event Format.Groups Properties Preserved ✅ 21ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 19ms
Event Format.Event Has Required Root Fields Batch ✅ 18ms
Event Format.Event Uuid Is Valid Batch ✅ 17ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 17ms
Event Format.Distinct Id Is String Batch ✅ 18ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 16ms
Event Format.Custom Properties Preserved Batch ✅ 16ms
Event Format.Set Properties Preserved Batch ✅ 21ms
Event Format.Set Once Properties Preserved Batch ✅ 21ms
Event Format.Groups Properties Preserved Batch ✅ 33ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 18ms
Batch Behavior.Multiple Events In Single Batch ✅ 25ms
Batch Behavior.Batch Envelope Smoke ✅ 24ms
Batch Behavior.Flush At Triggers Batch ✅ 1017ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 13ms
Deduplication.Generates Unique Uuids ✅ 23ms
Deduplication.Different Events Same Content Different Uuids ✅ 14ms
Deduplication.Preserves Uuid On Retry ✅ 7011ms
Deduplication.Preserves Timestamp On Retry ✅ 7014ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7016ms
Deduplication.No Duplicate Events In Batch ✅ 25ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 14ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 12018ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 7013ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 2029ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 7015ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 15ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 18ms
Response Format Validation.Success No Retry After When All Ok ✅ 16ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2008ms
Response Format Validation.Success No Retry After When Drop Only ✅ 17ms
Response Format Validation.Response Echoes Request Id ✅ 13ms
Retry Behavior.Retries On 408 ✅ 7014ms
Retry Behavior.Retries On 500 ✅ 7014ms
Retry Behavior.Retries On 503 ✅ 7023ms
Retry Behavior.Retries On 504 ✅ 7013ms
Retry Behavior.Retries On 404 ✅ 7012ms
Retry Behavior.Retries On 429 ✅ 6014ms
Retry Behavior.Retries On 502 ✅ 6015ms
Retry Behavior.Retryable Errors Have Retry After ✅ 4011ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10018ms
Retry Behavior.Does Not Retry On 400 ✅ 4015ms
Retry Behavior.Does Not Retry On 401 ✅ 4015ms
Retry Behavior.Does Not Retry On 402 ✅ 4016ms
Retry Behavior.Does Not Retry On 413 ✅ 4017ms
Retry Behavior.Does Not Retry On 415 ✅ 4014ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4015ms
Retry Behavior.Implements Backoff ✅ 14013ms
Partial Batch Handling.Handles 200 Full Success ✅ 2013ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3020ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3019ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 3018ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7013ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7015ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7015ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7013ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7011ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7014ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7021ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3022ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 3027ms
Compression.Sends Gzip Content Encoding ✅ 16ms
Compression.No Content Encoding When Disabled ✅ 19ms
Compression.Compressed Body Is Decompressible ✅ 17ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4015ms
Event Options.Cookieless Mode Override ✅ 12ms
Event Options.Disable Skew Correction Override ✅ 13ms
Event Options.Process Person Profile Override ✅ 13ms
Event Options.Product Tour Id Override ✅ 12ms
Event Options.Unset Options Omitted ✅ 14ms
Event Options.Options Override In Batch ✅ 20ms
Event Options.Unknown Option Passes Through ✅ 15ms
Event Options.Option Value Is Not Converted ✅ 17ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 18ms
Event Options.Option Wins Over Legacy Property ✅ 14ms
Event Options.Legacy Property Fills Unset Option ✅ 14ms
Event Options.Null Option Falls Back To Legacy Property ✅ 14ms
Event Size.Event Over Ceiling Is Not Sent ✅ 3314ms
Event Size.Over Ceiling Drop Is Reported Or Logged Without Content ✅ 2046ms
Event Size.Batches Close Before Byte Target ✅ 6084ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 16ms
Property Layers.Caller Property Beats Registered Property ✅ 14ms
Property Layers.Before Send Runs Last ✅ 15ms
Fail Open.Empty Api Key Sends Nothing ✅ 1010ms
Fail Open.Personal Api Key Sends Nothing ✅ 1009ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 28ms
Request Payload.Flags Request Uses V2 Query Param ✅ 19ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 21ms
Request Payload.Flags Request Omits Authorization Header ✅ 16ms
Request Payload.Token In Flags Body Matches Init ✅ 18ms
Request Payload.Groups Round Trip ✅ 16ms
Request Payload.Groups Default To Empty Object ✅ 18ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 18ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 16ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 17ms
Request Lifecycle.No Flags Request On Init Alone ✅ 8ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 14ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 25ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 18ms
Retry Behavior.Retries Flags On 502 ✅ 318ms
Retry Behavior.Retries Flags On 504 ✅ 317ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 16ms

@ioannisj ioannisj left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 An AI agent wrote this on behalf of @ioannisj.

No findings. The builtAsScreen gate lines up with plain capture("$screen") on the native path, which doesn't move the current screen either (only screen() does), and the new test sits nicely next to the existing positive case.

@turnipdabeets
turnipdabeets merged commit 4749e54 into feat/v4-capture-v1 Nov 3, 2026
12 checks passed
@turnipdabeets
turnipdabeets deleted the fix/wrapper-screen-built-name branch November 3, 2026 13:53
turnipdabeets added a commit that referenced this pull request Nov 5, 2026
* feat!: disable the SDK when the project token is a personal API key

A `phx_` token is a personal API key; capture rejects every request made
with it. Setup now logs once and stays disabled, as for an empty token.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* feat!: send analytics events through capture v1

Analytics events now go to `POST /i/v1/analytics/events` with the project
token in an `Authorization: Bearer` header, `PostHog-Sdk-Info`,
`PostHog-Attempt`, `PostHog-Request-Id` and `PostHog-Request-Timestamp`.
The body is `{created_at, batch}`; each event drops `$lib`/`$lib_version`,
lifts string `$session_id`/`$window_id` to the root, and hoists the legacy
`$process_person_profile`, `$cookieless_mode`, `$ignore_sent_at` and
`$product_tour_id` properties into the new `PostHogEvent.options`.

The queue reads the per-event results: `ok`, `warning` and `drop` leave the
queue, only `retry` is kept and resent under the same request id and
`created_at` with the next attempt number. A 2xx without readable results
counts as delivered. 408 and 500/502/503/504 are retried; 429, 404 and
other 4xx are terminal, as are redirects other than a same-origin 307/308
(followed at most 5 times with the same body and headers). Backoff doubles
from 1 to 30 seconds with 25% jitter, and `Retry-After` is a floor capped
at 30 seconds for every queue. Replay and logs keep their retry sets.

Legacy cached events (`PostHogSendCachedEventsIntegration`) are sent
through capture v1 too; the ones capture asks to retry are re-queued at
the tail of the legacy file for the next launch.

`PostHogConfig.maxRetries` is renamed `pushNotificationSubscriptionMaxRetries`
since it only limits push-subscription retries. `PostHogApi.batch()` stays
for posthog-server.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* chore(compliance): run the capture v1 suite against the adapter

The adapter advertises `capture_v1` and `encoding_gzip`, tracks
`/i/v1/analytics/events` requests with `retry_attempt` derived from
`PostHog-Attempt`, maps `max_retries` to the renamed config field and runs
with `personProfiles = ALWAYS`, since the harness never identifies. The
workflow pins harness 1.20.0 and also runs for PRs that target the
`chore/v4-*` and `feat/v4-*` branches.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* docs: changeset bullets and migration guide for capture v1

Co-Authored-By: Claude Fable 5.1 <[email protected]>

* fix: resend a failed capture request as is, without client retries or logged bodies

The queue now resends exactly the files of a request that failed or came back
with events to retry, so the API keeps its request ID and `created_at` and bumps
`PostHog-Attempt`. Before, the next request took the head of the queue, and any
event queued since gave it a new ID at attempt 1. A 413 still halves the batch
as a new request, and a file the queue drops in the meantime starts over.

The capture client turns off OkHttp's `retryOnConnectionFailure`, which resent
a request on a reused connection with a stale attempt number and timestamp.

Debug logs for capture requests name the event count and the response status
instead of printing the request and response bodies, which hold event content.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: stop OkHttp resending a capture 503 with Retry-After 0

OkHttp's follow-up layer resends a 503 carrying `Retry-After: 0` even with
retryOnConnectionFailure off, repeating PostHog-Attempt and
PostHog-Request-Timestamp outside the queue's backoff. A network interceptor
on the capture client drops that header before the retry layer sees it; the
queue waits the same with or without it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: cap the capture v1 response read at 1 MiB

A successful capture response was read whole into memory. Read at most 1 MiB
and treat a larger or endless body as unreadable, which counts the batch as
delivered like any other 2xx without readable results.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep the queue's retry jitter in milliseconds

Rounding the jittered backoff to whole seconds removed the jitter from the
1 s and 2 s waits, so devices refused together retried together. The delay
is now kept in milliseconds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: send a 3.x event queued on disk through capture v1

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: list what a reverse proxy needs for capture v1

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: cap the time spent reading capture v1 results

A 2xx body that trickles bytes within the read timeout held the queue thread until it ended. Reading the results now has a 10 second deadline, after which the body counts as unreadable and the events as delivered, like an oversized body.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat!: add per-event capture options and one property precedence

`capture`, `screen`, `identify`, `setPersonProperties`, `alias`, `group` and
`captureException` take an `options` map that is copied onto the new
`PostHogEvent.options`, so PostHog can be told how to process one event apart
from its properties and `beforeSend` can edit it.

Properties are merged in one order: SDK-set values, registered super
properties, the caller's properties (with `$set`, `$set_once` and `$groups`
as per-call values), then `beforeSend`. `$is_identified`,
`$process_person_profile` and the replay debug properties are no longer
re-applied after the caller's values.

The SDK sets the `process_person_profile` option to `false` only when person
processing is off, and only when neither a caller option nor a caller or
registered `$process_person_profile` property already says so. It no longer
writes a `$process_person_profile` property.

Also: a blank `distinctId` argument on `capture` uses the current distinct id;
a `$set` sent with `process_person_profile: false` neither stores nor matches
the person-properties hash; an event returned by `beforeSend` keeps the uuid of
the client capture it came from; the minimal `$feature_flag_called` allowlist
keeps `$cookieless_mode`, `$ignore_sent_at` and `$product_tour_id`; the
compliance adapter forwards `options` and advertises `event_options`.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DRbQPidntd4hVufNpMT19L

* test: cover capture options, property precedence and the personless option

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DRbQPidntd4hVufNpMT19L

* docs: describe capture options and property precedence for 4.0

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DRbQPidntd4hVufNpMT19L

* fix: skip PostHog-reserved deep link query parameters

A link opened with ?$process_person_profile=true copied that parameter into the Deep Link Opened properties, where capture v1 turns it into the process_person_profile option and enables person processing despite PersonProfiles.NEVER. Query parameters whose name starts with $ are now skipped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say the retry jitter keeps millisecond precision

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: keep the personless option when the legacy property is null

A Java caller can pass a null `$process_person_profile`. It no longer counts as an override, so the SDK still sends `process_person_profile: false` when person processing is off.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: escape dollar-prefixed keys in the Kotlin migration examples

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* feat: add captureAi on the capture v1 AI endpoint

`captureAi` takes the same arguments as `capture`, goes through the same
enrichment, precedence, personless default and `beforeSend`, and queues the
event on its own disk queue, executor and retry identity for
`POST /i/v1/ai/events`. It returns the event's uuid once it's handed to the
queue, or null when the SDK is disabled or opted out or `beforeSend` dropped
it. A name not starting with `$ai_` logs one debug line and is sent unchanged;
`capture` never reroutes `$ai_*` events. The AI request client uses
`captureAiTimeout` for its call, read and write timeouts and honors
`captureAiCompression` through its own gzip interceptor instance.

`PostHogQueue` becomes byte-aware through `EndpointSpec`: `maxBatchBytes`
bounds a request window (check before append, an oversize record goes alone)
and flushes as soon as that much is queued; `maxQueueBytes` is a FIFO disk cap
on top of `maxQueueSize`, enforced on add and on load; `maxRecordBytes` drops a
record over the limit when it's queued, logging only its name and size. The
analytics spec gets a 5 MiB request cap; the AI spec gets 5 MiB requests,
50 MiB on disk and `captureAiMaxEventBytes` (at most 8 MiB) plus a 64 KiB
envelope per event.

New `PostHogConfig` knobs: `captureAiCompression`, `captureAiMaxQueueSize`,
`captureAiTimeout` and `captureAiMaxEventBytes`; non-positive values use the
defaults. `PostHogAndroid` stores the AI queue under `posthog-disk-ai-queue`.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DRbQPidntd4hVufNpMT19L

* test: cover captureAi, the AI lane and byte-aware batching

SDK-level: routing by method, beforeSend and options, null returns, a
terminal 404, the oversize drop (name and size only in the log), the non-`$ai_`
debug line, flush and close draining the AI queue, compression per lane and the
AI timeout. API-level: the AI endpoint's headers and body, separate retry
identities per lane, and an AI queue keeping only the events asked to retry.
Queue-level, on a synthetic record: byte-limited request windows, an oversize
record sent alone, the flush at a request's worth of bytes, the per-record
drop, the disk cap on add and on load, and the spec limits and config
fallbacks. The Android setup test covers the AI storage prefix.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DRbQPidntd4hVufNpMT19L

* chore(compliance): run the capture v1 AI suite against the adapter

Adds `POST /capture_ai`, which routes through `captureAi` and returns the uuid
it sent, tracks `/i/v1/ai/events` requests next to the analytics ones and
advertises `capture_ai_v1`. Also restores the capture v1 adapter changes
(tracking by capture path, `PostHog-Attempt` as the retry count,
`personProfiles = ALWAYS`, the renamed push retries field) that the options
commit reverted.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DRbQPidntd4hVufNpMT19L

* docs: changeset bullets and migration guide for captureAi

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DRbQPidntd4hVufNpMT19L

* docs: list the AI endpoint in the migration guide's endpoint table

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: check the AI event size before queuing and return null when over

The queue measured an AI event after `encryption`, so an encryption that
grows the data dropped events PostHog would accept, and captureAi had
already returned the uuid. captureAi now measures the serialized event
before encryption and returns null when it's over the limit. The queue's
per-record limit had no other user and is removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: describe the AI size limit and timeout as they behave

The limit covers an AI event's properties up to 8 MiB, with 64 KiB for the
rest of the serialized event, measured before encryption. captureAiTimeout
is a timeout without network activity, not a call timeout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(compliance): report no uuid when captureAi doesn't queue the event

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: log each event capture v1 drops with the reason it gives

Debug logs named only the drop count, so a misrouted_event or
ai_event_too_big drop was invisible. Each dropped event is now logged with
its name, uuid and the server's details, never its properties. Adds a test
that an AI event with exactly 8 MiB of properties is sent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: stop measuring an AI event once it passes the size limit

captureAi checks the size on the caller's thread, so a huge prompt or
base64 payload no longer costs a full serialization just to be dropped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: log an unserializable property by key and type, not by value

Properties can hold prompts and other user content.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say the AI size check builds the full JSON tree before it stops counting

* test: double bytes instead of Base64 in the AI encryption test

java.util.Base64 isn't on API 23, so animalsnifferTest failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: make the unserializable Turn a plain class

The data class hashCode calls Double.hashCode(double), which isn't on API 23.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: write encrypted test streams in blocks

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: retry capture v1 requests that get 404 or 429 (#897)

* fix: retry capture v1 requests that get 404 or 429

A 404 usually means a misconfigured proxy and a 429 now only comes from an overloaded ingress, so keep the events queued and retry them with the usual backoff and Retry-After handling instead of dropping them. Matches PostHog/posthog-go#395.

* fix: warn once when capture v1 returns 404

A 404 now keeps events queued, so nothing tells the app that the route
is missing. Print one warning per client, even without debug, naming the
URL and the path a reverse proxy or self-hosted instance must serve.

* fix: leave host credentials and query out of the capture v1 404 warning

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs: escape dollar signs in captureAi examples

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: give the serializer privacy fixture a prompt-bearing toString

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: make captureAiMaxEventBytes limit the whole serialized event

Match posthog-ios, posthog-python and the Flutter desktop lane: the value
is the serialized event size, defaulting to 8 MiB + 64 KiB, instead of a
properties size the SDK added 64 KiB to. A value over the ceiling now
logs a warning when it is clamped.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: subtract a queued file's counted size when it's removed

A file deleted from the cache dir reads as 0 bytes, so the queue kept
counting it and evicted new AI events against bytes that were gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: read lenient false person-processing values before caching person properties

Capture reads "false", 0 and similar strings as false, so a $set sent
with them is personless and must not store the person-properties hash.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: send a fatal event even while a request waits to be retried

The fatal flush resent only the retry set, so the crash report stayed
on disk as the process exited.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(compliance): count only delivered events when capture asks to retry some

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(compliance): run the 1.26.0 test harness against the adapter (#902)

* chore(compliance): run the 1.25.0 test harness against the adapter

Bump the harness from 1.20.0 (CI) and 0.10.0 (local docker-compose) to
1.25.0, and declare the 1.25 capabilities the SDK supports:
generated_payloads, log_capture, property_layers and fail_open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(compliance): count the marker's serialized bytes when padding AI properties

* chore(compliance): bump the test harness to 1.26.0

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat: let wrapper SDKs run their own beforeSend on the fully built event (#904)

* chore(api): keep @PostHogInternal declarations out of the API dumps

* feat: let wrapper SDKs build an event, run their own beforeSend, then send it

* fix: start the replay debug window when the wrapper's event is queued

* fix: defer wrapper build state changes and the replay debug claim to send

A built event the wrapper drops no longer caches the screen name, updates the
person properties used for flags, or claims the replay debug bundle. The send
does all three, as capture does, so the window starts with the event that
carries the bundle.

* fix: keep wrapper builds state-free and send each built event once

Build defers person processing to send and records the built name for replay
debug eligibility. Send adds only the throttled replay debug keys (API 23 safe,
keeps keys the wrapper's hook removed), skips flag person properties when the
user changed since the build, and ignores a second send of the same event.

* fix: tie a wrapper send to the build's setup, instance and identity

A send now drops an event this instance didn't build or built before setup()/close(), skips person processing and flag person properties after reset() (also when reuseAnonymousId keeps the distinct id), and adds the replay debug values snapshotted at build.

* fix: don't restore a wrapper screen name after a reset since the build

* fix: add the replay debug bundle to wrapper AI events, like captureAi

* fix: read replay debug state once per wrapper build

The build's required replay keys and the deferred bundle came from two
debugProperties() reads, so a replay transition between them could send
$recording_status=buffering without its hold reason.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix: record the wrapper build's identity before enriching the event

A reset() or identify() during the build left the event with the old
distinct ID while the build recorded the new identity, so the send applied
the old user's $set to the new user's flag properties.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(compliance): read capture responses outside the adapter lock

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: drop the oldest AI events while too many wait to be written to disk (#901)

* fix: drop an AI event while too many wait to be written to disk

One executor both writes and sends AI events, so during a slow upload new
multi-MiB events waited in memory with no limit. captureAi now returns
null while captureAiMaxQueueSize events or 50 MiB are waiting, the same
limits the disk queue keeps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: drop the oldest waiting AI events instead of the new one

While a slow upload holds the AI executor, captureAi now keeps the new
event and drops the oldest events still waiting to be written once
captureAiMaxQueueSize events or 50 MiB wait, the same as the disk queue
already does. captureAi returns null again only for disabled, opted-out,
beforeSend-dropped or oversized events.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: queue no more AI write tasks than events waiting to be written

Dropping a waiting AI event no longer leaves its executor task behind
for nothing: a dropped event's task writes a newer one instead, so the
executor never holds more tasks than events are waiting.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: queue AI write tasks in the order their events were added

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs: tell proxies that strip credentials to keep the project token

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say which capture drop logs need debug and what they print

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: add 402, 404 warning, debug logs and AI queue details to the 4.0 guide

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore: keep PostHogInternal APIs in the api dumps

apiCheck is the only check that flags a changed internal signature that
posthog-server or a wrapper SDK relies on. The dumps list them again, as on main.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: remove leftover conflict markers from the 4.0 guide

* fix: keep the capture result when an event handler throws

* fix: match capture v1 rules for empty names, drop logs, retry identity and typed values (#903)

* fix: match the capture v1 rules for empty names, drop logs, retry identity and typed values

* fix: merge a $groups map in properties into the groups set with group()

* fix: log an empty-name drop once, without the generic dropped-event line

* fix: clamp each duplicates_dropped count to the batch size

* fix: keep the pending retry map off Java 8 Map methods missing on API 23

* fix: keep the personless option over a legacy property and drop a 2xx without results (#905)

* fix: keep the personless option when a legacy person processing property is set

A passed or registered `$process_person_profile` property no longer stops
the SDK from setting `process_person_profile: false` when person
processing is off. The send hoists the property only into an unset
option, so an option set by any layer wins, as the capture spec requires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: treat a 2xx without readable per-event results as a terminal failure

A 2xx whose body can't be read as `{"results": {...}}` no longer counts
the batch as delivered. It throws a PostHogApiError with that status,
which the queue drops without retrying, and logs only the status and
event count, as the capture spec requires.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix: warn about empty and personal API keys without debug and log rejected batches (#909)

* fix: let a caller's $process_person_profile override the personless option

Reverts the person-processing half of #905. A passed or registered
`$process_person_profile` property again stops the SDK from adding
`process_person_profile: false` when person processing is off, so the
caller wins, as the client property precedence in sdk-specs `capture`
(#143) requires. The send still hoists the property into the option.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: log drop reasons per batch and batch migrated legacy events by bytes

- The capture v1 per-batch debug line names the endpoint and counts the
  dropped events by their `details` tag (opaque strings, never uuids or
  content), e.g. `ai_event_too_big: 1, misrouted_event: 1`.
- Sending events cached by 2.x/3.x closes a batch before it would pass
  the 5 MiB capture v1 byte target, as the queue does, so a large
  migrated batch isn't lost to a 413. Order is kept and an event over
  the target ships alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say the token warnings print without debug

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: name the records in the flush drop log

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test: cover the capture rules on the captureStateless path

Also adds the posthog-server changeset for them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: fold the server capture changes into the server changeset

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* docs: keep PostHog keys when a proxy strips Authorization

* fix: only move the current screen for wrapper events built as $screen (#930)

* fix: keep a wrapper event sent for another distinct id out of the current user's person state (#928)

* docs: review beforeSend hooks for the 4.0 event shape (#924)

* docs: ask users to review beforeSend hooks for the 4.0 event shape

* docs: cover replacing event.options in beforeSend

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* feat!: report the device language as $locale and add $app_language (#927)

* feat!: report the device language as $locale and add $app_language

* fix: address the pre-PR review of $locale and $app_language

* docs: correct the $locale migration notes

* fix: read a registered $app_language without relying on merge order

* fix: read locales and the locale config only on the worker thread

Setup and capture no longer make Binder calls or parse the locale config, and
supported locales changed at runtime with setOverrideLocaleConfig are picked up.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: leave out $app_language when several declared regions could match

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: stop reading a registered $app_language on every event

Registered properties are merged later and win anyway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: guard LocaleList calls for the API 23 lint check

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix: read the override locale config only when the locales change

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: say when $app_language is left out with no match

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix: keep events with non-string map keys and align capture v1 logs (#929)

* fix: align capture v1 logs, request IDs and beforeSend handling

* fix: copy arrays and cycles for beforeSend, and keep the hook failure log debug-only

* fix: keep array types in the copy beforeSend hooks get

* fix: keep sorted map lookups and copy primitive arrays for beforeSend

* fix: read an HTTP-date Retry-After

* fix: log the key type when a map key can't be serialized

* docs: link the capture v1 upgrade guide and align the 4.0 guide with the backend guides (#934)

* docs: link the capture v1 SDK upgrade guide

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: add timestamps, UUID v7 and the person-only drop to the 4.0 guide

* docs: align the 4.0 capture v1 guide with the backend SDK guides

Claude-Session: https://claude.ai/code/session_019xLynws83apuXa9LfRy9Aq

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>

* fix!: keep the 3.x PostHogEvent constructor and copy

posthog-server is a minor release and its beforeSend gets PostHogEvent,
so a new trailing constructor parameter broke Java callers and copy
calls compiled against 3.x. options is now a property. When beforeSend
returns a different event with null options, such as a copy, it keeps
the options of the event the hook was given.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* docs: close the 4.0 guide gaps found in the release review

- Remove the two internal TODO comments.
- Say surveys-compose works with core 6.x and 7.0.
- List RRStyle and the trimmed RRWireframe under internal APIs.
- captureAi also returns null for an empty event name.
- Note per-event product quota drops, the per-batch drop reasons and
  the removed per-event "queued" log line.
- Self-hosting needs /i/v1/ai/events for captureAi.
- A whitespace-only token disables the SDK too.
- Describe the 3.x $set/$set_once/$groups replacement rule and the
  registered $set rule.
- KDoc: the AI size drop logs the limit, not the event's size.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants