Skip to content

Commit 4cf7262

Browse files
authored
feat(tracing): Handle incoming tracestate data, allow for third-party data (#3275)
1 parent 965013c commit 4cf7262

File tree

19 files changed

+526
-144
lines changed

19 files changed

+526
-144
lines changed

packages/node/src/handlers.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-lines */
22
/* eslint-disable @typescript-eslint/no-explicit-any */
33
import { captureException, getCurrentHub, startTransaction, withScope } from '@sentry/core';
4-
import { extractTraceparentData, Span } from '@sentry/tracing';
4+
import { extractSentrytraceData, extractTracestateData, Span } from '@sentry/tracing';
55
import { Event, ExtractedNodeRequestData, Transaction } from '@sentry/types';
66
import { forget, isPlainObject, isString, logger, normalize, stripUrlQueryAndFragment } from '@sentry/utils';
77
import * as cookie from 'cookie';
@@ -55,17 +55,21 @@ export function tracingHandler(): (
5555
res: http.ServerResponse,
5656
next: (error?: any) => void,
5757
): void {
58-
// If there is a trace header set, we extract the data from it (parentSpanId, traceId, and sampling decision)
59-
let traceparentData;
60-
if (req.headers && isString(req.headers['sentry-trace'])) {
61-
traceparentData = extractTraceparentData(req.headers['sentry-trace'] as string);
58+
// Extract data from trace headers
59+
let traceparentData, tracestateData;
60+
if (req.headers?.['sentry-trace']) {
61+
traceparentData = extractSentrytraceData(req.headers['sentry-trace'] as string);
62+
}
63+
if (req.headers?.tracestate) {
64+
tracestateData = extractTracestateData(req.headers.tracestate as string);
6265
}
6366

6467
const transaction = startTransaction(
6568
{
6669
name: extractExpressTransactionName(req, { path: true, method: true }),
6770
op: 'http.server',
6871
...traceparentData,
72+
...(tracestateData && { metadata: { tracestate: tracestateData } }),
6973
},
7074
{ request: extractRequestData(req) },
7175
);

packages/node/test/handlers.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,11 @@ describe('tracingHandler', () => {
221221
expect(startTransaction).toHaveBeenCalled();
222222
});
223223

224-
it("pulls parent's data from tracing header on the request", () => {
225-
req.headers = { 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0' };
224+
it('pulls data from tracing headers on the request', () => {
225+
req.headers = {
226+
'sentry-trace': '12312012123120121231201212312012-1121201211212012-0',
227+
tracestate: 'sentry=doGsaREgReaT',
228+
};
226229

227230
sentryTracingMiddleware(req, res, next);
228231

@@ -232,6 +235,7 @@ describe('tracingHandler', () => {
232235
expect(transaction.traceId).toEqual('12312012123120121231201212312012');
233236
expect(transaction.parentSpanId).toEqual('1121201211212012');
234237
expect(transaction.sampled).toEqual(false);
238+
expect(transaction.metadata?.tracestate).toEqual({ sentry: 'sentry=doGsaREgReaT' });
235239
});
236240

237241
it('extracts request data for sampling context', () => {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as sentryCore from '@sentry/core';
22
import { Hub } from '@sentry/hub';
33
import * as hubModule from '@sentry/hub';
4-
import { addExtensionMethods, Span, TRACEPARENT_REGEXP, Transaction } from '@sentry/tracing';
4+
import { addExtensionMethods, SENTRY_TRACE_REGEX, Span, Transaction } from '@sentry/tracing';
55
import * as http from 'http';
66
import * as nock from 'nock';
77

@@ -77,7 +77,7 @@ describe('tracing', () => {
7777

7878
expect(sentryTraceHeader).toBeDefined();
7979
expect(tracestateHeader).toBeDefined();
80-
expect(TRACEPARENT_REGEXP.test(sentryTraceHeader as string)).toBe(true);
80+
expect(SENTRY_TRACE_REGEX.test(sentryTraceHeader as string)).toBe(true);
8181
});
8282

8383
it("doesn't attach tracing headers to outgoing sentry requests", () => {

packages/serverless/src/awslambda.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
withScope,
1010
} from '@sentry/node';
1111
import * as Sentry from '@sentry/node';
12-
import { extractTraceparentData } from '@sentry/tracing';
12+
import { extractSentrytraceData, extractTracestateData } from '@sentry/tracing';
1313
import { Integration } from '@sentry/types';
14-
import { isString, logger } from '@sentry/utils';
14+
import { logger } from '@sentry/utils';
1515
// NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil
1616
// eslint-disable-next-line import/no-unresolved
1717
import { Context, Handler } from 'aws-lambda';
@@ -192,7 +192,7 @@ export function wrapHandler<TEvent, TResult>(
192192
};
193193
let timeoutWarningTimer: NodeJS.Timeout;
194194

195-
// AWSLambda is like Express. It makes a distinction about handlers based on it's last argument
195+
// AWSLambda is like Express. It makes a distinction about handlers based on its last argument
196196
// async (event) => async handler
197197
// async (event, context) => async handler
198198
// (event, context, callback) => sync handler
@@ -243,16 +243,22 @@ export function wrapHandler<TEvent, TResult>(
243243
}, timeoutWarningDelay);
244244
}
245245

246-
// Applying `sentry-trace` to context
247-
let traceparentData;
246+
// Extract tracing data from headers
247+
let traceparentData, tracestateData;
248248
const eventWithHeaders = event as { headers?: { [key: string]: string } };
249-
if (eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])) {
250-
traceparentData = extractTraceparentData(eventWithHeaders.headers['sentry-trace'] as string);
249+
250+
if (eventWithHeaders.headers?.['sentry-trace']) {
251+
traceparentData = extractSentrytraceData(eventWithHeaders.headers['sentry-trace'] as string);
252+
}
253+
if (eventWithHeaders.headers?.tracestate) {
254+
tracestateData = extractTracestateData(eventWithHeaders.headers.tracestate as string);
251255
}
256+
252257
const transaction = startTransaction({
253258
name: context.functionName,
254259
op: 'awslambda.handler',
255260
...traceparentData,
261+
...(tracestateData && { metadata: { tracestate: tracestateData } }),
256262
});
257263

258264
const hub = getCurrentHub();

packages/serverless/src/gcpfunction/http.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { captureException, flush, getCurrentHub, Handlers, startTransaction } from '@sentry/node';
2-
import { extractTraceparentData } from '@sentry/tracing';
3-
import { isString, logger, stripUrlQueryAndFragment } from '@sentry/utils';
2+
import { extractSentrytraceData, extractTracestateData } from '@sentry/tracing';
3+
import { logger, stripUrlQueryAndFragment } from '@sentry/utils';
44

55
import { domainify, getActiveDomain, proxyFunction } from './../utils';
66
import { HttpFunction, WrapperOptions } from './general';
@@ -49,16 +49,22 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial<HttpFunctionWr
4949
const reqMethod = (req.method || '').toUpperCase();
5050
const reqUrl = stripUrlQueryAndFragment(req.originalUrl || req.url || '');
5151

52-
// Applying `sentry-trace` to context
53-
let traceparentData;
52+
// Extract tracing data from headers
53+
let traceparentData, tracestateData;
5454
const reqWithHeaders = req as { headers?: { [key: string]: string } };
55-
if (reqWithHeaders.headers && isString(reqWithHeaders.headers['sentry-trace'])) {
56-
traceparentData = extractTraceparentData(reqWithHeaders.headers['sentry-trace'] as string);
55+
56+
if (reqWithHeaders.headers?.['sentry-trace']) {
57+
traceparentData = extractSentrytraceData(reqWithHeaders.headers['sentry-trace'] as string);
58+
}
59+
if (reqWithHeaders.headers?.tracestate) {
60+
tracestateData = extractTracestateData(reqWithHeaders.headers.tracestate as string);
5761
}
62+
5863
const transaction = startTransaction({
5964
name: `${reqMethod} ${reqUrl}`,
6065
op: 'gcp.function.http',
6166
...traceparentData,
67+
...(tracestateData && { metadata: { tracestate: tracestateData } }),
6268
});
6369

6470
// getCurrentHub() is expected to use current active domain as a carrier

packages/serverless/test/awslambda.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,35 @@ describe('AWSLambda', () => {
221221
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
222222
});
223223

224+
test('incoming trace headers are correctly parsed and used', async () => {
225+
expect.assertions(1);
226+
227+
fakeEvent.headers = {
228+
'sentry-trace': '12312012123120121231201212312012-1121201211212012-0',
229+
tracestate: 'sentry=doGsaREgReaT,maisey=silly,charlie=goofy',
230+
};
231+
232+
const handler: Handler = (_event, _context, callback) => {
233+
expect(Sentry.startTransaction).toBeCalledWith(
234+
expect.objectContaining({
235+
traceId: '12312012123120121231201212312012',
236+
parentSpanId: '1121201211212012',
237+
parentSampled: false,
238+
metadata: {
239+
tracestate: {
240+
sentry: 'sentry=doGsaREgReaT',
241+
thirdparty: 'maisey=silly,charlie=goofy',
242+
},
243+
},
244+
}),
245+
);
246+
247+
callback(undefined, { its: 'fine' });
248+
};
249+
const wrappedHandler = wrapHandler(handler);
250+
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
251+
});
252+
224253
test('capture error', async () => {
225254
expect.assertions(10);
226255

@@ -278,6 +307,35 @@ describe('AWSLambda', () => {
278307
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
279308
});
280309

310+
test('incoming trace headers are correctly parsed and used', async () => {
311+
expect.assertions(1);
312+
313+
fakeEvent.headers = {
314+
'sentry-trace': '12312012123120121231201212312012-1121201211212012-0',
315+
tracestate: 'sentry=doGsaREgReaT,maisey=silly,charlie=goofy',
316+
};
317+
318+
const handler: Handler = async (_event, _context, callback) => {
319+
expect(Sentry.startTransaction).toBeCalledWith(
320+
expect.objectContaining({
321+
traceId: '12312012123120121231201212312012',
322+
parentSpanId: '1121201211212012',
323+
parentSampled: false,
324+
metadata: {
325+
tracestate: {
326+
sentry: 'sentry=doGsaREgReaT',
327+
thirdparty: 'maisey=silly,charlie=goofy',
328+
},
329+
},
330+
}),
331+
);
332+
333+
callback(undefined, { its: 'fine' });
334+
};
335+
const wrappedHandler = wrapHandler(handler);
336+
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
337+
});
338+
281339
test('capture error', async () => {
282340
expect.assertions(10);
283341

@@ -328,6 +386,35 @@ describe('AWSLambda', () => {
328386
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
329387
});
330388

389+
test('incoming trace headers are correctly parsed and used', async () => {
390+
expect.assertions(1);
391+
392+
fakeEvent.headers = {
393+
'sentry-trace': '12312012123120121231201212312012-1121201211212012-0',
394+
tracestate: 'sentry=doGsaREgReaT,maisey=silly,charlie=goofy',
395+
};
396+
397+
const handler: Handler = async (_event, _context, callback) => {
398+
expect(Sentry.startTransaction).toBeCalledWith(
399+
expect.objectContaining({
400+
traceId: '12312012123120121231201212312012',
401+
parentSpanId: '1121201211212012',
402+
parentSampled: false,
403+
metadata: {
404+
tracestate: {
405+
sentry: 'sentry=doGsaREgReaT',
406+
thirdparty: 'maisey=silly,charlie=goofy',
407+
},
408+
},
409+
}),
410+
);
411+
412+
callback(undefined, { its: 'fine' });
413+
};
414+
const wrappedHandler = wrapHandler(handler);
415+
await wrappedHandler(fakeEvent, fakeContext, fakeCallback);
416+
});
417+
331418
test('capture error', async () => {
332419
expect.assertions(10);
333420

packages/serverless/test/gcpfunction.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,35 @@ describe('GCPFunction', () => {
120120
expect(Sentry.flush).toBeCalledWith(2000);
121121
});
122122

123+
test('incoming trace headers are correctly parsed and used', async () => {
124+
expect.assertions(1);
125+
126+
const handler: HttpFunction = (_req, res) => {
127+
res.statusCode = 200;
128+
res.end();
129+
};
130+
const wrappedHandler = wrapHttpFunction(handler);
131+
const traceHeaders = {
132+
'sentry-trace': '12312012123120121231201212312012-1121201211212012-0',
133+
tracestate: 'sentry=doGsaREgReaT,maisey=silly,charlie=goofy',
134+
};
135+
await handleHttp(wrappedHandler, traceHeaders);
136+
137+
expect(Sentry.startTransaction).toBeCalledWith(
138+
expect.objectContaining({
139+
traceId: '12312012123120121231201212312012',
140+
parentSpanId: '1121201211212012',
141+
parentSampled: false,
142+
metadata: {
143+
tracestate: {
144+
sentry: 'sentry=doGsaREgReaT',
145+
thirdparty: 'maisey=silly,charlie=goofy',
146+
},
147+
},
148+
}),
149+
);
150+
});
151+
123152
test('capture error', async () => {
124153
expect.assertions(5);
125154

packages/tracing/src/browser/browsertracing.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getGlobalObject, logger } from '@sentry/utils';
55
import { startIdleTransaction } from '../hubextensions';
66
import { DEFAULT_IDLE_TIMEOUT, IdleTransaction } from '../idletransaction';
77
import { SpanStatus } from '../spanstatus';
8-
import { extractTraceparentData, secToMs } from '../utils';
8+
import { extractSentrytraceData, extractTracestateData, secToMs } from '../utils';
99
import { registerBackgroundTabDetection } from './backgroundtab';
1010
import { MetricsInstrumentation } from './metrics';
1111
import {
@@ -191,7 +191,7 @@ export class BrowserTracing implements Integration {
191191
// eslint-disable-next-line @typescript-eslint/unbound-method
192192
const { beforeNavigate, idleTimeout, maxTransactionDuration } = this.options;
193193

194-
const parentContextFromHeader = context.op === 'pageload' ? getHeaderContext() : undefined;
194+
const parentContextFromHeader = context.op === 'pageload' ? extractTraceDataFromMetaTags() : undefined;
195195

196196
const expandedContext = {
197197
...context,
@@ -230,14 +230,22 @@ export class BrowserTracing implements Integration {
230230
}
231231

232232
/**
233-
* Gets transaction context from a sentry-trace meta.
233+
* Gets transaction context data from `sentry-trace` and `tracestate` <meta> tags.
234234
*
235-
* @returns Transaction context data from the header or undefined if there's no header or the header is malformed
235+
* @returns Transaction context data or undefined neither tag exists or has valid data
236236
*/
237-
export function getHeaderContext(): Partial<TransactionContext> | undefined {
238-
const header = getMetaContent('sentry-trace');
239-
if (header) {
240-
return extractTraceparentData(header);
237+
export function extractTraceDataFromMetaTags(): Partial<TransactionContext> | undefined {
238+
const sentrytraceValue = getMetaContent('sentry-trace');
239+
const tracestateValue = getMetaContent('tracestate');
240+
241+
const sentrytraceData = sentrytraceValue ? extractSentrytraceData(sentrytraceValue) : undefined;
242+
const tracestateData = tracestateValue ? extractTracestateData(tracestateValue) : undefined;
243+
244+
if (sentrytraceData || tracestateData?.sentry || tracestateData?.thirdparty) {
245+
return {
246+
...sentrytraceData,
247+
...(tracestateData && { metadata: { tracestate: tracestateData } }),
248+
};
241249
}
242250

243251
return undefined;

packages/tracing/src/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ addExtensionMethods();
2222
export { addExtensionMethods };
2323

2424
export {
25-
extractTraceparentData,
25+
extractSentrytraceData,
26+
extractTracestateData,
2627
getActiveTransaction,
2728
hasTracingEnabled,
29+
SENTRY_TRACE_REGEX,
2830
stripUrlQueryAndFragment,
29-
TRACEPARENT_REGEXP,
3031
} from './utils';

0 commit comments

Comments
 (0)