diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html index 5048dfd754f2..e54da47ff09d 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html @@ -7,5 +7,6 @@ + diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts index 88a1c89fba0d..93ceb1e70001 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts @@ -55,3 +55,39 @@ sentryTest('captures Breadcrumb for clicks & debounces them for a second', async }, ]); }); + +sentryTest( + 'uses the annotated component name in the breadcrumb messages and adds it to the data object', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + await page.click('#annotated-button'); + await page.evaluate('Sentry.captureException("test exception")'); + + const eventData = await promise; + + expect(eventData.breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > AnnotatedButton', + data: { 'ui.component_name': 'AnnotatedButton' }, + }, + ]); + }, +); diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html index b3d53fbf9a3e..a16ca41e45da 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html @@ -7,5 +7,6 @@ + diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts index b3393561f331..3a25abb1f9fe 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts @@ -64,3 +64,48 @@ sentryTest('captures Breadcrumb for events on inputs & debounced them', async ({ }, ]); }); + +sentryTest( + 'includes the annotated component name within the breadcrumb message and data', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + await page.click('#annotated-input'); + await page.type('#annotated-input', 'John', { delay: 1 }); + + await page.evaluate('Sentry.captureException("test exception")'); + const eventData = await promise; + expect(eventData.exception?.values).toHaveLength(1); + + expect(eventData.breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > AnnotatedInput', + data: { 'ui.component_name': 'AnnotatedInput' }, + }, + { + timestamp: expect.any(Number), + category: 'ui.input', + message: 'body > AnnotatedInput', + data: { 'ui.component_name': 'AnnotatedInput' }, + }, + ]); + }, +); diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 102d1d7e500d..c14465b3448b 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -12,6 +12,7 @@ import type { IntegrationFn, } from '@sentry/types'; import type { + Breadcrumb, FetchBreadcrumbData, FetchBreadcrumbHint, XhrBreadcrumbData, @@ -24,6 +25,7 @@ import { addFetchInstrumentationHandler, addHistoryInstrumentationHandler, addXhrInstrumentationHandler, + getComponentName, getEventDescription, htmlTreeAsString, logger, @@ -133,6 +135,7 @@ function _getDomBreadcrumbHandler( } let target; + let componentName; let keyAttrs = typeof dom === 'object' ? dom.serializeAttribute : undefined; let maxStringLength = @@ -152,9 +155,10 @@ function _getDomBreadcrumbHandler( // Accessing event.target can throw (see getsentry/raven-js#838, #768) try { const event = handlerData.event as Event | Node; - target = _isEvent(event) - ? htmlTreeAsString(event.target, { keyAttrs, maxStringLength }) - : htmlTreeAsString(event, { keyAttrs, maxStringLength }); + const element = _isEvent(event) ? event.target : event; + + target = htmlTreeAsString(element, { keyAttrs, maxStringLength }); + componentName = getComponentName(element); } catch (e) { target = ''; } @@ -163,17 +167,20 @@ function _getDomBreadcrumbHandler( return; } - addBreadcrumb( - { - category: `ui.${handlerData.name}`, - message: target, - }, - { - event: handlerData.event, - name: handlerData.name, - global: handlerData.global, - }, - ); + const breadcrumb: Breadcrumb = { + category: `ui.${handlerData.name}`, + message: target, + }; + + if (componentName) { + breadcrumb.data = { 'ui.component_name': componentName }; + } + + addBreadcrumb(breadcrumb, { + event: handlerData.event, + name: handlerData.name, + global: handlerData.global, + }); }; }