Skip to content

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

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, with the wrapper suite (20 tests), using swift test --no-parallel -Xswiftc -DTESTING --filter PostHogWrapperEventTest on macOS, not a simulator or device. swiftformat --lint is clean. I didn't run the full suite for this change.

📝 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-ios Compliance Report

Date: 2026-11-03T01:05:05.507541+00:00
Duration: 952779ms

✅ All Tests Passed!

233/233 tests passed


Capture_V1 Tests

✅ 106/106 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 2893ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2863ms
Required Headers.Has Authorization Bearer Header ✅ 400ms
Required Headers.Has Content Type Json ✅ 2736ms
Required Headers.Has Posthog Sdk Info Format ✅ 2777ms
Required Headers.Has Posthog Attempt Header ✅ 2729ms
Required Headers.Has Posthog Request Id ✅ 2844ms
Required Headers.Has Posthog Request Timestamp ✅ 2750ms
Required Headers.Has User Agent ✅ 2811ms
Body Format.Body Has Created At And Batch ✅ 2814ms
Body Format.No Api Key In Body ✅ 2718ms
Body Format.No Sent At In Body ✅ 2735ms
Event Format.Event Has Required Root Fields ✅ 2777ms
Event Format.Event Uuid Is Valid ✅ 2731ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2786ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2821ms
Event Format.Distinct Id Is String ✅ 2805ms
Event Format.Distinct Id At Root Not Properties ✅ 2812ms
Event Format.Custom Properties Preserved ✅ 2813ms
Event Format.Set Properties Preserved ✅ 356ms
Event Format.Set Once Properties Preserved ✅ 2740ms
Event Format.Groups Properties Preserved ✅ 2657ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2745ms
Event Format.Event Has Required Root Fields Batch ✅ 2854ms
Event Format.Event Uuid Is Valid Batch ✅ 2869ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2902ms
Event Format.Distinct Id Is String Batch ✅ 2827ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2778ms
Event Format.Custom Properties Preserved Batch ✅ 2727ms
Event Format.Set Properties Preserved Batch ✅ 2851ms
Event Format.Set Once Properties Preserved Batch ✅ 2819ms
Event Format.Groups Properties Preserved Batch ✅ 2884ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2742ms
Batch Behavior.Multiple Events In Single Batch ✅ 2998ms
Batch Behavior.Batch Envelope Smoke ✅ 2873ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 318ms
Batch Behavior.Flush At Triggers Batch ✅ 1167ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2708ms
Deduplication.Generates Unique Uuids ✅ 2911ms
Deduplication.Different Events Same Content Different Uuids ✅ 2789ms
Deduplication.Preserves Uuid On Retry ✅ 9225ms
Deduplication.Preserves Timestamp On Retry ✅ 9150ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7855ms
Deduplication.No Duplicate Events In Batch ✅ 2870ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 396ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 15745ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9332ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 4969ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9331ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2730ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2763ms
Response Format Validation.Success No Retry After When All Ok ✅ 2740ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2770ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2839ms
Response Format Validation.Response Echoes Request Id ✅ 398ms
Retry Behavior.Retries On 408 ✅ 9249ms
Retry Behavior.Retries On 500 ✅ 9278ms
Retry Behavior.Retries On 503 ✅ 11620ms
Retry Behavior.Retries On 504 ✅ 5398ms
Retry Behavior.Retries On 404 ✅ 8892ms
Retry Behavior.Retries On 429 ✅ 9327ms
Retry Behavior.Retries On 502 ✅ 8715ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6282ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 8345ms
Retry Behavior.Does Not Retry On 400 ✅ 2322ms
Retry Behavior.Does Not Retry On 401 ✅ 4827ms
Retry Behavior.Does Not Retry On 402 ✅ 4753ms
Retry Behavior.Does Not Retry On 413 ✅ 2409ms
Retry Behavior.Does Not Retry On 415 ✅ 4806ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4794ms
Retry Behavior.Implements Backoff ✅ 12365ms
Partial Batch Handling.Handles 200 Full Success ✅ 4674ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5860ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5909ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5900ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7906ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7729ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7872ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7796ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7866ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7784ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7711ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5838ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 5848ms
Compression.Sends Gzip Content Encoding ✅ 2746ms
Compression.No Content Encoding When Disabled ✅ 2671ms
Compression.Compressed Body Is Decompressible ✅ 2694ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4818ms
Event Options.Cookieless Mode Override ✅ 2815ms
Event Options.Disable Skew Correction Override ✅ 314ms
Event Options.Process Person Profile Override ✅ 2805ms
Event Options.Product Tour Id Override ✅ 2780ms
Event Options.Unset Options Omitted ✅ 2671ms
Event Options.Options Override In Batch ✅ 2757ms
Event Options.Unknown Option Passes Through ✅ 2778ms
Event Options.Option Value Is Not Converted ✅ 293ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2801ms
Event Options.Option Wins Over Legacy Property ✅ 2803ms
Event Options.Legacy Property Fills Unset Option ✅ 413ms
Event Options.Null Option Falls Back To Legacy Property ✅ 357ms
Property Layers.Caller Property Beats Registered Property ✅ 2761ms
Property Layers.Before Send Runs Last ✅ 2731ms
Fail Open.Empty Api Key Sends Nothing ✅ 1260ms
Fail Open.Personal Api Key Sends Nothing ✅ 1260ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2854ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2771ms

