Skip to content

Commit 2789420

Browse files
authored
Merge pull request #9510 from getsentry/prepare-release/7.80.0
meta: Update changelog for 7.80.0
2 parents b435ea5 + e0a9d58 commit 2789420

File tree

57 files changed

+744
-205
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+744
-205
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
## 7.80.0
8+
9+
- feat(astro): Add distributed tracing via `<meta>` tags (#9483)
10+
- feat(node): Capture internal server errors in trpc middleware (#9482)
11+
- feat(remix): Export a type to use for `MetaFunction` parameters (#9493)
12+
- fix(astro): Mark SDK package as Astro-external (#9509)
13+
- ref(nextjs): Don't initialize Server SDK during build (#9503)
14+
715
## 7.79.0
816

917
- feat(tracing): Add span `origin` to trace context (#9472)

packages/astro/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ import { sequence } from "astro:middleware";
6464
import * as Sentry from "@sentry/astro";
6565

6666
export const onRequest = sequence(
67-
Sentry.sentryMiddleware(),
68-
// Add your other handlers after sentryMiddleware
67+
Sentry.handleRequest(),
68+
// Add your other handlers after Sentry.handleRequest()
6969
);
7070
```
7171

packages/astro/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,8 @@
7474
},
7575
"volta": {
7676
"extends": "../../package.json"
77+
},
78+
"astro": {
79+
"external": true
7780
}
7881
}

packages/astro/src/server/meta.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { getDynamicSamplingContextFromClient } from '@sentry/core';
2+
import type { Hub, Span } from '@sentry/types';
3+
import {
4+
dynamicSamplingContextToSentryBaggageHeader,
5+
generateSentryTraceHeader,
6+
logger,
7+
TRACEPARENT_REGEXP,
8+
} from '@sentry/utils';
9+
10+
/**
11+
* Extracts the tracing data from the current span or from the client's scope
12+
* (via transaction or propagation context) and renders the data to <meta> tags.
13+
*
14+
* This function creates two serialized <meta> tags:
15+
* - `<meta name="sentry-trace" content="..."/>`
16+
* - `<meta name="baggage" content="..."/>`
17+
*
18+
* TODO: Extract this later on and export it from the Core or Node SDK
19+
*
20+
* @param span the currently active span
21+
* @param client the SDK's client
22+
*
23+
* @returns an object with the two serialized <meta> tags
24+
*/
25+
export function getTracingMetaTags(span: Span | undefined, hub: Hub): { sentryTrace: string; baggage?: string } {
26+
const scope = hub.getScope();
27+
const client = hub.getClient();
28+
const { dsc, sampled, traceId } = scope.getPropagationContext();
29+
const transaction = span?.transaction;
30+
31+
const sentryTrace = span ? span.toTraceparent() : generateSentryTraceHeader(traceId, undefined, sampled);
32+
33+
const dynamicSamplingContext = transaction
34+
? transaction.getDynamicSamplingContext()
35+
: dsc
36+
? dsc
37+
: client
38+
? getDynamicSamplingContextFromClient(traceId, client, scope)
39+
: undefined;
40+
41+
const baggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext);
42+
43+
const isValidSentryTraceHeader = TRACEPARENT_REGEXP.test(sentryTrace);
44+
if (!isValidSentryTraceHeader) {
45+
logger.warn('Invalid sentry-trace data. Returning empty <meta name="sentry-trace"/> tag');
46+
}
47+
48+
const validBaggage = isValidBaggageString(baggage);
49+
if (!validBaggage) {
50+
logger.warn('Invalid baggage data. Returning empty <meta name="baggage"/> tag');
51+
}
52+
53+
return {
54+
sentryTrace: `<meta name="sentry-trace" content="${isValidSentryTraceHeader ? sentryTrace : ''}"/>`,
55+
baggage: baggage && `<meta name="baggage" content="${validBaggage ? baggage : ''}"/>`,
56+
};
57+
}
58+
59+
/**
60+
* Tests string against baggage spec as defined in:
61+
*
62+
* - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition
63+
* - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6
64+
*
65+
* exported for testing
66+
*/
67+
export function isValidBaggageString(baggage?: string): boolean {
68+
if (!baggage || !baggage.length) {
69+
return false;
70+
}
71+
const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+";
72+
const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+';
73+
const spaces = '\\s*';
74+
const baggageRegex = new RegExp(
75+
`^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`,
76+
);
77+
return baggageRegex.test(baggage);
78+
}

packages/astro/src/server/middleware.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
1-
import { captureException, configureScope, startSpan } from '@sentry/node';
1+
import { captureException, configureScope, getCurrentHub, startSpan } from '@sentry/node';
2+
import type { Hub, Span } from '@sentry/types';
23
import { addExceptionMechanism, objectify, stripUrlQueryAndFragment, tracingContextFromHeaders } from '@sentry/utils';
34
import type { APIContext, MiddlewareResponseHandler } from 'astro';
45

6+
import { getTracingMetaTags } from './meta';
7+
58
type MiddlewareOptions = {
69
/**
710
* If true, the client IP will be attached to the event by calling `setUser`.
8-
* Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
911
*
10-
* This will only work if your app is configured for SSR
12+
* Important: Only enable this option if your Astro app is configured for (hybrid) SSR
13+
* via the `output: 'server' | 'hybrid'` option in your `astro.config.mjs` file.
14+
* Otherwise, Astro will throw an error when starting the server.
15+
*
16+
* Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
1117
*
1218
* @default false (recommended)
1319
*/
1420
trackClientIp?: boolean;
1521

1622
/**
1723
* If true, the headers from the request will be attached to the event by calling `setExtra`.
24+
*
1825
* Only set this to `true` if you're fine with collecting potentially personally identifiable information (PII).
1926
*
2027
* @default false (recommended)
@@ -93,11 +100,42 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
93100
},
94101
},
95102
async span => {
96-
const res = await next();
97-
if (span && res.status) {
98-
span.setHttpStatus(res.status);
103+
const originalResponse = await next();
104+
105+
if (span && originalResponse.status) {
106+
span.setHttpStatus(originalResponse.status);
107+
}
108+
109+
const hub = getCurrentHub();
110+
const client = hub.getClient();
111+
const contentType = originalResponse.headers.get('content-type');
112+
113+
const isPageloadRequest = contentType && contentType.startsWith('text/html');
114+
if (!isPageloadRequest || !client) {
115+
return originalResponse;
99116
}
100-
return res;
117+
118+
// Type case necessary b/c the body's ReadableStream type doesn't include
119+
// the async iterator that is actually available in Node
120+
// We later on use the async iterator to read the body chunks
121+
// see https://github.com/microsoft/TypeScript/issues/39051
122+
const originalBody = originalResponse.body as NodeJS.ReadableStream | null;
123+
if (!originalBody) {
124+
return originalResponse;
125+
}
126+
127+
const newResponseStream = new ReadableStream({
128+
start: async controller => {
129+
for await (const chunk of originalBody) {
130+
const html = typeof chunk === 'string' ? chunk : new TextDecoder().decode(chunk);
131+
const modifiedHtml = addMetaTagToHead(html, hub, span);
132+
controller.enqueue(new TextEncoder().encode(modifiedHtml));
133+
}
134+
controller.close();
135+
},
136+
});
137+
138+
return new Response(newResponseStream, originalResponse);
101139
},
102140
);
103141
return res;
@@ -109,6 +147,20 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH
109147
};
110148
};
111149

150+
/**
151+
* This function optimistically assumes that the HTML coming in chunks will not be split
152+
* within the <head> tag. If this still happens, we simply won't replace anything.
153+
*/
154+
function addMetaTagToHead(htmlChunk: string, hub: Hub, span?: Span): string {
155+
if (typeof htmlChunk !== 'string') {
156+
return htmlChunk;
157+
}
158+
159+
const { sentryTrace, baggage } = getTracingMetaTags(span, hub);
160+
const content = `<head>\n${sentryTrace}\n${baggage}\n`;
161+
return htmlChunk.replace('<head>', content);
162+
}
163+
112164
/**
113165
* Interpolates the route from the URL and the passed params.
114166
* Best we can do to get a route name instead of a raw URL.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import * as SentryCore from '@sentry/core';
2+
import { vi } from 'vitest';
3+
4+
import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta';
5+
6+
const mockedSpan = {
7+
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
8+
transaction: {
9+
getDynamicSamplingContext: () => ({
10+
environment: 'production',
11+
}),
12+
},
13+
};
14+
15+
const mockedHub = {
16+
getScope: () => ({
17+
getPropagationContext: () => ({
18+
traceId: '123',
19+
}),
20+
}),
21+
getClient: () => ({}),
22+
};
23+
24+
describe('getTracingMetaTags', () => {
25+
it('returns the tracing tags from the span, if it is provided', () => {
26+
{
27+
// @ts-expect-error - only passing a partial span object
28+
const tags = getTracingMetaTags(mockedSpan, mockedHub);
29+
30+
expect(tags).toEqual({
31+
sentryTrace: '<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1"/>',
32+
baggage: '<meta name="baggage" content="sentry-environment=production"/>',
33+
});
34+
}
35+
});
36+
37+
it('returns propagationContext DSC data if no span is available', () => {
38+
const tags = getTracingMetaTags(undefined, {
39+
...mockedHub,
40+
// @ts-expect-error - only passing a partial scope object
41+
getScope: () => ({
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+
}),
53+
});
54+
55+
expect(tags).toEqual({
56+
sentryTrace: expect.stringMatching(
57+
/<meta name="sentry-trace" content="12345678901234567890123456789012-(.{16})-1"\/>/,
58+
),
59+
baggage:
60+
'<meta name="baggage" content="sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012"/>',
61+
});
62+
});
63+
64+
it('returns only the `sentry-trace` tag if no DSC is available', () => {
65+
vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
66+
trace_id: '',
67+
public_key: undefined,
68+
});
69+
70+
const tags = getTracingMetaTags(
71+
// @ts-expect-error - only passing a partial span object
72+
{
73+
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
74+
transaction: undefined,
75+
},
76+
mockedHub,
77+
);
78+
79+
expect(tags).toEqual({
80+
sentryTrace: '<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1"/>',
81+
});
82+
});
83+
84+
it('returns only the `sentry-trace` tag if no DSC is available', () => {
85+
vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({
86+
trace_id: '',
87+
public_key: undefined,
88+
});
89+
90+
const tags = getTracingMetaTags(
91+
// @ts-expect-error - only passing a partial span object
92+
{
93+
toTraceparent: () => '12345678901234567890123456789012-1234567890123456-1',
94+
transaction: undefined,
95+
},
96+
{
97+
...mockedHub,
98+
getClient: () => undefined,
99+
},
100+
);
101+
102+
expect(tags).toEqual({
103+
sentryTrace: '<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1"/>',
104+
});
105+
});
106+
});
107+
108+
describe('isValidBaggageString', () => {
109+
it.each([
110+
'sentry-environment=production',
111+
'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=abc',
112+
// @ is allowed in values
113+
114+
// spaces are allowed around the delimiters
115+
'sentry-environment=staging , sentry-public_key=key ,[email protected]',
116+
'sentry-environment=staging , thirdparty=value ,[email protected]',
117+
// these characters are explicitly allowed for keys in the baggage spec:
118+
"!#$%&'*+-.^_`|~1234567890abcxyzABCXYZ=true",
119+
// special characters in values are fine (except for ",;\ - see other test)
120+
'key=(value)',
121+
'key=[{(value)}]',
122+
'key=some$value',
123+
'key=more#value',
124+
'key=max&value',
125+
'key=max:value',
126+
'key=x=value',
127+
])('returns true if the baggage string is valid (%s)', baggageString => {
128+
expect(isValidBaggageString(baggageString)).toBe(true);
129+
});
130+
131+
it.each([
132+
// baggage spec doesn't permit leading spaces
133+
' sentry-environment=production,sentry-publickey=key,sentry-trace_id=abc',
134+
// no spaces in keys or values
135+
'sentry-public key=key',
136+
'sentry-publickey=my key',
137+
// no delimiters ("(),/:;<=>?@[\]{}") in keys
138+
'asdf(x=value',
139+
'asdf)x=value',
140+
'asdf,x=value',
141+
'asdf/x=value',
142+
'asdf:x=value',
143+
'asdf;x=value',
144+
'asdf<x=value',
145+
'asdf>x=value',
146+
'asdf?x=value',
147+
'asdf@x=value',
148+
'asdf[x=value',
149+
'asdf]x=value',
150+
'asdf\\x=value',
151+
'asdf{x=value',
152+
'asdf}x=value',
153+
// no ,;\" in values
154+
'key=va,lue',
155+
'key=va;lue',
156+
'key=va\\lue',
157+
'key=va"lue"',
158+
// baggage headers can have properties but we currently don't support them
159+
'sentry-environment=production;prop1=foo;prop2=bar,nextkey=value',
160+
// no fishy stuff
161+
'absolutely not a valid baggage string',
162+
'val"/><script>alert("xss")</script>',
163+
'something"/>',
164+
'<script>alert("xss")</script>',
165+
'/>',
166+
'" onblur="alert("xss")',
167+
])('returns false if the baggage string is invalid (%s)', baggageString => {
168+
expect(isValidBaggageString(baggageString)).toBe(false);
169+
});
170+
171+
it('returns false if the baggage string is empty', () => {
172+
expect(isValidBaggageString('')).toBe(false);
173+
});
174+
175+
it('returns false if the baggage string is empty', () => {
176+
expect(isValidBaggageString(undefined)).toBe(false);
177+
});
178+
});

0 commit comments

Comments
 (0)