diff --git a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts index 1f762f429b..939da135f2 100644 --- a/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts +++ b/experimental/packages/opentelemetry-instrumentation-fetch/src/fetch.ts @@ -27,6 +27,7 @@ import { AttributeNames } from './enums/AttributeNames'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import { FetchError, FetchResponse, SpanData } from './types'; import { VERSION } from './version'; +import { _globalThis } from '@opentelemetry/core'; // how long to wait for observer to collect information about resources // this is needed as event "load" is called before observer @@ -288,13 +289,14 @@ export class FetchInstrumentation extends InstrumentationBase< /** * Patches the constructor of fetch */ - private _patchConstructor(): (original: Window['fetch']) => Window['fetch'] { + private _patchConstructor(): (original: typeof fetch) => typeof fetch { return original => { const plugin = this; return function patchConstructor( - this: Window, - ...args: Parameters + this: typeof globalThis, + ...args: Parameters ): Promise { + const self = this; const url = args[0] instanceof Request ? args[0].url : args[0]; const options = args[0] instanceof Request ? args[0] : args[1] || {}; const createdSpan = plugin._createSpan(url, options); @@ -377,11 +379,13 @@ export class FetchInstrumentation extends InstrumentationBase< () => { plugin._addHeaders(options, url); plugin._tasksCount++; + // TypeScript complains about arrow function captured a this typed as globalThis + // ts(7041) return original - .apply(this, options instanceof Request ? [options] : [url, options]) + .apply(self, options instanceof Request ? [options] : [url, options]) .then( - onSuccess.bind(this, createdSpan, resolve), - onError.bind(this, createdSpan, reject) + onSuccess.bind(self, createdSpan, resolve), + onError.bind(self, createdSpan, reject) ); } ); @@ -420,18 +424,17 @@ export class FetchInstrumentation extends InstrumentationBase< private _prepareSpanData(spanUrl: string): SpanData { const startTime = core.hrTime(); const entries: PerformanceResourceTiming[] = []; - if (typeof window.PerformanceObserver === 'undefined') { + if (PerformanceObserver == null) { return { entries, startTime, spanUrl }; } const observer: PerformanceObserver = new PerformanceObserver(list => { const perfObsEntries = list.getEntries() as PerformanceResourceTiming[]; - const urlNormalizingAnchor = web.getUrlNormalizingAnchor(); - urlNormalizingAnchor.href = spanUrl; + const parsedUrl = web.parseUrl(spanUrl); perfObsEntries.forEach(entry => { if ( entry.initiatorType === 'fetch' && - entry.name === urlNormalizingAnchor.href + entry.name === parsedUrl.href ) { entries.push(entry); } @@ -447,18 +450,18 @@ export class FetchInstrumentation extends InstrumentationBase< * implements enable function */ override enable(): void { - if (isWrapped(window.fetch)) { - this._unwrap(window, 'fetch'); + if (isWrapped(fetch)) { + this._unwrap(_globalThis, 'fetch'); this._diag.debug('removing previous patch for constructor'); } - this._wrap(window, 'fetch', this._patchConstructor()); + this._wrap(_globalThis, 'fetch', this._patchConstructor()); } /** * implements unpatch function */ override disable(): void { - this._unwrap(window, 'fetch'); + this._unwrap(_globalThis, 'fetch'); this._usedResources = new WeakSet(); } } diff --git a/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts b/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts index 1d0c9faac1..dfadaed386 100644 --- a/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts +++ b/experimental/packages/opentelemetry-instrumentation-xml-http-request/src/xhr.ts @@ -29,7 +29,6 @@ import { parseUrl, PerformanceTimingNames as PTN, shouldPropagateTraceHeaders, - getUrlNormalizingAnchor } from '@opentelemetry/sdk-trace-web'; import { EventNames } from './enums/EventNames'; import { @@ -209,21 +208,20 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase { const entries = list.getEntries() as PerformanceResourceTiming[]; - const urlNormalizingAnchor = getUrlNormalizingAnchor(); - urlNormalizingAnchor.href = spanUrl; + const parsedUrl = parseUrl(spanUrl); entries.forEach(entry => { if ( entry.initiatorType === 'xmlhttprequest' && - entry.name === urlNormalizingAnchor.href + entry.name === parsedUrl.href ) { if (xhrMem.createdResources) { xhrMem.createdResources.entries.push(entry); diff --git a/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts b/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts index 222f138d07..96008e4172 100644 --- a/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts +++ b/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts @@ -94,7 +94,7 @@ function sendWithXhr( urlStr: string, xhrHeaders: Record = {} ) { - const xhr = new window.XMLHttpRequest(); + const xhr = new XMLHttpRequest(); xhr.open('POST', urlStr); Object.entries(xhrHeaders).forEach(([k, v]) => { xhr.setRequestHeader(k, v); diff --git a/packages/opentelemetry-sdk-trace-web/src/utils.ts b/packages/opentelemetry-sdk-trace-web/src/utils.ts index 829955feb0..58831f4f21 100644 --- a/packages/opentelemetry-sdk-trace-web/src/utils.ts +++ b/packages/opentelemetry-sdk-trace-web/src/utils.ts @@ -30,7 +30,7 @@ import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; // Used to normalize relative URLs let urlNormalizingAnchor: HTMLAnchorElement | undefined; -export function getUrlNormalizingAnchor(): HTMLAnchorElement { +function getUrlNormalizingAnchor(): HTMLAnchorElement { if (!urlNormalizingAnchor) { urlNormalizingAnchor = document.createElement('a'); } @@ -140,9 +140,8 @@ export function getResource( initiatorType?: string ): PerformanceResourceTimingInfo { // de-relativize the URL before usage (does no harm to absolute URLs) - const urlNormalizingAnchor = getUrlNormalizingAnchor(); - urlNormalizingAnchor.href = spanUrl; - spanUrl = urlNormalizingAnchor.href; + const parsedSpanUrl = parseUrl(spanUrl); + spanUrl = parsedSpanUrl.toString(); const filteredResources = filterResourcesForSpan( spanUrl, @@ -165,7 +164,6 @@ export function getResource( } const sorted = sortResources(filteredResources); - const parsedSpanUrl = parseUrl(spanUrl); if (parsedSpanUrl.origin !== window.location.origin && sorted.length > 1) { let corsPreFlightRequest: PerformanceResourceTiming | undefined = sorted[0]; let mainRequest: PerformanceResourceTiming = findMainRequest( @@ -280,15 +278,48 @@ function filterResourcesForSpan( } /** - * Parses url using anchor element + * 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): HTMLAnchorElement { - const element = document.createElement('a'); +export function parseUrl(url: string): URLLike { + if (typeof URL === 'function') { + return new URL(url); + } + 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 diff --git a/packages/opentelemetry-sdk-trace-web/test/utils.test.ts b/packages/opentelemetry-sdk-trace-web/test/utils.test.ts index 085c621352..b8217e4f13 100644 --- a/packages/opentelemetry-sdk-trace-web/test/utils.test.ts +++ b/packages/opentelemetry-sdk-trace-web/test/utils.test.ts @@ -29,8 +29,11 @@ import { addSpanNetworkEvents, getElementXPath, getResource, + normalizeUrl, + parseUrl, PerformanceEntries, shouldPropagateTraceHeaders, + URLLike, } from '../src'; import { PerformanceTimingNames as PTN } from '../src/enums/PerformanceTimingNames'; @@ -587,6 +590,49 @@ describe('utils', () => { assert.strictEqual(result, false); }); }); + + describe('parseUrl', () => { + const urlFields: Array = [ + 'hash', + 'host', + 'hostname', + 'href', + 'origin', + 'password', + 'pathname', + 'port', + 'protocol', + 'search', + 'username', + ]; + it('should parse url', () => { + const url = parseUrl('https://opentelemetry.io/foo'); + urlFields.forEach(field => { + assert.strictEqual(typeof url[field], 'string'); + }); + }); + + it('should parse url with fallback', () => { + sinon.stub(window, 'URL').value(undefined); + const url = parseUrl('https://opentelemetry.io/foo'); + urlFields.forEach(field => { + assert.strictEqual(typeof url[field], 'string'); + }); + }); + }); + + describe('normalizeUrl', () => { + it('should normalize url', () => { + const url = normalizeUrl('https://opentelemetry.io/你好'); + assert.strictEqual(url, 'https://opentelemetry.io/%E4%BD%A0%E5%A5%BD'); + }); + + it('should parse url with fallback', () => { + sinon.stub(window, 'URL').value(undefined); + const url = normalizeUrl('https://opentelemetry.io/你好'); + assert.strictEqual(url, 'https://opentelemetry.io/%E4%BD%A0%E5%A5%BD'); + }); + }); }); function getElementByXpath(path: string) {