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..f95e857d5637 --- /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').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').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').fill(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').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').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').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/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..9b4470118422 --- /dev/null +++ b/packages/integration-tests/suites/replay/privacyInputMaskAll/test.ts @@ -0,0 +1,135 @@ +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(); + } + + // We want to ensure to check the correct event payloads + let firstInputMutationSegmentId: number | undefined = undefined; + const reqPromise0 = waitForReplayRequest(page, 0); + 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({ + 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').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').fill(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(); + } + + // We want to ensure to check the correct event payloads + let firstInputMutationSegmentId: number | undefined = undefined; + const reqPromise0 = waitForReplayRequest(page, 0); + 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({ + 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').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').fill(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/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/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