Skip to content

feat(replay): Use data sentry element as fallback for the component name #11383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<body>
<button id="button1" type="button">Button 1</button>
<button id="button2" type="button">Button 2</button>
<button id="annotated-button" type="button" data-sentry-component="AnnotatedButton">Button 3</button>
<button id="annotated-button" type="button" data-sentry-component="AnnotatedButton" data-sentry-element="StyledButton">Button 3</button>
<button id="annotated-button-2" type="button" data-sentry-element="StyledButton">Button 4</button>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ sentryTest(

await page.goto(url);
await page.locator('#annotated-button').click();
await page.locator('#annotated-button-2').click();

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

Expand All @@ -88,6 +89,12 @@ sentryTest(
message: 'body > AnnotatedButton',
data: { 'ui.component_name': 'AnnotatedButton' },
},
{
timestamp: expect.any(Number),
category: 'ui.click',
message: 'body > StyledButton',
data: { 'ui.component_name': 'StyledButton' },
},
]);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<body>
<input id="input1" type="text" />
<input id="input2" type="text" />
<input id="annotated-input" data-sentry-component="AnnotatedInput" type="text" />
<input id="annotated-input" data-sentry-component="AnnotatedInput" data-sentry-element="StyledInput" type="text" />
<input id="annotated-input-2" data-sentry-element="StyledInput" type="text" />
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ sentryTest(
await page.goto(url);

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

await page.evaluate('Sentry.captureException("test exception")');
const eventData = await promise;
Expand All @@ -95,6 +96,12 @@ sentryTest(
message: 'body > AnnotatedInput',
data: { 'ui.component_name': 'AnnotatedInput' },
},
{
timestamp: expect.any(Number),
category: 'ui.input',
message: 'body > StyledInput',
data: { 'ui.component_name': 'StyledInput' },
},
]);
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
<meta charset="utf-8" />
</head>
<body>
<button data-sentry-component="MyCoolButton" id="button">😎</button>
<input data-sentry-component="MyCoolInput" id="input" />
<button data-sentry-component="MyCoolButton" data-sentry-element="StyledCoolButton" id="button">😎</button>
<input data-sentry-component="MyCoolInput" data-sentry-element="StyledCoolInput" id="input" />
<button data-sentry-element="StyledCoolButton" id="button2">😎</button>
<input data-sentry-element="StyledCoolInput" id="input2" />
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,10 @@ sentryTest('captures component name attribute when available', async ({ forceFlu
data: {
nodeId: expect.any(Number),
node: {
attributes: { id: 'button', 'data-sentry-component': 'MyCoolButton' },
attributes: {
id: 'button',
'data-sentry-component': 'MyCoolButton',
},
id: expect.any(Number),
tagName: 'button',
textContent: '**',
Expand All @@ -72,7 +75,95 @@ sentryTest('captures component name attribute when available', async ({ forceFlu
data: {
nodeId: expect.any(Number),
node: {
attributes: { id: 'input', 'data-sentry-component': 'MyCoolInput' },
attributes: {
id: 'input',
'data-sentry-component': 'MyCoolInput',
},
id: expect.any(Number),
tagName: 'input',
textContent: '',
},
},
},
]);
});

sentryTest('sets element name to component name attribute', async ({ forceFlushReplay, getLocalTestPath, page }) => {
if (shouldSkipReplayTest()) {
sentryTest.skip();
}

const reqPromise0 = waitForReplayRequest(page, 0);

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;
await forceFlushReplay();

const reqPromise1 = waitForReplayRequest(page, (event, res) => {
return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click');
});
const reqPromise2 = waitForReplayRequest(page, (event, res) => {
return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.input');
});

await page.locator('#button2').click();

await page.locator('#input2').focus();
await page.keyboard.press('Control+A');
await page.keyboard.type('Hello', { delay: 10 });

await forceFlushReplay();
const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1);
const { breadcrumbs: breadcrumbs2 } = getCustomRecordingEvents(await reqPromise2);

// Combine the two together
breadcrumbs2.forEach(breadcrumb => {
if (!breadcrumbs.some(b => b.category === breadcrumb.category && b.timestamp === breadcrumb.timestamp)) {
breadcrumbs.push(breadcrumb);
}
});

expect(breadcrumbs).toEqual([
{
timestamp: expect.any(Number),
type: 'default',
category: 'ui.click',
message: 'body > StyledCoolButton',
data: {
nodeId: expect.any(Number),
node: {
attributes: {
id: 'button2',
'data-sentry-component': 'StyledCoolButton',
},
id: expect.any(Number),
tagName: 'button',
textContent: '**',
},
},
},
{
timestamp: expect.any(Number),
type: 'default',
category: 'ui.input',
message: 'body > StyledCoolInput',
data: {
nodeId: expect.any(Number),
node: {
attributes: {
id: 'input2',
'data-sentry-component': 'StyledCoolInput',
},
id: expect.any(Number),
tagName: 'input',
textContent: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ const delay = e => {

document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay);
document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay);
document.querySelector('[data-test-id=styled-button]').addEventListener('click', delay);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
<body>
<div>Rendered Before Long Task</div>
<button data-test-id="interaction-button">Click Me</button>
<button data-test-id="annotated-button" data-sentry-component="AnnotatedButton">Click Me</button>
<button data-test-id="annotated-button" data-sentry-component="AnnotatedButton" data-sentry-element="StyledButton">Click Me</button>
<button data-test-id="styled-button" data-sentry-element="StyledButton">Click Me</button>
<script src="https://example.com/path/to/script.js"></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,35 @@ sentryTest(
expect(interactionSpan.description).toBe('body > AnnotatedButton');
},
);

sentryTest(
'should use the element name for a clicked element when no component name',
async ({ browserName, getLocalTestPath, page }) => {
const supportedBrowsers = ['chromium', 'firefox'];

if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) {
sentryTest.skip();
}

await page.route('**/path/to/script.js', (route: Route) =>
route.fulfill({ path: `${__dirname}/assets/script.js` }),
);

const url = await getLocalTestPath({ testDir: __dirname });

await page.goto(url);
await getFirstSentryEnvelopeRequest<Event>(page);

await page.locator('[data-test-id=styled-button]').click();

const envelopes = await getMultipleSentryEnvelopeRequests<TransactionJSON>(page, 1);
expect(envelopes).toHaveLength(1);
const eventData = envelopes[0];

expect(eventData.spans).toHaveLength(1);

const interactionSpan = eventData.spans![0];
expect(interactionSpan.op).toBe('ui.interaction.click');
expect(interactionSpan.description).toBe('body > StyledButton');
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const ATTRIBUTES_TO_RECORD = new Set([
*/
export function getAttributesToRecord(attributes: Record<string, unknown>): Record<string, unknown> {
const obj: Record<string, unknown> = {};
if (!attributes['data-sentry-component'] && attributes['data-sentry-element']) {
attributes['data-sentry-component'] = attributes['data-sentry-element'];
}
for (const key in attributes) {
if (ATTRIBUTES_TO_RECORD.has(key)) {
let normalizedKey = key;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,21 @@ it('records only included attributes', function () {
}),
).toEqual({});
});

it('records data-sentry-element as data-sentry-component when appropriate', function () {
expect(
getAttributesToRecord({
['data-sentry-component']: 'component',
['data-sentry-element']: 'element',
}),
).toEqual({
['data-sentry-component']: 'component',
});
expect(
getAttributesToRecord({
['data-sentry-element']: 'element',
}),
).toEqual({
['data-sentry-component']: 'element',
});
});
22 changes: 16 additions & 6 deletions packages/utils/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,13 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string {
// @ts-expect-error WINDOW has HTMLElement
if (WINDOW.HTMLElement) {
// If using the component name annotation plugin, this value may be available on the DOM node
if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) {
return elem.dataset['sentryComponent'];
if (elem instanceof HTMLElement && elem.dataset) {
if (elem.dataset['sentryComponent']) {
return elem.dataset['sentryComponent'];
}
if (elem.dataset['sentryElement']) {
return elem.dataset['sentryElement'];
}
}
}

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

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

if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) {
return currentElem.dataset['sentryComponent'];
if (currentElem instanceof HTMLElement) {
if (currentElem.dataset['sentryComponent']) {
return currentElem.dataset['sentryComponent'];
}
if (currentElem.dataset['sentryElement']) {
return currentElem.dataset['sentryElement'];
}
}

currentElem = currentElem.parentNode;
Expand Down