diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html index e54da47ff09d..97d2b9069eb4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html @@ -7,6 +7,7 @@ - + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts index 0b877fb7c8d8..d6dd646b9965 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts @@ -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")')]); @@ -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' }, + }, ]); }, ); diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html index a16ca41e45da..38934ca803a4 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html @@ -7,6 +7,7 @@ - + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts index b4549a105c7a..d7544fbf891e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts @@ -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; @@ -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' }, + }, ]); }, ); diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html index 1cb45daa349a..2501d595be00 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/template.html @@ -4,7 +4,9 @@ - - + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts index 99b7a71273e3..9ad1a99c32aa 100644 --- a/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/captureComponentName/test.ts @@ -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: '**', @@ -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: '', diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js index a37a2c70ad27..fecb5ca8a758 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/assets/script.js @@ -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); diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html index 3357fb20a94e..d3bda54d7443 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/template.html @@ -6,7 +6,8 @@
Rendered Before Long Task
- + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts index 224d352ec851..5b032ace7f8c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/interactions/test.ts @@ -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(page); + + await page.locator('[data-test-id=styled-button]').click(); + + const envelopes = await getMultipleSentryEnvelopeRequests(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'); + }, +); diff --git a/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts b/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts index f50c2b9f9088..d2d062926811 100644 --- a/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts +++ b/packages/replay-internal/src/coreHandlers/util/getAttributesToRecord.ts @@ -20,6 +20,9 @@ const ATTRIBUTES_TO_RECORD = new Set([ */ export function getAttributesToRecord(attributes: Record): Record { const obj: Record = {}; + 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; diff --git a/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts b/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts index 46211820e2c7..393d58427e34 100644 --- a/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts +++ b/packages/replay-internal/test/unit/coreHandlers/util/getAttributesToRecord.test.ts @@ -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', + }); +}); diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index 5024038822b9..371e7e96c8c2 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -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']; + } } } @@ -166,8 +171,8 @@ export function getDomElement(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 */ @@ -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;