From 747c0f06712242809656baee9069f5281a96d054 Mon Sep 17 00:00:00 2001 From: Martin Sonnberger Date: Tue, 27 Oct 2026 15:53:25 +0100 Subject: [PATCH] refactor(browser-utils): Use convention constants for attribute keys Co-Authored-By: GPT-6 --- .../src/performance/elementTiming.ts | 26 ++++++---- .../browser-utils/src/performance/entries.ts | 37 ++++++++++----- .../src/performance/resourceTiming.ts | 47 +++++++++++++------ .../browser-utils/src/web-vitals/emitSpan.ts | 3 +- .../browser-utils/src/web-vitals/spans.ts | 18 ++++--- .../browser-utils/src/web-vitals/tracking.ts | 42 +++++++++++++---- 6 files changed, 122 insertions(+), 51 deletions(-) diff --git a/packages/browser-utils/src/performance/elementTiming.ts b/packages/browser-utils/src/performance/elementTiming.ts index 5b8e5f01218e..1663f5096c00 100644 --- a/packages/browser-utils/src/performance/elementTiming.ts +++ b/packages/browser-utils/src/performance/elementTiming.ts @@ -1,3 +1,13 @@ +import { + SENTRY_ORIGIN, + UI_ELEMENT_HEIGHT, + UI_ELEMENT_ID, + UI_ELEMENT_IDENTIFIER, + UI_ELEMENT_PAINT_TYPE, + UI_ELEMENT_TYPE, + UI_ELEMENT_URL, + UI_ELEMENT_WIDTH, +} from '@sentry/conventions/attributes'; import type { IntegrationFn } from '@sentry/core'; import { browserPerformanceTimeOrigin, defineIntegration, filterCollectedUrl, metrics } from '@sentry/core'; import { addPerformanceInstrumentationHandler } from '../instrumentation/performanceObserver'; @@ -41,32 +51,32 @@ const _elementTimingIntegration = (() => { const loadTime = elementEntry.loadTime; const metricAttributes: Record = { - 'sentry.origin': 'auto.ui.browser.element_timing', - 'ui.element.identifier': identifier, + [SENTRY_ORIGIN]: 'auto.ui.browser.element_timing', + [UI_ELEMENT_IDENTIFIER]: identifier, }; if (paintType) { - metricAttributes['ui.element.paint_type'] = paintType; + metricAttributes[UI_ELEMENT_PAINT_TYPE] = paintType; } if (elementEntry.id) { - metricAttributes['ui.element.id'] = elementEntry.id; + metricAttributes[UI_ELEMENT_ID] = elementEntry.id; } if (elementEntry.element) { - metricAttributes['ui.element.type'] = elementEntry.element.tagName.toLowerCase(); + metricAttributes[UI_ELEMENT_TYPE] = elementEntry.element.tagName.toLowerCase(); } if (elementEntry.url) { - metricAttributes['ui.element.url'] = filterCollectedUrl(elementEntry.url); + metricAttributes[UI_ELEMENT_URL] = filterCollectedUrl(elementEntry.url); } if (elementEntry.naturalWidth) { - metricAttributes['ui.element.width'] = elementEntry.naturalWidth; + metricAttributes[UI_ELEMENT_WIDTH] = elementEntry.naturalWidth; } if (elementEntry.naturalHeight) { - metricAttributes['ui.element.height'] = elementEntry.naturalHeight; + metricAttributes[UI_ELEMENT_HEIGHT] = elementEntry.naturalHeight; } if (renderTime > 0) { diff --git a/packages/browser-utils/src/performance/entries.ts b/packages/browser-utils/src/performance/entries.ts index 905fa44426f7..8beb84e555af 100644 --- a/packages/browser-utils/src/performance/entries.ts +++ b/packages/browser-utils/src/performance/entries.ts @@ -28,6 +28,17 @@ import { URL_FULL, URL_SCHEME, SENTRY_ORIGIN, + BROWSER_SCRIPT_INVOKER, + BROWSER_SCRIPT_INVOKER_TYPE, + BROWSER_SCRIPT_SOURCE_CHAR_POSITION, + CONNECTIONTYPE, + DEVICEMEMORY, + DEVICE_MEMORY_ESTIMATED_CAPACITY, + DEVICE_PROCESSOR_COUNT, + EFFECTIVECONNECTIONTYPE, + HARDWARECONCURRENCY, + HTTP_RESPONSE_BODY_DECODED_SIZE, + RESOURCE_RENDER_BLOCKING_STATUS, } from '@sentry/conventions/attributes'; import { BROWSER_CACHE, @@ -171,8 +182,8 @@ export function startTrackingLongAnimationFrames(): void { const initialScript = entry.scripts[0]; const { invoker, invokerType, sourceURL, sourceFunctionName, sourceCharPosition } = initialScript; - attributes['browser.script.invoker'] = invoker; - attributes['browser.script.invoker_type'] = invokerType; + attributes[BROWSER_SCRIPT_INVOKER] = invoker; + attributes[BROWSER_SCRIPT_INVOKER_TYPE] = invokerType; if (sourceURL) { attributes[CODE_FILE_PATH] = sourceURL; } @@ -180,7 +191,7 @@ export function startTrackingLongAnimationFrames(): void { attributes[CODE_FUNCTION_NAME] = sourceFunctionName; } if (sourceCharPosition !== -1) { - attributes['browser.script.source_char_position'] = sourceCharPosition; + attributes[BROWSER_SCRIPT_SOURCE_CHAR_POSITION] = sourceCharPosition; } startAndEndSpan(parent, startTime, startTime + duration, { @@ -478,10 +489,10 @@ export function _addResourceSpans( ['transferSize', HTTP_RESPONSE_SIZE], ['encodedBodySize', HTTP_RESPONSE_BODY_SIZE], - ['decodedBodySize', 'http.response.body.decoded_size'], + ['decodedBodySize', HTTP_RESPONSE_BODY_DECODED_SIZE], // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/renderBlockingStatus - ['renderBlockingStatus', 'resource.render_blocking_status'], + ['renderBlockingStatus', RESOURCE_RENDER_BLOCKING_STATUS], // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/deliveryType ['deliveryType', 'http.response_delivery_type'], @@ -516,13 +527,15 @@ function _trackNavigator(span: Span, spanStreamingEnabled: boolean | undefined): if (connection) { if (connection.effectiveType) { span.setAttribute( - spanStreamingEnabled ? NETWORK_CONNECTION_EFFECTIVE_TYPE : 'effectiveConnectionType', + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + spanStreamingEnabled ? NETWORK_CONNECTION_EFFECTIVE_TYPE : EFFECTIVECONNECTIONTYPE, connection.effectiveType, ); } if (connection.type) { - span.setAttribute(spanStreamingEnabled ? NETWORK_CONNECTION_TYPE : 'connectionType', connection.type); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(spanStreamingEnabled ? NETWORK_CONNECTION_TYPE : CONNECTIONTYPE, connection.type); } if (isMeasurementValue(connection.rtt)) { @@ -538,17 +551,19 @@ function _trackNavigator(span: Span, spanStreamingEnabled: boolean | undefined): if (isMeasurementValue(navigator.deviceMemory)) { if (spanStreamingEnabled) { - span.setAttribute('device.memory.estimated_capacity', navigator.deviceMemory); + span.setAttribute(DEVICE_MEMORY_ESTIMATED_CAPACITY, navigator.deviceMemory); } else { - span.setAttribute('deviceMemory', `${navigator.deviceMemory} GB`); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(DEVICEMEMORY, `${navigator.deviceMemory} GB`); } } if (isMeasurementValue(navigator.hardwareConcurrency)) { if (spanStreamingEnabled) { - span.setAttribute('device.processor_count', navigator.hardwareConcurrency); + span.setAttribute(DEVICE_PROCESSOR_COUNT, navigator.hardwareConcurrency); } else { - span.setAttribute('hardwareConcurrency', String(navigator.hardwareConcurrency)); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(HARDWARECONCURRENCY, String(navigator.hardwareConcurrency)); } } } diff --git a/packages/browser-utils/src/performance/resourceTiming.ts b/packages/browser-utils/src/performance/resourceTiming.ts index 172a454f31da..8fb0b459b87b 100644 --- a/packages/browser-utils/src/performance/resourceTiming.ts +++ b/packages/browser-utils/src/performance/resourceTiming.ts @@ -1,3 +1,20 @@ +import { + HTTP_REQUEST_CONNECTION_END, + HTTP_REQUEST_CONNECT_START, + HTTP_REQUEST_DOMAIN_LOOKUP_END, + HTTP_REQUEST_DOMAIN_LOOKUP_START, + HTTP_REQUEST_FETCH_START, + HTTP_REQUEST_REDIRECT_END, + HTTP_REQUEST_REDIRECT_START, + HTTP_REQUEST_REQUEST_START, + HTTP_REQUEST_RESPONSE_END, + HTTP_REQUEST_RESPONSE_START, + HTTP_REQUEST_SECURE_CONNECTION_START, + HTTP_REQUEST_TIME_TO_FIRST_BYTE, + HTTP_REQUEST_WORKER_START, + NETWORK_PROTOCOL_NAME, + NETWORK_PROTOCOL_VERSION, +} from '@sentry/conventions/attributes'; import type { SpanAttributes } from '@sentry/core'; import { _INTERNAL_performanceTimeToSeconds } from '@sentry/core'; import { extractNetworkProtocol, msToSec } from './utils'; @@ -18,8 +35,8 @@ export function resourceTimingToSpanAttributes(resourceTiming: PerformanceResour // valid for `nextHopProtocol` to be an empty string. if (resourceTiming.nextHopProtocol != undefined) { const { name, version } = extractNetworkProtocol(resourceTiming.nextHopProtocol); - timingSpanData['network.protocol.version'] = version; - timingSpanData['network.protocol.name'] = name; + timingSpanData[NETWORK_PROTOCOL_VERSION] = version; + timingSpanData[NETWORK_PROTOCOL_NAME] = name; } if (_INTERNAL_performanceTimeToSeconds(resourceTiming.startTime) === undefined) { @@ -35,29 +52,29 @@ export function resourceTimingToSpanAttributes(resourceTiming: PerformanceResour return dropUndefinedKeysFromObject({ ...timingSpanData, - 'http.request.redirect_start': getAbsoluteTime(resourceTiming.redirectStart), - 'http.request.redirect_end': getAbsoluteTime(resourceTiming.redirectEnd), + [HTTP_REQUEST_REDIRECT_START]: getAbsoluteTime(resourceTiming.redirectStart), + [HTTP_REQUEST_REDIRECT_END]: getAbsoluteTime(resourceTiming.redirectEnd), - 'http.request.worker_start': getAbsoluteTime(resourceTiming.workerStart), + [HTTP_REQUEST_WORKER_START]: getAbsoluteTime(resourceTiming.workerStart), - 'http.request.fetch_start': getAbsoluteTime(resourceTiming.fetchStart), + [HTTP_REQUEST_FETCH_START]: getAbsoluteTime(resourceTiming.fetchStart), - 'http.request.domain_lookup_start': getAbsoluteTime(resourceTiming.domainLookupStart), - 'http.request.domain_lookup_end': getAbsoluteTime(resourceTiming.domainLookupEnd), + [HTTP_REQUEST_DOMAIN_LOOKUP_START]: getAbsoluteTime(resourceTiming.domainLookupStart), + [HTTP_REQUEST_DOMAIN_LOOKUP_END]: getAbsoluteTime(resourceTiming.domainLookupEnd), - 'http.request.connect_start': getAbsoluteTime(resourceTiming.connectStart), - 'http.request.secure_connection_start': getAbsoluteTime(resourceTiming.secureConnectionStart), - 'http.request.connection_end': getAbsoluteTime(resourceTiming.connectEnd), + [HTTP_REQUEST_CONNECT_START]: getAbsoluteTime(resourceTiming.connectStart), + [HTTP_REQUEST_SECURE_CONNECTION_START]: getAbsoluteTime(resourceTiming.secureConnectionStart), + [HTTP_REQUEST_CONNECTION_END]: getAbsoluteTime(resourceTiming.connectEnd), - 'http.request.request_start': getAbsoluteTime(resourceTiming.requestStart), + [HTTP_REQUEST_REQUEST_START]: getAbsoluteTime(resourceTiming.requestStart), - 'http.request.response_start': getAbsoluteTime(resourceTiming.responseStart), - 'http.request.response_end': getAbsoluteTime(resourceTiming.responseEnd), + [HTTP_REQUEST_RESPONSE_START]: getAbsoluteTime(resourceTiming.responseStart), + [HTTP_REQUEST_RESPONSE_END]: getAbsoluteTime(resourceTiming.responseEnd), // For TTFB we actually want the relative time from timeOrigin to responseStart // This way, TTFB always measures the "first page load" experience. // see: https://web.dev/articles/ttfb#measure-resource-requests - 'http.request.time_to_first_byte': + [HTTP_REQUEST_TIME_TO_FIRST_BYTE]: resourceTiming.responseStart != null ? msToSec(resourceTiming.responseStart) : undefined, }); } diff --git a/packages/browser-utils/src/web-vitals/emitSpan.ts b/packages/browser-utils/src/web-vitals/emitSpan.ts index 5e25931f4bf8..90b11673e1b6 100644 --- a/packages/browser-utils/src/web-vitals/emitSpan.ts +++ b/packages/browser-utils/src/web-vitals/emitSpan.ts @@ -11,6 +11,7 @@ import { USER_AGENT_ORIGINAL, SENTRY_OP, SENTRY_ORIGIN, + SENTRY_PAGELOAD_SPAN_ID, } from '@sentry/conventions/attributes'; import { WINDOW } from '../types'; import type { MetricNavigationType } from '../instrumentation/performanceObserver'; @@ -100,7 +101,7 @@ export function _emitWebVitalSpan(options: WebVitalSpanOptions): void { if (parentSpan && spanToJSON(parentSpan).attributes[SENTRY_OP] === 'pageload') { // for LCP and CLS, we collect the pageload span id as an attribute - attributes['sentry.pageload.span_id'] = parentSpan.spanContext().spanId; + attributes[SENTRY_PAGELOAD_SPAN_ID] = parentSpan.spanContext().spanId; } if (softNavigationId != null) { diff --git a/packages/browser-utils/src/web-vitals/spans.ts b/packages/browser-utils/src/web-vitals/spans.ts index 3361254ffc1d..ba08eceafcb0 100644 --- a/packages/browser-utils/src/web-vitals/spans.ts +++ b/packages/browser-utils/src/web-vitals/spans.ts @@ -37,6 +37,12 @@ import { BROWSER_WEB_VITAL_INP_TARGET, UI_COMPONENT_NAME, SENTRY_OP, + BROWSER_WEB_VITAL_LCP_ELEMENT, + BROWSER_WEB_VITAL_LCP_ID, + BROWSER_WEB_VITAL_LCP_LOAD_TIME, + BROWSER_WEB_VITAL_LCP_RENDER_TIME, + BROWSER_WEB_VITAL_LCP_SIZE, + BROWSER_WEB_VITAL_LCP_URL, } from '@sentry/conventions/attributes'; import { _emitWebVitalSpan } from './emitSpan'; import { isValidLcpMetric } from './lcp'; @@ -214,16 +220,16 @@ export function _sendLcpSpan( const attributes: SpanAttributes = {}; if (selector) { - attributes['browser.web_vital.lcp.element'] = selector; + attributes[BROWSER_WEB_VITAL_LCP_ELEMENT] = selector; } if (componentName) { attributes[UI_COMPONENT_NAME] = componentName; } - entry?.id && (attributes['browser.web_vital.lcp.id'] = entry.id); - entry?.url && (attributes['browser.web_vital.lcp.url'] = filterCollectedUrl(entry.url, client)); - entry?.loadTime != null && (attributes['browser.web_vital.lcp.load_time'] = entry.loadTime); - entry?.renderTime != null && (attributes['browser.web_vital.lcp.render_time'] = entry.renderTime); - entry?.size != null && (attributes['browser.web_vital.lcp.size'] = entry.size); + entry?.id && (attributes[BROWSER_WEB_VITAL_LCP_ID] = entry.id); + entry?.url && (attributes[BROWSER_WEB_VITAL_LCP_URL] = filterCollectedUrl(entry.url, client)); + entry?.loadTime != null && (attributes[BROWSER_WEB_VITAL_LCP_LOAD_TIME] = entry.loadTime); + entry?.renderTime != null && (attributes[BROWSER_WEB_VITAL_LCP_RENDER_TIME] = entry.renderTime); + entry?.size != null && (attributes[BROWSER_WEB_VITAL_LCP_SIZE] = entry.size); _emitWebVitalSpan({ name, diff --git a/packages/browser-utils/src/web-vitals/tracking.ts b/packages/browser-utils/src/web-vitals/tracking.ts index c83fd40f02f0..ee2907cf3e92 100644 --- a/packages/browser-utils/src/web-vitals/tracking.ts +++ b/packages/browser-utils/src/web-vitals/tracking.ts @@ -1,6 +1,20 @@ import type { Client, Measurements, Span } from '@sentry/core'; import { browserPerformanceTimeOrigin, debug, filterCollectedUrl, setMeasurement, spanToJSON } from '@sentry/core'; -import { BROWSER_NAVIGATION_TYPE, SENTRY_OP } from '@sentry/conventions/attributes'; +import { + BROWSER_NAVIGATION_TYPE, + SENTRY_OP, + BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START, + BROWSER_PERFORMANCE_TIME_ORIGIN, + BROWSER_WEB_VITAL_TTFB_REQUEST_TIME, + LCP_ELEMENT, + LCP_ID, + LCP_LOADTIME, + LCP_RENDERTIME, + LCP_SIZE, + LCP_URL, + PERFORMANCE_ACTIVATIONSTART, + PERFORMANCE_TIMEORIGIN, +} from '@sentry/conventions/attributes'; import { DEBUG_BUILD } from '../debug-build'; import { htmlTreeAsString } from '../htmlTreeAsString'; import type { MetricNavigationType } from '../instrumentation/performanceObserver'; @@ -186,7 +200,7 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption } }); if (_measurements['ttfb.requestTime']) { - setAttr('ttfb.requestTime', _measurements['ttfb.requestTime'].value, 'browser.web_vital.ttfb.request_time'); + setAttr('ttfb.requestTime', _measurements['ttfb.requestTime'].value, BROWSER_WEB_VITAL_TTFB_REQUEST_TIME); } } else { // If CLS is tracked as a span (span streaming), don't record CLS as a measurement @@ -213,7 +227,8 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption } // Set timeOrigin which denotes the timestamp which to base the LCP/FCP/FP/TTFB measurements on - span.setAttribute(spanStreamingEnabled ? 'browser.performance.time_origin' : 'performance.timeOrigin', timeOrigin); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(spanStreamingEnabled ? BROWSER_PERFORMANCE_TIME_ORIGIN : PERFORMANCE_TIMEORIGIN, timeOrigin); // In prerendering scenarios, where a page might be prefetched and pre-rendered before the user clicks the link, // the navigation starts earlier than when the user clicks it. Web Vitals should always be based on the @@ -222,7 +237,8 @@ export function addWebVitalsToSpan(span: Span, options: AddWebVitalsToSpanOption // This is user action is called "activation" and the time between navigation and activation is stored in // the `activationStart` attribute of the "navigation" PerformanceEntry. span.setAttribute( - spanStreamingEnabled ? 'browser.performance.navigation.activation_start' : 'performance.activationStart', + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + spanStreamingEnabled ? BROWSER_PERFORMANCE_NAVIGATION_ACTIVATION_START : PERFORMANCE_ACTIVATIONSTART, getActivationStart(), ); } @@ -244,31 +260,37 @@ function _setWebVitalAttributes(span: Span, options: AddWebVitalsToSpanOptions): // Capture Properties of the LCP element that contributes to the LCP. if (_lcpEntry.element) { - span.setAttribute('lcp.element', htmlTreeAsString(_lcpEntry.element)); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(LCP_ELEMENT, htmlTreeAsString(_lcpEntry.element)); } if (_lcpEntry.id) { - span.setAttribute('lcp.id', _lcpEntry.id); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(LCP_ID, _lcpEntry.id); } if (_lcpEntry.url) { // Trim URL to the first 200 characters. - span.setAttribute('lcp.url', filterCollectedUrl(_lcpEntry.url.trim()).slice(0, 200)); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(LCP_URL, filterCollectedUrl(_lcpEntry.url.trim()).slice(0, 200)); } if (_lcpEntry.loadTime != null) { // loadTime is the time of LCP that's related to receiving the LCP element response.. - span.setAttribute('lcp.loadTime', _lcpEntry.loadTime); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(LCP_LOADTIME, _lcpEntry.loadTime); } if (_lcpEntry.renderTime != null) { // renderTime is loadTime + rendering time // it's 0 if the LCP element is loaded from a 3rd party origin that doesn't send the // `Timing-Allow-Origin` header. - span.setAttribute('lcp.renderTime', _lcpEntry.renderTime); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(LCP_RENDERTIME, _lcpEntry.renderTime); } - span.setAttribute('lcp.size', _lcpEntry.size); + // oxlint-disable-next-line typescript/no-deprecated -- Preserve the existing attribute name. + span.setAttribute(LCP_SIZE, _lcpEntry.size); } // Only add CLS attributes if CLS is being recorded on the pageload span