diff --git a/CHANGELOG.md b/CHANGELOG.md index 6315e5ba18af..60ff525786f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.80.2-alpha.1 + +No longer will prioritize the component names for replays, as this will break searching by CSS selector. + +## 7.80.2-alpha.0 + +- feat(utils): Prioritize Component name attributes over HTML Tree String (#9496) + ## 7.80.1 - fix(astro): Adjust Vite plugin config to upload server source maps (#9541) 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..e4bf6204f578 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..3d13bdac8a87 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,38 @@ sentryTest('captures Breadcrumb for clicks & debounces them for a second', async }, ]); }); + +sentryTest( + 'prioritizes the annotated component name within the breadcrumb message', + 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: '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..a09f5ccff8ec 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..b6cd4241e78a 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,46 @@ sentryTest('captures Breadcrumb for events on inputs & debounced them', async ({ }, ]); }); + +sentryTest( + 'prioritizes the annotated component name within the breadcrumb message', + 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: 'AnnotatedInput', + }, + { + timestamp: expect.any(Number), + category: 'ui.input', + message: '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..1a93874744b7 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..fd4910faf2cc 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('AnnotatedButton'); + }, +); diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index f71361b7d96e..59e7c8327f5a 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -10,8 +10,8 @@ import type { } from '@sentry/types/build/types/breadcrumb'; import { addInstrumentationHandler, + getElementIdentifier, getEventDescription, - htmlTreeAsString, logger, parseUrl, safeJoin, @@ -153,8 +153,8 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa try { const event = handlerData.event as Event | Node; target = _isEvent(event) - ? htmlTreeAsString(event.target, { keyAttrs, maxStringLength }) - : htmlTreeAsString(event, { keyAttrs, maxStringLength }); + ? getElementIdentifier(event.target, { keyAttrs, maxStringLength }) + : getElementIdentifier(event, { keyAttrs, maxStringLength }); } catch (e) { target = ''; } diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index edeedcee679f..05b12c4b2ff4 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -2,7 +2,7 @@ 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 { browserPerformanceTimeOrigin, getElementIdentifier, logger } from '@sentry/utils'; import { addClsInstrumentationHandler, @@ -100,7 +100,7 @@ export function startTrackingInteractions(): void { const duration = msToSec(entry.duration); transaction.startChild({ - description: htmlTreeAsString(entry.target), + description: getElementIdentifier(entry.target), op: `ui.interaction.${entry.name}`, origin: 'auto.ui.browser.metrics', startTimestamp: startTime, @@ -470,7 +470,7 @@ function _tagMetricInfo(transaction: Transaction): void { // Capture Properties of the LCP element that contributes to the LCP. if (_lcpEntry.element) { - transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element)); + transaction.setTag('lcp.element', getElementIdentifier(_lcpEntry.element)); } if (_lcpEntry.id) { @@ -489,7 +489,7 @@ function _tagMetricInfo(transaction: Transaction): void { if (_clsEntry && _clsEntry.sources) { __DEBUG_BUILD__ && logger.log('[Measurements] Adding CLS Data'); _clsEntry.sources.forEach((source, index) => - transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), + transaction.setTag(`cls.source.${index + 1}`, getElementIdentifier(source.node)), ); } } diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index d2d8f7af9a72..4b98736550cc 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -157,3 +157,19 @@ export function getDomElement(selector: string): E | null { } return null; } + +/** + * Given a child DOM element, returns the component name of the element. + * If the component name does not exist, this function will fallback to `htmlTreeAsString` + * e.g. [HTMLElement] => MyComponentName + */ +export function getElementIdentifier( + elem: unknown, + options: string[] | { keyAttrs?: string[]; maxStringLength?: number } = {}, +): string { + if (elem instanceof HTMLElement && elem.dataset) { + return elem.dataset['component'] || elem.dataset['element'] || htmlTreeAsString(elem, options); + } + + return htmlTreeAsString(elem, options); +} diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts index e4e866bf2763..f6356bccf3b7 100644 --- a/packages/utils/src/object.ts +++ b/packages/utils/src/object.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { WrappedFunction } from '@sentry/types'; -import { htmlTreeAsString } from './browser'; +import { getElementIdentifier } from './browser'; import { isElement, isError, isEvent, isInstanceOf, isPlainObject, isPrimitive } from './is'; import { logger } from './logger'; import { truncate } from './string'; @@ -150,7 +150,7 @@ export function convertToPlainObject(value: V): /** Creates a string representation of the target of an `Event` object */ function serializeEventTarget(target: unknown): string { try { - return isElement(target) ? htmlTreeAsString(target) : Object.prototype.toString.call(target); + return isElement(target) ? getElementIdentifier(target) : Object.prototype.toString.call(target); } catch (_oO) { return ''; } diff --git a/packages/utils/test/browser.test.ts b/packages/utils/test/browser.test.ts index 040789fe8426..3099da0680e2 100644 --- a/packages/utils/test/browser.test.ts +++ b/packages/utils/test/browser.test.ts @@ -4,8 +4,11 @@ import { getDomElement, htmlTreeAsString } from '../src/browser'; 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', () => {