Skip to content

Commit 6a76525

Browse files
authored
feat(replay): Use data sentry element as fallback for the component name (#11383)
Adds element name as fallback. If component name exists, use that, else if element name exists use that, else use selector name. The element name and component name mirrors that of [React components vs elements](https://legacy.reactjs.org/blog/2015/12/18/react-components-elements-and-instances.html) Example with Replay Play/Pause Button: | Name Type | HTML Tree| -------------|------------| | Component Name | none > none > ReplayControls > ButtonBar > none | | Element Name | FullViewport > none > ButtonGrid > ButtonGrid > ReplayPlayPauseButton | | Element + Component fallback | FullViewPort > none > ButtonGrid > ButtonGrid > ReplayPlayPauseButton | | Component + Element fallback | FullViewPort > none > ReplayControls > ButtonBar > ReplayPlayPauseButton | More elements have an `data-sentry-element` attribute than `data-sentry-component` attribute, so adding in the element name means more elements in the tree will have a descriptive name. Furthermore, the component name usually provides more context (eg. ReplayControls vs ButtonGrid) so it should take precedence over the element name Relates to getsentry/sentry#64673
1 parent 3c07bcb commit 6a76525

File tree

12 files changed

+187
-13
lines changed

12 files changed

+187
-13
lines changed

dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<body>
88
<button id="button1" type="button">Button 1</button>
99
<button id="button2" type="button">Button 2</button>
10-
<button id="annotated-button" type="button" data-sentry-component="AnnotatedButton">Button 3</button>
10+
<button id="annotated-button" type="button" data-sentry-component="AnnotatedButton" data-sentry-element="StyledButton">Button 3</button>
11+
<button id="annotated-button-2" type="button" data-sentry-element="StyledButton">Button 4</button>
1112
</body>
1213
</html>

dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ sentryTest(
7878

7979
await page.goto(url);
8080
await page.locator('#annotated-button').click();
81+
await page.locator('#annotated-button-2').click();
8182

8283
const [eventData] = await Promise.all([promise, page.evaluate('Sentry.captureException("test exception")')]);
8384

@@ -88,6 +89,12 @@ sentryTest(
8889
message: 'body > AnnotatedButton',
8990
data: { 'ui.component_name': 'AnnotatedButton' },
9091
},
92+
{
93+
timestamp: expect.any(Number),
94+
category: 'ui.click',
95+
message: 'body > StyledButton',
96+
data: { 'ui.component_name': 'StyledButton' },
97+
},
9198
]);
9299
},
93100
);

dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<body>
88
<input id="input1" type="text" />
99
<input id="input2" type="text" />
10-
<input id="annotated-input" data-sentry-component="AnnotatedInput" type="text" />
10+
<input id="annotated-input" data-sentry-component="AnnotatedInput" data-sentry-element="StyledInput" type="text" />
11+
<input id="annotated-input-2" data-sentry-element="StyledInput" type="text" />
1112
</body>
1213
</html>

dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ sentryTest(
8383
await page.goto(url);
8484

8585
await page.locator('#annotated-input').pressSequentially('John', { delay: 1 });
86+
await page.locator('#annotated-input-2').pressSequentially('John', { delay: 1 });
8687

8788
await page.evaluate('Sentry.captureException("test exception")');
8889
const eventData = await promise;
@@ -95,6 +96,12 @@ sentryTest(
9596
message: 'body > AnnotatedInput',
9697
data: { 'ui.component_name': 'AnnotatedInput' },
9798
},
99+
{
100+
timestamp: expect.any(Number),
101+
category: 'ui.input',
102+
message: 'body > StyledInput',
103+
data: { 'ui.component_name': 'StyledInput' },
104+
},
98105
]);
99106
},
100107
);

dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
<meta charset="utf-8" />
55
</head>
66
<body>
7-
<button data-sentry-component="MyCoolButton" id="button">😎</button>
8-
<input data-sentry-component="MyCoolInput" id="input" />
7+
<button data-sentry-component="MyCoolButton" data-sentry-element="StyledCoolButton" id="button">😎</button>
8+
<input data-sentry-component="MyCoolInput" data-sentry-element="StyledCoolInput" id="input" />
9+
<button data-sentry-element="StyledCoolButton" id="button2">😎</button>
10+
<input data-sentry-element="StyledCoolInput" id="input2" />
911
</body>
1012
</html>

dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ sentryTest('captures component name attribute when available', async ({ forceFlu
5757
data: {
5858
nodeId: expect.any(Number),
5959
node: {
60-
attributes: { id: 'button', 'data-sentry-component': 'MyCoolButton' },
60+
attributes: {
61+
id: 'button',
62+
'data-sentry-component': 'MyCoolButton',
63+
},
6164
id: expect.any(Number),
6265
tagName: 'button',
6366
textContent: '**',
@@ -72,7 +75,95 @@ sentryTest('captures component name attribute when available', async ({ forceFlu
7275
data: {
7376
nodeId: expect.any(Number),
7477
node: {
75-
attributes: { id: 'input', 'data-sentry-component': 'MyCoolInput' },
78+
attributes: {
79+
id: 'input',
80+
'data-sentry-component': 'MyCoolInput',
81+
},
82+
id: expect.any(Number),
83+
tagName: 'input',
84+
textContent: '',
85+
},
86+
},
87+
},
88+
]);
89+
});
90+
91+
sentryTest('sets element name to component name attribute', async ({ forceFlushReplay, getLocalTestPath, page }) => {
92+
if (shouldSkipReplayTest()) {
93+
sentryTest.skip();
94+
}
95+
96+
const reqPromise0 = waitForReplayRequest(page, 0);
97+
98+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
99+
return route.fulfill({
100+
status: 200,
101+
contentType: 'application/json',
102+
body: JSON.stringify({ id: 'test-id' }),
103+
});
104+
});
105+
106+
const url = await getLocalTestPath({ testDir: __dirname });
107+
108+
await page.goto(url);
109+
await reqPromise0;
110+
await forceFlushReplay();
111+
112+
const reqPromise1 = waitForReplayRequest(page, (event, res) => {
113+
return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click');
114+
});
115+
const reqPromise2 = waitForReplayRequest(page, (event, res) => {
116+
return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.input');
117+
});
118+
119+
await page.locator('#button2').click();
120+
121+
await page.locator('#input2').focus();
122+
await page.keyboard.press('Control+A');
123+
await page.keyboard.type('Hello', { delay: 10 });
124+
125+
await forceFlushReplay();
126+
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
127+
const { breadcrumbs: breadcrumbs2 } = getCustomRecordingEvents(await reqPromise2);
128+
129+
// Combine the two together
130+
breadcrumbs2.forEach(breadcrumb => {
131+
if (!breadcrumbs.some(b => b.category === breadcrumb.category && b.timestamp === breadcrumb.timestamp)) {
132+
breadcrumbs.push(breadcrumb);
133+
}
134+
});
135+
136+
expect(breadcrumbs).toEqual([
137+
{
138+
timestamp: expect.any(Number),
139+
type: 'default',
140+
category: 'ui.click',
141+
message: 'body > StyledCoolButton',
142+
data: {
143+
nodeId: expect.any(Number),
144+
node: {
145+
attributes: {
146+
id: 'button2',
147+
'data-sentry-component': 'StyledCoolButton',
148+
},
149+
id: expect.any(Number),
150+
tagName: 'button',
151+
textContent: '**',
152+
},
153+
},
154+
},
155+
{
156+
timestamp: expect.any(Number),
157+
type: 'default',
158+
category: 'ui.input',
159+
message: 'body > StyledCoolInput',
160+
data: {
161+
nodeId: expect.any(Number),
162+
node: {
163+
attributes: {
164+
id: 'input2',
165+
'data-sentry-component': 'StyledCoolInput',
166+
},
76167
id: expect.any(Number),
77168
tagName: 'input',
78169
textContent: '',

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ const delay = e => {
1515

1616
document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay);
1717
document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay);
18+
document.querySelector('[data-test-id=styled-button]').addEventListener('click', delay);

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
<body>
77
<div>Rendered Before Long Task</div>
88
<button data-test-id="interaction-button">Click Me</button>
9-
<button data-test-id="annotated-button" data-sentry-component="AnnotatedButton">Click Me</button>
9+
<button data-test-id="annotated-button" data-sentry-component="AnnotatedButton" data-sentry-element="StyledButton">Click Me</button>
10+
<button data-test-id="styled-button" data-sentry-element="StyledButton">Click Me</button>
1011
<script src="https://example.com/path/to/script.js"></script>
1112
</body>
1213
</html>

dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,35 @@ sentryTest(
112112
expect(interactionSpan.description).toBe('body > AnnotatedButton');
113113
},
114114
);
115+
116+
sentryTest(
117+
'should use the element name for a clicked element when no component name',
118+
async ({ browserName, getLocalTestPath, page }) => {
119+
const supportedBrowsers = ['chromium', 'firefox'];
120+
121+
if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
122+
sentryTest.skip();
123+
}
124+
125+
await page.route('**/path/to/script.js', (route: Route) =>
126+
route.fulfill({ path: `${__dirname}/assets/script.js` }),
127+
);
128+
129+
const url = await getLocalTestPath({ testDir: __dirname });
130+
131+
await page.goto(url);
132+
await getFirstSentryEnvelopeRequest<Event>(page);
133+
134+
await page.locator('[data-test-id=styled-button]').click();
135+
136+
const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
137+
expect(envelopes).toHaveLength(1);
138+
const eventData = envelopes[0];
139+
140+
expect(eventData.spans).toHaveLength(1);
141+
142+
const interactionSpan = eventData.spans![0];
143+
expect(interactionSpan.op).toBe('ui.interaction.click');
144+
expect(interactionSpan.description).toBe('body > StyledButton');
145+
},
146+
);

packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const ATTRIBUTES_TO_RECORD = new Set([
2020
*/
2121
export function getAttributesToRecord(attributes: Record<string, unknown>): Record<string, unknown> {
2222
const obj: Record<string, unknown> = {};
23+
if (!attributes['data-sentry-component'] && attributes['data-sentry-element']) {
24+
attributes['data-sentry-component'] = attributes['data-sentry-element'];
25+
}
2326
for (const key in attributes) {
2427
if (ATTRIBUTES_TO_RECORD.has(key)) {
2528
let normalizedKey = key;

packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,21 @@ it('records only included attributes', function () {
3030
}),
3131
).toEqual({});
3232
});
33+
34+
it('records data-sentry-element as data-sentry-component when appropriate', function () {
35+
expect(
36+
getAttributesToRecord({
37+
['data-sentry-component']: 'component',
38+
['data-sentry-element']: 'element',
39+
}),
40+
).toEqual({
41+
['data-sentry-component']: 'component',
42+
});
43+
expect(
44+
getAttributesToRecord({
45+
['data-sentry-element']: 'element',
46+
}),
47+
).toEqual({
48+
['data-sentry-component']: 'element',
49+
});
50+
});

packages/utils/src/browser.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,13 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string {
8888
// @ts-expect-error WINDOW has HTMLElement
8989
if (WINDOW.HTMLElement) {
9090
// If using the component name annotation plugin, this value may be available on the DOM node
91-
if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) {
92-
return elem.dataset['sentryComponent'];
91+
if (elem instanceof HTMLElement && elem.dataset) {
92+
if (elem.dataset['sentryComponent']) {
93+
return elem.dataset['sentryComponent'];
94+
}
95+
if (elem.dataset['sentryElement']) {
96+
return elem.dataset['sentryElement'];
97+
}
9398
}
9499
}
95100

@@ -166,8 +171,8 @@ export function getDomElement<E = any>(selector: string): E | null {
166171

167172
/**
168173
* Given a DOM element, traverses up the tree until it finds the first ancestor node
169-
* that has the `data-sentry-component` attribute. This attribute is added at build-time
170-
* by projects that have the component name annotation plugin installed.
174+
* that has the `data-sentry-component` or `data-sentry-element` attribute with `data-sentry-component` taking
175+
* precendence. This attribute is added at build-time by projects that have the component name annotation plugin installed.
171176
*
172177
* @returns a string representation of the component for the provided DOM element, or `null` if not found
173178
*/
@@ -184,8 +189,13 @@ export function getComponentName(elem: unknown): string | null {
184189
return null;
185190
}
186191

187-
if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) {
188-
return currentElem.dataset['sentryComponent'];
192+
if (currentElem instanceof HTMLElement) {
193+
if (currentElem.dataset['sentryComponent']) {
194+
return currentElem.dataset['sentryComponent'];
195+
}
196+
if (currentElem.dataset['sentryElement']) {
197+
return currentElem.dataset['sentryElement'];
198+
}
189199
}
190200

191201
currentElem = currentElem.parentNode;

0 commit comments

Comments
 (0)