Capture_Ai_V1 Tests

✅ 110/110 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 2842ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 348ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 2722ms
Endpoint And Method.Capture Ai Does Not Reroute Non Ai Events ✅ 2812ms
Lane Isolation.Failing Ai Lane Does Not Delay Analytics ✅ 11307ms
Required Headers.Has Authorization Bearer Header ✅ 2805ms
Required Headers.Has Content Type Json ✅ 2821ms
Required Headers.Has Posthog Sdk Info Format ✅ 2806ms
Required Headers.Has Posthog Attempt Header ✅ 2694ms
Required Headers.Has Posthog Request Id ✅ 2786ms
Required Headers.Has Posthog Request Timestamp ✅ 2768ms
Required Headers.Has User Agent ✅ 2780ms
Body Format.Body Has Created At And Batch ✅ 2723ms
Body Format.No Api Key In Body ✅ 2811ms
Body Format.No Sent At In Body ✅ 2762ms
Event Format.Event Has Required Root Fields ✅ 2801ms
Event Format.Event Uuid Is Valid ✅ 2754ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2881ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2838ms
Event Format.Distinct Id Is String ✅ 2910ms
Event Format.Distinct Id At Root Not Properties ✅ 553ms
Event Format.Custom Properties Preserved ✅ 382ms
Event Format.Set Properties Preserved ✅ 2985ms
Event Format.Set Once Properties Preserved ✅ 2889ms
Event Format.Groups Properties Preserved ✅ 2809ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2830ms
Event Format.Event Has Required Root Fields Batch ✅ 2891ms
Event Format.Event Uuid Is Valid Batch ✅ 2904ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2902ms
Event Format.Distinct Id Is String Batch ✅ 567ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 3028ms
Event Format.Custom Properties Preserved Batch ✅ 2809ms
Event Format.Set Properties Preserved Batch ✅ 2876ms
Event Format.Set Once Properties Preserved Batch ✅ 2827ms
Event Format.Groups Properties Preserved Batch ✅ 2985ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 611ms
Batch Behavior.Multiple Events In Single Batch ✅ 3103ms
Batch Behavior.Batch Envelope Smoke ✅ 2922ms
Batch Behavior.Flush At Triggers Batch ✅ 1291ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2974ms
Deduplication.Generates Unique Uuids ✅ 3008ms
Deduplication.Different Events Same Content Different Uuids ✅ 2905ms
Deduplication.Preserves Uuid On Retry ✅ 9189ms
Deduplication.Preserves Timestamp On Retry ✅ 5446ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7933ms
Deduplication.No Duplicate Events In Batch ✅ 3095ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2846ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16775ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9385ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 2831ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 5498ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2870ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 3012ms
Response Format Validation.Success No Retry After When All Ok ✅ 2904ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2902ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2976ms
Response Format Validation.Response Echoes Request Id ✅ 2742ms
Retry Behavior.Retries On 408 ✅ 9263ms
Retry Behavior.Retries On 500 ✅ 9375ms
Retry Behavior.Retries On 503 ✅ 5413ms
Retry Behavior.Retries On 504 ✅ 9182ms
Retry Behavior.Retries On 404 ✅ 9086ms
Retry Behavior.Retries On 429 ✅ 5483ms
Retry Behavior.Retries On 502 ✅ 9298ms
Retry Behavior.Retryable Errors Have Retry After ✅ 2467ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10831ms
Retry Behavior.Does Not Retry On 400 ✅ 4869ms
Retry Behavior.Does Not Retry On 401 ✅ 2503ms
Retry Behavior.Does Not Retry On 402 ✅ 2496ms
Retry Behavior.Does Not Retry On 413 ✅ 4873ms
Retry Behavior.Does Not Retry On 415 ✅ 4875ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4874ms
Retry Behavior.Implements Backoff ✅ 18314ms
Partial Batch Handling.Handles 200 Full Success ✅ 2435ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5990ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5819ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5921ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 8026ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 8002ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7969ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7827ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7906ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7930ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7961ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5921ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 5878ms
Compression.Sends Gzip Content Encoding ✅ 2828ms
Compression.No Content Encoding When Disabled ✅ 3032ms
Compression.Compressed Body Is Decompressible ✅ 3493ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4943ms
Event Options.Cookieless Mode Override ✅ 3030ms
Event Options.Disable Skew Correction Override ✅ 3247ms
Event Options.Process Person Profile Override ✅ 2933ms
Event Options.Product Tour Id Override ✅ 3068ms
Event Options.Unset Options Omitted ✅ 2751ms
Event Options.Options Override In Batch ✅ 563ms
Event Options.Unknown Option Passes Through ✅ 2782ms
Event Options.Option Value Is Not Converted ✅ 406ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2761ms
Event Options.Option Wins Over Legacy Property ✅ 2804ms
Event Options.Legacy Property Fills Unset Option ✅ 2720ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2755ms
Event Size.Event Over Ceiling Is Not Sent ✅ 4333ms
Event Size.Batches Close Before Byte Target ✅ 8766ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2859ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2828ms
Property Layers.Caller Property Beats Registered Property ✅ 2794ms
Property Layers.Before Send Runs Last ✅ 416ms
Fail Open.Empty Api Key Sends Nothing ✅ 1337ms
Fail Open.Personal Api Key Sends Nothing ✅ 1366ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2880ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2874ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2898ms
Request Payload.Flags Request Omits Authorization Header ✅ 2882ms
Request Payload.Token In Flags Body Matches Init ✅ 3011ms
Request Payload.Groups Round Trip ✅ 2964ms
Request Payload.Groups Default To Empty Object ✅ 3005ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2764ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2842ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2897ms
Request Lifecycle.No Flags Request On Init Alone ✅ 99ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 469ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5664ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2892ms
Retry Behavior.Retries Flags On 502 ✅ 3311ms
Retry Behavior.Retries Flags On 504 ✅ 3081ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 2963ms

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code looks good; no qualifying findings. Verdict: correct.

