Skip to content

Commit d48711f

Browse files
Lms24lforst
andauthored
feat(tracing): Add empty baggage header propagation to outgoing requests (#5133)
Adds the propagation of an "empty" baggage header. The word "empty" is however kind of misleading as the header is not necessarily empty. In order to comply with the baggage spec, as of this patch, we propagate incoming (3rd party) baggage to outgoing requests. The important part is that we actually add the `baggage` HTTP header to outgoing requests which is a breaking change in terms of CORS rules having to be adjusted. We don't yet add `sentry-` baggage entries to the propagated baggage. This will come in a follow up PR which does not necessarily have to be part of the initial v7 release as it is no longer a breaking change. Overall, this is heavily inspired from #3945 (thanks @lobsterkatie for doing the hard work) More specifically, this PR does the following things: 1. Extract incoming baggage headers and store them in the created transaction's metadata. Incoming baggage data is intercepted at: * Node SDK: TracingHandler * Serverless SDK: AWS wrapHandler * Serverless SDK: GCP wrapHttpFunction * Next.js: SDK makeWrappedReqHandler * Next.js: SDK withSentry * BrowserTracing Integration: by parsing the `<meta>` tags (analogously to the `sentry-trace` header) 2. Add the extracted baggage data to outgoing requests we instrument at: * Node SDK: HTTP integration * Tracing: instrumented Fetch and XHR callbacks Co-authored-by: Luca Forstner <[email protected]>
1 parent 9759e27 commit d48711f

File tree

26 files changed

+507
-59
lines changed

26 files changed

+507
-59
lines changed

MIGRATION.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Below we will outline all the breaking changes you should consider when upgradin
1010
- We bumped the TypeScript version we generate our types with to 3.8.3. Please check if your TypeScript projects using TypeScript version 3.7 or lower still compile. Otherwise, upgrade your TypeScript version.
1111
- `whitelistUrls` and `blacklistUrls` have been renamed to `allowUrls` and `denyUrls` in the `Sentry.init()` options.
1212
- The `UserAgent` integration is now called `HttpContext`.
13+
- If you are using Performance Monitoring and with tracing enabled, you might have to [make adjustments to
14+
your server's CORS settings](#-propagation-of-baggage-header)
1315

1416
## Dropping Support for Node.js v6
1517

@@ -319,6 +321,13 @@ session.update({ environment: 'prod' });
319321
session.close('ok');
320322
```
321323

324+
## Propagation of Baggage Header
325+
326+
We introduced a new way of propagating tracing and transaction-related information between services. This
327+
change adds the [`baggage` HTTP header](https://www.w3.org/TR/baggage/) to outgoing requests if the instrumentation of requests is enabled. Since this adds a header to your HTTP requests, you might need
328+
to adjust your Server's CORS settings to allow this additional header. Take a look at the [Sentry docs](https://docs.sentry.io/platforms/javascript/performance/connect-services/#navigation-and-other-xhr-requests)
329+
for more in-depth instructions what to change.
330+
322331
## General API Changes
323332

324333
For our efforts to reduce bundle size of the SDK we had to remove and refactor parts of the package which introduced a few changes to the API:

packages/core/test/lib/envelope.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { DsnComponents, Event } from '@sentry/types';
2-
import { EventTraceContext } from '@sentry/types/build/types/envelope';
1+
import { DsnComponents, Event, EventTraceContext } from '@sentry/types';
32

43
import { createEventEnvelope } from '../../src/envelope';
54

packages/integration-tests/suites/tracing/browsertracing/meta/template.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
<head>
33
<meta charset="utf-8" />
44
<meta name="sentry-trace" content="12312012123120121231201212312012-1121201211212012-1" />
5+
<meta name="baggage" content="sentry-version=2.1.12" />
56
</head>
67
</html>

packages/integration-tests/suites/tracing/browsertracing/meta/test.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { expect } from '@playwright/test';
2-
import { Event } from '@sentry/types';
2+
import { Event, EventEnvelopeHeaders } from '@sentry/types';
33

44
import { sentryTest } from '../../../../utils/fixtures';
5-
import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
5+
import { envelopeHeaderRequestParser, getFirstSentryEnvelopeRequest } from '../../../../utils/helpers';
66

77
sentryTest(
88
'should create a pageload transaction based on `sentry-trace` <meta>',
@@ -21,6 +21,20 @@ sentryTest(
2121
},
2222
);
2323

24+
// TODO this we can't really test until we actually propagate sentry- entries in baggage
25+
// skipping for now but this must be adjusted later on
26+
sentryTest.skip(
27+
'should pick up `baggage` <meta> tag and propagate the content in transaction',
28+
async ({ getLocalTestPath, page }) => {
29+
const url = await getLocalTestPath({ testDir: __dirname });
30+
31+
const envHeader = await getFirstSentryEnvelopeRequest<EventEnvelopeHeaders>(page, url, envelopeHeaderRequestParser);
32+
33+
expect(envHeader.trace).toBeDefined();
34+
expect(envHeader.trace).toEqual('{version:2.1.12}');
35+
},
36+
);
37+
2438
sentryTest(
2539
"should create a navigation that's not influenced by `sentry-trace` <meta>",
2640
async ({ getLocalTestPath, page }) => {

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@ import {
88
startTransaction,
99
} from '@sentry/node';
1010
import { extractTraceparentData, getActiveTransaction, hasTracingEnabled } from '@sentry/tracing';
11-
import { addExceptionMechanism, fill, isString, logger, stripUrlQueryAndFragment } from '@sentry/utils';
11+
import {
12+
addExceptionMechanism,
13+
fill,
14+
isString,
15+
logger,
16+
parseBaggageString,
17+
stripUrlQueryAndFragment,
18+
} from '@sentry/utils';
1219
import * as domain from 'domain';
1320
import * as http from 'http';
1421
import { default as createNextServer } from 'next';
@@ -252,6 +259,9 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
252259
IS_DEBUG_BUILD && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`);
253260
}
254261

