Skip to content

Commit a400285

Browse files
committed
actually add files in node lol
1 parent 9296b88 commit a400285

File tree

2 files changed

+272
-0
lines changed

2 files changed

+272
-0
lines changed

packages/node/src/utils/meta.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
getDynamicSamplingContextFromClient,
3+
getDynamicSamplingContextFromSpan,
4+
getRootSpan,
5+
spanToTraceHeader,
6+
} from '@sentry/core';
7+
import type { Client, Scope, Span } from '@sentry/types';
8+
import {
9+
TRACEPARENT_REGEXP,
10+
dynamicSamplingContextToSentryBaggageHeader,
11+
generateSentryTraceHeader,
12+
logger,
13+
} from '@sentry/utils';
14+
15+
/**
16+
* Extracts the tracing data from the current span or from the client's scope (via transaction or propagation context)
17+
* and serializes the data to <meta> tag contents.
18+
*
19+
* Use this function to obtain the tracing meta tags you can inject when rendering an HTML response to continue
20+
* the server-initiated trace on the client.
21+
*
22+
* @param span the currently active span
23+
* @param client the SDK's client
24+
*
25+
* @returns an object with the two meta tags. The object keys are the name of the meta tag,
26+
* the respective value is the content.
27+
*/
28+
export function getTracingMetaTags(
29+
span: Span | undefined,
30+
scope: Scope,
31+
client: Client | undefined,
32+
): { 'sentry-trace': string; baggage?: string } {
33+
const { dsc, sampled, traceId } = scope.getPropagationContext();
34+
const rootSpan = span && getRootSpan(span);
35+
36+
const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled);
37+
38+
const dynamicSamplingContext = rootSpan
39+
? getDynamicSamplingContextFromSpan(rootSpan)
40+
: dsc
41+
? dsc
42+
: client
43+
? getDynamicSamplingContextFromClient(traceId, client)
44+
: undefined;
45+
46+
const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
47+
48+
const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace);
49+
if (!isValidSentryTraceHeader) {
50+
logger.warn('Invalid sentry-trace data. Returning empty "sentry-trace" meta tag');
51+
}
52+
53+
const validBaggage = isValidBaggageString(baggage);
54+
if (!validBaggage) {
55+
logger.warn('Invalid baggage data. Not returning "baggage" meta tag');
56+
}
57+
58+
return {
59+
'sentry-trace': isValidSentryTraceHeader ? sentryTrace : '',
60+
...(validBaggage && { baggage }),
61+
};
62+
}
63+
64+
/**
65+
* Tests string against baggage spec as defined in:
66+
*
67+
* - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition
68+
* - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
69+
*
70+
* exported for testing
71+
*/
72+
export function isValidBaggageString(baggage?: string): boolean {
73+
if (!baggage || !baggage.length) {
74+
return false;
75+
}
76+
const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+";
77+
const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+';
78+
const spaces = '\\s*';
79+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input
80+
const baggageRegex = new RegExp(
81+
`^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`,
82+
);
83+
return baggageRegex.test(baggage);
84+
}

