Skip to content

chore!: iOS Major Release 4.0 - #873

Open
turnipdabeets wants to merge 17 commits into
mainfrom
chore/v4-remove-deprecated
Open

turnipdabeets wants to merge 17 commits into
mainfrom
chore/v4-remove-deprecated

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Closes #557

First PR stacked on release/4.0, the base branch for the 4.0 major. It removes every public API that is already marked deprecated.

Removed Replacement
PostHogConfig.apiKey, init(apiKey:), init(apiKey:host:) projectToken, init(projectToken:), init(projectToken:host:)
PostHogConfig.propertiesSanitizer, PostHogPropertiesSanitizer PostHogConfig.setBeforeSend(_:)
PostHogConfig.evaluationEnvironments evaluationContexts
PostHogConfig.remoteConfig none, it was a no-op
PostHogSDK.getFeatureFlagPayload(_:) getFeatureFlagResult(_:sendFeatureFlagEvent:)?.payload
PostHogSessionReplayConfig.debouncerDelay throttleDelay
PostHogSessionReplayConfig.maskPhotoLibraryImages none, it was a no-op

Public API change: this is a breaking removal, see the api/posthog-ios.public-api.txt diff.

Left in place on purpose:

  • The deprecated members in PostHog/Utils/Reachability.swift, which is internal vendored code.
  • PostHogConfig(apiKey:) in scripts/downgrade-compatibility-smoke, which compiles against 3.48.0.
  • The internal PostHogEvent.apiKey and PostHogRemoteConfig.getFeatureFlagPayload.
  • The storage migrations migrateLegacyStorage (added in 3.20) and mergeLegacyContainerIfNeeded (the app-group merge, added in 3.27). Both run on every init, but removing them would cost data. If someone updates an app straight from an older version to 4.x, their users lose their distinct ID and any queued events. In the last 7 days (US), about 8% of iOS SDK end-users were on an app build older than 3.27, and about 6% of apps had not shipped 3.27 or later. The app-group merge only affects apps that set appGroupIdentifier, so its share is smaller. We'll revisit in a later major.

Wrapper SDKs: posthog-flutter called getFeatureFlagPayload; PostHog/posthog-flutter#630 moves it off. The React Native plugin calls none of the removed APIs. Both pin posthog-ios below 4.0.

Wrapper SDK releases

posthog-ios 4.0 ships with a major release of each wrapper that embeds it. Each adoption PR waits until 4.0 is published.

Wrapper Release PR Adopts posthog-ios 4.0 Docs
posthog_flutter 6.0 PostHog/posthog-flutter#637 PostHog/posthog-flutter#649 PostHog/posthog.com#20924
@posthog/react-native-plugin 3.0 PostHog/posthog-js#5194 PostHog/posthog-js#5237, plus PostHog/posthog-js#5236 if #907 ships PostHog/posthog.com#20922

💚 How did you test it?

  • make test passes locally (909 tests, macOS).
  • The iOS SDK builds locally, via make apiUpdate.
  • Not run locally: make build for the other platforms and example apps, and the iOS simulator test job.
  • Tests that only covered the removed aliases are deleted.

📝 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

The changeset is major and was written by hand in the changeset format. The release workflow only runs on main, so nothing publishes until release/4.0 merges there.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Written with Claude Code (Opus 5.5).
  • Scope is limited to declarations already carrying @available(*, deprecated), plus the code, tests and comments that only existed to support them.
  • Docs on posthog.com that mention the removed options are not updated here.

🤖 Generated with Claude Code

@turnipdabeets turnipdabeets self-assigned this Sep 30, 2026

@greptile-apps greptile-apps 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.

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.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

posthog-ios Compliance Report

Date: 2026-10-09 16:05:23 UTC
Duration: 217665ms

⚠️ Some Tests Failed

44/45 tests passed, 1 failed


Capture Tests

