From 619fa9079bc0c49e76bc67c2f35358c86c6ade22 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Mon, 7 Sep 2026 16:07:41 +0200 Subject: [PATCH] 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}