Skip to content

Commit ea6b9ef

Browse files
committed
add tracestate header to outgoing requests
1 parent d90032b commit ea6b9ef

File tree

4 files changed

+161
-43
lines changed

4 files changed

+161
-43
lines changed

packages/node/src/integrations/http.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getCurrentHub } from '@sentry/core';
2-
import { Integration, Span } from '@sentry/types';
2+
import { Integration, Span, TraceHeaders } from '@sentry/types';
33
import { fill, logger, parseSemver } from '@sentry/utils';
44
import * as http from 'http';
55
import * as https from 'https';
@@ -115,11 +115,9 @@ function _createWrappedRequestMethodFactory(
115115
op: 'request',
116116
});
117117

118-
const sentryTraceHeader = span.toTraceparent();
119-
logger.log(
120-
`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to ${requestUrl}: `,
121-
);
122-
requestOptions.headers = { ...requestOptions.headers, 'sentry-trace': sentryTraceHeader };
118+
const traceHeaders = span.getTraceHeaders();
119+
logger.log(`[Tracing] Adding sentry-trace and tracestate headers to outgoing request to ${requestUrl}.`);
120+
requestOptions.headers = { ...requestOptions.headers, ...(traceHeaders as TraceHeaders) };
123121
}
124122
}
125123

packages/node/test/integrations/http.test.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { NodeClient } from '../../src/client';
99
import { Http as HttpIntegration } from '../../src/integrations/http';
1010