⚠️ 28/29 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 3172ms
Format Validation.Event Has Uuid ✅ 2709ms
Format Validation.Event Has Lib Properties ✅ 2772ms
Format Validation.Distinct Id Is String ✅ 2684ms
Format Validation.Token Is Present ✅ 2708ms
Format Validation.Custom Properties Preserved ✅ 2760ms
Format Validation.Event Has Timestamp ✅ 2817ms
Retry Behavior.Retries On 503 ✅ 11818ms
Retry Behavior.Does Not Retry On 400 ✅ 4702ms
Retry Behavior.Does Not Retry On 401 ✅ 4888ms
Retry Behavior.Respects Retry After Header ✅ 7707ms
Retry Behavior.Implements Backoff ✅ 21698ms
Retry Behavior.Retries On 500 ✅ 9207ms
Retry Behavior.Retries On 502 ✅ 9106ms
Retry Behavior.Retries On 504 ✅ 9190ms
Retry Behavior.Max Retries Respected ❌ 15272ms
Deduplication.Generates Unique Uuids ✅ 3196ms
Deduplication.Preserves Uuid On Retry ✅ 5262ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10367ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7776ms
Deduplication.No Duplicate Events In Batch ✅ 2782ms
Deduplication.Different Events Have Different Uuids ✅ 2844ms
Compression.Sends Gzip When Enabled ✅ 2776ms
Batch Format.Uses Proper Batch Structure ✅ 2757ms
Batch Format.Flush With No Events Sends Nothing ✅ 339ms
Batch Format.Multiple Events Batched Together ✅ 2813ms
Error Handling.Does Not Retry On 403 ✅ 4716ms
Error Handling.Does Not Retry On 413 ✅ 4736ms
Error Handling.Retries On 408 ✅ 9296ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 5

Feature_Flags Tests

✅ 16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2768ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2666ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2776ms
Request Payload.Flags Request Omits Authorization Header ✅ 2723ms
Request Payload.Token In Flags Body Matches Init ✅ 2775ms
Request Payload.Groups Round Trip ✅ 2769ms
Request Payload.Groups Default To Empty Object ✅ 2701ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It ✅ 2691ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2706ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2726ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2806ms
Request Lifecycle.No Flags Request On Init Alone ✅ 58ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2779ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5490ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2742ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 2972ms

@greptile-apps

