Skip to content

Commit ceb4f4e

Browse files
authored
Merge 1269102 into 3269a06
2 parents 3269a06 + 1269102 commit ceb4f4e

File tree

11 files changed

+248
-33
lines changed

11 files changed

+248
-33
lines changed

packages/browser-integration-tests/suites/replay/customEvents/init.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ window.Replay = new Sentry.Replay({
55
flushMinDelay: 500,
66
flushMaxDelay: 500,
77
useCompression: false,
8+
blockAllMedia: false,
89
});
910

1011
Sentry.init({

packages/browser-integration-tests/suites/replay/customEvents/template.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44
<meta charset="utf-8" />
55
</head>
66
<body>
7-
<button id="go-background">New Tab</button>
7+
<div role="button" id="error" class="btn btn-error" aria-label="An Error">An Error</div>
8+
<button>
9+
<img id="img"
10+
alt="Alt Text"
11+
/>
12+
</button>
13+
<button class="sentry-unmask" aria-label="Unmasked label">
14+
Unmasked
15+
</button>
816
</body>
917
</html>

packages/browser-integration-tests/suites/replay/customEvents/test.ts

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type { PerformanceSpan } from '../../../utils/replayHelpers';
1414
import {
1515
getCustomRecordingEvents,
1616
getReplayEvent,
17+
getReplayRecordingContent,
1718
shouldSkipReplayTest,
1819
waitForReplayRequest,
1920
} from '../../../utils/replayHelpers';
@@ -81,8 +82,9 @@ sentryTest(
8182

8283
sentryTest(
8384
'replay recording should contain a click breadcrumb when a button is clicked',
84-
async ({ getLocalTestPath, page }) => {
85-
if (shouldSkipReplayTest()) {
85+
async ({ forceFlushReplay, getLocalTestPath, page, browserName }) => {
86+
// TODO(replay): This is flakey on firefox and webkit where clicks are flakey
87+
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
8688
sentryTest.skip();
8789
}
8890

@@ -100,21 +102,80 @@ sentryTest(
100102
const url = await getLocalTestPath({ testDir: __dirname });
101103

102104
await page.goto(url);
103-
const replayEvent0 = getReplayEvent(await reqPromise0);
104-
const { breadcrumbs: breadcrumbs0 } = getCustomRecordingEvents(await reqPromise0);
105-
106-
expect(replayEvent0).toEqual(getExpectedReplayEvent({ segment_id: 0 }));
107-
expect(breadcrumbs0.length).toEqual(0);
108-
109-
await page.click('button');
110-
111-
const replayEvent1 = getReplayEvent(await reqPromise1);
112-
const { breadcrumbs: breadcrumbs1 } = getCustomRecordingEvents(await reqPromise1);
105+
await reqPromise0;
106+
107+
await page.click('#error');
108+
await page.click('#img');
109+
await page.click('.sentry-unmask');
110+
await forceFlushReplay();
111+
const req1 = await reqPromise1;
112+
const content1 = getReplayRecordingContent(req1);
113+
expect(content1.breadcrumbs).toEqual(
114+
expect.arrayContaining([
115+
{
116+
...expectedClickBreadcrumb,
117+
message: 'body > div#error.btn.btn-error[aria-label="An Error"]',
118+
data: {
119+
nodeId: expect.any(Number),
120+
node: {
121+
attributes: {
122+
'aria-label': '** *****',
123+
class: 'btn btn-error',
124+
id: 'error',
125+
role: 'button',
126+
},
127+
id: expect.any(Number),
128+
tagName: 'div',
129+
textContent: '** *****',
130+
},
131+
},
132+
},
133+
]),
134+
);
113135

114-
expect(replayEvent1).toEqual(
115-
getExpectedReplayEvent({ segment_id: 1, urls: [], replay_start_timestamp: undefined }),
136+
expect(content1.breadcrumbs).toEqual(
137+
expect.arrayContaining([
138+
{
139+
...expectedClickBreadcrumb,
140+
message: 'body > button > img#img[alt="Alt Text"]',
141+
data: {
142+
nodeId: expect.any(Number),
143+
node: {
144+
attributes: {
145+
alt: 'Alt Text',
146+
id: 'img',
147+
},
148+
id: expect.any(Number),
149+
tagName: 'img',
150+
textContent: '',
151+
},
152+
},
153+
},
154+
]),
116155
);
117156

118-
expect(breadcrumbs1).toEqual([expectedClickBreadcrumb]);
157+
expect(content1.breadcrumbs).toEqual(
158+
expect.arrayContaining([
159+
{
160+
...expectedClickBreadcrumb,
161+
message: 'body > button.sentry-unmask[aria-label="Unmasked label"]',
162+
data: {
163+
nodeId: expect.any(Number),
164+
node: {
165+
attributes: {
166+
// TODO(rrweb): This is a bug in our rrweb fork!
167+
// This attribute should be unmasked.
168+
// 'aria-label': 'Unmasked label',
169+
'aria-label': '******** *****',
170+
class: 'sentry-unmask',
171+
},
172+
id: expect.any(Number),
173+
tagName: 'button',
174+
textContent: 'Unmasked',
175+
},
176+
},
177+
},
178+
]),
179+
);
119180
},
120181
);

packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,17 @@ sentryTest(
9999
{
100100
...expectedClickBreadcrumb,
101101
message: 'body > button#error',
102+
data: {
103+
nodeId: expect.any(Number),
104+
node: {
105+
attributes: {
106+
id: 'error',
107+
},
108+
id: expect.any(Number),
109+
tagName: 'button',
110+
textContent: '***** *****',
111+
},
112+
},
102113
},
103114
]),
104115
);
@@ -131,7 +142,19 @@ sentryTest(
131142

132143
expect(content2.breadcrumbs).toEqual(
133144
expect.arrayContaining([
134-
{ ...expectedClickBreadcrumb, message: 'body > button#log' },
145+
{
146+
...expectedClickBreadcrumb,
147+
message: 'body > button#log',
148+
data: {
149+
node: {
150+
attributes: { id: 'log' },
151+
id: expect.any(Number),
152+
tagName: 'button',
153+
textContent: '*** ***** ** *** *******',
154+
},
155+
nodeId: expect.any(Number),
156+
},
157+
},
135158
{ ...expectedConsoleBreadcrumb, level: 'log', message: 'Some message' },
136159
]),
137160
);

packages/browser-integration-tests/suites/replay/errors/errorsInSession/test.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import {
1212

1313
sentryTest(
1414
'[session-mode] replay event should contain an error id of an error that occurred during session recording',
15-
async ({ getLocalTestPath, page }) => {
16-
if (shouldSkipReplayTest()) {
15+
async ({ getLocalTestPath, page, browserName }) => {
16+
// TODO(replay): This is flakey on firefox where clicks are flakey
17+
if (shouldSkipReplayTest() || ['firefox'].includes(browserName)) {
1718
sentryTest.skip();
1819
}
1920

@@ -62,7 +63,23 @@ sentryTest(
6263
);
6364

6465
expect(content1.breadcrumbs).toEqual(
65-
expect.arrayContaining([{ ...expectedClickBreadcrumb, message: 'body > button#error' }]),
66+
expect.arrayContaining([
67+
{
68+
...expectedClickBreadcrumb,
69+
message: 'body > button#error',
70+
data: {
71+
node: {
72+
attributes: {
73+
id: 'error',
74+
},
75+
id: expect.any(Number),
76+
tagName: 'button',
77+
textContent: '***** *****',
78+
},
79+
nodeId: expect.any(Number),
80+
},
81+
},
82+
]),
6683
);
6784
},
6885
);
@@ -108,7 +125,23 @@ sentryTest(
108125

109126
// The button click that triggered the error should still be recorded
110127
expect(content1.breadcrumbs).toEqual(
111-
expect.arrayContaining([{ ...expectedClickBreadcrumb, message: 'body > button#drop' }]),
128+
expect.arrayContaining([
129+
{
130+
...expectedClickBreadcrumb,
131+
message: 'body > button#drop',
132+
data: {
133+
node: {
134+
attributes: {
135+
id: 'drop',
136+
},
137+
id: expect.any(Number),
138+
tagName: 'button',
139+
textContent: '***** ***** *** **** **',
140+
},
141+
nodeId: expect.any(Number),
142+
},
143+
},
144+
]),
112145
);
113146
},
114147
);

packages/browser-integration-tests/utils/replayEventTemplates.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ export const expectedClickBreadcrumb = {
161161
message: expect.any(String),
162162
data: {
163163
nodeId: expect.any(Number),
164+
node: {
165+
attributes: {
166+
id: expect.any(String),
167+
},
168+
id: expect.any(Number),
169+
tagName: expect.any(String),
170+
textContent: expect.any(String),
171+
},
164172
},
165173
};
166174

packages/replay/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@babel/core": "^7.17.5",
4646
"@sentry-internal/replay-worker": "7.43.0",
4747
"@sentry-internal/rrweb": "1.105.0",
48+
"@sentry-internal/rrweb-snapshot": "1.105.0",
4849
"jsdom-worker": "^0.2.1",
4950
"tslib": "^1.9.3"
5051
},

packages/replay/src/coreHandlers/handleDom.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import { record } from '@sentry-internal/rrweb';
1+
import type { INode } from '@sentry-internal/rrweb-snapshot';
2+
import { NodeType } from '@sentry-internal/rrweb-snapshot';
23
import type { Breadcrumb } from '@sentry/types';
34
import { htmlTreeAsString } from '@sentry/utils';
45

56
import type { ReplayContainer } from '../types';
67
import { createBreadcrumb } from '../util/createBreadcrumb';
78
import { addBreadcrumbEvent } from './addBreadcrumbEvent';
9+
import { getAttributesToRecord } from './util/getAttributesToRecord';
810

911
interface DomHandlerData {
1012
name: string;
@@ -31,9 +33,8 @@ export const handleDomListener: (replay: ReplayContainer) => (handlerData: DomHa
3133
* An event handler to react to DOM events.
3234
*/
3335
function handleDom(handlerData: DomHandlerData): Breadcrumb | null {
34-
// Taken from https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/integrations/breadcrumbs.ts#L112
3536
let target;
36-
let targetNode;
37+
let targetNode: Node | INode | undefined;
3738

3839
// Accessing event.target can throw (see getsentry/raven-js#838, #768)
3940
try {
@@ -43,18 +44,32 @@ function handleDom(handlerData: DomHandlerData): Breadcrumb | null {
4344
target = '<unknown>';
4445
}
4546

46-
if (target.length === 0) {
47-
return null;
48-
}
47+
// `__sn` property is the serialized node created by rrweb
48+
const serializedNode =
49+
targetNode && '__sn' in targetNode && targetNode.__sn.type === NodeType.Element ? targetNode.__sn : null;
4950

5051
return createBreadcrumb({
5152
category: `ui.${handlerData.name}`,
5253
message: target,
53-
data: {
54-
// Not sure why this errors, Node should be correct (Argument of type 'Node' is not assignable to parameter of type 'INode')
55-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
56-
...(targetNode ? { nodeId: record.mirror.getId(targetNode as any) } : {}),
57-
},
54+
data: serializedNode
55+
? {
56+
nodeId: serializedNode.id,
57+
node: {
58+
id: serializedNode.id,
59+
tagName: serializedNode.tagName,
60+
textContent: targetNode
61+
? Array.from(targetNode.childNodes)
62+
.map(
63+
(node: Node | INode) => '__sn' in node && node.__sn.type === NodeType.Text && node.__sn.textContent,
64+
)
65+
.filter(Boolean) // filter out empty values
66+
.map(text => (text as string).trim())
67+
.join('')
68+
: '',
69+
attributes: getAttributesToRecord(serializedNode.attributes),
70+
},
71+
}
72+
: {},
5873
});
5974
}
6075

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Note that these are the serialized attributes and not attributes directly on
2+
// the DOM Node. Attributes we are interested in:
3+
const ATTRIBUTES_TO_RECORD = new Set([
4+
'id',
5+
'class',
6+
'aria-label',
7+
'role',
8+
'name',
9+
'alt',
10+
'title',
11+
'data-test-id',
12+
'data-testid',
13+
]);
14+
15+
/**
16+
* Inclusion list of attributes that we want to record from the DOM element
17+
*/
18+
export function getAttributesToRecord(attributes: Record<string, unknown>): Record<string, unknown> {
19+
const obj: Record<string, unknown> = {};
20+
for (const key in attributes) {
21+
if (ATTRIBUTES_TO_RECORD.has(key)) {
22+
let normalizedKey = key;
23+
24+
if (key === 'data-testid' || key === 'data-test-id') {
25+
normalizedKey = 'testId';
26+
}
27+
28+
obj[normalizedKey] = attributes[key];
29+
}
30+
}
31+
32+
return obj;
33+
}

packages/replay/test/integration/errorSampleRate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe('Integration | errorSampleRate', () => {
5454
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
5555
expect(replay).not.toHaveLastSentReplay();
5656

57-
// Does not capture mouse click
57+
// Does not capture on mouse click
5858
domHandler({
5959
name: 'click',
6060
});

0 commit comments

Comments
 (0)