Skip to content

Commit c192c9a

Browse files
billyvgmydea
andauthored
test(replay): Add integration tests for input masking on change (#7260)
--------- Co-authored-by: Francesco Novy <[email protected]>
1 parent 67a0dcf commit c192c9a

File tree

9 files changed

+369
-10
lines changed

9 files changed

+369
-10
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { Replay } from '@sentry/replay';
3+
4+
window.Sentry = Sentry;
5+
window.Replay = new Replay({
6+
flushMinDelay: 200,
7+
flushMaxDelay: 200,
8+
useCompression: false,
9+
maskAllInputs: false,
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 0,
15+
replaysSessionSampleRate: 1.0,
16+
replaysOnErrorSampleRate: 0.0,
17+
18+
integrations: [window.Replay],
19+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<input id="input" />
8+
<input id="input-masked" data-sentry-mask />
9+
<input id="input-ignore" data-sentry-ignore />
10+
11+
<textarea id="textarea"></textarea>
12+
<textarea id="textarea-masked" data-sentry-mask></textarea>
13+
<textarea id="textarea-ignore" data-sentry-ignore></textarea>
14+
</body>
15+
</html>
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { expect } from '@playwright/test';
2+
import { IncrementalSource } from '@sentry-internal/rrweb';
3+
import type { inputData } from '@sentry-internal/rrweb/typings/types';
4+
5+
import { sentryTest } from '../../../utils/fixtures';
6+
import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers';
7+
import {
8+
getIncrementalRecordingSnapshots,
9+
shouldSkipReplayTest,
10+
waitForReplayRequest,
11+
} from '../../../utils/replayHelpers';
12+
13+
function isInputMutation(
14+
snap: IncrementalRecordingSnapshot,
15+
): snap is IncrementalRecordingSnapshot & { data: inputData } {
16+
return snap.data.source == IncrementalSource.Input;
17+
}
18+
19+
sentryTest(
20+
'should mask input initial value and its changes',
21+
async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => {
22+
// TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation.
23+
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
24+
sentryTest.skip();
25+
}
26+
27+
const reqPromise0 = waitForReplayRequest(page, 0);
28+
const reqPromise1 = waitForReplayRequest(page, 1);
29+
const reqPromise2 = waitForReplayRequest(page, 2);
30+
const reqPromise3 = waitForReplayRequest(page, 3);
31+
32+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
33+
return route.fulfill({
34+
status: 200,
35+
contentType: 'application/json',
36+
body: JSON.stringify({ id: 'test-id' }),
37+
});
38+
});
39+
40+
const url = await getLocalTestPath({ testDir: __dirname });
41+
42+
await page.goto(url);
43+
44+
await reqPromise0;
45+
46+
const text = 'test';
47+
48+
await page.locator('#input').fill(text);
49+
await forceFlushReplay();
50+
const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation);
51+
const lastSnapshot = snapshots[snapshots.length - 1];
52+
expect(lastSnapshot.data.text).toBe(text);
53+
54+
await page.locator('#input-masked').fill(text);
55+
await forceFlushReplay();
56+
const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation);
57+
const lastSnapshot2 = snapshots2[snapshots2.length - 1];
58+
expect(lastSnapshot2.data.text).toBe('*'.repeat(text.length));
59+
60+
await page.locator('#input-ignore').fill(text);
61+
await forceFlushReplay();
62+
const snapshots3 = getIncrementalRecordingSnapshots(await reqPromise3).filter(isInputMutation);
63+
expect(snapshots3.length).toBe(0);
64+
},
65+
);
66+
67+
sentryTest(
68+
'should mask textarea initial value and its changes',
69+
async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => {
70+
// TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation.
71+
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
72+
sentryTest.skip();
73+
}
74+
75+
const reqPromise0 = waitForReplayRequest(page, 0);
76+
const reqPromise1 = waitForReplayRequest(page, 1);
77+
const reqPromise2 = waitForReplayRequest(page, 2);
78+
const reqPromise3 = waitForReplayRequest(page, 3);
79+
80+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
81+
return route.fulfill({
82+
status: 200,
83+
contentType: 'application/json',
84+
body: JSON.stringify({ id: 'test-id' }),
85+
});
86+
});
87+
88+
const url = await getLocalTestPath({ testDir: __dirname });
89+
90+
await page.goto(url);
91+
await reqPromise0;
92+
93+
const text = 'test';
94+
await page.locator('#textarea').fill(text);
95+
await forceFlushReplay();
96+
const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation);
97+
const lastSnapshot = snapshots[snapshots.length - 1];
98+
expect(lastSnapshot.data.text).toBe(text);
99+
100+
await page.locator('#textarea-masked').fill(text);
101+
await forceFlushReplay();
102+
const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation);
103+
const lastSnapshot2 = snapshots2[snapshots2.length - 1];
104+
expect(lastSnapshot2.data.text).toBe('*'.repeat(text.length));
105+
106+
await page.locator('#textarea-ignore').fill(text);
107+
await forceFlushReplay();
108+
const snapshots3 = getIncrementalRecordingSnapshots(await reqPromise3).filter(isInputMutation);
109+
expect(snapshots3.length).toBe(0);
110+
},
111+
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { Replay } from '@sentry/replay';
3+
4+
window.Sentry = Sentry;
5+
window.Replay = new Replay({
6+
flushMinDelay: 200,
7+
flushMaxDelay: 200,
8+
useCompression: false,
9+
maskAllInputs: true,
10+
});
11+
12+
Sentry.init({
13+
dsn: 'https://[email protected]/1337',
14+
sampleRate: 0,
15+
replaysSessionSampleRate: 1.0,
16+
replaysOnErrorSampleRate: 0.0,
17+
18+
integrations: [window.Replay],
19+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<input id="input" />
8+
<input id="input-unmasked" data-sentry-unmask />
9+
10+
<textarea id="textarea"></textarea>
11+
<textarea id="textarea-unmasked" data-sentry-unmask></textarea>
12+
</body>
13+
</html>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { expect } from '@playwright/test';
2+
import { IncrementalSource } from '@sentry-internal/rrweb';
3+
import type { inputData } from '@sentry-internal/rrweb/typings/types';
4+
5+
import { sentryTest } from '../../../utils/fixtures';
6+
import type { IncrementalRecordingSnapshot } from '../../../utils/replayHelpers';
7+
import {
8+
getIncrementalRecordingSnapshots,
9+
shouldSkipReplayTest,
10+
waitForReplayRequest,
11+
} from '../../../utils/replayHelpers';
12+
13+
function isInputMutation(
14+
snap: IncrementalRecordingSnapshot,
15+
): snap is IncrementalRecordingSnapshot & { data: inputData } {
16+
return snap.data.source == IncrementalSource.Input;
17+
}
18+
19+
sentryTest(
20+
'should mask input initial value and its changes from `maskAllInputs` and allow unmasked selector',
21+
async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => {
22+
// TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation.
23+
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
24+
sentryTest.skip();
25+
}
26+
27+
// We want to ensure to check the correct event payloads
28+
let firstInputMutationSegmentId: number | undefined = undefined;
29+
const reqPromise0 = waitForReplayRequest(page, 0);
30+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
31+
const check =
32+
firstInputMutationSegmentId === undefined && getIncrementalRecordingSnapshots(res).some(isInputMutation);
33+
34+
if (check) {
35+
firstInputMutationSegmentId = event.segment_id;
36+
}
37+
38+
return check;
39+
});
40+
const reqPromise2 = waitForReplayRequest(page, (event, res) => {
41+
return (
42+
typeof firstInputMutationSegmentId === 'number' &&
43+
firstInputMutationSegmentId < event.segment_id &&
44+
getIncrementalRecordingSnapshots(res).some(isInputMutation)
45+
);
46+
});
47+
48+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
49+
return route.fulfill({
50+
status: 200,
51+
contentType: 'application/json',
52+
body: JSON.stringify({ id: 'test-id' }),
53+
});
54+
});
55+
56+
const url = await getLocalTestPath({ testDir: __dirname });
57+
58+
await page.goto(url);
59+
await reqPromise0;
60+
61+
const text = 'test';
62+
63+
await page.locator('#input').fill(text);
64+
await forceFlushReplay();
65+
66+
const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation);
67+
const lastSnapshot = snapshots[snapshots.length - 1];
68+
expect(lastSnapshot.data.text).toBe('*'.repeat(text.length));
69+
70+
await page.locator('#input-unmasked').fill(text);
71+
await forceFlushReplay();
72+
const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation);
73+
const lastSnapshot2 = snapshots2[snapshots2.length - 1];
74+
expect(lastSnapshot2.data.text).toBe(text);
75+
},
76+
);
77+
78+
sentryTest(
79+
'should mask textarea initial value and its changes from `maskAllInputs` and allow unmasked selector',
80+
async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => {
81+
// TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation.
82+
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
83+
sentryTest.skip();
84+
}
85+
86+
// We want to ensure to check the correct event payloads
87+
let firstInputMutationSegmentId: number | undefined = undefined;
88+
const reqPromise0 = waitForReplayRequest(page, 0);
89+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
90+
const check =
91+
firstInputMutationSegmentId === undefined && getIncrementalRecordingSnapshots(res).some(isInputMutation);
92+
93+
if (check) {
94+
firstInputMutationSegmentId = event.segment_id;
95+
}
96+
97+
return check;
98+
});
99+
const reqPromise2 = waitForReplayRequest(page, (event, res) => {
100+
return (
101+
typeof firstInputMutationSegmentId === 'number' &&
102+
firstInputMutationSegmentId < event.segment_id &&
103+
getIncrementalRecordingSnapshots(res).some(isInputMutation)
104+
);
105+
});
106+
107+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
108+
return route.fulfill({
109+
status: 200,
110+
contentType: 'application/json',
111+
body: JSON.stringify({ id: 'test-id' }),
112+
});
113+
});
114+
115+
const url = await getLocalTestPath({ testDir: __dirname });
116+
117+
await page.goto(url);
118+
119+
await reqPromise0;
120+
121+
const text = 'test';
122+
123+
await page.locator('#textarea').fill(text);
124+
await forceFlushReplay();
125+
const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation);
126+
const lastSnapshot = snapshots[snapshots.length - 1];
127+
expect(lastSnapshot.data.text).toBe('*'.repeat(text.length));
128+
129+
await page.locator('#textarea-unmasked').fill(text);
130+
await forceFlushReplay();
131+
const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation);
132+
const lastSnapshot2 = snapshots2[snapshots2.length - 1];
133+
expect(lastSnapshot2.data.text).toBe(text);
134+
},
135+
);