@turnipdabeets
turnipdabeets merged commit 6c70fff into feat/v4-capture-v1 Nov 3, 2026
47 checks passed
@turnipdabeets
turnipdabeets deleted the fix/wrapper-screen-built-name branch November 3, 2026 13:51
turnipdabeets added a commit that referenced this pull request Nov 5, 2026
* feat!: send analytics events through capture v1

Post analytics events to /i/v1/analytics/events with Bearer auth and the
PostHog-Sdk-Info/Attempt/Request-Id/Request-Timestamp headers. The V1
request builder promotes $session_id/$window_id to the event root, drops
$lib/$lib_version, and sends each UUID once per batch.

Per-event results drive the queue: ok/warning/drop are removed and only
retry events stay queued with backoff, reusing the request ID with an
incremented attempt. Only 408/500/502/503/504 and network errors are
retried. A 404 falls back to /batch for the rest of the session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: declare capture_v1 in the compliance adapter

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: document capture auth header, cover fallback retry policy, map adapter init options

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: log dropped duplicate UUIDs, cover a failing /batch fallback, clarify changeset

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: follow capture redirects only to the configured origin

Capture V1 follows only same-origin 307/308 redirects (at most 5) and
re-sets the original headers; any other redirect is returned and handled
as terminal. Other endpoints keep stripping custom headers cross-host.
Also caps Retry-After at the 30s backoff ceiling, logs per-batch drop
counts instead of event UUIDs, and omits empty session/window IDs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat!: add per-event capture options

Adds an options parameter to the most complete capture overload and a
mutable PostHogEvent.options that beforeSend can edit and the queue
persists. At send time the legacy $cookieless_mode, $ignore_sent_at,
$product_tour_id and $process_person_profile properties are removed and
fill their unset options, matching posthog-python/go/rs. The /batch
fallback folds options back into those properties, and replay snapshots
never send options. The old seven-argument Objective-C selector keeps
working. The compliance adapter passes options and declares
event_options.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: clarify capture options precedence and the Retry-After cap

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: track capture V1 requests in the compliance adapter

