Skip to content

Commit 1446996

Browse files
committed
ref(tracing): Check mutability state of incoming baggage headers
1 parent 8e78e7c commit 1446996

File tree

8 files changed

+81
-33
lines changed

8 files changed

+81
-33
lines changed

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
fill,
1414
isString,
1515
logger,
16-
parseBaggageString,
16+
parseAndFreezeBaggageIfNecessary,
1717
stripUrlQueryAndFragment,
1818
} from '@sentry/utils';
1919
import * as domain from 'domain';
@@ -259,8 +259,8 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
259259
IS_DEBUG_BUILD && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`);
260260
}
261261

262-
const baggage =
263-
nextReq.headers && isString(nextReq.headers.baggage) && parseBaggageString(nextReq.headers.baggage);
262+
const rawBaggageString = nextReq.headers && isString(nextReq.headers.baggage) && nextReq.headers.baggage;
263+
const baggage = parseAndFreezeBaggageIfNecessary(rawBaggageString, traceparentData);
264264

265265
// pull off query string, if any
266266
const reqPath = stripUrlQueryAndFragment(nextReq.url);
@@ -273,9 +273,8 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
273273
{
274274
name: `${namePrefix}${reqPath}`,
275275
op: 'http.server',
276-
metadata: { requestPath: reqPath },
276+
metadata: { requestPath: reqPath, baggage },
277277
...traceparentData,
278-
...(baggage && { metadata: { baggage: baggage } }),
279278
},
280279
// Extra context passed to the `tracesSampler` (Note: We're combining `nextReq` and `req` this way in order
281280
// to not break people's `tracesSampler` functions, even though the format of `nextReq` has changed (see

packages/nextjs/src/utils/withSentry.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isString,
77
logger,
88
objectify,
9+
parseAndFreezeBaggageIfNecessary,
910
parseBaggageString,
1011
stripUrlQueryAndFragment,
1112
} from '@sentry/utils';
@@ -55,7 +56,8 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
5556
IS_DEBUG_BUILD && logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`);
5657
}
5758

58-
const baggage = req.headers && isString(req.headers.baggage) && parseBaggageString(req.headers.baggage);
59+
const rawBaggageString = req.headers && isString(req.headers.baggage) && req.headers.baggage;
60+
const baggage = parseAndFreezeBaggageIfNecessary(rawBaggageString, traceparentData);
5961

6062
const url = `${req.url}`;
6163
// pull off query string, if any
@@ -75,7 +77,7 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
7577
name: `${reqMethod}${reqPath}`,
7678
op: 'http.server',
7779
...traceparentData,
78-
...(baggage && { metadata: { baggage: baggage } }),
80+
metadata: { baggage: baggage },
7981
},
8082
// extra context passed to the `tracesSampler`
8183
{ request: req },

packages/node/src/handlers.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
isString,
99
logger,
1010
normalize,
11-
parseBaggageString,
11+
parseAndFreezeBaggageIfNecessary,
1212
stripUrlQueryAndFragment,
1313
} from '@sentry/utils';
1414
import * as cookie from 'cookie';
@@ -64,14 +64,16 @@ export function tracingHandler(): (
6464
// If there is a trace header set, we extract the data from it (parentSpanId, traceId, and sampling decision)
6565
const traceparentData =
6666
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);
67+
const rawBaggageString = req.headers && isString(req.headers.baggage) && req.headers.baggage;
68+
69+
const baggage = parseAndFreezeBaggageIfNecessary(rawBaggageString, traceparentData);
6870

6971
const transaction = startTransaction(
7072
{
7173
name: extractExpressTransactionName(req, { path: true, method: true }),
7274
op: 'http.server',
7375
...traceparentData,
74-
...(baggage && { metadata: { baggage: baggage } }),
76+
metadata: { baggage: baggage },
7577
},
7678
// extra context passed to the tracesSampler
7779
{ request: extractRequestData(req) },

packages/serverless/src/awslambda.ts

Lines changed: 5 additions & 6 deletions
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, parseBaggageString } from '@sentry/utils';
14+
import { extensionRelayDSN, isString, logger, parseAndFreezeBaggageIfNecessary } 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';
@@ -289,16 +289,15 @@ export function wrapHandler<TEvent, TResult>(
289289
traceparentData = extractTraceparentData(eventWithHeaders.headers['sentry-trace']);
290290
}
291291

