diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index a3200abdeeaa..aadde247642c 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -300,7 +300,7 @@ interface AddPerformanceEntriesOptions { /** Add performance related spans to a transaction */ export function addPerformanceEntries(span: Span, options: AddPerformanceEntriesOptions): void { const performance = getBrowserPerformanceAPI(); - if (!performance || !WINDOW.performance.getEntries || !browserPerformanceTimeOrigin) { + if (!performance || !performance.getEntries || !browserPerformanceTimeOrigin) { // Gatekeeper if performance API not available return; } @@ -311,8 +311,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries const { op, start_timestamp: transactionStartTime } = spanToJSON(span); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - performanceEntries.slice(_performanceCursor).forEach((entry: Record) => { + performanceEntries.slice(_performanceCursor).forEach(entry => { const startTime = msToSec(entry.startTime); const duration = msToSec( // Inexplicably, Chrome sometimes emits a negative duration. We need to work around this. @@ -328,7 +327,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries switch (entry.entryType) { case 'navigation': { - _addNavigationSpans(span, entry, timeOrigin); + _addNavigationSpans(span, entry as PerformanceNavigationTiming, timeOrigin); break; } case 'mark': @@ -350,10 +349,9 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries break; } case 'resource': { - _addResourceSpans(span, entry, entry.name as string, startTime, duration, timeOrigin); + _addResourceSpans(span, entry as PerformanceResourceTiming, entry.name, startTime, duration, timeOrigin); break; } - default: // Ignore other entry types. } }); @@ -411,11 +409,13 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries _measurements = {}; } -/** Create measure related spans */ +/** + * Create measure related spans. + * Exported only for tests. + */ export function _addMeasureSpans( span: Span, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - entry: Record, + entry: PerformanceEntry, startTime: number, duration: number, timeOrigin: number, @@ -454,34 +454,53 @@ export function _addMeasureSpans( } /** Instrument navigation entries */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function _addNavigationSpans(span: Span, entry: Record, timeOrigin: number): void { - ['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'].forEach(event => { +function _addNavigationSpans(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void { + (['unloadEvent', 'redirect', 'domContentLoadedEvent', 'loadEvent', 'connect'] as const).forEach(event => { _addPerformanceNavigationTiming(span, entry, event, timeOrigin); }); - _addPerformanceNavigationTiming(span, entry, 'secureConnection', timeOrigin, 'TLS/SSL', 'connectEnd'); - _addPerformanceNavigationTiming(span, entry, 'fetch', timeOrigin, 'cache', 'domainLookupStart'); + _addPerformanceNavigationTiming(span, entry, 'secureConnection', timeOrigin, 'TLS/SSL'); + _addPerformanceNavigationTiming(span, entry, 'fetch', timeOrigin, 'cache'); _addPerformanceNavigationTiming(span, entry, 'domainLookup', timeOrigin, 'DNS'); + _addRequest(span, entry, timeOrigin); } +type StartEventName = + | 'secureConnection' + | 'fetch' + | 'domainLookup' + | 'unloadEvent' + | 'redirect' + | 'connect' + | 'domContentLoadedEvent' + | 'loadEvent'; + +type EndEventName = + | 'connectEnd' + | 'domainLookupStart' + | 'domainLookupEnd' + | 'unloadEventEnd' + | 'redirectEnd' + | 'connectEnd' + | 'domContentLoadedEventEnd' + | 'loadEventEnd'; + /** Create performance navigation related spans */ function _addPerformanceNavigationTiming( span: Span, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - entry: Record, - event: string, + entry: PerformanceNavigationTiming, + event: StartEventName, timeOrigin: number, - name?: string, - eventEnd?: string, + name: string = event, ): void { - const end = eventEnd ? (entry[eventEnd] as number | undefined) : (entry[`${event}End`] as number | undefined); - const start = entry[`${event}Start`] as number | undefined; + const eventEnd = _getEndPropertyNameForNavigationTiming(event) satisfies keyof PerformanceNavigationTiming; + const end = entry[eventEnd]; + const start = entry[`${event}Start`]; if (!start || !end) { return; } startAndEndSpan(span, timeOrigin + msToSec(start), timeOrigin + msToSec(end), { - op: `browser.${name || event}`, + op: `browser.${name}`, name: entry.name, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.browser.metrics', @@ -489,9 +508,18 @@ function _addPerformanceNavigationTiming( }); } +function _getEndPropertyNameForNavigationTiming(event: StartEventName): EndEventName { + if (event === 'secureConnection') { + return 'connectEnd'; + } + if (event === 'fetch') { + return 'domainLookupStart'; + } + return `${event}End`; +} + /** Create request and response related spans */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function _addRequest(span: Span, entry: Record, timeOrigin: number): void { +function _addRequest(span: Span, entry: PerformanceNavigationTiming, timeOrigin: number): void { const requestStartTimestamp = timeOrigin + msToSec(entry.requestStart as number); const responseEndTimestamp = timeOrigin + msToSec(entry.responseEnd as number); const responseStartTimestamp = timeOrigin + msToSec(entry.responseStart as number); @@ -518,19 +546,13 @@ function _addRequest(span: Span, entry: Record, timeOrigin: number) } } -export interface ResourceEntry extends Record { - initiatorType?: string; - transferSize?: number; - encodedBodySize?: number; - decodedBodySize?: number; - renderBlockingStatus?: string; - deliveryType?: string; -} - -/** Create resource-related spans */ +/** + * Create resource-related spans. + * Exported only for tests. + */ export function _addResourceSpans( span: Span, - entry: ResourceEntry, + entry: PerformanceResourceTiming, resourceUrl: string, startTime: number, duration: number, @@ -551,13 +573,19 @@ export function _addResourceSpans( setResourceEntrySizeData(attributes, entry, 'encodedBodySize', 'http.response_content_length'); setResourceEntrySizeData(attributes, entry, 'decodedBodySize', 'http.decoded_response_content_length'); - if (entry.deliveryType != null) { - attributes['http.response_delivery_type'] = entry.deliveryType; + // `deliveryType` is experimental and does not exist everywhere + const deliveryType = (entry as { deliveryType?: 'cache' | 'navigational-prefetch' | '' }).deliveryType; + if (deliveryType != null) { + attributes['http.response_delivery_type'] = deliveryType; } - if ('renderBlockingStatus' in entry) { - attributes['resource.render_blocking_status'] = entry.renderBlockingStatus; + // Types do not reflect this property yet + const renderBlockingStatus = (entry as { renderBlockingStatus?: 'render-blocking' | 'non-render-blocking' }) + .renderBlockingStatus; + if (renderBlockingStatus) { + attributes['resource.render_blocking_status'] = renderBlockingStatus; } + if (parsedUrl.protocol) { attributes['url.scheme'] = parsedUrl.protocol.split(':').pop(); // the protocol returned by parseUrl includes a :, but OTEL spec does not, so we remove it. } @@ -655,8 +683,8 @@ function _setWebVitalAttributes(span: Span): void { function setResourceEntrySizeData( attributes: SpanAttributes, - entry: ResourceEntry, - key: keyof Pick, + entry: PerformanceResourceTiming, + key: keyof Pick, dataKey: 'http.response_transfer_size' | 'http.response_content_length' | 'http.decoded_response_content_length', ): void { const entryVal = entry[key]; diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 6ca155bccfc2..2ff1c2df209a 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -9,7 +9,6 @@ import { spanToJSON, } from '@sentry/core'; import type { Span } from '@sentry/core'; -import type { ResourceEntry } from '../../src/metrics/browserMetrics'; import { _addMeasureSpans, _addResourceSpans } from '../../src/metrics/browserMetrics'; import { WINDOW } from '../../src/types'; import { TestClient, getDefaultClientOptions } from '../utils/TestClient'; @@ -31,6 +30,17 @@ const originalLocation = WINDOW.location; const resourceEntryName = 'https://example.com/assets/to/css'; +interface AdditionalPerformanceResourceTiming { + renderBlockingStatus?: 'non-blocking' | 'blocking' | ''; + deliveryType?: 'cache' | 'navigational-prefetch' | ''; +} + +function mockPerformanceResourceTiming( + data: Partial & AdditionalPerformanceResourceTiming, +): PerformanceResourceTiming & AdditionalPerformanceResourceTiming { + return data as PerformanceResourceTiming & AdditionalPerformanceResourceTiming; +} + describe('_addMeasureSpans', () => { const span = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); @@ -54,13 +64,12 @@ describe('_addMeasureSpans', () => { spans.push(span); }); - const entry: Omit = { + const entry = { entryType: 'measure', name: 'measure-1', duration: 10, startTime: 12, - detail: undefined, - }; + } as PerformanceEntry; const timeOrigin = 100; const startTime = 23; @@ -116,13 +125,13 @@ describe('_addResourceSpans', () => { spans.push(span); }); - const entry: ResourceEntry = { + const entry = mockPerformanceResourceTiming({ initiatorType: 'xmlhttprequest', transferSize: 256, encodedBodySize: 256, decodedBodySize: 256, renderBlockingStatus: 'non-blocking', - }; + }); _addResourceSpans(span, entry, resourceEntryName, 123, 456, 100); expect(spans).toHaveLength(0); @@ -135,13 +144,13 @@ describe('_addResourceSpans', () => { spans.push(span); }); - const entry: ResourceEntry = { + const entry = mockPerformanceResourceTiming({ initiatorType: 'fetch', transferSize: 256, encodedBodySize: 256, decodedBodySize: 256, renderBlockingStatus: 'non-blocking', - }; + }); _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 456, 100); expect(spans).toHaveLength(0); @@ -154,13 +163,13 @@ describe('_addResourceSpans', () => { spans.push(span); }); - const entry: ResourceEntry = { + const entry = mockPerformanceResourceTiming({ initiatorType: 'css', transferSize: 256, encodedBodySize: 456, decodedBodySize: 593, renderBlockingStatus: 'non-blocking', - }; + }); const timeOrigin = 100; const startTime = 23; @@ -222,9 +231,9 @@ describe('_addResourceSpans', () => { ]; for (let i = 0; i < table.length; i++) { const { initiatorType, op } = table[i]!; - const entry: ResourceEntry = { + const entry = mockPerformanceResourceTiming({ initiatorType, - }; + }); _addResourceSpans(span, entry, 'https://example.com/assets/to/me', 123, 234, 465); expect(spans).toHaveLength(i + 1); @@ -239,13 +248,13 @@ describe('_addResourceSpans', () => { spans.push(span); }); - const entry: ResourceEntry = { + const entry = mockPerformanceResourceTiming({ initiatorType: 'css', transferSize: 0, encodedBodySize: 0, decodedBodySize: 0, renderBlockingStatus: 'non-blocking', - }; + }); _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); @@ -274,12 +283,12 @@ describe('_addResourceSpans', () => { spans.push(span); }); - const entry: ResourceEntry = { + const entry = mockPerformanceResourceTiming({ initiatorType: 'css', transferSize: 2147483647, encodedBodySize: 2147483647, decodedBodySize: 2147483647, - }; + }); _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); @@ -316,7 +325,7 @@ describe('_addResourceSpans', () => { transferSize: null, encodedBodySize: null, decodedBodySize: null, - } as unknown as ResourceEntry; + } as unknown as PerformanceResourceTiming; _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345); @@ -341,7 +350,7 @@ describe('_addResourceSpans', () => { // resource delivery types: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/deliveryType // i.e. better but not yet widely supported way to check for browser cache hit - it.each(['cache', 'navigational-prefetch', ''])( + it.each(['cache', 'navigational-prefetch', ''] as const)( 'attaches delivery type ("%s") to resource spans if available', deliveryType => { const spans: Span[] = []; @@ -350,13 +359,13 @@ describe('_addResourceSpans', () => { spans.push(span); }); - const entry: ResourceEntry = { + const entry = mockPerformanceResourceTiming({ initiatorType: 'css', transferSize: 0, encodedBodySize: 0, decodedBodySize: 0, deliveryType, - }; + }); _addResourceSpans(span, entry, resourceEntryName, 100, 23, 345);