262+
const baggage =
263+
nextReq.headers && isString(nextReq.headers.baggage) && parseBaggageString(nextReq.headers.baggage);
264+
255265
// pull off query string, if any
256266
const reqPath = stripUrlQueryAndFragment(nextReq.url);
257267

@@ -265,6 +275,7 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
265275
op: 'http.server',
266276
metadata: { requestPath: reqPath },
267277
...traceparentData,
278+
...(baggage && { metadata: { baggage: baggage } }),
268279
},
269280
// Extra context passed to the `tracesSampler` (Note: We're combining `nextReq` and `req` this way in order
270281
// to not break people's `tracesSampler` functions, even though the format of `nextReq` has changed (see

packages/nextjs/src/utils/withSentry.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { captureException, flush, getCurrentHub, Handlers, startTransaction } from '@sentry/node';
22
import { extractTraceparentData, hasTracingEnabled } from '@sentry/tracing';
33
import { Transaction } from '@sentry/types';
4-
import { addExceptionMechanism, isString, logger, objectify, stripUrlQueryAndFragment } from '@sentry/utils';
4+
import {
5+
addExceptionMechanism,
6+
isString,
7+
logger,
8+
objectify,
9+
parseBaggageString,
10+
stripUrlQueryAndFragment,
11+
} from '@sentry/utils';
512
import * as domain from 'domain';
613
import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next';
714

@@ -48,6 +55,8 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
4855
IS_DEBUG_BUILD && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`);
4956
}
5057

58+
const baggage = req.headers && isString(req.headers.baggage) && parseBaggageString(req.headers.baggage);
59+
5160
const url = `${req.url}`;
5261
// pull off query string, if any
5362
let reqPath = stripUrlQueryAndFragment(url);
@@ -66,6 +75,7 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
6675
name: `${reqMethod}${reqPath}`,
6776
op: 'http.server',
6877
...traceparentData,
78+
...(baggage && { metadata: { baggage: baggage } }),
6979
},
7080
// extra context passed to the `tracesSampler`
7181
{ request: req },
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import * as path from 'path';
2+
3+
import { getAPIResponse, runServer } from '../../../../utils/index';
4+
import { TestAPIResponse } from '../server';
5+
6+
test('Should assign `baggage` header which contains 3rd party trace baggage data of an outgoing request.', async () => {
7+
const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`);
8+
9+
const response = (await getAPIResponse(new URL(`${url}/express`), {
10+
baggage: 'foo=bar,bar=baz',
11+
})) as TestAPIResponse;
12+
13+
expect(response).toBeDefined();
14+
expect(response).toMatchObject({
15+
test_data: {
16+
host: 'somewhere.not.sentry',
17+
baggage: expect.stringContaining('foo=bar,bar=baz'),
18+
},
19+
});
20+
});
21+
22+
test('Should assign `baggage` header which contains sentry trace baggage data of an outgoing request.', async () => {
23+
const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`);
24+
25+
const response = (await getAPIResponse(new URL(`${url}/express`), {
26+
baggage: 'sentry-version=1.0.0,sentry-environment=production',
27+
})) as TestAPIResponse;
28+
29+
expect(response).toBeDefined();
30+
expect(response).toMatchObject({
31+
test_data: {
32+
host: 'somewhere.not.sentry',
33+
baggage: expect.stringContaining('sentry-version=1.0.0,sentry-environment=production'),
34+
},
35+
});
36+
});
37+
38+
test('Should assign `baggage` header which contains sentry and 3rd party trace baggage data of an outgoing request.', async () => {
39+
const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`);
40+
41+
const response = (await getAPIResponse(new URL(`${url}/express`), {
42+
baggage: 'sentry-version=1.0.0,sentry-environment=production,dogs=great',
43+
})) as TestAPIResponse;
44+
45+
expect(response).toBeDefined();
46+
expect(response).toMatchObject({
47+
test_data: {
48+
host: 'somewhere.not.sentry',
49+
baggage: expect.stringContaining('dogs=great,sentry-version=1.0.0,sentry-environment=production'),
50+
},
51+
});
52+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as path from 'path';
2+
3+
import { getAPIResponse, runServer } from '../../../../utils/index';
4+
import { TestAPIResponse } from '../server';
5+
6+
test('should attach a `baggage` header to an outgoing request.', async () => {
7+
const url = await runServer(__dirname, `${path.resolve(__dirname, '..')}/server.ts`);
8+
9+
const response = (await getAPIResponse(new URL(`${url}/express`))) as TestAPIResponse;
10+
11+
expect(response).toBeDefined();
12+
expect(response).toMatchObject({
13+
test_data: {
14+
host: 'somewhere.not.sentry',
15+
// TODO this is currently still empty but eventually it should contain sentry data
16+
baggage: expect.stringMatching(''),
17+
},
18+
});
19+
});

packages/node-integration-tests/suites/express/sentry-trace/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import http from 'http';
66

77
const app = express();
88

9-
export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string } };
9+
export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } };
1010

