Skip to content

Commit 0a6b4f2

Browse files
authored
feat(utils): Introduce Baggage API (#5066)
This patch introduced a basic data structure for baggage, as well as controls to create, manipulate and serialize the baggage data structure. The baggage data structure represents key,value pairs based on the baggage spec: https://www.w3.org/TR/baggage It is expected that users interact with baggage using the helpers methods: `createBaggage`, `getBaggageValue`, and `setBaggageValue`. Internally, the baggage data structure is a tuple of length 2, separating baggage values based on if they are related to Sentry or not. If the baggage values are set/used by sentry, they will be stored in an object to be easily accessed. If they are not, they are kept as a string to be only accessed when serialized at baggage propagation time. As a next step, let's add the baggage values to the envelope header so they can be processed and used by relay - this will allow us to do some early validations of our assumptions.
1 parent 33e48ca commit 0a6b4f2

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

packages/utils/src/baggage.ts

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { IS_DEBUG_BUILD } from './flags';
2+
import { logger } from './logger';
3+
4+
export type AllowedBaggageKeys = 'environment' | 'release'; // TODO: Add remaining allowed baggage keys | 'transaction' | 'userid' | 'usersegment';
5+
export type BaggageObj = Partial<Record<AllowedBaggageKeys, string> & Record<string, string>>;
6+
7+
/**
8+
* The baggage data structure represents key,value pairs based on the baggage
9+
* spec: https://www.w3.org/TR/baggage
10+
*
11+
* It is expected that users interact with baggage using the helpers methods:
12+
* `createBaggage`, `getBaggageValue`, and `setBaggageValue`.
13+
*
14+
* Internally, the baggage data structure is a tuple of length 2, separating baggage values
15+
* based on if they are related to Sentry or not. If the baggage values are
16+
* set/used by sentry, they will be stored in an object to be easily accessed.
17+
* If they are not, they are kept as a string to be only accessed when serialized
18+
* at baggage propagation time.
19+
*/
20+
export type Baggage = [BaggageObj, string];
21+
22+
export const BAGGAGE_HEADER_NAME = 'baggage';
23+
24+
export const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-';
25+
26+
export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = /^sentry-/;
27+
28+
/**
29+
* Max length of a serialized baggage string
30+
*
31+
* https://www.w3.org/TR/baggage/#limits
32+
*/
33+
export const MAX_BAGGAGE_STRING_LENGTH = 8192;
34+
35+
/** Create an instance of Baggage */
36+
export function createBaggage(initItems: BaggageObj, baggageString: string = ''): Baggage {
37+
return [{ ...initItems }, baggageString];
38+
}
39+
40+
/** Get a value from baggage */
41+
export function getBaggageValue(baggage: Baggage, key: keyof BaggageObj): BaggageObj[keyof BaggageObj] {
42+
return baggage[0][key];
43+
}
44+
45+
/** Add a value to baggage */
46+
export function setBaggageValue(baggage: Baggage, key: keyof BaggageObj, value: BaggageObj[keyof BaggageObj]): void {
47+
baggage[0][key] = value;
48+
}
49+
50+
/** Serialize a baggage object */
51+
export function serializeBaggage(baggage: Baggage): string {
52+
return Object.keys(baggage[0]).reduce((prev, key: keyof BaggageObj) => {
53+
const val = baggage[0][key] as string;
54+
const baggageEntry = `${SENTRY_BAGGAGE_KEY_PREFIX}${encodeURIComponent(key)}=${encodeURIComponent(val)}`;
55+
const newVal = prev === '' ? baggageEntry : `${prev},${baggageEntry}`;
56+
if (newVal.length > MAX_BAGGAGE_STRING_LENGTH) {
57+
IS_DEBUG_BUILD &&
58+
logger.warn(`Not adding key: ${key} with val: ${val} to baggage due to exceeding baggage size limits.`);
59+
return prev;
60+
} else {
61+
return newVal;
62+
}
63+
}, baggage[1]);
64+
}
65+
66+
/** Parse a baggage header to a string */
67+
export function parseBaggageString(inputBaggageString: string): Baggage {
68+
return inputBaggageString.split(',').reduce(
69+
([baggageObj, baggageString], curr) => {
70+
const [key, val] = curr.split('=');
71+
if (SENTRY_BAGGAGE_KEY_PREFIX_REGEX.test(key)) {
72+
const baggageKey = decodeURIComponent(key.split('-')[1]);
73+
return [
74+
{
75+
...baggageObj,
76+
[baggageKey]: decodeURIComponent(val),
77+
},
78+
baggageString,
79+
];
80+
} else {
81+
return [baggageObj, baggageString === '' ? curr : `${baggageString},${curr}`];
82+
}
83+
},
84+
[{}, ''],
85+
);
86+
}

packages/utils/test/baggage.test.ts

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { createBaggage, getBaggageValue, parseBaggageString, serializeBaggage, setBaggageValue } from '../src/baggage';
2+
3+
describe('Baggage', () => {
4+
describe('createBaggage', () => {
5+
it.each([
6+
['creates an empty baggage instance', {}, [{}, '']],
7+
[
8+
'creates a baggage instance with initial values',
9+
{ environment: 'production', anyKey: 'anyValue' },
10+
[{ environment: 'production', anyKey: 'anyValue' }, ''],
11+
],
12+
])('%s', (_: string, input, output) => {
13+
expect(createBaggage(input)).toEqual(output);
14+
});
15+
});
16+
17+
describe('getBaggageValue', () => {
18+
it.each([
19+
[
20+
'gets a baggage item',
21+
createBaggage({ environment: 'production', anyKey: 'anyValue' }),
22+
'environment',
23+
'production',
24+
],
25+
['finds undefined items', createBaggage({}), 'environment', undefined],
26+
])('%s', (_: string, baggage, key, value) => {
27+
expect(getBaggageValue(baggage, key)).toEqual(value);
28+
});
29+
});
30+
31+
describe('setBaggageValue', () => {
32+
it.each([
33+
['sets a baggage item', createBaggage({}), 'environment', 'production'],
34+
['overwrites a baggage item', createBaggage({ environment: 'development' }), 'environment', 'production'],
35+
])('%s', (_: string, baggage, key, value) => {
36+
setBaggageValue(baggage, key, value);
37+
expect(getBaggageValue(baggage, key)).toEqual(value);
38+
});
39+
});
40+
41+
describe('serializeBaggage', () => {
42+
it.each([
43+
['serializes empty baggage', createBaggage({}), ''],
44+
[
45+
'serializes baggage with a single value',
46+
createBaggage({ environment: 'production' }),
47+
'sentry-environment=production',
48+
],
49+
[
50+
'serializes baggage with multiple values',
51+
createBaggage({ environment: 'production', release: '10.0.2' }),
52+
'sentry-environment=production,sentry-release=10.0.2',
53+
],
54+
[
55+
'keeps non-sentry prefixed baggage items',
56+
createBaggage(
57+
{ environment: 'production', release: '10.0.2' },
58+
'userId=alice,serverNode=DF%2028,isProduction=false',
59+
),
60+
'userId=alice,serverNode=DF%2028,isProduction=false,sentry-environment=production,sentry-release=10.0.2',
61+
],
62+
[
63+
'can only use non-sentry prefixed baggage items',
64+
createBaggage({}, 'userId=alice,serverNode=DF%2028,isProduction=false'),
65+
'userId=alice,serverNode=DF%2028,isProduction=false',
66+
],
67+
])('%s', (_: string, baggage, serializedBaggage) => {
68+
expect(serializeBaggage(baggage)).toEqual(serializedBaggage);
69+
});
70+
});
71+
72+
describe('parseBaggageString', () => {
73+
it.each([
74+
['parses an empty string', '', createBaggage({})],
75+
[
76+
'parses sentry values into baggage',
77+
'sentry-environment=production,sentry-release=10.0.2',
78+
createBaggage({ environment: 'production', release: '10.0.2' }),
79+
],
80+
[
81+
'parses arbitrary baggage headers',
82+
'userId=alice,serverNode=DF%2028,isProduction=false,sentry-environment=production,sentry-release=10.0.2',
83+
createBaggage(
84+
{ environment: 'production', release: '10.0.2' },
85+
'userId=alice,serverNode=DF%2028,isProduction=false',
86+
),
87+
],
88+
])('%s', (_: string, baggageString, baggage) => {
89+
expect(parseBaggageString(baggageString)).toEqual(baggage);
90+
});
91+
});
92+
});

0 commit comments

Comments
 (0)