Skip to content

Commit 6364402

Browse files
authored
fix(tracing): Use Request.headers.append correctly (#3311)
1 parent 9921baa commit 6364402

File tree

3 files changed

+119
-19
lines changed

3 files changed

+119
-19
lines changed

packages/tracing/src/browser/request.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ export interface FetchData {
5858
endTimestamp?: number;
5959
}
6060

61+
type PolymorphicRequestHeaders =
62+
| Record<string, string>
63+
| Array<[string, string]>
64+
// the below is not preicsely the Header type used in Request, but it'll pass duck-typing
65+
| {
66+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
67+
[key: string]: any;
68+
append: (key: string, value: string) => void;
69+
};
70+
6171
/** Data returned from XHR request */
6272
export interface XHRData {
6373
xhr?: {
@@ -187,22 +197,26 @@ export function fetchCallback(
187197
const request = (handlerData.args[0] = handlerData.args[0] as string | Request);
188198
// eslint-disable-next-line @typescript-eslint/no-explicit-any
189199
const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string]: any }) || {});
190-
let headers = options.headers;
200+
let headers: PolymorphicRequestHeaders = options.headers;
191201
if (isInstanceOf(request, Request)) {
192202
headers = (request as Request).headers;
193203
}
204+
const traceHeaders = span.getTraceHeaders() as Record<string, string>;
194205
if (headers) {
195-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
196-
if (typeof headers.append === 'function') {
197-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
198-
headers.append(Object.entries(span.getTraceHeaders()));
206+
if ('append' in headers && typeof headers.append === 'function') {
207+
headers.append('sentry-trace', traceHeaders['sentry-trace']);
208+
if (traceHeaders.tracestate) {
209+
headers.append('tracestate', traceHeaders.tracestate);
210+
}
199211
} else if (Array.isArray(headers)) {
200-
headers = [...headers, ...Object.entries(span.getTraceHeaders())];
212+
// TODO use the nicer version below once we stop supporting Node 6
213+
// headers = [...headers, ...Object.entries(traceHeaders)];
214+
headers = [...headers, ['sentry-trace', traceHeaders['sentry-trace']], ['tracestate', traceHeaders.tracestate]];
201215
} else {
202-
headers = { ...headers, ...span.getTraceHeaders() };
216+
headers = { ...headers, ...traceHeaders };
203217
}
204218
} else {
205-
headers = span.getTraceHeaders();
219+
headers = traceHeaders;
206220
}
207221
options.headers = headers;
208222
}

packages/tracing/test/browser/request.test.ts

Lines changed: 84 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '../../src/browser/request';
1313
import { addExtensionMethods } from '../../src/hubextensions';
1414
import * as tracingUtils from '../../src/utils';
15+
import { objectFromEntries } from '../testutils';
1516

1617
// This is a normal base64 regex, modified to reflect that fact that we strip the trailing = or == off
1718
const stripped_base64 = '([a-zA-Z0-9+/]{4})*([a-zA-Z0-9+/]{2,3})?';
@@ -23,9 +24,29 @@ const TRACESTATE_HEADER_REGEX = new RegExp(
2324

2425
beforeAll(() => {
2526
addExtensionMethods();
26-
// @ts-ignore need to override global Request because it's not in the jest environment (even with an
27-
// `@jest-environment jsdom` directive, for some reason)
28-
global.Request = {};
27+
28+
// Add Request to the global scope (necessary because for some reason Request isn't in the jest environment, even with
29+
// an `@jest-environment jsdom` directive)
30+
31+
type MockHeaders = {
32+
[key: string]: any;
33+
append: (key: string, value: string) => void;
34+
};
35+
36+
class Request {
37+
public headers: MockHeaders;
38+
constructor() {
39+
// We need our headers to act like an object for key-lookup purposes, but also have an append method that adds
40+
// items as its siblings. This hack precludes a key named `append`, of course, but for our purposes it's enough.
41+
const headers = {} as MockHeaders;
42+
headers.append = (key: string, value: any): void => {
43+
headers[key] = value;
44+
};
45+
this.headers = headers;
46+
}
47+
}
48+
49+
(global as any).Request = Request;
2950
});
3051

3152
const hasTracingEnabled = jest.spyOn(tracingUtils, 'hasTracingEnabled');
@@ -63,7 +84,7 @@ describe('registerRequestInstrumentation', () => {
6384
});
6485
});
6586

