Skip to content

Commit 02be07d

Browse files
authored
fix(astro): Fix astro trace propagation issues (#14501)
Noticed this while working on #14481. The way we try-catched the astro server request code lead to the http.server span not being attached to servers correctly - we had a try-catch block _outside_ of the `startSpan` call, where we sent caught errors to sentry. but any error caught this way would not have an active span (because by the time the `catch` part triggers, `startSpan` is over), and thus the http.server span would not be attached to the error. By moving this try-catch inside of the `startSpan` call, we can correctly assign the span to errors. I also tried to add some tests to this - there is still a problem in there which the tests show, which I'll look at afterwards (and/or they may get fixed by #14481)
1 parent 17396a1 commit 02be07d

File tree

4 files changed

+97
-46
lines changed

4 files changed

+97
-46
lines changed

dev-packages/e2e-tests/test-applications/astro-4/tests/errors.server.test.ts

+55-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,32 @@
11
import { expect, test } from '@playwright/test';
2-
import { waitForError } from '@sentry-internal/test-utils';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
33

44
test.describe('server-side errors', () => {
55
test('captures SSR error', async ({ page }) => {
66
const errorEventPromise = waitForError('astro-4', errorEvent => {
77
return errorEvent?.exception?.values?.[0]?.value === "Cannot read properties of undefined (reading 'x')";
88
});
99

10+
const transactionEventPromise = waitForTransaction('astro-4', transactionEvent => {
11+
return transactionEvent.transaction === 'GET /ssr-error';
12+
});
13+
1014
await page.goto('/ssr-error');
1115

1216
const errorEvent = await errorEventPromise;
17+
const transactionEvent = await transactionEventPromise;
18+
19+
expect(transactionEvent).toMatchObject({
20+
transaction: 'GET /ssr-error',
21+
spans: [],
22+
});
23+
24+
const traceId = transactionEvent.contexts?.trace?.trace_id;
25+
const spanId = transactionEvent.contexts?.trace?.span_id;
26+
27+
expect(traceId).toMatch(/[a-f0-9]{32}/);
28+
expect(spanId).toMatch(/[a-f0-9]{16}/);
29+
expect(transactionEvent.contexts?.trace?.parent_span_id).toBeUndefined();
1330

1431
expect(errorEvent).toMatchObject({
1532
contexts: {
@@ -20,8 +37,8 @@ test.describe('server-side errors', () => {
2037
os: expect.any(Object),
2138
runtime: expect.any(Object),
2239
trace: {
23-
span_id: '', //TODO: This is a bug! We should expect.stringMatching(/[a-f0-9]{16}/) instead of ''
24-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
40+
span_id: spanId,
41+
trace_id: traceId,
2542
},
2643
},
2744
environment: 'qa',
@@ -69,18 +86,50 @@ test.describe('server-side errors', () => {
6986
const errorEventPromise = waitForError('astro-4', errorEvent => {
7087
return errorEvent?.exception?.values?.[0]?.value === 'Endpoint Error';
7188
});
89+
const transactionEventApiPromise = waitForTransaction('astro-4', transactionEvent => {
90+
return transactionEvent.transaction === 'GET /endpoint-error/api';
91+
});
92+
const transactionEventEndpointPromise = waitForTransaction('astro-4', transactionEvent => {
93+
return transactionEvent.transaction === 'GET /endpoint-error';
94+
});
7295

7396
await page.goto('/endpoint-error');
7497
await page.getByText('Get Data').click();
7598

7699
const errorEvent = await errorEventPromise;
100+
const transactionEventApi = await transactionEventApiPromise;
101+
const transactionEventEndpoint = await transactionEventEndpointPromise;
102+
103+
expect(transactionEventEndpoint).toMatchObject({
104+
transaction: 'GET /endpoint-error',
105+
spans: [],
106+
});
107+
108+
const traceId = transactionEventEndpoint.contexts?.trace?.trace_id;
109+
const endpointSpanId = transactionEventApi.contexts?.trace?.span_id;
110+
111+
expect(traceId).toMatch(/[a-f0-9]{32}/);
112+
expect(endpointSpanId).toMatch(/[a-f0-9]{16}/);
113+
114+
expect(transactionEventApi).toMatchObject({
115+
transaction: 'GET /endpoint-error/api',
116+
spans: [],
117+
});
118+
119+
const spanId = transactionEventApi.contexts?.trace?.span_id;
120+
const parentSpanId = transactionEventApi.contexts?.trace?.parent_span_id;
121+
122+
expect(spanId).toMatch(/[a-f0-9]{16}/);
123+
// TODO: This is incorrect, for whatever reason, it should be the endpointSpanId ideally
124+
expect(parentSpanId).toMatch(/[a-f0-9]{16}/);
125+
expect(parentSpanId).not.toEqual(endpointSpanId);
77126

78127
expect(errorEvent).toMatchObject({
79128
contexts: {
80129
trace: {
81-
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
82-
span_id: expect.stringMatching(/[a-f0-9]{16}/),
83-
trace_id: expect.stringMatching(/[a-f0-9]{32}/),
130+
parent_span_id: parentSpanId,
131+
span_id: spanId,
132+
trace_id: traceId,
84133
},
85134
},
86135
exception: {

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"clean:build": "lerna run clean",
1818
"clean:caches": "yarn rimraf eslintcache .nxcache && yarn jest --clearCache",
1919
"clean:deps": "lerna clean --yes && rm -rf node_modules && yarn",
20-
"clean:tarballs": "rimraf -g **/*.tgz",
20+
"clean:tarballs": "rimraf {packages,dev-packages}/*/*.tgz",
2121
"clean:all": "run-s clean:build clean:tarballs clean:caches clean:deps",
2222
"fix": "run-s fix:biome fix:prettier fix:lerna",
2323
"fix:lerna": "lerna run fix",

packages/astro/src/server/middleware.ts

+40-39
Original file line numberDiff line numberDiff line change
@@ -155,49 +155,50 @@ async function instrumentRequest(
155155
op: 'http.server',
156156
},
157157
async span => {
158-
const originalResponse = await next();
159-
160-
if (originalResponse.status) {
161-
setHttpStatus(span, originalResponse.status);
162-
}
163-
164-
const client = getClient();
165-
const contentType = originalResponse.headers.get('content-type');
166-
167-
const isPageloadRequest = contentType && contentType.startsWith('text/html');
168-
if (!isPageloadRequest || !client) {
169-
return originalResponse;
170-
}
171-
172-
// Type case necessary b/c the body's ReadableStream type doesn't include
173-
// the async iterator that is actually available in Node
174-
// We later on use the async iterator to read the body chunks
175-
// see https://github.com/microsoft/TypeScript/issues/39051
176-
const originalBody = originalResponse.body as NodeJS.ReadableStream | null;
177-
if (!originalBody) {
178-
return originalResponse;
158+
try {
159+
const originalResponse = await next();
160+
if (originalResponse.status) {
161+
setHttpStatus(span, originalResponse.status);
162+
}
163+
164+
const client = getClient();
165+
const contentType = originalResponse.headers.get('content-type');
166+
167+
const isPageloadRequest = contentType && contentType.startsWith('text/html');
168+
if (!isPageloadRequest || !client) {
169+
return originalResponse;
170+
}
171+
172+
// Type case necessary b/c the body's ReadableStream type doesn't include
173+
// the async iterator that is actually available in Node
174+
// We later on use the async iterator to read the body chunks
175+
// see https://github.com/microsoft/TypeScript/issues/39051
176+
const originalBody = originalResponse.body as NodeJS.ReadableStream | null;
177+
if (!originalBody) {
178+
return originalResponse;
179+
}
180+
181+
const decoder = new TextDecoder();
182+
183+
const newResponseStream = new ReadableStream({
184+
start: async controller => {
185+
for await (const chunk of originalBody) {
186+
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
187+
const modifiedHtml = addMetaTagToHead(html);
188+
controller.enqueue(new TextEncoder().encode(modifiedHtml));
189+
}
190+
controller.close();
191+
},
192+
});
193+
194+
return new Response(newResponseStream, originalResponse);
195+
} catch (e) {
196+
sendErrorToSentry(e);
197+
throw e;
179198
}
180-
181-
const decoder = new TextDecoder();
182-
183-
const newResponseStream = new ReadableStream({
184-
start: async controller => {
185-
for await (const chunk of originalBody) {
186-
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
187-
const modifiedHtml = addMetaTagToHead(html);
188-
controller.enqueue(new TextEncoder().encode(modifiedHtml));
189-
}
190-
controller.close();
191-
},
192-
});
193-
194-
return new Response(newResponseStream, originalResponse);
195199
},
196200
);
197201
return res;
198-
} catch (e) {
199-
sendErrorToSentry(e);
200-
throw e;
201202
} finally {
202203
vercelWaitUntil(
203204
(async () => {

packages/opentelemetry/src/utils/generateSpanContextForPropagationContext.ts

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function generateSpanContextForPropagationContext(propagationContext: Pro
1717

1818
const spanContext: SpanContext = {
1919
traceId: propagationContext.traceId,
20+
// TODO: Do not create an invalid span context here
2021
spanId: propagationContext.parentSpanId || '',
2122
isRemote: true,
2223
traceFlags: propagationContext.sampled ? TraceFlags.SAMPLED : TraceFlags.NONE,

0 commit comments

Comments
 (0)