Skip to content

chore!: iOS Major Release 4.0 - #873

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

turnipdabeets wants to merge 18 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

The 4.0 major release branch. Its first change removes every public API that is already marked deprecated; the other 4.0 changes are listed under "Also on this branch".

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.

Also on this branch

This branch is the 4.0 integration branch. Each item below was reviewed and merged as its own PR, and each has its own changeset.

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-23T17:28:47.945070+00:00
Duration: 228326ms

⚠️ Some Tests Failed

46/47 tests passed, 1 failed


Capture Tests

⚠️ 29/30 tests passed, 1 failed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 2904ms
Format Validation.Event Has Uuid ✅ 328ms
Format Validation.Event Has Lib Properties ✅ 2674ms
Format Validation.Distinct Id Is String ✅ 2704ms
Format Validation.Token Is Present ✅ 2737ms
Format Validation.Custom Properties Preserved ✅ 313ms
Format Validation.Event Has Timestamp ✅ 2750ms
Format Validation.Non Utc Event Timestamp Is Converted To Utc ✅ 2702ms
Retry Behavior.Retries On 503 ✅ 11306ms
Retry Behavior.Does Not Retry On 400 ✅ 4668ms
Retry Behavior.Does Not Retry On 401 ✅ 4829ms
Retry Behavior.Respects Retry After Header ✅ 5301ms
Retry Behavior.Implements Backoff ✅ 21746ms
Retry Behavior.Retries On 500 ✅ 9355ms
Retry Behavior.Retries On 502 ✅ 5405ms
Retry Behavior.Retries On 504 ✅ 9369ms
Retry Behavior.Max Retries Respected ❌ 21235ms
Deduplication.Generates Unique Uuids ✅ 2782ms
Deduplication.Preserves Uuid On Retry ✅ 8732ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 16811ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7700ms
Deduplication.No Duplicate Events In Batch ✅ 2857ms
Deduplication.Different Events Have Different Uuids ✅ 2817ms
Compression.Sends Gzip When Enabled ✅ 2752ms
Batch Format.Uses Proper Batch Structure ✅ 2805ms
Batch Format.Flush With No Events Sends Nothing ✅ 290ms
Batch Format.Multiple Events Batched Together ✅ 2921ms
Error Handling.Does Not Retry On 403 ✅ 4786ms
Error Handling.Does Not Retry On 413 ✅ 4710ms
Error Handling.Retries On 408 ✅ 8802ms

Failures

retry_behavior.max_retries_respected

Expected 4 requests, got 6

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 2787ms
Request Payload.Flags Request Uses V2 Query Param ✅ 2824ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 2756ms
Request Payload.Flags Request Omits Authorization Header ✅ 2749ms
Request Payload.Token In Flags Body Matches Init ✅ 2855ms
Request Payload.Groups Round Trip ✅ 2865ms
Request Payload.Groups Default To Empty Object ✅ 2789ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 2681ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 2810ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 2767ms
Request Lifecycle.No Flags Request On Init Alone ✅ 65ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 2799ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 5501ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 2734ms
Retry Behavior.Retries Flags On 502 ✅ 2708ms
Retry Behavior.Retries Flags On 504 ✅ 2822ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 2850ms

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

Comment thread PostHog/Replay/PostHogReplayIntegration.swift Outdated
@turnipdabeets
turnipdabeets force-pushed the chore/v4-remove-deprecated branch from 0901a7b to 2ed8f7f Compare October 20, 2026 19:54

@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 2ed8f7f: no findings. This pull request is large, so the review ran in 4 parts with less depth than usual.

Comment thread PostHog/Replay/PostHogReplayIntegration.swift

@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 0274681: 1 Must fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

This is review round 6 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.

.flatMap(UIViewController.getViewControllerTypeName)
.flatMap { PostHogScreenNameSanitizer.sanitize(rawScreenName: $0) }

scheduleSettledCapture(window: window, screenName: screenName, postHog: postHog)

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.

P1 · Mask the full rendered extent of unclipped images

For a UIImageView with .scaleAspectFill and clipsToBounds = false, mandatory screenshots upload image pixels outside its bounds despite maskAllImages = true. MaskedRegion covers only the view bounds; wireframes omitted the sensitive image entirely. Preserve the privacy invariant in AGENTS.md:12 by expanding masks to the rendered image extent.

@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 dcd51a5: 2 Must fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

This is review round 7 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.

.flatMap(UIViewController.getViewControllerTypeName)
.flatMap { PostHogScreenNameSanitizer.sanitize(rawScreenName: $0) }

scheduleSettledCapture(window: window, screenName: screenName, postHog: postHog)

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.

P1 · Mask attributed image attachments before switching to screenshots

With maskAllTextInputs = false and maskAllImages = true, a UILabel containing an image NSTextAttachment now uploads it unmasked: isLabelSensitive ignores image masking, and attachments have no UIImageView to visit. Wireframes sent only the label's text. Preserve AGENTS.md:12 by masking attributed image attachments before capture.

.flatMap(UIViewController.getViewControllerTypeName)
.flatMap { PostHogScreenNameSanitizer.sanitize(rawScreenName: $0) }

scheduleSettledCapture(window: window, screenName: screenName, postHog: postHog)

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.

P1 · Include text shadows in masks before requiring screenshots

With clipsToBounds = false and an offset CALayer text shadow, a UILabel draws readable glyphs outside its bounds. MaskedRegion covers only those bounds, so mandatory screenshots upload the shadow despite maskAllTextInputs = true; wireframes serialized masked text instead. Expand text masks to cover the rendered shadow extent before making screenshot capture unconditional.

@turnipdabeets
turnipdabeets force-pushed the chore/v4-remove-deprecated branch from dcd51a5 to 0274681 Compare October 23, 2026 16:59
turnipdabeets and others added 18 commits October 23, 2026 13:08
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
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) <[email protected]>
Collapse Package.swift and [email protected] 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) <[email protected]>
* 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) <[email protected]>

* 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) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
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) <[email protected]>
* chore!: remove the v2 legacy queue migration for v4

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* 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) <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* 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) <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* 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) <[email protected]>

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

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* 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) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* chore: replace vendored Reachability with NWPathMonitor

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

* 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) <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
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) <[email protected]>
NetworkSample is compiled out when the SessionReplay package trait is off.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@turnipdabeets
turnipdabeets force-pushed the chore/v4-remove-deprecated branch from 0274681 to ce9e3ac Compare October 23, 2026 17:13

@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 ce9e3ac: 1 Must fix. This pull request is large, so the review ran in 4 parts with less depth than usual.

This is review round 8 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.

.flatMap(UIViewController.getViewControllerTypeName)
.flatMap { PostHogScreenNameSanitizer.sanitize(rawScreenName: $0) }

scheduleSettledCapture(window: window, screenName: screenName, postHog: postHog)

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.

P1 · Traverse masked controls' children before requiring screenshots

When an unclipped UIButton positions its title outside its bounds with titleEdgeInsets, mandatory screenshots upload that text despite maskAllTextInputs = true. findMaskableWidgets masks only the button and returns before visiting its title label; the removed wireframe path recursively masked that label. Continue traversing masked controls' descendants.

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

3 participants