292-
const baggage =
293-
eventWithHeaders.headers &&
294-
isString(eventWithHeaders.headers.baggage) &&
295-
parseBaggageString(eventWithHeaders.headers.baggage);
292+
const rawBaggageString =
293+
eventWithHeaders.headers && isString(eventWithHeaders.headers.baggage) && eventWithHeaders.headers.baggege;
294+
const baggage = parseAndFreezeBaggageIfNecessary(rawBaggageString, traceparentData);
296295

297296
const transaction = startTransaction({
298297
name: context.functionName,
299298
op: 'awslambda.handler',
300299
...traceparentData,
301-
...(baggage && { metadata: { baggage: baggage } }),
300+
metadata: { baggage: baggage },
302301
});
303302

304303
const hub = getCurrentHub();

packages/serverless/src/gcpfunction/http.ts

Lines changed: 6 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';
22
import { extractTraceparentData } from '@sentry/tracing';
3-
import { isString, logger, parseBaggageString, stripUrlQueryAndFragment } from '@sentry/utils';
3+
import { isString, logger, parseAndFreezeBaggageIfNecessary, stripUrlQueryAndFragment } from '@sentry/utils';
44

55
import { IS_DEBUG_BUILD } from '../flags';
66
import { domainify, getActiveDomain, proxyFunction } from './../utils';
@@ -57,16 +57,16 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial<HttpFunctionWr
5757
traceparentData = extractTraceparentData(reqWithHeaders.headers['sentry-trace']);
5858
}
5959

60-
const baggage =
61-
reqWithHeaders.headers &&
62-
isString(reqWithHeaders.headers.baggage) &&
63-
parseBaggageString(reqWithHeaders.headers.baggage);
60+
const rawBaggageString =
61+
reqWithHeaders.headers && isString(reqWithHeaders.headers.baggage) && reqWithHeaders.headers.baggage;
62+
63+
const baggage = parseAndFreezeBaggageIfNecessary(rawBaggageString, traceparentData);
6464

6565
const transaction = startTransaction({
6666
name: `${reqMethod} ${reqUrl}`,
6767
op: 'gcp.function.http',
6868
...traceparentData,
69-
...(baggage && { metadata: { baggage: baggage } }),
69+
metadata: { baggage: baggage },
7070
});
7171

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

packages/tracing/src/browser/browsertracing.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Hub } from '@sentry/hub';
22
import { EventProcessor, Integration, Transaction, TransactionContext } from '@sentry/types';
3-
import { getGlobalObject, logger, parseBaggageString } from '@sentry/utils';
3+
import { getGlobalObject, logger, parseAndFreezeBaggageIfNecessary, parseBaggageString } from '@sentry/utils';
44

55
import { IS_DEBUG_BUILD } from '../flags';
66
import { startIdleTransaction } from '../hubextensions';
@@ -255,7 +255,7 @@ export function extractTraceDataFromMetaTags(): Partial<TransactionContext> | un
255255
const baggageValue = getMetaContent('baggage');
256256

257257
const sentrytraceData = sentrytraceValue ? extractTraceparentData(sentrytraceValue) : undefined;
258-
const baggage = baggageValue ? parseBaggageString(baggageValue) : undefined;
258+
const baggage = parseAndFreezeBaggageIfNecessary(baggageValue, sentrytraceValue);
259259

260260
// TODO more extensive checks for baggage validity/emptyness?
261261
if (sentrytraceData || baggage) {

packages/types/src/baggage.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ export type BaggageObj = Partial<Record<AllowedBaggageKeys, string> & Record<str
88
* It is expected that users interact with baggage using the helpers methods:
99
* `createBaggage`, `getBaggageValue`, and `setBaggageValue`.
1010
*
11-
* Internally, the baggage data structure is a tuple of length 2, separating baggage values
11+
* Internally, the baggage data structure is a tuple of length 3, separating baggage values
1212
* based on if they are related to Sentry or not. If the baggage values are
1313
* set/used by sentry, they will be stored in an object to be easily accessed.
1414
* If they are not, they are kept as a string to be only accessed when serialized
1515
* at baggage propagation time.
16+
* The third tuple member controls the mutability of the baggage. If it is `true`,
17+
* the baggage can not be modified any longer (i.e. is immutable).
1618
*/
17-
export type Baggage = [BaggageObj, string];
19+
export type Baggage = [BaggageObj, string, boolean];

packages/utils/src/baggage.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Baggage, BaggageObj } from '@sentry/types';
1+
import { Baggage, BaggageObj, TraceparentData } from '@sentry/types';
22

