Repository navigation
chore!: iOS Major Release 4.0 - #873
turnipdabeets wants to merge 83 commits into
Conversation
There was a problem hiding this comment.
This review was skipped because it would exceed your organization's monthly flex usage limit. Raise the limit in billing settings or wait until the next billing period resets limits.
posthog-ios Compliance ReportDate: 2026-11-15T03:08:41.275357+00:00 ✅ All Tests Passed!234/234 tests passed Capture_V1 Tests✅ 106/106 tests passed View Details
Capture_Ai_V1 Tests✅ 111/111 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
6db44c0 to
9393686
Compare
|
[High risk] Removes public API surface for iOS SDK major version. The PR appears safe to merge; no actionable regression was identified. Reviews (1) · Last reviewed commit: "chore!: remove deprecated public APIs fo..." |
e1cc8cf to
5b6c50e
Compare
…ading /flags still returns enabled: true for a flag with an empty variant, so other PostHog SDKs can still report it as enabled after this upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat: let wrapper SDKs register capture options for every event Adds registerOptionsForWrapper(_:) and unregisterOptionsForWrapper(_:) behind @_spi(PostHogInternal), so posthog-flutter can offer global options without adding public API to posthog-ios. Registered options are persisted, cleared by reset(), saved into the crash context, and filled under each event's own options before beforeSend runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: wait for each registered-options event on its own * fix: read registered options under the lock that guards their writes --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix: omit null-valued event properties
Drop properties whose value is NSNull (or a nil boxed in Any) from analytics
and AI events when they are written to the queue and again when they are
sent, recursing into nested dictionaries and dictionaries inside arrays.
Null array elements keep their index and emptied dictionaries stay {}.
$set, $set_once and $group_set values are left as given so a caller can
still clear a person or group property.
Implements the sdk-specs capture requirement "Capture drops null-valued
object properties" and capture-ai's matching requirement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: drop nulls in dictionaries with non-string keys
A dictionary with number or boolean keys, such as one added in beforeSend,
skipped the null cleanup and was then stringified by sanitizeDictionary,
so its null members reached the queue as JSON null.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cated # Conflicts: # PostHog.podspec # PostHog/PostHogVersion.swift # PostHog/Surveys/PostHogSurveysDefaultDelegate.swift # PostHogTests/PostHogSurveyEventsTest.swift
|
posthog-ios/PostHog/PostHogSDK.swift Lines 966 to 969 in 4cbc634 Merging |
… fill $lib as a pair (#1050) * fix: skip the person-properties hash for maybe-personless updates and fill $lib as a pair The hash guard read only the process_person_profile option, but a registered or legacy $process_person_profile false is hoisted into options later, at send. Such a $set recorded a hash it couldn't apply, so the same update sent after unregistering was dropped as a duplicate. Resolve the setting the way it's sent (non-null option, then the legacy property) and record a hash only when it's missing or a Boolean true. Fill the SDK's $lib and $lib_version as a pair after the caller's properties, per sdk-specs #146: another library's $lib never gets this SDK's version, and a $lib_version without a $lib is replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: clear the person-properties hash when queueing a maybe-personful update An update whose setting isn't a clear Boolean, such as the string "true", skipped the hash but left the previous one in place. PostHog may still apply it, so a later update back to the previous properties was dropped as a duplicate. Clear the stored hash instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cated Brings in #1048 (drop flag loads in flight across reset()). PostHogRemoteConfig.swift and its tests take the 4.0 resolution from #1044 (fix/v4-drop-flag-loads-across-reset), which keeps 4.0's FeatureFlagsLoadResult callback and the removed didReceiveFeatureFlags. PostHog.podspec stays deleted.
|
Fixed in e27cbe4, the reset now uses the Surveys trait guard. |
…etting (#1053) * docs: show how to keep identity calls under a registered personless setting Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: point registered personless users to identifiedOnly and note billing --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Anna Garcia <anna.g@posthog.com>
* feat: keep screenshotModeBackgroundCapture in 4.0 Reverts #937. GPU capture (#911) stays experimental and off by default, so the main-thread drawHierarchy path remains the default in 4.0 and background capture is the only off-main option. It unblocks the main-thread hangs reported in #1035. Drops the removed-API stub, the migration-guide row and the changeset mention, and restores the public API snapshot line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(replay): skip hidden or transitioning windows before background capture Background capture walked the hierarchy twice and rendered the window before prepareScreenshotWireframe discarded the frame during a view controller transition. Check visibility and transition state first, as scheduleSettledCapture already does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: order the restored screenshotModeBackgroundCapture snapshot line Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(replay): release background-capture mask samples on the main thread MaskedRegion retains its view, so the before sample could hold the last reference to a view and release it on the replay queue, running UIKit teardown off main. Drop it inside the second main.sync instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
RRWireframe only builds with SessionReplay in 4.0, so the no-traits test build failed on the tests main added in #1028.
4.0 renamed RRWireframe to RRScreenshotFrame, so the iOS test build failed on the tests from #1028. This replaces the trait gating from the previous two commits, which was the wrong fix: the type is not trait-gated.
Capture v1 moved to /docs/api/capture-v1 (posthog.com#21385). The masking table marks all three options as defaults, and the generated references copy is refreshed so it no longer says screenshotModeBackgroundCapture was removed.
* fix: keep a registered personless setting off identity events A registered `$process_person_profile: false`, or a `process_person_profile` option a wrapper registers, no longer applies to `$identify`, `$create_alias`, `$merge_dangerously`, `$groupidentify`, `$set` and `$set_once`. PostHog drops those events with person processing off. A value passed on the call or set in `beforeSend` still applies. * docs: note the identity-event exception on register and capture * docs: keep the $set and $set_once distinction in the migration guide * test: cover $set_once, $merge_dangerously, a call property and beforeSend
…#1061) * docs: sort the iOS 4.0.0 migration guide by when you make each change Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: keep the 4.0.0 guide's table cells to one value Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: move the posthog.com iOS SDK pages into the reference The posthog.com iOS SDK pages redirect to this reference once 4.0.0 ships, so their setup, how-to, and configuration sections now live in docs/getting-started.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: title a single migration guide's page with its H1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit 4f8c153) * Revert "docs: move the posthog.com iOS SDK pages into the reference" This reverts commit 0740925. * docs: add the iOS facts from the retired posthog.com pages that no other page has Drop the CocoaPods install line, since 4.x.x isn't published to CocoaPods. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: move the iOS one-API facts from getting-started into doc comments The sampling note moves to setBeforeSend. The surveys swizzling note is dropped, since 4.x.x surveys don't need swizzling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: preserve ios reference details * docs: name the SDK in versions, cut self-hosted notes, and split dashes and semicolons Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: address review on the reordered iOS 4.0 migration guide - Say 4.x.x sends analytics and AI events to capture v1, not every event - Say follow ups can wait because 4.x.x still accepts the old names and options - Add a checklist item for filters on a null `$feature_flag_response` - Say what `$ai_*` events sent with `capture` still do, and what `captureAi` adds - Say AI events queued by 4.x.x are sent again after a re-upgrade - Reword the retry rules intro, since not all of them are about failures - Regenerate references/posthog-ios-migration.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Anna Garcia <anna.g@posthog.com>
…cated Brings in 3.95.0-3.96.2 and GPU screenshot capture (#911, #966). Resolved for 4.0: screenshot capture always on (screenshotMode stays removed, GPU capture docs no longer reference it), RRWireframe -> RRScreenshotFrame, PostHog.podspec stays deleted, new GPU/replay sources and tests gated on the SessionReplay trait, survey cancel events gated on Surveys. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tests main added in #1059 expect NSNull, but 4.0 omits null properties, and the feature-flag-called spec allows an omitted response as the missing sentinel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # PostHog.podspec # PostHog/PostHogVersion.swift
…cated Brings in #1054 (cleared labels and replaced images stay masked, and the app-defined CALayer presentation guard from #1064). - isLabelSensitive keeps 4.0's image-attachment check alongside #1054's cleared-text check. - toWireframe stays removed (4.0 has no wireframe mode); main's hasSensitiveImageAttachment is dropped for 4.0's existing one. - #1054's wireframe-only test is dropped, its fixture uses RRScreenshotFrame, and the new replay test files are gated on the SessionReplay trait like the other replay suites.
Important
This is the posthog-ios 4.0 integration PR. Don't merge it until every item under "Before release" is done. It targets
mainwithout changesets pre mode, so merging it publishes 4.0.0. "Release day" and "After release" items don't block the merge. Every stacked 4.0 PR merges into this branch (chore/v4-remove-deprecated), and this PR brings them intomainat once. The branch carries one changeset,.changeset/v4-major-release.md.Closes #557
💡 Motivation and Context
posthog-ios 4.0:
/i/v1/analytics/events) withAuthorization: Bearer, and the newcaptureAisends AI events to/i/v1/ai/events. There's no/batchfallback.options, and one property precedence: SDK-set, then super properties, then event properties, thenbeforeSend.The upgrade guide is
MIGRATION.md, sorted into Before, During and Follow ups. It's also served at/docs/libraries/ios/migration.Public API change: breaking, see the
api/posthog-ios.public-api.txtdiff. Removed and renamed 3.x APIs fail to compile with an error naming the replacement (#977).Docs and onboarding that ship with this release (merge once 4.0.0 is tagged on GitHub):
from: "4.0.0"), flags, logs, push and tutorial snippets, screenshot-only mobile replay pagesWhat gets released
posthog-ios(Swift Package Manager)PostHogpod (CocoaPods)Wrappers stay on 3.x until their own majors adopt 4.0: posthog_flutter
mainpins< 4.0.0, the React Native plugin pins~> 3.92.7, and both import posthog-ios with@_spi(PostHogInternal).feat/v6-desktop-capture-v1)screenshotMode) and #5449 (SPM by default), merged into the release branch; PostHog/posthog-js#5734 (captureMethod) is openReact Native cost of SPM only: the plugin's minimum React Native rises from 0.71 to 0.75 (the first with
spm_dependency), and its 3.0 makes SPM the default (#5449). No Podfile linkage or Expo change is needed. posthog-js#5237 turns theSurveystrait off in the plugin'sPackage.swift, but the plugin still setsconfig.surveys = false, which only compiles with the trait. Guard that line with a define only the plugin'sPackage.swiftsets, sincespm_dependencycan't pass traits.Merged into this branch
apiKey,propertiesSanitizer,evaluationEnvironments,remoteConfig,getFeatureFlagPayload(_:),debouncerDelay,maskPhotoLibraryImagesswift-tools-version: 6.2;Package@swift-5.9.swiftremovedSessionReplay,CrashReportingandSurveystraits, on by default; a trait that's off also removes its public API, snapshotted in CI@_spi(PostHogInternal)PostHogDataMode.cellularPostHogConsoleLog*$set; 3.x migrations are keptversionandbuildfrom lifecycle eventsreloadFeatureFlagscallback receives aPostHogFeatureFlagsLoadednilNWPathMonitorscreenshotModescreenshotModeBackgroundCapture(still experimental)didReceiveFeatureFlagsnotification; useonFeatureFlags(_:)setBeforeSend(_:)accepts plain closure arrays instead of trapping at runtimeunavailablefix-its for removed and renamed APIsisFeatureEnabled(_:defaultValue:)setSdkName(_:version:)(SPI, Objective-C) for wrapper SDKsregisterOptionsForWrapper(_:)/unregisterOptionsForWrapper(_:)(SPI, Objective-C)captureAi(#939), no/batchfallback (#940),$locale/$app_language(#1009),maxRetries→pushNotificationSubscriptionMaxRetries, and the capture v1 fixes (#1018, #1019, #1025, #1026)$liband$lib_versionper event, filled last and as a pair; skip the person-properties hash for updates that may be personlessNSNullandnilevent properties$process_person_profileno longer applies to$identify,$create_alias,$merge_dangerously,$groupidentify,$setand$set_onceRELEASING.mdand the CocoaPods noticeMIGRATION.mdfor all of 4.0, sorted by when you make each change, plusdocs/getting-started.mdfor the generated referencemainis merged in up to 3.96.2 plus #911 and #966 (experimental GPU capture, off by default): #1059 (failed flag records), #1063 (background-capture masks), #1042 and #1043 (surveys). 3.x changesets were released frommain, so they aren't repeated in the 4.0 changeset. Where they met 4.0, 4.0's shape is kept: a failed flag's$feature_flag_calledomits$feature_flag_responseinstead of sendingnull(06d91a9e8), which the guide's "Check null properties" covers.mainis then merged up to 3.97.0 (ef45d7b59): only the release commit, whosescreenshotModeGPUCapturechangeset was released frommain. The podspec stays deleted.Left in place on purpose:
PostHogEvent.apiKeyandPostHogRemoteConfig.getFeatureFlagPayload, andPostHogConfig(apiKey:)inscripts/downgrade-compatibility-smoke, which compiles against 3.48.0.migrateLegacyStorage(3.20) andmergeLegacyContainerIfNeeded(3.27). Removing them would lose the distinct ID and queued events for apps updating straight from an older version: in the last 7 days (US), about 8% of iOS SDK end users were on an app build older than 3.27. We'll revisit in a later major.sdk-specs: 4.0 implements the capture v1 contract (PostHog/sdk-specs#146, #147) and the property precedence from PostHog/sdk-specs#143. #938 lists where iOS differs from the spec. PostHog/sdk-specs#187 (4.0 compliance audit) is merged.
Before release
CALayersubclasses. With replay on, an app crashed when a masked view sat under aCALayersubclass with noinit(layer:), because mask measurement copied its presentation. Fixed by fix(replay): keep cleared labels and replaced images masked until the screen updates #1054 (with the guard from fix(replay): never copy an app-defined layer's presentation #1064), merged tomainand into this branch in623adb575. That merge keeps 4.0's image-attachment masking inisLabelSensitive, drops fix(replay): keep cleared labels and replaced images masked until the screen updates #1054's wireframe-only test, and gates its new replay tests on theSessionReplaytrait.captureMethod) merges, or is dropped. It replacesscreenshotModeBackgroundCaptureandscreenshotModeGPUCapture(shipped in 3.97.0) with onecaptureMethodoption; both getunavailablestubs, and the changeset names both. Needs review. This branch is merged into it (4a61c74d8). docs(onboarding): update ios onboarding for posthog-ios 4.0 posthog#129618's replay snippet setsscreenshotModeBackgroundCapture = false, which won't compile after feat!: replace replay capture flags with captureMethod #1071, and feat(react-native): add iOSCaptureMethod for standard, background or GPU replay capture posthog-js#5734 depends on it.mainin right before merging, after it releases fix(replay): keep cleared labels and replaced images masked until the screen updates #1054 as a 3.x patch: until then fix(replay): keep cleared labels and replaced images masked until the screen updates #1054's changesets (lazy-maps-hug.md,quiet-layers-copy.md) sit on this branch and would land in the 4.0 changelog. Also bring in any 3.x fixes that land first, such as fix: keep tracking app lifecycle after reset() #1068 and fix: invoke multicast subscribers in subscription order so Application Opened keeps its $session_id #1069, and rerun CI.products/growth/backend/sdk_health.py) flags every iOS 3.x project as outdated as soon as 4.0.0 is the latest release.mainruleset, replace the required checkBuild SDK (Swift 5 compatibility on Xcode 15.4 with CocoaPods)withBuild SDK (minimum toolchain, Xcode 26.0.1). This branch removes that job (chore!: publish 4.0 to Swift Package Manager only #972), so the old check never reports and blocks the merge. Swap it right before merging:maindoesn't run the new job until then.MIGRATION.mdopens with a link to/docs/api/capture-v1, a 404 until it does. Not a hard blocker.Release day, after 4.0.0 is tagged
context_mill_version(feat: install SDKs at the docs' version and keep agents on today's majors context-mill#502, feat(cli): add --context-mill-version to pin skills to a release wizard#1541, feat(wizard): pin cloud runs to a context-mill release posthog#132411, all merged).Package.resolvedonce the tag exists).After release
libraries/ios/index.mdx,configuration.mdx,usage.mdx) with the generated 4.x reference, after the iOS, Android, Flutter and React Native majors publish. #21807 marks theconfiguration.mdxoptions 4.0 removes or renames, so the page holds until then. #22061 no longer touches iOS.$app_language($localecollected different from iOS posthog-android#822): chore(taxonomy): add $app_language for mobile SDKs posthog#128366 (taxonomy) is merged; add a "Most common app language" tile to the global Mobile Analytics dashboard template, then close$localecollected different from iOS posthog-android#822.Found in testing, not 4.0 regressions
The 2026-11-05 device run turned up three things that behave the same in 3.93.6. None blocks 4.0:
optOut()thenoptIn()logs "Failed to enable PHPLCrashReporter: A PLCrashReporter instance has already been enabled". Crash reporting keeps working.identify(_:)on an already identified user with a different ID is ignored untilreset(), as in 3.x.💚 How did you test it?
2026-11-12, iPhone 17 Pro simulator (iOS 26.4), at
79325748a, through a logging proxy to PostHog Cloud (US), covering what merged after the 2026-11-05 run:okfor every event.nullarray elements and$setvalues are still sent.$liband$lib_versionare filled as a pair (fix: skip the person-properties hash for maybe-personless updates and fill $lib as a pair #1050). A backdated timestamp, options, the top-levelsession_idand Bearer auth arrive as expected.reset()(fix(flags): drop flag loads in flight across reset() #1048) with a new session and distinct ID.screenshotModeBackgroundCapture(feat: keep screenshotModeBackgroundCapture in 4.0 #1052): 26 to 27 frames in 30 s, about the same as onmain(3.94.4, 23 to 27), so the run-loop scheduler (feat(replay): schedule capture from a main run-loop observer instead of swizzling UIView layout #896) shows no regression. Text fields and labels are masked in the decoded frames.CALayercrash under "Before merging" was found in this run.2026-11-05: all runs at
91118b1(after #938 and #1027 merged). The two commits after it change only docs and the harness pin.CI: every check is green on
91118b1: build (iOS, macOS, tvOS, watchOS, visionOS, SPM, no-traits, minimum toolchain Xcode 26.0.1),test,test-no-traits, the threetest-ios-simulatorjobs, replay masking snapshots, public API snapshot, examples, downgrade compatibility (3.48.0, 3.48.3, 3.58.0), lint.SDK compliance harness: 234/234 on harness 1.26.0 in CI (all capture v1, AI and flags suites). Locally on harness 1.28.0, the latest release: also 234/234, so this PR bumps the pin to 1.28.0. 1.27.0 and 1.28.0 only touch feature flag rules v2 contracts and add a
feature_flags_v4suite that iOS doesn't opt into.End to end against PostHog Cloud (US), on an iPhone 14 Pro Max (iOS 26.5.2) and an iPhone 17 simulator (iOS 26.4). A scripted probe in the example app (behind a launch argument, not committed) drove the SDK; every result below was checked in the ingested data with SQL, not just in the app log.
Application Updatedhasprevious_version3.93. A 3.x event with$process_person_profile: falsearrived asfull, because 3.93.6 had already storedtruefor an identified user.capturewith properties,timestamp:(−1 h),options: ["process_person_profile": false], the legacy$process_person_profileproperty,screen,identify,alias,group,setPersonProperties,register/unregister, a 300 KB event, a 120-event burstpropertyless, nested number and bool keys arrive as strings, super properties apply and stop afterunregister.beforeSendreturningnil, a 250-byte distinct ID,capturewhile opted outdistinct_id_too_large: 1in the per-event results. AbeforeSendedit arrived.captureAi$ai_generationon/i/v1/ai/eventscaptureAireturned.$locale/$app_languagesetAppLanguage("fr-CA"),setAppLanguage(nil)$localeisen-US(3.x senten).$app_languageisen,fr-CAafter the override, andenafter the reset.isFeatureEnabled(_:defaultValue:),setPersonPropertiesForFlagswithreloadFeatureFlags { result in },onFeatureFlags$feature_flag_calledand$experiment_exposurearrived.Application Installed,Updated,OpenedandBackgroundedarrived, withoutversionandbuild.captureException(handled) and two Swift runtime traps, one of them afteroptOut()/optIn()error. Each crash arrived as afatal, unhandled$exceptionwith the Swift message on the next launch.captureLogsurvey shownarrived for the survey on device and simulator.ph-no-capturelabel, a text field, a secure field and an image; the uploads were captured through a local proxy and decodedmobile,posthog-ios). Text, both text fields and the masked label are masked; the SF Symbol stays visible, which is intended. Network and console plugins start from remote config.NWPathMonitor)Not run:
make testlocally (CI covers it on this head), the macOS, tvOS, watchOS and visionOS apps on devices (CI builds them; #938 ran macOS end to end), and touch autocapture ($autocapture) on device.Since that run: CI is green on
06d91a9e8(48 checks), which adds #1061 (docs), #1067 (registered personless setting off identity events),mainup to 3.96.2 (#1059, #1063, #1042, #1043) and the experimental GPU capture (#911, #966). The device runs weren't repeated on it.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset fileThe changeset is
majorand was written by hand. Its 3.x-only entries were released frommain, so they aren't repeated. The release workflow runs onmain, so merging this PR publishes 4.0.0.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Claude Code (Opus 5.5) wrote the stacked PRs and ran the release reviews and device tests above. The end-to-end probe and the decoded replay frames stay local and aren't committed.
🤖 Generated with Claude Code