Repository navigation
feat!: send analytics events through capture v1 - #938
turnipdabeets wants to merge 25 commits into
Conversation
posthog-ios Compliance ReportDate: 2026-10-23T16:06:42.862768+00:00
|
| 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 |
0074220 to
90ca8a1
Compare
|
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 |
da14915 to
d22dd3a
Compare
There was a problem hiding this comment.
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.
d22dd3a to
799c91c
Compare
799c91c to
5c48793
Compare
1e6de37 to
bc7f223
Compare
bc7f223 to
6674ffe
Compare
|
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. |
|
|
||
| // 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 } |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
| 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: "")`. |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
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]>
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]>
…plits Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
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]>
…guide 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]>
…guide 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]>
7413fa6 to
03cc612
Compare
💡 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>plusPostHog-Sdk-Info,PostHog-Attempt,PostHog-Request-IdandPostHog-Request-Timestampheaders. The body is{created_at, batch}, with noapi_keyorsent_at.$session_id/$window_idmove to the event root, and$lib/$lib_versionare no longer sent as properties (the server reads them fromPostHog-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.ok/warning/dropevents leave the queue, and onlyretryevents are resent. A retry of the same events keeps its request ID andcreated_atand increments the attempt.Retry-Afteris honored up to 30 seconds. The cap is in the shared queue, so replay and logs get it too.host, re-sends the headers URLSession strips, and stops after 5 hops. Other redirects are returned as terminal. Without this, URLSession dropsAuthorizationon every redirect, POSTs the body to a cross-origin 307/308 target, and turns a 302 into a GET.Per-event options (new public API)
capture(..., timestamp:, options:)andPostHogEvent.options, which is persisted in the queue and can be edited inbeforeSend. The Objective-C selector iscaptureWithEvent:...:timestamp:options:; the old...:timestamp:selector stays as a shim that is hidden from Swift.screen,identify,setPersonProperties,alias,groupandcaptureExceptiontake the sameoptions, applied to the event each sends (Objective-C: new selectors ending inoptions:; the old selectors still work). Spec divergence: sdk-specs defines event options only forcapture; these follow the go/python V1 branches (posthog-rsalias/group_identifytake no options).process_person_profile,cookieless_mode,disable_skew_correctionandproduct_tour_id. Any other key is sent as-is.$process_person_profile,$cookieless_mode,$ignore_sent_atand$product_tour_idproperties are converted when the request is built, afterbeforeSend. 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).process_person_profile: falseonly 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/batchand keeps using/batchfor 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/batchand the fallback, so we can decide separately whether 4.0 keeps it.Follow-ups (separate PRs)
captureAion/i/v1/ai/events./batchand the fallback, stacked on this PR.Breaking changes
/i/v1/analytics/eventswith Bearer auth/batchfallback; a 403/405 from a proxy drops the events.Authorization: Bearer, replacing a customAuthorizationfromrequestHeadersAuthorizationneed another header. TherequestHeadersdoc is updated.Retry-Afteris capped at 30shostpointing at something that redirects elsewhereoptions, and the server drops events with unreadable option valuescapture(..., options:)andPostHogEvent.optionsmaxRetriesis renamed topushNotificationSubscriptionMaxRetries(no alias)All of the above are in
.changeset/v4-capture-v1.mdand the newdocs/migration-3.x-to-4.0.md.Differences from the other V1 SDKs
/batchfallback on 404: iOS-only, see above.PostHog-Attempt: 1. These headers only feed server logs and metrics.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.$feature_flag_calledallowlist also keeps$cookieless_mode,$ignore_sent_atand$product_tour_id, so they survive until they are converted to options.config.compression, which defaults to.gzip. The server SDKs send uncompressed by default.flushAtcan trigger a send while retries have no limit.Retry-Aftermaximum 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: falsefor anonymous users: sent only when person processing is off, which under the defaultpersonProfiles = .identifiedOnlyincludes anonymous users. Nothing is sent otherwise. Spec divergence:capture/spec.md"Caller can't override person-processing state" expects$process_person_profile: falsein the queued event's properties; iOS puts it inoptions. The compliance adapter runs withpersonProfiles = .alwaysto model a server SDK.💚 How did you test it?
/batchretry policy, and a failing fallback requestRetry-Aftercap, jittered exponential backoff, and empty and non-string session/window IDsbeforeSendreturns from an earlier call still gets its own UUID/batchfold-back, and the$feature_flag_calledallowlist$process_person_profilemoving intooptions.main, includingtest-ios-simulator,test-no-traitsandPublic API snapshot.make lintpasses, andmake teston 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 onchore/v4-remove-deprecatedlocally.1.14.0withcapture_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 onmainhistorical_migration_set_in_body: iOS has no historical-migration modeAuthorizationon same- and cross-origin redirects, re-sends the body on 307, and returns a 302 response when the redirect isn't followed.📝 Checklist
If releasing new changes
pnpm changesetto 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 harnessyaml-parity-v1features 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, theRetry-Aftercap, privacy-safe logging and the options rules came from that audit.Maintainer decisions: keep
/batchas 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; putcaptureAiin a separate PR.🤖 Generated with Claude Code