From dac84882a80aaba1ed01613c4e4b05323b786ff1 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 7 Mar 2023 16:20:29 +0100 Subject: [PATCH 1/4] test(replay): Add integration tests for input masking on change This adds integration tests for input masking specifically when `maskAllInputs = false`. remove unused snapshots skip firefox for these tests due to flakeyness TEMP: run 100x shorter test text Revert "TEMP: run 100x" This reverts commit 08967e2955b8e3a51a909f658948a57b1b312756. skip webkit --- .../suites/replay/privacyInput/init.js | 19 +++ .../suites/replay/privacyInput/template.html | 15 +++ .../suites/replay/privacyInput/test.ts | 111 ++++++++++++++++++ .../suites/replay/privacyInputMaskAll/init.js | 19 +++ .../replay/privacyInputMaskAll/template.html | 13 ++ .../suites/replay/privacyInputMaskAll/test.ts | 101 ++++++++++++++++ packages/integration-tests/utils/fixtures.ts | 15 +++ .../integration-tests/utils/replayHelpers.ts | 30 ++++- 8 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 packages/integration-tests/suites/replay/privacyInput/init.js create mode 100644 packages/integration-tests/suites/replay/privacyInput/template.html create mode 100644 packages/integration-tests/suites/replay/privacyInput/test.ts create mode 100644 packages/integration-tests/suites/replay/privacyInputMaskAll/init.js create mode 100644 packages/integration-tests/suites/replay/privacyInputMaskAll/template.html create mode 100644 packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts diff --git a/packages/integration-tests/suites/replay/privacyInput/init.js b/packages/integration-tests/suites/replay/privacyInput/init.js new file mode 100644 index 000000000000..a09c517b6a92 --- /dev/null +++ b/packages/integration-tests/suites/replay/privacyInput/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; +import { Replay } from '@sentry/replay'; + +window.Sentry = Sentry; +window.Replay = new Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + useCompression: false, + maskAllInputs: false, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + integrations: [window.Replay], +}); diff --git a/packages/integration-tests/suites/replay/privacyInput/template.html b/packages/integration-tests/suites/replay/privacyInput/template.html new file mode 100644 index 000000000000..735abb395522 --- /dev/null +++ b/packages/integration-tests/suites/replay/privacyInput/template.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/packages/integration-tests/suites/replay/privacyInput/test.ts b/packages/integration-tests/suites/replay/privacyInput/test.ts new file mode 100644 index 000000000000..926578088675 --- /dev/null +++ b/packages/integration-tests/suites/replay/privacyInput/test.ts @@ -0,0 +1,111 @@ +import { expect } from '@playwright/test'; +import { IncrementalSource } from '@sentry-internal/rrweb'; +import type { inputData } from '@sentry-internal/rrweb/typings/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; +import { + getIncrementalRecordingSnapshots, + shouldSkipReplayTest, + waitForReplayRequest, +} from '../../../utils/replayHelpers'; + +function isInputMutation( + snap: IncrementalRecordingSnapshot, +): snap is IncrementalRecordingSnapshot & { data: inputData } { + return snap.data.source == IncrementalSource.Input; +} + +sentryTest( + 'should mask input initial value and its changes', + async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => { + // TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation. + if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + const reqPromise3 = waitForReplayRequest(page, 3); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + await reqPromise0; + + const text = 'test'; + + await page.locator('#input').type(text); + await forceFlushReplay(); + const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); + const lastSnapshot = snapshots[snapshots.length - 1]; + expect(lastSnapshot.data.text).toBe(text); + + await page.locator('#input-masked').type(text); + await forceFlushReplay(); + const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); + const lastSnapshot2 = snapshots2[snapshots2.length - 1]; + expect(lastSnapshot2.data.text).toBe('*'.repeat(text.length)); + + await page.locator('#input-ignore').type(text); + await forceFlushReplay(); + const snapshots3 = getIncrementalRecordingSnapshots(await reqPromise3).filter(isInputMutation); + expect(snapshots3.length).toBe(0); + }, +); + +sentryTest( + 'should mask textarea initial value and its changes', + async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => { + // TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation. + if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + const reqPromise3 = waitForReplayRequest(page, 3); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await reqPromise0; + + const text = 'test'; + await page.locator('#textarea').type(text); + await forceFlushReplay(); + const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); + const lastSnapshot = snapshots[snapshots.length - 1]; + expect(lastSnapshot.data.text).toBe(text); + + await page.locator('#textarea-masked').type(text); + await forceFlushReplay(); + const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); + const lastSnapshot2 = snapshots2[snapshots2.length - 1]; + expect(lastSnapshot2.data.text).toBe('*'.repeat(text.length)); + + await page.locator('#textarea-ignore').type(text); + await forceFlushReplay(); + const snapshots3 = getIncrementalRecordingSnapshots(await reqPromise3).filter(isInputMutation); + expect(snapshots3.length).toBe(0); + }, +); diff --git a/packages/integration-tests/suites/replay/privacyInputMaskAll/init.js b/packages/integration-tests/suites/replay/privacyInputMaskAll/init.js new file mode 100644 index 000000000000..6345c0f75f4e --- /dev/null +++ b/packages/integration-tests/suites/replay/privacyInputMaskAll/init.js @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/browser'; +import { Replay } from '@sentry/replay'; + +window.Sentry = Sentry; +window.Replay = new Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + useCompression: false, + maskAllInputs: true, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + integrations: [window.Replay], +}); diff --git a/packages/integration-tests/suites/replay/privacyInputMaskAll/template.html b/packages/integration-tests/suites/replay/privacyInputMaskAll/template.html new file mode 100644 index 000000000000..404bed05a6d0 --- /dev/null +++ b/packages/integration-tests/suites/replay/privacyInputMaskAll/template.html @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts b/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts new file mode 100644 index 000000000000..41721c1afee9 --- /dev/null +++ b/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -0,0 +1,101 @@ +import { expect } from '@playwright/test'; +import { IncrementalSource } from '@sentry-internal/rrweb'; +import type { inputData } from '@sentry-internal/rrweb/typings/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers'; +import { + getIncrementalRecordingSnapshots, + shouldSkipReplayTest, + waitForReplayRequest, +} from '../../../utils/replayHelpers'; + +function isInputMutation( + snap: IncrementalRecordingSnapshot, +): snap is IncrementalRecordingSnapshot & { data: inputData } { + return snap.data.source == IncrementalSource.Input; +} + +sentryTest( + 'should mask input initial value and its changes from `maskAllInputs` and allow unmasked selector', + async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => { + // TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation. + if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + await reqPromise0; + + const text = 'test'; + + await page.locator('#input').type(text); + await forceFlushReplay(); + const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); + const lastSnapshot = snapshots[snapshots.length - 1]; + expect(lastSnapshot.data.text).toBe('*'.repeat(text.length)); + + await page.locator('#input-unmasked').type(text); + await forceFlushReplay(); + const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); + const lastSnapshot2 = snapshots2[snapshots2.length - 1]; + expect(lastSnapshot2.data.text).toBe(text); + }, +); + +sentryTest( + 'should mask textarea initial value and its changes from `maskAllInputs` and allow unmasked selector', + async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => { + // TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation. + if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + + await reqPromise0; + + const text = 'test'; + + await page.locator('#textarea').type(text); + await forceFlushReplay(); + const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); + const lastSnapshot = snapshots[snapshots.length - 1]; + expect(lastSnapshot.data.text).toBe('*'.repeat(text.length)); + + await page.locator('#textarea-unmasked').type(text); + await forceFlushReplay(); + const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); + const lastSnapshot2 = snapshots2[snapshots2.length - 1]; + expect(lastSnapshot2.data.text).toBe(text); + }, +); diff --git a/packages/integration-tests/utils/fixtures.ts b/packages/integration-tests/utils/fixtures.ts index 0d4c75353700..05ac906ad2d2 100644 --- a/packages/integration-tests/utils/fixtures.ts +++ b/packages/integration-tests/utils/fixtures.ts @@ -25,6 +25,7 @@ export type TestFixtures = { _autoSnapshotSuffix: void; testDir: string; getLocalTestPath: (options: { testDir: string }) => Promise; + forceFlushReplay: () => Promise; runInChromium: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown; runInFirefox: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown; runInWebkit: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown; @@ -92,6 +93,20 @@ const sentryTest = base.extend({ return fn(...args); }); }, + + forceFlushReplay: ({ page }, use) => { + return use(() => + page.evaluate(` + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); + `), + ); + }, }); export { sentryTest }; diff --git a/packages/integration-tests/utils/replayHelpers.ts b/packages/integration-tests/utils/replayHelpers.ts index c5c3e1f50ae9..46a2a9e9e89e 100644 --- a/packages/integration-tests/utils/replayHelpers.ts +++ b/packages/integration-tests/utils/replayHelpers.ts @@ -1,3 +1,5 @@ +import type { fullSnapshotEvent, incrementalSnapshotEvent } from '@sentry-internal/rrweb'; +import { EventType } from '@sentry-internal/rrweb'; import type { InternalEventContext, RecordingEvent, @@ -20,10 +22,18 @@ export type PerformanceSpan = { data: Record; }; -type RecordingSnapshot = eventWithTime & { +export type FullRecordingSnapshot = eventWithTime & { timestamp: 0; + data: fullSnapshotEvent['data']; }; +export type IncrementalRecordingSnapshot = eventWithTime & { + timestamp: 0; + data: incrementalSnapshotEvent['data']; +}; + +export type RecordingSnapshot = FullRecordingSnapshot | IncrementalRecordingSnapshot; + /** * Waits for a replay request to be sent by the page and returns it. * @@ -67,6 +77,14 @@ export function isReplayEvent(event: Event): event is ReplayEvent { return event.type === 'replay_event'; } +function isIncrementalSnapshot(event: RecordingEvent): event is IncrementalRecordingSnapshot { + return event.type === EventType.IncrementalSnapshot; +} + +function isFullSnapshot(event: RecordingEvent): event is FullRecordingSnapshot { + return event.type === EventType.FullSnapshot; +} + /** * This returns the replay container (assuming it exists). * Note that due to how this works with playwright, this is a POJO copy of replay. @@ -144,16 +162,16 @@ function getReplayPerformanceSpans(recordingEvents: RecordingEvent[]): Performan .map(data => data.payload) as PerformanceSpan[]; } -export function getFullRecordingSnapshots(resOrReq: Request | Response): RecordingSnapshot[] { +export function getFullRecordingSnapshots(resOrReq: Request | Response): FullRecordingSnapshot[] { const replayRequest = getRequest(resOrReq); const events = getDecompressedRecordingEvents(replayRequest); - return events.filter(event => event.type === 2); + return events.filter(isFullSnapshot); } -export function getIncrementalRecordingSnapshots(resOrReq: Request | Response): RecordingSnapshot[] { +export function getIncrementalRecordingSnapshots(resOrReq: Request | Response): IncrementalRecordingSnapshot[] { const replayRequest = getRequest(resOrReq); const events = getDecompressedRecordingEvents(replayRequest); - return events.filter(event => event.type === 3); + return events.filter(isIncrementalSnapshot); } function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] { @@ -166,7 +184,7 @@ function getDecompressedRecordingEvents(resOrReq: Request | Response): Recording event => typeof event.data === 'object' && event.data && (event.data as Record).source !== 1, ) .map(event => { - return { ...event, timestamp: 0 }; + return { ...event, timestamp: 0 } as RecordingSnapshot; }) ); } From d6d33710e4da01b94d3b8cfa24dd8b3be21c19cd Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 7 Mar 2023 16:44:58 +0100 Subject: [PATCH 2/4] ref: Use `fill` instead of `type` --- .../suites/replay/privacyInput/test.ts | 12 ++++++------ .../suites/replay/privacyInputMaskAll/test.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/integration-tests/suites/replay/privacyInput/test.ts b/packages/integration-tests/suites/replay/privacyInput/test.ts index 926578088675..f95e857d5637 100644 --- a/packages/integration-tests/suites/replay/privacyInput/test.ts +++ b/packages/integration-tests/suites/replay/privacyInput/test.ts @@ -45,19 +45,19 @@ sentryTest( const text = 'test'; - await page.locator('#input').type(text); + await page.locator('#input').fill(text); await forceFlushReplay(); const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); const lastSnapshot = snapshots[snapshots.length - 1]; expect(lastSnapshot.data.text).toBe(text); - await page.locator('#input-masked').type(text); + await page.locator('#input-masked').fill(text); await forceFlushReplay(); const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); const lastSnapshot2 = snapshots2[snapshots2.length - 1]; expect(lastSnapshot2.data.text).toBe('*'.repeat(text.length)); - await page.locator('#input-ignore').type(text); + await page.locator('#input-ignore').fill(text); await forceFlushReplay(); const snapshots3 = getIncrementalRecordingSnapshots(await reqPromise3).filter(isInputMutation); expect(snapshots3.length).toBe(0); @@ -91,19 +91,19 @@ sentryTest( await reqPromise0; const text = 'test'; - await page.locator('#textarea').type(text); + await page.locator('#textarea').fill(text); await forceFlushReplay(); const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); const lastSnapshot = snapshots[snapshots.length - 1]; expect(lastSnapshot.data.text).toBe(text); - await page.locator('#textarea-masked').type(text); + await page.locator('#textarea-masked').fill(text); await forceFlushReplay(); const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); const lastSnapshot2 = snapshots2[snapshots2.length - 1]; expect(lastSnapshot2.data.text).toBe('*'.repeat(text.length)); - await page.locator('#textarea-ignore').type(text); + await page.locator('#textarea-ignore').fill(text); await forceFlushReplay(); const snapshots3 = getIncrementalRecordingSnapshots(await reqPromise3).filter(isInputMutation); expect(snapshots3.length).toBe(0); diff --git a/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts b/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts index 41721c1afee9..3a4d7c39b076 100644 --- a/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts +++ b/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -44,13 +44,13 @@ sentryTest( const text = 'test'; - await page.locator('#input').type(text); + await page.locator('#input').fill(text); await forceFlushReplay(); const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); const lastSnapshot = snapshots[snapshots.length - 1]; expect(lastSnapshot.data.text).toBe('*'.repeat(text.length)); - await page.locator('#input-unmasked').type(text); + await page.locator('#input-unmasked').fill(text); await forceFlushReplay(); const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); const lastSnapshot2 = snapshots2[snapshots2.length - 1]; @@ -86,13 +86,13 @@ sentryTest( const text = 'test'; - await page.locator('#textarea').type(text); + await page.locator('#textarea').fill(text); await forceFlushReplay(); const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); const lastSnapshot = snapshots[snapshots.length - 1]; expect(lastSnapshot.data.text).toBe('*'.repeat(text.length)); - await page.locator('#textarea-unmasked').type(text); + await page.locator('#textarea-unmasked').fill(text); await forceFlushReplay(); const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation); const lastSnapshot2 = snapshots2[snapshots2.length - 1]; From e4da1f80ccd6fe565124fb579101060f60e83080 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 7 Mar 2023 16:59:43 +0100 Subject: [PATCH 3/4] refd: Improve breadcrumb handling --- packages/integration-tests/utils/replayHelpers.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/integration-tests/utils/replayHelpers.ts b/packages/integration-tests/utils/replayHelpers.ts index 46a2a9e9e89e..8d0c6531432f 100644 --- a/packages/integration-tests/utils/replayHelpers.ts +++ b/packages/integration-tests/utils/replayHelpers.ts @@ -85,6 +85,10 @@ function isFullSnapshot(event: RecordingEvent): event is FullRecordingSnapshot { return event.type === EventType.FullSnapshot; } +function isCustomSnapshot(event: RecordingEvent): event is RecordingEvent & { data: CustomRecordingEvent } { + return event.type === EventType.Custom; +} + /** * This returns the replay container (assuming it exists). * Note that due to how this works with playwright, this is a POJO copy of replay. @@ -146,10 +150,10 @@ export function getCustomRecordingEvents(resOrReq: Request | Response): CustomRe } function getAllCustomRrwebRecordingEvents(recordingEvents: RecordingEvent[]): CustomRecordingEvent[] { - return recordingEvents.filter(event => event.type === 5).map(event => event.data as CustomRecordingEvent); + return recordingEvents.filter(isCustomSnapshot).map(event => event.data); } -function getReplayBreadcrumbs(recordingEvents: RecordingEvent[], category?: string): Breadcrumb[] { +function getReplayBreadcrumbs(recordingEvents: RecordingSnapshot[], category?: string): Breadcrumb[] { return getAllCustomRrwebRecordingEvents(recordingEvents) .filter(data => data.tag === 'breadcrumb') .map(data => data.payload) From ea808177b06e5b244026be12625d990c2c7a8fba Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 8 Mar 2023 18:37:54 +0100 Subject: [PATCH 4/4] fix flaky test? run 100 times fix flaky? unrun 100 --- .../suites/replay/privacyInputMaskAll/test.ts | 44 ++++++++++++++++--- .../suites/replay/sessionExpiry/test.ts | 2 +- .../integration-tests/utils/replayHelpers.ts | 12 ++++- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts b/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts index 3a4d7c39b076..9b4470118422 100644 --- a/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts +++ b/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -24,9 +24,26 @@ sentryTest( sentryTest.skip(); } + // We want to ensure to check the correct event payloads + let firstInputMutationSegmentId: number | undefined = undefined; const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); - const reqPromise2 = waitForReplayRequest(page, 2); + const reqPromise1 = waitForReplayRequest(page, (event, res) => { + const check = + firstInputMutationSegmentId === undefined && getIncrementalRecordingSnapshots(res).some(isInputMutation); + + if (check) { + firstInputMutationSegmentId = event.segment_id; + } + + return check; + }); + const reqPromise2 = waitForReplayRequest(page, (event, res) => { + return ( + typeof firstInputMutationSegmentId === 'number' && + firstInputMutationSegmentId < event.segment_id && + getIncrementalRecordingSnapshots(res).some(isInputMutation) + ); + }); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ @@ -39,13 +56,13 @@ sentryTest( const url = await getLocalTestPath({ testDir: __dirname }); await page.goto(url); - await reqPromise0; const text = 'test'; await page.locator('#input').fill(text); await forceFlushReplay(); + const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation); const lastSnapshot = snapshots[snapshots.length - 1]; expect(lastSnapshot.data.text).toBe('*'.repeat(text.length)); @@ -66,9 +83,26 @@ sentryTest( sentryTest.skip(); } + // We want to ensure to check the correct event payloads + let firstInputMutationSegmentId: number | undefined = undefined; const reqPromise0 = waitForReplayRequest(page, 0); - const reqPromise1 = waitForReplayRequest(page, 1); - const reqPromise2 = waitForReplayRequest(page, 2); + const reqPromise1 = waitForReplayRequest(page, (event, res) => { + const check = + firstInputMutationSegmentId === undefined && getIncrementalRecordingSnapshots(res).some(isInputMutation); + + if (check) { + firstInputMutationSegmentId = event.segment_id; + } + + return check; + }); + const reqPromise2 = waitForReplayRequest(page, (event, res) => { + return ( + typeof firstInputMutationSegmentId === 'number' && + firstInputMutationSegmentId < event.segment_id && + getIncrementalRecordingSnapshots(res).some(isInputMutation) + ); + }); await page.route('https://dsn.ingest.sentry.io/**/*', route => { return route.fulfill({ diff --git a/packages/integration-tests/suites/replay/sessionExpiry/test.ts b/packages/integration-tests/suites/replay/sessionExpiry/test.ts index c574ac570ec6..d817e7175840 100644 --- a/packages/integration-tests/suites/replay/sessionExpiry/test.ts +++ b/packages/integration-tests/suites/replay/sessionExpiry/test.ts @@ -14,7 +14,7 @@ import { // Session should expire after 2s - keep in sync with init.js const SESSION_TIMEOUT = 2000; -sentryTest('handles an expired session RUN', async ({ getLocalTestPath, page }) => { +sentryTest('handles an expired session', async ({ getLocalTestPath, page }) => { if (shouldSkipReplayTest()) { sentryTest.skip(); } diff --git a/packages/integration-tests/utils/replayHelpers.ts b/packages/integration-tests/utils/replayHelpers.ts index 8d0c6531432f..8722fe245a23 100644 --- a/packages/integration-tests/utils/replayHelpers.ts +++ b/packages/integration-tests/utils/replayHelpers.ts @@ -46,7 +46,13 @@ export type RecordingSnapshot = FullRecordingSnapshot | IncrementalRecordingSnap * @param segmentId the segment_id of the replay event * @returns */ -export function waitForReplayRequest(page: Page, segmentId?: number): Promise { +export function waitForReplayRequest( + page: Page, + segmentIdOrCallback?: number | ((event: ReplayEvent, res: Response) => boolean), +): Promise { + const segmentId = typeof segmentIdOrCallback === 'number' ? segmentIdOrCallback : undefined; + const callback = typeof segmentIdOrCallback === 'function' ? segmentIdOrCallback : undefined; + return page.waitForResponse(res => { const req = res.request(); @@ -62,6 +68,10 @@ export function waitForReplayRequest(page: Page, segmentId?: number): Promise