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}