1111
Sentry.init({
1212
dsn: 'https://[email protected]/1337',

packages/node/src/handlers.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isString,
99
logger,
1010
normalize,
11+
parseBaggageString,
1112
stripUrlQueryAndFragment,
1213
} from '@sentry/utils';
1314
import * as cookie from 'cookie';
@@ -61,16 +62,16 @@ export function tracingHandler(): (
6162
next: (error?: any) => void,
6263
): void {
6364
// If there is a trace header set, we extract the data from it (parentSpanId, traceId, and sampling decision)
64-
let traceparentData;
65-
if (req.headers && isString(req.headers['sentry-trace'])) {
66-
traceparentData = extractTraceparentData(req.headers['sentry-trace']);
67-
}
65+
const traceparentData =
66+
req.headers && isString(req.headers['sentry-trace']) && extractTraceparentData(req.headers['sentry-trace']);
67+
const baggage = req.headers && isString(req.headers.baggage) && parseBaggageString(req.headers.baggage);
6868

6969
const transaction = startTransaction(
7070
{
7171
name: extractExpressTransactionName(req, { path: true, method: true }),
7272
op: 'http.server',
7373
...traceparentData,
74+
...(baggage && { metadata: { baggage: baggage } }),
7475
},
7576
// extra context passed to the tracesSampler
7677
{ request: extractRequestData(req) },

packages/node/src/integrations/http.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getCurrentHub } from '@sentry/core';
22
import { Integration, Span } from '@sentry/types';
3-
import { fill, logger, parseSemver } from '@sentry/utils';
3+
import { fill, logger, mergeAndSerializeBaggage, parseSemver } from '@sentry/utils';
44
import * as http from 'http';
55
import * as https from 'https';
66

@@ -123,7 +123,14 @@ function _createWrappedRequestMethodFactory(
123123
logger.log(
124124
`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to ${requestUrl}: `,
125125
);
126-
requestOptions.headers = { ...requestOptions.headers, 'sentry-trace': sentryTraceHeader };
126+
127+
const headerBaggageString = requestOptions.headers && (requestOptions.headers.baggage as string);
128+
129+
requestOptions.headers = {
130+
...requestOptions.headers,
131+
'sentry-trace': sentryTraceHeader,
132+
baggage: mergeAndSerializeBaggage(span.getBaggage(), headerBaggageString),
133+
};
127134
}
128135
}
129136

packages/node/test/handlers.test.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as sentryCore from '@sentry/core';
22
import * as sentryHub from '@sentry/hub';
33
import { Hub } from '@sentry/hub';
44
import { Transaction } from '@sentry/tracing';
5-
import { Runtime } from '@sentry/types';
5+
import { Baggage, Runtime } from '@sentry/types';
66
import { SentryError } from '@sentry/utils';
77
import * as http from 'http';
88
import * as net from 'net';
@@ -368,6 +368,41 @@ describe('tracingHandler', () => {
368368
expect(transaction.traceId).toEqual('12312012123120121231201212312012');
369369
expect(transaction.parentSpanId).toEqual('1121201211212012');
370370
expect(transaction.sampled).toEqual(false);
371+
expect(transaction.metadata?.baggage).toBeUndefined();
372+
});
373+
374+
it("pulls parent's data from tracing and baggage headers on the request", () => {
375+
req.headers = {
376+
'sentry-trace': '12312012123120121231201212312012-1121201211212012-0',
377+
baggage: 'sentry-version=1.0,sentry-environment=production',
378+
};
379+
380+
sentryTracingMiddleware(req, res, next);
381+
382+
const transaction = (res as any).__sentry_transaction;
383+
384+
// since we have no tracesSampler defined, the default behavior (inherit if possible) applies
385+
expect(transaction.traceId).toEqual('12312012123120121231201212312012');
386+
expect(transaction.parentSpanId).toEqual('1121201211212012');
387+
expect(transaction.sampled).toEqual(false);
388+
expect(transaction.metadata?.baggage).toBeDefined();
389+
expect(transaction.metadata?.baggage).toEqual([{ version: '1.0', environment: 'production' }, ''] as Baggage);
390+
});
391+
392+
it("pulls parent's baggage (sentry + third party entries) headers on the request", () => {
393+
req.headers = {
394+
baggage: 'sentry-version=1.0,sentry-environment=production,dogs=great,cats=boring',
395+
};
396+
397+
sentryTracingMiddleware(req, res, next);
398+
399+
const transaction = (res as any).__sentry_transaction;
400+
401+
expect(transaction.metadata?.baggage).toBeDefined();
402+
expect(transaction.metadata?.baggage).toEqual([
403+
{ version: '1.0', environment: 'production' },
404+
'dogs=great,cats=boring',
405+
] as Baggage);
371406
});
372407

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

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,43 @@ describe('tracing', () => {
8686

8787
expect(sentryTraceHeader).not.toBeDefined();
8888
});
89+
90+
it('attaches the baggage header to outgoing non-sentry requests', async () => {
91+
nock('http://dogs.are.great').get('/').reply(200);
92+
93+
createTransactionOnScope();
94+
95+
const request = http.get('http://dogs.are.great/');
96+
const baggageHeader = request.getHeader('baggage') as string;
97+
98+
expect(baggageHeader).toBeDefined();
99+
// this might change once we actually add our baggage data to the header
100+
expect(baggageHeader).toEqual('');
101+
});
102+
103+
it('propagates 3rd party baggage header data to outgoing non-sentry requests', async () => {
104+
nock('http://dogs.are.great').get('/').reply(200);
105+
106+
createTransactionOnScope();
107+
108+
const request = http.get({ host: 'http://dogs.are.great/', headers: { baggage: 'dog=great' } });
109+
const baggageHeader = request.getHeader('baggage') as string;
110+
111+
expect(baggageHeader).toBeDefined();
112+
// this might change once we actually add our baggage data to the header
113+
expect(baggageHeader).toEqual('dog=great');
114+
});
115+
116+
it("doesn't attach the sentry-trace header to outgoing sentry requests", () => {
117+
nock('http://squirrelchasers.ingest.sentry.io').get('/api/12312012/store/').reply(200);
118+
119+
createTransactionOnScope();
120+
121+
const request = http.get('http://squirrelchasers.ingest.sentry.io/api/12312012/store/');
122+
const baggage = request.getHeader('baggage');
123+
124+
expect(baggage).not.toBeDefined();
125+
});
89126
});
90127

91128
describe('default protocols', () => {

packages/serverless/src/awslambda.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@sentry/node';
1212
import { extractTraceparentData } from '@sentry/tracing';
1313
import { Integration } from '@sentry/types';
14-
import { extensionRelayDSN, isString, logger } from '@sentry/utils';
14+
import { extensionRelayDSN, isString, logger, parseBaggageString } 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';
@@ -288,10 +288,17 @@ export function wrapHandler<TEvent, TResult>(
288288
if (eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])) {
289289
traceparentData = extractTraceparentData(eventWithHeaders.headers['sentry-trace']);
290290
}
291+
292+
const baggage =
293+
eventWithHeaders.headers &&
294+
isString(eventWithHeaders.headers.baggage) &&
295+
parseBaggageString(eventWithHeaders.headers.baggage);
296+
291297
const transaction = startTransaction({
292298
name: context.functionName,
293299
op: 'awslambda.handler',
294300
...traceparentData,
301+
...(baggage && { metadata: { baggage: baggage } }),
295302
});
296303

297304
const hub = getCurrentHub();

0 commit comments

Comments
 (0)