From 619fa9079bc0c49e76bc67c2f35358c86c6ade22 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 7 Sep 2026 16:07:41 +0200 Subject: [PATCH 1/2] test(e2e): Restore iOS replay assertion in captureReplay test Re-adds the assertReplay.yml check removed in #6072. That assertion was flaky because buffer-mode replay (replaysOnErrorSampleRate) only attaches a replay_id to the error event if the native replay buffer captured a frame before the error fired; on slow CI the exception was captured while the buffer was still empty, so the event was sent permanently without a replay_id and no server-side retry could recover it. Make the test deterministic by priming the buffer before capturing the exception: a new side-effect-free "Replay Ping" counter in the e2e harness is tapped repeatedly to mutate the view hierarchy so the native capture records frames first. iOS only, matching the assertion scope (Android does not reliably capture replays in CI, see #4277). Co-Authored-By: Claude Opus 4.8 --- .../e2e-tests/maestro/captureReplay.yml | 11 +++++++++ .../e2e-tests/maestro/utils/assertReplay.yml | 23 +++++++++++++++++++ .../maestro/utils/primeReplayBuffer.yml | 16 +++++++++++++ dev-packages/e2e-tests/src/EndToEndTests.tsx | 9 ++++++++ 4 files changed, 59 insertions(+) create mode 100644 dev-packages/e2e-tests/maestro/utils/assertReplay.yml create mode 100644 dev-packages/e2e-tests/maestro/utils/primeReplayBuffer.yml diff --git a/dev-packages/e2e-tests/maestro/captureReplay.yml b/dev-packages/e2e-tests/maestro/captureReplay.yml index a001c713f8..9499f427c2 100644 --- a/dev-packages/e2e-tests/maestro/captureReplay.yml +++ b/dev-packages/e2e-tests/maestro/captureReplay.yml @@ -5,5 +5,16 @@ jsEngine: graaljs file: utils/launchTestAppClear.yml env: replaysOnErrorSampleRate: 1.0 +# Prime the native replay buffer before capturing the exception so a replay_id +# is attached to the event (iOS only; Android does not reliably capture replays +# in CI — see https://github.com/getsentry/sentry-react-native/pull/4277). +- runFlow: + file: utils/primeReplayBuffer.yml + when: + platform: iOS - tapOn: "Capture Exception" - runFlow: utils/assertEventIdVisible.yml +- runFlow: + file: utils/assertReplay.yml + when: + platform: iOS diff --git a/dev-packages/e2e-tests/maestro/utils/assertReplay.yml b/dev-packages/e2e-tests/maestro/utils/assertReplay.yml new file mode 100644 index 0000000000..b9885b8227 --- /dev/null +++ b/dev-packages/e2e-tests/maestro/utils/assertReplay.yml @@ -0,0 +1,23 @@ +appId: ${APP_ID} +jsEngine: graaljs +--- +- extendedWaitUntil: + visible: + id: "eventId" + timeout: 60_000 # 60 seconds + +- copyTextFrom: + id: "eventId" +- assertTrue: ${maestro.copiedText} + +- runScript: + file: sentryApi.js + env: + fetch: replay + eventId: ${maestro.copiedText} + sentryAuthToken: ${SENTRY_AUTH_TOKEN} + +- assertTrue: ${output.replayId} +- assertTrue: ${output.replayDuration} +- assertTrue: ${output.replaySegments} +- assertTrue: ${output.replayCodec == "ftypmp42"} diff --git a/dev-packages/e2e-tests/maestro/utils/primeReplayBuffer.yml b/dev-packages/e2e-tests/maestro/utils/primeReplayBuffer.yml new file mode 100644 index 0000000000..7fdb3ab132 --- /dev/null +++ b/dev-packages/e2e-tests/maestro/utils/primeReplayBuffer.yml @@ -0,0 +1,16 @@ +appId: ${APP_ID} +jsEngine: graaljs +--- +# Buffer-mode replay (replaysOnErrorSampleRate) only attaches a replay_id to the +# error event if the native replay buffer captured at least one frame before the +# error fired. Immediately after launch the buffer can still be empty on slow CI +# simulators, so the event is sent without a replay_id and no server-side retry +# can recover it. Tap the "Replay Ping" counter repeatedly to mutate the view +# hierarchy (and spend a few seconds of wall-clock while Maestro re-reads the UI +# between taps) so the native capture records frames before the exception is +# captured. If this proves insufficient on CI, increase the repeat count. +- repeat: + times: 8 + commands: + - tapOn: + id: 'replayPing' diff --git a/dev-packages/e2e-tests/src/EndToEndTests.tsx b/dev-packages/e2e-tests/src/EndToEndTests.tsx index b0fec06b7e..b37c14ad15 100644 --- a/dev-packages/e2e-tests/src/EndToEndTests.tsx +++ b/dev-packages/e2e-tests/src/EndToEndTests.tsx @@ -8,6 +8,12 @@ const EndToEndTestsScreen = (): React.JSX.Element => { const [isReady, setIsReady] = React.useState(false); const [eventId, setEventId] = React.useState(null); const [error, setError] = React.useState('No error'); + // Buffer-mode replay (replaysOnErrorSampleRate) only attaches a replay_id to + // an error event if the native replay buffer captured at least one frame + // before the error fired. This counter is tapped by the captureReplay e2e + // flow to mutate the view hierarchy (side-effect free, no events sent) so the + // buffer records frames before the exception is captured. + const [replayPingCount, setReplayPingCount] = React.useState(0); React.useEffect(() => { const client: Sentry.ReactNativeClient | undefined = Sentry.getClient(); @@ -78,6 +84,9 @@ const EndToEndTestsScreen = (): React.JSX.Element => { setEventId(null)}> Clear Event Id + setReplayPingCount((count) => count + 1)}> + Replay Ping {replayPingCount} + {testCases.map((testCase) => ( {testCase.name} From 9502e65f8483390d7c29eb6f8b07506291668516 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 7 Sep 2026 16:43:16 +0200 Subject: [PATCH 2/2] test(e2e): Evaluate replay assertion on Android Runs the captureReplay buffer-priming and assertReplay check on Android in addition to iOS. Android has never asserted replays in CI (see #4277, "android doesn't seem to capture replays in CI"); this re-evaluates that now that deterministic buffer priming lands the assertion reliably on iOS. Relax the codec assertion from an exact `ftypmp42` major brand to a valid MP4 container check (the "ftyp" box), since iOS (AVAssetWriter) and Android (MediaMuxer) can emit different major brands for the same valid MP4. The Android E2E result on this PR is the deliverable: it empirically shows whether Android now captures a replay in CI. If red, keep the assertion iOS-only. Co-Authored-By: Claude Opus 4.8 --- dev-packages/e2e-tests/maestro/captureReplay.yml | 16 ++++++---------- .../e2e-tests/maestro/utils/assertReplay.yml | 5 ++++- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/dev-packages/e2e-tests/maestro/captureReplay.yml b/dev-packages/e2e-tests/maestro/captureReplay.yml index 9499f427c2..6833d82f0b 100644 --- a/dev-packages/e2e-tests/maestro/captureReplay.yml +++ b/dev-packages/e2e-tests/maestro/captureReplay.yml @@ -6,15 +6,11 @@ jsEngine: graaljs env: replaysOnErrorSampleRate: 1.0 # Prime the native replay buffer before capturing the exception so a replay_id -# is attached to the event (iOS only; Android does not reliably capture replays -# in CI — see https://github.com/getsentry/sentry-react-native/pull/4277). -- runFlow: - file: utils/primeReplayBuffer.yml - when: - platform: iOS +# is attached to the event. Runs on both platforms: Android historically did not +# reliably capture replays in CI (see +# https://github.com/getsentry/sentry-react-native/pull/4277); this re-evaluates +# that with deterministic buffer priming. +- runFlow: utils/primeReplayBuffer.yml - tapOn: "Capture Exception" - runFlow: utils/assertEventIdVisible.yml -- runFlow: - file: utils/assertReplay.yml - when: - platform: iOS +- runFlow: utils/assertReplay.yml diff --git a/dev-packages/e2e-tests/maestro/utils/assertReplay.yml b/dev-packages/e2e-tests/maestro/utils/assertReplay.yml index b9885b8227..e21f10f3fe 100644 --- a/dev-packages/e2e-tests/maestro/utils/assertReplay.yml +++ b/dev-packages/e2e-tests/maestro/utils/assertReplay.yml @@ -20,4 +20,7 @@ jsEngine: graaljs - assertTrue: ${output.replayId} - assertTrue: ${output.replayDuration} - assertTrue: ${output.replaySegments} -- assertTrue: ${output.replayCodec == "ftypmp42"} +# Assert a valid MP4 container was produced (the "ftyp" box at byte offset 4) +# rather than a platform-specific major brand: iOS (AVAssetWriter) emits "mp42" +# while Android (MediaMuxer) may emit a different brand such as "isom". +- assertTrue: ${output.replayCodec.startsWith("ftyp")}