Skip to content

Commit 3b5f3fb

Browse files
committed
fix(tracing): Use Request.headers.append correctly (#3311)
1 parent 37dd9ac commit 3b5f3fb

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
@@ -6,6 +6,7 @@ import { Span, SpanStatus, Transaction } from '../../src';
66
import { fetchCallback, FetchData, instrumentOutgoingRequests, xhrCallback, XHRData } from '../../src/browser/request';
77
import { addExtensionMethods } from '../../src/hubextensions';
88
import * as tracingUtils from '../../src/utils';
9+
import { objectFromEntries } from '../testutils';
910

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

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

2546
const hasTracingEnabled = jest.spyOn(tracingUtils, 'hasTracingEnabled');
@@ -57,7 +78,7 @@ describe('instrumentOutgoingRequests', () => {
5778
});
5879
});
5980

60-
describe('callbacks', () => {
81+
describe('fetch and xhr callbacks', () => {
6182
let hub: Hub;
6283
let transaction: Transaction;
6384
const alwaysCreateSpan = () => true;
@@ -193,15 +214,67 @@ describe('callbacks', () => {
193214
expect(newSpan!.status).toBe(SpanStatus.fromHttpCode(404));
194215
});
195216

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

200-
fetchCallback(handlerData, alwaysCreateSpan, {});
221+
fetchCallback(handlerData, alwaysCreateSpan, {});
201222

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

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)