Skip to content

Commit bff4d68

Browse files
committed
xhr tests working
1 parent dce344f commit bff4d68

File tree

2 files changed

+122
-3
lines changed

2 files changed

+122
-3
lines changed

packages/tracing/test/hub.test.ts

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
/* eslint-disable @typescript-eslint/unbound-method */
22
import { BrowserClient } from '@sentry/browser';
33
import { getMainCarrier, Hub } from '@sentry/hub';
4+
import * as hubModule from '@sentry/hub';
45
import * as utilsModule from '@sentry/utils'; // for mocking
56
import { getGlobalObject, isNodeEnv, logger } from '@sentry/utils';
67
import * as nodeHttpModule from 'http';
78

9+
import { BrowserTracing } from '../src/browser/browsertracing';
810
import { addExtensionMethods } from '../src/hubextensions';
11+
import { extractTraceparentData, TRACEPARENT_REGEXP } from '../src/utils';
12+
import { addDOMPropertiesToGlobal, getSymbolObjectKeyByName } from './testutils';
913

1014
addExtensionMethods();
1115

16+
// we have to add things into the real global object (rather than mocking the return value of getGlobalObject)
17+
// because there are modules which call getGlobalObject as they load, which is too early for jest to intervene
18+
addDOMPropertiesToGlobal(['XMLHttpRequest', 'Event', 'location', 'document']);
19+
1220
describe('Hub', () => {
1321
beforeEach(() => {
1422
jest.spyOn(logger, 'warn');
@@ -306,12 +314,80 @@ describe('Hub', () => {
306314
expect(child.sampled).toBe(transaction.sampled);
307315
});
308316

309-
it('should propagate sampling decision to child transactions in XHR header', () => {
310-
// TODO fix this and write the test
317+
it('should propagate positive sampling decision to child transactions in XHR header', () => {
318+
const hub = new Hub(
319+
new BrowserClient({
320+
dsn: 'https://[email protected]/1121',
321+
tracesSampleRate: 1,
322+
integrations: [new BrowserTracing()],
323+
}),
324+
);
325+
jest.spyOn(hubModule, 'getCurrentHub').mockReturnValue(hub);
326+
327+
const transaction = hub.startTransaction({ name: 'dogpark' });
328+
hub.configureScope(scope => {
329+
scope.setSpan(transaction);
330+
});
331+
332+
const request = new XMLHttpRequest();
333+
request.open('GET', '/chase-partners');
334+
335+
// mock a response having been received successfully (we have to do it in this roundabout way because readyState
336+
// is readonly and changing it doesn't trigger a readystatechange event)
337+
Object.defineProperty(request, 'readyState', { value: 4 });
338+
request.dispatchEvent(new Event('readystatechange'));
339+
340+
// this looks weird, it's true, but it's really just `request.impl.flag.requestHeaders` - it's just that the
341+
// `impl` key is a symbol rather than a string, and therefore needs to be referred to by reference rather than
342+
// value
343+
const headers = (request as any)[getSymbolObjectKeyByName(request, 'impl') as symbol].flag.requestHeaders;
344+
345+
// check that sentry-trace header is added to request
346+
expect(headers).toEqual(expect.objectContaining({ 'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP) }));
347+
348+
// check that sampling decision is passed down correctly
349+
expect(transaction.sampled).toBe(true);
350+
expect(extractTraceparentData(headers['sentry-trace'])!.parentSampled).toBe(true);
351+
});
352+
353+
it('should propagate negative sampling decision to child transactions in XHR header', () => {
354+
const hub = new Hub(
355+
new BrowserClient({
356+
dsn: 'https://[email protected]/1121',
357+
tracesSampleRate: 1,
358+
integrations: [new BrowserTracing()],
359+
}),
360+
);
361+
jest.spyOn(hubModule, 'getCurrentHub').mockReturnValue(hub);
362+
363+
const transaction = hub.startTransaction({ name: 'dogpark', sampled: false });
364+
hub.configureScope(scope => {
365+
scope.setSpan(transaction);
366+
});
367+
368+
const request = new XMLHttpRequest();
369+
request.open('GET', '/chase-partners');
370+
371+
// mock a response having been received successfully (we have to do it in this roundabout way because readyState
372+
// is readonly and changing it doesn't trigger a readystatechange event)
373+
Object.defineProperty(request, 'readyState', { value: 4 });
374+
request.dispatchEvent(new Event('readystatechange'));
375+
376+
// this looks weird, it's true, but it's really just `request.impl.flag.requestHeaders` - it's just that the
377+
// `impl` key is a symbol rather than a string, and therefore needs to be referred to by reference rather than
378+
// value
379+
const headers = (request as any)[getSymbolObjectKeyByName(request, 'impl') as symbol].flag.requestHeaders;
380+
381+
// check that sentry-trace header is added to request
382+
expect(headers).toEqual(expect.objectContaining({ 'sentry-trace': expect.stringMatching(TRACEPARENT_REGEXP) }));
383+
384+
// check that sampling decision is passed down correctly
385+
expect(transaction.sampled).toBe(false);
386+
expect(extractTraceparentData(headers['sentry-trace'])!.parentSampled).toBe(false);
311387
});
312388

313389
it('should propagate sampling decision to child transactions in fetch header', () => {
314-
// TODO fix this and write the test
390+
// TODO (kmclb)
315391
});
316392

317393
it("should inherit parent's sampling decision when creating a new transaction if tracesSampler is undefined", () => {

packages/tracing/test/testutils.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { getGlobalObject } from '@sentry/utils';
2+
import { JSDOM } from 'jsdom';
3+
4+
/**
5+
* Injects DOM properties into node global object.
6+
*
7+
* Useful for running tests where some of the tested code applies to @sentry/node and some applies to @sentry/browser
8+
* (e.g. tests in @sentry/tracing or @sentry/hub). Note that not all properties from the browser `window` object are
9+
* available.
10+
*
11+
* @param properties The names of the properties to add
12+
*/
13+
export function addDOMPropertiesToGlobal(properties: string[]): void {
14+
// we have to add things into the real global object (rather than mocking the return value of getGlobalObject)
15+
// because there are modules which call getGlobalObject as they load, which is too early for jest to intervene
16+
const { window } = new JSDOM('', { url: 'http://dogs.are.great/' });
17+
const global = getGlobalObject<NodeJS.Global & Window>();
18+
19+
properties.forEach(prop => {
20+
(global as any)[prop] = window[prop];
21+
});
22+
}
23+
24+
/**
25+
* Returns the symbol with the given description being used as a key in the given object.
26+
*
27+
* In the case where there are multiple symbols in the object with the same description, it throws an error.
28+
*
29+
* @param obj The object whose symbol-type key you want
30+
* @param description The symbol's descriptor
31+
* @returns The first symbol found in the object with the given description, or undefined if not found.
32+
*/
33+
export function getSymbolObjectKeyByName(obj: Record<string | symbol, any>, description: string): symbol | undefined {
34+
const symbols = Object.getOwnPropertySymbols(obj);
35+
36+
const matches = symbols.filter(sym => sym.toString() === `Symbol(${description})`);
37+
38+
if (matches.length > 1) {
39+
throw new Error(`More than one symbol key found with description '${description}'.`);
40+
}
41+
42+
return matches[0] || undefined;
43+
}

0 commit comments

Comments
 (0)