1111
describe('tracing', () => {
12-
function createTransactionOnScope() {
12+
function createTransactionOnScope(): Transaction {
1313
const hub = new Hub(
1414
new NodeClient({
1515
dsn: 'https://[email protected]/12312012',
@@ -24,7 +24,10 @@ describe('tracing', () => {
2424
jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub);
2525
jest.spyOn(hubModule, 'getCurrentHub').mockReturnValue(hub);
2626

27-
const transaction = hub.startTransaction({ name: 'dogpark' });
27+
// we have to cast this to a Transaction (the class) because hub.startTransaction only returns a Transaction (the
28+
// interface, which doesn't have things like spanRecorder) (and because @sentry/hub can't depend on @sentry/tracing,
29+
// we can't fix that)
30+
const transaction = hub.startTransaction({ name: 'dogpark' }) as Transaction;
2831
hub.getScope()?.setSpan(transaction);
2932

3033
return transaction;
@@ -36,7 +39,7 @@ describe('tracing', () => {
3639
.reply(200);
3740

3841
const transaction = createTransactionOnScope();
39-
const spans = (transaction as Span).spanRecorder?.spans as Span[];
42+
const spans = transaction.spanRecorder?.spans as Span[];
4043

4144
http.get('http://dogs.are.great/');
4245

@@ -55,7 +58,7 @@ describe('tracing', () => {
5558
.reply(200);
5659

5760
const transaction = createTransactionOnScope();
58-
const spans = (transaction as Span).spanRecorder?.spans as Span[];
61+
const spans = transaction.spanRecorder?.spans as Span[];
5962

6063
http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/');
6164

@@ -64,21 +67,23 @@ describe('tracing', () => {
6467
expect((spans[0] as Transaction).name).toEqual('dogpark');
6568
});
6669

67-
it('attaches the sentry-trace header to outgoing non-sentry requests', async () => {
70+
it('attaches tracing headers to outgoing non-sentry requests', async () => {
6871
nock('http://dogs.are.great')
6972
.get('/')
7073
.reply(200);
7174

7275
createTransactionOnScope();
7376

7477
const request = http.get('http://dogs.are.great/');
75-
const sentryTraceHeader = request.getHeader('sentry-trace') as string;
78+
const sentryTraceHeader = request.getHeader('sentry-trace');
79+
const tracestateHeader = request.getHeader('tracestate');
7680

7781
expect(sentryTraceHeader).toBeDefined();
78-
expect(SENTRY_TRACE_REGEX.test(sentryTraceHeader)).toBe(true);
82+
expect(tracestateHeader).toBeDefined();
83+
expect(SENTRY_TRACE_REGEX.test(sentryTraceHeader as string)).toBe(true);
7984
});
8085

81-
it("doesn't attach the sentry-trace header to outgoing sentry requests", () => {
86+
it("doesn't attach tracing headers to outgoing sentry requests", () => {
8287
nock('http://squirrelchasers.ingest.sentry.io')
8388
.get('/api/12312012/store/')
8489
.reply(200);
@@ -87,7 +92,9 @@ describe('tracing', () => {
8792

8893
const request = http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/');
8994
const sentryTraceHeader = request.getHeader('sentry-trace');
95+
const tracestateHeader = request.getHeader('tracestate');
9096

9197
expect(sentryTraceHeader).not.toBeDefined();
98+
expect(tracestateHeader).not.toBeDefined();
9299
});
93100
});

packages/tracing/src/browser/request.ts

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { Span } from '@sentry/types';
12
import { addInstrumentationHandler, isInstanceOf, isMatchingPattern } from '@sentry/utils';
23

3-
import { Span } from '../span';
44
import { SpanStatus } from '../spanstatus';
55
import { getActiveTransaction, hasTracingEnabled } from '../utils';
66

@@ -59,6 +59,16 @@ export interface FetchData {
5959
endTimestamp?: number;
6060
}
6161

62+
type PolymorphicRequestHeaders =
63+
| Record<string, string>
64+
| Array<[string, string]>
65+
// the below is not preicsely the Header type used in Request, but it'll pass duck-typing
66+
| {
67+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
68+
[key: string]: any;
69+
append: (key: string, value: string) => void;
70+
};
71+
6272
/** Data returned from XHR request */
6373
export interface XHRData {
6474
xhr?: {
@@ -183,22 +193,26 @@ export function fetchCallback(
183193
const request = (handlerData.args[0] = handlerData.args[0] as string | Request);
184194
// eslint-disable-next-line @typescript-eslint/no-explicit-any
185195
const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string]: any }) || {});
186-
let headers = options.headers;
196+
let headers: PolymorphicRequestHeaders = options.headers;
187197
if (isInstanceOf(request, Request)) {
188198
headers = (request as Request).headers;
189199
}
200+
const traceHeaders = span.getTraceHeaders() as Record<string, string>;
190201
if (headers) {
191-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
192-
if (typeof headers.append === 'function') {
193-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
194-
headers.append('sentry-trace', span.toTraceparent());
202+
if ('append' in headers && typeof headers.append === 'function') {
203+
headers.append('sentry-trace', traceHeaders['sentry-trace']);
204+
if (traceHeaders.tracestate) {
205+
headers.append('tracestate', traceHeaders.tracestate);
206+
}
195207
} else if (Array.isArray(headers)) {
196-
headers = [...headers, ['sentry-trace', span.toTraceparent()]];
208+
// TODO use the nicer version below once we stop supporting Node 6
209+
// headers = [...headers, ...Object.entries(traceHeaders)];
210+
headers = [...headers, ['sentry-trace', traceHeaders['sentry-trace']], ['tracestate', traceHeaders.tracestate]];
197211
} else {
198-
headers = { ...headers, 'sentry-trace': span.toTraceparent() };
212+
headers = { ...headers, ...traceHeaders };
199213
}
200214
} else {
201-
headers = { 'sentry-trace': span.toTraceparent() };
215+
headers = traceHeaders;
202216
}
203217
options.headers = headers;
204218
}
@@ -254,7 +268,11 @@ export function xhrCallback(
254268

255269
if (handlerData.xhr.setRequestHeader) {
256270
try {
257-
handlerData.xhr.setRequestHeader('sentry-trace', span.toTraceparent());
271+
const sentryHeaders = span.getTraceHeaders();
272+
handlerData.xhr.setRequestHeader('sentry-trace', sentryHeaders['sentry-trace']);
273+
if (sentryHeaders.tracestate) {
274+
handlerData.xhr.setRequestHeader('tracestate', sentryHeaders.tracestate);
275+
}
258276
} catch (_) {
259277
// Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.
260278
}

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

Lines changed: 114 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,41 @@ 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';
10+
11+
// This is a normal base64 regex, modified to reflect that fact that we strip the trailing = or == off
12+
const stripped_base64 = '([a-zA-Z0-9+/]{4})*([a-zA-Z0-9+/]{2,3})?';
13+
14+
const TRACESTATE_HEADER_REGEX = new RegExp(
15+
`sentry=(${stripped_base64})` + // our part of the header - should be the only part or at least the first part
16+
`(,\\w+=\\w+)*`, // any number of copies of a comma followed by `name=value`
17+
);
918

1019
beforeAll(() => {
1120
addExtensionMethods();
12-
// @ts-ignore need to override global Request because it's not in the jest environment (even with an
13-
// `@jest-environment jsdom` directive, for some reason)
14-
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;
1544
});
1645

1746
const hasTracingEnabled = jest.spyOn(tracingUtils, 'hasTracingEnabled');
@@ -49,15 +78,15 @@ describe('instrumentOutgoingRequests', () => {
4978
});
5079
});
5180

52-
describe('callbacks', () => {
81+
describe('fetch and xhr callbacks', () => {
5382
let hub: Hub;
5483
let transaction: Transaction;
5584
const alwaysCreateSpan = () => true;
5685
const neverCreateSpan = () => false;
5786
const fetchHandlerData: FetchData = {
5887
args: ['http://dogs.are.great/', {}],
5988
fetchData: { url: 'http://dogs.are.great/', method: 'GET' },
60-
startTimestamp: 1356996072000,
89+
startTimestamp: 2012112120121231,
6190
};
6291
const xhrHandlerData: XHRData = {
6392
xhr: {
@@ -72,18 +101,27 @@ describe('callbacks', () => {
72101
// setRequestHeader: XMLHttpRequest.prototype.setRequestHeader,
73102
setRequestHeader,
74103
},
75-
startTimestamp: 1353501072000,
104+
startTimestamp: 2012112120121231,
76105
};
77-
const endTimestamp = 1356996072000;
106+
const endTimestamp = 2013041520130908;
78107

79108
beforeAll(() => {
80-
hub = new Hub(new BrowserClient({ tracesSampleRate: 1 }));
109+
hub = new Hub(
110+
new BrowserClient({
111+
dsn: 'https://[email protected]/12312012',
112+
environment: 'dogpark',
113+
release: 'off.leash.park',
114+
115+
tracesSampleRate: 1,
116+
}),
117+
);
81118
makeMain(hub);
82119
});
83120

84121
beforeEach(() => {
85-
transaction = hub.startTransaction({ name: 'organizations/users/:userid', op: 'pageload' }) as Transaction;
122+
transaction = hub.startTransaction({ name: 'meetNewDogFriend', op: 'wag.tail' }) as Transaction;
86123
hub.configureScope(scope => scope.setSpan(transaction));
124+
jest.clearAllMocks();
87125
});
88126

89127
describe('fetchCallback()', () => {
@@ -111,20 +149,17 @@ describe('callbacks', () => {
111149
expect(spans).toEqual({});
112150
});
113151

114-
it('does not add fetch request headers if tracing is disabled', () => {
152+
it('does not add tracing headers if tracing is disabled', () => {
115153
hasTracingEnabled.mockReturnValueOnce(false);
116154

117155
// make a local copy so the global one doesn't get mutated
118-
const handlerData: FetchData = {
119-
args: ['http://dogs.are.great/', {}],
120-
fetchData: { url: 'http://dogs.are.great/', method: 'GET' },
121-
startTimestamp: 1353501072000,
122-
};
156+
const handlerData = { ...fetchHandlerData };
123157

124158
fetchCallback(handlerData, alwaysCreateSpan, {});
125159

126160
const headers = (handlerData.args[1].headers as Record<string, string>) || {};
127161
expect(headers['sentry-trace']).not.toBeDefined();
162+
expect(headers['tracestate']).not.toBeDefined();
128163
});
129164

130165
it('creates and finishes fetch span on active transaction', () => {
@@ -179,8 +214,67 @@ describe('callbacks', () => {
179214
expect(newSpan!.status).toBe(SpanStatus.fromHttpCode(404));
180215
});
181216

182-
it('adds sentry-trace header to fetch requests', () => {
183-
// TODO
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'), {}] };
220+
221+
fetchCallback(handlerData, alwaysCreateSpan, {});
222+
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+
});
184278
});
185279
});
186280

@@ -201,21 +295,22 @@ describe('callbacks', () => {
201295
expect(spans).toEqual({});
202296
});
203297

204-
it('does not add xhr request headers if tracing is disabled', () => {
298+
it('does not add tracing headers if tracing is disabled', () => {
205299
hasTracingEnabled.mockReturnValueOnce(false);
206300

207301
xhrCallback(xhrHandlerData, alwaysCreateSpan, {});
208302

209303
expect(setRequestHeader).not.toHaveBeenCalled();
210304
});
211305

212-
it('adds sentry-trace header to XHR requests', () => {
306+
it('adds tracing headers to XHR requests', () => {
213307
xhrCallback(xhrHandlerData, alwaysCreateSpan, {});
214308

215309
expect(setRequestHeader).toHaveBeenCalledWith(
216310
'sentry-trace',
217311
expect.stringMatching(tracingUtils.SENTRY_TRACE_REGEX),
218312
);
313+
expect(setRequestHeader).toHaveBeenCalledWith('tracestate', expect.stringMatching(TRACESTATE_HEADER_REGEX));
219314
});
220315

221316
it('creates and finishes XHR span on active transaction', () => {

0 commit comments

Comments
 (0)