Repository navigation
feat: let wrapper SDKs run their own beforeSend on the fully built event - #904
turnipdabeets wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 5585bc1: 1 Should fix.
See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.
posthog-android Compliance ReportDate: 2026-10-29T01:38:50.549321+00:00
|
| Test | Status | Duration |
|---|---|---|
| Endpoint And Method.Targets V1 Endpoint | ✅ | 383ms |
| Endpoint And Method.Does Not Use Legacy Endpoints | ✅ | 39ms |
| Required Headers.Has Authorization Bearer Header | ✅ | 26ms |
| Required Headers.Has Content Type Json | ✅ | 25ms |
| Required Headers.Has Posthog Sdk Info Format | ✅ | 25ms |
| Required Headers.Has Posthog Attempt Header | ✅ | 27ms |
| Required Headers.Has Posthog Request Id | ✅ | 24ms |
| Required Headers.Has Posthog Request Timestamp | ✅ | 26ms |
| Required Headers.Has User Agent | ✅ | 25ms |
| Body Format.Body Has Created At And Batch | ✅ | 22ms |
| Body Format.No Api Key In Body | ✅ | 23ms |
| Body Format.No Sent At In Body | ✅ | 23ms |
| Event Format.Event Has Required Root Fields | ✅ | 21ms |
| Event Format.Event Uuid Is Valid | ✅ | 20ms |
| Event Format.Event Timestamp Is Rfc3339 | ✅ | 20ms |
| Event Format.Non Utc Event Timestamp Is Converted To Utc | ✅ | 25ms |
| Event Format.Distinct Id Is String | ✅ | 21ms |
| Event Format.Distinct Id At Root Not Properties | ✅ | 22ms |
| Event Format.Custom Properties Preserved | ✅ | 22ms |
| Event Format.Set Properties Preserved | ✅ | 24ms |
| Event Format.Set Once Properties Preserved | ✅ | 27ms |
| Event Format.Groups Properties Preserved | ✅ | 21ms |
| Event Format.Sdk Generates Uuid If Not Provided | ✅ | 28ms |
| Event Format.Event Has Required Root Fields Batch | ✅ | 30ms |
| Event Format.Event Uuid Is Valid Batch | ✅ | 27ms |
| Event Format.Event Timestamp Is Rfc3339 Batch | ✅ | 25ms |
| Event Format.Distinct Id Is String Batch | ✅ | 29ms |
| Event Format.Distinct Id At Root Not Properties Batch | ✅ | 27ms |
| Event Format.Custom Properties Preserved Batch | ✅ | 27ms |
| Event Format.Set Properties Preserved Batch | ✅ | 27ms |
| Event Format.Set Once Properties Preserved Batch | ✅ | 25ms |
| Event Format.Groups Properties Preserved Batch | ✅ | 26ms |
| Event Format.Sdk Generates Uuid If Not Provided Batch | ✅ | 41ms |
| Batch Behavior.Multiple Events In Single Batch | ✅ | 36ms |
| Batch Behavior.Batch Envelope Smoke | ✅ | 31ms |
| Batch Behavior.Flush With No Events Sends Nothing | ✅ | 16ms |
| Batch Behavior.Flush At Triggers Batch | ✅ | 1023ms |
| Batch Behavior.Created At Reflects Batch Creation Time | ✅ | 25ms |
| Deduplication.Generates Unique Uuids | ✅ | 45ms |
| Deduplication.Different Events Same Content Different Uuids | ✅ | 22ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 7017ms |
| Deduplication.Preserves Timestamp On Retry | ✅ | 7014ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 7024ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 34ms |
| Header Behavior On Retry.Attempt Header Starts At One | ✅ | 18ms |
| Header Behavior On Retry.Attempt Header Increments On Retry | ✅ | 12026ms |
| Header Behavior On Retry.Request Id Preserved On Retry | ✅ | 7017ms |
| Header Behavior On Retry.Different Requests Have Different Request Ids | ✅ | 2028ms |
| Header Behavior On Retry.Request Timestamp Changes On Retry | ✅ | 7018ms |
| Response Format Validation.Success Response Has Uuid Keyed Results | ✅ | 17ms |
| Response Format Validation.Success Response Has Ok For Each Event | ✅ | 22ms |
| Response Format Validation.Success No Retry After When All Ok | ✅ | 18ms |
| Response Format Validation.Success Retry After Present When Retry Events | ✅ | 2010ms |
| Response Format Validation.Success No Retry After When Drop Only | ✅ | 19ms |
| Response Format Validation.Response Echoes Request Id | ✅ | 16ms |
| Retry Behavior.Retries On 408 | ✅ | 7015ms |
| Retry Behavior.Retries On 500 | ✅ | 7017ms |
| Retry Behavior.Retries On 503 | ✅ | 7021ms |
| Retry Behavior.Retries On 504 | ✅ | 7015ms |
| Retry Behavior.Retryable Errors Have Retry After | ✅ | 4014ms |
| Retry Behavior.Respects Retry After On Retryable Error | ✅ | 10024ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 4020ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 4021ms |
| Retry Behavior.Does Not Retry On 402 | ✅ | 4019ms |
| Retry Behavior.Does Not Retry On 413 | ✅ | 4018ms |
| Retry Behavior.Does Not Retry On 415 | ✅ | 4018ms |
| Retry Behavior.Non Retryable Errors Have No Retry After | ✅ | 4018ms |
| Retry Behavior.Implements Backoff | ✅ | 17022ms |
| Retry Behavior.Max Retries Respected | ❌ | 17035ms |
| Partial Batch Handling.Handles 200 Full Success | ✅ | 2015ms |
| Partial Batch Handling.Handles 200 With All Ok | ✅ | 3026ms |
| Partial Batch Handling.Does Not Retry Dropped Events | ✅ | 3023ms |
| Partial Batch Handling.Does Not Retry Limited Events | ✅ | 3023ms |
| Partial Batch Handling.Prunes Ok Events On Partial Retry | ✅ | 7015ms |
| Partial Batch Handling.Prunes Dropped Events On Partial Retry | ✅ | 7017ms |
| Partial Batch Handling.Retries Only Retry Events From Partial | ✅ | 7014ms |
| Partial Batch Handling.Partial Retry Preserves Uuids | ✅ | 7017ms |
| Partial Batch Handling.Partial Retry Attempt Header Increments | ✅ | 7016ms |
| Partial Batch Handling.Partial Retry Request Id Preserved | ✅ | 7014ms |
| Partial Batch Handling.Respects Retry After On Partial | ✅ | 7022ms |
| Partial Batch Handling.Unknown Result Treated As Terminal | ✅ | 3021ms |
| Partial Batch Handling.Mixed Ok Drop Limited No Retry | ✅ | 3029ms |
| 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 | ✅ | 4015ms |
| Event Options.Cookieless Mode Override | ✅ | 16ms |
| Event Options.Disable Skew Correction Override | ✅ | 13ms |
| Event Options.Process Person Profile Override | ✅ | 13ms |
| Event Options.Product Tour Id Override | ✅ | 17ms |
| Event Options.Unset Options Omitted | ✅ | 18ms |
| Event Options.Options Override In Batch | ✅ | 26ms |
| Event Options.Unknown Option Passes Through | ✅ | 17ms |
| Event Options.Option Value Is Not Converted | ✅ | 15ms |
| Event Options.Lenient Bool Option Values Are Not Converted | ✅ | 16ms |
| Event Options.Option Wins Over Legacy Property | ✅ | 19ms |
| Event Options.Legacy Property Fills Unset Option | ✅ | 16ms |
| Event Options.Null Option Falls Back To Legacy Property | ✅ | 15ms |
| Geoip And Historical Migration.Geoip Disable Injected Into Properties | ❌ | 15ms |
| Geoip And Historical Migration.Historical Migration Set In Body | ❌ | 19ms |
| Geoip And Historical Migration.Historical Migration Absent By Default | ✅ | 15ms |
Failures
retry_behavior.max_retries_respected
Expected 4 requests, got 5
geoip_and_historical_migration.geoip_disable_injected_into_properties
Expected $geoip_disable='True', got 'None'. Available properties: ['$is_identified', '$recording_status', '$sdk_debug_pending_queue_size']
geoip_and_historical_migration.historical_migration_set_in_body
Body missing 'historical_migration' field. Body keys: ['created_at', 'batch']
Capture_Ai_V1 Tests
View Details
| Test | Status | Duration |
|---|---|---|
| Endpoint And Method.Targets V1 Endpoint | ✅ | 19ms |
| Endpoint And Method.Does Not Use Legacy Endpoints | ✅ | 17ms |
| Endpoint And Method.Capture Does Not Reroute Ai Named Events | ✅ | 15ms |
| Required Headers.Has Authorization Bearer Header | ✅ | 19ms |
| Required Headers.Has Content Type Json | ✅ | 17ms |
| Required Headers.Has Posthog Sdk Info Format | ✅ | 21ms |
| Required Headers.Has Posthog Attempt Header | ✅ | 18ms |
| Required Headers.Has Posthog Request Id | ✅ | 21ms |
| Required Headers.Has Posthog Request Timestamp | ✅ | 25ms |
| Required Headers.Has User Agent | ✅ | 24ms |
| Body Format.Body Has Created At And Batch | ✅ | 19ms |
| Body Format.No Api Key In Body | ✅ | 20ms |
| Body Format.No Sent At In Body | ✅ | 16ms |
| Event Format.Event Has Required Root Fields | ✅ | 15ms |
| Event Format.Event Uuid Is Valid | ✅ | 23ms |
| Event Format.Event Timestamp Is Rfc3339 | ✅ | 16ms |
| Event Format.Non Utc Event Timestamp Is Converted To Utc | ✅ | 14ms |
| Event Format.Distinct Id Is String | ✅ | 14ms |
| Event Format.Distinct Id At Root Not Properties | ✅ | 13ms |
| Event Format.Custom Properties Preserved | ✅ | 15ms |
| Event Format.Set Properties Preserved | ✅ | 20ms |
| Event Format.Set Once Properties Preserved | ✅ | 19ms |
| Event Format.Groups Properties Preserved | ✅ | 18ms |
| Event Format.Sdk Generates Uuid If Not Provided | ✅ | 18ms |
| Event Format.Capture Ai Keeps Supplied Uuid | ❌ | 11ms |
| Event Format.Event Has Required Root Fields Batch | ❌ | 22ms |
| Event Format.Event Uuid Is Valid Batch | ✅ | 24ms |
| Event Format.Event Timestamp Is Rfc3339 Batch | ✅ | 24ms |
| Event Format.Distinct Id Is String Batch | ✅ | 17ms |
| Event Format.Distinct Id At Root Not Properties Batch | ✅ | 19ms |
| Event Format.Custom Properties Preserved Batch | ✅ | 27ms |
| Event Format.Set Properties Preserved Batch | ✅ | 27ms |
| Event Format.Set Once Properties Preserved Batch | ✅ | 26ms |
| Event Format.Groups Properties Preserved Batch | ✅ | 24ms |
| Event Format.Sdk Generates Uuid If Not Provided Batch | ✅ | 21ms |
| Batch Behavior.Multiple Events In Single Batch | ✅ | 24ms |
| Batch Behavior.Batch Envelope Smoke | ✅ | 24ms |
| Batch Behavior.Flush At Triggers Batch | ✅ | 1017ms |
| Batch Behavior.Created At Reflects Batch Creation Time | ✅ | 14ms |
| Deduplication.Generates Unique Uuids | ✅ | 31ms |
| Deduplication.Different Events Same Content Different Uuids | ✅ | 17ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 7011ms |
| Deduplication.Preserves Timestamp On Retry | ✅ | 7016ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 7016ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 23ms |
| Header Behavior On Retry.Attempt Header Starts At One | ✅ | 13ms |
| Header Behavior On Retry.Attempt Header Increments On Retry | ✅ | 12022ms |
| Header Behavior On Retry.Request Id Preserved On Retry | ✅ | 7017ms |
| 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 | ✅ | 14ms |
| 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 | ✅ | 2009ms |
| Response Format Validation.Success No Retry After When Drop Only | ✅ | 17ms |
| Response Format Validation.Response Echoes Request Id | ✅ | 13ms |
| Retry Behavior.Retries On 408 | ✅ | 7013ms |
| Retry Behavior.Retries On 500 | ✅ | 7016ms |
| Retry Behavior.Retries On 503 | ✅ | 7022ms |
| Retry Behavior.Retries On 504 | ✅ | 7016ms |
| Retry Behavior.Retryable Errors Have Retry After | ✅ | 4011ms |
| Retry Behavior.Respects Retry After On Retryable Error | ✅ | 10019ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 4018ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 4018ms |
| Retry Behavior.Does Not Retry On 402 | ✅ | 4016ms |
| Retry Behavior.Does Not Retry On 413 | ✅ | 4019ms |
| Retry Behavior.Does Not Retry On 415 | ✅ | 4015ms |
| Retry Behavior.Non Retryable Errors Have No Retry After | ✅ | 4016ms |
| Retry Behavior.Implements Backoff | ✅ | 17030ms |
| Retry Behavior.Max Retries Respected | ❌ | 17030ms |
| Partial Batch Handling.Handles 200 Full Success | ✅ | 2015ms |
| Partial Batch Handling.Handles 200 With All Ok | ✅ | 3022ms |
| Partial Batch Handling.Does Not Retry Dropped Events | ✅ | 3021ms |
| Partial Batch Handling.Does Not Retry Limited Events | ✅ | 3018ms |
| Partial Batch Handling.Prunes Ok Events On Partial Retry | ✅ | 7015ms |
| Partial Batch Handling.Prunes Dropped Events On Partial Retry | ✅ | 7015ms |
| Partial Batch Handling.Retries Only Retry Events From Partial | ✅ | 7017ms |
| Partial Batch Handling.Partial Retry Preserves Uuids | ✅ | 7011ms |
| 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 | ✅ | 7018ms |
| Partial Batch Handling.Unknown Result Treated As Terminal | ✅ | 3021ms |
| Partial Batch Handling.Mixed Ok Drop Limited No Retry | ✅ | 3024ms |
| Compression.Sends Gzip Content Encoding | ✅ | 14ms |
| Compression.No Content Encoding When Disabled | ✅ | 11ms |
| Compression.Compressed Body Is Decompressible | ✅ | 13ms |
| Error Handling.Does Not Retry On Unknown 4Xx | ✅ | 4016ms |
| Event Options.Cookieless Mode Override | ✅ | 14ms |
| Event Options.Disable Skew Correction Override | ✅ | 15ms |
| Event Options.Process Person Profile Override | ✅ | 15ms |
| Event Options.Product Tour Id Override | ✅ | 12ms |
| Event Options.Unset Options Omitted | ✅ | 14ms |
| Event Options.Options Override In Batch | ✅ | 18ms |
| Event Options.Unknown Option Passes Through | ✅ | 11ms |
| Event Options.Option Value Is Not Converted | ✅ | 13ms |
| Event Options.Lenient Bool Option Values Are Not Converted | ✅ | 15ms |
| Event Options.Option Wins Over Legacy Property | ✅ | 15ms |
| Event Options.Legacy Property Fills Unset Option | ✅ | 13ms |
| Event Options.Null Option Falls Back To Legacy Property | ✅ | 12ms |
| Geoip And Historical Migration.Geoip Disable Injected Into Properties | ❌ | 13ms |
| Geoip And Historical Migration.Historical Migration Absent By Default | ✅ | 14ms |
Failures
event_format.capture_ai_keeps_supplied_uuid
Expected last action result '0198c0de-0000-7000-8000-000000000abc', got '01a1854b-e652-7e7e-b9be-9eaa6e5bf834'
event_format.event_has_required_root_fields_batch
Expected 1 requests, got 2
retry_behavior.max_retries_respected
Expected 4 requests, got 5
geoip_and_historical_migration.geoip_disable_injected_into_properties
Expected $geoip_disable='True', got 'None'. Available properties: ['$is_identified', '$recording_status', '$sdk_debug_session_start', '$sdk_debug_pending_queue_size']
Feature_Flags Tests
✅ 17/17 tests passed
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 31ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 17ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 18ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 16ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 16ms |
| Request Payload.Groups Round Trip | ✅ | 15ms |
| Request Payload.Groups Default To Empty Object | ✅ | 15ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 15ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 15ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 15ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 5ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 13ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 23ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 19ms |
| Retry Behavior.Retries Flags On 502 | ✅ | 319ms |
| Retry Behavior.Retries Flags On 504 | ✅ | 317ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 17ms |
…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.
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.
There was a problem hiding this comment.
PostHog Review · Standard · reviewed e73e33b: 1 Should fix.
This is review round 3 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.
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.
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 887cae5: 2 Should fix.
This is review round 4 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.
| if (!ai && event.event == PostHogEventName.SCREEN.event) { | ||
| (properties["\$screen_name"] as? String)?.trim()?.takeIf { it.isNotEmpty() }?.let { cacheScreenName(it) } |
There was a problem hiding this comment.
P2 · Guard deferred screen caching against resets
Build a wrapper $screen event, call reset(), then send the event after the asynchronous hook completes. cacheScreenName restores the previous user's screen into the cleared context, so subsequent events and logs inherit that stale $screen_name. Apply the reset-generation guard to this deferred cache update while still delivering the original event.
| built.uuid?.let { uuid -> | ||
| // Snapshotted now, so a new session during the round trip doesn't change them. | ||
| val deferredReplayDebug = | ||
| if (!ai && isReplayDebugEvent(event)) { |
There was a problem hiding this comment.
P2 · Preserve replay debug enrichment on the wrapper AI lane
With an active session and no debug window, wrapper $ai_generation events omit $sdk_debug_session_start and the optional replay bundle because !ai disables deferred enrichment; native captureAi includes them. This breaks the replay-debug contract required by AGENTS.md:46. Remove the AI exclusions in both build and send enrichment.
💡 Motivation and Context
posthog-flutter runs its Dart
beforeSendbefore handing the event to this SDK. So on Android the Dart hook only sees the call's own properties: no SDK or registered properties, session, screen, uuid or timestamp. Anything it changes is overwritten when this SDK fills properties afterwards. Flutter desktop and native Android already run the hook on the full event.This PR adds two internal methods so a wrapper can do the same on Android:
buildEventForWrapper(...)builds the event the waycapture/captureAiwould: same checks, same property fill, a new uuid and timestamp. It doesn't runbeforeSendand doesn't queue the event.sendBuiltEventForWrapper(event, ai)runs the nativebeforeSendchain and queues the event on the analytics or AI lane, keeping its uuid and timestamp. It doesn't fill properties again.capturenow runs through the same pieces (prepare →beforeSend→ enqueue). It behaves the same: the existing suite passes unchanged.Both methods are
@PostHogInternaland@suppressed in KDoc, with no-op defaults onPostHogInterface. To keep them out of the public API, the first commit adds@PostHogInternalto the binary-compatibility validator'snonPublicMarkers. That also drops every existing@PostHogInternaldeclaration from the.apidumps (about 900 lines). Those declarations are still public on the JVM, butapiCheckno longer guards them. That commit stands alone so it's easy to drop or discuss separately.Notes:
capture,captureAi,screenand exceptions sent as$exceptionevents. Building changes no SDK state, so a wrapper can drop the event:sendBuiltEventForWrappercaches a$screenevent's$screen_nameand updates the person properties used for flags, asscreen()andcapturedo.identify,aliasandgrouparen't covered. They change state (distinct id, groups, flag reloads) around their capture call, so splitting them needs its own design.sendBuiltEventForWrapperclaims the full bundle and starts the window when the event is queued, ascapturedoes.sendBuiltEventForWrapper, which is after the wrapper's hook.💚 How did you test it?
PostHogWrapperEventTest:beforeSendor queueingbeforeSendbeforeSenddrops the event, when opted out, or on an opt-out between build and sendsetup()orclose()ran since the build, or when it was already sentreset()counts even whenreuseAnonymousIdkeeps the distinct id), and adds the replay debug values snapshotted at build.$screenhandling./gradlew spotlessApply apiCheck :posthog:test :posthog-android:testDebugUnitTest :posthog-server:testall pass.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
beforeSendon the full event on iOS and Android.beforeSendand rejected it: native hooks are synchronous and a method-channel reply is async, so it would deadlock on the main thread. That approach was also tried and dropped in posthog-flutter Allow configuring SDK HTTP client proxy settings #255.🤖 Generated with Claude Code