Skip to content

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

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

turnipdabeets wants to merge 65 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 capture to capture V1 for 4.0: analytics events go to POST /i/v1/analytics/events and the new captureAi(...) sends AI events to POST /i/v1/ai/events. This matches 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 their AI lanes (PostHog/posthog-rs#250, PostHog/posthog-go#323, PostHog/posthog-python#1017, #1022), and Ingestion's goal of moving mobile SDKs to the V1 endpoints with captureAi in every V1 release.

This PR now contains the whole capture V1 stack: #939 (captureAi) and #940 (removing the /batch fallback) were merged into it.

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. Retry identity is tracked per endpoint, so the analytics and AI lanes don't reset each other's attempt numbers.
  • Only 404, 408, 429, 500, 502, 503, 504 and network errors are retried. 3xx is terminal. 413 keeps halving the batch.
  • No /batch fallback: a host that doesn't serve the V1 route (a self-hosted instance without CAPTURE_V1_SINKS, or a reverse proxy that doesn't forward it) drops events until it does. PostHogApi.batch(events:) and the options-to-legacy-properties fold-back are gone. /snapshot keeps its own retry policy.
  • 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.
  • Analytics batches are capped at 5 MiB per request; an event larger than that is sent on its own.
  • With debug on, drop/retry logging is one count summary per batch, with no UUIDs or payloads, and each event a 2xx drops is logged with its name, UUID and the server's details (e.g. ai_event_too_big, misrouted_event), never its properties.

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.

Property precedence

  • Event properties are merged as: properties the SDK sets itself, then registered super properties, then the properties you pass, then beforeSend. Per-call $set, $set_once and groups win over registered ones.

captureAi (new public API)

  • captureAi takes the same arguments as capture and goes through the same enrichment and beforeSend. Only the delivery route differs. capture never reroutes $ai_* events.
  • AI events go to their own disk queue (posthog.aiFolder). It is flushed by flush() and close(), and created with the other queues so events left on disk by a previous launch still send.
  • captureAi returns the queued event's UUID (@discardableResult), or nil when the event isn't queued: opted out, disabled, dropped by beforeSend, or over the size limit.
  • New PostHogConfig settings tune the AI lane separately:
    • captureAiCompression (default .gzip), independent of config.compression.
    • captureAiMaxQueueSize (default 100).
    • captureAiTimeout (default 60 s), instead of 10 s.
    • captureAiMaxEventBytes (default 8 MiB + 64 KiB). It can only lower the limit: a larger value is clamped to the default with a debug warning.
    • A value of 0 or less for the numeric settings uses the default.
  • The queue batches by bytes as well as count (5 MiB soft limit), drops an event over captureAiMaxEventBytes when it is queued, flushes as soon as it holds 5 MiB, and keeps at most captureAiMaxQueueSize events and 50 MiB on disk, dropping the oldest first. Without the byte cap, 100 events at the size limit could take ~800 MiB.

Other fixes in the stack

  • Jittered exponential backoff between failed sends (1 s doubling to 30 s, ±25%), for events, replay and logs.
  • maxRetries is renamed to pushNotificationSubscriptionMaxRetries (no alias).
  • A personal API key (phx_...) as projectToken disables the SDK at setup.
  • A blank distinctId argument uses the current distinct ID.
  • Each capture gets its own UUID even when beforeSend returns an earlier event; event UUIDs are no longer deduplicated before capture.
  • Person properties sent without person processing are no longer deduplicated.

Breaking changes

Change Who notices
Endpoint moves to /i/v1/analytics/events with Bearer auth, no /batch fallback Reverse proxies, allowlists, WAF rules and self-hosted instances must serve the new path. A 404 keeps the events queued and retries them; a 403 or 405 drops them.
Capture sends Authorization: Bearer, replacing a custom Authorization from requestHeaders Proxies that authenticate with Authorization need another header. The requestHeaders doc is updated.
3xx is 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
Super properties override SDK properties; super and event properties override $is_identified, $process_person_profile, $geoip_disable Apps that register super properties with SDK names
maxRetries is renamed to pushNotificationSubscriptionMaxRetries (no alias) Apps that set it. It has only limited push-subscription registration since 3.73.0.
New capture(..., options:), PostHogEvent.options, captureAi and captureAi* config Additive

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