/flush waits for intercepted requests to settle, but the interceptor
only matched legacy paths, so it returned before V1 requests reached
the mock and fast tests intermittently saw no requests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: note the Retry-After cap applies to every queue and 413 still splits

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: rewrite the capture V1 changeset as changelog entries

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: stop the capture redirect tests racing the HTTP stubs

OHHTTPStubs delivers a stubbed 3xx before the redirect decision, so
URLSession can end the task with no response. Check the 5-redirect
limit against the delegate directly, and check the followed request
rather than the final result.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: resend a capture V1 batch whose transfer failed after a 2xx

A connection that drops after the 2xx header arrives never delivers the
per-event results, so the batch now counts as unanswered and is resent
instead of being removed. The PostHogApi URLSession is invalidated on deinit
so its redirect delegate is released.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor: stop deduplicating event UUIDs before capture

The SDK generates every event UUID, so a batch can't repeat one. Leave
duplicate rejection to the server, like the other capture v1 SDKs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: add the 4.0 capture v1 migration guide

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: give each capture its own UUID when beforeSend returns an earlier event

beforeSend can return an event object kept from an earlier call. Its UUID
then repeats in the queue, and capture V1 rejects the whole batch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(queue): back off exponentially with jitter between failed sends

The shared queue grew its wait by 1 second per consecutive failure. It
now doubles from 1 second to the 30 second ceiling and scales each wait
by a random factor in [0.75, 1.25), capped at the ceiling, matching the
capture V1 multiplier, ceiling and jitter of posthog-python, posthog-go
and posthog-rs. A clamped Retry-After stays a floor applied after jitter.

The 1 second base is kept rather than the server SDKs' 100ms: this queue
retries without a count limit and every capture over flushAt is a retry
trigger, so a 100ms base would send several requests in a row during an
outage. Retries stay unbounded so records are only removed on delivery,
a terminal response, or queue capacity.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: describe the jittered exponential backoff in the 4.0 migration guide

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(config): rename maxRetries to pushNotificationSubscriptionMaxRetries