greptile-apps Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[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..."

@turnipdabeets

Copy link
Copy Markdown
Contributor Author

SwiftPM floor: options if Xcode 16 support is needed later

4.0 ships Package.swift at tools 6.2 (Xcode 26). App Store and TestFlight builds already need Xcode 26, so this only affects SwiftPM users who build outside App Store Connect (ad-hoc, enterprise, Mac) and are still on Xcode 16. CocoaPods builds of 4.0 already work on Xcode 16.0 and 16.4.

If needed, either of these can ship in a 4.x release without breaking anyone. Both were checked on Xcode 16 in CI:

  • 6.1 (Xcode 16.3+): change the tools version line. Traits keep working.
  • 6.0 (Xcode 16.0+): Package.swift at 6.0 without traits, plus Package@swift-6.1.swift with them. The 6.0 manifest must add swiftSettings: [.define("SessionReplay"), .define("CrashReporting"), .define("Surveys")]. Without these, the #if checks compile replay, crash reporting and surveys out.

Either option needs a CI job on the oldest supported Xcode. Raising the floor again before 5.0 would break those users.

turnipdabeets and others added 15 commits October 9, 2026 10:53
Removes PostHogConfig.apiKey and the apiKey initializers,
propertiesSanitizer and PostHogPropertiesSanitizer, evaluationEnvironments,
remoteConfig, PostHogSDK.getFeatureFlagPayload, and the session replay
debouncerDelay and maskPhotoLibraryImages options.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hangeset

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Raise the iOS floor from 13 to 15 (Mac Catalyst follows) and remove the
availability checks and fallbacks that only protected iOS 13/14.
macOS, tvOS, watchOS and visionOS minimums are unchanged, so checks that
also gate those platforms keep their non-iOS clauses.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves postHogSdkName, postHogVersion, PostHogSessionManager,
PostHogStorageManager, PostHogConfig.storageManager,
PostHogConfig.snapshotEndpoint, dateToMillis and imageToBase64 behind
@_spi(PostHogInternal), and makes unused public helpers internal.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PostHogDataMode.cellular has always behaved the same as .any. Remove it;
callers switch to .any with identical flushing behavior.

.wifi and .any keep explicit raw values 0 and 2 so the Objective-C
constants and any stored raw values stay the same as in 3.x.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Collapse Package.swift and Package@swift-5.9.swift into a single
swift-tools-version 6.2 manifest that keeps Swift 5 language mode, and
drop the compiler-version fallbacks that a Swift 6 floor makes dead.
CI's minimum-toolchain, Pods-example and CocoaPods publish jobs move to
Xcode 26.0.1.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore!: raise minimum macOS to 11, tvOS to 15 and watchOS to 8

Xcode 26, which the package now requires, supports deployment targets
down to macOS 11, tvOS 15 and watchOS 8, so the old 10.15 / 13 / 6
floors could no longer be built or tested. Raise them in Package.swift,
the podspec, PostHog.xcodeproj and the downgrade smoke package, and
remove the availability checks and fallbacks the new floors make dead.
iOS 15, Mac Catalyst and visionOS 1 are unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore!: raise minimum watchOS to 10

No posthog-ios events came from watchOS 9 or earlier in the last 30 days
on US or EU Cloud, and watchOS 10 supports the same hardware as watchOS 9
(Apple Watch Series 4 and later).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PostHogLogLevel and PostHogLogEntry belong to session replay console
capture, but their names read like the Logs product's
PostHogLogSeverity and PostHogLogRecord. Rename them to
PostHogConsoleLogLevel and PostHogConsoleLogEntry, matching the
existing PostHogSessionReplayConsoleLogConfig naming. No behavior change.

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore!: remove the v2 legacy queue migration for v4

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore!: stop reading the 2.x event format in queued events

Drop the top-level $set and message_id fallbacks in PostHogEvent.fromJSON.
They only served events copied unchanged from the 2.x queue, whose import
was removed in the previous commit.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: narrow the changeset to events created by 3.x

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore!: drop legacy version and build from lifecycle events

Application Installed, Application Updated and Application Opened no
longer set `version`/`build`; every event already carries `$app_version`
and `$app_build` from the static context. `previous_version` and
`previous_build` stay on Application Updated. Matches posthog-react-native
4.0 (#557).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: check the background-open event in the from_background test

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore!: pass the reload result to reloadFeatureFlags' callback

reloadFeatureFlags(_:) now hands its callback a PostHogFeatureFlagsLoaded,
the same type onFeatureFlags receives, so callers can tell a failed reload
from a successful one. errorsLoading is true when the request failed or the
SDK isn't set up; flags and variants then hold the last known values.

Part of #557.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: snapshot a failed reload's last known flags once for all callbacks

Coalesced reload callbacks each read the live cache on failure, so a
reset() in one callback emptied the last known flags reported to the
next. The load now captures its result once, before fan-out.

Also updates the setPersonPropertiesForFlags example and note for the
new callback argument.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: gate the first failed reload so the reset regression always coalesces

The coalesced-failure + reset() test assumed the first reload was still
in flight when the next two registered. Hold its response until both
callbacks are pending, and check both shared one request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore!: mark off-main public callbacks @sendable for v4 (#914)

* chore!: mark off-main public callbacks @sendable for v4

The reloadFeatureFlags callback, beforeSend blocks, getAnonymousId,
pushIdentityProvider and logSanitizer run off the main thread but weren't
@sendable. In Swift 6 apps a reloadFeatureFlags closure written in
main-actor code traps at runtime; the other callbacks race silently.
Also make PostHogFeatureFlagsLoaded Sendable and add a Swift 6
language-mode test target that calls the API as an app would.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore: match the public API snapshot to CI's Xcode 26.3 output

Xcode 26.3's symbol graph leaves @sendable out of declaration
fragments, which 26.4 prints; the mangled names still differ.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: store bridged flag values in PostHogFeatureFlagsLoaded

The init kept the original variant values after filtering, so a mutable
NSString passed via bootstrap stayed mutable inside a Sendable payload.
Store the `as? Bool`/`as? String` results instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: clarify the Sendable callbacks changeset

Name the config properties unambiguously and warn that array
conversions crash in both directions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore!: return nil for malformed feature flag payloads

Follow the sdk-specs payload decoding contract: a serialized payload that
fails to decode (malformed, empty or whitespace-only) now reads as nil and is
logged, instead of leaking the raw string. The flag's value, enabled state and
sibling payloads are unaffected.

Bootstrapped payloads are already decoded, so they are now serialized when
seeded into the payload cache. Reads decode each payload exactly once, so a
bootstrapped "123" or "true" string stays a String instead of becoming a
number or Bool.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: cover bootstrapped string payloads read back after a restart

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: clear leftover queue files before each ignoredExceptionTypes capture test

storage.reset() keeps the event queue, and getBatchedEvents returns once the
mock receives a batch, before the SDK deletes the sent file. The previous
test's $exception could survive on disk and be resent by the next SDK,
failing the "drops ignored types" assertion on the iOS simulator job.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore: replace vendored Reachability with NWPathMonitor

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: match the old reachability notifications

Notify on every path update and report the current connection when the
notifier starts, as the SCNetworkReachability version did.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: cap the first-path wait and test the reachability notifier

Wait for the first path at most 100 ms after init in total, not 100 ms
per read. Add tests for the start/update/stop notification rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: report reachability from the connection at delivery

A start notification queued before a newer path could arrive last and
pause the queues while online. Also narrow the changeset to what is
actually preserved and list the edge-state changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: cover unknown and known network state in the context test

Seed the context's reachability instead of relying on a live NWPathMonitor
delivering its first path within 100 ms, and assert that network properties
are omitted until a path arrives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: drive queue reachability tests with update(_:) and cover data modes

The logs and replay queue reachability tests used a live NWPathMonitor, so a
real path update could race the simulated offline event. Drive them through
update(_:) instead, and add tests for .wifi waiting for a Wi-Fi path and .any
sending before the first path. Mark the changeset major like the rest of v4.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat: add SessionReplayWebP and CrashReporting package traits

SwiftPM apps can now leave out the vendored libwebp and PLCrashReporter
by disabling the traits. Both are on by default, so existing setups are
unchanged; CocoaPods and xcodeproj builds always include both.

Also moves the ignoredExceptionTypes matcher out of the crash-reporting
gate so it applies to manual captures everywhere, which fixes it on
watchOS and visionOS.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: gate WebP suites on the SessionReplayWebP trait

The WebP test suites called `UIImage.webpData`, which SwiftPM compiles out on
iOS when the trait is off, so the test target failed to build. Gate them like
the encoder and add a separate encoding test that expects WebP or the JPEG
fallback depending on the trait.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: compile the iOS test target with package traits off

`make test` runs on macOS, where the iOS-only trait gates compile out, so a
test calling trait-gated SDK code on iOS went unnoticed. Build the iOS test
target for the simulator with traits off and with each trait on its own.

The concurrent flag reload suite's `.timeLimit` needs iOS 16, so limit it to
macOS SwiftPM builds rather than every SwiftPM build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat!: replace the SessionReplayWebP trait with SessionReplay

SessionReplay now gates the whole replay engine along with libwebp, so an
app that doesn't use replay can leave all of it out. The public replay API
(config, masking modifiers, URLSession helpers, start/stop recording) stays
compiled; with the trait off the replay integration is a stub that is
skipped with `disabledByPackageTrait`.

A finer split (replay without WebP) can come later in a minor release by
keeping SessionReplay as an umbrella trait that enables the new ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: gate the remaining replay tests on the SessionReplay trait

The session replay suite and the two SDK tests that assert replay debug
properties need the replay engine, so skip them in SPM builds with the
SessionReplay trait off. Also list buildTestsSpmTraitsIOS for trait-gated
changes in the contributor guide.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test: limit the Swift 6 callback suite's timeLimit to macOS

`.timeLimit` needs iOS 16, above the iOS 15 minimum, so the iOS SwiftPM
test build failed after merging the 4.0 base.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat: add Surveys package trait (#918)

* feat: add Surveys package trait

SwiftPM apps that don't use surveys can now leave out the survey engine and
UI by disabling the `Surveys` trait. It is on by default, so existing setups
are unchanged; CocoaPods and xcodeproj builds always include surveys.

The public survey API (`config.surveys`, `surveysConfig`, the delegate
protocol and display models) stays compiled. With the trait off, the survey
integration is a stub that is skipped with `disabledByPackageTrait`.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: note the Surveys trait on surveysConfig and surveysDelegate

A custom surveysDelegate is never called when the trait is off, so say so
where apps set it. Also drop the unused clearInstalls() from the stub.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets force-pushed the chore/v4-remove-deprecated branch 2 times, most recently from 3e050a7 to 7bb79ca Compare October 9, 2026 15:50

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Not approved yet — waiting on the conditions below.

Re-add the stamphog label to request another review once you have addressed this.

Gates denied this change because it modifies toolchain and CI infrastructure and exceeds the automated-review size limit; no current-head review or ownership/familiarity assurance is reported. Route the release to human maintainers, disclose its broader scope, and address the persisted-data loss before re-requesting.

  • 👍 on the PR from greptile-apps[bot].
  • PostHog/PostHogFileBackedQueue.swift: setup deletes legacy queue files rather than importing their events, losing queued analytics on direct upgrades from version 2. PostHog/Models/PostHogEvent.swift also drops legacy person-property updates from already-imported events. Restore preservation of these persisted records or obtain explicit human review of the data-loss behavior.
  • Trusted gate denial: dependency/toolchain and CI infrastructure changes are excluded from automated approval, and the cross-cutting change exceeds the automated-review size ceiling. Route this release to human maintainers.
  • Undisclosed risky behavior: the description limits the scope to deprecated declarations and their supporting code, but Package.swift adds optional feature traits, workflows change build and release execution, and PostHog/PostHogFileBackedQueue.swift deletes legacy queued data. Update the description to disclose the full scope and request human review; changeset documentation does not make the stated scope accurate.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✗ matches: deps_toolchain, infra_cicd
size ✗ too large for auto-review (10396L substantive in global — ceiling is 800L; 10396L, 156F total, 10628L/176F incl. docs/generated/snapshots)
tier ✗ classified as T2-never: T2-never (10628L, 176F, cross-cutting, chore)
stamphog 2.4.1 .stamphog/policy.yml @ 7bb79ca · reviewed head 7bb79ca

* chore!: remove wireframe session replay capture

Session replay on iOS now always captures masked screenshots. Wireframe
mode skipped SwiftUI-rooted windows entirely and leaked ph-no-capture
containers (sdk-specs n26). `screenshotMode` is kept as a deprecated
no-op so wrappers that set it keep compiling. The replay meta href is
now resolved from the visible screen on every capture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: keep secure text fields masked under ph-no-mask

Wireframe masked secure fields regardless of ph-no-mask; screenshot capture
stopped the walk at the unmasked ancestor. Now that screenshots are the only
mode, keep secure-entry inputs masked in that subtree, matching Android's
password handling and the session-replay-privacy spec.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix: mask full-window no-capture leaves and secure fields under a no-mask cover ancestor

A full-window view tagged ph-no-capture with no subviews now contributes its
own mask instead of relying on descendants it doesn't have. When a ph-no-mask
ancestor sits above a selected presentation cover, secure-entry fields inside
the cover are still masked.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(replay): keep view controller titles out of the replay href

A plain UIViewController's title is display text and can contain user
data, so the replay meta href now uses the controller's type name only.
$screen naming still falls back to the title.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
if !viewRect.equalTo(windowRect) {
// Check if the rectangles do not match. A full-window leaf has no descendants
// to carry the mask, so it is masked itself.
if !viewRect.equalTo(windowRect) || view.subviews.isEmpty {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Low: Full-window no-capture containers expose their own drawing

Consider a full-window custom UIView tagged ph-no-capture that draws a secret at the top and contains a small child at the bottom: this branch collects only the child's mask, leaving the secret readable in the uploaded screenshot. For apps previously using default wireframes, replay viewers can now read that custom-drawn content despite the explicit privacy marker. (raw.githubusercontent.com)

Redact the tagged view's own rendered region even when it has subviews, rather than assuming descendant masks cover its content; preserve intentional unmask overrides separately.

@veria-ai

veria-ai Bot commented Oct 9, 2026

Copy link
Copy Markdown

PR overview

This PR introduces the iOS 4.0 major release, including changes to session replay screenshot masking for view hierarchies.

One privacy issue remains open: certain full-window custom views marked as no-capture can still expose their own drawn content in uploaded replay screenshots. This can make sensitive content readable to replay viewers despite the app’s explicit privacy marker, though exposure depends on the affected view layout. No issues have been fixed or addressed yet.

Open issues (1)

Fixed/addressed: 0 · PR risk: 5/10

Screenshots are always captured on the main thread with the settle-then-shoot
path. Removes the opt-in off-main render path, its sweptRects mask helper and
the tests that only covered it.

Co-authored-by: Claude Opus 5.5 (1M context) <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.

Next iOS major version plan

2 participants