packages/node/test/utils/meta.test.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import * as SentryCore from '@sentry/core';
2+
import { SentrySpan } from '@sentry/core';
3+
4+
import { getTracingMetaTags, isValidBaggageString } from '../../src/utils/meta';
5+
6+
const TRACE_FLAG_SAMPLED = 1;
7+
8+
const mockedSpan = new SentrySpan({
9+
traceId: '12345678901234567890123456789012',
10+
spanId: '1234567890123456',
11+
sampled: true,
12+
});
13+
14+
const mockedClient = {} as any;
15+
16+
const mockedScope = {
17+
getPropagationContext: () => ({
18+
traceId: '123',
19+
}),
20+
} as any;
21+
22+
describe('getTracingMetaTags', () => {
23+
it('returns the tracing meta tags from the span, if it is provided', () => {
24+
{
25+
jest.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({
26+
environment: 'production',
27+
});
28+
29+
const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient);
30+
31+
expect(tags).toEqual({
32+
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
33+
baggage: 'sentry-environment=production',
34+
});
35+
}
36+
});
37+
38+
it('returns propagationContext DSC data if no span is available', () => {
39+
const tags = getTracingMetaTags(
40+
undefined,
41+
{
42+
getPropagationContext: () => ({
43+
traceId: '12345678901234567890123456789012',
44+
sampled: true,
45+
spanId: '1234567890123456',
46+
dsc: {
47+
environment: 'staging',
48+
public_key: 'key',
49+
trace_id: '12345678901234567890123456789012',
50+
},
51+
}),
52+
} as any,
53+
mockedClient,
54+
);
55+
56+
expect(tags).toEqual({
57+
'sentry-trace': expect.stringMatching(/12345678901234567890123456789012-(.{16})-1/),
58+
baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012',
59+
});
60+
});
61+
62+
it('returns only the `sentry-trace` tag if no DSC is available', () => {
63+
jest.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
64+
trace_id: '',
65+
public_key: undefined,
66+
});
67+
68+
const tags = getTracingMetaTags(
69+
// @ts-expect-error - we don't need to provide all the properties
70+
{
71+
isRecording: () => true,
72+
spanContext: () => {
73+
return {
74+
traceId: '12345678901234567890123456789012',
75+
spanId: '1234567890123456',
76+
traceFlags: TRACE_FLAG_SAMPLED,
77+
};
78+
},
79+
},
80+
mockedScope,
81+
mockedClient,
82+
);
83+
84+
expect(tags).toEqual({
85+
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
86+
});
87+
});
88+
89+
it('returns only the `sentry-trace` tag if no DSC is available without a client', () => {
90+
jest.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
91+
trace_id: '',
92+
public_key: undefined,
93+
});
94+
95+
const tags = getTracingMetaTags(
96+
// @ts-expect-error - we don't need to provide all the properties
97+
{
98+
isRecording: () => true,
99+
spanContext: () => {
100+
return {
101+
traceId: '12345678901234567890123456789012',
102+
spanId: '1234567890123456',
103+
traceFlags: TRACE_FLAG_SAMPLED,
104+
};
105+
},
106+
},
107+
mockedScope,
108+
undefined,
109+
);
110+
111+
expect(tags).toEqual({
112+
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
113+
});
114+
expect('baggage' in tags).toBe(false);
115+
});
116+
});
117+
118+
describe('isValidBaggageString', () => {
119+
it.each([
120+
'sentry-environment=production',
121+
'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=abc',
122+
// @ is allowed in values
123+
124+
// spaces are allowed around the delimiters
125+
'sentry-environment=staging , sentry-public_key=key ,[email protected]',
126+
'sentry-environment=staging , thirdparty=value ,[email protected]',
127+
// these characters are explicitly allowed for keys in the baggage spec:
128+
"!#$%&'*+-.^_`|~1234567890abcxyzABCXYZ=true",
129+
// special characters in values are fine (except for ",;\ - see other test)
130+
'key=(value)',
131+
'key=[{(value)}]',
132+
'key=some$value',
133+
'key=more#value',
134+
'key=max&value',
135+
'key=max:value',
136+
'key=x=value',
137+
])('returns true if the baggage string is valid (%s)', baggageString => {
138+
expect(isValidBaggageString(baggageString)).toBe(true);
139+
});
140+
141+
it.each([
142+
// baggage spec doesn't permit leading spaces
143+
' sentry-environment=production,sentry-publickey=key,sentry-trace_id=abc',
144+
// no spaces in keys or values
145+
'sentry-public key=key',
146+
'sentry-publickey=my key',
147+
// no delimiters ("(),/:;<=>?@[\]{}") in keys
148+
'asdf(x=value',
149+
'asdf)x=value',
150+
'asdf,x=value',
151+
'asdf/x=value',
152+
'asdf:x=value',
153+
'asdf;x=value',
154+
'asdf<x=value',
155+
'asdf>x=value',
156+
'asdf?x=value',
157+
'asdf@x=value',
158+
'asdf[x=value',
159+
'asdf]x=value',
160+
'asdf\\x=value',
161+
'asdf{x=value',
162+
'asdf}x=value',
163+
// no ,;\" in values
164+
'key=va,lue',
165+
'key=va;lue',
166+
'key=va\\lue',
167+
'key=va"lue"',
168+
// baggage headers can have properties but we currently don't support them
169+
'sentry-environment=production;prop1=foo;prop2=bar,nextkey=value',
170+
// no fishy stuff
171+
'absolutely not a valid baggage string',
172+
'val"/><script>alert("xss")</script>',
173+
'something"/>',
174+
'<script>alert("xss")</script>',
175+
'/>',
176+
'" onblur="alert("xss")',
177+
])('returns false if the baggage string is invalid (%s)', baggageString => {
178+
expect(isValidBaggageString(baggageString)).toBe(false);
179+
});
180+
181+
it('returns false if the baggage string is empty', () => {
182+
expect(isValidBaggageString('')).toBe(false);
183+
});
184+
185+
it('returns false if the baggage string is empty', () => {
186+
expect(isValidBaggageString(undefined)).toBe(false);
187+
});
188+
});

0 commit comments

Comments
 (0)