Differences from the spec and the other V1 SDKs

  • 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 and AI follows captureAiCompression, both .gzip by default. 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. 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.
  • captureAi (the capture-ai spec only covers server SDKs):
    • No uuid argument, like iOS capture; it does return the UUID, like the spec.
    • The queue is created at setup, not lazily, so an offline mobile queue sends events persisted by an earlier launch.
    • The queue limit is 100 events and 50 MiB, not 1000 events, to keep the disk use of multi-MB events down on devices.

💚 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
    • a 404 is retried and every later batch still goes to V1 (no /batch 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, and the $feature_flag_called allowlist
    • "Capture V1 AI lane" with MockPostHogServer's new /i/v1/ai/events stub: routing, 404 retried, oversized events dropped when queued, the captureAiMaxEventBytes clamp, splitting at the byte limit, the returned UUID, and flushing once a batch's worth of bytes is queued
    • PostHogFileBackedQueue byte cap (evicts the oldest, keeps an oversized newest record, trims on load) and byte-bounded reads
  • The event-shape golden was re-recorded on purpose; the only change is $process_person_profile moving into options.
  • CI passed on each PR of the stack, including test-ios-simulator, test-no-traits and Public API snapshot.
  • After merging the stack and the latest chore/v4-remove-deprecated: swift build --build-tests -Xswiftc -DTESTING passes, and the capture, queue, API and SDK suites (243 tests) pass. One SDK identity test ("identifies an anonymous user via identify() when the id already matches the persisted distinct id") failed in 2 of 4 local runs of that combination and passes on its own.
  • SDK compliance harness 1.14.0: capture_v1 98/101 and capture_ai_v1 97/101. The failures:
    • max_retries_respected: iOS has no retry limit on purpose; also fails on main
    • unset_options_omitted: mobile-only failure
    • capture_ai_keeps_supplied_uuid: captureAi doesn't take a UUID. The test aborts before flushing, so its request lands in the next test and fails event_has_required_root_fields_batch too.
  • 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.
  • Not done: docs for captureAi on posthog.com / LLM analytics.

📝 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, options and AI lane against the V1 server source (rust/capture/src/v1), the harness yaml-parity-v1 features, the sdk-specs capture-ai spec and the Rust, Go and Python V1 branches. Three review cycles plus a per-SDK parity audit fed fixes back in.

Maintainer decisions: add a public options API in 4.0; keep the Objective-C shim for the old selector; ship 4.0 without the /batch fallback; give AI events a separate lane with a small disk queue.

🤖 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-29T16:11:14.394268+00:00
Duration: 969701ms

✅ 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 ✅ 3124ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2737ms
Required Headers.Has Authorization Bearer Header ✅ 2811ms
Required Headers.Has Content Type Json ✅ 2761ms
Required Headers.Has Posthog Sdk Info Format ✅ 2700ms
Required Headers.Has Posthog Attempt Header ✅ 2749ms
Required Headers.Has Posthog Request Id ✅ 2733ms
Required Headers.Has Posthog Request Timestamp ✅ 2668ms
Required Headers.Has User Agent ✅ 2797ms
Body Format.Body Has Created At And Batch ✅ 2775ms
Body Format.No Api Key In Body ✅ 2660ms
Body Format.No Sent At In Body ✅ 2767ms
Event Format.Event Has Required Root Fields ✅ 378ms
Event Format.Event Uuid Is Valid ✅ 2755ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2666ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2764ms
Event Format.Distinct Id Is String ✅ 2789ms
Event Format.Distinct Id At Root Not Properties ✅ 376ms
Event Format.Custom Properties Preserved ✅ 2769ms
Event Format.Set Properties Preserved ✅ 2800ms
Event Format.Set Once Properties Preserved ✅ 2749ms
Event Format.Groups Properties Preserved ✅ 2684ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2806ms
Event Format.Event Has Required Root Fields Batch ✅ 2690ms
Event Format.Event Uuid Is Valid Batch ✅ 423ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2771ms
Event Format.Distinct Id Is String Batch ✅ 530ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2827ms
Event Format.Custom Properties Preserved Batch ✅ 2853ms
Event Format.Set Properties Preserved Batch ✅ 2867ms
Event Format.Set Once Properties Preserved Batch ✅ 2732ms
Event Format.Groups Properties Preserved Batch ✅ 2801ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2834ms
Batch Behavior.Multiple Events In Single Batch ✅ 2896ms
Batch Behavior.Batch Envelope Smoke ✅ 466ms
Batch Behavior.Flush With No Events Sends Nothing ✅ 334ms
Batch Behavior.Flush At Triggers Batch ✅ 1175ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2795ms
Deduplication.Generates Unique Uuids ✅ 2967ms
Deduplication.Different Events Same Content Different Uuids ✅ 3271ms
Deduplication.Preserves Uuid On Retry ✅ 9271ms
Deduplication.Preserves Timestamp On Retry ✅ 5420ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7897ms
Deduplication.No Duplicate Events In Batch ✅ 2907ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2769ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16265ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9372ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 7646ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9414ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 357ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2761ms
Response Format Validation.Success No Retry After When All Ok ✅ 2885ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2993ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2809ms
Response Format Validation.Response Echoes Request Id ✅ 2658ms
Retry Behavior.Retries On 408 ✅ 5320ms
Retry Behavior.Retries On 500 ✅ 9251ms
Retry Behavior.Retries On 503 ✅ 11736ms
Retry Behavior.Retries On 504 ✅ 5393ms
Retry Behavior.Retries On 404 ✅ 5413ms
Retry Behavior.Retries On 429 ✅ 8824ms
Retry Behavior.Retries On 502 ✅ 9311ms
Retry Behavior.Retryable Errors Have Retry After ✅ 6288ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 10728ms
Retry Behavior.Does Not Retry On 400 ✅ 4693ms
Retry Behavior.Does Not Retry On 401 ✅ 4787ms
Retry Behavior.Does Not Retry On 402 ✅ 2372ms
Retry Behavior.Does Not Retry On 413 ✅ 4661ms
Retry Behavior.Does Not Retry On 415 ✅ 4838ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4740ms
Retry Behavior.Implements Backoff ✅ 18211ms
Partial Batch Handling.Handles 200 Full Success ✅ 4720ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5736ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5778ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5868ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7739ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7873ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7877ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7860ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7835ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7849ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7680ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5704ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 5826ms
Compression.Sends Gzip Content Encoding ✅ 2730ms
Compression.No Content Encoding When Disabled ✅ 2629ms
Compression.Compressed Body Is Decompressible ✅ 2797ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4710ms
Event Options.Cookieless Mode Override ✅ 2993ms
Event Options.Disable Skew Correction Override ✅ 394ms
Event Options.Process Person Profile Override ✅ 2783ms
Event Options.Product Tour Id Override ✅ 2725ms
Event Options.Unset Options Omitted ✅ 2767ms
Event Options.Options Override In Batch ✅ 2732ms
Event Options.Unknown Option Passes Through ✅ 2907ms
Event Options.Option Value Is Not Converted ✅ 2827ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 2739ms
Event Options.Option Wins Over Legacy Property ✅ 2664ms
Event Options.Legacy Property Fills Unset Option ✅ 2764ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2767ms
Property Layers.Caller Property Beats Registered Property ✅ 2680ms
Property Layers.Before Send Runs Last ✅ 2763ms
Fail Open.Empty Api Key Sends Nothing ✅ 1202ms
Fail Open.Personal Api Key Sends Nothing ✅ 1320ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2715ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2720ms

Capture_Ai_V1 Tests

✅ 110/110 tests passed

View Details
Test Status Duration
Endpoint And Method.Targets V1 Endpoint ✅ 290ms
Endpoint And Method.Does Not Use Legacy Endpoints ✅ 2729ms
Endpoint And Method.Capture Does Not Reroute Ai Named Events ✅ 2749ms
Endpoint And Method.Capture Ai Does Not Reroute Non Ai Events ✅ 2749ms
Lane Isolation.Failing Ai Lane Does Not Delay Analytics ✅ 11259ms
Required Headers.Has Authorization Bearer Header ✅ 2718ms
Required Headers.Has Content Type Json ✅ 2798ms
Required Headers.Has Posthog Sdk Info Format ✅ 2655ms
Required Headers.Has Posthog Attempt Header ✅ 2912ms
Required Headers.Has Posthog Request Id ✅ 2755ms
Required Headers.Has Posthog Request Timestamp ✅ 420ms
Required Headers.Has User Agent ✅ 2834ms
Body Format.Body Has Created At And Batch ✅ 2866ms
Body Format.No Api Key In Body ✅ 2770ms
Body Format.No Sent At In Body ✅ 415ms
Event Format.Event Has Required Root Fields ✅ 2823ms
Event Format.Event Uuid Is Valid ✅ 2881ms
Event Format.Event Timestamp Is Rfc3339 ✅ 2805ms
Event Format.Non Utc Event Timestamp Is Converted To Utc ✅ 2865ms
Event Format.Distinct Id Is String ✅ 2809ms
Event Format.Distinct Id At Root Not Properties ✅ 2795ms
Event Format.Custom Properties Preserved ✅ 2763ms
Event Format.Set Properties Preserved ✅ 358ms
Event Format.Set Once Properties Preserved ✅ 2775ms
Event Format.Groups Properties Preserved ✅ 2766ms
Event Format.Sdk Generates Uuid If Not Provided ✅ 2692ms
Event Format.Event Has Required Root Fields Batch ✅ 2876ms
Event Format.Event Uuid Is Valid Batch ✅ 2899ms
Event Format.Event Timestamp Is Rfc3339 Batch ✅ 2750ms
Event Format.Distinct Id Is String Batch ✅ 2885ms
Event Format.Distinct Id At Root Not Properties Batch ✅ 2862ms
Event Format.Custom Properties Preserved Batch ✅ 2769ms
Event Format.Set Properties Preserved Batch ✅ 2700ms
Event Format.Set Once Properties Preserved Batch ✅ 2900ms
Event Format.Groups Properties Preserved Batch ✅ 2867ms
Event Format.Sdk Generates Uuid If Not Provided Batch ✅ 2866ms
Batch Behavior.Multiple Events In Single Batch ✅ 3164ms
Batch Behavior.Batch Envelope Smoke ✅ 2845ms
Batch Behavior.Flush At Triggers Batch ✅ 1186ms
Batch Behavior.Created At Reflects Batch Creation Time ✅ 2743ms
Deduplication.Generates Unique Uuids ✅ 3265ms
Deduplication.Different Events Same Content Different Uuids ✅ 2822ms
Deduplication.Preserves Uuid On Retry ✅ 9238ms
Deduplication.Preserves Timestamp On Retry ✅ 9200ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7914ms
Deduplication.No Duplicate Events In Batch ✅ 2939ms
Header Behavior On Retry.Attempt Header Starts At One ✅ 2738ms
Header Behavior On Retry.Attempt Header Increments On Retry ✅ 16344ms
Header Behavior On Retry.Request Id Preserved On Retry ✅ 9201ms
Header Behavior On Retry.Different Requests Have Different Request Ids ✅ 5054ms
Header Behavior On Retry.Request Timestamp Changes On Retry ✅ 9170ms
Response Format Validation.Success Response Has Uuid Keyed Results ✅ 2769ms
Response Format Validation.Success Response Has Ok For Each Event ✅ 2835ms
Response Format Validation.Success No Retry After When All Ok ✅ 2798ms
Response Format Validation.Success Retry After Present When Retry Events ✅ 2713ms
Response Format Validation.Success No Retry After When Drop Only ✅ 2754ms
Response Format Validation.Response Echoes Request Id ✅ 2757ms
Retry Behavior.Retries On 408 ✅ 9259ms
Retry Behavior.Retries On 500 ✅ 9254ms
Retry Behavior.Retries On 503 ✅ 11266ms
Retry Behavior.Retries On 504 ✅ 9303ms
Retry Behavior.Retries On 404 ✅ 5424ms
Retry Behavior.Retries On 429 ✅ 5428ms
Retry Behavior.Retries On 502 ✅ 9217ms
Retry Behavior.Retryable Errors Have Retry After ✅ 2353ms
Retry Behavior.Respects Retry After On Retryable Error ✅ 8424ms
Retry Behavior.Does Not Retry On 400 ✅ 2367ms
Retry Behavior.Does Not Retry On 401 ✅ 4914ms
Retry Behavior.Does Not Retry On 402 ✅ 4838ms
Retry Behavior.Does Not Retry On 413 ✅ 4833ms
Retry Behavior.Does Not Retry On 415 ✅ 4775ms
Retry Behavior.Non Retryable Errors Have No Retry After ✅ 4673ms
Retry Behavior.Implements Backoff ✅ 18283ms
Partial Batch Handling.Handles 200 Full Success ✅ 4767ms
Partial Batch Handling.Handles 200 With All Ok ✅ 5782ms
Partial Batch Handling.Does Not Retry Dropped Events ✅ 5787ms
Partial Batch Handling.Does Not Retry Warning Events ✅ 5752ms
Partial Batch Handling.Prunes Ok Events On Partial Retry ✅ 7807ms
Partial Batch Handling.Prunes Dropped Events On Partial Retry ✅ 7715ms
Partial Batch Handling.Retries Only Retry Events From Partial ✅ 7856ms
Partial Batch Handling.Partial Retry Preserves Uuids ✅ 7872ms
Partial Batch Handling.Partial Retry Attempt Header Increments ✅ 7682ms
Partial Batch Handling.Partial Retry Request Id Preserved ✅ 7750ms
Partial Batch Handling.Respects Retry After On Partial ✅ 7867ms
Partial Batch Handling.Unknown Result Treated As Terminal ✅ 5764ms
Partial Batch Handling.Mixed Ok Drop Warning No Retry ✅ 5865ms
Compression.Sends Gzip Content Encoding ✅ 2765ms
Compression.No Content Encoding When Disabled ✅ 2647ms
Compression.Compressed Body Is Decompressible ✅ 2858ms
Error Handling.Does Not Retry On Unknown 4Xx ✅ 4840ms
Event Options.Cookieless Mode Override ✅ 2698ms
Event Options.Disable Skew Correction Override ✅ 2663ms
Event Options.Process Person Profile Override ✅ 2810ms
Event Options.Product Tour Id Override ✅ 2665ms
Event Options.Unset Options Omitted ✅ 299ms
Event Options.Options Override In Batch ✅ 2875ms
Event Options.Unknown Option Passes Through ✅ 2710ms
Event Options.Option Value Is Not Converted ✅ 2685ms
Event Options.Lenient Bool Option Values Are Not Converted ✅ 418ms
Event Options.Option Wins Over Legacy Property ✅ 2817ms
Event Options.Legacy Property Fills Unset Option ✅ 2779ms
Event Options.Null Option Falls Back To Legacy Property ✅ 2767ms
Event Size.Event Over Ceiling Is Not Sent ✅ 6952ms
Event Size.Batches Close Before Byte Target ✅ 8570ms
Geoip And Historical Migration.Geoip Disable Injected Into Properties ✅ 2811ms
Geoip And Historical Migration.Historical Migration Absent By Default ✅ 2740ms
Property Layers.Caller Property Beats Registered Property ✅ 2792ms
Property Layers.Before Send Runs Last ✅ 2736ms
Fail Open.Empty Api Key Sends Nothing ✅ 1236ms
Fail Open.Personal Api Key Sends Nothing ✅ 1302ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2794ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2806ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2671ms
Request Payload.Flags Request Omits Authorization Header ✅ 2670ms
Request Payload.Token In Flags Body Matches Init ✅ 2790ms
Request Payload.Groups Round Trip ✅ 2728ms
Request Payload.Groups Default To Empty Object ✅ 2733ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2783ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2764ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2671ms
Request Lifecycle.No Flags Request On Init Alone ✅ 90ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2726ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5526ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2706ms
Retry Behavior.Retries Flags On 502 ✅ 2767ms
Retry Behavior.Retries Flags On 504 ✅ 2819ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 2939ms

@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 Outdated
@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
Comment thread PostHog/PostHogQueue.swift
turnipdabeets and others added 5 commits October 27, 2026 09:49
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alse

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-remove-batch-merge

# Conflicts:
#	PostHog/PostHogApi.swift
@turnipdabeets turnipdabeets changed the title feat!: send analytics events through capture v1 feat!: send events through capture v1 and add captureAi Oct 28, 2026

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

This is review round 5 on this pull request. When findings keep coming back in the same code, the approach is usually the problem. Before the next fix, check whether a different design would remove the whole class of findings.

Comment thread PostHog/PostHogQueue.swift
Comment thread PostHog/PostHogSDK.swift
Comment thread sdk_compliance_adapter/Sources/main.swift
turnipdabeets and others added 3 commits October 28, 2026 11:59
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>
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
turnipdabeets and others added 4 commits October 28, 2026 13:30
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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

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>
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 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>

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