feat(replay): Add ReplayFrameObserver for snapshot testing#5386
Conversation
|
📲 Install BuildsAndroid
|
5b10cdd to
e7452f7
Compare
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9770665 | 315.64 ms | 378.00 ms | 62.36 ms |
| ad8da22 | 314.52 ms | 352.47 ms | 37.95 ms |
| 382d6c1 | 306.85 ms | 368.70 ms | 61.85 ms |
| 72020f8 | 312.32 ms | 370.94 ms | 58.62 ms |
| d501a7e | 348.06 ms | 431.42 ms | 83.36 ms |
| 44472da | 324.77 ms | 360.60 ms | 35.83 ms |
| 22f4345 | 312.78 ms | 347.40 ms | 34.62 ms |
| b03edbb | 314.90 ms | 350.22 ms | 35.33 ms |
| 6b019b7 | 403.90 ms | 546.09 ms | 142.19 ms |
| 694d587 | 305.45 ms | 378.38 ms | 72.94 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 9770665 | 0 B | 0 B | 0 B |
| ad8da22 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| 382d6c1 | 1.58 MiB | 2.29 MiB | 719.85 KiB |
| 72020f8 | 1.58 MiB | 2.19 MiB | 620.21 KiB |
| d501a7e | 0 B | 0 B | 0 B |
| 44472da | 0 B | 0 B | 0 B |
| 22f4345 | 1.58 MiB | 2.29 MiB | 719.83 KiB |
| b03edbb | 1.58 MiB | 2.13 MiB | 557.32 KiB |
| 6b019b7 | 0 B | 0 B | 0 B |
| 694d587 | 1.58 MiB | 2.19 MiB | 620.06 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 |
| 1cddad0 | 342.73 ms | 424.14 ms | 81.41 ms |
| a9ea107 | 311.04 ms | 361.61 ms | 50.57 ms |
| 11100f7 | 322.44 ms | 377.60 ms | 55.16 ms |
| 91a12dd | 326.85 ms | 373.23 ms | 46.37 ms |
| 62bcea4 | 359.22 ms | 426.90 ms | 67.67 ms |
| 8d0611b | 308.40 ms | 356.90 ms | 48.50 ms |
| c906754 | 338.11 ms | 408.86 ms | 70.75 ms |
| b002297 | 298.42 ms | 348.54 ms | 50.12 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| cd28dc9 | 0 B | 0 B | 0 B |
| 1cddad0 | 0 B | 0 B | 0 B |
| a9ea107 | 0 B | 0 B | 0 B |
| 11100f7 | 0 B | 0 B | 0 B |
| 91a12dd | 0 B | 0 B | 0 B |
| 62bcea4 | 0 B | 0 B | 0 B |
| 8d0611b | 0 B | 0 B | 0 B |
| c906754 | 0 B | 0 B | 0 B |
| b002297 | 0 B | 0 B | 0 B |
e7452f7 to
2aff4b0
Compare
markushi
left a comment
There was a problem hiding this comment.
LGTM! Let's discuss first if the API should be experimental for internal, but other than that no blockers.
0xadam-brown
left a comment
There was a problem hiding this comment.
Thanks for this @runningcode. A few comments.
Big picture, did we think about defining a new ReplayScreenshotObserver interface inside the replay module (akin to ScreenshotRecorderCallback) rather than routing through the (universally available) SentryReplayOptions?
An interface in the replay module would let us avoid the Hint indirection, and it'd avoid the tension of putting @ApiStatus.Internal on an *Options member.
Thoughts?
375389b to
f2c0c49
Compare
max is off but his points were addressed I believe
romtsn
left a comment
There was a problem hiding this comment.
LGTM. I think would be still good to call onMaskedFrameCaptured in the other onScreenshotRecorded()
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) <[email protected]>
…(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) <[email protected]>
…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) <[email protected]>
…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) <[email protected]>
Co-authored-by: Markus Hintersteiner <[email protected]>
…r (JAVA-504) Move the frame observer API from the core sentry module to sentry-android-replay so it can use Bitmap directly instead of the Hint indirection. The new ReplaySnapshotObserver fun interface lives in the replay module and is set on ReplayIntegration. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…in test (JAVA-504) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…AVA-504) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…VA-504) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…AVA-504) Move ReplaySnapshotTest to a conditional androidTestReplay source set so it's only compiled when APPLY_SENTRY_INTEGRATIONS is true. The test imports replay classes that aren't on the classpath otherwise. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…VA-504) Consumers of the observer API receive a copy of the bitmap instead of the replay system's shared instance. This eliminates race conditions and crashes when consumers store or use the bitmap asynchronously. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…with Hint API (JAVA-504)
Move ReplaySnapshotObserver from the replay module to SentryReplayOptions
in the core module and change the callback signature to use Hint instead
of Bitmap. The bitmap is now accessible via TypeCheckHint.REPLAY_FRAME_BITMAP.
This allows configuring the observer during Sentry.init{} alongside other
replay options, removing the need to cast replayController to
ReplayIntegration.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…A-504) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…r (JAVA-504) Rename the interface to ReplayFrameObserver and the callback method to onMaskedFrameCaptured to clarify that frames have masking applied. Also update the changelog with a usage snippet. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…orded (JAVA-504) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
7d51e78 to
62c502c
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f064cdf. Configure here.
* feat(replay): Add beforeStoreFrame callback (JAVA-504)
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) <[email protected]>
* feat(replay): Add replay snapshot UI test with Sauce Labs collection (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) <[email protected]>
* fix(replay): Use Java API in snapshot test to avoid extension dep (JAVA-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) <[email protected]>
* fix(replay): Skip snapshot test on GH emulators and add changelog (JAVA-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) <[email protected]>
* Apply suggestion from @markushi
Co-authored-by: Markus Hintersteiner <[email protected]>
* refactor(replay): Replace beforeStoreFrame with ReplaySnapshotObserver (JAVA-504)
Move the frame observer API from the core sentry module to
sentry-android-replay so it can use Bitmap directly instead of
the Hint indirection. The new ReplaySnapshotObserver fun interface
lives in the replay module and is set on ReplayIntegration.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(replay): Mark ReplaySnapshotObserver as experimental and use Set in test (JAVA-504)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(replay): Add @ApiStatus.Experimental to ReplaySnapshotObserver (JAVA-504)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(replay): Make snapshotObserver public for cross-module access (JAVA-504)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(replay): Exclude ReplaySnapshotTest when integrations disabled (JAVA-504)
Move ReplaySnapshotTest to a conditional androidTestReplay source set
so it's only compiled when APPLY_SENTRY_INTEGRATIONS is true. The test
imports replay classes that aren't on the classpath otherwise.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(replay): Copy bitmap before passing to ReplaySnapshotObserver (JAVA-504)
Consumers of the observer API receive a copy of the bitmap instead of
the replay system's shared instance. This eliminates race conditions
and crashes when consumers store or use the bitmap asynchronously.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor(replay): Move ReplaySnapshotObserver to SentryReplayOptions with Hint API (JAVA-504)
Move ReplaySnapshotObserver from the replay module to SentryReplayOptions
in the core module and change the callback signature to use Hint instead
of Bitmap. The bitmap is now accessible via TypeCheckHint.REPLAY_FRAME_BITMAP.
This allows configuring the observer during Sentry.init{} alongside other
replay options, removing the need to cast replayController to
ReplayIntegration.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(replay): Remove unnecessary jetbrains-annotations dependency (JAVA-504)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* refactor(replay): Rename ReplaySnapshotObserver to ReplayFrameObserver (JAVA-504)
Rename the interface to ReplayFrameObserver and the callback method
to onMaskedFrameCaptured to clarify that frames have masking applied.
Also update the changelog with a usage snippet.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* Format code
* fix(replay): Call onMaskedFrameCaptured in File-based onScreenshotRecorded (JAVA-504)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
* fix(changelog): Move replay entry to Unreleased section (JAVA-504)
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Markus Hintersteiner <[email protected]>
Co-authored-by: Sentry Github Bot <[email protected]>

Summary
ReplayFrameObserverfunctional interface that fires right before a replay frame is stored to disk, receiving the masked bitmap, timestamp, and screen namesnapshotObserveronReplayIntegrationso callers can set it after SDK initReplaySnapshotTestUI integration test that captures masked replay frames on Sauce LabsuseTestStorageServiceand*.pngartifact collection in the Sauce Labs configReplaySnapshotTestto a conditionalandroidTestReplaysource set so it compiles only whenAPPLY_SENTRY_INTEGRATIONS=trueRelates to JAVA-504
🤖 Generated with Claude Code