33
import { IS_DEBUG_BUILD } from './flags';
44
import { logger } from './logger';
@@ -17,8 +17,8 @@ export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = /^sentry-/;
1717
export const MAX_BAGGAGE_STRING_LENGTH = 8192;
1818

1919
/** Create an instance of Baggage */
20-
export function createBaggage(initItems: BaggageObj, baggageString: string = ''): Baggage {
21-
return [{ ...initItems }, baggageString];
20+
export function createBaggage(initItems: BaggageObj, baggageString: string = '', frozen: boolean = false): Baggage {
21+
return [{ ...initItems }, baggageString, frozen];
2222
}
2323

2424
/** Get a value from baggage */
@@ -28,7 +28,9 @@ export function getBaggageValue(baggage: Baggage, key: keyof BaggageObj): Baggag
2828

2929
/** Add a value to baggage */
3030
export function setBaggageValue(baggage: Baggage, key: keyof BaggageObj, value: BaggageObj[keyof BaggageObj]): void {
31-
baggage[0][key] = value;
31+
if (!isBaggageFrozen(baggage)) {
32+
baggage[0][key] = value;
33+
}
3234
}
3335

3436
/** Check if the Sentry part of the passed baggage (i.e. the first element in the tuple) is empty */
@@ -55,6 +57,23 @@ export function getThirdPartyBaggage(baggage: Baggage): string {
5557
return baggage[1];
5658
}
5759

60+
/**
61+
* Checks if baggage is frozen (i.e. immutable)
62+
* @param baggage
63+
* @returns true if baggage is frozen, else false
64+
*/
65+
export function isBaggageFrozen(baggage: Baggage): boolean {
66+
return baggage[2];
67+
}
68+
69+
/**
70+
* Freezes baggage (i.e. makes it immutable)
71+
* @param baggage
72+
*/
73+
export function freezeBaggage(baggage: Baggage): void {
74+
baggage[2] = true;
75+
}
76+
5877
/** Serialize a baggage object */
5978
export function serializeBaggage(baggage: Baggage): string {
6079
return Object.keys(baggage[0]).reduce((prev, key: keyof BaggageObj) => {
@@ -71,7 +90,7 @@ export function serializeBaggage(baggage: Baggage): string {
7190
}, baggage[1]);
7291
}
7392

74-
/** Parse a baggage header to a string */
93+
/** Parse a baggage header from a string and return a Baggage object */
7594
export function parseBaggageString(inputBaggageString: string): Baggage {
7695
return inputBaggageString.split(',').reduce(
7796
([baggageObj, baggageString], curr) => {
@@ -84,12 +103,13 @@ export function parseBaggageString(inputBaggageString: string): Baggage {
84103
[baggageKey]: decodeURIComponent(val),
85104
},
86105
baggageString,
106+
false,
87107
];
88108
} else {
89-
return [baggageObj, baggageString === '' ? curr : `${baggageString},${curr}`];
109+
return [baggageObj, baggageString === '' ? curr : `${baggageString},${curr}`, false];
90110
}
91111
},
92-
[{}, ''],
112+
[{}, '', false],
93113
);
94114
}
95115

@@ -121,3 +141,27 @@ export function mergeAndSerializeBaggage(incomingBaggage?: Baggage, headerBaggag
121141
);
122142
return serializeBaggage(finalBaggage);
123143
}
144+
145+
/**
146+
* Helper function that takes a raw baggage string (if available) and the processed sentry-trace header
147+
* data (if available), parses the baggage string and creates a Baggage object
148+
* If there is no baggage string, it will create an empty Baggage object.
149+
* In a second step, this functions determines when the created Baggage object should be frozen to prevent mutation
150+
* of the Sentry data.
151+
*
152+
* Extracted this logic to a function because it's duplicated in a lot of places.
153+
*
154+
* @param rawBaggageString
155+
* @param traceparentData
156+
*/
157+
// parseBaggagePlusMutability
158+
export function parseAndFreezeBaggageIfNecessary(
159+
rawBaggageString: string | false | undefined | null,
160+
traceparentData: TraceparentData | string | false | undefined | null,
161+
): Baggage {
162+
const baggage = parseBaggageString(rawBaggageString || '');
163+
if (!isSentryBaggageEmpty(baggage) || (traceparentData && isSentryBaggageEmpty(baggage))) {
164+
freezeBaggage(baggage);
165+
}
166+
return baggage;
167+
}

0 commit comments

Comments
 (0)