maxRetries reads like an event-queue retry limit, but since 3.73.0 (#788)
it has only bounded push-subscription registration retries. Event, replay
and log retries stay unlimited so records are kept until delivery.

maxRetries remains as a deprecated @objc alias that reads and writes the
new property, with the same default of 3.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(config)!: drop the deprecated maxRetries alias

4.0 already breaks the API, and maxRetries only limited push registration.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(capture): send empty session and window IDs and log dropped non-strings

Capture V1 now sends an empty or whitespace `$session_id`/`$window_id` at
the event root unchanged, matching posthog-go, posthog-rs and posthog-python.
A non-string value is still dropped, now with a warning that names the key
and its JSON type, never the value. Null drops silently.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: treat a capture V1 2xx whose results can't be read as delivered

A connection that drops after the 2xx status arrives no longer resends the
batch. The server already stored the batch's ok events, and capture doesn't
dedup a resend by UUID, so resending stored them twice. Such a 2xx is now
handled like one with a malformed body: the batch counts as delivered and the
SDK logs only the event count.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: send process_person_profile only when person processing is off

The SDK wrote $process_person_profile (true or false) on every event and
relied on hoisting to turn it into an option. It now sets the
process_person_profile: false option only when person processing is off,
and sends nothing otherwise, which is the server default. A caller's
$process_person_profile property is still ignored; the option still
overrides it, and beforeSend still has the final say. The crash-context
snapshot keeps the property, since it stores only properties.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: describe how 4.0 sends process_person_profile in the migration guide

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(compliance): run the adapter with personProfiles = .always

The harness models a server SDK: it never identifies and expects person
processing on unless an event opts out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat!: accept options on screen, identify, alias, group and captureException

screen, identify, setPersonProperties, alias, group and both
captureException overloads take the same per-event options argument as
capture. The options go on the event each call sends ($screen,
$identify or $set, $create_alias, $groupidentify, $exception), so the
legacy-property hoisting, the SDK's process_person_profile default and
beforeSend apply exactly as they do for capture. Objective-C gets new
selectors ending in options:, and the old selectors keep working through
Swift-hidden shims.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: don't deduplicate person properties sent without person processing

A $set sent with process_person_profile: false stored its hash, so the same properties set later with person processing were suppressed and never reached the person.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: use the current distinct ID for a blank distinctId argument

An empty or whitespace-only distinctId now falls back like nil, instead of capture v1 dropping the event.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: disable the SDK when projectToken is a personal API key

PostHog rejects a phx_ key for capture with a 401, so every event was dropped. Setup now disables the SDK, as for an empty token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: mark the migration guide to link the public capture v1 guide

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat!: let event properties override the SDK's own values

Caller event properties now win over $is_identified, $process_person_profile, $geoip_disable and the session replay debug values, which become defaults. Registered super properties still can't override them, and beforeSend still has the final say: super properties < SDK context and values < event properties < beforeSend.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat!: let super properties override properties the SDK sets

Event properties merge as: SDK-set properties, then registered super properties, then the caller's properties, then beforeSend. A registered key now wins over SDK context and over $is_identified, $process_person_profile, $geoip_disable and the replay debug values.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: fix what beforeSend sees for $lib and note registered $session_id

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: send both GeoIP override events in one batch

With flushAt 1 and no queue timer, the second event could be queued
while the first request was in flight and never sent on slow runners.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add captureAi on the capture V1 AI endpoint

captureAi(...) takes capture's arguments, runs the same enrichment and
beforeSend, and queues the event on its own disk queue for
/i/v1/ai/events. It reuses the capture V1 transport (headers, per-event
results, retry identity per endpoint, redirects, Retry-After), with no
/batch fallback. The AI queue batches by bytes (5 MiB) as well as count,
keeps at most 100 events, and drops events over 8 MiB + 64 KiB logging
only the name and size. capture() never reroutes $ai_ events.

The compliance adapter gains /capture_ai and the capture_ai_v1 capability.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: let per-call $set, $set_once and groups win over registered ones

Registered super properties were merged after the typed per-call values, so a registered $set, $set_once or $groups overrode userProperties, userPropertiesSetOnce and per-call groups. Apply the typed values and the groups set with group() after the registered merge, as in 3.x and posthog-android 4.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: cap AI queue disk use and return the event UUID from captureAi

The AI queue now drops its oldest events once they take more than 50 MiB
on disk, on top of the 100-event limit. captureAi returns the queued
event's UUID, or nil when the event isn't queued.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: flush and bound AI queue reads by batch bytes, and flush it on close()

- close() flushes the AI queue along with the others, and aiQueue is guarded
  by setupLock like the other queues.
- The AI queue flushes once a batch's worth of bytes (5 MiB) is queued, so
  multi-MB events are sent before the 50 MiB cap evicts them.
- A flush reads about one byte-sized batch from disk instead of the whole
  window, so large AI events aren't all held in memory at once.
- The compliance adapter's /capture_ai accepts bodies up to 9 MB and passes
  the harness timestamp.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: return success from the compliance adapter's /capture_ai

The harness reads the action result's success field, as posthog-python's
adapter returns it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(queue): cap analytics batches at 5 MiB

Capture V1 rejects requests over 10 MiB, and analytics batches were capped
by count only. Give the analytics endpoint the same 5 MiB soft byte limit
as the AI lane (renamed to captureV1MaxBatchBytes). An event over the limit
is still sent alone, and 413 halving is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: give AI requests a 60 s timeout and log non-$ai_ captureAi names

Document the AI lane's fixed limits on captureAi.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: note the 5 MiB analytics batch cap in the migration guide

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: add an AI events section to the migration guide

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add captureAi settings to PostHogConfig

Add captureAiCompression, captureAiMaxQueueSize, captureAiTimeout and
captureAiMaxEventBytes, matching the AI lane settings in the backend v1
SDKs. Defaults keep the current behavior: gzip, 100 events, 60 s, 8 MiB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: make captureAiMaxEventBytes limit the whole serialized event

It capped properties but the SDK added 64 KiB on top of any value, so a
lowered limit let much larger events through. Like posthog-python's
capture_ai_max_event_bytes, it now limits the whole serialized event and
defaults to 8 MiB plus 64 KiB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat!: remove the /batch fallback from capture V1

Analytics events now only go to /i/v1/analytics/events. A 404 is terminal
like any other non-retriable status, matching posthog-python, posthog-go
and posthog-rs, instead of resending the batch to /batch for the session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: stop ignoring /batch in replay network capture

The SDK no longer sends to /batch, so replay doesn't need to filter it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: drop the /batch fallback from the migration guide

A 404 from capture v1 now drops the events, as the changeset says.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep ignoring /batch in replay network capture

React Native apps record replay through posthog-ios while their JS SDK
still posts to /batch, so those requests would show up in recordings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: list the reverse proxy requirements for capture v1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(compliance): disable AI compression when enable_compression is false

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: log capture V1 drop reasons and the captureAiMaxEventBytes clamp

- Log each event a 2xx drops at debug level with its name, UUID and the
  server's details (e.g. ai_event_too_big), never its properties.
- Check the AI event size limit in the queue via maxRecordBytes, so an
  oversized event logs only the size line, not "unserialisable record".
- Warn once at setup when captureAiMaxEventBytes is over the ceiling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: note the captureAiMaxEventBytes clamp warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: combine the capture V1, options and captureAi changesets into one major entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: flush analytics events at the batch byte limit

The analytics queue sets maxBatchBytes but no maxQueueBytes, and the file
queue only tracked byteCount when maxQueueBytes was set, so byteCount stayed
0 and the byte threshold never triggered a flush. Track bytes whenever the
endpoint batches by bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep a caller-supplied UUID in the /capture_ai adapter route

Adds an @_spi(PostHogInternal) captureAi overload that takes the event UUID,
and the compliance adapter passes the harness's uuid through it, so
capture_ai_keeps_supplied_uuid gets the same UUID back and on the wire.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: update the no-traits API snapshot for the capture V1 API changes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: show capture options and captureAi in the example apps (#985)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* revert: drop the adapter-only captureAi UUID overload

This reverts commit 1e0cc55.

captureAi takes no UUID, like capture, so capture_ai_keeps_supplied_uuid
tests a server-SDK feature iOS does not offer. Passing it through an
internal overload only the adapter can call made the harness test code
apps never run. The test is an expected failure again, as on Android.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

* 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 Envoy, so neither should drop events. Matches 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 out of the capture v1 404 warning

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: flush analytics once a batch's worth of bytes is queued (#986)

The early flush at maxBatchBytes read the file queue's byteCount, which was
only tracked when a disk byte cap (maxQueueBytes) was set. Analytics has a
batch byte limit but no disk cap, so byteCount stayed 0 and large events
waited for flushAt or the timer. Track file sizes whenever the endpoint has
a batch byte limit, without adding eviction.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

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

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

Declare disable_geoip, generated_payloads, property_layers and fail_open,
and add the adapter fields they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(compliance): cap generated payload size before allocating padding

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: don't log the response body of failed capture requests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: summarize capture v1 in MIGRATION.md and link the guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: add long distinct IDs and phx_ keys to the 4.0 changelog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: add the proxy Authorization rule to the migration guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: note non-JSON values, 402, AI flushes and registered options in the guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: add build and send steps for wrapper SDKs' beforeSend (#991)

* feat: add build and send steps for wrapper SDKs' beforeSend

Wrapper SDKs such as posthog-flutter run their own beforeSend in another
runtime. buildEventForWrapper and buildScreenEventForWrapper return the
event capture would queue, with SDK, registered and call properties merged,
and sendBuiltEventForWrapper runs native beforeSend and queues it without
filling properties again. capture and screen now go through the same two
steps. The methods are @_spi(PostHogInternal), not public API.

* feat: build wrapper $screen events and claim the replay debug bundle at send

buildEventForWrapper now builds a $screen event like screen(), from its
$screen_name property, so wrappers use one entry point. The throttled
replay debug bundle is claimed when a wrapper event is sent rather than
built, so a wrapper that never sends an event can't hold it.

* fix: record a wrapper's $screen at send and use the send's lane

A wrapper $screen is now cached as the current screen once it's sent, from
the final event, so a wrapper hook that renames or drops it is respected.
sendBuiltEventForWrapper uses its own ai value when it differs from the
build's, with a log.

* fix: take a wrapper event's flag person properties from the sent event, send it once under a lock, and keep its replay removals

- Feature flags get the $set/$set_once left on the event after the wrapper's hook, and none when reset() or identify() changed the identity since the build.
- The send-once marker is read and cleared under a lock.
- Send adds only the deferred replay debug properties, so a required key the wrapper removed stays removed.

* fix: defer a wrapper build's person processing to send and keep its replay eligibility from the built name

A build the wrapper drops no longer turns person processing on; the event's
options are computed as though it were on, and send turns it on. Replay debug
eligibility comes from the name the event was built with, so a hook that
renames it doesn't change it.

* fix: drop a wrapper event built before close/setup or on another instance, and keep identity and replay values from its build

A send now rejects a built event from another SDK instance or an earlier setup, turns on person processing only for the identity it was built for, and adds the replay debug values snapshotted at build.

* fix: treat a reset that keeps the distinct ID as a new identity for wrapper sends

* 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: select wrapper send queues atomically with the setup check

* fix: drop empty event names locally and count duplicates capture dropped (#989)

* fix: drop empty event names locally and count duplicates capture dropped

Drop an event whose name is empty after beforeSend before it's queued,
logging the first one. Read duplicates_dropped from capture v1 results and
add it to the per-batch debug count.

* fix: let typed per-call $set, $set_once and $groups win key by key over the same maps in properties

* fix: clamp the duplicates_dropped count so a malformed response can't overflow it

* fix: drop a 2xx batch without readable results and send crash-context person processing as an option (#990)

Count a capture v1 2xx whose body can't be read as per-event results as a
dropped batch in the debug log, not as delivered. The records are still
removed and not retried.

Save person processing in the crash context as the process_person_profile
option instead of the legacy $process_person_profile property, and pass it
when the crash and memory exception reporters send the $exception.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop empty names on every path, ignore whitespace identify, and log terminal batch losses

- Check for an empty event name in queueEvent, so screen, identify,
  alias, group and autocapture events that beforeSend renames to ""
  are dropped like capture's.
- identify ignores a whitespace-only distinct ID and no longer logs it.
- The capture v1 aggregate debug line names the endpoint and status,
  and counts the batch on a terminal status (400, 401, 402, 415, or a
  single-event 413).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: log capture V1 drop reasons in the batch line instead of per event

The v1 rules allow one aggregate line per batch on a client SDK, carrying
the count, the cause and the endpoint, in place of per-event reporting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: merge caller-supplied $groups key by key when a value isn't a string

A non-string value failed the [String: String] cast, so the caller's map
replaced the registered and per-call groups entirely.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: treat a nil boxed in Any as unset for person processing and option hoisting

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep person processing and GeoIP settings on a crash event with no saved context

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: print the empty and personal API key setup errors without debug (#996)

* fix: print the empty and personal API key setup errors without debug

* test: check the disabled setup warnings print without debug

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: say the personal API key warning prints without debug

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

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

* fix: keep a capture's userProperties out of the next user's flags when beforeSend resets (#1014)

The person properties for flags were written after beforeSend, to whoever the
SDK's user was by then, so a hook calling reset() or identify() gave them to
the replacement user.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: review beforeSend hooks, debug-only warning and per-event uuids in the 4.0 guide (#1007)

* docs: note beforeSend event shape, debug-only warning and per-event uuids in the 4.0 guide

* docs: tell proxies to pass the capture response body through

* fix: align capture v1 session IDs, request IDs and logs (#1011)

* fix: align capture v1 session IDs, request IDs, logs and guide

* fix: keep the Objective-C beforeSend failure log debug-only

* chore(compliance): clear adapter log lines through the stdout pipe

* ci(compliance): raise the job timeout to 45 minutes

* chore(compliance): return the state fields the harness requires

* fix: drop a $snapshot whose $session_id is blank or not a string

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: a null caller $session_id falls back to the SDK session

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(compliance): count retries from the PostHog-Attempt header

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(compliance): read pending events from the queue folders

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

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

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

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

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

* docs: note that $locale follows the per-app language on iOS but not Android

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

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

* docs: link the capture v1 SDK upgrade guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

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

* docs: mark the unconfirmed mobile AI-lane and backoff defaults as TODOs

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

* docs: a whitespace-only project token disables the SDK too

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

* docs: drop the confirmed capture v1 TODOs from the migration guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test: register legacy option properties after flags load to fix a missed-callback flake (#1026)

keepsLegacyOptionPropertiesOnMinimalFeatureFlagEvents registered super
properties between getSut(preloadFeatureFlags: true) and
waitForFeatureFlagsLoaded. The preload's onFeatureFlagsLoaded could fire
during that disk write, before the helper subscribed, so the wait timed
out after 30s on the iOS simulator job.

* fix: send nested non-string keys as strings, honor compression = .none for AI, warn on 401

- A nested dictionary's number, boolean and NSNull keys are sent as
  their string form, and only a key with no string form is removed,
  as SDK-v1 INV-25 requires. 3.x and earlier 4.0 builds removed the
  whole nested dictionary.
- compression = .none now also sends captureAi requests uncompressed,
  as sdk-specs http-client "Opting out covers every endpoint the SDK
  compresses" (@client) requires. captureAiCompression still applies
  while compression is on.
- The first 401 from capture prints a warning, also without debug,
  with PostHog's error code (missing_authorization or
  invalid_api_token) and a proxy hint. A 401 stays terminal, as in
  every capture v1 SDK and sdk-specs #146.
- The transport error debug log prints the error's domain and code
  instead of its description, which includes the request URL and any
  proxy credentials in host.
- Changeset and MIGRATION.md: add the missing breaking lines ($lib,
  session/window ids, 200-byte names, 404 retry, debug log format),
  move $locale up with them, document nested keys, identify's
  whitespace check, the 1,000,000-byte analytics event limit and when
  beforeSend runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* revert: keep the AI lane's compression independent of compression = .none

The SDK-v1 rubric locks per-lane compression for iOS (T-COMPRESSION,
Eli, 2026-10-21) and FC-11 (the AI lane ignores the analytics setting),
as Android and Flutter do. dfbd583 made compression = .none also
disable gzip for AI requests; this restores captureAiCompression as
the only AI setting. The nested-key, 401-warning and log changes from
that commit stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* revert: drop the iOS-only 401 warning to match the other capture v1 SDKs

Every capture v1 SDK treats a 401 as terminal with no special warning
(SDK-v1 rubric DECISIONS, accepted residual risk; MG-19 covers proxies
in the migration guide). Keep the per-batch debug drop line only. The
shared warning-URL helper and the 404 warning stay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop a capture built before close() and setup() instead of queueing it on the new setup (#1019)

* fix: drop a capture built before close() and setup() instead of queueing it on the new setup

captureInternal builds the event, then picks the queues. A close() and setup()
on another thread in between sent the old setup's event to the new setup's
project. It now records the setup generation before building and picks the
queues under setupLock only if it hasn't changed, like the wrapper send path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: read the setup generation before building a wrapper event

buildEventForWrapper stamped the setup generation after the build, so a
close() and setup() during the build tagged the old setup's event with the
new generation and the send accepted it. It now reads the generation first,
like captureInternal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop a screen built before close() and setup() instead of queueing it on the new setup

screen() read its queue after prepareScreenEvent with no setup-generation
check, so a close() and setup() with another project in between queued a
$screen built with the old setup's distinct ID and super properties on the
new project. It now reads the generation first and takes the queue under
setupLock only if the generation is unchanged, like captureInternal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: check the new setup's queues stay empty when a capture is dropped across reconfigure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test: check setSdkName also names the SDK in PostHog-Sdk-Info

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: resend a capture v1 retry alone so it keeps its request ID and attempt (#1025)

* fix: resend a capture v1 retry alone so it keeps its request ID and attempt

After a partial retry or a retryable failure, the next flush peeked a fresh
FIFO window, so events queued since joined the batch. Its UUID set no longer
matched the pending retry, and the request went out under a new
PostHog-Request-Id and created_at at PostHog-Attempt 1.

The queue now remembers the entries to retry and resends exactly those first,
as posthog-android 4.0 and the backend v1 SDKs do. If any of them was evicted
or removed since, normal batching resumes as a new request. A multi-event 413
still halves the batch as a new request. The state is in memory only.

* test: wait for the flush to finish before the next one in the retry identity tests

* test: wait for the replay queue to remove the sent snapshot before checking its depth

The mock server records the request before the response reaches the queue, so
reading depth right after it raced the queue's removal.

* test: run the retry-alone test on the AI queue too

The mock's AI endpoint now answers like the analytics one by default
(return500, captureV1EventResult), so the same scenario drives both lanes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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