Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2e46974
feat!: send analytics events through capture v1
turnipdabeets Oct 9, 2026
c1850fd
chore: declare capture_v1 in the compliance adapter
turnipdabeets Oct 9, 2026
ef924f0
chore: document capture auth header, cover fallback retry policy, map…
turnipdabeets Oct 9, 2026
32ef0b9
chore: log dropped duplicate UUIDs, cover a failing /batch fallback, …
turnipdabeets Oct 9, 2026
27469d7
fix: follow capture redirects only to the configured origin
turnipdabeets Oct 9, 2026
804b1e9
feat!: add per-event capture options
turnipdabeets Oct 9, 2026
bc85281
docs: clarify capture options precedence and the Retry-After cap
turnipdabeets Oct 9, 2026
9007cf6
chore: track capture V1 requests in the compliance adapter
turnipdabeets Oct 9, 2026
1505ffb
docs: note the Retry-After cap applies to every queue and 413 still s…
turnipdabeets Oct 10, 2026
5852d90
docs: rewrite the capture V1 changeset as changelog entries
turnipdabeets Oct 10, 2026
fad2204
test: stop the capture redirect tests racing the HTTP stubs
turnipdabeets Oct 10, 2026
d5fc585
fix: resend a capture V1 batch whose transfer failed after a 2xx
turnipdabeets Oct 20, 2026
fef9de3
refactor: stop deduplicating event UUIDs before capture
turnipdabeets Oct 22, 2026
29503f3
docs: add the 4.0 capture v1 migration guide
turnipdabeets Oct 22, 2026
81fed4c
fix: give each capture its own UUID when beforeSend returns an earlie…
turnipdabeets Oct 22, 2026
f04315e
fix(queue): back off exponentially with jitter between failed sends
turnipdabeets Oct 22, 2026
17751d7
docs: describe the jittered exponential backoff in the 4.0 migration …
turnipdabeets Oct 22, 2026
564b9df
feat(config): rename maxRetries to pushNotificationSubscriptionMaxRet…
turnipdabeets Oct 22, 2026
621bb26
refactor(config)!: drop the deprecated maxRetries alias
turnipdabeets Oct 22, 2026
b0636c9
fix(capture): send empty session and window IDs and log dropped non-s…
turnipdabeets Oct 22, 2026
ce3ba68
fix: treat a capture V1 2xx whose results can't be read as delivered
turnipdabeets Oct 22, 2026
b10cabc
fix: send process_person_profile only when person processing is off
turnipdabeets Oct 22, 2026
08a631b
docs: describe how 4.0 sends process_person_profile in the migration …
turnipdabeets Oct 22, 2026
02be3d5
test(compliance): run the adapter with personProfiles = .always
turnipdabeets Oct 22, 2026
68d328c
feat!: accept options on screen, identify, alias, group and captureEx…
turnipdabeets Oct 22, 2026
1491001
fix: don't deduplicate person properties sent without person processing
turnipdabeets Oct 23, 2026
a9c3065
fix: use the current distinct ID for a blank distinctId argument
turnipdabeets Oct 23, 2026
25fbf00
fix: disable the SDK when projectToken is a personal API key
turnipdabeets Oct 23, 2026
9367a2e
docs: mark the migration guide to link the public capture v1 guide
turnipdabeets Oct 23, 2026
31a80cb
feat!: let event properties override the SDK's own values
turnipdabeets Oct 23, 2026
74d8c21
feat!: let super properties override properties the SDK sets
turnipdabeets Oct 23, 2026
44cf275
docs: fix what beforeSend sees for $lib and note registered $session_id
turnipdabeets Oct 23, 2026
b50af50
test: send both GeoIP override events in one batch
turnipdabeets Oct 23, 2026
2b7cbd7
fix: let per-call $set, $set_once and groups win over registered ones
turnipdabeets Oct 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/v4-capture-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'posthog-ios': minor
---

Add per-event capture options with `capture(..., timestamp:options:)` and `PostHogEvent.options`, which `beforeSend` can read and change. `screen`, `identify`, `setPersonProperties`, `alias`, `group` and `captureException` take the same `options` argument.
16 changes: 16 additions & 0 deletions .changeset/v4-capture-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'posthog-ios': major
---

**Breaking:** send events to capture V1 (`/i/v1/analytics/events`) with the project token in an `Authorization: Bearer` header.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

