Skip to content

Commit

Permalink
fix: remove window and document dependencies in web packages (#2689)
Browse files Browse the repository at this point in the history
Co-authored-by: Valentin Marchaud <contact@vmarchaud.fr>
  • Loading branch information
legendecas and vmarchaud committed Dec 24, 2021
1 parent 50252a9 commit 5d22aba
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Window['fetch']>
this: typeof globalThis,
...args: Parameters<typeof fetch>
): Promise<Response> {
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);
Expand Down Expand Up @@ -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)
);
}
);
Expand Down Expand Up @@ -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);
}
Expand All @@ -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<PerformanceResourceTiming>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
parseUrl,
PerformanceTimingNames as PTN,
shouldPropagateTraceHeaders,
getUrlNormalizingAnchor
} from '@opentelemetry/sdk-trace-web';
import { EventNames } from './enums/EventNames';
import {
Expand Down Expand Up @@ -209,21 +208,20 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
const xhrMem = this._xhrMem.get(xhr);
if (
!xhrMem ||
typeof window.PerformanceObserver === 'undefined' ||
typeof window.PerformanceResourceTiming === 'undefined'
PerformanceObserver == null ||
PerformanceResourceTiming == null
) {
return;
}
xhrMem.createdResources = {
observer: new PerformanceObserver(list => {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ function sendWithXhr(
urlStr: string,
xhrHeaders: Record<string, string> = {}
) {
const xhr = new window.XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open('POST', urlStr);
Object.entries(xhrHeaders).forEach(([k, v]) => {
xhr.setRequestHeader(k, v);
Expand Down
47 changes: 39 additions & 8 deletions packages/opentelemetry-sdk-trace-web/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions packages/opentelemetry-sdk-trace-web/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ import {
addSpanNetworkEvents,
getElementXPath,
getResource,
normalizeUrl,
parseUrl,
PerformanceEntries,
shouldPropagateTraceHeaders,
URLLike,
} from '../src';
import { PerformanceTimingNames as PTN } from '../src/enums/PerformanceTimingNames';

Expand Down Expand Up @@ -587,6 +590,49 @@ describe('utils', () => {
assert.strictEqual(result, false);
});
});

describe('parseUrl', () => {
const urlFields: Array<keyof URLLike> = [
'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) {
Expand Down

0 comments on commit 5d22aba

Please sign in to comment.