fix(replay): stop the layoutSublayers swizzle running UIKit layout off-main - #809
Closed
posthog[bot] wants to merge 4 commits into
Closed
fix(replay): stop the layoutSublayers swizzle running UIKit layout off-main#809posthog[bot] wants to merge 4 commits into
posthog[bot] wants to merge 4 commits into
Conversation
…f-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
Contributor
Author
🦔 PostHog Review reviewed this pull requestFound 2 must fix, 1 should fix, 0 consider. Published 3 findings (view the review). Resolved comments: 1 fixed, 2 already settled |
Contributor
posthog-ios Compliance ReportDate: 2026-09-08 21:09:07 UTC ✅ All Tests Passed!45/45 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 16/16 tests passed View Details
|
Generated-By: PostHog Desktop Task-Id: 296e6380-9948-4bfe-9ab0-73486280ebf0
Contributor
Author
|
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
…tion 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
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
Member
|
see #806 (comment) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
layoutSublayers. Every iOS replay user is on the path, becausePostHogReplayIntegration.start()subscribed to the layout publisher unconditionally and neitherthrottleDelaynorscreenshotModegated it.ph_swizzled_layoutSublayers(of:)forwarded to UIKit before any thread check. Core Animation can call the method on a background thread when it commits a thread-local transaction during thread cleanup (CA::Transaction::release_thread). The forwarded call then runs Auto Layout off-main andNSISEngineraisesNSInternalInconsistencyException, which terminates the host app.enableSwizzling = false, which also drops autocapture, screen views, replay and surveys.Closes #806
Before / after, off the main thread
📝 Changes
PostHogConfig.captureViewLayoutChanges(defaulttrue)captureViewLayoutChangessits onPostHogConfigrather thansessionReplayConfigbecause the hook is shared: surveys subscribe to the same publisher and are enabled by default, and the publisher installs the swizzle for any subscriber. With the flag off, replay keeps console logs, network telemetry and interaction events but captures no screen content, and surveys appear only on app activation or an event trigger.💚 How did you test it?
ApplicationViewLayoutPublisherTest.layoutStaysOnMainThreadinstalls the swizzle, callslayoutSublayers(of:)from a backgroundThread, and assertslayoutSubviews()never ran off-main. Against the old code the view records an off-main layout pass; against the new code it does not.ViewLayoutCaptureOptOutTestinstalls the survey integration against an injected publisher with the flag both on and off, and asserts the subscription only exists when the flag is on.Tests / test-ios-simulator), alongside the build, example and lint matrix.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Fully autonomous
Written by Claude Opus 5 running in PostHog Desktop, from the linked inbox report.
Decisions on the off-main path:
self.ph_swizzled_layoutSublayers(of: layer))UIView, and a view that is mid-deallocwhen Core Animation reaches its layer delegate would be resurrected.DispatchQueue.main.async { layer.setNeedsLayout() }CALayer, which the transaction already holds, and re-arms the layout through the normal main-thread path. The delegate is resolved fresh at that point, so a deallocated view is simply skipped.The maintainer comment on #806 reports that this exception also reproduces with the swizzle absent, and cautions against moving the original call. That is a UIKit-level hazard the SDK cannot remove for apps in general — but the swizzle sits in front of UIKit whenever replay is on, so the SDK can stop the off-main pass for its own users, which is what this does. The off-main notification branch is dropped as well: the deferred main-thread layout produces that notification itself.
The review bot found the first version of the opt-out did not remove the hook, because surveys kept subscribing. It offered two fixes: honour one flag in both subscriptions, or narrow the wording. Commit
dd78003(a separate automated task on this branch) took the wording route;5832c02supersedes it with the behaviour route, so the flag now does what its name says.Not addressed: why the background transaction contains pending view-layout work in the first place. That trigger is still open on #806, and this change makes the SDK safe on the path rather than explaining it.
Created with PostHog Desktop from this inbox report.