Skip to content

feat!: send events through capture v1 and add captureAi - #881

Open
turnipdabeets wants to merge 48 commits into
chore/v4-remove-deprecatedfrom
feat/v4-capture-v1
Open

turnipdabeets wants to merge 48 commits into
chore/v4-remove-deprecatedfrom
feat/v4-capture-v1

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #871 (chore/v4-remove-deprecated).

💡 Motivation and Context

Moves capture to capture v1 for posthog-android 4.0: analytics events go to POST /i/v1/analytics/events and the new captureAi(...) sends AI events to POST /i/v1/ai/events. This matches posthog-ios 4.0 (PostHog/posthog-ios#938) and the posthog-python 8.0, posthog-go 2.0 and posthog-rs 1.0 v1 releases and their AI lanes, and Ingestion's goal of moving the mobile SDKs to the v1 endpoints with captureAi in every v1 release. Flutter and React Native need captureAi on both natives before they can expose it.

This PR now contains the whole capture v1 stack: #884 (captureAi, including #897, which retries 404 and 429) was merged into #882 (per-event options and one property precedence), and #882 was merged into this PR.

Transport

  • Authorization: Bearer <projectToken> plus PostHog-Sdk-Info (<sdkName>/<sdkVersion>, which Flutter, React Native and KMP already set), PostHog-Attempt, PostHog-Request-Id and PostHog-Request-Timestamp. The body is {created_at, batch}, with no api_key or sent_at.
  • $session_id/$window_id move to the event root, and $lib/$lib_version are no longer sent as properties (the server reads them from PostHog-Sdk-Info). A string session/window ID, including "", is sent unchanged; a non-string is dropped with a debug log naming the key and JSON type; null drops silently.
  • The server's per-event results are applied: ok/warning/drop events leave the queue, only retry events are resent, and the response Retry-After is honored for that resend. The queue resends exactly the events of a request that failed or came back with events to retry, before newer ones, so the retry keeps its request ID and created_at and increments the attempt. Retry identity is kept per endpoint, so the analytics and AI lanes don't reset each other's attempt numbers. A 413 halving is a new request. Events missing from the results, and a 2xx whose results can't be read (malformed body, a transfer that failed after the status, or a response over 1 MiB), count as delivered: a resend would duplicate stored events.
  • Network errors and 404, 408, 429, 500, 502, 503 and 504 are retried (fix: retry capture v1 requests that get 404 or 429 #897, following fix: retry capture requests that get 404 or 429 posthog-go#395: drop events only when they can never be delivered or the buffer is full). 3xx and other statuses are terminal. 413 keeps halving the batch.
  • No /batch fallback: a host that doesn't serve the v1 route keeps the events queued (404 is retried) until it does. PostHogApi.batch() stays only for posthog-server, which keeps /batch until its own major. Events cached by pre-3.0 versions (PostHogSendCachedEventsIntegration) and 3.x events still queued on disk are sent through v1 too.
  • Retry-After is honored as a floor, capped at 30 seconds, and the backoff doubles from 1 s to 30 s with ±25% jitter kept in milliseconds. Both apply to every queue (events, replay, logs).
  • Capture follows only 307/308 redirects to the same origin as host, at most 5 hops, re-sending body and headers. Other redirects are terminal. OkHttp would otherwise follow any 3xx, drop Authorization cross-host and turn a 302 into a GET, so automatic redirects are off for the capture client. So is retryOnConnectionFailure: an OkHttp resend would repeat the attempt number and timestamp.
  • Requests carry at most maxBatchSize events and about 5 MiB (checked before appending; an event larger than that is sent on its own), and the queue sends as soon as that much is queued, for analytics and AI.
  • With debug on, drop/retry logging is one count line per batch, with no UUIDs or payloads; a batch dropped by a terminal status gets no count line. Debug logs name the event count and response status, never request or response bodies. With debug on, each event a 2xx drops is logged with its name, uuid and the server's details (e.g. misrouted_event, ai_event_too_big), never its properties.

Per-event options (new public API)

  • options: Map<String, Any?>? = null as the last parameter of capture, screen, identify, setPersonProperties, alias, group and captureException on PostHogInterface, PostHog and the PostHog companion, and PostHogEvent.options (persisted in the queue, editable in beforeSend). For identify the options apply to the $identify or $set event it sends; for group to $groupidentify; for alias to $create_alias.
  • Known keys: process_person_profile, cookieless_mode, disable_skew_correction and product_tour_id. Any other key is sent as-is; values are never coerced (the server parses them leniently).
  • The legacy $process_person_profile, $cookieless_mode, $ignore_sent_at and $product_tour_id properties are converted when the request is built, after beforeSend. Each is always removed from properties and only fills an option that is unset or null.
  • Precedence, lowest first: legacy property, SDK default, caller option, beforeSend. An option set to null counts as unset.
  • The SDK sets process_person_profile: false only when person processing is off (personProfiles = NEVER, or IDENTIFIED_ONLY before identify), and no longer writes a $process_person_profile property on every event. A null legacy value keeps the personless default.
  • The 3-arg identify moved from PostHogCoreInterface to PostHogStatelessInterface; PostHogInterface has the 4-arg one.

Property precedence (PostHog/sdk-specs#143)

  • Properties the SDK sets itself (context, SDK info, feature flags, $screen_name, session id, $is_identified, replay debug values) → registered super properties → the properties passed to the call (userProperties, userPropertiesSetOnce and groups are per-call values) → beforeSend. In 3.x the SDK's values beat super properties, and $is_identified, $process_person_profile and the replay debug values also beat caller properties.
  • A blank caller $screen_name, $session_id or distinct_id still replaces the SDK's value, as in 3.x.

captureAi (new public API)

  • captureAi takes the same arguments as capture (including options) and goes through the same enrichment, precedence, personless default and beforeSend. Only the queue differs. capture never reroutes $ai_* events; a captureAi name that doesn't start with $ai_ logs a debug line and is sent unchanged (the server reports misrouted_event).
  • AI events go to their own disk queue (posthog-disk-ai-queue, PostHogConfig.aiStoragePrefix) with its own executor and retry state, created at setup so events left on disk by a previous launch still send, and flushed by flush() and close(). A failing or slow AI lane never delays or drops analytics events.
  • captureAi returns the queued event's uuid, or null when the event isn't queued: disabled, opted out, dropped by beforeSend, or over the size limit.
  • A serialized AI event over captureAiMaxEventBytes, measured before encryption, is dropped in captureAi, which logs (debug) only the event name and the limit. The AI queue keeps at most captureAiMaxQueueSize events and 50 MiB on disk, dropping the oldest first.
  • New PostHogConfig settings, named after the server SDKs': captureAiCompression (gzip), captureAiMaxQueueSize (100), captureAiTimeout (60 s read/write idle timeouts on a derived OkHttp client), captureAiMaxEventBytes (8 MiB + 64 KiB for the whole serialized event: PostHog's 8 MiB properties limit plus room for the rest; it can only lower, and a larger value logs a debug line). Non-positive values fall back to the defaults.

Other changes in the stack

  • maxRetries is renamed pushNotificationSubscriptionMaxRetries (no alias): it only limits push-subscription retries. Event, replay and log retries have no limit, as before.
  • A projectToken starting with phx_ (a personal API key) disables the SDK at setup, like an empty token. posthog-server inherits this through PostHogStateless.setup, so it gets its own patch changeset.
  • An empty or whitespace-only distinctId argument to capture uses the current distinct ID instead of dropping the event.
  • An event beforeSend returns keeps the uuid of the capture it came from, so a hook that returns a cached earlier event can't repeat a uuid.
  • A $set/$identify sent with process_person_profile: false neither stores nor matches the person-properties dedup hash.
  • The minimal $feature_flag_called event also keeps $cookieless_mode, $ignore_sent_at and $product_tour_id, so they survive until they are converted to options.
  • Deep Link Opened skips query parameters whose name starts with $, so a link can't set a PostHog property such as $process_person_profile.
  • An unserializable property is logged by key and type, never by value.
  • Compliance adapter: advertises capture_v1, capture_ai_v1, event_options and encoding_gzip; tracks both v1 routes; forwards options and timestamp; runs with personProfiles = ALWAYS like a server SDK. The workflow pins harness 1.20.0 and also runs for PRs into chore/v4-*/feat/v4-*.
  • docs/migration-3.x-to-4.0.md and the 4.0 changeset bullets.

Breaking changes

Change Who notices
Endpoint moves to /i/v1/analytics/events with Bearer auth; no /batch fallback Reverse proxies, allowlists, WAF rules and self-hosted instances must serve the new path. A 404 keeps events queued; a 403/405 drops them.
Capture sends Authorization: Bearer, replacing a custom Authorization from requestHeaders Proxies that authenticate with Authorization need another header
3xx is terminal, 404 is retried; Retry-After capped at 30 s; jittered backoff Redirecting or rate-limited setups; log-based expectations on retry timing
Only same-origin 307/308 redirects are followed host pointing at something that redirects elsewhere
Legacy option properties are sent as options; the server drops events with unreadable values Apps that set these properties to odd values
New trailing options parameter on seven PostHogInterface methods, PostHogEvent.options constructor parameter, new captureAi Java callers (pass null), anything implementing PostHogInterface, Java callers of the full PostHogEvent constructor / copy
PostHogCoreInterface.identify moved to PostHogStatelessInterface / PostHogInterface Code that holds a PostHogCoreInterface reference and calls identify
Super and event properties override SDK-set properties, including $is_identified, $process_person_profile and replay debug values Apps that register or pass those keys and expected the SDK's value
$process_person_profile is sent only when person processing is off, as an option Filters on $process_person_profile = true
beforeSend can't change an event's uuid; $-prefixed deep link parameters are skipped Hooks that set uuids; links that set PostHog properties
maxRetries → pushNotificationSubscriptionMaxRetries; phx_ tokens disable the SDK Apps that set it; misconfigured apps (they got 401s before)

All of these are in .changeset/android-v4-major.md (plus .changeset/server-personal-api-key-token.md) and in docs/migration-3.x-to-4.0.md.

Differences from the spec and the other v1 SDKs (same as posthog-ios 4.0 unless noted)

  • No retry limit: events stay in the persisted queue bounded by maxQueueSize. Server SDKs give up after 4 attempts.
  • 413 halves the batch until a single oversized event is dropped (proxies often cap below 10 MiB).
  • Backoff starts at 1 s, not 100 ms, with jitter in milliseconds.
  • Gzip on by default, for analytics (compression) and AI (captureAiCompression).
  • No global or context options. A registered legacy $ key becomes its option at send time.
  • A caller or registered legacy $process_person_profile suppresses the SDK's personless option instead of losing to it (fixed in fix: keep the personless option over a legacy property and drop a 2xx without results #905).
  • A 2xx whose results can't be read counts as delivered; the spec makes it a terminal failure of the batch (fixed in fix: keep the personless option over a legacy property and drop a 2xx without results #905).
  • Typed $set/$set_once/$groups call values beat a registered property of the same key; posthog-ios keeps the registered one.
  • process_person_profile: false goes in options, not properties. capture/spec.md "Caller can't override person-processing state" expects the property; the compliance adapter runs with personProfiles = ALWAYS.
  • captureAi (the capture-ai spec covers server SDKs): no uuid argument, like Android capture, but it returns the uuid; oversize events are dropped at the call; the queue is created at setup, not lazily; the queue limit is 100 events and 50 MiB, not 1000 events.

💚 How did you test it?

  • New suites cover the stack:
    • PostHogCaptureV1Test: the wire format and headers, $lib strip, session/window lift, legacy-property hoisting and precedence, per-event results and the kept retry set, retry identity, status classification (404 and 429 retried), redirects, 2xx with unreadable results or a failed transfer, Retry-After on a 2xx, the AI path and the per-event drop log.
    • PostHogQueueTest, PostHogQueueDurabilityTest: partial retention, jitter bounds, the 30 s Retry-After cap, the partial-retry Retry-After floor, and a fatal event sent while a request waits to be retried.
    • PostHogCaptureOptionsTest: options reaching PostHogEvent.options for every method, beforeSend edits, the personless option under each personProfiles mode, precedence, the blank-distinctId fallback, the dedup hash with a personless $set (including lenient false values such as "false" and 0), the allowlist and uuid stability.
    • PostHogCaptureAiTest: routing, beforeSend and options, 404 retried, oversize events dropped when queued, splitting at the byte limit, flushing once a batch's worth of bytes is queued, the return value, per-lane timeout and compression, separate retry identity, flush()/close() draining the AI queue, an event with exactly 8 MiB of properties.
    • PostHogQueueByteLimitsTest: byte accounting on add/load/remove (including a file deleted from disk), eviction of the oldest, check-before-append windows, an oversize record alone.
    • PostHogSendCachedLegacyEventsIntegrationTest: the legacy queue on v1.
  • The Android event-shape snapshot was re-recorded on purpose (v1 envelope, options, no $lib or $process_person_profile).
  • After merging feat!: add captureAi on the capture v1 AI endpoint #884 and feat!: add per-event capture options and one property precedence #882 and the latest chore/v4-remove-deprecated (chore!: pass the load result to the feature flags callback #877 conflicted only in the changeset): ./gradlew apiCheck spotlessCheck :posthog:test :posthog-android:testReleaseUnitTest :posthog-server:test pass locally. The compliance adapter compiles.
  • New tests for the last review round were checked against the old code: each fails without its fix.
  • Compliance harness 1.20.0 in CI on de16564: 212/219 (capture_v1 98/101, capture_ai_v1 97/101, feature_flags 17/17). The failures:
    • max_retries_respected on both suites: there's no retry limit on purpose. The analytics one is timing-dependent: the 1 s, 2 s, 4 s, 8 s backoff with ±25% jitter puts a 5th attempt at about 15 s, the edge of the harness wait, so it passed on feat!: add captureAi on the capture v1 AI endpoint #884's run.
    • capture_ai_keeps_supplied_uuid: captureAi takes no uuid. The test aborts before flushing, so its request lands in the next test and fails event_has_required_root_fields_batch too.
    • geoip_disable_injected_into_properties on both suites (no SDK surface) and historical_migration_set_in_body (no historical-migration mode).
  • CI passed on feat!: add captureAi on the capture v1 AI endpoint #884 (Build Job, compliance) before it was merged.
  • Not done: docs for captureAi on posthog.com / LLM analytics; a device run against PostHog Cloud.

📝 Checklist

  • All commits are signed
  • 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.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file (bullets added to .changeset/android-v4-major.md, plus .changeset/server-personal-api-key-token.md)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Built with Claude Code. Subagents implemented the transport, options and AI lane against the capture v1 server contract, the harness capture_v1/capture_ai_v1 contracts, sdk-specs#143, the sdk-specs capture-ai spec and the posthog-ios 4.0 branch; review cycles on each PR of the stack fed fixes back. #884 and #882 were squash-merged into this branch after approval.

Maintainer decisions: no /batch fallback from the start (iOS added and then removed one); keep PostHogApi.batch() for posthog-server; keep unlimited retries and 413 halving as mobile divergences; retry 404 and 429 (posthog-go#395); add a public options API in 4.0 and move identify off PostHogCoreInterface rather than carry two overloads; keep the uuid pin client-only; a separate AI lane with a small disk queue, config named after the server SDKs.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 24, 2026

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 71d07c1: 2 Must fix, 2 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

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.

Comment thread posthog/src/main/java/com/posthog/PostHogEvent.kt
Comment thread posthog/src/main/java/com/posthog/internal/PostHogSendCachedEventsIntegration.kt Outdated
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-10-29T21:52:25.680387+00:00
Duration: 474226ms

✅ 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 ✅ 282ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 21ms
Required Headers.Has Authorization Bearer Header ✅ 22ms
Required Headers.Has Content Type Json ✅ 24ms
Required Headers.Has Posthog Sdk Info Format ✅ 21ms
Required Headers.Has Posthog Attempt Header ✅ 18ms
Required Headers.Has Posthog Request Id ✅ 17ms
Required Headers.Has Posthog Request Timestamp ✅ 22ms
Required Headers.Has User Agent ✅ 15ms
Body Format.Body Has Created At And Batch ✅ 14ms
Body Format.No Api Key In Body ✅ 17ms
Body Format.No Sent At In Body ✅ 14ms
Event Format.Event Has Required Root Fields ✅ 17ms
Event Format.Event Uuid Is Valid ✅ 15ms
Event Format.Event Timestamp Is Rfc3339 ✅ 16ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 17ms
Event Format.Distinct Id Is String ✅ 13ms
Event Format.Distinct Id At Root Not Properties ✅ 15ms
Event Format.Custom Properties Preserved ✅ 16ms
Event Format.Set Properties Preserved ✅ 19ms
Event Format.Set Once Properties Preserved ✅ 19ms
Event Format.Groups Properties Preserved ✅ 19ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 18ms
Event Format.Event Has Required Root Fields Batch ✅ 22ms
Event Format.Event Uuid Is Valid Batch ✅ 17ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 21ms
Event Format.Distinct Id Is String Batch ✅ 22ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 19ms
Event Format.Custom Properties Preserved Batch ✅ 20ms
Event Format.Set Properties Preserved Batch ✅ 20ms
Event Format.Set Once Properties Preserved Batch ✅ 23ms
Event Format.Groups Properties Preserved Batch ✅ 19ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 20ms
Batch Behavior.Multiple Events In Single Batch ✅ 28ms
Batch Behavior.Batch Envelope Smoke ✅ 41ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 15ms
Batch Behavior.Flush At Triggers Batch ✅ 1020ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 22ms
Deduplication.Generates Unique Uuids ✅ 24ms
Deduplication.Different Events Same Content Different Uuids ✅ 16ms
Deduplication.Preserves Uuid On Retry ✅ 7013ms
Deduplication.Preserves Timestamp On Retry ✅ 7013ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7024ms
Deduplication.No Duplicate Events In Batch ✅ 26ms
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 ✅ 7015ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 2023ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 7017ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 16ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 20ms
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 ✅ 18ms
Response Format Validation.Response Echoes Request Id ✅ 15ms
Retry Behavior.Retries On 408 ✅ 7011ms
Retry Behavior.Retries On 500 ✅ 7016ms
Retry Behavior.Retries On 503 ✅ 7017ms
Retry Behavior.Retries On 504 ✅ 7013ms
Retry Behavior.Retries On 404 ✅ 7014ms
Retry Behavior.Retries On 429 ✅ 7017ms
Retry Behavior.Retries On 502 ✅ 6013ms
Retry Behavior.Retryable Errors Have Retry After ✅ 4011ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10014ms
Retry Behavior.Does Not Retry On 400 ✅ 4015ms
Retry Behavior.Does Not Retry On 401 ✅ 4016ms
Retry Behavior.Does Not Retry On 402 ✅ 4015ms
Retry Behavior.Does Not Retry On 413 ✅ 4016ms
Retry Behavior.Does Not Retry On 415 ✅ 4018ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4017ms
Retry Behavior.Implements Backoff ✅ 14029ms
Partial Batch Handling.Handles 200 Full Success ✅ 2016ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3028ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3022ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 3023ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7014ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7016ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7011ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7016ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7015ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7019ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7023ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3024ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 3026ms
Compression.Sends Gzip Content Encoding ✅ 16ms
Compression.No Content Encoding When Disabled ✅ 20ms
Compression.Compressed Body Is Decompressible ✅ 20ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4024ms
Event Options.Cookieless Mode Override ✅ 13ms
Event Options.Disable Skew Correction Override ✅ 11ms
Event Options.Process Person Profile Override ✅ 10ms
Event Options.Product Tour Id Override ✅ 10ms
Event Options.Unset Options Omitted ✅ 10ms
Event Options.Options Override In Batch ✅ 15ms
Event Options.Unknown Option Passes Through ✅ 12ms
Event Options.Option Value Is Not Converted ✅ 14ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 19ms
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 ✅ 18ms
Property Layers.Caller Property Beats Registered Property ✅ 17ms
Property Layers.Before Send Runs Last ✅ 22ms
Fail Open.Empty Api Key Sends Nothing ✅ 3017ms
Fail Open.Personal Api Key Sends Nothing ✅ 3010ms
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 ✅ 15ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 13ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 13ms
Endpoint And Method.Capture Ai Does Not Reroute Non Ai Events ✅ 12ms
Lane Isolation.Failing Ai Lane Does Not Delay Analytics ✅ 7025ms
Required Headers.Has Authorization Bearer Header ✅ 14ms
Required Headers.Has Content Type Json ✅ 14ms
Required Headers.Has Posthog Sdk Info Format ✅ 14ms
Required Headers.Has Posthog Attempt Header ✅ 14ms
Required Headers.Has Posthog Request Id ✅ 13ms
Required Headers.Has Posthog Request Timestamp ✅ 15ms
Required Headers.Has User Agent ✅ 11ms
Body Format.Body Has Created At And Batch ✅ 11ms
Body Format.No Api Key In Body ✅ 10ms
Body Format.No Sent At In Body ✅ 10ms
Event Format.Event Has Required Root Fields ✅ 9ms
Event Format.Event Uuid Is Valid ✅ 13ms
Event Format.Event Timestamp Is Rfc3339 ✅ 12ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 13ms
Event Format.Distinct Id Is String ✅ 11ms
Event Format.Distinct Id At Root Not Properties ✅ 10ms
Event Format.Custom Properties Preserved ✅ 11ms
Event Format.Set Properties Preserved ✅ 12ms
Event Format.Set Once Properties Preserved ✅ 13ms
Event Format.Groups Properties Preserved ✅ 14ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 16ms
Event Format.Event Has Required Root Fields Batch ✅ 14ms
Event Format.Event Uuid Is Valid Batch ✅ 13ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 12ms
Event Format.Distinct Id Is String Batch ✅ 18ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 16ms
Event Format.Custom Properties Preserved Batch ✅ 19ms
Event Format.Set Properties Preserved Batch ✅ 21ms
Event Format.Set Once Properties Preserved Batch ✅ 18ms
Event Format.Groups Properties Preserved Batch ✅ 23ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 18ms
Batch Behavior.Multiple Events In Single Batch ✅ 25ms
Batch Behavior.Batch Envelope Smoke ✅ 19ms
Batch Behavior.Flush At Triggers Batch ✅ 1015ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 13ms
Deduplication.Generates Unique Uuids ✅ 17ms
Deduplication.Different Events Same Content Different Uuids ✅ 13ms
Deduplication.Preserves Uuid On Retry ✅ 7010ms
Deduplication.Preserves Timestamp On Retry ✅ 7015ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7015ms
Deduplication.No Duplicate Events In Batch ✅ 21ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 13ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 12015ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 7015ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 2022ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 7015ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 12ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 20ms
Response Format Validation.Success No Retry After When All Ok ✅ 13ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2007ms
Response Format Validation.Success No Retry After When Drop Only ✅ 16ms
Response Format Validation.Response Echoes Request Id ✅ 10ms
Retry Behavior.Retries On 408 ✅ 7013ms
Retry Behavior.Retries On 500 ✅ 7014ms
Retry Behavior.Retries On 503 ✅ 7023ms
Retry Behavior.Retries On 504 ✅ 7014ms
Retry Behavior.Retries On 404 ✅ 6013ms
Retry Behavior.Retries On 429 ✅ 6012ms
Retry Behavior.Retries On 502 ✅ 7014ms
Retry Behavior.Retryable Errors Have Retry After ✅ 4011ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10013ms
Retry Behavior.Does Not Retry On 400 ✅ 4014ms
Retry Behavior.Does Not Retry On 401 ✅ 4014ms
Retry Behavior.Does Not Retry On 402 ✅ 4015ms
Retry Behavior.Does Not Retry On 413 ✅ 4016ms
Retry Behavior.Does Not Retry On 415 ✅ 4015ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4017ms
Retry Behavior.Implements Backoff ✅ 14012ms
Partial Batch Handling.Handles 200 Full Success ✅ 2011ms
Partial Batch Handling.Handles 200 With All Ok ✅ 3018ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 3017ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 3021ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7014ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7010ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7013ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7014ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7012ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7014ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7019ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 3019ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 3022ms
Compression.Sends Gzip Content Encoding ✅ 12ms
Compression.No Content Encoding When Disabled ✅ 14ms
Compression.Compressed Body Is Decompressible ✅ 15ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4015ms
Event Options.Cookieless Mode Override ✅ 11ms
Event Options.Disable Skew Correction Override ✅ 9ms
Event Options.Process Person Profile Override ✅ 10ms
Event Options.Product Tour Id Override ✅ 9ms
Event Options.Unset Options Omitted ✅ 11ms
Event Options.Options Override In Batch ✅ 14ms
Event Options.Unknown Option Passes Through ✅ 11ms
Event Options.Option Value Is Not Converted ✅ 11ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 10ms
Event Options.Option Wins Over Legacy Property ✅ 9ms
Event Options.Legacy Property Fills Unset Option ✅ 10ms
Event Options.Null Option Falls Back To Legacy Property ✅ 12ms
Event Size.Event Over Ceiling Is Not Sent ✅ 3258ms
Event Size.Over Ceiling Drop Is Reported Or Logged Without Content ✅ 2040ms
Event Size.Batches Close Before Byte Target ✅ 6061ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 14ms
Property Layers.Caller Property Beats Registered Property ✅ 11ms
Property Layers.Before Send Runs Last ✅ 11ms
Fail Open.Empty Api Key Sends Nothing ✅ 1006ms
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 ✅ 29ms
Request Payload.Flags Request Uses V2 Query Param ✅ 17ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 16ms
Request Payload.Flags Request Omits Authorization Header ✅ 16ms
Request Payload.Token In Flags Body Matches Init ✅ 16ms
Request Payload.Groups Round Trip ✅ 13ms
Request Payload.Groups Default To Empty Object ✅ 21ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 16ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 13ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 15ms
Request Lifecycle.No Flags Request On Init Alone ✅ 6ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 12ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 20ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 17ms
Retry Behavior.Retries Flags On 502 ✅ 319ms
Retry Behavior.Retries Flags On 504 ✅ 314ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 15ms

@turnipdabeets
turnipdabeets force-pushed the feat/v4-capture-v1 branch 2 times, most recently from e4e5fc5 to 8a765aa Compare October 24, 2026 18:13
@turnipdabeets
turnipdabeets changed the base branch from chore/v4-remove-deprecated to main October 26, 2026 18:03
@turnipdabeets
turnipdabeets added this pull request to stack #888 October 26, 2026 18:03
@turnipdabeets
turnipdabeets removed this pull request from stack #888 October 26, 2026 18:06
@turnipdabeets
turnipdabeets changed the base branch from main to chore/v4-remove-deprecated October 26, 2026 18:06
@turnipdabeets
turnipdabeets added this pull request to stack #890 October 26, 2026 18:06
@turnipdabeets
turnipdabeets marked this pull request as ready for review October 26, 2026 18:44
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 26, 2026 18:44

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 483626f: no findings. This pull request is large, so the review ran in 4 parts with less depth than usual.

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed c7fec3a: 1 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

Comment thread posthog/src/main/java/com/posthog/internal/PostHogApi.kt
Comment thread posthog/src/main/java/com/posthog/internal/PostHogApi.kt Outdated
@veria-ai

veria-ai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

Comment thread posthog/src/main/java/com/posthog/internal/PostHogQueue.kt Outdated
Comment thread posthog/src/test/java/com/posthog/internal/PostHogCaptureV1Test.kt
Comment thread docs/migration-3.x-to-4.0.md Outdated

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 1426e51: 1 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

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.

Comment thread posthog/src/main/java/com/posthog/internal/PostHogApi.kt
turnipdabeets and others added 2 commits October 27, 2026 12:58
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]>
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]>
turnipdabeets and others added 9 commits October 27, 2026 18:44
java.util.Base64 isn't on API 23, so animalsnifferTest failed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The data class hashCode calls Double.hashCode(double), which isn't on API 23.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
* 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]>
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]>

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 3106d18: 6 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

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.

Comment thread posthog/src/main/java/com/posthog/PostHog.kt Outdated
Comment thread posthog/src/main/java/com/posthog/PostHog.kt
Comment thread posthog/src/main/java/com/posthog/PostHog.kt Outdated
Comment thread posthog/src/main/java/com/posthog/internal/PostHogQueue.kt
turnipdabeets and others added 5 commits October 28, 2026 17:06
… feat/v4-capture-v1

# Conflicts:
#	.changeset/android-v4-major.md
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]>
…n 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]>
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]>
turnipdabeets and others added 2 commits October 29, 2026 11:47
)

* 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]>
…ent (#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]>

@posthog posthog Bot 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.

PostHog Review · Standard · reviewed 565a80d: 2 Should fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

This is review round 5 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.

Comment thread posthog/src/main/java/com/posthog/PostHog.kt Outdated
turnipdabeets and others added 3 commits October 29, 2026 17:27
… feat/v4-capture-v1

# Conflicts:
#	.changeset/android-v4-major.md
#	posthog/api/posthog.api
#	posthog/src/main/java/com/posthog/PostHog.kt
#	posthog/src/test/java/com/posthog/PostHogBootstrapTest.kt
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]>

This branch has not been deployed

No deployments
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.

3 participants