v1 is strict about dropping events with missing names or distinct_ids, or if either exceeds 200 bytes in size (legacy backend already silently enforces this) we should probably mention this in the migration guide. If you like I can add that migration guide to your feature branch after all these PRs land? I'm still making parity updates locking decisions as Rust/Go/Python v1 open PRs land, but I've got similar guides drafted in PRs on those v1 SDK branches as an example. General new requirements will be covered in the new draft public docs for release to avoid duplication. let me know what you think or if this should be a follow on

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.

I added an iOS migration guide here, modeled on yours: https://github.com/PostHog/posthog-ios/blob/feat/v4-capture-v1/docs/migration-3.x-to-4.0.md - let me know how this looks :)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks great, thanks! Can the guide get a hidden TODO to link the public capture v1 SDK upgrade guide (PostHog/posthog.com#21386) for details once it's published? The Go, Python and Rust guides carry the same marker.

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.

Added in 1963f5d, same marker as the Go, Python and Rust guides.


- If you use a reverse proxy, forward `/i/v1/analytics/events`. A 404 falls back to `/batch` until the app restarts; other errors such as 403 or 405 drop the events.
- Capture requests replace any `Authorization` header from `requestHeaders`, so authenticate your proxy with a different header.
- Only a 307 or 308 redirect to the same origin as `host` is followed, at most 5 times. Other redirects drop the events, so point `host` at the final host.
- 429 responses are no longer retried, and `Retry-After` pauses sending for at most 30 seconds, including session replay and logs.
- After a failed send, the wait before the next one now doubles from 1 second up to 30 seconds, with up to 25% random jitter, instead of growing by 1 second per failure. This applies to events, session replay and logs, and queued records are still kept until they're delivered.
- `maxRetries` is renamed to `pushNotificationSubscriptionMaxRetries`, because it only limits push-notification subscription retries. Event, session replay and log retries still have no limit.
- The `$process_person_profile`, `$cookieless_mode`, `$ignore_sent_at` and `$product_tour_id` properties are sent as capture options, and PostHog drops an event whose option value it can't read.
- Event properties are merged as: properties the SDK sets itself, then registered super properties, then the properties you pass, then `beforeSend`. Super properties now override SDK properties such as `$os`, and both super and event properties override `$is_identified`, `$process_person_profile`, `$geoip_disable` and the session replay debug values.

