feat(replay): internal session-replay snapshot hook for out-of-engine capture#714
Merged
Conversation
…gine capture Adds PostHogSDK.captureSessionReplaySnapshot(afterScreenUpdates:), gated behind @_spi(PostHogInternal), so first-party wrapper SDKs (posthog-flutter) can capture the current native window on their own cadence to record native screens that cover an out-of-engine UI. Shares the existing screenshot path and masking; not a public API. The bridge screenshot path dedups unchanged frames (hash of the encoded image) so a static native screen on a fixed cadence is not re-sent every tick, matching the wireframe path's change-driven behavior. Bridged episodes emit their own meta: an episode's first frame re-arms the meta event, which carries the covering view controller's name, mirroring the Android bridge. Native (non-bridge) capture is unaffected — the re-arm is gated on the bridge-only first-frame flag. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 13, 2026
turnipdabeets
marked this pull request as ready for review
July 13, 2026 23:57
Contributor
|
Contributor
posthog-ios Compliance ReportDate: 2026-07-14 20:23:09 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
|
afterScreenUpdates applies until the episode's first frame is captured (it carries the meta/hash re-arm), not just the first attempt; off-main calls block on a synchronous main-queue hop and must not come from a queue the main thread can wait on.
ioannisj
approved these changes
Jul 14, 2026
ioannisj
left a comment
Contributor
There was a problem hiding this comment.
LG left a couple of comments
…d unify the first-frame flag naming
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
First-party wrapper SDKs (starting with posthog-flutter) need to record native screens that cover their engine-rendered UI — paywalls and other view controllers presented on top of a Flutter surface. The wrapper runs its own out-of-engine occlusion detection and needs to drive a one-shot capture of the current native window on its own cadence.
This adds an SPI (
@_spi(PostHogInternal)) hook for that. It is not public API: it carries no source/binary stability guarantees and shares snapshot state with the normal timer-driven capture, so only a coordinating first-party SDK should call it.Consumed by PostHog/posthog-flutter#473. PostHog/posthog-flutter#151 is the end-user issue this enables fixing.
🔨 Changes
PostHogSDK.captureSessionReplaySnapshot(afterScreenUpdates:)— SPI entry point; callable from any thread, hops to main for the capture, returns whether a frame was captured so the caller can retry.PostHogReplayIntegration.captureBridgeSnapshot(settlePresentation:)— renders and enqueues the current key window's screenshot.settlePresentationpassesafterScreenUpdates: true(used only on an episode's first frame — it flickers secure text fields, so steady-state ticks avoid it).href, and an episode's first frame re-arms the meta event so every episode opens with one. Native (non-bridge) capture is unaffected — the re-arm is gated on the bridge-only first-frame flag.renderAndEnqueueScreenshotnow returnsBooland bails (return false) on a failed capture, so nothing is enqueued and the caller can retry.lastImageHash, so a recurring native screen always re-sends its opening frame under the always-on screenshot dedup (fix(replay): skip re-sending unchanged screenshots in screenshot mode #711).💚 How did you test it?
native_occlusion_episode_test.dartover there).📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
afterScreenUpdatesonly on episode-first frames because it visibly flickers secure text fields; per-episode meta re-arm added after review so replay names the covering native screen instead of keeping the stale wrapper screen name.