Skip to content

Commit 7d73dbb

Browse files
committed
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 08967e2. skip webkit
1 parent 2b44452 commit 7d73dbb

File tree

8 files changed

+317
-6
lines changed

8 files changed

+317
-6
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').type(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').type(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').type(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').type(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').type(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').type(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: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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+
const reqPromise0 = waitForReplayRequest(page, 0);
28+
const reqPromise1 = waitForReplayRequest(page, 1);
29+
const reqPromise2 = waitForReplayRequest(page, 2);
30+
31+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
32+
return route.fulfill({
33+
status: 200,
34+
contentType: 'application/json',
35+
body: JSON.stringify({ id: 'test-id' }),
36+
});
37+
});
38+
39+
const url = await getLocalTestPath({ testDir: __dirname });
40+
41+
await page.goto(url);
42+
43+
await reqPromise0;
44+
45+
const text = 'test';
46+
47+
await page.locator('#input').type(text);
48+
await forceFlushReplay();
49+
const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation);
50+
const lastSnapshot = snapshots[snapshots.length - 1];
51+
expect(lastSnapshot.data.text).toBe('*'.repeat(text.length));
52+
53+
await page.locator('#input-unmasked').type(text);
54+
await forceFlushReplay();
55+
const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation);
56+
const lastSnapshot2 = snapshots2[snapshots2.length - 1];
57+
expect(lastSnapshot2.data.text).toBe(text);
58+
},
59+
);
60+
61+
sentryTest(
62+
'should mask textarea initial value and its changes from `maskAllInputs` and allow unmasked selector',
63+
async ({ browserName, forceFlushReplay, getLocalTestPath, page }) => {
64+
// TODO(replay): This is flakey on firefox and webkit (~1%) where we do not always get the latest mutation.
65+
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
66+
sentryTest.skip();
67+
}
68+
69+
const reqPromise0 = waitForReplayRequest(page, 0);
70+
const reqPromise1 = waitForReplayRequest(page, 1);
71+
const reqPromise2 = waitForReplayRequest(page, 2);
72+
73+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
74+
return route.fulfill({
75+
status: 200,
76+
contentType: 'application/json',
77+
body: JSON.stringify({ id: 'test-id' }),
78+
});
79+
});
80+
81+
const url = await getLocalTestPath({ testDir: __dirname });
82+
83+
await page.goto(url);
84+
85+
await reqPromise0;
86+
87+
const text = 'test';
88+
89+
await page.locator('#textarea').type(text);
90+
await forceFlushReplay();
91+
const snapshots = getIncrementalRecordingSnapshots(await reqPromise1).filter(isInputMutation);
92+
const lastSnapshot = snapshots[snapshots.length - 1];
93+
expect(lastSnapshot.data.text).toBe('*'.repeat(text.length));
94+
95+
await page.locator('#textarea-unmasked').type(text);
96+
await forceFlushReplay();
97+
const snapshots2 = getIncrementalRecordingSnapshots(await reqPromise2).filter(isInputMutation);
98+
const lastSnapshot2 = snapshots2[snapshots2.length - 1];
99+
expect(lastSnapshot2.data.text).toBe(text);
100+
},
101+
);

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 };

packages/integration-tests/utils/replayHelpers.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { fullSnapshotEvent, incrementalSnapshotEvent } from '@sentry-internal/rrweb';
2+
import { EventType } from '@sentry-internal/rrweb';
13
import type {
24
InternalEventContext,
35
RecordingEvent,
@@ -20,10 +22,18 @@ export type PerformanceSpan = {
2022
data: Record<string, number>;
2123
};
2224

23-
type RecordingSnapshot = eventWithTime & {
25+
export type FullRecordingSnapshot = eventWithTime & {
2426
timestamp: 0;
27+
data: fullSnapshotEvent['data'];
2528
};
2629

30+
export type IncrementalRecordingSnapshot = eventWithTime & {
31+
timestamp: 0;
32+
data: incrementalSnapshotEvent['data'];
33+
};
34+
35+
export type RecordingSnapshot = FullRecordingSnapshot | IncrementalRecordingSnapshot;
36+
2737
/**
2838
* Waits for a replay request to be sent by the page and returns it.
2939
*
@@ -67,6 +77,14 @@ export function isReplayEvent(event: Event): event is ReplayEvent {
6777
return event.type === 'replay_event';
6878
}
6979

80+
function isIncrementalSnapshot(event: RecordingEvent): event is IncrementalRecordingSnapshot {
81+
return event.type === EventType.IncrementalSnapshot;
82+
}
83+
84+
function isFullSnapshot(event: RecordingEvent): event is FullRecordingSnapshot {
85+
return event.type === EventType.FullSnapshot;
86+
}
87+
7088
/**
7189
* This returns the replay container (assuming it exists).
7290
* 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
144162
.map(data => data.payload) as PerformanceSpan[];
145163
}
146164

147-
export function getFullRecordingSnapshots(resOrReq: Request | Response): RecordingSnapshot[] {
165+
export function getFullRecordingSnapshots(resOrReq: Request | Response): FullRecordingSnapshot[] {
148166
const replayRequest = getRequest(resOrReq);
149167
const events = getDecompressedRecordingEvents(replayRequest);
150-
return events.filter(event => event.type === 2);
168+
return events.filter(isFullSnapshot);
151169
}
152170

153-
export function getIncrementalRecordingSnapshots(resOrReq: Request | Response): RecordingSnapshot[] {
171+
export function getIncrementalRecordingSnapshots(resOrReq: Request | Response): IncrementalRecordingSnapshot[] {
154172
const replayRequest = getRequest(resOrReq);
155173
const events = getDecompressedRecordingEvents(replayRequest);
156-
return events.filter(event => event.type === 3);
174+
return events.filter(isIncrementalSnapshot);
157175
}
158176

159177
function getDecompressedRecordingEvents(resOrReq: Request | Response): RecordingSnapshot[] {
@@ -166,7 +184,7 @@ function getDecompressedRecordingEvents(resOrReq: Request | Response): Recording
166184
event => typeof event.data === 'object' && event.data && (event.data as Record<string, unknown>).source !== 1,
167185
)
168186
.map(event => {
169-
return { ...event, timestamp: 0 };
187+
return { ...event, timestamp: 0 } as RecordingSnapshot;
170188
})
171189
);
172190
}

0 commit comments

Comments
 (0)