66-
describe('callbacks', () => {
87+
describe('fetch and xhr callbacks', () => {
6788
let hub: Hub;
6889
let transaction: Transaction;
6990
const alwaysCreateSpan = () => true;
@@ -199,15 +220,67 @@ describe('callbacks', () => {
199220
expect(newSpan!.status).toBe(SpanStatus.fromHttpCode(404));
200221
});
201222

202-
it('adds tracing headers to fetch requests', () => {
203-
// make a local copy so the global one doesn't get mutated
204-
const handlerData = { ...fetchHandlerData };
223+
describe('adding tracing headers to fetch requests', () => {
224+
it('can handle headers added with an `append` method', () => {
225+
const handlerData: FetchData = { ...fetchHandlerData, args: [new Request('http://dogs.are.great'), {}] };
205226

206-
fetchCallback(handlerData, alwaysCreateSpan, {});
227+
fetchCallback(handlerData, alwaysCreateSpan, {});
207228

208-
const headers = (handlerData.args[1].headers as Record<string, string>) || {};
209-
expect(headers['sentry-trace']).toBeDefined();
210-
expect(headers['tracestate']).toBeDefined();
229+
const headers = handlerData.args[1].headers;
230+
expect(headers['sentry-trace']).toBeDefined();
231+
expect(headers['tracestate']).toBeDefined();
232+
});
233+
234+
it('can handle existing headers in array form', () => {
235+
const handlerData = {
236+
...fetchHandlerData,
237+
args: [
238+
'http://dogs.are.great/',
239+
{
240+
headers: [
241+
['GREETING_PROTOCOL', 'mutual butt sniffing'],
242+
['TAIL_ACTION', 'wagging'],
243+
],
244+
},
245+
],
246+
};
247+
248+
fetchCallback(handlerData, alwaysCreateSpan, {});
249+
250+
const headers = objectFromEntries((handlerData.args[1] as any).headers);
251+
expect(headers['sentry-trace']).toBeDefined();
252+
expect(headers['tracestate']).toBeDefined();
253+
});
254+
255+
it('can handle existing headers in object form', () => {
256+
const handlerData = {
257+
...fetchHandlerData,
258+
args: [
259+
'http://dogs.are.great/',
260+
{
261+
headers: { GREETING_PROTOCOL: 'mutual butt sniffing', TAIL_ACTION: 'wagging' },
262+
},
263+
],
264+
};
265+
266+
fetchCallback(handlerData, alwaysCreateSpan, {});
267+
268+
const headers = (handlerData.args[1] as any).headers;
269+
expect(headers['sentry-trace']).toBeDefined();
270+
expect(headers['tracestate']).toBeDefined();
271+
});
272+
273+
it('can handle there being no existing headers', () => {
274+
// override the value of `args`, even though we're overriding it with the same data, as a means of deep copying
275+
// the one part which gets mutated
276+
const handlerData = { ...fetchHandlerData, args: ['http://dogs.are.great/', {}] };
277+
278+
fetchCallback(handlerData, alwaysCreateSpan, {});
279+
280+
const headers = (handlerData.args[1] as any).headers;
281+
expect(headers['sentry-trace']).toBeDefined();
282+
expect(headers['tracestate']).toBeDefined();
283+
});
211284
});
212285
});
213286

packages/tracing/test/testutils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,16 @@ export const testOnlyIfNodeVersionAtLeast = (minVersion: number): jest.It => {
5656

5757
return it;
5858
};
59+
60+
/** Polyfill for `Object.fromEntries`, for Node < 12 */
61+
export const objectFromEntries =
62+
'fromEntries' in Object
63+
? (Object as { fromEntries: any }).fromEntries
64+
: (entries: Array<[string, any]>): Record<string, any> => {
65+
const result: Record<string, any> = {};
66+
entries.forEach(entry => {
67+
const [key, value] = entry;
68+
result[key] = value;
69+
});
70+
return result;
71+
};

0 commit comments

Comments
 (0)