Skip to content

feat!: send analytics events through capture v1 - #938

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

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

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Moves analytics capture to capture V1 (POST /i/v1/analytics/events) for 4.0, in line with the posthog-python 8.0, posthog-go 2.0 and posthog-rs 1.0 V1 releases (PostHog/posthog-python#1016, PostHog/posthog-go#320, PostHog/posthog-rs#231) and Ingestion's goal of moving mobile SDKs to the V1 endpoints.

Transport

  • Authorization: Bearer <projectToken> plus PostHog-Sdk-Info, PostHog-Attempt, PostHog-Request-Id and PostHog-Request-Timestamp headers. 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 warning naming the key and JSON type; null drops silently.
  • The server's per-event results are applied: ok/warning/drop events leave the queue, and only retry events are resent. A retry of the same events keeps its request ID and created_at and increments the attempt.
  • Only 408, 500, 502, 503, 504 and network errors are retried. 429 and 3xx are terminal. 413 keeps halving the batch.
  • Retry-After is honored up to 30 seconds. The cap is in the shared queue, so replay and logs get it too.
  • A 2xx whose per-event results can't be read (malformed body, or the transfer fails after the status) counts as delivered, like posthog-go and posthog-rs. A resend would duplicate the stored events.
  • Capture follows only 307/308 redirects to the same origin as host, re-sends the headers URLSession strips, and stops after 5 hops. Other redirects are returned as terminal. Without this, URLSession drops Authorization on every redirect, POSTs the body to a cross-origin 307/308 target, and turns a 302 into a GET.
  • Drop/retry logging is one count summary per batch, with no UUIDs or payloads.

Per-event options (new public API)

  • capture(..., timestamp:, options:) and PostHogEvent.options, which is persisted in the queue and can be edited in beforeSend. The Objective-C selector is captureWithEvent:...:timestamp:options:; the old ...:timestamp: selector stays as a shim that is hidden from Swift.
  • screen, identify, setPersonProperties, alias, group and captureException take the same options, applied to the event each sends (Objective-C: new selectors ending in options:; the old selectors still work). Spec divergence: sdk-specs defines event options only for capture; these follow the go/python V1 branches (posthog-rs alias/group_identify take no options).
  • Known keys: process_person_profile, cookieless_mode, disable_skew_correction and product_tour_id. Any other key is sent as-is.
  • 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 one is always removed from properties and only fills an option that is unset or null. Values are not coerced (the server parses them leniently).
  • Precedence, lowest first: legacy property, SDK default (process_person_profile: false only when person processing is off), caller option, beforeSend. These are the same rules as the Python, Go and Rust V1 branches.

Self-hosted fallback (iOS-only)
A host without the V1 route (self-hosted without CAPTURE_V1_SINKS) returns 404. On a 404 the SDK resends that batch to /batch and keeps using /batch for the rest of the app session. Options are folded back into the legacy properties there. The next launch tries V1 again. Rust, Go and Python treat a 404 as terminal and document "confirm your instance serves the v1 route"; an installed mobile app can't be redeployed quickly, so iOS falls back instead. A follow-up PR stacked on this one removes /batch and the fallback, so we can decide separately whether 4.0 keeps it.

Follow-ups (separate PRs)

  • captureAi on /i/v1/ai/events.
  • Removing /batch and the fallback, stacked on this PR.

Breaking changes

Change Who notices
Endpoint moves to /i/v1/analytics/events with Bearer auth Reverse proxies, allowlists and WAF rules must forward the new path. Only a 404 triggers the /batch fallback; a 403/405 from a proxy drops the events.
Capture sends Authorization: Bearer, replacing a custom Authorization from requestHeaders Proxies that authenticate with Authorization need another header. The requestHeaders doc is updated.
429 and 3xx are terminal on V1; Retry-After is capped at 30s Rate-limited or redirecting setups
Only same-origin 307/308 redirects are followed host pointing at something that redirects elsewhere
Legacy option properties are sent as options, and the server drops events with unreadable option values Apps that set these properties
New capture(..., options:) and PostHogEvent.options Additive
maxRetries is renamed to pushNotificationSubscriptionMaxRetries (no alias) Apps that set it. It has only limited push-subscription registration since 3.73.0.

All of the above are in .changeset/v4-capture-v1.md and the new docs/migration-3.x-to-4.0.md.

Differences from the other V1 SDKs

  • /batch fallback on 404: iOS-only, see above.
  • 413 halves the batch until a single oversized event is dropped. Python and Go treat 413 as terminal for the whole request.
  • Retry identity resets when new events join a retried batch: the request then gets a new request ID and PostHog-Attempt: 1. These headers only feed server logs and metrics.
  • No retry limit: events stay in the persisted offline queue, bounded by maxQueueSize, as before. The server SDKs give up after 3 retries. Mobile devices are offline often, and a 3-attempt cap wiped buffered events on flaky networks until fix(queue): preserve durable records across retries #788 removed it.
  • Minimal $feature_flag_called allowlist also keeps $cookieless_mode, $ignore_sent_at and $product_tour_id, so they survive until they are converted to options.
  • Gzip is on by default: capture follows config.compression, which defaults to .gzip. The server SDKs send uncompressed by default.
  • Backoff starts at 1 s, not 100 ms: the wait doubles to 30 seconds with ±25% jitter, like the server SDKs (fix: add jitter to capture retry waits posthog-rs#290, fix: add jitter to default capture retry waits posthog-go#370). It starts at 1 s because every capture past flushAt can trigger a send while retries have no limit.
  • Logs Retry-After maximum drops from 5 minutes to 30 seconds: the 30-second cap applies to every queue, replacing the 5-minute logs maximum from fix(logs): clamp Retry-After to a documented 5-minute maximum #925.
  • process_person_profile: false for anonymous users: sent only when person processing is off, which under the default personProfiles = .identifiedOnly includes anonymous users. Nothing is sent otherwise. Spec divergence: capture/spec.md "Caller can't override person-processing state" expects $process_person_profile: false in the queued event's properties; iOS puts it in options. The compliance adapter runs with personProfiles = .always to model a server SDK.

💚 How did you test it?

  • New Swift Testing suites cover:
    • the V1 wire format and headers
    • per-event results and partial retry through the queue
    • retry identity and status-code classification
    • the 404 fallback: that it sticks for the session, its /batch retry policy, and a failing fallback request
    • the redirect decisions
    • the Retry-After cap, jittered exponential backoff, and empty and non-string session/window IDs
    • a 2xx whose transfer then fails counts as delivered
    • an event beforeSend returns from an earlier call still gets its own UUID
    • options: conversion of each legacy property, precedence, persistence round-trip including 3.x events without options, the /batch fold-back, and the $feature_flag_called allowlist
  • The event-shape golden was re-recorded on purpose; the only change is $process_person_profile moving into options.
  • CI passes after rebasing onto main, including test-ios-simulator, test-no-traits and Public API snapshot.
  • Locally, make lint passes, and make test on the top of the stack (feat!: remove the /batch fallback from capture V1 #940) ran 1185 tests. The only failures are 13 group-container storage issues ("Operation not permitted"), which also fail on chore/v4-remove-deprecated locally.
  • SDK compliance harness 1.14.0 with capture_v1 + event_options, run locally on the branch: 116/118. The 2 failures:
    • max_retries_respected: iOS has no retry limit on purpose; also fails on main
    • historical_migration_set_in_body: iOS has no historical-migration mode
  • URLSession's redirect behaviour was checked against local servers: it strips Authorization on same- and cross-origin redirects, re-sends the body on 307, and returns a 302 response when the redirect isn't followed.

📝 Checklist

  • 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

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Built with Claude Code. Subagents implemented the transport and options work against the V1 server source (rust/capture/src/v1), the harness yaml-parity-v1 features and the Rust, Go and Python V1 branches, including their open stacked PRs. Three review cycles plus a per-SDK parity audit fed fixes back in. The redirect handling, the Retry-After cap, privacy-safe logging and the options rules came from that audit.

Maintainer decisions: keep /batch as a 404 fallback, with removal as its own stacked PR; add a public options API in 4.0; keep the Objective-C shim for the old selector; put captureAi in a separate PR.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-23T16:06:42.862768+00:00
Duration: 482493ms

⚠️ Some Tests Failed

116/118 tests passed, 2 failed


Capture_V1 Tests

⚠️ 99/101 tests passed, 2 failed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 3040ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2725ms
Required Headers.Has Authorization Bearer Header ✅ 2835ms
Required Headers.Has Content Type Json ✅ 2819ms
Required Headers.Has Posthog Sdk Info Format ✅ 2777ms
Required Headers.Has Posthog Attempt Header ✅ 2767ms
Required Headers.Has Posthog Request Id ✅ 2837ms
Required Headers.Has Posthog Request Timestamp ✅ 2867ms
Required Headers.Has User Agent ✅ 2956ms
Body Format.Body Has Created At And Batch ✅ 456ms
Body Format.No Api Key In Body ✅ 2696ms
Body Format.No Sent At In Body ✅ 385ms
Event Format.Event Has Required Root Fields ✅ 401ms
Event Format.Event Uuid Is Valid ✅ 2832ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2768ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 313ms
Event Format.Distinct Id Is String ✅ 435ms
Event Format.Distinct Id At Root Not Properties ✅ 2817ms
Event Format.Custom Properties Preserved ✅ 2809ms
Event Format.Set Properties Preserved ✅ 2793ms
Event Format.Set Once Properties Preserved ✅ 2806ms
Event Format.Groups Properties Preserved ✅ 2752ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2949ms
Event Format.Event Has Required Root Fields Batch ✅ 2980ms
Event Format.Event Uuid Is Valid Batch ✅ 564ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 3288ms
Event Format.Distinct Id Is String Batch ✅ 3039ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2854ms
Event Format.Custom Properties Preserved Batch ✅ 538ms
Event Format.Set Properties Preserved Batch ✅ 2958ms
Event Format.Set Once Properties Preserved Batch ✅ 2948ms
Event Format.Groups Properties Preserved Batch ✅ 2892ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 3213ms
Batch Behavior.Multiple Events In Single Batch ✅ 3149ms
Batch Behavior.Batch Envelope Smoke ✅ 3198ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 431ms
Batch Behavior.Flush At Triggers Batch ✅ 1288ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 415ms
Deduplication.Generates Unique Uuids ✅ 3273ms
Deduplication.Different Events Same Content Different Uuids ✅ 3266ms
Deduplication.Preserves Uuid On Retry ✅ 9317ms
Deduplication.Preserves Timestamp On Retry ✅ 9382ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7931ms
Deduplication.No Duplicate Events In Batch ✅ 3185ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 560ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16883ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9477ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7517ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 5444ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2828ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2847ms
Response Format Validation.Success No Retry After When All Ok ✅ 2902ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2849ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2919ms
Response Format Validation.Response Echoes Request Id ✅ 2816ms
Retry Behavior.Retries On 408 ✅ 9275ms
Retry Behavior.Retries On 500 ✅ 9263ms
Retry Behavior.Retries On 503 ✅ 11911ms
Retry Behavior.Retries On 504 ✅ 9351ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6271ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10839ms
Retry Behavior.Does Not Retry On 400 ✅ 4702ms
Retry Behavior.Does Not Retry On 401 ✅ 4721ms
Retry Behavior.Does Not Retry On 402 ✅ 2419ms
Retry Behavior.Does Not Retry On 413 ✅ 4897ms
Retry Behavior.Does Not Retry On 415 ✅ 2457ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4794ms
Retry Behavior.Implements Backoff ✅ 21255ms
Retry Behavior.Max Retries Respected ❌ 15381ms
Partial Batch Handling.Handles 200 Full Success ✅ 4785ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5778ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5803ms
Partial Batch Handling.Does Not Retry Limited Events ✅ 5870ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7818ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7772ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7912ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7858ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7904ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7864ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7823ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5905ms
Partial Batch Handling.Mixed Ok Drop Limited No Retry ✅ 6108ms
Compression.Sends Gzip Content Encoding ✅ 2896ms
Compression.No Content Encoding When Disabled ✅ 2906ms
Compression.Compressed Body Is Decompressible ✅ 2889ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4772ms
Event Options.Cookieless Mode Override ✅ 2675ms
Event Options.Disable Skew Correction Override ✅ 2786ms
Event Options.Process Person Profile Override ✅ 2848ms
Event Options.Product Tour Id Override ✅ 2828ms
Event Options.Unset Options Omitted ✅ 2690ms
Event Options.Options Override In Batch ✅ 2773ms
Event Options.Unknown Option Passes Through ✅ 2956ms
Event Options.Option Value Is Not Converted ✅ 509ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 492ms
Event Options.Option Wins Over Legacy Property ✅ 2849ms
Event Options.Legacy Property Fills Unset Option ✅ 2851ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2804ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 451ms
Geoip And Historical Migration.Historical Migration Set In Body ❌ 512ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2879ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 5

geoip_and_historical_migration.historical_migration_set_in_body

Body missing 'historical_migration' field. Body keys: ['batch', 'created_at']

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2861ms
Request Payload.Flags Request Uses V2 Query Param ✅ 3063ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2875ms
Request Payload.Flags Request Omits Authorization Header ✅ 2995ms
Request Payload.Token In Flags Body Matches Init ✅ 2892ms
Request Payload.Groups Round Trip ✅ 2991ms
Request Payload.Groups Default To Empty Object ✅ 2948ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2832ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2966ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2985ms
Request Lifecycle.No Flags Request On Init Alone ✅ 133ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 559ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5687ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2897ms
Retry Behavior.Retries Flags On 502 ✅ 2998ms
Retry Behavior.Retries Flags On 504 ✅ 2766ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 2976ms

@github-actions

Copy link
Copy Markdown
Contributor

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the stale label – otherwise this will be closed in another week.

@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 d22dd3a: 1 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.

Comment thread PostHog/PostHogApi.swift Outdated
Comment thread PostHog/PostHogApi.swift
Comment thread sdk_compliance_adapter/Sources/RequestInterceptor.swift
@turnipdabeets
turnipdabeets marked this pull request as ready for review October 20, 2026 17:01
@turnipdabeets
turnipdabeets requested a review from a team as a code owner October 20, 2026 17:01
@turnipdabeets
turnipdabeets force-pushed the feat/v4-capture-v1 branch 2 times, most recently from 1e6de37 to bc7f223 Compare October 20, 2026 19:07
@marandaneto

Copy link
Copy Markdown
Member

make sure this is not adding breaking changes to the public API, it should be addition otherwise part of the major

@turnipdabeets

Copy link
Copy Markdown
Contributor Author

make sure this is not adding breaking changes to the public API, it should be addition otherwise part of the major

@marandaneto These are going into iOS major release

@marandaneto

Copy link
Copy Markdown
Member

make sure this is not adding breaking changes to the public API, it should be addition otherwise part of the major

@marandaneto These are going into iOS major release

The changeset is minor 👀

@turnipdabeets

Copy link
Copy Markdown
Contributor Author

make sure this is not adding breaking changes to the public API, it should be addition otherwise part of the major

@marandaneto These are going into iOS major release

The changeset is minor 👀

That's only because it's going into the major branch. I don't think there's breaking changes anyway, but if there are we're safe since it's going into the major branch.

Comment thread PostHog/PostHogApi.swift Outdated

// The server rejects the whole batch with 400 when a UUID repeats.
var seenUuids = Set<String>()
let uniqueEvents = events.filter { seenUuids.insert($0.uuid.postHogUuidString).inserted }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should we drop the SDK-side UUID deduplication? the backend will now reject batches with duplicate event UUIDs and the v1 SDKs should ensure they are generated if missing from any event. If a user supplies duplicate UUIDs on purpose, the backend will report it and they should update their call sites. This will also be called out in the public release docs. If there's a reason this SDK in particular should do this, I may be missing context, so push back if you think this is a bad idea. Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, dropped in ae0ffa2. iOS callers can't set event UUIDs, so the guard could only mask an SDK bug.

payload.completion(true)
}

private func pauseForBackoff(retryAfter: TimeInterval?) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

other v1 SDKs are now using exponential backoff with jitter, and a shorter default number of retries. Could this SDK be converted to do this as well (possibly with a larger default attempt count?) and apply the other backoff/jitter features? if there's context I'm missing on past issues and the retry count needs to be infinite, that works for me - we should probably update the other retry features into parity though. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

iOS keeps unlimited retries because mobile devices are offline often; a 3-attempt cap used to wipe buffered events on flaky networks until #788 removed it. Switched to exponential backoff with jitter for parity in b7184db, and renamed maxRetries so it's clear it's only for push in 706812b.

With `debug` on, the SDK logs how many events were dropped.

- An event with an empty event name is dropped, for example `capture("")`.
- An event with an empty or whitespace-only distinct ID is dropped, for example `capture("signed_up", distinctId: "")`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

An empty distinctId argument drops the event, while nil uses the current distinct ID. Should an empty string fall back the same way, instead of being dropped by PostHog?

| Logs | `/i/v1/logs` | `/i/v1/logs`, unchanged |
| Feature flags | `/flags` | `/flags`, unchanged |

Capture v1 requests send the project token in an `Authorization: Bearer <projectToken>` header instead of an `api_key` body field.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A personal API key (phx_) isn't rejected at setup. It sends and gets a 401, and the events are dropped. Server SDKs disable themselves with one error log. This one is a code gap, not just the guide.

turnipdabeets and others added 25 commits October 23, 2026 11:37
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) <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
… adapter init options

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…clarify changeset

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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) <[email protected]>
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) <[email protected]>
/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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…r 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) <[email protected]>
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) <[email protected]>
…ries

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) <[email protected]>
4.0 already breaks the API, and maxRetries only limited push registration.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…trings

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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
…ception

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) <[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 03cc612: no findings. This pull request is large, so the review ran in 4 parts with less depth than usual.

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