Skip to content

Commit ae47cea

Browse files
committed
add method to extract tracestate data from header
1 parent da6fccf commit ae47cea

File tree

3 files changed

+136
-1
lines changed

3 files changed

+136
-1
lines changed

packages/tracing/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export { addExtensionMethods };
2323

2424
export {
2525
extractSentrytraceData,
26+
extractTracestateData,
2627
getActiveTransaction,
2728
hasTracingEnabled,
2829
SENTRY_TRACE_REGEX,

packages/tracing/src/utils.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,36 @@ export const SENTRY_TRACE_REGEX = new RegExp(
1010
'[ \\t]*$', // whitespace
1111
);
1212

13+
// This is a normal base64 regex, modified to reflect that fact that we strip the
14+
// trailing = or == off
15+
const base64Stripped =
16+
// any of the characters in the base64 "alphabet", in multiples of 4
17+
'([a-zA-Z0-9+/]{4})*' +
18+
// either nothing or 2 or 3 base64-alphabet characters (see
19+
// https://en.wikipedia.org/wiki/Base64#Decoding_Base64_without_padding
20+
// forwhy there's never only 1 extra character)
21+
'([a-zA-Z0-9+/]{2,3})?';
22+
23+
// comma-delimited list of entries of the form `xxx=yyy`
24+
const tracestateEntry = '[^=]+=[^=]+';
25+
const TRACESTATE_ENTRIES_REGEX = new RegExp(
26+
// one or more xxxxx=yyyy entries
27+
`^(${tracestateEntry})+` +
28+
// each entry except the last must be followed by a comma
29+
'(,|$)',
30+
);
31+
32+
// this doesn't check that the value is valid, just that there's something there of the form `sentry=xxxx`
33+
const SENTRY_TRACESTATE_ENTRY_REGEX = new RegExp(
34+
// either sentry is the first entry or there's stuff immediately before it,
35+
// ending in a commma (this prevents matching something like `coolsentry=xxx`)
36+
'(?:^|.+,)' +
37+
// sentry's part, not including the potential comma
38+
'(sentry=[^,]*)' +
39+
// either there's a comma and another vendor's entry or we end
40+
'(?:,.+|$)',
41+
);
42+
1343
/**
1444
* Determines if tracing is currently enabled.
1545
*
@@ -28,6 +58,7 @@ export function hasTracingEnabled(options: Options): boolean {
2858
*/
2959
export function extractSentrytraceData(header: string): TraceparentData | undefined {
3060
const matches = header.match(SENTRY_TRACE_REGEX);
61+
3162
if (matches) {
3263
let parentSampled: boolean | undefined;
3364
if (matches[3] === '1') {
@@ -41,9 +72,56 @@ export function extractSentrytraceData(header: string): TraceparentData | undefi
4172
parentSpanId: matches[2],
4273
};
4374
}
75+
4476
return undefined;
4577
}
4678

79+
type TracestateHeaderData = { sentry?: string; thirdparty?: string };
80+
81+
/**
82+
* Extract data from an incoming `tracestate` header
83+
*
84+
* @param header
85+
* @returns Object containing data from the header
86+
*/
87+
export function extractTracestateData(header: string): TracestateHeaderData {
88+
let sentryEntry, thirdPartyEntry;
89+
90+
if (header) {
91+
let before, after;
92+
93+
// find sentry's entry, if any
94+
const sentryMatch = SENTRY_TRACESTATE_ENTRY_REGEX.exec(header);
95+
96+
if (sentryMatch !== null) {
97+
sentryEntry = sentryMatch[1];
98+
99+
// remove the commas after the split so we don't end up with `xxx=yyy,,zzz=qqq` (double commas) when we put them
100+
// back together
101+
[before, after] = header.split(sentryEntry).map(s => s.replace(/^,*|,*$/g, ''));
102+
103+
// extract sentry's value from its entry and test to make sure it's valid; if it isn't, discard the entire entry
104+
// so that a new one will be created by the Transaction contrcutor
105+
const sentryValue = sentryEntry.replace('sentry=', '');
106+
if (!new RegExp(`^${base64Stripped}$`).test(sentryValue)) {
107+
sentryEntry = undefined;
108+
}
109+
} else {
110+
// this could just as well be `before`; we just need to get the thirdparty data into one or the other since
111+
// there's no valid Sentry entry
112+
after = header;
113+
}
114+
115+
// if either thirdparty part is invalid or empty, remove it before gluing them together
116+
const validThirdpartyEntries = [before, after].filter(x => TRACESTATE_ENTRIES_REGEX.test(x || ''));
117+
if (validThirdpartyEntries.length) {
118+
thirdPartyEntry = validThirdpartyEntries.join(',');
119+
}
120+
}
121+
122+
return { sentry: sentryEntry, thirdparty: thirdPartyEntry };
123+
}
124+
47125
/** Grabs active transaction off scope, if any */
48126
export function getActiveTransaction<T extends Transaction>(hub: Hub = getCurrentHub()): T | undefined {
49127
return hub?.getScope()?.getTransaction() as T | undefined;

packages/tracing/test/utils.test.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { extractSentrytraceData } from '../src/utils';
1+
import { extractSentrytraceData, extractTracestateData } from '../src/utils';
22

33
describe('extractSentrytraceData', () => {
44
test('no sample', () => {
@@ -62,3 +62,59 @@ describe('extractSentrytraceData', () => {
6262
expect(extractSentrytraceData('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-x')).toBeUndefined();
6363
});
6464
});
65+
66+
describe('extractTracestateData', () => {
67+
it.each([
68+
// sentry only
69+
['sentry only', 'sentry=doGsaREgReaT', 'sentry=doGsaREgReaT', undefined],
70+
// sentry only, invalid (`!` isn't a valid base64 character)
71+
['sentry only, invalid', 'sentry=doGsaREgReaT!', undefined, undefined],
72+
// stuff before
73+
['stuff before', 'maisey=silly,sentry=doGsaREgReaT', 'sentry=doGsaREgReaT', 'maisey=silly'],
74+
// stuff after
75+
['stuff after', 'sentry=doGsaREgReaT,maisey=silly', 'sentry=doGsaREgReaT', 'maisey=silly'],
76+
// stuff before and after
77+
[
78+
'stuff before and after',
79+
'charlie=goofy,sentry=doGsaREgReaT,maisey=silly',
80+
'sentry=doGsaREgReaT',
81+
'charlie=goofy,maisey=silly',
82+
],
83+
// multiple before
84+
[
85+
'multiple before',
86+
'charlie=goofy,maisey=silly,sentry=doGsaREgReaT',
87+
'sentry=doGsaREgReaT',
88+
'charlie=goofy,maisey=silly',
89+
],
90+
// multiple after
91+
[
92+
'multiple after',
93+
'sentry=doGsaREgReaT,charlie=goofy,maisey=silly',
94+
'sentry=doGsaREgReaT',
95+
'charlie=goofy,maisey=silly',
96+
],
97+
// multiple before and after
98+
[
99+
'multiple before and after',
100+
'charlie=goofy,maisey=silly,sentry=doGsaREgReaT,bodhi=floppy,cory=loyal',
101+
'sentry=doGsaREgReaT',
102+
'charlie=goofy,maisey=silly,bodhi=floppy,cory=loyal',
103+
],
104+
// only third-party data
105+
['only third-party data', 'maisey=silly', undefined, 'maisey=silly'],
106+
// invalid third-party data, valid sentry data
107+
[
108+
'invalid third-party data, valid sentry data',
109+
'maisey_is_silly,sentry=doGsaREgReaT',
110+
'sentry=doGsaREgReaT',
111+
undefined,
112+
],
113+
// valid third party data, invalid sentry data
114+
['valid third-party data, invalid sentry data', 'maisey=silly,sentry=doGsaREgReaT!', undefined, 'maisey=silly'],
115+
// nothing valid at all
116+
['nothing valid at all', 'maisey_is_silly,sentry=doGsaREgReaT!', undefined, undefined],
117+
])('%s', (_testTitle: string, header: string, sentry?: string, thirdparty?: string): void => {
118+
expect(extractTracestateData(header)).toEqual({ sentry, thirdparty });
119+
});
120+
});

0 commit comments

Comments
 (0)