See the [migration guide](https://github.com/PostHog/posthog-ios/blob/main/docs/migration-3.x-to-4.0.md) for what to check before you upgrade.
4 changes: 4 additions & 0 deletions PostHog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
6A0C1F2E3B4D5A6B7C8D9E02 /* PostHogOptOutPersistenceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0C1F2E3B4D5A6B7C8D9E01 /* PostHogOptOutPersistenceTest.swift */; };
A1C081010000000000000001 /* PostHogReplayBatchBoundaryTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C081010000000000000002 /* PostHogReplayBatchBoundaryTest.swift */; };
690FF0E92AEFD3BD00A0B06B /* PostHogQueueTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */; };
A1C0DE0000000000000000F2 /* PostHogCaptureV1Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE0000000000000000F1 /* PostHogCaptureV1Test.swift */; };
690FF0EF2AEFF23D00A0B06B /* OHHTTPStubsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 690FF0EE2AEFF23D00A0B06B /* OHHTTPStubsSwift */; };
690FF0F12AEFF24200A0B06B /* OHHTTPStubs in Frameworks */ = {isa = PBXBuildFile; productRef = 690FF0F02AEFF24200A0B06B /* OHHTTPStubs */; };
E1A0B0203A0D000100000001 /* CwlCatchException in Frameworks */ = {isa = PBXBuildFile; productRef = E1A0B0213A0D000100000001 /* CwlCatchException */; };
Expand Down Expand Up @@ -966,6 +967,7 @@
6A0C1F2E3B4D5A6B7C8D9E01 /* PostHogOptOutPersistenceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogOptOutPersistenceTest.swift; sourceTree = "<group>"; };
A1C081010000000000000002 /* PostHogReplayBatchBoundaryTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogReplayBatchBoundaryTest.swift; sourceTree = "<group>"; };
690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogQueueTest.swift; sourceTree = "<group>"; };
A1C0DE0000000000000000F1 /* PostHogCaptureV1Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogCaptureV1Test.swift; sourceTree = "<group>"; };
690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogSDKTest.swift; sourceTree = "<group>"; };
690FF1732AF3CE8A00A0B06B /* PostHogExampleWatchOS.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PostHogExampleWatchOS.xcodeproj; path = PostHogExampleWatchOS/PostHogExampleWatchOS.xcodeproj; sourceTree = "<group>"; };
69261D122AD5685B00232EC7 /* PostHogRemoteConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogRemoteConfig.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1888,6 +1890,7 @@
DAA1B2D02F8C100000112233 /* RageClickDetectorTest.swift */,
DAA1B2D22F8C100000112233 /* PostHogRageClickIntegrationTest.swift */,
690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */,
A1C0DE0000000000000000F1 /* PostHogCaptureV1Test.swift */,
A1C081010000000000000002 /* PostHogReplayBatchBoundaryTest.swift */,
690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */,
5CA51F0E2B5C9A0100000002 /* PostHogScreenNameTest.swift */,
Expand Down Expand Up @@ -3806,6 +3809,7 @@
AE3C6A5B6DAB01EF554A0270 /* PostHogPushNotificationTest.swift in Sources */,
D36F92424A13D7CE61B6E348 /* PostHogSwizzlerTest.swift in Sources */,
690FF0E92AEFD3BD00A0B06B /* PostHogQueueTest.swift in Sources */,
A1C0DE0000000000000000F2 /* PostHogCaptureV1Test.swift in Sources */,
A1C081010000000000000001 /* PostHogReplayBatchBoundaryTest.swift in Sources */,
3AE3FB4B2993A68500AFFC18 /* PostHogStorageTest.swift in Sources */,
DAD39DF62D56178D002A1A69 /* UtilsTest.swift in Sources */,
Expand Down
29 changes: 27 additions & 2 deletions PostHog/Models/PostHogEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,41 @@ import Foundation
/// Event properties that will be serialized with the event.
@objc public var properties: [String: Any]

/// Options that tell PostHog how to process this event, separate from its properties.
///
/// Holds the options passed to `capture(..., options:)`. Known keys are `process_person_profile`,
/// `cookieless_mode`, `disable_skew_correction` and `product_tour_id`; unknown keys are sent
/// unchanged and ignored by PostHog. When the event is sent, the legacy `$process_person_profile`, `$cookieless_mode`,
/// `$ignore_sent_at` and `$product_tour_id` properties are removed from `properties` and used
/// for their options, unless that option is set here.
@objc public var options: [String: Any]

/// Event timestamp. The absolute instant is serialized in UTC, regardless of the calendar or
/// time zone used to create it.
@objc public var timestamp: Date

/// Unique event identifier generated by the SDK.
@objc public private(set) var uuid: UUID
@objc public internal(set) var uuid: UUID
// Only used for Replay.
// Serialized as api_key on the wire.
var projectToken: String?

/// Set by the SDK when this event took the throttled replay debug bundle, so it survives `beforeSend` renames or replacement.
var carriesReplayDebugBundle = false

init(event: String, distinctId: String, properties: [String: Any]? = nil, timestamp: Date = Date(), uuid: UUID = UUID.v7(), projectToken: String? = nil) {
init(
event: String,
distinctId: String,
properties: [String: Any]? = nil,
options: [String: Any]? = nil,
timestamp: Date = Date(),
uuid: UUID = UUID.v7(),
projectToken: String? = nil
) {
self.event = event
self.distinctId = distinctId
self.properties = properties ?? [:]
self.options = options ?? [:]
self.timestamp = timestamp
self.uuid = uuid
self.projectToken = projectToken
Expand Down Expand Up @@ -75,6 +93,8 @@ import Foundation
event: event,
distinctId: distinctId,
properties: properties,
// Events queued by older versions have no options.
options: json["options"] as? [String: Any],
timestamp: timestampDate,
uuid: uuidObj,
projectToken: projectToken
Expand All @@ -90,6 +110,11 @@ import Foundation
"uuid": uuid.postHogUuidString,
]

// Stored for the queue only: each endpoint shapes its own wire format.
if !options.isEmpty {
json["options"] = options
}

if let projectToken {
// Wire field name remains api_key, but it carries the PostHog project token.
json["api_key"] = projectToken
Expand Down
Loading
Loading