Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(xhr): make performance observer work with relative urls #2226

Merged
merged 8 commits into from Jun 23, 2021
2 changes: 1 addition & 1 deletion packages/opentelemetry-core/src/utils/url.ts
Expand Up @@ -17,7 +17,7 @@ export function urlMatches(url: string, urlToMatch: string | RegExp): boolean {
if (typeof urlToMatch === 'string') {
return url === urlToMatch;
} else {
return !!url.match(urlToMatch);
return urlToMatch.test(url);
}
}
/**
Expand Down
Expand Up @@ -46,6 +46,16 @@ import { AttributeNames } from './enums/AttributeNames';
// safe enough
const OBSERVER_WAIT_TIME_MS = 300;

// Used to normalize relative URLs
let a: HTMLAnchorElement | undefined;
obecny marked this conversation as resolved.
Show resolved Hide resolved
const getUrlNormalizingAnchor = () => {
if (!a) {
a = document.createElement('a');
}

return a;
};

export type XHRCustomAttributeFunction = (
span: api.Span,
xhr: XMLHttpRequest
Expand Down Expand Up @@ -216,10 +226,13 @@ export class XMLHttpRequestInstrumentation extends InstrumentationBase<XMLHttpRe
xhrMem.createdResources = {
observer: new PerformanceObserver(list => {
const entries = list.getEntries() as PerformanceResourceTiming[];
const urlNormalizingAnchor = getUrlNormalizingAnchor();
urlNormalizingAnchor.href = spanUrl;

entries.forEach(entry => {
if (
entry.initiatorType === 'xmlhttprequest' &&
entry.name === spanUrl
entry.name === urlNormalizingAnchor.href
Copy link
Contributor

@MSNev MSNev Jun 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This won't work with all IE versions (depending on how it's hosted) as it (IE) can rewrite the URL as http://xxxx:443/yyy (which is just annoying).

For Application Insights we added a helper function that we run everything through to avoid the issue to ensure that all URL's (page and dependency (XHR)) calls get reported as expected. https://github.com/microsoft/ApplicationInsights-JS/blob/master/shared/AppInsightsCommon/src/UrlHelperFuncs.ts#L86-L93

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the URL passed into the above function is generated by the same href trick being used here, we just "cache" a couple to avoid recreating new elements everytime.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure which versions of IE we support right now ? cc @obecny @dyladan

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry was on vacation. Anyways IE doesn't have PerformanceObserver support so this should not be an issue here as this code is never ran in IE. But this .href trick is used elsewhere also so maybe lets create a separate issue to figure it out?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vmarchaud officially i'm not sure we claim any specific browser support. Unofficially, I think we only really work in IE 11

@MSNev is this a blocker for this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe it's a blocker, and it can be fixed later (if people complain).

And it should be fixed more generally so that any checked / reported URLs don't get logged as http:///domain.xxx:443/ which is the main issue and only becomes a code bug when comparing different (unnormalized) Urls.

) {
if (xhrMem.createdResources) {
xhrMem.createdResources.entries.push(entry);
Expand Down
Expand Up @@ -125,6 +125,36 @@ function createMainResource(resource = {}): PerformanceResourceTiming {
return mainResource;
}

function createFakePerformanceObs(url: string) {
class FakePerfObs implements PerformanceObserver {
constructor(private readonly cb: PerformanceObserverCallback) {}
observe() {
const absoluteUrl = url.startsWith('http') ? url : location.origin + url;
const resources: PerformanceObserverEntryList = {
getEntries(): PerformanceEntryList {
return [
createResource({ name: absoluteUrl }) as any,
createMainResource({ name: absoluteUrl }) as any,
];
},
getEntriesByName(): PerformanceEntryList {
return [];
},
getEntriesByType(): PerformanceEntryList {
return [];
},
};
this.cb(resources, this);
}
disconnect() {}
takeRecords(): PerformanceEntryList {
return [];
}
}

return FakePerfObs;
}

describe('xhr', () => {
const asyncTests = [{ async: true }, { async: false }];
asyncTests.forEach(test => {
Expand Down Expand Up @@ -200,6 +230,11 @@ describe('xhr', () => {
'getEntriesByType'
);
spyEntries.withArgs('resource').returns(resources);

sinon
.stub(window, 'PerformanceObserver')
.value(createFakePerformanceObs(fileUrl));

xmlHttpRequestInstrumentation = new XMLHttpRequestInstrumentation(
config
);
Expand All @@ -221,7 +256,7 @@ describe('xhr', () => {

rootSpan = webTracerWithZone.startSpan('root');
api.context.with(api.setSpan(api.context.active(), rootSpan), () => {
getData(
void getData(
new XMLHttpRequest(),
fileUrl,
() => {
Expand Down Expand Up @@ -635,20 +670,11 @@ describe('xhr', () => {

beforeEach(done => {
requests = [];
const resources: PerformanceResourceTiming[] = [];
resources.push(
createResource({
name: firstUrl,
}),
createResource({
name: secondUrl,
})
);
const reusableReq = new XMLHttpRequest();
api.context.with(
api.setSpan(api.context.active(), rootSpan),
() => {
getData(
void getData(
reusableReq,
firstUrl,
() => {
Expand All @@ -665,7 +691,7 @@ describe('xhr', () => {
api.context.with(
api.setSpan(api.context.active(), rootSpan),
() => {
getData(
void getData(
reusableReq,
secondUrl,
() => {
Expand Down Expand Up @@ -728,6 +754,35 @@ describe('xhr', () => {
assert.ok(attributes['xhr-custom-attribute'] === 'bar');
});
});

describe('when using relative url', () => {
beforeEach(done => {
clearData();
const propagateTraceHeaderCorsUrls = [window.location.origin];
prepareData(done, '/get', { propagateTraceHeaderCorsUrls });
});

it('should create correct span with events', () => {
// no prefetch span because mock observer uses location.origin as url when relative
// and prefetch span finding compares url origins
const span: tracing.ReadableSpan = exportSpy.args[0][0][0];
const events = span.events;

assert.strictEqual(
exportSpy.args.length,
1,
`Wrong number of spans: ${exportSpy.args.length}`
);

assert.strictEqual(events.length, 12, `number of events is wrong: ${events.length}`);
assert.strictEqual(
events[8].name,
PTN.REQUEST_START,
`event ${PTN.REQUEST_START} is not defined`
);
});
});

});

describe('when request is NOT successful', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/opentelemetry-web/src/utils.ts
Expand Up @@ -155,7 +155,7 @@ export function getResource(
mainRequest: filteredResources[0],
};
}
const sorted = sortResources(filteredResources.slice());
const sorted = sortResources(filteredResources);

const parsedSpanUrl = parseUrl(spanUrl);
if (parsedSpanUrl.origin !== window.location.origin && sorted.length > 1) {
Expand Down