packages/integration-tests/suites/replay/sessionExpiry/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
// Session should expire after 2s - keep in sync with init.js
1515
const SESSION_TIMEOUT = 2000;
1616

17-
sentryTest('handles an expired session RUN', async ({ getLocalTestPath, page }) => {
17+
sentryTest('handles an expired session', async ({ getLocalTestPath, page }) => {
1818
if (shouldSkipReplayTest()) {
1919
sentryTest.skip();
2020
}

packages/integration-tests/utils/fixtures.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type TestFixtures = {
2525
_autoSnapshotSuffix: void;
2626
testDir: string;
2727
getLocalTestPath: (options: { testDir: string }) => Promise<string>;
28+
forceFlushReplay: () => Promise<string>;
2829
runInChromium: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown;
2930
runInFirefox: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown;
3031
runInWebkit: (fn: (...args: unknown[]) => unknown, args?: unknown[]) => unknown;
@@ -92,6 +93,20 @@ const sentryTest = base.extend<TestFixtures>({
9293
return fn(...args);
9394
});
9495
},
96+
97+
forceFlushReplay: ({ page }, use) => {
98+
return use(() =>
99+
page.evaluate(`
100+
Object.defineProperty(document, 'visibilityState', {
101+
configurable: true,
102+
get: function () {
103+
return 'hidden';
104+
},
105+
});
106+
document.dispatchEvent(new Event('visibilitychange'));
107+
`),
108+
);
109+
},
95110
});
96111

97112
export { sentryTest };

0 commit comments

Comments
 (0)