From c371f531382147759c8597568f75d1f9f2b54fc2 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 3 Sep 2026 16:13:44 +0200 Subject: [PATCH 1/7] chore: move sdk-trace-web utils to web-common --- .../package.json | 4 +- .../src/fetch.ts | 31 +- .../tsconfig.esm.json | 6 +- .../tsconfig.esnext.json | 6 +- .../tsconfig.json | 6 +- experimental/packages/web-common/package.json | 1 + .../src/enums/PerformanceTimingNames.ts | 30 + experimental/packages/web-common/src/index.ts | 21 +- .../packages/web-common/src/semconv.ts | 30 + .../web-common/src/types/Performance.ts | 48 ++ .../web-common/src/utils-resource-timings.ts | 506 ++++++++++++++ .../test/utils-resource-timings.test.ts | 651 ++++++++++++++++++ .../packages/web-common/tsconfig.esm.json | 3 + .../packages/web-common/tsconfig.esnext.json | 3 + .../packages/web-common/tsconfig.json | 3 + package-lock.json | 29 +- 16 files changed, 1327 insertions(+), 51 deletions(-) create mode 100644 experimental/packages/web-common/src/enums/PerformanceTimingNames.ts create mode 100644 experimental/packages/web-common/src/types/Performance.ts create mode 100644 experimental/packages/web-common/src/utils-resource-timings.ts create mode 100644 experimental/packages/web-common/test/utils-resource-timings.test.ts diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/package.json b/experimental/packages/opentelemetry-instrumentation-fetch/package.json index 08574ee9a2f..3eb7d22be5b 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/package.json +++ b/experimental/packages/opentelemetry-instrumentation-fetch/package.json @@ -86,8 +86,8 @@ "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/instrumentation": "0.222.0", - "@opentelemetry/sdk-trace-web": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/semantic-conventions": "^1.29.0", + "@opentelemetry/web-common": "0.222.0" }, "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation-fetch", "sideEffects": false, diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts index 5d4573df31d..ed5f5bc7e5c 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts @@ -17,8 +17,6 @@ import { safeExecuteInTheMiddle, } from '@opentelemetry/instrumentation'; import * as core from '@opentelemetry/core'; -import * as web from '@opentelemetry/sdk-trace-web'; -import { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv'; import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, @@ -28,7 +26,16 @@ import { ATTR_SERVER_PORT, ATTR_URL_FULL, } from '@opentelemetry/semantic-conventions'; +import type { PropagateTraceHeaderCorsUrls } from '@opentelemetry/web-common'; +import { + PerformanceTimingNames, + addSpanNetworkEvents, + getResource, + parseUrl, + shouldPropagateTraceHeaders, +} from '@opentelemetry/web-common'; import type { FetchError, FetchResponse, SpanData } from './types'; +import { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv'; import { getFetchBodyLength, normalizeHttpRequestMethod, @@ -67,7 +74,7 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig { // is not available clearTimingResources?: boolean; // urls which should include trace headers when origin doesn't match - propagateTraceHeaderCorsUrls?: web.PropagateTraceHeaderCorsUrls; + propagateTraceHeaderCorsUrls?: PropagateTraceHeaderCorsUrls; /** * URLs that partially match any regex in ignoreUrls will not be traced. * In addition, URLs that are _exact matches_ of strings in ignoreUrls will @@ -121,20 +128,18 @@ export class FetchInstrumentation extends InstrumentationBase=1.4.0 <1.10.0" }, "dependencies": { + "@opentelemetry/core": "2.11.0", "@opentelemetry/sdk-logs": "0.222.0", "@opentelemetry/sdk-trace": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" diff --git a/experimental/packages/web-common/src/enums/PerformanceTimingNames.ts b/experimental/packages/web-common/src/enums/PerformanceTimingNames.ts new file mode 100644 index 00000000000..ef22ed8d7bc --- /dev/null +++ b/experimental/packages/web-common/src/enums/PerformanceTimingNames.ts @@ -0,0 +1,30 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum PerformanceTimingNames { + CONNECT_END = 'connectEnd', + CONNECT_START = 'connectStart', + DECODED_BODY_SIZE = 'decodedBodySize', + DOM_COMPLETE = 'domComplete', + DOM_CONTENT_LOADED_EVENT_END = 'domContentLoadedEventEnd', + DOM_CONTENT_LOADED_EVENT_START = 'domContentLoadedEventStart', + DOM_INTERACTIVE = 'domInteractive', + DOMAIN_LOOKUP_END = 'domainLookupEnd', + DOMAIN_LOOKUP_START = 'domainLookupStart', + ENCODED_BODY_SIZE = 'encodedBodySize', + FETCH_START = 'fetchStart', + LOAD_EVENT_END = 'loadEventEnd', + LOAD_EVENT_START = 'loadEventStart', + NAVIGATION_START = 'navigationStart', + REDIRECT_END = 'redirectEnd', + REDIRECT_START = 'redirectStart', + REQUEST_START = 'requestStart', + RESPONSE_END = 'responseEnd', + RESPONSE_START = 'responseStart', + SECURE_CONNECTION_START = 'secureConnectionStart', + START_TIME = 'startTime', + UNLOAD_EVENT_END = 'unloadEventEnd', + UNLOAD_EVENT_START = 'unloadEventStart', +} diff --git a/experimental/packages/web-common/src/index.ts b/experimental/packages/web-common/src/index.ts index 96d5e80759e..9589da5a19f 100644 --- a/experimental/packages/web-common/src/index.ts +++ b/experimental/packages/web-common/src/index.ts @@ -12,7 +12,26 @@ export { createLocalStorageSessionStore, } from './utils'; export type { Session } from './types/Session'; -export type { SessionIdGenerator } from './types/SessionIdGenerator'; +export type { SessionIdGenerator } from './types/SessionIdGenerator'; export type { SessionPublisher } from './types/SessionPublisher'; export type { SessionObserver } from './types/SessionObserver'; export type { SessionStore } from './types/SessionStore'; + +export type { + PerformanceEntries, + PerformanceLegacy, + PerformanceResourceTimingInfo, +} from './types/Performance'; +export type { PropagateTraceHeaderCorsUrls, URLLike } from './utils-resource-timings'; +export { PerformanceTimingNames } from './enums/PerformanceTimingNames'; +export { + addSpanNetworkEvent, + addSpanNetworkEvents, + getElementXPath, + getResource, + hasKey, + normalizeUrl, + parseUrl, + shouldPropagateTraceHeaders, + sortResources, +} from './utils-resource-timings'; diff --git a/experimental/packages/web-common/src/semconv.ts b/experimental/packages/web-common/src/semconv.ts index 1ac1c56f933..b54cae97575 100644 --- a/experimental/packages/web-common/src/semconv.ts +++ b/experimental/packages/web-common/src/semconv.ts @@ -9,3 +9,33 @@ * @example "00112233-4455-6677-8899-aabbccddeeff" */ export const ATTR_SESSION_ID = 'session.id' as const; + +/* + * This file contains a copy of unstable semantic convention definitions + * used by this package. + * @see https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions#unstable-semconv + */ + +/** + * Deprecated, use `http.response.header.` instead. + * + * @example 3495 + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replaced by `http.response.header.`. + */ +export const ATTR_HTTP_RESPONSE_CONTENT_LENGTH = + 'http.response_content_length' as const; + +/** + * Deprecated, use `http.response.body.size` instead. + * + * @example 5493 + * + * @experimental This attribute is experimental and is subject to breaking changes in minor releases of `@opentelemetry/semantic-conventions`. + * + * @deprecated Replace by `http.response.body.size`. + */ +export const ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED = + 'http.response_content_length_uncompressed' as const; diff --git a/experimental/packages/web-common/src/types/Performance.ts b/experimental/packages/web-common/src/types/Performance.ts new file mode 100644 index 00000000000..6d6a1547fec --- /dev/null +++ b/experimental/packages/web-common/src/types/Performance.ts @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { PerformanceTimingNames } from '../enums/PerformanceTimingNames'; + +export type PerformanceEntries = { + [PerformanceTimingNames.CONNECT_END]?: number; + [PerformanceTimingNames.CONNECT_START]?: number; + [PerformanceTimingNames.DECODED_BODY_SIZE]?: number; + [PerformanceTimingNames.DOM_COMPLETE]?: number; + [PerformanceTimingNames.DOM_CONTENT_LOADED_EVENT_END]?: number; + [PerformanceTimingNames.DOM_CONTENT_LOADED_EVENT_START]?: number; + [PerformanceTimingNames.DOM_INTERACTIVE]?: number; + [PerformanceTimingNames.DOMAIN_LOOKUP_END]?: number; + [PerformanceTimingNames.DOMAIN_LOOKUP_START]?: number; + [PerformanceTimingNames.ENCODED_BODY_SIZE]?: number; + [PerformanceTimingNames.FETCH_START]?: number; + [PerformanceTimingNames.LOAD_EVENT_END]?: number; + [PerformanceTimingNames.LOAD_EVENT_START]?: number; + [PerformanceTimingNames.REDIRECT_END]?: number; + [PerformanceTimingNames.REDIRECT_START]?: number; + [PerformanceTimingNames.REQUEST_START]?: number; + [PerformanceTimingNames.RESPONSE_END]?: number; + [PerformanceTimingNames.RESPONSE_START]?: number; + [PerformanceTimingNames.SECURE_CONNECTION_START]?: number; + [PerformanceTimingNames.START_TIME]?: number; + [PerformanceTimingNames.UNLOAD_EVENT_END]?: number; + [PerformanceTimingNames.UNLOAD_EVENT_START]?: number; +}; + +/** + * This interface defines a fallback to read performance metrics, + * this happens for example on Safari Mac + */ +export interface PerformanceLegacy { + timing?: PerformanceEntries; +} + +/** + * This interface is used in {@link getResource} function to return + * main request and it's corresponding PreFlight request + */ +export interface PerformanceResourceTimingInfo { + corsPreFlightRequest?: PerformanceResourceTiming; + mainRequest?: PerformanceResourceTiming; +} diff --git a/experimental/packages/web-common/src/utils-resource-timings.ts b/experimental/packages/web-common/src/utils-resource-timings.ts new file mode 100644 index 00000000000..1a2ef1998be --- /dev/null +++ b/experimental/packages/web-common/src/utils-resource-timings.ts @@ -0,0 +1,506 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { Span, HrTime } from '@opentelemetry/api'; +import { + hrTimeToNanoseconds, + timeInputToHrTime, + urlMatches, +} from '@opentelemetry/core'; +import type { + PerformanceEntries, + PerformanceResourceTimingInfo, +} from './types/Performance'; +import { + ATTR_HTTP_RESPONSE_CONTENT_LENGTH, + ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED, +} from './semconv'; +import { PerformanceTimingNames } from './enums/PerformanceTimingNames'; + +type PropagateTraceHeaderCorsUrl = string | RegExp; + +/** + * urls which should include trace headers when origin doesn't match + */ +export type PropagateTraceHeaderCorsUrls = + | PropagateTraceHeaderCorsUrl + | PropagateTraceHeaderCorsUrl[]; + +// Used to normalize relative URLs +let urlNormalizingAnchor: HTMLAnchorElement | undefined; +function getUrlNormalizingAnchor(): HTMLAnchorElement { + if (!urlNormalizingAnchor) { + urlNormalizingAnchor = document.createElement('a'); + } + + return urlNormalizingAnchor; +} + +/** + * Helper function to be able to use enum as typed key in type and in interface when using forEach + * @param obj + * @param key + */ +export function hasKey( + obj: O, + key: PropertyKey +): key is keyof O { + return key in obj; +} + +/** + * Helper function for starting an event on span based on {@link PerformanceEntries} + * @param span + * @param performanceName name of performance entry for time start + * @param entries + * @param ignoreZeros + */ +export function addSpanNetworkEvent( + span: Span, + performanceName: string, + entries: PerformanceEntries, + ignoreZeros = true +): Span | undefined { + if ( + hasKey(entries, performanceName) && + typeof entries[performanceName] === 'number' && + !(ignoreZeros && entries[performanceName] === 0) + ) { + return span.addEvent(performanceName, entries[performanceName]); + } + + return undefined; +} + +/** + * Helper function for adding network events and content length attributes. + */ +export function addSpanNetworkEvents( + span: Span, + resource: PerformanceEntries, + ignoreNetworkEvents = false, + ignoreZeros?: boolean, + skipOldSemconvContentLengthAttrs?: boolean +): void { + if (ignoreZeros === undefined) { + ignoreZeros = resource[PerformanceTimingNames.START_TIME] !== 0; + } + + if (!ignoreNetworkEvents) { + addSpanNetworkEvent( + span, + PerformanceTimingNames.FETCH_START, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.DOMAIN_LOOKUP_START, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.DOMAIN_LOOKUP_END, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.CONNECT_START, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.SECURE_CONNECTION_START, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.CONNECT_END, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.REQUEST_START, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.RESPONSE_START, + resource, + ignoreZeros + ); + addSpanNetworkEvent( + span, + PerformanceTimingNames.RESPONSE_END, + resource, + ignoreZeros + ); + } + + if (!skipOldSemconvContentLengthAttrs) { + // This block adds content-length-related span attributes using the + // *old* HTTP semconv (v1.7.0). + const encodedLength = resource[PerformanceTimingNames.ENCODED_BODY_SIZE]; + if (encodedLength !== undefined) { + span.setAttribute(ATTR_HTTP_RESPONSE_CONTENT_LENGTH, encodedLength); + } + + const decodedLength = resource[PerformanceTimingNames.DECODED_BODY_SIZE]; + // Spec: Not set if transport encoding not used (in which case encoded and decoded sizes match) + if (decodedLength !== undefined && encodedLength !== decodedLength) { + span.setAttribute( + ATTR_HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED, + decodedLength + ); + } + } +} + +/** + * sort resources by startTime + * @param filteredResources + */ +export function sortResources( + filteredResources: PerformanceResourceTiming[] +): PerformanceResourceTiming[] { + return filteredResources.slice().sort((a, b) => { + const valueA = a[PerformanceTimingNames.FETCH_START]; + const valueB = b[PerformanceTimingNames.FETCH_START]; + if (valueA > valueB) { + return 1; + } else if (valueA < valueB) { + return -1; + } + return 0; + }); +} + +/** Returns the origin if present (if in browser context). */ +function getOrigin(): string | undefined { + return typeof location !== 'undefined' ? location.origin : undefined; +} + +/** + * Get closest performance resource ignoring the resources that have been + * already used. + * @param spanUrl + * @param startTimeHR + * @param endTimeHR + * @param resources + * @param ignoredResources + * @param initiatorType + */ +export function getResource( + spanUrl: string, + startTimeHR: HrTime, + endTimeHR: HrTime, + resources: PerformanceResourceTiming[], + ignoredResources: WeakSet = new WeakSet(), + initiatorType?: string +): PerformanceResourceTimingInfo { + // de-relativize the URL before usage (does no harm to absolute URLs) + const parsedSpanUrl = parseUrl(spanUrl); + spanUrl = parsedSpanUrl.toString(); + + const filteredResources = filterResourcesForSpan( + spanUrl, + startTimeHR, + endTimeHR, + resources, + ignoredResources, + initiatorType + ); + + if (filteredResources.length === 0) { + return { + mainRequest: undefined, + }; + } + if (filteredResources.length === 1) { + return { + mainRequest: filteredResources[0], + }; + } + const sorted = sortResources(filteredResources); + + if (parsedSpanUrl.origin !== getOrigin() && sorted.length > 1) { + let corsPreFlightRequest: PerformanceResourceTiming | undefined = sorted[0]; + let mainRequest: PerformanceResourceTiming = findMainRequest( + sorted, + corsPreFlightRequest[PerformanceTimingNames.RESPONSE_END], + endTimeHR + ); + + const responseEnd = + corsPreFlightRequest[PerformanceTimingNames.RESPONSE_END]; + const fetchStart = mainRequest[PerformanceTimingNames.FETCH_START]; + + // no corsPreFlightRequest + if (fetchStart < responseEnd) { + mainRequest = corsPreFlightRequest; + corsPreFlightRequest = undefined; + } + + return { + corsPreFlightRequest, + mainRequest, + }; + } else { + return { + mainRequest: filteredResources[0], + }; + } +} + +/** + * Will find the main request skipping the cors pre flight requests + * @param resources + * @param corsPreFlightRequestEndTime + * @param spanEndTimeHR + */ +function findMainRequest( + resources: PerformanceResourceTiming[], + corsPreFlightRequestEndTime: number, + spanEndTimeHR: HrTime +): PerformanceResourceTiming { + const spanEndTime = hrTimeToNanoseconds(spanEndTimeHR); + const minTime = hrTimeToNanoseconds( + timeInputToHrTime(corsPreFlightRequestEndTime) + ); + + let mainRequest: PerformanceResourceTiming = resources[1]; + let bestGap; + + const length = resources.length; + for (let i = 1; i < length; i++) { + const resource = resources[i]; + const resourceStartTime = hrTimeToNanoseconds( + timeInputToHrTime(resource[PerformanceTimingNames.FETCH_START]) + ); + + const resourceEndTime = hrTimeToNanoseconds( + timeInputToHrTime(resource[PerformanceTimingNames.RESPONSE_END]) + ); + + const currentGap = spanEndTime - resourceEndTime; + + if (resourceStartTime >= minTime && (!bestGap || currentGap < bestGap)) { + bestGap = currentGap; + mainRequest = resource; + } + } + return mainRequest; +} + +/** + * Filter all resources that has started and finished according to span start time and end time. + * It will return the closest resource to a start time + * @param spanUrl + * @param startTimeHR + * @param endTimeHR + * @param resources + * @param ignoredResources + */ +function filterResourcesForSpan( + spanUrl: string, + startTimeHR: HrTime, + endTimeHR: HrTime, + resources: PerformanceResourceTiming[], + ignoredResources: WeakSet, + initiatorType?: string +) { + const startTime = hrTimeToNanoseconds(startTimeHR); + const endTime = hrTimeToNanoseconds(endTimeHR); + let filteredResources = resources.filter(resource => { + const resourceStartTime = hrTimeToNanoseconds( + timeInputToHrTime(resource[PerformanceTimingNames.FETCH_START]) + ); + const resourceEndTime = hrTimeToNanoseconds( + timeInputToHrTime(resource[PerformanceTimingNames.RESPONSE_END]) + ); + + return ( + resource.initiatorType.toLowerCase() === + (initiatorType || 'xmlhttprequest') && + resource.name === spanUrl && + resourceStartTime >= startTime && + resourceEndTime <= endTime + ); + }); + + if (filteredResources.length > 0) { + filteredResources = filteredResources.filter(resource => { + return !ignoredResources.has(resource); + }); + } + + return filteredResources; +} + +/** + * The URLLike interface represents an URL and HTMLAnchorElement compatible fields. + */ +export interface URLLike { + hash: string; + host: string; + hostname: string; + href: string; + readonly origin: string; + password: string; + pathname: string; + port: string; + protocol: string; + search: string; + username: string; +} + +/** + * Parses url using URL constructor or fallback to anchor element. + * @param url + */ +export function parseUrl(url: string): URLLike { + if (typeof URL === 'function') { + return new URL( + url, + typeof document !== 'undefined' + ? document.baseURI + : typeof location !== 'undefined' // Some JS runtimes (e.g. Deno) don't define this + ? location.href + : undefined + ); + } + const element = getUrlNormalizingAnchor(); + element.href = url; + return element; +} + +/** + * Parses url using URL constructor or fallback to anchor element and serialize + * it to a string. + * + * Performs the steps described in https://html.spec.whatwg.org/multipage/urls-and-fetching.html#parse-a-url + * + * @param url + */ +export function normalizeUrl(url: string): string { + const urlLike = parseUrl(url); + return urlLike.href; +} + +/** + * Get element XPath + * @param target - target element + * @param optimised - when id attribute of element is present the xpath can be + * simplified to contain id + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getElementXPath(target: any, optimised?: boolean): string { + if (target.nodeType === Node.DOCUMENT_NODE) { + return '/'; + } + const targetValue = getNodeValue(target, optimised); + if (optimised && targetValue.indexOf('@id') > 0) { + return targetValue; + } + let xpath = ''; + if (target.parentNode) { + xpath += getElementXPath(target.parentNode, optimised); + } + xpath += targetValue; + + return xpath; +} + +/** + * get node index within the siblings + * @param target + */ +function getNodeIndex(target: HTMLElement): number { + if (!target.parentNode) { + return 0; + } + const allowedTypes = [target.nodeType]; + if (target.nodeType === Node.CDATA_SECTION_NODE) { + allowedTypes.push(Node.TEXT_NODE); + } + let elements = Array.from(target.parentNode.childNodes); + elements = elements.filter((element: Node) => { + const localName = (element as HTMLElement).localName; + return ( + allowedTypes.indexOf(element.nodeType) >= 0 && + localName === target.localName + ); + }); + if (elements.length >= 1) { + return elements.indexOf(target) + 1; // xpath starts from 1 + } + // if there are no other similar child xpath doesn't need index + return 0; +} + +/** + * get node value for xpath + * @param target + * @param optimised + */ +function getNodeValue(target: HTMLElement, optimised?: boolean): string { + const nodeType = target.nodeType; + const index = getNodeIndex(target); + let nodeValue: string; + if (nodeType === Node.ELEMENT_NODE) { + const id = target.getAttribute('id'); + if (optimised && id) { + return `//*[@id="${id}"]`; + } + nodeValue = target.localName; + } else if ( + nodeType === Node.TEXT_NODE || + nodeType === Node.CDATA_SECTION_NODE + ) { + nodeValue = 'text()'; + } else if (nodeType === Node.COMMENT_NODE) { + nodeValue = 'comment()'; + } else { + return ''; + } + // if index is 1 it can be omitted in xpath + if (nodeValue && index > 1) { + return `/${nodeValue}[${index}]`; + } + return `/${nodeValue}`; +} + +/** + * Checks if trace headers should be propagated + * @param spanUrl + * @private + */ +export function shouldPropagateTraceHeaders( + spanUrl: string, + propagateTraceHeaderCorsUrls?: PropagateTraceHeaderCorsUrls +): boolean { + let propagateTraceHeaderUrls = propagateTraceHeaderCorsUrls || []; + if ( + typeof propagateTraceHeaderUrls === 'string' || + propagateTraceHeaderUrls instanceof RegExp + ) { + propagateTraceHeaderUrls = [propagateTraceHeaderUrls]; + } + const parsedSpanUrl = parseUrl(spanUrl); + + if (parsedSpanUrl.origin === getOrigin()) { + return true; + } else { + return propagateTraceHeaderUrls.some(propagateTraceHeaderUrl => + urlMatches(spanUrl, propagateTraceHeaderUrl) + ); + } +} diff --git a/experimental/packages/web-common/test/utils-resource-timings.test.ts b/experimental/packages/web-common/test/utils-resource-timings.test.ts new file mode 100644 index 00000000000..d775b3afc23 --- /dev/null +++ b/experimental/packages/web-common/test/utils-resource-timings.test.ts @@ -0,0 +1,651 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { hrTimeToNanoseconds } from '@opentelemetry/core'; +import * as core from '@opentelemetry/core'; +import type * as tracing from '@opentelemetry/sdk-trace-base'; +import type { HrTime } from '@opentelemetry/api'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import type { PerformanceEntries, URLLike } from '../src'; +import { + addSpanNetworkEvent, + addSpanNetworkEvents, + getResource, + normalizeUrl, + parseUrl, + shouldPropagateTraceHeaders, +} from '../src'; +import { PerformanceTimingNames as PTN } from '../src'; + +const SECOND_TO_NANOSECONDS = 1e9; + +function createHrTime(startTime: HrTime, addToStart: number): HrTime { + let seconds = startTime[0]; + let nanos = startTime[1] + addToStart; + if (nanos >= SECOND_TO_NANOSECONDS) { + nanos = SECOND_TO_NANOSECONDS - nanos; + seconds++; + } + return [seconds, nanos]; +} + +function createResource( + resource = {}, + startTime: HrTime, + addToStart: number +): PerformanceResourceTiming { + const fetchStart = core.hrTimeToNanoseconds(startTime) + 1; + const responseEnd = fetchStart + addToStart; + const million = 1000 * 1000; // used to convert nano to milli + const defaultResource = { + connectEnd: 0, + connectStart: 0, + decodedBodySize: 0, + domainLookupEnd: 0, + domainLookupStart: 0, + encodedBodySize: 0, + fetchStart: fetchStart / million, + initiatorType: 'xmlhttprequest', + nextHopProtocol: '', + redirectEnd: 0, + redirectStart: 0, + requestStart: 0, + responseEnd: responseEnd / million, + responseStart: 0, + secureConnectionStart: 0, + transferSize: 0, + workerStart: 0, + duration: 0, + entryType: '', + name: '', + startTime: 0, + }; + return Object.assign( + {}, + defaultResource, + resource + ) as PerformanceResourceTiming; +} + +describe('utils', function () { + afterEach(() => { + sinon.restore(); + }); + + describe('addSpanNetworkEvents', function () { + it('should add all network events to span', function () { + const addEventSpy = sinon.spy(); + const setAttributeSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + setAttribute: setAttributeSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.START_TIME]: 123, + [PTN.FETCH_START]: 123, + [PTN.DOMAIN_LOOKUP_START]: 123, + [PTN.DOMAIN_LOOKUP_END]: 123, + [PTN.CONNECT_START]: 123, + [PTN.SECURE_CONNECTION_START]: 123, + [PTN.CONNECT_END]: 123, + [PTN.REQUEST_START]: 123, + [PTN.RESPONSE_START]: 123, + [PTN.RESPONSE_END]: 123, + [PTN.DECODED_BODY_SIZE]: 123, + [PTN.ENCODED_BODY_SIZE]: 61, + } as PerformanceEntries; + + assert.strictEqual(setAttributeSpy.callCount, 0); + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvents(span, entries); + assert.strictEqual(setAttributeSpy.callCount, 2); + assert.strictEqual(addEventSpy.callCount, 9); + }); + it('should ignore network events when ignoreNetworkEvents is true', function () { + const addEventSpy = sinon.spy(); + const setAttributeSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + setAttribute: setAttributeSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.START_TIME]: 123, + [PTN.FETCH_START]: 123, + [PTN.DOMAIN_LOOKUP_START]: 123, + [PTN.DOMAIN_LOOKUP_END]: 123, + [PTN.CONNECT_START]: 123, + [PTN.SECURE_CONNECTION_START]: 123, + [PTN.CONNECT_END]: 123, + [PTN.REQUEST_START]: 123, + [PTN.RESPONSE_START]: 123, + [PTN.RESPONSE_END]: 123, + [PTN.DECODED_BODY_SIZE]: 123, + [PTN.ENCODED_BODY_SIZE]: 61, + } as PerformanceEntries; + + assert.strictEqual(setAttributeSpy.callCount, 0); + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvents(span, entries, true); + assert.strictEqual(setAttributeSpy.callCount, 2); + assert.strictEqual(addEventSpy.callCount, 0); + }); + it('should ignore zero timings by default', function () { + const addEventSpy = sinon.spy(); + const setAttributeSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + setAttribute: setAttributeSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.START_TIME]: 123, + [PTN.FETCH_START]: 123, + [PTN.DOMAIN_LOOKUP_START]: 0, + [PTN.DOMAIN_LOOKUP_END]: 0, + [PTN.CONNECT_START]: 0, + [PTN.SECURE_CONNECTION_START]: 0, + [PTN.CONNECT_END]: 0, + [PTN.REQUEST_START]: 0, + [PTN.RESPONSE_START]: 0, + [PTN.RESPONSE_END]: 130, + [PTN.DECODED_BODY_SIZE]: 0, + [PTN.ENCODED_BODY_SIZE]: 0, + } as PerformanceEntries; + + assert.strictEqual(setAttributeSpy.callCount, 0); + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvents(span, entries); + assert.strictEqual(setAttributeSpy.callCount, 1); + assert.strictEqual(addEventSpy.callCount, 2); + }); + it('should not ignore zero timings by default if startTime = 0', function () { + const addEventSpy = sinon.spy(); + const setAttributeSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + setAttribute: setAttributeSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.START_TIME]: 0, + [PTN.FETCH_START]: 0, + [PTN.DOMAIN_LOOKUP_START]: 0, + [PTN.DOMAIN_LOOKUP_END]: 0, + [PTN.CONNECT_START]: 0, + [PTN.SECURE_CONNECTION_START]: 0, + [PTN.CONNECT_END]: 1, + [PTN.REQUEST_START]: 2, + [PTN.RESPONSE_START]: 3, + [PTN.RESPONSE_END]: 4, + [PTN.DECODED_BODY_SIZE]: 123, + [PTN.ENCODED_BODY_SIZE]: 61, + } as PerformanceEntries; + + assert.strictEqual(setAttributeSpy.callCount, 0); + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvents(span, entries); + assert.strictEqual(setAttributeSpy.callCount, 2); + assert.strictEqual(addEventSpy.callCount, 9); + }); + it('should not ignore zero timings if ignoreZeros = false', function () { + const addEventSpy = sinon.spy(); + const setAttributeSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + setAttribute: setAttributeSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.START_TIME]: 123, + [PTN.FETCH_START]: 123, + [PTN.DOMAIN_LOOKUP_START]: 0, + [PTN.DOMAIN_LOOKUP_END]: 0, + [PTN.CONNECT_START]: 0, + [PTN.SECURE_CONNECTION_START]: 0, + [PTN.CONNECT_END]: 0, + [PTN.REQUEST_START]: 0, + [PTN.RESPONSE_START]: 0, + [PTN.RESPONSE_END]: 130, + [PTN.DECODED_BODY_SIZE]: 0, + [PTN.ENCODED_BODY_SIZE]: 0, + } as PerformanceEntries; + + assert.strictEqual(setAttributeSpy.callCount, 0); + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvents(span, entries, false, false); + assert.strictEqual(setAttributeSpy.callCount, 1); + assert.strictEqual(addEventSpy.callCount, 9); + }); + it('should only include encoded size when content encoding is being used', function () { + const addEventSpy = sinon.spy(); + const setAttributeSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + setAttribute: setAttributeSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.DECODED_BODY_SIZE]: 123, + [PTN.ENCODED_BODY_SIZE]: 123, + } as PerformanceEntries; + + assert.strictEqual(setAttributeSpy.callCount, 0); + + addSpanNetworkEvents(span, entries); + + assert.strictEqual(addEventSpy.callCount, 0); + assert.strictEqual(setAttributeSpy.callCount, 1); + }); + }); + describe('addSpanNetworkEvent', function () { + [-2, 123].forEach(value => { + describe(`when entry is ${value}`, function () { + it('should add event to span', function () { + const addEventSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.FETCH_START]: value, + } as PerformanceEntries; + + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvent(span, PTN.FETCH_START, entries); + + assert.strictEqual(addEventSpy.callCount, 1); + const args = addEventSpy.args[0]; + + assert.strictEqual(args[0], 'fetchStart'); + assert.strictEqual(args[1], value); + }); + }); + }); + describe('when entry is zero', function () { + it('should not add event to span by default', function () { + const addEventSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.SECURE_CONNECTION_START]: 0, + } as PerformanceEntries; + + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvent(span, PTN.SECURE_CONNECTION_START, entries); + + assert.strictEqual(addEventSpy.callCount, 0); + }); + it('should add event to span if ignoreZeros = false', function () { + const addEventSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.SECURE_CONNECTION_START]: 0, + } as PerformanceEntries; + + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvent(span, PTN.SECURE_CONNECTION_START, entries, false); + + assert.strictEqual(addEventSpy.callCount, 1); + const args = addEventSpy.args[0]; + + assert.strictEqual(args[0], 'secureConnectionStart'); + assert.strictEqual(args[1], 0); + }); + }); + describe('when entry is not numeric', function () { + it('should NOT add event to span', function () { + const addEventSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.FETCH_START]: 'non-numeric', + } as unknown; + + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvent( + span, + PTN.FETCH_START, + entries as PerformanceEntries + ); + + assert.strictEqual(addEventSpy.callCount, 0); + }); + }); + describe('when entries does NOT contain the performance', function () { + it('should NOT add event to span', function () { + const addEventSpy = sinon.spy(); + const span = { + addEvent: addEventSpy, + } as unknown as tracing.Span; + const entries = { + [PTN.FETCH_START]: 123, + } as PerformanceEntries; + + assert.strictEqual(addEventSpy.callCount, 0); + + addSpanNetworkEvent(span, 'foo', entries); + + assert.strictEqual( + addEventSpy.callCount, + 0, + 'should not call addEvent' + ); + }); + }); + }); + + describe('getResource', function () { + const startTime = [0, 123123123] as HrTime; + beforeEach(() => { + const time = createHrTime(startTime, 500); + sinon.stub(performance, 'timeOrigin').value(0); + sinon.stub(performance, 'now').callsFake(() => hrTimeToNanoseconds(time)); + }); + + describe('when resources are empty', function () { + it('should return undefined', function () { + const spanStartTime = createHrTime(startTime, 1); + const spanEndTime = createHrTime(startTime, 100); + const spanUrl = 'http://foo.com/bar.json'; + const resources: PerformanceResourceTiming[] = []; + + const resource = getResource( + spanUrl, + spanStartTime, + spanEndTime, + resources + ); + + assert.deepStrictEqual( + resource.mainRequest, + undefined, + 'main request should be undefined' + ); + }); + }); + + describe('when resources has correct entry', function () { + it('should return the closest one', function () { + const spanStartTime = createHrTime(startTime, 1); + const spanEndTime = createHrTime(startTime, 402); + const spanUrl = 'http://foo.com/bar.json'; + const resources: PerformanceResourceTiming[] = []; + + // this one started earlier + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, -1), + 100 + ) + ); + + // this one is correct + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 1), + 400 + ) + ); + + // this one finished after span + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 1), + 1000 + ) + ); + + const resource = getResource( + spanUrl, + spanStartTime, + spanEndTime, + resources + ); + + assert.deepStrictEqual( + resource.mainRequest, + resources[1], + 'main request should be defined' + ); + }); + describe('But one resource has been already used', function () { + it('should return the next closest', function () { + const spanStartTime = createHrTime(startTime, 1); + const spanEndTime = createHrTime(startTime, 402); + const spanUrl = 'http://foo.com/bar.json'; + const resources: PerformanceResourceTiming[] = []; + + // this one started earlier + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, -1), + 100 + ) + ); + + // this one is correct but ignored + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 1), + 400 + ) + ); + + // this one is also correct + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 1), + 300 + ) + ); + + // this one finished after span + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 1), + 1000 + ) + ); + + const ignoredResources = new WeakSet(); + ignoredResources.add(resources[1]); + const resource = getResource( + spanUrl, + spanStartTime, + spanEndTime, + resources, + ignoredResources + ); + + assert.deepStrictEqual( + resource.mainRequest, + resources[2], + 'main request should be defined' + ); + }); + }); + }); + + describe('when there are multiple resources from CorsPreflight requests', function () { + it('should return main request and cors preflight request', function () { + const spanStartTime = createHrTime(startTime, 1); + const spanEndTime = createHrTime(startTime, 182); + const spanUrl = 'http://foo.com/bar.json'; + const resources: PerformanceResourceTiming[] = []; + + // this one started earlier + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 1), + 10 + ) + ); + + // this one is correct + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 1), + 11 + ) + ); + + // this one finished after span + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 50), + 100 + ) + ); + + // this one finished after span + resources.push( + createResource( + { + name: 'http://foo.com/bar.json', + }, + createHrTime(startTime, 50), + 130 + ) + ); + + const resource = getResource( + spanUrl, + spanStartTime, + spanEndTime, + resources, + undefined + ); + + assert.deepStrictEqual( + resource.corsPreFlightRequest, + resources[0], + 'cors preflight request should be defined' + ); + + assert.deepStrictEqual( + resource.mainRequest, + resources[3], + 'main request should be defined' + ); + }); + }); + }); + + describe('shouldPropagateTraceHeaders', function () { + it('should propagate trace when url is the same as origin', function () { + const result = shouldPropagateTraceHeaders( + `${globalThis.location.origin}/foo/bar` + ); + assert.strictEqual(result, true); + }); + it('should propagate trace when url match', function () { + const result = shouldPropagateTraceHeaders( + 'http://foo.com', + 'http://foo.com' + ); + assert.strictEqual(result, true); + }); + it('should propagate trace when url match regexp', function () { + const result = shouldPropagateTraceHeaders('http://foo.com', /foo.+/); + assert.strictEqual(result, true); + }); + it('should propagate trace when url match array of string', function () { + const result = shouldPropagateTraceHeaders('http://foo.com', [ + 'http://foo.com', + ]); + assert.strictEqual(result, true); + }); + it('should propagate trace when url match array of regexp', function () { + const result = shouldPropagateTraceHeaders('http://foo.com', [/foo.+/]); + assert.strictEqual(result, true); + }); + it("should NOT propagate trace when url doesn't match", function () { + const result = shouldPropagateTraceHeaders('http://foo.com'); + assert.strictEqual(result, false); + }); + }); + + describe('parseUrl', function () { + const urlFields: Array = [ + 'hash', + 'host', + 'hostname', + 'href', + 'origin', + 'password', + 'pathname', + 'port', + 'protocol', + 'search', + 'username', + ]; + it('should parse url', function () { + const url = parseUrl('https://opentelemetry.io/foo'); + urlFields.forEach(field => { + assert.strictEqual(typeof url[field], 'string'); + }); + }); + + it('should parse relative url', function () { + const url = parseUrl('/foo'); + urlFields.forEach(field => { + assert.strictEqual(typeof url[field], 'string'); + }); + }); + }); + + describe('normalizeUrl', function () { + it('should normalize url', function () { + const url = normalizeUrl('https://opentelemetry.io/你好'); + assert.strictEqual(url, 'https://opentelemetry.io/%E4%BD%A0%E5%A5%BD'); + }); + + it('should normalize relative url', function () { + const url = normalizeUrl('/你好'); + const urlObj = new URL(url); + assert.strictEqual(urlObj.pathname, '/%E4%BD%A0%E5%A5%BD'); + }); + }); +}); diff --git a/experimental/packages/web-common/tsconfig.esm.json b/experimental/packages/web-common/tsconfig.esm.json index ae62c70d5d8..f1dc049981b 100644 --- a/experimental/packages/web-common/tsconfig.esm.json +++ b/experimental/packages/web-common/tsconfig.esm.json @@ -17,6 +17,9 @@ { "path": "../../../api" }, + { + "path": "../../../packages/opentelemetry-core" + }, { "path": "../../../packages/sdk-trace" }, diff --git a/experimental/packages/web-common/tsconfig.esnext.json b/experimental/packages/web-common/tsconfig.esnext.json index e481c874227..330dba937b7 100644 --- a/experimental/packages/web-common/tsconfig.esnext.json +++ b/experimental/packages/web-common/tsconfig.esnext.json @@ -17,6 +17,9 @@ { "path": "../../../api" }, + { + "path": "../../../packages/opentelemetry-core" + }, { "path": "../../../packages/sdk-trace" }, diff --git a/experimental/packages/web-common/tsconfig.json b/experimental/packages/web-common/tsconfig.json index 5ff2dd1460b..dba23102377 100644 --- a/experimental/packages/web-common/tsconfig.json +++ b/experimental/packages/web-common/tsconfig.json @@ -17,6 +17,9 @@ { "path": "../../../api" }, + { + "path": "../../../packages/opentelemetry-core" + }, { "path": "../../../packages/sdk-trace" }, diff --git a/package-lock.json b/package-lock.json index 29b88d2cd9d..2468c7b82ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -777,8 +777,8 @@ "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/instrumentation": "0.222.0", - "@opentelemetry/sdk-trace-web": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/semantic-conventions": "^1.29.0", + "@opentelemetry/web-common": "0.222.0" }, "devDependencies": { "@babel/core": "7.29.7", @@ -1231,6 +1231,7 @@ "version": "0.222.0", "license": "Apache-2.0", "dependencies": { + "@opentelemetry/core": "2.11.0", "@opentelemetry/sdk-logs": "0.222.0", "@opentelemetry/sdk-trace": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -5265,9 +5266,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5282,9 +5280,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5299,9 +5294,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5316,9 +5308,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -14548,9 +14537,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -14565,9 +14551,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -14582,9 +14565,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -14599,9 +14579,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ From 6d5bf39278f1df8d714315d6086b7f5ef9ee7947 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 3 Sep 2026 18:25:27 +0200 Subject: [PATCH 2/7] chore: replace sdk-trace utils in xhr instrumentation --- .../src/fetch.ts | 31 ++++++++----------- .../package.json | 4 +-- .../src/xhr.ts | 2 +- .../tsconfig.esm.json | 6 ++-- .../tsconfig.esnext.json | 6 ++-- .../tsconfig.json | 6 ++-- package-lock.json | 4 +-- 7 files changed, 27 insertions(+), 32 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts index ed5f5bc7e5c..4b74108dc7d 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts @@ -17,6 +17,8 @@ import { safeExecuteInTheMiddle, } from '@opentelemetry/instrumentation'; import * as core from '@opentelemetry/core'; +import * as web from '@opentelemetry/web-common'; +import { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv'; import { ATTR_ERROR_TYPE, ATTR_HTTP_REQUEST_METHOD, @@ -26,16 +28,7 @@ import { ATTR_SERVER_PORT, ATTR_URL_FULL, } from '@opentelemetry/semantic-conventions'; -import type { PropagateTraceHeaderCorsUrls } from '@opentelemetry/web-common'; -import { - PerformanceTimingNames, - addSpanNetworkEvents, - getResource, - parseUrl, - shouldPropagateTraceHeaders, -} from '@opentelemetry/web-common'; import type { FetchError, FetchResponse, SpanData } from './types'; -import { ATTR_HTTP_REQUEST_BODY_SIZE } from './semconv'; import { getFetchBodyLength, normalizeHttpRequestMethod, @@ -74,7 +67,7 @@ export interface FetchInstrumentationConfig extends InstrumentationConfig { // is not available clearTimingResources?: boolean; // urls which should include trace headers when origin doesn't match - propagateTraceHeaderCorsUrls?: PropagateTraceHeaderCorsUrls; + propagateTraceHeaderCorsUrls?: web.PropagateTraceHeaderCorsUrls; /** * URLs that partially match any regex in ignoreUrls will not be traced. * In addition, URLs that are _exact matches_ of strings in ignoreUrls will @@ -128,18 +121,20 @@ export class FetchInstrumentation extends InstrumentationBase Date: Thu, 3 Sep 2026 18:27:10 +0200 Subject: [PATCH 3/7] fix: fix lint issue --- experimental/packages/web-common/src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/experimental/packages/web-common/src/index.ts b/experimental/packages/web-common/src/index.ts index 9589da5a19f..ee05e109164 100644 --- a/experimental/packages/web-common/src/index.ts +++ b/experimental/packages/web-common/src/index.ts @@ -12,7 +12,7 @@ export { createLocalStorageSessionStore, } from './utils'; export type { Session } from './types/Session'; -export type { SessionIdGenerator } from './types/SessionIdGenerator'; +export type { SessionIdGenerator } from './types/SessionIdGenerator'; export type { SessionPublisher } from './types/SessionPublisher'; export type { SessionObserver } from './types/SessionObserver'; export type { SessionStore } from './types/SessionStore'; @@ -22,7 +22,10 @@ export type { PerformanceLegacy, PerformanceResourceTimingInfo, } from './types/Performance'; -export type { PropagateTraceHeaderCorsUrls, URLLike } from './utils-resource-timings'; +export type { + PropagateTraceHeaderCorsUrls, + URLLike, +} from './utils-resource-timings'; export { PerformanceTimingNames } from './enums/PerformanceTimingNames'; export { addSpanNetworkEvent, From 0e65bce1731fbb99ae8dad964d73012b422e16e4 Mon Sep 17 00:00:00 2001 From: David Luna Date: Thu, 3 Sep 2026 19:00:41 +0200 Subject: [PATCH 4/7] fix: fix bad imports --- .../src/utils.ts | 2 +- .../src/utils.ts | 2 +- .../test/utils-resource-timings.test.ts | 24 +++++++++---------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/src/utils.ts b/experimental/packages/opentelemetry-instrumentation-fetch/src/utils.ts index b8e80feae7b..24421e9b1bf 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/src/utils.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/src/utils.ts @@ -8,7 +8,7 @@ import { diag } from '@opentelemetry/api'; import { getStringListFromEnv } from '@opentelemetry/core'; -import type { URLLike } from '@opentelemetry/sdk-trace-web'; +import type { URLLike } from '@opentelemetry/web-common'; const DIAG_LOGGER = diag.createComponentLogger({ namespace: '@opentelemetry/opentelemetry-instrumentation-fetch/utils', diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/utils.ts b/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/utils.ts index 56f843b4d58..d00d53ab54a 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/utils.ts +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/utils.ts @@ -8,7 +8,7 @@ import * as api from '@opentelemetry/api'; import { getStringListFromEnv } from '@opentelemetry/core'; -import type { URLLike } from '@opentelemetry/sdk-trace-web'; +import type { URLLike } from '@opentelemetry/web-common'; const DIAG_LOGGER = api.diag.createComponentLogger({ namespace: diff --git a/experimental/packages/web-common/test/utils-resource-timings.test.ts b/experimental/packages/web-common/test/utils-resource-timings.test.ts index d775b3afc23..100865c478a 100644 --- a/experimental/packages/web-common/test/utils-resource-timings.test.ts +++ b/experimental/packages/web-common/test/utils-resource-timings.test.ts @@ -5,7 +5,7 @@ import { hrTimeToNanoseconds } from '@opentelemetry/core'; import * as core from '@opentelemetry/core'; -import type * as tracing from '@opentelemetry/sdk-trace-base'; +import type { Span } from '@opentelemetry/sdk-trace'; import type { HrTime } from '@opentelemetry/api'; import * as assert from 'assert'; @@ -83,7 +83,7 @@ describe('utils', function () { const span = { addEvent: addEventSpy, setAttribute: setAttributeSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.START_TIME]: 123, [PTN.FETCH_START]: 123, @@ -112,7 +112,7 @@ describe('utils', function () { const span = { addEvent: addEventSpy, setAttribute: setAttributeSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.START_TIME]: 123, [PTN.FETCH_START]: 123, @@ -141,7 +141,7 @@ describe('utils', function () { const span = { addEvent: addEventSpy, setAttribute: setAttributeSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.START_TIME]: 123, [PTN.FETCH_START]: 123, @@ -170,7 +170,7 @@ describe('utils', function () { const span = { addEvent: addEventSpy, setAttribute: setAttributeSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.START_TIME]: 0, [PTN.FETCH_START]: 0, @@ -199,7 +199,7 @@ describe('utils', function () { const span = { addEvent: addEventSpy, setAttribute: setAttributeSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.START_TIME]: 123, [PTN.FETCH_START]: 123, @@ -228,7 +228,7 @@ describe('utils', function () { const span = { addEvent: addEventSpy, setAttribute: setAttributeSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.DECODED_BODY_SIZE]: 123, [PTN.ENCODED_BODY_SIZE]: 123, @@ -249,7 +249,7 @@ describe('utils', function () { const addEventSpy = sinon.spy(); const span = { addEvent: addEventSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.FETCH_START]: value, } as PerformanceEntries; @@ -271,7 +271,7 @@ describe('utils', function () { const addEventSpy = sinon.spy(); const span = { addEvent: addEventSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.SECURE_CONNECTION_START]: 0, } as PerformanceEntries; @@ -286,7 +286,7 @@ describe('utils', function () { const addEventSpy = sinon.spy(); const span = { addEvent: addEventSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.SECURE_CONNECTION_START]: 0, } as PerformanceEntries; @@ -307,7 +307,7 @@ describe('utils', function () { const addEventSpy = sinon.spy(); const span = { addEvent: addEventSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.FETCH_START]: 'non-numeric', } as unknown; @@ -328,7 +328,7 @@ describe('utils', function () { const addEventSpy = sinon.spy(); const span = { addEvent: addEventSpy, - } as unknown as tracing.Span; + } as unknown as Span; const entries = { [PTN.FETCH_START]: 123, } as PerformanceEntries; From c56c98077d47261979bc9179f7bcb2d0f66df23a Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 4 Sep 2026 09:52:52 +0200 Subject: [PATCH 5/7] fix: remove more bad imports --- .../test/fetch.test.ts | 8 +++----- .../test/unmocked.test.ts | 6 +++--- .../test/xhr.test.ts | 8 ++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts b/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts index 2778c3552d8..3e41c058623 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/test/fetch.test.ts @@ -18,10 +18,8 @@ import { } from '@opentelemetry/propagator-b3'; import { ZoneContextManager } from '@opentelemetry/context-zone'; import * as tracing from '@opentelemetry/sdk-trace'; -import { - PerformanceTimingNames as PTN, - WebTracerProvider, -} from '@opentelemetry/sdk-trace-web'; +import { PerformanceTimingNames as PTN } from '@opentelemetry/web-common'; +import { TracerProvider } from '@opentelemetry/sdk-trace'; import * as assert from 'assert'; import * as sinon from 'sinon'; import type { @@ -334,7 +332,7 @@ describe('fetch', () => { const fetchInstrumentation: FetchInstrumentation = new FetchInstrumentation(config); const dummySpanExporter = new DummySpanExporter(); - const webTracerProviderWithZone = new WebTracerProvider({ + const webTracerProviderWithZone = new TracerProvider({ spanProcessors: [ new tracing.SimpleSpanProcessor({ exporter: dummySpanExporter }), ], diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/unmocked.test.ts b/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/unmocked.test.ts index 13f668ca3fb..3ddcc0c94f8 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/unmocked.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/unmocked.test.ts @@ -5,7 +5,7 @@ import type { Span } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; +import { TracerProvider } from '@opentelemetry/sdk-trace'; import { XMLHttpRequestInstrumentation } from '../src'; import * as assert from 'assert'; @@ -27,10 +27,10 @@ class TestSpanProcessor implements SpanProcessor { describe('unmocked xhr', () => { let testSpans: TestSpanProcessor; - let provider: WebTracerProvider; + let provider: TracerProvider; beforeEach(() => { testSpans = new TestSpanProcessor(); - provider = new WebTracerProvider({ + provider = new TracerProvider({ spanProcessors: [testSpans], }); registerInstrumentations({ diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts b/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts index 3da5924983b..fdc47f9cc5a 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/test/xhr.test.ts @@ -18,9 +18,9 @@ import { ZoneContextManager } from '@opentelemetry/context-zone'; import * as tracing from '@opentelemetry/sdk-trace'; import { PerformanceTimingNames as PTN, - WebTracerProvider, parseUrl, -} from '@opentelemetry/sdk-trace-web'; +} from '@opentelemetry/web-common'; +import { TracerProvider } from '@opentelemetry/sdk-trace'; import * as assert from 'assert'; import * as sinon from 'sinon'; import { ATTR_HTTP_REQUEST_BODY_SIZE } from '../src/semconv'; @@ -293,7 +293,7 @@ describe('xhr', () => { let requests: any[] = []; let contextManager: ZoneContextManager; let webTracerWithZone: api.Tracer; - let webTracerProviderWithZone: WebTracerProvider; + let webTracerProviderWithZone: TracerProvider; let dummySpanExporter: DummySpanExporter; let spyEntries: any; let fakeNow = 0; @@ -352,7 +352,7 @@ describe('xhr', () => { ...config, }); dummySpanExporter = new DummySpanExporter(); - webTracerProviderWithZone = new WebTracerProvider({ + webTracerProviderWithZone = new TracerProvider({ spanProcessors: [ new tracing.SimpleSpanProcessor({ exporter: dummySpanExporter }), ], From b9939c0abbffd5e52be5560ccdc97262716903a1 Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 4 Sep 2026 12:43:03 +0200 Subject: [PATCH 6/7] chore: update changelog --- experimental/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index 4a38faa6de2..ea545a8ab8a 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -16,6 +16,10 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :house: Internal +* refactor(web-common, instrumentation-fetch, instrumentation-xml-http-request): move utils from `@opentelemetry/sdk-trace-web` + into `@opentelemetry/web-common` and use them in `@opentelemetry/instrumentation-fetch` and `@opentelemetry/instrumentation-xml-http-request`. + With this change the instrumentations do not depend on SDK packages an the utils are kept in a shared package so we avoid code duplication. + ## 0.222.0 ### :boom: Breaking Changes From 7d7c043496dd1981d1095adebd7e97c6a95c34ad Mon Sep 17 00:00:00 2001 From: David Luna Date: Fri, 4 Sep 2026 12:55:29 +0200 Subject: [PATCH 7/7] fix: fix markdown lint issue --- experimental/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index ea545a8ab8a..d6b70bff432 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -16,7 +16,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :house: Internal -* refactor(web-common, instrumentation-fetch, instrumentation-xml-http-request): move utils from `@opentelemetry/sdk-trace-web` +* refactor(web-common, instrumentation-fetch, instrumentation-xml-http-request): move utils from `@opentelemetry/sdk-trace-web` into `@opentelemetry/web-common` and use them in `@opentelemetry/instrumentation-fetch` and `@opentelemetry/instrumentation-xml-http-request`. With this change the instrumentations do not depend on SDK packages an the utils are kept in a shared package so we avoid code duplication.