diff --git a/.size-limit.js b/.size-limit.js index 8eef4950f00d..32d5d19e1495 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -135,7 +135,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '42 KB', + limit: '43 KB', }, // Svelte SDK (ESM) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts index f47552f5e9b6..633be5f570b5 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/browserTracingIntegration/http-timings/test.ts @@ -3,7 +3,7 @@ import type { Event } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should create fetch spans with http timing @firefox', async ({ browserName, getLocalTestUrl, page }) => { +sentryTest('creates fetch spans with http timing', async ({ browserName, getLocalTestUrl, page }) => { const supportedBrowsers = ['chromium', 'firefox']; if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { @@ -40,6 +40,8 @@ sentryTest('should create fetch spans with http timing @firefox', async ({ brows trace_id: tracingEvent.contexts?.trace?.trace_id, data: expect.objectContaining({ 'http.request.redirect_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.worker_start': expect.any(Number), 'http.request.fetch_start': expect.any(Number), 'http.request.domain_lookup_start': expect.any(Number), 'http.request.domain_lookup_end': expect.any(Number), @@ -49,6 +51,7 @@ sentryTest('should create fetch spans with http timing @firefox', async ({ brows 'http.request.request_start': expect.any(Number), 'http.request.response_start': expect.any(Number), 'http.request.response_end': expect.any(Number), + 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.version': expect.any(String), }), }), diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts index 38c7e61ff541..f748c339ce14 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-resource-spans/test.ts @@ -4,7 +4,7 @@ import { type Event, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORI import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; -sentryTest('should add resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { +sentryTest('adds resource spans to pageload transaction', async ({ getLocalTestUrl, page, browserName }) => { if (shouldSkipTracingTest()) { sentryTest.skip(); } @@ -74,6 +74,19 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca 'http.decoded_response_content_length': expect.any(Number), 'http.response_content_length': expect.any(Number), 'http.response_transfer_size': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.worker_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.name': '', 'network.protocol.version': 'unknown', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.img', @@ -82,6 +95,7 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca 'url.same_origin': false, 'url.scheme': 'https', ...(!isWebkitRun && { + 'http.response.status_code': expect.any(Number), 'resource.render_blocking_status': 'non-blocking', 'http.response_delivery_type': '', }), @@ -96,11 +110,30 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca trace_id: traceId, }); + // range check: TTFB must be >0 (at least in this case) and it's reasonable to + // assume <10 seconds. This also tests that we're reporting TTFB in seconds. + const imgSpanTtfb = imgSpan?.data['http.request.time_to_first_byte']; + expect(imgSpanTtfb).toBeGreaterThan(0); + expect(imgSpanTtfb).toBeLessThan(10); + expect(linkSpan).toEqual({ data: { 'http.decoded_response_content_length': expect.any(Number), 'http.response_content_length': expect.any(Number), 'http.response_transfer_size': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.worker_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.name': '', 'network.protocol.version': 'unknown', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.link', @@ -109,6 +142,7 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca 'url.same_origin': false, 'url.scheme': 'https', ...(!isWebkitRun && { + 'http.response.status_code': expect.any(Number), 'resource.render_blocking_status': 'non-blocking', 'http.response_delivery_type': '', }), @@ -128,6 +162,19 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca 'http.decoded_response_content_length': expect.any(Number), 'http.response_content_length': expect.any(Number), 'http.response_transfer_size': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.connect_start': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.worker_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.time_to_first_byte': expect.any(Number), 'network.protocol.name': '', 'network.protocol.version': 'unknown', 'sentry.op': 'resource.script', @@ -136,6 +183,7 @@ sentryTest('should add resource spans to pageload transaction', async ({ getLoca 'url.same_origin': false, 'url.scheme': 'https', ...(!isWebkitRun && { + 'http.response.status_code': expect.any(Number), 'resource.render_blocking_status': 'non-blocking', 'http.response_delivery_type': '', }), diff --git a/packages/browser-utils/src/index.ts b/packages/browser-utils/src/index.ts index bf6605f3f399..accf3cb3a278 100644 --- a/packages/browser-utils/src/index.ts +++ b/packages/browser-utils/src/index.ts @@ -30,4 +30,6 @@ export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from './instrument/ export { getBodyString, getFetchRequestArgBody, serializeFormData } from './networkUtils'; +export { resourceTimingToSpanAttributes } from './metrics/resourceTiming'; + export type { FetchHint, NetworkMetaWarning, XhrHint } from './types'; diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 870558ada39d..8b1592408e8a 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -22,13 +22,8 @@ import { addTtfbInstrumentationHandler, } from './instrument'; import { trackLcpAsStandaloneSpan } from './lcp'; -import { - extractNetworkProtocol, - getBrowserPerformanceAPI, - isMeasurementValue, - msToSec, - startAndEndSpan, -} from './utils'; +import { resourceTimingToSpanAttributes } from './resourceTiming'; +import { getBrowserPerformanceAPI, isMeasurementValue, msToSec, startAndEndSpan } from './utils'; import { getActivationStart } from './web-vitals/lib/getActivationStart'; import { getNavigationEntry } from './web-vitals/lib/getNavigationEntry'; import { getVisibilityWatcher } from './web-vitals/lib/getVisibilityWatcher'; @@ -637,7 +632,7 @@ export function _addResourceSpans( startTime: number, duration: number, timeOrigin: number, - ignoreResourceSpans?: Array, + ignoredResourceSpanOps?: Array, ): void { // we already instrument based on fetch and xhr, so we don't need to // duplicate spans here. @@ -646,31 +641,15 @@ export function _addResourceSpans( } const op = entry.initiatorType ? `resource.${entry.initiatorType}` : 'resource.other'; - if (ignoreResourceSpans?.includes(op)) { + if (ignoredResourceSpanOps?.includes(op)) { return; } - const parsedUrl = parseUrl(resourceUrl); - const attributes: SpanAttributes = { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', }; - setResourceEntrySizeData(attributes, entry, 'transferSize', 'http.response_transfer_size'); - setResourceEntrySizeData(attributes, entry, 'encodedBodySize', 'http.response_content_length'); - setResourceEntrySizeData(attributes, entry, 'decodedBodySize', 'http.decoded_response_content_length'); - - // `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; - } - // 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; - } + const parsedUrl = parseUrl(resourceUrl); 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. @@ -682,13 +661,22 @@ export function _addResourceSpans( attributes['url.same_origin'] = resourceUrl.includes(WINDOW.location.origin); - // Checking for only `undefined` and `null` is intentional because it's - // valid for `nextHopProtocol` to be an empty string. - if (entry.nextHopProtocol != null) { - const { name, version } = extractNetworkProtocol(entry.nextHopProtocol); - attributes['network.protocol.name'] = name; - attributes['network.protocol.version'] = version; - } + _setResourceRequestAttributes(entry, attributes, [ + // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus + ['responseStatus', 'http.response.status_code'], + + ['transferSize', 'http.response_transfer_size'], + ['encodedBodySize', 'http.response_content_length'], + ['decodedBodySize', 'http.decoded_response_content_length'], + + // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/renderBlockingStatus + ['renderBlockingStatus', 'resource.render_blocking_status'], + + // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/deliveryType + ['deliveryType', 'http.response_delivery_type'], + ]); + + const attributesWithResourceTiming: SpanAttributes = { ...attributes, ...resourceTimingToSpanAttributes(entry) }; const startTimestamp = timeOrigin + startTime; const endTimestamp = startTimestamp + duration; @@ -696,7 +684,7 @@ export function _addResourceSpans( startAndEndSpan(span, startTimestamp, endTimestamp, { name: resourceUrl.replace(WINDOW.location.origin, ''), op, - attributes, + attributes: attributesWithResourceTiming, }); } @@ -776,16 +764,37 @@ function _setWebVitalAttributes(span: Span, options: AddPerformanceEntriesOption } } -function setResourceEntrySizeData( +type ExperimentalResourceTimingProperty = + | 'renderBlockingStatus' + | 'deliveryType' + // For some reason, TS during build, errors on `responseStatus` not being a property of + // PerformanceResourceTiming while it actually is. Hence, we're adding it here. + // Perhaps because response status is not yet available in Webkit/Safari. + // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming/responseStatus + | 'responseStatus'; + +/** + * Use this to set any attributes we can take directly form the PerformanceResourceTiming entry. + * + * This is just a mapping function for entry->attribute to keep bundle-size minimal. + * Experimental properties are also accepted (see {@link ExperimentalResourceTimingProperty}). + * Assumes that all entry properties might be undefined for browser-specific differences. + * Only accepts string and number values for now and also sets 0-values. + */ +export function _setResourceRequestAttributes( + entry: Partial & Partial>, attributes: SpanAttributes, - entry: PerformanceResourceTiming, - key: keyof Pick, - dataKey: 'http.response_transfer_size' | 'http.response_content_length' | 'http.decoded_response_content_length', + properties: [keyof PerformanceResourceTiming | ExperimentalResourceTimingProperty, string][], ): void { - const entryVal = entry[key]; - if (entryVal != null && entryVal < MAX_INT_AS_BYTES) { - attributes[dataKey] = entryVal; - } + properties.forEach(([entryKey, attributeKey]) => { + const entryVal = entry[entryKey]; + if ( + entryVal != null && + ((typeof entryVal === 'number' && entryVal < MAX_INT_AS_BYTES) || typeof entryVal === 'string') + ) { + attributes[attributeKey] = entryVal; + } + }); } /** diff --git a/packages/browser-utils/src/metrics/resourceTiming.ts b/packages/browser-utils/src/metrics/resourceTiming.ts new file mode 100644 index 000000000000..fe613355c55d --- /dev/null +++ b/packages/browser-utils/src/metrics/resourceTiming.ts @@ -0,0 +1,60 @@ +import type { SpanAttributes } from '@sentry/core'; +import { browserPerformanceTimeOrigin } from '@sentry/core'; +import { extractNetworkProtocol, getBrowserPerformanceAPI } from './utils'; + +function getAbsoluteTime(time = 0): number { + return ((browserPerformanceTimeOrigin() || performance.timeOrigin) + time) / 1000; +} + +/** + * Converts a PerformanceResourceTiming entry to span data for the resource span. Most importantly, + * it converts the timing values from timestamps relative to the `performance.timeOrigin` to absolute timestamps + * in seconds. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming#timestamps + * + * @param resourceTiming + * @returns An array where the first element is the attribute name and the second element is the attribute value. + */ +export function resourceTimingToSpanAttributes(resourceTiming: PerformanceResourceTiming): SpanAttributes { + const timingSpanData: SpanAttributes = {}; + // Checking for only `undefined` and `null` is intentional because it's + // 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; + } + + if (!(browserPerformanceTimeOrigin() || getBrowserPerformanceAPI()?.timeOrigin)) { + return timingSpanData; + } + + return { + ...timingSpanData, + + 'http.request.redirect_start': getAbsoluteTime(resourceTiming.redirectStart), + 'http.request.redirect_end': getAbsoluteTime(resourceTiming.redirectEnd), + + 'http.request.worker_start': getAbsoluteTime(resourceTiming.workerStart), + + '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.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.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': (resourceTiming.responseStart ?? 0) / 1000, + }; +} diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 50dcfd65a528..c717bb81ca0b 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -1,4 +1,4 @@ -import type { Span } from '@sentry/core'; +import type { Span, SpanAttributes } from '@sentry/core'; import { getClient, getCurrentScope, @@ -10,7 +10,12 @@ import { spanToJSON, } from '@sentry/core'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { _addMeasureSpans, _addNavigationSpans, _addResourceSpans } from '../../src/metrics/browserMetrics'; +import { + _addMeasureSpans, + _addNavigationSpans, + _addResourceSpans, + _setResourceRequestAttributes, +} from '../../src/metrics/browserMetrics'; import { WINDOW } from '../../src/types'; import { getDefaultClientOptions, TestClient } from '../utils/TestClient'; @@ -289,6 +294,19 @@ describe('_addResourceSpans', () => { ['url.same_origin']: true, ['network.protocol.name']: 'http', ['network.protocol.version']: '1.1', + 'http.request.connect_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.time_to_first_byte': 0, + 'http.request.worker_start': expect.any(Number), }, }), ); @@ -404,7 +422,7 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ - data: { + data: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', ['http.decoded_response_content_length']: entry.decodedBodySize, @@ -416,7 +434,7 @@ describe('_addResourceSpans', () => { ['url.same_origin']: true, ['network.protocol.name']: 'http', ['network.protocol.version']: '2', - }, + }), }), ); }); @@ -441,7 +459,7 @@ describe('_addResourceSpans', () => { expect(spans).toHaveLength(1); expect(spanToJSON(spans[0]!)).toEqual( expect.objectContaining({ - data: { + data: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'resource.css', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.resource.browser.metrics', 'server.address': 'example.com', @@ -449,7 +467,7 @@ describe('_addResourceSpans', () => { 'url.scheme': 'https', ['network.protocol.name']: 'http', ['network.protocol.version']: '3', - }, + }), description: '/assets/to/css', timestamp: 468, op: 'resource.css', @@ -489,6 +507,19 @@ describe('_addResourceSpans', () => { 'url.scheme': 'https', ['network.protocol.name']: 'http', ['network.protocol.version']: '3', + 'http.request.connect_start': expect.any(Number), + 'http.request.connection_end': expect.any(Number), + 'http.request.domain_lookup_end': expect.any(Number), + 'http.request.domain_lookup_start': expect.any(Number), + 'http.request.fetch_start': expect.any(Number), + 'http.request.redirect_end': expect.any(Number), + 'http.request.redirect_start': expect.any(Number), + 'http.request.request_start': expect.any(Number), + 'http.request.response_end': expect.any(Number), + 'http.request.response_start': expect.any(Number), + 'http.request.secure_connection_start': expect.any(Number), + 'http.request.time_to_first_byte': 0, + 'http.request.worker_start': expect.any(Number), }, description: '/assets/to/css', timestamp: 468, @@ -709,6 +740,75 @@ describe('_addNavigationSpans', () => { }); }); +describe('_setResourceRequestAttributes', () => { + it('sets resource request attributes', () => { + const attributes: SpanAttributes = {}; + + const entry = mockPerformanceResourceTiming({ + transferSize: 0, + deliveryType: 'cache', + renderBlockingStatus: 'non-blocking', + responseStatus: 200, + redirectStart: 100, + responseStart: 200, + }); + + _setResourceRequestAttributes(entry, attributes, [ + ['transferSize', 'http.response_transfer_size'], + ['deliveryType', 'http.response_delivery_type'], + ['renderBlockingStatus', 'resource.render_blocking_status'], + ['responseStatus', 'http.response.status_code'], + ['redirectStart', 'http.request.redirect_start'], + ['responseStart', 'http.response.start'], + ]); + + expect(attributes).toEqual({ + 'http.response_transfer_size': 0, + 'http.request.redirect_start': 100, + 'http.response.start': 200, + 'http.response.status_code': 200, + 'http.response_delivery_type': 'cache', + 'resource.render_blocking_status': 'non-blocking', + }); + }); + + it("doesn't set other attributes", () => { + const attributes: SpanAttributes = {}; + + const entry = mockPerformanceResourceTiming({ + transferSize: 0, + deliveryType: 'cache', + renderBlockingStatus: 'non-blocking', + }); + + _setResourceRequestAttributes(entry, attributes, [['transferSize', 'http.response_transfer_size']]); + + expect(attributes).toEqual({ + 'http.response_transfer_size': 0, + }); + }); + + it("doesn't set non-primitive or undefined values", () => { + const attributes: SpanAttributes = {}; + + const entry = mockPerformanceResourceTiming({ + transferSize: undefined, + // @ts-expect-error null is invalid but let's test it anyway + deliveryType: null, + // @ts-expect-error object is invalid but let's test it anyway + renderBlockingStatus: { blocking: 'non-blocking' }, + }); + + _setResourceRequestAttributes(entry, attributes, [ + ['transferSize', 'http.response_transfer_size'], + ['deliveryType', 'http.response_delivery_type'], + ['renderBlockingStatus', 'resource.render_blocking_status'], + ]); + + expect(attributes).toEqual({}); + }); +}); + const setGlobalLocation = (location: Location) => { // @ts-expect-error need to delete this in order to set to new value delete WINDOW.location; diff --git a/packages/browser-utils/test/instrument/metrics/elementTiming.test.ts b/packages/browser-utils/test/metrics/elementTiming.test.ts similarity index 98% rename from packages/browser-utils/test/instrument/metrics/elementTiming.test.ts rename to packages/browser-utils/test/metrics/elementTiming.test.ts index 04456ceadc44..14431415873b 100644 --- a/packages/browser-utils/test/instrument/metrics/elementTiming.test.ts +++ b/packages/browser-utils/test/metrics/elementTiming.test.ts @@ -1,8 +1,8 @@ import * as sentryCore from '@sentry/core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { _onElementTiming, startTrackingElementTiming } from '../../../src/metrics/elementTiming'; -import * as browserMetricsInstrumentation from '../../../src/metrics/instrument'; -import * as browserMetricsUtils from '../../../src/metrics/utils'; +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(); diff --git a/packages/browser-utils/test/instrument/metrics/inpt.test.ts b/packages/browser-utils/test/metrics/inpt.test.ts similarity index 95% rename from packages/browser-utils/test/instrument/metrics/inpt.test.ts rename to packages/browser-utils/test/metrics/inpt.test.ts index 437ae650d0fe..bfa44b17a5b4 100644 --- a/packages/browser-utils/test/instrument/metrics/inpt.test.ts +++ b/packages/browser-utils/test/metrics/inpt.test.ts @@ -1,8 +1,8 @@ import { afterEach } from 'node:test'; import { describe, expect, it, vi } from 'vitest'; -import { _onInp, _trackINP } from '../../../src/metrics/inp'; -import * as instrument from '../../../src/metrics/instrument'; -import * as utils from '../../../src/metrics/utils'; +import { _onInp, _trackINP } from '../../src/metrics/inp'; +import * as instrument from '../../src/metrics/instrument'; +import * as utils from '../../src/metrics/utils'; describe('_trackINP', () => { const addInpInstrumentationHandler = vi.spyOn(instrument, 'addInpInstrumentationHandler'); diff --git a/packages/browser/test/tracing/resource-timing.test.ts b/packages/browser-utils/test/metrics/resourceTiming.test.ts similarity index 60% rename from packages/browser/test/tracing/resource-timing.test.ts rename to packages/browser-utils/test/metrics/resourceTiming.test.ts index c3ad8b9c0c85..881a7075441e 100644 --- a/packages/browser/test/tracing/resource-timing.test.ts +++ b/packages/browser-utils/test/metrics/resourceTiming.test.ts @@ -1,8 +1,8 @@ import * as utils from '@sentry/core'; -import * as browserUtils from '@sentry-internal/browser-utils'; import type { MockInstance } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { resourceTimingToSpanAttributes } from '../../src/tracing/resource-timing'; +import { resourceTimingToSpanAttributes } from '../../src/metrics/resourceTiming'; +import * as browserMetricsUtils from '../../src/metrics/utils'; describe('resourceTimingToSpanAttributes', () => { let browserPerformanceTimeOriginSpy: MockInstance; @@ -11,7 +11,7 @@ describe('resourceTimingToSpanAttributes', () => { beforeEach(() => { vi.clearAllMocks(); browserPerformanceTimeOriginSpy = vi.spyOn(utils, 'browserPerformanceTimeOrigin'); - extractNetworkProtocolSpy = vi.spyOn(browserUtils, 'extractNetworkProtocol'); + extractNetworkProtocolSpy = vi.spyOn(browserMetricsUtils, 'extractNetworkProtocol'); }); afterEach(() => { @@ -48,7 +48,7 @@ describe('resourceTimingToSpanAttributes', () => { }; describe('with network protocol information', () => { - it('should extract network protocol when nextHopProtocol is available', () => { + it('extracts network protocol when nextHopProtocol is available', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: 'h2', }); @@ -70,16 +70,16 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); expect(extractNetworkProtocolSpy).toHaveBeenCalledWith('h2'); - expect(result).toEqual([ - ['network.protocol.version', '2.0'], - ['network.protocol.name', 'http'], - ]); + expect(result).toEqual({ + 'network.protocol.version': '2.0', + 'network.protocol.name': 'http', + }); // Restore global performance global.performance = originalPerformance; }); - it('should handle different network protocols', () => { + it('handles different network protocols', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: 'http/1.1', }); @@ -101,16 +101,16 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); expect(extractNetworkProtocolSpy).toHaveBeenCalledWith('http/1.1'); - expect(result).toEqual([ - ['network.protocol.version', '1.1'], - ['network.protocol.name', 'http'], - ]); + expect(result).toEqual({ + 'network.protocol.version': '1.1', + 'network.protocol.name': 'http', + }); // Restore global performance global.performance = originalPerformance; }); - it('should extract network protocol even when nextHopProtocol is empty', () => { + it('extracts network protocol even when nextHopProtocol is empty', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: '', }); @@ -132,16 +132,16 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); expect(extractNetworkProtocolSpy).toHaveBeenCalledWith(''); - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + }); // Restore global performance global.performance = originalPerformance; }); - it('should not extract network protocol when nextHopProtocol is undefined', () => { + it("doesn't extract network protocol when nextHopProtocol is undefined", () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: undefined as any, }); @@ -158,7 +158,7 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); expect(extractNetworkProtocolSpy).not.toHaveBeenCalled(); - expect(result).toEqual([]); + expect(result).toEqual({}); // Restore global performance global.performance = originalPerformance; @@ -166,7 +166,7 @@ describe('resourceTimingToSpanAttributes', () => { }); describe('without browserPerformanceTimeOrigin', () => { - it('should return only network protocol data when browserPerformanceTimeOrigin is not available', () => { + it('returns only network protocol data when browserPerformanceTimeOrigin is not available', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: 'h2', }); @@ -187,16 +187,16 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', '2.0'], - ['network.protocol.name', 'http'], - ]); + expect(result).toEqual({ + 'network.protocol.version': '2.0', + 'network.protocol.name': 'http', + }); // Restore global performance global.performance = originalPerformance; }); - it('should return network protocol attributes even when empty string and no browserPerformanceTimeOrigin', () => { + it('returns network protocol attributes even when empty string and no browserPerformanceTimeOrigin', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: '', }); @@ -217,10 +217,10 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + }); // Restore global performance global.performance = originalPerformance; @@ -232,10 +232,12 @@ describe('resourceTimingToSpanAttributes', () => { browserPerformanceTimeOriginSpy.mockReturnValue(1000000); // 1 second in milliseconds }); - it('should include all timing attributes when browserPerformanceTimeOrigin is available', () => { + it('includes all timing attributes when browserPerformanceTimeOrigin is available', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: 'h2', redirectStart: 10, + redirectEnd: 20, + workerStart: 22, fetchStart: 25, domainLookupStart: 30, domainLookupEnd: 35, @@ -254,23 +256,26 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', '2.0'], - ['network.protocol.name', 'http'], - ['http.request.redirect_start', 1000.01], // (1000000 + 10) / 1000 - ['http.request.fetch_start', 1000.025], // (1000000 + 25) / 1000 - ['http.request.domain_lookup_start', 1000.03], // (1000000 + 30) / 1000 - ['http.request.domain_lookup_end', 1000.035], // (1000000 + 35) / 1000 - ['http.request.connect_start', 1000.04], // (1000000 + 40) / 1000 - ['http.request.secure_connection_start', 1000.045], // (1000000 + 45) / 1000 - ['http.request.connection_end', 1000.05], // (1000000 + 50) / 1000 - ['http.request.request_start', 1000.055], // (1000000 + 55) / 1000 - ['http.request.response_start', 1000.15], // (1000000 + 150) / 1000 - ['http.request.response_end', 1000.2], // (1000000 + 200) / 1000 - ]); + expect(result).toEqual({ + 'network.protocol.version': '2.0', + 'network.protocol.name': 'http', + 'http.request.redirect_start': 1000.01, // (1000000 + 10) / 1000 + 'http.request.redirect_end': 1000.02, // (1000000 + 20) / 1000 + 'http.request.worker_start': 1000.022, // (1000000 + 22) / 1000 + 'http.request.fetch_start': 1000.025, // (1000000 + 25) / 1000 + 'http.request.domain_lookup_start': 1000.03, // (1000000 + 30) / 1000 + 'http.request.domain_lookup_end': 1000.035, // (1000000 + 35) / 1000 + 'http.request.connect_start': 1000.04, // (1000000 + 40) / 1000 + 'http.request.secure_connection_start': 1000.045, // (1000000 + 45) / 1000 + 'http.request.connection_end': 1000.05, // (1000000 + 50) / 1000 + 'http.request.request_start': 1000.055, // (1000000 + 55) / 1000 + 'http.request.response_start': 1000.15, // (1000000 + 150) / 1000 + 'http.request.response_end': 1000.2, // (1000000 + 200) / 1000 + 'http.request.time_to_first_byte': 0.15, // 150 / 1000 + }); }); - it('should handle zero timing values', () => { + it('handles zero timing values', () => { extractNetworkProtocolSpy.mockReturnValue({ name: '', version: 'unknown', @@ -292,23 +297,26 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ['http.request.redirect_start', 1000], // (1000000 + 0) / 1000 - ['http.request.fetch_start', 1000], - ['http.request.domain_lookup_start', 1000], - ['http.request.domain_lookup_end', 1000], - ['http.request.connect_start', 1000], - ['http.request.secure_connection_start', 1000], - ['http.request.connection_end', 1000], - ['http.request.request_start', 1000], - ['http.request.response_start', 1000], - ['http.request.response_end', 1000], - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + 'http.request.redirect_start': 1000, // (1000000 + 0) / 1000 + 'http.request.redirect_end': 1000.02, + 'http.request.worker_start': 1000, + 'http.request.fetch_start': 1000, + 'http.request.domain_lookup_start': 1000, + 'http.request.domain_lookup_end': 1000, + 'http.request.connect_start': 1000, + 'http.request.secure_connection_start': 1000, + 'http.request.connection_end': 1000, + 'http.request.request_start': 1000, + 'http.request.response_start': 1000, + 'http.request.response_end': 1000, + 'http.request.time_to_first_byte': 0, + }); }); - it('should combine network protocol and timing attributes', () => { + it('combines network protocol and timing attributes', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: 'http/1.1', redirectStart: 5, @@ -330,25 +338,28 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', '1.1'], - ['network.protocol.name', 'http'], - ['http.request.redirect_start', 1000.005], - ['http.request.fetch_start', 1000.01], - ['http.request.domain_lookup_start', 1000.015], - ['http.request.domain_lookup_end', 1000.02], - ['http.request.connect_start', 1000.025], - ['http.request.secure_connection_start', 1000.03], - ['http.request.connection_end', 1000.035], - ['http.request.request_start', 1000.04], - ['http.request.response_start', 1000.08], - ['http.request.response_end', 1000.1], - ]); + expect(result).toEqual({ + 'network.protocol.version': '1.1', + 'network.protocol.name': 'http', + 'http.request.redirect_start': 1000.005, + 'http.request.redirect_end': 1000.02, + 'http.request.worker_start': 1000, + 'http.request.fetch_start': 1000.01, + 'http.request.domain_lookup_start': 1000.015, + 'http.request.domain_lookup_end': 1000.02, + 'http.request.connect_start': 1000.025, + 'http.request.secure_connection_start': 1000.03, + 'http.request.connection_end': 1000.035, + 'http.request.request_start': 1000.04, + 'http.request.response_start': 1000.08, + 'http.request.response_end': 1000.1, + 'http.request.time_to_first_byte': 0.08, + }); }); }); describe('fallback to performance.timeOrigin', () => { - it('should use performance.timeOrigin when browserPerformanceTimeOrigin returns null', () => { + it('uses performance.timeOrigin when browserPerformanceTimeOrigin returns null', () => { // Mock browserPerformanceTimeOrigin to return null for the main check browserPerformanceTimeOriginSpy.mockReturnValue(null); @@ -374,13 +385,13 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); // When browserPerformanceTimeOrigin returns null, function returns early with only network protocol attributes - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + }); }); - it('should use performance.timeOrigin fallback in getAbsoluteTime when available', () => { + it('uses performance.timeOrigin fallback in getAbsoluteTime when available', () => { // Mock browserPerformanceTimeOrigin to return 500000 for the main check browserPerformanceTimeOriginSpy.mockReturnValue(500000); @@ -392,6 +403,8 @@ describe('resourceTimingToSpanAttributes', () => { const mockResourceTiming = createMockResourceTiming({ nextHopProtocol: '', redirectStart: 20, + redirectEnd: 30, + workerStart: 35, fetchStart: 40, domainLookupStart: 60, domainLookupEnd: 80, @@ -405,23 +418,26 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ['http.request.redirect_start', 500.02], // (500000 + 20) / 1000 - ['http.request.fetch_start', 500.04], // (500000 + 40) / 1000 - ['http.request.domain_lookup_start', 500.06], // (500000 + 60) / 1000 - ['http.request.domain_lookup_end', 500.08], // (500000 + 80) / 1000 - ['http.request.connect_start', 500.1], // (500000 + 100) / 1000 - ['http.request.secure_connection_start', 500.12], // (500000 + 120) / 1000 - ['http.request.connection_end', 500.14], // (500000 + 140) / 1000 - ['http.request.request_start', 500.16], // (500000 + 160) / 1000 - ['http.request.response_start', 500.3], // (500000 + 300) / 1000 - ['http.request.response_end', 500.4], // (500000 + 400) / 1000 - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + 'http.request.redirect_start': 500.02, // (500000 + 20) / 1000 + 'http.request.redirect_end': 500.03, // (500000 + 30) / 1000 + 'http.request.worker_start': 500.035, // (500000 + 35) / 1000 + 'http.request.fetch_start': 500.04, // (500000 + 40) / 1000 + 'http.request.domain_lookup_start': 500.06, // (500000 + 60) / 1000 + 'http.request.domain_lookup_end': 500.08, // (500000 + 80) / 1000 + 'http.request.connect_start': 500.1, // (500000 + 100) / 1000 + 'http.request.secure_connection_start': 500.12, // (500000 + 120) / 1000 + 'http.request.connection_end': 500.14, // (500000 + 140) / 1000 + 'http.request.request_start': 500.16, // (500000 + 160) / 1000 + 'http.request.response_start': 500.3, // (500000 + 300) / 1000 + 'http.request.response_end': 500.4, // (500000 + 400) / 1000 + 'http.request.time_to_first_byte': 0.3, // 300 / 1000 + }); }); - it('should handle case when neither browserPerformanceTimeOrigin nor performance.timeOrigin is available', () => { + it('handles case when neither browserPerformanceTimeOrigin nor performance.timeOrigin is available', () => { browserPerformanceTimeOriginSpy.mockReturnValue(null); extractNetworkProtocolSpy.mockReturnValue({ @@ -443,10 +459,10 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); // When neither timing source is available, should return network protocol attributes for empty string - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + }); // Restore global performance global.performance = originalPerformance; @@ -454,7 +470,7 @@ describe('resourceTimingToSpanAttributes', () => { }); describe('edge cases', () => { - it('should handle undefined timing values', () => { + it('handles undefined timing values', () => { browserPerformanceTimeOriginSpy.mockReturnValue(1000000); extractNetworkProtocolSpy.mockReturnValue({ @@ -466,6 +482,7 @@ describe('resourceTimingToSpanAttributes', () => { nextHopProtocol: '', redirectStart: undefined as any, fetchStart: undefined as any, + workerStart: undefined as any, domainLookupStart: undefined as any, domainLookupEnd: undefined as any, connectStart: undefined as any, @@ -478,23 +495,26 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ['http.request.redirect_start', 1000], // (1000000 + 0) / 1000 - ['http.request.fetch_start', 1000], - ['http.request.domain_lookup_start', 1000], - ['http.request.domain_lookup_end', 1000], - ['http.request.connect_start', 1000], - ['http.request.secure_connection_start', 1000], - ['http.request.connection_end', 1000], - ['http.request.request_start', 1000], - ['http.request.response_start', 1000], - ['http.request.response_end', 1000], - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + 'http.request.redirect_start': 1000, // (1000000 + 0) / 1000 + 'http.request.redirect_end': 1000.02, + 'http.request.worker_start': 1000, + 'http.request.fetch_start': 1000, + 'http.request.domain_lookup_start': 1000, + 'http.request.domain_lookup_end': 1000, + 'http.request.connect_start': 1000, + 'http.request.secure_connection_start': 1000, + 'http.request.connection_end': 1000, + 'http.request.request_start': 1000, + 'http.request.response_start': 1000, + 'http.request.response_end': 1000, + 'http.request.time_to_first_byte': 0, + }); }); - it('should handle very large timing values', () => { + it('handles very large timing values', () => { browserPerformanceTimeOriginSpy.mockReturnValue(1000000); extractNetworkProtocolSpy.mockReturnValue({ @@ -518,20 +538,23 @@ describe('resourceTimingToSpanAttributes', () => { const result = resourceTimingToSpanAttributes(mockResourceTiming); - expect(result).toEqual([ - ['network.protocol.version', 'unknown'], - ['network.protocol.name', ''], - ['http.request.redirect_start', 1999.999], // (1000000 + 999999) / 1000 - ['http.request.fetch_start', 1999.999], - ['http.request.domain_lookup_start', 1999.999], - ['http.request.domain_lookup_end', 1999.999], - ['http.request.connect_start', 1999.999], - ['http.request.secure_connection_start', 1999.999], - ['http.request.connection_end', 1999.999], - ['http.request.request_start', 1999.999], - ['http.request.response_start', 1999.999], - ['http.request.response_end', 1999.999], - ]); + expect(result).toEqual({ + 'network.protocol.version': 'unknown', + 'network.protocol.name': '', + 'http.request.redirect_start': 1999.999, // (1000000 + 999999) / 1000 + 'http.request.redirect_end': 1000.02, + 'http.request.worker_start': 1000, + 'http.request.fetch_start': 1999.999, + 'http.request.domain_lookup_start': 1999.999, + 'http.request.domain_lookup_end': 1999.999, + 'http.request.connect_start': 1999.999, + 'http.request.secure_connection_start': 1999.999, + 'http.request.connection_end': 1999.999, + 'http.request.request_start': 1999.999, + 'http.request.response_start': 1999.999, + 'http.request.response_end': 1999.999, + 'http.request.time_to_first_byte': 999.999, + }); }); }); }); diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index d046793b42a1..1216c93ec9d3 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -22,11 +22,11 @@ import type { XhrHint } from '@sentry-internal/browser-utils'; import { addPerformanceInstrumentationHandler, addXhrInstrumentationHandler, + resourceTimingToSpanAttributes, SENTRY_XHR_DATA_KEY, } from '@sentry-internal/browser-utils'; import type { BrowserClient } from '../client'; import { WINDOW } from '../helpers'; -import { resourceTimingToSpanAttributes } from './resource-timing'; /** Options for Request Instrumentation */ export interface RequestInstrumentationOptions { @@ -249,8 +249,7 @@ function addHTTPTimings(span: Span): void { const cleanup = addPerformanceInstrumentationHandler('resource', ({ entries }) => { entries.forEach(entry => { if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) { - const spanAttributes = resourceTimingToSpanAttributes(entry); - spanAttributes.forEach(attributeArray => span.setAttribute(...attributeArray)); + span.setAttributes(resourceTimingToSpanAttributes(entry)); // In the next tick, clean this handler up // We have to wait here because otherwise this cleans itself up before it is fully done setTimeout(cleanup); diff --git a/packages/browser/src/tracing/resource-timing.ts b/packages/browser/src/tracing/resource-timing.ts deleted file mode 100644 index c741a7e91016..000000000000 --- a/packages/browser/src/tracing/resource-timing.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Span } from '@sentry/core'; -import { browserPerformanceTimeOrigin } from '@sentry/core'; -import { extractNetworkProtocol } from '@sentry-internal/browser-utils'; - -function getAbsoluteTime(time: number = 0): number { - return ((browserPerformanceTimeOrigin() || performance.timeOrigin) + time) / 1000; -} - -/** - * Converts a PerformanceResourceTiming entry to span data for the resource span. - * - * @param resourceTiming - * @returns An array where the first element is the attribute name and the second element is the attribute value. - */ -export function resourceTimingToSpanAttributes( - resourceTiming: PerformanceResourceTiming, -): Array> { - const timingSpanData: Array> = []; - // Checking for only `undefined` and `null` is intentional because it's - // valid for `nextHopProtocol` to be an empty string. - if (resourceTiming.nextHopProtocol != undefined) { - const { name, version } = extractNetworkProtocol(resourceTiming.nextHopProtocol); - timingSpanData.push(['network.protocol.version', version], ['network.protocol.name', name]); - } - if (!browserPerformanceTimeOrigin()) { - return timingSpanData; - } - return [ - ...timingSpanData, - ['http.request.redirect_start', getAbsoluteTime(resourceTiming.redirectStart)], - ['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.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.response_start', getAbsoluteTime(resourceTiming.responseStart)], - ['http.request.response_end', getAbsoluteTime(resourceTiming.responseEnd)], - ]; -}