From d1d755608475c4da0e4ddcbee69595a295e3fb97 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 10 Feb 2026 18:33:38 +0100 Subject: [PATCH 1/4] fix(browser): Fix elementtiming span timestamps and attribute names --- .../src/metrics/elementTiming.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/browser-utils/src/metrics/elementTiming.ts b/packages/browser-utils/src/metrics/elementTiming.ts index f746b16645af..29ec5e689876 100644 --- a/packages/browser-utils/src/metrics/elementTiming.ts +++ b/packages/browser-utils/src/metrics/elementTiming.ts @@ -49,6 +49,13 @@ export const _onElementTiming = ({ entries }: { entries: PerformanceEntry[] }): ? spanToJSON(rootSpan).description : getCurrentScope().getScopeData().transactionName; + const timeOrigin = browserPerformanceTimeOrigin(); + if (!timeOrigin) { + // If there's no reliable time origin, we might as well not record the spans here + // as their data will be unreliable. + return; + } + entries.forEach(entry => { const elementEntry = entry as PerformanceElementTiming; @@ -70,9 +77,9 @@ export const _onElementTiming = ({ entries }: { entries: PerformanceEntry[] }): // - `timestampInSeconds()` as a safeguard // see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming/renderTime#cross-origin_image_render_time const [spanStartTime, spanStartTimeSource] = loadTime - ? [msToSec(loadTime), 'load-time'] + ? [msToSec(timeOrigin + loadTime), 'load-time'] : renderTime - ? [msToSec(renderTime), 'render-time'] + ? [msToSec(timeOrigin + renderTime), 'render-time'] : [timestampInSeconds(), 'entry-emission']; const duration = @@ -92,18 +99,18 @@ export const _onElementTiming = ({ entries }: { entries: PerformanceEntry[] }): // recording the source of the span start time, as it varies depending on available data 'sentry.span_start_time_source': spanStartTimeSource, 'sentry.transaction_name': transactionName, - 'element.id': elementEntry.id, - 'element.type': elementEntry.element?.tagName?.toLowerCase() || 'unknown', - 'element.size': + 'ui.element.id': elementEntry.id, + 'ui.element.type': elementEntry.element?.tagName?.toLowerCase() || 'unknown', + 'ui.element.dimensions': elementEntry.naturalWidth && elementEntry.naturalHeight ? `${elementEntry.naturalWidth}x${elementEntry.naturalHeight}` : undefined, - 'element.render_time': renderTime, - 'element.load_time': loadTime, + 'ui.element.render_time': renderTime, + 'ui.element.load_time': loadTime, // `url` is `0`(number) for text paints (hence we fall back to undefined) - 'element.url': elementEntry.url || undefined, - 'element.identifier': elementEntry.identifier, - 'element.paint_type': paintType, + 'ui.element.url': elementEntry.url || undefined, + 'ui.element.identifier': elementEntry.identifier, + 'ui.element.paint_type': paintType, }; startSpan( From 42504719acf0348437d8e729dfc277d445f0c59c Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 11 Feb 2026 12:57:56 +0100 Subject: [PATCH 2/4] rework impl, unit tests --- .../src/metrics/elementTiming.ts | 93 ++++----- .../test/metrics/elementTiming.test.ts | 196 +++++++++--------- 2 files changed, 133 insertions(+), 156 deletions(-) diff --git a/packages/browser-utils/src/metrics/elementTiming.ts b/packages/browser-utils/src/metrics/elementTiming.ts index 29ec5e689876..5a6258cd2134 100644 --- a/packages/browser-utils/src/metrics/elementTiming.ts +++ b/packages/browser-utils/src/metrics/elementTiming.ts @@ -1,4 +1,3 @@ -import type { SpanAttributes } from '@sentry/core'; import { browserPerformanceTimeOrigin, getActiveSpan, @@ -9,13 +8,12 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON, startSpan, - timestampInSeconds, } from '@sentry/core'; import { addPerformanceInstrumentationHandler } from './instrument'; import { getBrowserPerformanceAPI, msToSec } from './utils'; // ElementTiming interface based on the W3C spec -interface PerformanceElementTiming extends PerformanceEntry { +export interface PerformanceElementTiming extends PerformanceEntry { renderTime: number; loadTime: number; intersectionRect: DOMRectReadOnly; @@ -57,71 +55,50 @@ export const _onElementTiming = ({ entries }: { entries: PerformanceEntry[] }): } entries.forEach(entry => { - const elementEntry = entry as PerformanceElementTiming; + const { naturalWidth, naturalHeight, url, identifier, name, renderTime, loadTime, startTime, id, element } = + entry as PerformanceElementTiming; - // Skip entries without identifier (elementtiming attribute) - if (!elementEntry.identifier) { + // Skip: + // - entries without identifier (elementtiming attribute) + // - entries without startTime (e.g. 3rd party Image nodes w/o Timing-Allow-Origin header returned instantly from cache) + if (!identifier || !startTime) { return; } - // `name` contains the type of the element paint. Can be `'image-paint'` or `'text-paint'`. - // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming#instance_properties - const paintType = elementEntry.name as 'image-paint' | 'text-paint' | undefined; - - const renderTime = elementEntry.renderTime; - const loadTime = elementEntry.loadTime; - - // starting the span at: - // - `loadTime` if available (should be available for all "image-paint" entries, 0 otherwise) - // - `renderTime` if available (available for all entries, except 3rd party images, but these should be covered by `loadTime`, 0 otherwise) - // - `timestampInSeconds()` as a safeguard - // see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceElementTiming/renderTime#cross-origin_image_render_time - const [spanStartTime, spanStartTimeSource] = loadTime - ? [msToSec(timeOrigin + loadTime), 'load-time'] - : renderTime - ? [msToSec(timeOrigin + renderTime), 'render-time'] - : [timestampInSeconds(), 'entry-emission']; - - const duration = - paintType === 'image-paint' - ? // for image paints, we can acually get a duration because image-paint entries also have a `loadTime` - // and `renderTime`. `loadTime` is the time when the image finished loading and `renderTime` is the - // time when the image finished rendering. - msToSec(Math.max(0, (renderTime ?? 0) - (loadTime ?? 0))) - : // for `'text-paint'` entries, we can't get a duration because the `loadTime` is always zero. - 0; - - const attributes: SpanAttributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.elementtiming', - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.elementtiming', - // name must be user-entered, so we can assume low cardinality - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', - // recording the source of the span start time, as it varies depending on available data - 'sentry.span_start_time_source': spanStartTimeSource, - 'sentry.transaction_name': transactionName, - 'ui.element.id': elementEntry.id, - 'ui.element.type': elementEntry.element?.tagName?.toLowerCase() || 'unknown', - 'ui.element.dimensions': - elementEntry.naturalWidth && elementEntry.naturalHeight - ? `${elementEntry.naturalWidth}x${elementEntry.naturalHeight}` - : undefined, - 'ui.element.render_time': renderTime, - 'ui.element.load_time': loadTime, - // `url` is `0`(number) for text paints (hence we fall back to undefined) - 'ui.element.url': elementEntry.url || undefined, - 'ui.element.identifier': elementEntry.identifier, - 'ui.element.paint_type': paintType, - }; + // Span durations + // Case 1: Text nodes: point-in-time spans at `renderTime` + // Case 2: Image nodes: spans from `loadTime` to `renderTime` (i.e. "effective render time") + // Case 3: 3rd party Image nodes w/o Timing-Allow-Origin header: point-in-time spans at `loadTime` + // Case 4: Both times are 0 is already covered by the `startTime` check above + const relativeStartTime = loadTime > 0 ? loadTime : renderTime; + const relativeEndTime = renderTime > 0 ? renderTime : loadTime; startSpan( { - name: `element[${elementEntry.identifier}]`, - attributes, - startTime: spanStartTime, + name: `element[${identifier}]`, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.elementtiming', + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.elementtiming', + // name must be user-entered, so we can assume low cardinality + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + 'sentry.transaction_name': transactionName, + 'ui.element.id': id, + 'ui.element.type': element?.tagName?.toLowerCase() || 'unknown', + 'ui.element.width': naturalWidth, + 'ui.element.height': naturalHeight, + 'ui.element.render_time': renderTime, + 'ui.element.load_time': loadTime, + // `url` is `0`(number) for text paints (hence we fall back to undefined) + 'ui.element.url': url || undefined, + 'ui.element.identifier': identifier, + // `name` contains the type of the element paint. Can be `'image-paint'` or `'text-paint'`. + 'ui.element.paint_type': name, + }, + startTime: msToSec(timeOrigin + relativeStartTime), onlyIfParent: true, }, span => { - span.end(spanStartTime + duration); + span.end(msToSec(timeOrigin + relativeEndTime)); }, ); }); diff --git a/packages/browser-utils/test/metrics/elementTiming.test.ts b/packages/browser-utils/test/metrics/elementTiming.test.ts index 14431415873b..85f309a985b8 100644 --- a/packages/browser-utils/test/metrics/elementTiming.test.ts +++ b/packages/browser-utils/test/metrics/elementTiming.test.ts @@ -1,18 +1,22 @@ -import * as sentryCore from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { PerformanceElementTiming } from '../../src/metrics/elementTiming'; import { _onElementTiming, startTrackingElementTiming } from '../../src/metrics/elementTiming'; import * as browserMetricsInstrumentation from '../../src/metrics/instrument'; import * as browserMetricsUtils from '../../src/metrics/utils'; describe('_onElementTiming', () => { const spanEndSpy = vi.fn(); - const startSpanSpy = vi.spyOn(sentryCore, 'startSpan').mockImplementation((opts, cb) => { + const startSpanSpy = vi.spyOn(SentryCore, 'startSpan').mockImplementation((opts, cb) => { // @ts-expect-error - only passing a partial span. This is fine for the test. cb({ end: spanEndSpy, }); }); + const timeOrigin = Date.now(); + vi.spyOn(SentryCore, 'browserPerformanceTimeOrigin').mockReturnValue(timeOrigin); + beforeEach(() => { startSpanSpy.mockClear(); spanEndSpy.mockClear(); @@ -33,49 +37,47 @@ describe('_onElementTiming', () => { expect(startSpanSpy).not.toHaveBeenCalled(); }); - describe('span start time', () => { - it('uses the load time as span start time if available', () => { - const entry = { - name: 'image-paint', - entryType: 'element', - startTime: 0, - duration: 0, - renderTime: 100, - loadTime: 50, - identifier: 'test-element', - } as Partial; + it("does nothing if there's no time origin", () => { + vi.spyOn(SentryCore, 'browserPerformanceTimeOrigin').mockReturnValueOnce(undefined); - // @ts-expect-error - only passing a partial entry. This is fine for the test. - _onElementTiming({ entries: [entry] }); + const entry = { + name: 'image-paint', + entryType: 'element', + startTime: 0, + duration: 0, + } as Partial; - expect(startSpanSpy).toHaveBeenCalledWith( - expect.objectContaining({ - name: 'element[test-element]', - startTime: 0.05, - attributes: expect.objectContaining({ - 'sentry.op': 'ui.elementtiming', - 'sentry.origin': 'auto.ui.browser.elementtiming', - 'sentry.source': 'component', - 'sentry.span_start_time_source': 'load-time', - 'element.render_time': 100, - 'element.load_time': 50, - 'element.identifier': 'test-element', - 'element.paint_type': 'image-paint', - }), - }), - expect.any(Function), - ); - }); + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); - it('uses the render time as span start time if load time is not available', () => { + expect(startSpanSpy).not.toHaveBeenCalled(); + }); + + it.each([0, undefined])('does nothing if startTime is %s (i.e. no loadTime, no renderTime)', startTime => { + const entry = { + name: 'image-paint', + entryType: 'element', + startTime, + duration: 0, + } as Partial; + + // @ts-expect-error - only passing a partial entry. This is fine for the test. + _onElementTiming({ entries: [entry] }); + + expect(startSpanSpy).not.toHaveBeenCalled(); + }); + + describe('span start time', () => { + it('uses loadTime as span start time if available', () => { const entry = { name: 'image-paint', entryType: 'element', - startTime: 0, + startTime: 100, duration: 0, renderTime: 100, + loadTime: 50, identifier: 'test-element', - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -83,30 +85,30 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'element[test-element]', - startTime: 0.1, + startTime: (timeOrigin + entry.loadTime!) / 1000, attributes: expect.objectContaining({ 'sentry.op': 'ui.elementtiming', 'sentry.origin': 'auto.ui.browser.elementtiming', 'sentry.source': 'component', - 'sentry.span_start_time_source': 'render-time', - 'element.render_time': 100, - 'element.load_time': undefined, - 'element.identifier': 'test-element', - 'element.paint_type': 'image-paint', + 'ui.element.render_time': 100, + 'ui.element.load_time': 50, + 'ui.element.identifier': 'test-element', + 'ui.element.paint_type': 'image-paint', }), }), expect.any(Function), ); }); - it('falls back to the time of handling the entry if load and render time are not available', () => { + it('uses renderTime as span start time if loadTime is not available', () => { const entry = { - name: 'image-paint', + name: 'text-paint', entryType: 'element', - startTime: 0, + startTime: 100, duration: 0, + renderTime: 100, identifier: 'test-element', - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -114,16 +116,15 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'element[test-element]', - startTime: expect.any(Number), + startTime: (timeOrigin + entry.renderTime!) / 1000, attributes: expect.objectContaining({ 'sentry.op': 'ui.elementtiming', 'sentry.origin': 'auto.ui.browser.elementtiming', 'sentry.source': 'component', - 'sentry.span_start_time_source': 'entry-emission', - 'element.render_time': undefined, - 'element.load_time': undefined, - 'element.identifier': 'test-element', - 'element.paint_type': 'image-paint', + 'ui.element.render_time': 100, + 'ui.element.load_time': undefined, + 'ui.element.identifier': 'test-element', + 'ui.element.paint_type': 'text-paint', }), }), expect.any(Function), @@ -136,12 +137,12 @@ describe('_onElementTiming', () => { const entry = { name: 'image-paint', entryType: 'element', - startTime: 0, + startTime: 1500, duration: 0, renderTime: 1505, loadTime: 1500, identifier: 'test-element', - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -149,29 +150,29 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'element[test-element]', - startTime: 1.5, + startTime: (timeOrigin + entry.loadTime!) / 1000, attributes: expect.objectContaining({ - 'element.render_time': 1505, - 'element.load_time': 1500, - 'element.paint_type': 'image-paint', + 'ui.element.render_time': 1505, + 'ui.element.load_time': 1500, + 'ui.element.paint_type': 'image-paint', }), }), expect.any(Function), ); - expect(spanEndSpy).toHaveBeenCalledWith(1.505); + expect(spanEndSpy).toHaveBeenCalledWith((timeOrigin + entry.renderTime!) / 1000); }); it('uses 0 as duration for text paints', () => { const entry = { name: 'text-paint', entryType: 'element', - startTime: 0, + startTime: 1600, duration: 0, loadTime: 0, renderTime: 1600, identifier: 'test-element', - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -179,30 +180,29 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'element[test-element]', - startTime: 1.6, + startTime: (timeOrigin + entry.renderTime!) / 1000, attributes: expect.objectContaining({ - 'element.paint_type': 'text-paint', - 'element.render_time': 1600, - 'element.load_time': 0, + 'ui.element.paint_type': 'text-paint', + 'ui.element.render_time': 1600, + 'ui.element.load_time': 0, }), }), expect.any(Function), ); - expect(spanEndSpy).toHaveBeenCalledWith(1.6); + expect(spanEndSpy).toHaveBeenCalledWith((timeOrigin + entry.renderTime!) / 1000); }); - // per spec, no other kinds are supported but let's make sure we're defensive - it('uses 0 as duration for other kinds of entries', () => { + it('uses 0 duration for 3rd party image nodes w/o Timing-Allow-Origin header', () => { const entry = { - name: 'somethingelse', + name: 'image-paint', entryType: 'element', - startTime: 0, + startTime: 1700, duration: 0, - loadTime: 0, - renderTime: 1700, + loadTime: 1700, + renderTime: 0, identifier: 'test-element', - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -210,17 +210,17 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ name: 'element[test-element]', - startTime: 1.7, + startTime: (timeOrigin + entry.loadTime!) / 1000, attributes: expect.objectContaining({ - 'element.paint_type': 'somethingelse', - 'element.render_time': 1700, - 'element.load_time': 0, + 'ui.element.paint_type': 'image-paint', + 'ui.element.render_time': 0, + 'ui.element.load_time': 1700, }), }), expect.any(Function), ); - expect(spanEndSpy).toHaveBeenCalledWith(1.7); + expect(spanEndSpy).toHaveBeenCalledWith((timeOrigin + entry.loadTime!) / 1000); }); }); @@ -229,14 +229,14 @@ describe('_onElementTiming', () => { const entry = { name: 'image-paint', entryType: 'element', - startTime: 0, + startTime: 100, duration: 0, renderTime: 100, identifier: 'my-image', element: { tagName: 'IMG', }, - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -244,30 +244,28 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ attributes: expect.objectContaining({ - 'element.type': 'img', - 'element.identifier': 'my-image', - 'element.paint_type': 'image-paint', - 'element.render_time': 100, - 'element.load_time': undefined, - 'element.size': undefined, - 'element.url': undefined, + 'ui.element.type': 'img', + 'ui.element.identifier': 'my-image', + 'ui.element.paint_type': 'image-paint', + 'ui.element.render_time': 100, }), }), expect.any(Function), ); }); - it('sets element size if available', () => { + it('sets element dimensions if available', () => { const entry = { name: 'image-paint', entryType: 'element', - startTime: 0, + loadToe: 50, + startTime: 100, duration: 0, renderTime: 100, naturalWidth: 512, naturalHeight: 256, identifier: 'my-image', - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -275,8 +273,9 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ attributes: expect.objectContaining({ - 'element.size': '512x256', - 'element.identifier': 'my-image', + 'ui.element.width': 512, + 'ui.element.height': 256, + 'ui.element.identifier': 'my-image', }), }), expect.any(Function), @@ -287,11 +286,13 @@ describe('_onElementTiming', () => { const entry = { name: 'image-paint', entryType: 'element', - startTime: 0, + startTime: 100, + renderTime: 100, + loadTime: 50, duration: 0, url: 'https://santry.com/image.png', identifier: 'my-image', - } as Partial; + } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. _onElementTiming({ entries: [entry] }); @@ -299,8 +300,8 @@ describe('_onElementTiming', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ attributes: expect.objectContaining({ - 'element.identifier': 'my-image', - 'element.url': 'https://santry.com/image.png', + 'ui.element.identifier': 'my-image', + 'ui.element.url': 'https://santry.com/image.png', }), }), expect.any(Function), @@ -311,7 +312,7 @@ describe('_onElementTiming', () => { const entry = { name: 'image-paint', entryType: 'element', - startTime: 0, + startTime: 100, duration: 0, renderTime: 100, identifier: 'my-image', @@ -326,7 +327,6 @@ describe('_onElementTiming', () => { 'sentry.op': 'ui.elementtiming', 'sentry.origin': 'auto.ui.browser.elementtiming', 'sentry.source': 'component', - 'sentry.span_start_time_source': 'render-time', 'sentry.transaction_name': undefined, }), }), From 362bd033bf3cd65d32f259e6d5d2891521221302 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 12 Mar 2026 11:16:28 +0100 Subject: [PATCH 3/4] fix typo --- packages/browser-utils/test/metrics/elementTiming.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-utils/test/metrics/elementTiming.test.ts b/packages/browser-utils/test/metrics/elementTiming.test.ts index 85f309a985b8..e74ef9374d1a 100644 --- a/packages/browser-utils/test/metrics/elementTiming.test.ts +++ b/packages/browser-utils/test/metrics/elementTiming.test.ts @@ -258,7 +258,7 @@ describe('_onElementTiming', () => { const entry = { name: 'image-paint', entryType: 'element', - loadToe: 50, + loadTime: 50, startTime: 100, duration: 0, renderTime: 100, From 939611c8c244ce1abcb7310a289716fee5217eb7 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Mar 2026 11:55:52 +0100 Subject: [PATCH 4/4] fix tests --- .../tracing/metrics/element-timing/test.ts | 119 +++++++++--------- .../test/metrics/elementTiming.test.ts | 2 + 2 files changed, 62 insertions(+), 59 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts index d5dabb5d0ca5..522881e7c3dd 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts @@ -26,8 +26,8 @@ sentryTest( // Check image-fast span (this is served with a 100ms delay) const imageFastSpan = elementTimingSpans?.find(({ description }) => description === 'element[image-fast]'); - const imageFastRenderTime = imageFastSpan?.data['element.render_time']; - const imageFastLoadTime = imageFastSpan?.data['element.load_time']; + const imageFastRenderTime = imageFastSpan?.data['ui.element.render_time']; + const imageFastLoadTime = imageFastSpan?.data['ui.element.load_time']; const duration = imageFastSpan!.timestamp! - imageFastSpan!.start_timestamp; expect(imageFastSpan).toBeDefined(); @@ -35,15 +35,15 @@ sentryTest( 'sentry.op': 'ui.elementtiming', 'sentry.origin': 'auto.ui.browser.elementtiming', 'sentry.source': 'component', - 'sentry.span_start_time_source': 'load-time', - 'element.id': 'image-fast-id', - 'element.identifier': 'image-fast', - 'element.type': 'img', - 'element.size': '600x179', - 'element.url': 'https://sentry-test-site.example/path/to/image-fast.png', - 'element.render_time': expect.any(Number), - 'element.load_time': expect.any(Number), - 'element.paint_type': 'image-paint', + 'ui.element.id': 'image-fast-id', + 'ui.element.identifier': 'image-fast', + 'ui.element.type': 'img', + 'ui.element.width': 600, + 'ui.element.height': 179, + 'ui.element.url': 'https://sentry-test-site.example/path/to/image-fast.png', + 'ui.element.render_time': expect.any(Number), + 'ui.element.load_time': expect.any(Number), + 'ui.element.paint_type': 'image-paint', 'sentry.transaction_name': '/index.html', }); expect(imageFastRenderTime).toBeGreaterThan(90); @@ -55,22 +55,23 @@ sentryTest( expect(duration).toBeLessThan(20); // Check text1 span - const text1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'text1'); - const text1RenderTime = text1Span?.data['element.render_time']; - const text1LoadTime = text1Span?.data['element.load_time']; + const text1Span = elementTimingSpans?.find(({ data }) => data?.['ui.element.identifier'] === 'text1'); + const text1RenderTime = text1Span?.data['ui.element.render_time']; + const text1LoadTime = text1Span?.data['ui.element.load_time']; const text1Duration = text1Span!.timestamp! - text1Span!.start_timestamp; expect(text1Span).toBeDefined(); expect(text1Span?.data).toEqual({ 'sentry.op': 'ui.elementtiming', 'sentry.origin': 'auto.ui.browser.elementtiming', 'sentry.source': 'component', - 'sentry.span_start_time_source': 'render-time', - 'element.id': 'text1-id', - 'element.identifier': 'text1', - 'element.type': 'p', - 'element.render_time': expect.any(Number), - 'element.load_time': expect.any(Number), - 'element.paint_type': 'text-paint', + 'ui.element.id': 'text1-id', + 'ui.element.identifier': 'text1', + 'ui.element.type': 'p', + 'ui.element.width': 0, + 'ui.element.height': 0, + 'ui.element.render_time': expect.any(Number), + 'ui.element.load_time': expect.any(Number), + 'ui.element.paint_type': 'text-paint', 'sentry.transaction_name': '/index.html', }); expect(text1RenderTime).toBeGreaterThan(0); @@ -80,35 +81,35 @@ sentryTest( expect(text1Duration).toBe(0); // Check button1 span (no need for a full assertion) - const button1Span = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'button1'); + const button1Span = elementTimingSpans?.find(({ data }) => data?.['ui.element.identifier'] === 'button1'); expect(button1Span).toBeDefined(); expect(button1Span?.data).toMatchObject({ - 'element.identifier': 'button1', - 'element.type': 'button', - 'element.paint_type': 'text-paint', + 'ui.element.identifier': 'button1', + 'ui.element.type': 'button', + 'ui.element.paint_type': 'text-paint', 'sentry.transaction_name': '/index.html', }); // Check image-slow span - const imageSlowSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'image-slow'); + const imageSlowSpan = elementTimingSpans?.find(({ data }) => data?.['ui.element.identifier'] === 'image-slow'); expect(imageSlowSpan).toBeDefined(); expect(imageSlowSpan?.data).toEqual({ - 'element.id': '', - 'element.identifier': 'image-slow', - 'element.type': 'img', - 'element.size': '600x179', - 'element.url': 'https://sentry-test-site.example/path/to/image-slow.png', - 'element.paint_type': 'image-paint', - 'element.render_time': expect.any(Number), - 'element.load_time': expect.any(Number), + 'ui.element.id': '', + 'ui.element.identifier': 'image-slow', + 'ui.element.type': 'img', + 'ui.element.width': 600, + 'ui.element.height': 179, + 'ui.element.url': 'https://sentry-test-site.example/path/to/image-slow.png', + 'ui.element.paint_type': 'image-paint', + 'ui.element.render_time': expect.any(Number), + 'ui.element.load_time': expect.any(Number), 'sentry.op': 'ui.elementtiming', 'sentry.origin': 'auto.ui.browser.elementtiming', 'sentry.source': 'component', - 'sentry.span_start_time_source': 'load-time', 'sentry.transaction_name': '/index.html', }); - const imageSlowRenderTime = imageSlowSpan?.data['element.render_time']; - const imageSlowLoadTime = imageSlowSpan?.data['element.load_time']; + const imageSlowRenderTime = imageSlowSpan?.data['ui.element.render_time']; + const imageSlowLoadTime = imageSlowSpan?.data['ui.element.load_time']; const imageSlowDuration = imageSlowSpan!.timestamp! - imageSlowSpan!.start_timestamp; expect(imageSlowRenderTime).toBeGreaterThan(1400); expect(imageSlowRenderTime).toBeLessThan(2000); @@ -118,25 +119,25 @@ sentryTest( expect(imageSlowDuration).toBeLessThan(20); // Check lazy-image span - const lazyImageSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-image'); + const lazyImageSpan = elementTimingSpans?.find(({ data }) => data?.['ui.element.identifier'] === 'lazy-image'); expect(lazyImageSpan).toBeDefined(); expect(lazyImageSpan?.data).toEqual({ - 'element.id': '', - 'element.identifier': 'lazy-image', - 'element.type': 'img', - 'element.size': '600x179', - 'element.url': 'https://sentry-test-site.example/path/to/image-lazy.png', - 'element.paint_type': 'image-paint', - 'element.render_time': expect.any(Number), - 'element.load_time': expect.any(Number), + 'ui.element.id': '', + 'ui.element.identifier': 'lazy-image', + 'ui.element.type': 'img', + 'ui.element.width': 600, + 'ui.element.height': 179, + 'ui.element.url': 'https://sentry-test-site.example/path/to/image-lazy.png', + 'ui.element.paint_type': 'image-paint', + 'ui.element.render_time': expect.any(Number), + 'ui.element.load_time': expect.any(Number), 'sentry.op': 'ui.elementtiming', 'sentry.origin': 'auto.ui.browser.elementtiming', 'sentry.source': 'component', - 'sentry.span_start_time_source': 'load-time', 'sentry.transaction_name': '/index.html', }); - const lazyImageRenderTime = lazyImageSpan?.data['element.render_time']; - const lazyImageLoadTime = lazyImageSpan?.data['element.load_time']; + const lazyImageRenderTime = lazyImageSpan?.data['ui.element.render_time']; + const lazyImageLoadTime = lazyImageSpan?.data['ui.element.load_time']; const lazyImageDuration = lazyImageSpan!.timestamp! - lazyImageSpan!.start_timestamp; expect(lazyImageRenderTime).toBeGreaterThan(1000); expect(lazyImageRenderTime).toBeLessThan(1500); @@ -146,15 +147,15 @@ sentryTest( expect(lazyImageDuration).toBeLessThan(20); // Check lazy-text span - const lazyTextSpan = elementTimingSpans?.find(({ data }) => data?.['element.identifier'] === 'lazy-text'); + const lazyTextSpan = elementTimingSpans?.find(({ data }) => data?.['ui.element.identifier'] === 'lazy-text'); expect(lazyTextSpan?.data).toMatchObject({ - 'element.id': '', - 'element.identifier': 'lazy-text', - 'element.type': 'p', + 'ui.element.id': '', + 'ui.element.identifier': 'lazy-text', + 'ui.element.type': 'p', 'sentry.transaction_name': '/index.html', }); - const lazyTextRenderTime = lazyTextSpan?.data['element.render_time']; - const lazyTextLoadTime = lazyTextSpan?.data['element.load_time']; + const lazyTextRenderTime = lazyTextSpan?.data['ui.element.render_time']; + const lazyTextLoadTime = lazyTextSpan?.data['ui.element.load_time']; const lazyTextDuration = lazyTextSpan!.timestamp! - lazyTextSpan!.start_timestamp; expect(lazyTextRenderTime).toBeGreaterThan(1000); expect(lazyTextRenderTime).toBeLessThan(1500); @@ -202,15 +203,15 @@ sentryTest('emits element timing spans on navigation', async ({ getLocalTestUrl, // Image started loading after navigation, but render-time and load-time still start from the time origin // of the pageload. This is somewhat a limitation (though by design according to the ElementTiming spec) - expect((imageSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( + expect((imageSpan!.data['ui.element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( navigationStartTime, ); - expect((imageSpan!.data['element.load_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( + expect((imageSpan!.data['ui.element.load_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( navigationStartTime, ); - expect(textSpan?.data['element.load_time']).toBe(0); - expect((textSpan!.data['element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( + expect(textSpan?.data['ui.element.load_time']).toBe(0); + expect((textSpan!.data['ui.element.render_time']! as number) / 1000 + pageloadStartTime).toBeGreaterThan( navigationStartTime, ); }); diff --git a/packages/browser-utils/test/metrics/elementTiming.test.ts b/packages/browser-utils/test/metrics/elementTiming.test.ts index e74ef9374d1a..4ed5d69476d7 100644 --- a/packages/browser-utils/test/metrics/elementTiming.test.ts +++ b/packages/browser-utils/test/metrics/elementTiming.test.ts @@ -45,6 +45,7 @@ describe('_onElementTiming', () => { entryType: 'element', startTime: 0, duration: 0, + identifier: 'test-element', } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test. @@ -59,6 +60,7 @@ describe('_onElementTiming', () => { entryType: 'element', startTime, duration: 0, + identifier: 'test-element', } as Partial; // @ts-expect-error - only passing a partial entry. This is fine for the test.