From 5137250a901c42290f751b86a44b5bb7e5451a91 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:30:30 +0000 Subject: [PATCH 1/4] fix(replay): stop the layoutSublayers swizzle running UIKit layout off-main The swizzled `UIView.layoutSublayers(of:)` forwarded to UIKit before any thread check. Core Animation can call it on a background thread when it commits a thread-local transaction during thread cleanup, so the forwarded call reached Auto Layout off-main and raised `NSInternalInconsistencyException`. Off the main thread the hook now marks the layer for layout on the main queue and returns, so both the UIKit layout pass and the replay notification run on the main thread. Adds `sessionReplayConfig.captureViewLayoutChanges` so an app can remove the hook and keep the rest of session replay. Generated-By: PostHog Desktop Task-Id: 296e6380-9948-4bfe-9ab0-73486280ebf0 --- .changeset/replay-layout-off-main.md | 5 +++ PostHog/ApplicationViewLayoutPublisher.swift | 22 ++++++------ PostHog/Replay/PostHogReplayIntegration.swift | 2 +- .../Replay/PostHogSessionReplayConfig.swift | 13 +++++++ .../ApplicationViewLayoutPublisherTest.swift | 35 +++++++++++++++++++ 5 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 .changeset/replay-layout-off-main.md diff --git a/.changeset/replay-layout-off-main.md b/.changeset/replay-layout-off-main.md new file mode 100644 index 0000000000..904b7ea512 --- /dev/null +++ b/.changeset/replay-layout-off-main.md @@ -0,0 +1,5 @@ +--- +"posthog-ios": patch +--- + +Stop session replay's `UIView.layoutSublayers(of:)` hook from running UIKit layout on a background thread, which crashed the host app in the Auto Layout engine. Add `sessionReplayConfig.captureViewLayoutChanges` to remove the hook while keeping the rest of session replay. diff --git a/PostHog/ApplicationViewLayoutPublisher.swift b/PostHog/ApplicationViewLayoutPublisher.swift index 9a37052b1e..faee5fe80a 100644 --- a/PostHog/ApplicationViewLayoutPublisher.swift +++ b/PostHog/ApplicationViewLayoutPublisher.swift @@ -64,17 +64,19 @@ extension UIView { @objc func ph_swizzled_layoutSublayers(of layer: CALayer) { - ph_swizzled_layoutSublayers(of: layer) // call original, not altering execution logic - // Only notify on main thread - layoutSublayers can be called on background threads - // during thread cleanup (CA::Transaction::release_thread), which can cause crashes - // in the Auto Layout engine (NSISEngine) since it's not thread-safe. - if Thread.isMainThread { - ApplicationViewLayoutPublisher.shared.layoutSubviews() - } else { - DispatchQueue.main.async { - ApplicationViewLayoutPublisher.shared.layoutSubviews() - } + // Core Animation can call `layoutSublayers(of:)` on a background thread when it commits a + // thread-local transaction during thread cleanup (`CA::Transaction::release_thread`). + // UIKit's implementation is main-thread only: it runs Auto Layout, and `NSISEngine` raises + // `NSInternalInconsistencyException` off the main thread, which terminates the host app. + // Do not forward the call here. Mark the layer instead, so the layout pass and the + // notification both run on the main thread on the next Core Animation commit. + guard Thread.isMainThread else { + DispatchQueue.main.async { layer.setNeedsLayout() } + return } + + ph_swizzled_layoutSublayers(of: layer) // call original, not altering execution logic + ApplicationViewLayoutPublisher.shared.layoutSubviews() } } #endif diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index d23fa0ea66..3dfad685d7 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -270,7 +270,7 @@ } // flutter captures snapshots, so we don't need to capture them here - if isNotFlutter() { + if isNotFlutter(), postHog.config.sessionReplayConfig.captureViewLayoutChanges { let interval = postHog.config.sessionReplayConfig.throttleDelay viewLayoutToken = DI.main.viewLayoutPublisher.onViewLayout.subscribe(throttle: interval, trailing: true) { [weak self] in // called on main thread diff --git a/PostHog/Replay/PostHogSessionReplayConfig.swift b/PostHog/Replay/PostHogSessionReplayConfig.swift index 2c1b816b7a..e7cc5bc4c3 100644 --- a/PostHog/Replay/PostHogSessionReplayConfig.swift +++ b/PostHog/Replay/PostHogSessionReplayConfig.swift @@ -55,6 +55,19 @@ /// Default: false @objc public var screenshotModeBackgroundCapture: Bool = false + /// Capture screen content from `UIView` layout passes. + /// + /// Session replay hooks `UIView.layoutSublayers(of:)` to learn when the screen changed, and + /// takes a wireframe or a screenshot from that hook. Set this to `false` to remove the hook. + /// Console logs, network telemetry and interaction events keep working, but the SDK no longer + /// captures screen content, so recordings have no visuals. + /// + /// Use this only if the hook causes a problem in your app. Session replay is much less useful + /// without it. + /// + /// Default: true + @objc public var captureViewLayoutChanges: Bool = true + /// Debouncer delay used to reduce the number of snapshots captured and reduce performance impact /// This is used for capturing the view as a wireframe or screenshot /// The lower the number more snapshots will be captured but higher the performance impact diff --git a/PostHogTests/ApplicationViewLayoutPublisherTest.swift b/PostHogTests/ApplicationViewLayoutPublisherTest.swift index 3a64cc4aef..19c7d20947 100644 --- a/PostHogTests/ApplicationViewLayoutPublisherTest.swift +++ b/PostHogTests/ApplicationViewLayoutPublisherTest.swift @@ -9,6 +9,7 @@ import Foundation @testable import PostHog import Testing + import UIKit @Suite("Application View Publisher Test", .serialized, .resetsGlobalState) final class ApplicationViewLayoutPublisherTest { @@ -69,5 +70,39 @@ registrationToken = nil } + + @MainActor + @Test("does not run UIKit layout off the main thread") + func layoutStaysOnMainThread() async throws { + let sut = ApplicationViewLayoutPublisher.shared + // Subscribing installs the swizzle on `UIView.layoutSublayers(of:)`. + registrationToken = sut.onViewLayout.subscribe(throttle: 0) {} + + let view = ThreadRecordingView() + let layer = view.layer + + await withCheckedContinuation { continuation in + let thread = Thread { + view.layoutSublayers(of: layer) + continuation.resume() + } + thread.start() + } + + #expect(view.laidOutOffMainThread == false, "UIKit layout must never run on a background thread") + + registrationToken = nil + } + } + + private final class ThreadRecordingView: UIView { + private(set) var laidOutOffMainThread = false + + override func layoutSubviews() { + if !Thread.isMainThread { + laidOutOffMainThread = true + } + super.layoutSubviews() + } } #endif From 612c2fbbf880cc840b62b6848b62994a94eae10e Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:46:41 +0000 Subject: [PATCH 2/4] chore: update public API snapshot for captureViewLayoutChanges Generated-By: PostHog Desktop Task-Id: 296e6380-9948-4bfe-9ab0-73486280ebf0 --- api/posthog-ios.public-api.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/api/posthog-ios.public-api.txt b/api/posthog-ios.public-api.txt index 19e4d45a3c..513c498bbe 100644 --- a/api/posthog-ios.public-api.txt +++ b/api/posthog-ios.public-api.txt @@ -356,6 +356,7 @@ PostHog | PostHogSessionReplayConfig | class | @objc(PostHogSessionReplayConfig) PostHog | PostHogSessionReplayConfig.captureLogs | property | @objc var captureLogs: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureLogs PostHog | PostHogSessionReplayConfig.captureLogsConfig | property | @objc var captureLogsConfig: PostHogSessionReplayConsoleLogConfig | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureLogsConfig PostHog | PostHogSessionReplayConfig.captureNetworkTelemetry | property | @objc var captureNetworkTelemetry: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureNetworkTelemetry +PostHog | PostHogSessionReplayConfig.captureViewLayoutChanges | property | @objc var captureViewLayoutChanges: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureViewLayoutChanges PostHog | PostHogSessionReplayConfig.debouncerDelay | property | @objc var debouncerDelay: TimeInterval { get set } | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)debouncerDelay PostHog | PostHogSessionReplayConfig.maskAllImages | property | @objc var maskAllImages: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)maskAllImages PostHog | PostHogSessionReplayConfig.maskAllSandboxedViews | property | @objc var maskAllSandboxedViews: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)maskAllSandboxedViews From dd78003abd6131d82d08765c4cbbdfd1d269727c Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:50:22 +0000 Subject: [PATCH 3/4] docs(replay): scope captureViewLayoutChanges to replay's own subscription The layout hook is installed by subscriber count, not by a config flag, and surveys subscribe to the same publisher on iOS by default. Setting captureViewLayoutChanges to false therefore stops replay screen capture but does not necessarily uninstall the swizzle. Correct the property documentation and the changeset to say so, and point at config.surveys for removing the hook entirely. Generated-By: PostHog Desktop Task-Id: 16d7b477-619e-4c1b-af76-2e6ea818086b --- .changeset/replay-layout-off-main.md | 2 +- PostHog/Replay/PostHogSessionReplayConfig.swift | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.changeset/replay-layout-off-main.md b/.changeset/replay-layout-off-main.md index 904b7ea512..0d2c5649a6 100644 --- a/.changeset/replay-layout-off-main.md +++ b/.changeset/replay-layout-off-main.md @@ -2,4 +2,4 @@ "posthog-ios": patch --- -Stop session replay's `UIView.layoutSublayers(of:)` hook from running UIKit layout on a background thread, which crashed the host app in the Auto Layout engine. Add `sessionReplayConfig.captureViewLayoutChanges` to remove the hook while keeping the rest of session replay. +Stop session replay's `UIView.layoutSublayers(of:)` hook from running UIKit layout on a background thread, which crashed the host app in the Auto Layout engine. Add `sessionReplayConfig.captureViewLayoutChanges` to stop session replay using the hook while keeping the rest of session replay. Surveys subscribe to the same hook and are enabled by default, so set `surveys` to `false` as well to remove it entirely. diff --git a/PostHog/Replay/PostHogSessionReplayConfig.swift b/PostHog/Replay/PostHogSessionReplayConfig.swift index e7cc5bc4c3..12743267b4 100644 --- a/PostHog/Replay/PostHogSessionReplayConfig.swift +++ b/PostHog/Replay/PostHogSessionReplayConfig.swift @@ -58,9 +58,13 @@ /// Capture screen content from `UIView` layout passes. /// /// Session replay hooks `UIView.layoutSublayers(of:)` to learn when the screen changed, and - /// takes a wireframe or a screenshot from that hook. Set this to `false` to remove the hook. - /// Console logs, network telemetry and interaction events keep working, but the SDK no longer - /// captures screen content, so recordings have no visuals. + /// takes a wireframe or a screenshot from that hook. Set this to `false` to stop session replay + /// subscribing to it. Console logs, network telemetry and interaction events keep working, but + /// the SDK no longer captures screen content, so recordings have no visuals. + /// + /// Note: the hook is shared with surveys, which are enabled by default, and it is only removed + /// once nothing is subscribed to it. To take the hook out of your app entirely, also set + /// `PostHogConfig.surveys` to `false`. /// /// Use this only if the hook causes a problem in your app. Session replay is much less useful /// without it. From 5832c02fd55d9df656d8f3e81a5c3adb3b2bc0a7 Mon Sep 17 00:00:00 2001 From: "posthog[bot]" <206114724+posthog[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:51:19 +0000 Subject: [PATCH 4/4] fix(replay): make the layout hook opt-out actually remove the hook The flag lived on `sessionReplayConfig` and gated only the replay subscription. Surveys subscribe to the same publisher and are on by default, so the swizzle stayed installed and the escape hatch did not do what it promised. Moves the flag to `PostHogConfig`, where session replay and surveys both read it, and documents what each product loses. Adds a test that covers both settings through an injected publisher. This supersedes the documentation-only narrowing in dd78003, which described the old behaviour rather than changing it. Generated-By: PostHog Desktop Task-Id: 296e6380-9948-4bfe-9ab0-73486280ebf0 --- .changeset/replay-layout-off-main.md | 2 +- PostHog/PostHogConfig.swift | 18 ++++++++ PostHog/Replay/PostHogReplayIntegration.swift | 2 +- .../Replay/PostHogSessionReplayConfig.swift | 17 ------- .../Surveys/PostHogSurveyIntegration.swift | 6 ++- .../ApplicationViewLayoutPublisherTest.swift | 45 +++++++++++++++++++ api/posthog-ios.public-api.txt | 2 +- 7 files changed, 70 insertions(+), 22 deletions(-) diff --git a/.changeset/replay-layout-off-main.md b/.changeset/replay-layout-off-main.md index 0d2c5649a6..694e2dfc8b 100644 --- a/.changeset/replay-layout-off-main.md +++ b/.changeset/replay-layout-off-main.md @@ -2,4 +2,4 @@ "posthog-ios": patch --- -Stop session replay's `UIView.layoutSublayers(of:)` hook from running UIKit layout on a background thread, which crashed the host app in the Auto Layout engine. Add `sessionReplayConfig.captureViewLayoutChanges` to stop session replay using the hook while keeping the rest of session replay. Surveys subscribe to the same hook and are enabled by default, so set `surveys` to `false` as well to remove it entirely. +Stop session replay's `UIView.layoutSublayers(of:)` hook from running UIKit layout on a background thread, which crashed the host app in the Auto Layout engine. Add `captureViewLayoutChanges` to `PostHogConfig` to remove the hook, which session replay and surveys both honour. diff --git a/PostHog/PostHogConfig.swift b/PostHog/PostHogConfig.swift index 1d39e45bc6..8ffa8e1f02 100644 --- a/PostHog/PostHogConfig.swift +++ b/PostHog/PostHogConfig.swift @@ -167,6 +167,24 @@ public typealias BeforeSendBlock = (PostHogEvent) -> PostHogEvent? /// Default: true @objc public var enableSwizzling: Bool = true + /// Watch `UIView` layout passes to detect screen changes. + /// + /// The SDK hooks `UIView.layoutSublayers(of:)` to learn when the screen changed. Session replay + /// takes a wireframe or a screenshot from that hook, and surveys use it to find the moment to + /// show a survey. Set this to `false` to remove the hook. + /// + /// When disabled, session replay records console logs, network telemetry and interaction events + /// but captures no screen content, so recordings have no visuals. Surveys appear only when the + /// app becomes active or when an event triggers them. + /// + /// Use this only if the hook causes a problem in your app. Both products are much less useful + /// without it. + /// + /// Note: iOS only. Requires `enableSwizzling` to be `true`. + /// + /// Default: true + @objc public var captureViewLayoutChanges: Bool = true + #if os(iOS) || os(macOS) /// Automatically register the device's APNs token with PostHog by swizzling /// `application(_:didRegisterForRemoteNotificationsWithDeviceToken:)`, so Workflows can diff --git a/PostHog/Replay/PostHogReplayIntegration.swift b/PostHog/Replay/PostHogReplayIntegration.swift index 3dfad685d7..277481125b 100644 --- a/PostHog/Replay/PostHogReplayIntegration.swift +++ b/PostHog/Replay/PostHogReplayIntegration.swift @@ -270,7 +270,7 @@ } // flutter captures snapshots, so we don't need to capture them here - if isNotFlutter(), postHog.config.sessionReplayConfig.captureViewLayoutChanges { + if isNotFlutter(), postHog.config.captureViewLayoutChanges { let interval = postHog.config.sessionReplayConfig.throttleDelay viewLayoutToken = DI.main.viewLayoutPublisher.onViewLayout.subscribe(throttle: interval, trailing: true) { [weak self] in // called on main thread diff --git a/PostHog/Replay/PostHogSessionReplayConfig.swift b/PostHog/Replay/PostHogSessionReplayConfig.swift index 12743267b4..2c1b816b7a 100644 --- a/PostHog/Replay/PostHogSessionReplayConfig.swift +++ b/PostHog/Replay/PostHogSessionReplayConfig.swift @@ -55,23 +55,6 @@ /// Default: false @objc public var screenshotModeBackgroundCapture: Bool = false - /// Capture screen content from `UIView` layout passes. - /// - /// Session replay hooks `UIView.layoutSublayers(of:)` to learn when the screen changed, and - /// takes a wireframe or a screenshot from that hook. Set this to `false` to stop session replay - /// subscribing to it. Console logs, network telemetry and interaction events keep working, but - /// the SDK no longer captures screen content, so recordings have no visuals. - /// - /// Note: the hook is shared with surveys, which are enabled by default, and it is only removed - /// once nothing is subscribed to it. To take the hook out of your app entirely, also set - /// `PostHogConfig.surveys` to `false`. - /// - /// Use this only if the hook causes a problem in your app. Session replay is much less useful - /// without it. - /// - /// Default: true - @objc public var captureViewLayoutChanges: Bool = true - /// Debouncer delay used to reduce the number of snapshots captured and reduce performance impact /// This is used for capturing the view as a wireframe or screenshot /// The lower the number more snapshots will be captured but higher the performance impact diff --git a/PostHog/Surveys/PostHogSurveyIntegration.swift b/PostHog/Surveys/PostHogSurveyIntegration.swift index 30fe9f62b1..ddd818a365 100644 --- a/PostHog/Surveys/PostHogSurveyIntegration.swift +++ b/PostHog/Surveys/PostHogSurveyIntegration.swift @@ -98,8 +98,10 @@ self?.onEvent(event: event) } // TODO: listen to screen view events - didLayoutViewToken = DI.main.viewLayoutPublisher.onViewLayout.subscribe(throttle: 5) { [weak self] in - self?.showNextSurvey() + if postHog?.config.captureViewLayoutChanges ?? true { + didLayoutViewToken = DI.main.viewLayoutPublisher.onViewLayout.subscribe(throttle: 5) { [weak self] in + self?.showNextSurvey() + } } didBecomeActiveToken = DI.main.appLifecyclePublisher.onDidBecomeActive.subscribe { [weak self] in self?.showNextSurvey() diff --git a/PostHogTests/ApplicationViewLayoutPublisherTest.swift b/PostHogTests/ApplicationViewLayoutPublisherTest.swift index 19c7d20947..9c40b14439 100644 --- a/PostHogTests/ApplicationViewLayoutPublisherTest.swift +++ b/PostHogTests/ApplicationViewLayoutPublisherTest.swift @@ -95,6 +95,51 @@ } } + @Suite("View layout capture opt-out", .serialized, .resetsGlobalState) + struct ViewLayoutCaptureOptOutTest { + // Surveys are the second subscriber of the shared publisher, and the publisher installs the + // swizzle for any subscriber. The opt-out only removes the hook if surveys honour it too. + @Test("surveys skip the layout publisher when captureViewLayoutChanges is false", arguments: [true, false]) + func surveysHonourOptOut(captureViewLayoutChanges: Bool) throws { + let server = MockPostHogServer() + server.start() + let mockPublisher = MockViewLayoutPublisher() + DI.main.viewLayoutPublisher = mockPublisher + defer { + DI.main.viewLayoutPublisher = ApplicationViewLayoutPublisher.shared + server.stop() + } + + let config = PostHogConfig(projectToken: testProjectToken, host: "http://localhost:9090") + config._surveys = true + config.captureViewLayoutChanges = captureViewLayoutChanges + config.disableReachabilityForTesting = true + config.disableQueueTimerForTesting = true + config.captureApplicationLifecycleEvents = false + PostHogStorage(config).reset() + + let postHog = PostHogSDK.with(config) + PostHogSurveyIntegration.clearInstalls() + let integration = PostHogSurveyIntegration() + try #require(integration.install(postHog) == .installed) + defer { + integration.uninstall(postHog) + postHog.close() + postHog.reset() + } + + if captureViewLayoutChanges { + #expect(mockPublisher.onViewLayout.subscriberCount > 0) + } else { + #expect(mockPublisher.onViewLayout.subscriberCount == 0) + } + } + } + + private final class MockViewLayoutPublisher: ViewLayoutPublishing { + let onViewLayout = PostHogThrottledMulticastCallback() + } + private final class ThreadRecordingView: UIView { private(set) var laidOutOffMainThread = false diff --git a/api/posthog-ios.public-api.txt b/api/posthog-ios.public-api.txt index 513c498bbe..fcab4d3b44 100644 --- a/api/posthog-ios.public-api.txt +++ b/api/posthog-ios.public-api.txt @@ -58,6 +58,7 @@ PostHog | PostHogConfig.captureElementInteractions | property | @objc var captur PostHog | PostHogConfig.capturePushNotificationOpened | property | @objc var capturePushNotificationOpened: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)capturePushNotificationOpened PostHog | PostHogConfig.capturePushNotificationSubscriptions | property | @objc var capturePushNotificationSubscriptions: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)capturePushNotificationSubscriptions PostHog | PostHogConfig.captureScreenViews | property | @objc var captureScreenViews: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)captureScreenViews +PostHog | PostHogConfig.captureViewLayoutChanges | property | @objc var captureViewLayoutChanges: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)captureViewLayoutChanges PostHog | PostHogConfig.dataMode | property | @objc var dataMode: PostHogConfig.PostHogDataMode | c:@M@PostHog@objc(cs)PostHogConfig(py)dataMode PostHog | PostHogConfig.debug | property | @objc var debug: Bool | c:@M@PostHog@objc(cs)PostHogConfig(py)debug PostHog | PostHogConfig.defaultHost | type.property | static let defaultHost: String | s:7PostHog0aB6ConfigC11defaultHostSSvpZ @@ -356,7 +357,6 @@ PostHog | PostHogSessionReplayConfig | class | @objc(PostHogSessionReplayConfig) PostHog | PostHogSessionReplayConfig.captureLogs | property | @objc var captureLogs: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureLogs PostHog | PostHogSessionReplayConfig.captureLogsConfig | property | @objc var captureLogsConfig: PostHogSessionReplayConsoleLogConfig | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureLogsConfig PostHog | PostHogSessionReplayConfig.captureNetworkTelemetry | property | @objc var captureNetworkTelemetry: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureNetworkTelemetry -PostHog | PostHogSessionReplayConfig.captureViewLayoutChanges | property | @objc var captureViewLayoutChanges: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)captureViewLayoutChanges PostHog | PostHogSessionReplayConfig.debouncerDelay | property | @objc var debouncerDelay: TimeInterval { get set } | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)debouncerDelay PostHog | PostHogSessionReplayConfig.maskAllImages | property | @objc var maskAllImages: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)maskAllImages PostHog | PostHogSessionReplayConfig.maskAllSandboxedViews | property | @objc var maskAllSandboxedViews: Bool | c:@M@PostHog@objc(cs)PostHogSessionReplayConfig(py)maskAllSandboxedViews