feat(replay): Add beforeStoreFrame callback for snapshot testing#5386
feat(replay): Add beforeStoreFrame callback for snapshot testing#5386runningcode wants to merge 4 commits into
Conversation
Add an experimental callback that fires right before a replay frame is
stored to disk. The callback receives the masked bitmap (via Hint),
timestamp, and current screen name. This enables snapshot testing of
replay masking without needing to decode stored video segments.
Includes a Kotlin extension for ergonomic usage:
options.sessionReplay.beforeStoreFrame { bitmap, ts, screen -> ... }
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
📲 Install BuildsAndroid
|
5b10cdd to
e7452f7
Compare
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 674d437 | 355.28 ms | 504.18 ms | 148.90 ms |
| 2195398 | 321.31 ms | 391.66 ms | 70.35 ms |
| 62b579c | 312.88 ms | 361.57 ms | 48.70 ms |
| b77456b | 393.26 ms | 441.10 ms | 47.84 ms |
| 694d587 | 312.37 ms | 402.77 ms | 90.41 ms |
| 6b019b7 | 319.84 ms | 333.15 ms | 13.31 ms |
| 6edfca2 | 305.52 ms | 432.78 ms | 127.26 ms |
| b03edbb | 314.90 ms | 350.22 ms | 35.33 ms |
| d15471f | 294.13 ms | 399.49 ms | 105.36 ms |
| fcec2f2 | 314.96 ms | 373.66 ms | 58.70 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 674d437 | 1.58 MiB | 2.10 MiB | 530.94 KiB |
| 2195398 | 0 B | 0 B | 0 B |
| 62b579c | 0 B | 0 B | 0 B |
| b77456b | 1.58 MiB | 2.12 MiB | 548.11 KiB |
| 694d587 | 1.58 MiB | 2.19 MiB | 620.06 KiB |
| 6b019b7 | 0 B | 0 B | 0 B |
| 6edfca2 | 1.58 MiB | 2.13 MiB | 559.07 KiB |
| b03edbb | 1.58 MiB | 2.13 MiB | 557.32 KiB |
| d15471f | 1.58 MiB | 2.13 MiB | 559.54 KiB |
| fcec2f2 | 1.58 MiB | 2.12 MiB | 551.50 KiB |
Previous results on branch: no/java-504-replay-before-store-frame-callback
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| cd28dc9 | 316.30 ms | 354.64 ms | 38.34 ms |
| 62bcea4 | 359.22 ms | 426.90 ms | 67.67 ms |
| 1cddad0 | 342.73 ms | 424.14 ms | 81.41 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| cd28dc9 | 0 B | 0 B | 0 B |
| 62bcea4 | 0 B | 0 B | 0 B |
| 1cddad0 | 0 B | 0 B | 0 B |
…(JAVA-504) Add ReplaySnapshotTest that uses the beforeStoreFrame callback to capture masked replay frames during a Compose UI test. Frames are written to the Downloads/sauce_labs_custom_screenshots/ directory, which is the standard path Sauce Labs collects screenshots from. CI changes: - Add *.png to Sauce Labs artifact match patterns - Upload collected replay snapshots via sentry-cli build snapshots Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
e7452f7 to
2aff4b0
Compare
…VA-504) The Kotlin extension `beforeStoreFrame` comes from `sentry-android-replay` which may not resolve in the UI test module. Use the Java callback API directly instead. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…VA-504) GH Actions emulators don't support screenshot capture for replay, so the ReplaySnapshotTest needs the same assumeThat guard used by ReplayTest. Also adds a changelog entry for the beforeStoreFrame callback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| val callback = options.sessionReplay.beforeStoreFrame | ||
| if (callback != null) { | ||
| try { | ||
| val hint = Hint() | ||
| hint.set(TypeCheckHint.REPLAY_FRAME_BITMAP, bitmap) | ||
| callback.execute(hint, frameTimeStamp, screen) | ||
| } catch (e: Throwable) { | ||
| options.logger.log(ERROR, "Error in beforeStoreFrame callback", e) | ||
| } | ||
| } |
There was a problem hiding this comment.
Bug: A race condition exists where the shared screenshot bitmap can be overwritten by the next frame's PixelCopy operation while the beforeStoreFrame callback is still processing it.
Severity: HIGH
Suggested Fix
Provide a copy of the screenshot bitmap to the beforeStoreFrame callback instead of the shared instance. This can be achieved by creating a new bitmap using bitmap.copy(bitmap.config, true) before invoking the callback. This ensures the callback operates on an isolated version of the frame data, preventing concurrent modification issues.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location:
sentry-android-replay/src/main/java/io/sentry/android/replay/ReplayIntegration.kt#L314-L323
Potential issue: The `beforeStoreFrame` callback is passed a shared, mutable
`screenshot` bitmap that is reused across all frames. This callback executes on a
background thread, `replayExecutor`. Concurrently, the main thread schedules the next
frame capture, which uses `PixelCopy` to write to the same shared bitmap. There is no
synchronization between the `PixelCopy` write operation and the `beforeStoreFrame`
callback reading the bitmap. If the callback performs I/O, such as `bitmap.compress()`,
the bitmap's data can be overwritten by the next frame's data mid-read, leading to
corrupted images.
Did we get this right? 👍 / 👎 to inform future reviews.
| @Before | ||
| fun setup() { | ||
| // GH Actions emulators don't support capturing screenshots for replay | ||
| @Suppress("KotlinConstantConditions") |
There was a problem hiding this comment.
I copied this from ReplayTest but why are we even running these on emulators in gh actions?
Summary
BeforeStoreFrameCallbacktoSentryReplayOptionsthat fires right before a replay frame is stored to disk, receiving the masked bitmap, timestamp, and screen name viaHintSentryReplayOptions.beforeStoreFrame { bitmap, ts, screen -> }for ergonomic usageReplaySnapshotTestUI integration test that captures masked replay frames viaTestStorageon Sauce LabsuseTestStorageServiceand*.pngartifact collection in the Sauce Labs config, withsentry-cli build snapshotsupload in CIRelates to JAVA-504
🤖 Generated with Claude Code