From 9f5271095ba0cb503b9fa5a018615be59d6d234f Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Fri, 8 Dec 2023 15:47:20 -0500 Subject: [PATCH 01/13] Iterate on method for retrieving component names and send it in breadcrumbs separately --- .../browser/src/integrations/breadcrumbs.ts | 8 +++++ packages/utils/src/browser.ts | 32 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index cfcb255f5999..31095cfbe915 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -22,6 +22,7 @@ import { addFetchInstrumentationHandler, addHistoryInstrumentationHandler, addXhrInstrumentationHandler, + getComponentName, getEventDescription, htmlTreeAsString, logger, @@ -143,6 +144,7 @@ function addSentryBreadcrumb(event: SentryEvent): void { function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDataDom) => void { function _innerDomBreadcrumb(handlerData: HandlerDataDom): void { let target; + let componentName; let keyAttrs = typeof dom === 'object' ? dom.serializeAttribute : undefined; let maxStringLength = @@ -165,8 +167,13 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa target = _isEvent(event) ? htmlTreeAsString(event.target, { keyAttrs, maxStringLength }) : htmlTreeAsString(event, { keyAttrs, maxStringLength }); + + componentName = _isEvent(event) + ? getComponentName(event.target) + : getComponentName(event); } catch (e) { target = ''; + componentName = null; } if (target.length === 0) { @@ -177,6 +184,7 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa { category: `ui.${handlerData.name}`, message: target, + data: {componentName} }, { event: handlerData.event, diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index d2d8f7af9a72..e2809f9a2ea7 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -6,6 +6,10 @@ const WINDOW = getGlobalObject(); const DEFAULT_MAX_STRING_LENGTH = 80; +type SimpleNode = { + parentNode: SimpleNode; +} | null; + /** * Given a child DOM element, returns a query-selector statement describing that * and its ancestors @@ -16,9 +20,8 @@ export function htmlTreeAsString( elem: unknown, options: string[] | { keyAttrs?: string[]; maxStringLength?: number } = {}, ): string { - type SimpleNode = { - parentNode: SimpleNode; - } | null; + + console.log('html tree as string') if (!elem) { return ''; @@ -56,6 +59,7 @@ export function htmlTreeAsString( currentElem = currentElem.parentNode; } + console.log(out.reverse().join(separator)) return out.reverse().join(separator); } catch (_oO) { return ''; @@ -86,6 +90,11 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { return ''; } + // 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'] + } + out.push(elem.tagName.toLowerCase()); // Pairs of attribute keys defined in `serializeAttribute` and their values on element. @@ -157,3 +166,20 @@ export function getDomElement(selector: string): E | null { } return null; } + +export function getComponentName(elem: unknown) { + let currentElem = elem as SimpleNode; + const MAX_TRAVERSE_HEIGHT = 5; + + for (let i = 0; i < MAX_TRAVERSE_HEIGHT; i++) { + if (!currentElem) { + return null; + } + + if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) { + return `<${currentElem.dataset['sentryComponent']}>`; + } + + currentElem = currentElem.parentNode + } +} From 4b1418aaebc311817c81bdd149a6e14080cd68dc Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Tue, 12 Dec 2023 14:39:26 -0500 Subject: [PATCH 02/13] Add componentName in breadcrumb data --- packages/browser/src/integrations/breadcrumbs.ts | 16 +++++++++++----- packages/utils/src/browser.ts | 7 +++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 31095cfbe915..fdaae3f7154a 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -10,6 +10,7 @@ import type { Integration, } from '@sentry/types'; import type { + Breadcrumb, FetchBreadcrumbData, FetchBreadcrumbHint, XhrBreadcrumbData, @@ -180,12 +181,17 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa return; } + const breadcrumb: Breadcrumb = { + category: `ui.${handlerData.name}`, + message: target, + } + + if (componentName) { + breadcrumb.data = {componentName} + } + getCurrentHub().addBreadcrumb( - { - category: `ui.${handlerData.name}`, - message: target, - data: {componentName} - }, + breadcrumb, { event: handlerData.event, name: handlerData.name, diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index e2809f9a2ea7..75a3a5a38349 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -21,8 +21,6 @@ export function htmlTreeAsString( options: string[] | { keyAttrs?: string[]; maxStringLength?: number } = {}, ): string { - console.log('html tree as string') - if (!elem) { return ''; } @@ -59,7 +57,6 @@ export function htmlTreeAsString( currentElem = currentElem.parentNode; } - console.log(out.reverse().join(separator)) return out.reverse().join(separator); } catch (_oO) { return ''; @@ -177,9 +174,11 @@ export function getComponentName(elem: unknown) { } if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) { - return `<${currentElem.dataset['sentryComponent']}>`; + return currentElem.dataset['sentryComponent']; } currentElem = currentElem.parentNode } + + return null; } From 4fb6c09a43f9bb8bb1cf2dc19fbff75894bf8822 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Thu, 14 Dec 2023 14:05:55 -0500 Subject: [PATCH 03/13] Track data-sentry-component attribute for replay breadcrumbs --- packages/replay/src/coreHandlers/handleDom.ts | 2 +- packages/replay/src/coreHandlers/util/getAttributesToRecord.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/replay/src/coreHandlers/handleDom.ts b/packages/replay/src/coreHandlers/handleDom.ts index a5c20810481b..b26d2f598998 100644 --- a/packages/replay/src/coreHandlers/handleDom.ts +++ b/packages/replay/src/coreHandlers/handleDom.ts @@ -2,7 +2,7 @@ import { record } from '@sentry-internal/rrweb'; import type { serializedElementNodeWithId, serializedNodeWithId } from '@sentry-internal/rrweb-snapshot'; import { NodeType } from '@sentry-internal/rrweb-snapshot'; import type { Breadcrumb, HandlerDataDom } from '@sentry/types'; -import { htmlTreeAsString } from '@sentry/utils'; +import { getComponentName, htmlTreeAsString } from '@sentry/utils'; import type { ReplayContainer } from '../types'; import { createBreadcrumb } from '../util/createBreadcrumb'; diff --git a/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts b/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts index 13c756901028..9467ab08fa37 100644 --- a/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts +++ b/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts @@ -12,6 +12,7 @@ const ATTRIBUTES_TO_RECORD = new Set([ 'data-testid', 'disabled', 'aria-disabled', + 'data-sentry-component' ]); /** From 9a2e53c7ea65441f324c8533ee83c2ed6f2c0a57 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Thu, 14 Dec 2023 14:45:17 -0500 Subject: [PATCH 04/13] Document the function --- packages/replay/src/coreHandlers/handleDom.ts | 2 +- packages/utils/src/browser.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/replay/src/coreHandlers/handleDom.ts b/packages/replay/src/coreHandlers/handleDom.ts index b26d2f598998..a5c20810481b 100644 --- a/packages/replay/src/coreHandlers/handleDom.ts +++ b/packages/replay/src/coreHandlers/handleDom.ts @@ -2,7 +2,7 @@ import { record } from '@sentry-internal/rrweb'; import type { serializedElementNodeWithId, serializedNodeWithId } from '@sentry-internal/rrweb-snapshot'; import { NodeType } from '@sentry-internal/rrweb-snapshot'; import type { Breadcrumb, HandlerDataDom } from '@sentry/types'; -import { getComponentName, htmlTreeAsString } from '@sentry/utils'; +import { htmlTreeAsString } from '@sentry/utils'; import type { ReplayContainer } from '../types'; import { createBreadcrumb } from '../util/createBreadcrumb'; diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index 75a3a5a38349..de132b58ed44 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -164,6 +164,13 @@ export function getDomElement(selector: string): E | null { return 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. + * + * @returns a string representation of the component for the provided DOM element, or `null` if not found + */ export function getComponentName(elem: unknown) { let currentElem = elem as SimpleNode; const MAX_TRAVERSE_HEIGHT = 5; From 3fe57fa182927c96fb604cce73ef9fa2c8a767dd Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Thu, 14 Dec 2023 16:36:13 -0500 Subject: [PATCH 05/13] Add component name to interaction span databag --- .../tracing-internal/src/browser/metrics/index.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 5182897cb0b9..a2dc620cd6c3 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -1,8 +1,8 @@ /* eslint-disable max-lines */ import type { IdleTransaction, Transaction } from '@sentry/core'; import { getActiveTransaction } from '@sentry/core'; -import type { Measurements } from '@sentry/types'; -import { browserPerformanceTimeOrigin, htmlTreeAsString, logger } from '@sentry/utils'; +import type { Measurements, SpanContext } from '@sentry/types'; +import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../../common/debug-build'; import { @@ -102,13 +102,20 @@ export function startTrackingInteractions(): void { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); - transaction.startChild({ + const span: SpanContext = { description: htmlTreeAsString(entry.target), op: `ui.interaction.${entry.name}`, origin: 'auto.ui.browser.metrics', startTimestamp: startTime, endTimestamp: startTime + duration, - }); + } + + const componentName = getComponentName(entry.target); + if (componentName) { + span.data = {'ui.component_name': componentName} + } + + transaction.startChild(span); } } }); From 873820fa3f0099c6a63d78040bc0c745caf3173f Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Thu, 14 Dec 2023 17:16:24 -0500 Subject: [PATCH 06/13] Add component names to react profiler span databag --- packages/react/src/profiler.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 9647d34f0fb4..c23f2cb957a8 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -63,6 +63,7 @@ class Profiler extends React.Component { description: `<${name}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: {'ui.component_name': name} }); } } @@ -87,6 +88,7 @@ class Profiler extends React.Component { this._updateSpan = this._mountSpan.startChild({ data: { changedProps, + 'ui.component_name': this.props.name }, description: `<${this.props.name}>`, op: REACT_UPDATE_OP, @@ -120,6 +122,7 @@ class Profiler extends React.Component { op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: this._mountSpan.endTimestamp, + data: {'ui.component_name': name} }); } } @@ -184,6 +187,7 @@ function useProfiler( description: `<${name}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: {'ui.component_name': name} }); } @@ -203,6 +207,7 @@ function useProfiler( op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: mountSpan.endTimestamp, + data: {'ui.component_name': name} }); } }; From 665fd79728f2a7336406e192932e3aea4e549da8 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Thu, 14 Dec 2023 17:32:50 -0500 Subject: [PATCH 07/13] Add return type to getComponentName --- packages/utils/src/browser.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index de132b58ed44..e5b02f936613 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -171,7 +171,7 @@ export function getDomElement(selector: string): E | null { * * @returns a string representation of the component for the provided DOM element, or `null` if not found */ -export function getComponentName(elem: unknown) { +export function getComponentName(elem: unknown): string | null { let currentElem = elem as SimpleNode; const MAX_TRAVERSE_HEIGHT = 5; From 6fe6746f931ddfb5ecfbf30d3c9c297758c5fa71 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Fri, 15 Dec 2023 12:38:37 -0500 Subject: [PATCH 08/13] Run Biome formatter --- .../browser/src/integrations/breadcrumbs.ts | 21 +++++++------------ packages/react/src/profiler.tsx | 10 ++++----- .../util/getAttributesToRecord.ts | 2 +- .../src/browser/metrics/index.ts | 4 ++-- packages/utils/src/browser.ts | 5 ++--- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index fdaae3f7154a..999fe4190d67 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -169,9 +169,7 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa ? htmlTreeAsString(event.target, { keyAttrs, maxStringLength }) : htmlTreeAsString(event, { keyAttrs, maxStringLength }); - componentName = _isEvent(event) - ? getComponentName(event.target) - : getComponentName(event); + componentName = _isEvent(event) ? getComponentName(event.target) : getComponentName(event); } catch (e) { target = ''; componentName = null; @@ -184,20 +182,17 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa const breadcrumb: Breadcrumb = { category: `ui.${handlerData.name}`, message: target, - } + }; if (componentName) { - breadcrumb.data = {componentName} + breadcrumb.data = { componentName }; } - getCurrentHub().addBreadcrumb( - breadcrumb, - { - event: handlerData.event, - name: handlerData.name, - global: handlerData.global, - }, - ); + getCurrentHub().addBreadcrumb(breadcrumb, { + event: handlerData.event, + name: handlerData.name, + global: handlerData.global, + }); } return _innerDomBreadcrumb; diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index c23f2cb957a8..b8ffe2f1708a 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -63,7 +63,7 @@ class Profiler extends React.Component { description: `<${name}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', - data: {'ui.component_name': name} + data: { 'ui.component_name': name }, }); } } @@ -88,7 +88,7 @@ class Profiler extends React.Component { this._updateSpan = this._mountSpan.startChild({ data: { changedProps, - 'ui.component_name': this.props.name + 'ui.component_name': this.props.name, }, description: `<${this.props.name}>`, op: REACT_UPDATE_OP, @@ -122,7 +122,7 @@ class Profiler extends React.Component { op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: this._mountSpan.endTimestamp, - data: {'ui.component_name': name} + data: { 'ui.component_name': name }, }); } } @@ -187,7 +187,7 @@ function useProfiler( description: `<${name}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', - data: {'ui.component_name': name} + data: { 'ui.component_name': name }, }); } @@ -207,7 +207,7 @@ function useProfiler( op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: mountSpan.endTimestamp, - data: {'ui.component_name': name} + data: { 'ui.component_name': name }, }); } }; diff --git a/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts b/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts index 9467ab08fa37..f50c2b9f9088 100644 --- a/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts +++ b/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts @@ -12,7 +12,7 @@ const ATTRIBUTES_TO_RECORD = new Set([ 'data-testid', 'disabled', 'aria-disabled', - 'data-sentry-component' + 'data-sentry-component', ]); /** diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index a2dc620cd6c3..651246dfb688 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -108,11 +108,11 @@ export function startTrackingInteractions(): void { origin: 'auto.ui.browser.metrics', startTimestamp: startTime, endTimestamp: startTime + duration, - } + }; const componentName = getComponentName(entry.target); if (componentName) { - span.data = {'ui.component_name': componentName} + span.data = { 'ui.component_name': componentName }; } transaction.startChild(span); diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index e5b02f936613..0300133c8c84 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -20,7 +20,6 @@ export function htmlTreeAsString( elem: unknown, options: string[] | { keyAttrs?: string[]; maxStringLength?: number } = {}, ): string { - if (!elem) { return ''; } @@ -89,7 +88,7 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { // 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'] + return elem.dataset['sentryComponent']; } out.push(elem.tagName.toLowerCase()); @@ -184,7 +183,7 @@ export function getComponentName(elem: unknown): string | null { return currentElem.dataset['sentryComponent']; } - currentElem = currentElem.parentNode + currentElem = currentElem.parentNode; } return null; From ca37bcc8979285f8f927d893d7970d0be8cfd5d2 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Fri, 15 Dec 2023 13:07:03 -0500 Subject: [PATCH 09/13] Add HTMLElement type to tests --- packages/utils/test/browser.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/utils/test/browser.test.ts b/packages/utils/test/browser.test.ts index 040789fe8426..5c7df188664e 100644 --- a/packages/utils/test/browser.test.ts +++ b/packages/utils/test/browser.test.ts @@ -6,6 +6,8 @@ beforeAll(() => { const dom = new JSDOM(); // @ts-expect-error need to override global document global.document = dom.window.document; + // @ts-expect-error need to add HTMLElement type or it will not be found + global.HTMLElement = new JSDOM().window.HTMLElement; }); describe('htmlTreeAsString', () => { From 526c67dc4e61bb01d01ea9b80a39b8dcccf9d0b5 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Fri, 15 Dec 2023 13:39:41 -0500 Subject: [PATCH 10/13] Adjust tests to work with the new iteration --- .../Breadcrumbs/dom/click/template.html | 1 + .../Breadcrumbs/dom/click/test.ts | 36 +++++++++++++++ .../Breadcrumbs/dom/textInput/template.html | 1 + .../Breadcrumbs/dom/textInput/test.ts | 45 +++++++++++++++++++ .../interactions/assets/script.js | 1 + .../browsertracing/interactions/template.html | 1 + .../browsertracing/interactions/test.ts | 32 +++++++++++++ 7 files changed, 117 insertions(+) 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..f965b8c93a83 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: { componentName: '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..e36e8c17ed2e 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: { componentName: 'AnnotatedInput' }, + }, + { + timestamp: expect.any(Number), + category: 'ui.input', + message: 'body > AnnotatedInput', + data: { componentName: 'AnnotatedInput' }, + }, + ]); + }, +); diff --git a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js index 89d814bd397d..a37a2c70ad27 100644 --- a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js +++ b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js @@ -14,3 +14,4 @@ const delay = e => { }; document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay); +document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay); diff --git a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html index e16deb9ee519..3357fb20a94e 100644 --- a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html +++ b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html @@ -6,6 +6,7 @@
Rendered Before Long Task
+ diff --git a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts index e79b724ec91a..131403756251 100644 --- a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts +++ b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts @@ -80,3 +80,35 @@ sentryTest( } }, ); + +sentryTest( + 'should use the component name for a clicked element when it is available', + 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=annotated-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 > AnnotatedButton'); + }, +); From 41d98893d6bb7fcb9e9d3678f802ee3c6dae7df2 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Fri, 15 Dec 2023 13:45:02 -0500 Subject: [PATCH 11/13] Adjust React profiler tests --- packages/react/test/profiler.test.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 70eaff2d2c8b..fa629434cdf3 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -80,6 +80,7 @@ describe('withProfiler', () => { description: `<${UNKNOWN_COMPONENT}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': 'unknown' }, }); }); }); @@ -99,6 +100,7 @@ describe('withProfiler', () => { op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: undefined, + data: { 'ui.component_name': 'unknown' }, }); }); @@ -114,7 +116,6 @@ describe('withProfiler', () => { expect(mockStartChild).toHaveBeenCalledTimes(1); }); }); - describe('update span', () => { it('is created when component is updated', () => { const ProfiledComponent = withProfiler((props: { num: number }) =>
{props.num}
); @@ -126,7 +127,7 @@ describe('withProfiler', () => { rerender(); expect(mockStartChild).toHaveBeenCalledTimes(2); expect(mockStartChild).toHaveBeenLastCalledWith({ - data: { changedProps: ['num'] }, + data: { changedProps: ['num'], 'ui.component_name': 'unknown' }, description: `<${UNKNOWN_COMPONENT}>`, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', @@ -137,7 +138,7 @@ describe('withProfiler', () => { rerender(); expect(mockStartChild).toHaveBeenCalledTimes(3); expect(mockStartChild).toHaveBeenLastCalledWith({ - data: { changedProps: ['num'] }, + data: { changedProps: ['num'], 'ui.component_name': 'unknown' }, description: `<${UNKNOWN_COMPONENT}>`, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', @@ -180,6 +181,7 @@ describe('useProfiler()', () => { description: '', op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': 'Example' }, }); }); }); @@ -203,6 +205,7 @@ describe('useProfiler()', () => { description: '', op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': 'Example' }, }), ); }); From 62eb26140f0b2213863a7fcb861ffd487b700c43 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Fri, 15 Dec 2023 16:47:36 -0500 Subject: [PATCH 12/13] Add integration test for replays --- .../replay/captureComponentName/init.js | 17 ++++ .../replay/captureComponentName/template.html | 10 +++ .../replay/captureComponentName/test.ts | 83 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 packages/browser-integration-tests/suites/replay/captureComponentName/init.js create mode 100644 packages/browser-integration-tests/suites/replay/captureComponentName/template.html create mode 100644 packages/browser-integration-tests/suites/replay/captureComponentName/test.ts diff --git a/packages/browser-integration-tests/suites/replay/captureComponentName/init.js b/packages/browser-integration-tests/suites/replay/captureComponentName/init.js new file mode 100644 index 000000000000..dac512988b9a --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/captureComponentName/init.js @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + + integrations: [window.Replay], +}); diff --git a/packages/browser-integration-tests/suites/replay/captureComponentName/template.html b/packages/browser-integration-tests/suites/replay/captureComponentName/template.html new file mode 100644 index 000000000000..1cb45daa349a --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/captureComponentName/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/browser-integration-tests/suites/replay/captureComponentName/test.ts b/packages/browser-integration-tests/suites/replay/captureComponentName/test.ts new file mode 100644 index 000000000000..99b7a71273e3 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/captureComponentName/test.ts @@ -0,0 +1,83 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; + +sentryTest('captures component name attribute when available', 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('#button').click(); + + await page.locator('#input').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 > MyCoolButton', + data: { + nodeId: expect.any(Number), + node: { + attributes: { id: 'button', 'data-sentry-component': 'MyCoolButton' }, + id: expect.any(Number), + tagName: 'button', + textContent: '**', + }, + }, + }, + { + timestamp: expect.any(Number), + type: 'default', + category: 'ui.input', + message: 'body > MyCoolInput', + data: { + nodeId: expect.any(Number), + node: { + attributes: { id: 'input', 'data-sentry-component': 'MyCoolInput' }, + id: expect.any(Number), + tagName: 'input', + textContent: '', + }, + }, + }, + ]); +}); From 365f24d66905a1eced2a6b5c5e2db647a3661f61 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Tue, 19 Dec 2023 14:19:34 -0500 Subject: [PATCH 13/13] remove unnecessary null assignment --- packages/browser/src/integrations/breadcrumbs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 999fe4190d67..e36641c48ed1 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -172,7 +172,6 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa componentName = _isEvent(event) ? getComponentName(event.target) : getComponentName(event); } catch (e) { target = ''; - componentName = null; } if (target.length === 0) {