Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 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
042b7bf
feat: add captureAi on the capture V1 AI endpoint
turnipdabeets Oct 10, 2026
2b7cbd7
fix: let per-call $set, $set_once and groups win over registered ones
turnipdabeets Oct 25, 2026
3d8bec7
feat: cap AI queue disk use and return the event UUID from captureAi
turnipdabeets Oct 10, 2026
af822d8
fix: flush and bound AI queue reads by batch bytes, and flush it on c…
turnipdabeets Oct 20, 2026
d384209
chore: return success from the compliance adapter's /capture_ai
turnipdabeets Oct 20, 2026
27ec307
feat(queue): cap analytics batches at 5 MiB
turnipdabeets Oct 22, 2026
800e920
feat: give AI requests a 60 s timeout and log non-$ai_ captureAi names
turnipdabeets Oct 22, 2026
a0a729c
docs: note the 5 MiB analytics batch cap in the migration guide
turnipdabeets Oct 23, 2026
cf6c2ae
docs: add an AI events section to the migration guide
turnipdabeets Oct 23, 2026
a58da3c
feat: add captureAi settings to PostHogConfig
turnipdabeets Oct 23, 2026
ee92eab
fix: make captureAiMaxEventBytes limit the whole serialized event
turnipdabeets Oct 26, 2026
8ac03d0
feat!: remove the /batch fallback from capture V1
turnipdabeets Oct 10, 2026
a35f65c
chore: stop ignoring /batch in replay network capture
turnipdabeets Oct 21, 2026
9c7b166
docs: drop the /batch fallback from the migration guide
turnipdabeets Oct 23, 2026
65b2cc3
fix: keep ignoring /batch in replay network capture
turnipdabeets Oct 26, 2026
3479164
docs: list the reverse proxy requirements for capture v1
turnipdabeets Oct 27, 2026
949225e
test(compliance): disable AI compression when enable_compression is f…
turnipdabeets Oct 27, 2026
b6c4c91
fix: log capture V1 drop reasons and the captureAiMaxEventBytes clamp
turnipdabeets Oct 27, 2026
5a584cb
docs: note the captureAiMaxEventBytes clamp warning
turnipdabeets Oct 27, 2026
e5f3de6
Merge branch 'chore/v4-remove-deprecated' into feat/v4-capture-v1
turnipdabeets Oct 27, 2026
eb60169
Merge remote-tracking branch 'origin/feat/v4-capture-ai' into feat/v4…
turnipdabeets Oct 27, 2026
9881b2e
Merge branch 'feat/v4-capture-v1' into feat/v4-capture-ai
turnipdabeets Oct 28, 2026
2ed20ee
Merge branch 'chore/v4-remove-deprecated' into feat/v4-capture-v1
turnipdabeets Oct 28, 2026
078c6e4
chore: combine the capture V1, options and captureAi changesets into …
turnipdabeets Oct 28, 2026
9c1565b
fix: flush analytics events at the batch byte limit
turnipdabeets Oct 28, 2026
1e0cc55
fix: keep a caller-supplied UUID in the /capture_ai adapter route
turnipdabeets Oct 28, 2026
30a9645
chore: update the no-traits API snapshot for the capture V1 API changes
turnipdabeets Oct 28, 2026
1306280
chore: show capture options and captureAi in the example apps (#985)
turnipdabeets Oct 28, 2026
b53ff66
revert: drop the adapter-only captureAi UUID overload
turnipdabeets Oct 28, 2026
0fd48c0
fix: retry capture v1 requests that get 404 or 429 (#979)
turnipdabeets Oct 28, 2026
68c96cb
fix: flush analytics once a batch's worth of bytes is queued (#986)
turnipdabeets Oct 28, 2026
f5e139d
chore(compliance): bump the SDK test harness to 1.26.0 (#988)
turnipdabeets Oct 29, 2026
48e0e42
fix: don't log the response body of failed capture requests
turnipdabeets Oct 30, 2026
f9a2716
docs: summarize capture v1 in MIGRATION.md and link the guide
turnipdabeets Oct 30, 2026
da388b8
docs: add long distinct IDs and phx_ keys to the 4.0 changelog
turnipdabeets Oct 30, 2026
70e38fe
docs: add the proxy Authorization rule to the migration guide
turnipdabeets Oct 30, 2026
29bc86c
docs: note non-JSON values, 402, AI flushes and registered options in…
turnipdabeets Oct 30, 2026
8caa38d
feat: add build and send steps for wrapper SDKs' beforeSend (#991)
turnipdabeets Oct 30, 2026
d8b98f0
Merge chore/v4-remove-deprecated into feat/v4-capture-v1, folding cap…
turnipdabeets Oct 31, 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
13 changes: 13 additions & 0 deletions .changeset/v4-major-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ PostHog iOS 4.0: see the [migration guide](https://github.com/PostHog/posthog-io
- **Breaking:** remove `PostHogDataMode.cellular` — use `.any`
- **Breaking:** move `postHogSdkName`, `postHogVersion`, `PostHogConfig.snapshotEndpoint`, `PostHogSessionManager`, `PostHogStorageManager` and `PostHogConfig.storageManager` behind `@_spi(PostHogInternal)` — use `PostHogSDK` session and identity methods
- **Breaking:** remove `dateToMillis(_:)`, `imageToBase64(_:_:)`, `toISO8601String(_:)`, `toISO8601Date(_:)`, `sanitizeDictionary(_:)`, `deleteSafely(_:)`, `postHogiOSSdkName`, `Gzip`, `GzipError`, `CompressionLevel`, `ReachabilityError`, `ReachabilityChangedNotification`, `UIColor.init(hex:)` and `UIColor.hexDescription(_:)` from the public API
- **Breaking:** send analytics events to capture V1 (`/i/v1/analytics/events`) with the project token in an `Authorization: Bearer` header, replacing any `Authorization` from `requestHeaders` — serve this path on reverse proxies and self-hosted instances
- **Breaking:** follow only 307 and 308 redirects to the same origin as `host`, at most 5 times, and drop events on any other redirect
- **Breaking:** drop an event whose distinct ID is over 200 UTF-8 bytes instead of shortening the ID
- **Breaking:** disable the SDK at setup when `projectToken` is a personal API key (`phx_...`) — use your project token (`phc_...`)
- **Breaking:** pause sending for at most 30 seconds on `Retry-After`, including session replay and logs
- **Breaking:** send `$process_person_profile`, `$cookieless_mode`, `$ignore_sent_at` and `$product_tour_id` as capture options, so PostHog drops an event whose option value it can't read
- **Breaking:** let registered super properties override SDK properties such as `$os`, and let super and event properties override `$is_identified`, `$process_person_profile`, `$geoip_disable` and the session replay debug values
- **Breaking:** rename `maxRetries` to `pushNotificationSubscriptionMaxRetries`
- Add per-event capture options with `capture(..., timestamp:options:)` and `PostHogEvent.options`, also accepted by `screen`, `identify`, `setPersonProperties`, `alias`, `group` and `captureException`
- Add `captureAi(...)` to send AI events such as `$ai_generation` to `/i/v1/ai/events`, tuned with `captureAiCompression`, `captureAiMaxQueueSize`, `captureAiTimeout` and `captureAiMaxEventBytes`
- Change the wait after a failed send to double from 1 second up to 30 seconds with jitter, for events, session replay and logs
- Cap analytics batches at 5 MiB per request and send a larger event on its own
- Log the name, UUID and reason of each event PostHog drops when `debug` is on
- Add `SessionReplay`, `CrashReporting` and `Surveys` Swift package traits (on by default) that remove the feature and its public API when turned off
- Change network reachability detection to `NWPathMonitor`, so a path needing user action counts as reachable, queues pause while cellular data is off for the app, and early events may omit `$network_wifi` and `$network_cellular`
- Change the replay screen name to the visible screen, reporting the view name for SwiftUI screens and no longer using a plain `UIViewController`'s `title`
Expand Down
187 changes: 184 additions & 3 deletions MIGRATION.md

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions PostHog.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
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 */; };
A1C0DE00000000000000FAB2 /* PostHogWrapperEventTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE00000000000000FAB1 /* PostHogWrapperEventTest.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 @@ -961,6 +963,8 @@
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>"; };
A1C0DE00000000000000FAB1 /* PostHogWrapperEventTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHogWrapperEventTest.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 @@ -1884,6 +1888,8 @@
DAA1B2D02F8C100000112233 /* RageClickDetectorTest.swift */,
DAA1B2D22F8C100000112233 /* PostHogRageClickIntegrationTest.swift */,
690FF0E82AEFD3BD00A0B06B /* PostHogQueueTest.swift */,
A1C0DE0000000000000000F1 /* PostHogCaptureV1Test.swift */,
A1C0DE00000000000000FAB1 /* PostHogWrapperEventTest.swift */,
A1C081010000000000000002 /* PostHogReplayBatchBoundaryTest.swift */,
690FF0F42AF0F06100A0B06B /* PostHogSDKTest.swift */,
5CA51F0E2B5C9A0100000002 /* PostHogScreenNameTest.swift */,
Expand Down Expand Up @@ -3786,6 +3792,8 @@
AE3C6A5B6DAB01EF554A0270 /* PostHogPushNotificationTest.swift in Sources */,
D36F92424A13D7CE61B6E348 /* PostHogSwizzlerTest.swift in Sources */,
690FF0E92AEFD3BD00A0B06B /* PostHogQueueTest.swift in Sources */,
A1C0DE0000000000000000F2 /* PostHogCaptureV1Test.swift in Sources */,
A1C0DE00000000000000FAB2 /* PostHogWrapperEventTest.swift in Sources */,
A1C081010000000000000001 /* PostHogReplayBatchBoundaryTest.swift in Sources */,
3AE3FB4B2993A68500AFFC18 /* PostHogStorageTest.swift in Sources */,
DAD39DF62D56178D002A1A69 /* UtilsTest.swift in Sources */,
Expand Down
51 changes: 49 additions & 2 deletions PostHog/Models/PostHogEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,63 @@ 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) {
/// The caller's person properties, kept from build to send so feature flags see them once the event is accepted.
var flagUserProperties: [String: Any]?
var flagUserPropertiesSetOnce: [String: Any]?

/// Set when a wrapper build returns this event: true for the AI lane. Cleared once it's sent, so it's sent once.
var wrapperAiLane: Bool?
/// The SDK's distinct ID when a wrapper built this event, so a send after `reset()` or `identify()`
/// doesn't give the new user this event's person properties for feature flags.
var wrapperBuildDistinctId: String?
/// The SDK's reset count when a wrapper built this event, so a `reset()` that keeps the distinct ID
/// (`reuseAnonymousId`) still counts as a new identity at send.
var wrapperBuildResetGeneration: Int?
/// The SDK instance and setup generation that built this wrapper event, so a send after `close()`/`setup()`
/// or on another instance is dropped instead of sent with the new configuration.
var wrapperBuildSDK: ObjectIdentifier?
var wrapperBuildSetupGeneration: Int?
/// The optional replay debug values snapshotted when a wrapper built this event, set only when its built name
/// is eligible. Send adds them if it claims the throttled window.
var wrapperDeferredReplayDebug: [String: Any]?
/// True when a wrapper build passed person properties or groups. Person processing turns on at send.
var wrapperRequiresPersonProcessing = false

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 +115,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 +132,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