Skip to content

Commit 12ac49a

Browse files
authored
feat(react-router): Automatically flush on Vercel for request handlers (#17232)
1 parent 5652282 commit 12ac49a

File tree

2 files changed

+129
-54
lines changed

2 files changed

+129
-54
lines changed

packages/react-router/src/server/wrapSentryHandleRequest.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { context } from '@opentelemetry/api';
22
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
33
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
44
import {
5+
flushIfServerless,
56
getActiveSpan,
67
getRootSpan,
78
getTraceMetaTags,
@@ -58,10 +59,15 @@ export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest):
5859
});
5960
}
6061

61-
return originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext);
62+
try {
63+
return await originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext);
64+
} finally {
65+
await flushIfServerless();
66+
}
6267
};
6368
}
6469

70+
// todo(v11): remove this
6571
/** @deprecated Use `wrapSentryHandleRequest` instead. */
6672
export const sentryHandleRequest = wrapSentryHandleRequest;
6773

packages/react-router/test/server/wrapSentryHandleRequest.test.ts

Lines changed: 122 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { RPCType } from '@opentelemetry/core';
22
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
33
import {
4+
flushIfServerless,
45
getActiveSpan,
56
getRootSpan,
67
getTraceMetaTags,
@@ -15,13 +16,13 @@ vi.mock('@opentelemetry/core', () => ({
1516
RPCType: { HTTP: 'http' },
1617
getRPCMetadata: vi.fn(),
1718
}));
18-
1919
vi.mock('@sentry/core', () => ({
2020
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'sentry.source',
2121
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin',
2222
getActiveSpan: vi.fn(),
2323
getRootSpan: vi.fn(),
2424
getTraceMetaTags: vi.fn(),
25+
flushIfServerless: vi.fn(),
2526
}));
2627

2728
describe('wrapSentryHandleRequest', () => {
@@ -62,7 +63,8 @@ describe('wrapSentryHandleRequest', () => {
6263
(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockActiveSpan);
6364
(getRootSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockRootSpan);
6465
const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata);
65-
vi.mocked(vi.importActual('@opentelemetry/core')).getRPCMetadata = getRPCMetadata;
66+
(vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata =
67+
getRPCMetadata;
6668

6769
const routerContext = {
6870
staticHandlerContext: {
@@ -110,7 +112,8 @@ describe('wrapSentryHandleRequest', () => {
110112
(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(null);
111113

112114
const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata);
113-
vi.mocked(vi.importActual('@opentelemetry/core')).getRPCMetadata = getRPCMetadata;
115+
(vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata =
116+
getRPCMetadata;
114117

115118
const routerContext = {
116119
staticHandlerContext: {
@@ -122,6 +125,76 @@ describe('wrapSentryHandleRequest', () => {
122125

123126
expect(getRPCMetadata).not.toHaveBeenCalled();
124127
});
128+
129+
test('should call flushIfServerless on successful execution', async () => {
130+
const originalHandler = vi.fn().mockResolvedValue('success response');
131+
const wrappedHandler = wrapSentryHandleRequest(originalHandler);
132+
133+
const request = new Request('https://example.com');
134+
const responseStatusCode = 200;
135+
const responseHeaders = new Headers();
136+
const routerContext = { staticHandlerContext: { matches: [] } } as any;
137+
const loadContext = {} as any;
138+
139+
await wrappedHandler(request, responseStatusCode, responseHeaders, routerContext, loadContext);
140+
141+
expect(flushIfServerless).toHaveBeenCalled();
142+
});
143+
144+
test('should call flushIfServerless even when original handler throws an error', async () => {
145+
const mockError = new Error('Handler failed');
146+
const originalHandler = vi.fn().mockRejectedValue(mockError);
147+
const wrappedHandler = wrapSentryHandleRequest(originalHandler);
148+
149+
const request = new Request('https://example.com');
150+
const responseStatusCode = 200;
151+
const responseHeaders = new Headers();
152+
const routerContext = { staticHandlerContext: { matches: [] } } as any;
153+
const loadContext = {} as any;
154+
155+
await expect(
156+
wrappedHandler(request, responseStatusCode, responseHeaders, routerContext, loadContext),
157+
).rejects.toThrow('Handler failed');
158+
159+
expect(flushIfServerless).toHaveBeenCalled();
160+
});
161+
162+
test('should propagate errors from original handler', async () => {
163+
const mockError = new Error('Test error');
164+
const originalHandler = vi.fn().mockRejectedValue(mockError);
165+
const wrappedHandler = wrapSentryHandleRequest(originalHandler);
166+
167+
const request = new Request('https://example.com');
168+
const responseStatusCode = 500;
169+
const responseHeaders = new Headers();
170+
const routerContext = { staticHandlerContext: { matches: [] } } as any;
171+
const loadContext = {} as any;
172+
173+
await expect(wrappedHandler(request, responseStatusCode, responseHeaders, routerContext, loadContext)).rejects.toBe(
174+
mockError,
175+
);
176+
});
177+
});
178+
179+
test('should not set span attributes when parameterized path does not exist', async () => {
180+
const mockActiveSpan = {};
181+
const mockRootSpan = { setAttributes: vi.fn() };
182+
183+
(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockActiveSpan);
184+
(getRootSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockRootSpan);
185+
186+
const originalHandler = vi.fn().mockResolvedValue('test');
187+
const wrappedHandler = wrapSentryHandleRequest(originalHandler);
188+
189+
const routerContext = {
190+
staticHandlerContext: {
191+
matches: [],
192+
},
193+
} as any;
194+
195+
await wrappedHandler(new Request('https://guapo.chulo'), 200, new Headers(), routerContext, {} as any);
196+
197+
expect(mockRootSpan.setAttributes).not.toHaveBeenCalled();
125198
});
126199

127200
describe('getMetaTagTransformer', () => {
@@ -132,68 +205,64 @@ describe('getMetaTagTransformer', () => {
132205
);
133206
});
134207

135-
test('should inject meta tags before closing head tag', done => {
136-
const outputStream = new PassThrough();
137-
const bodyStream = new PassThrough();
138-
const transformer = getMetaTagTransformer(bodyStream);
208+
test('should inject meta tags before closing head tag', () => {
209+
return new Promise<void>(resolve => {
210+
const bodyStream = new PassThrough();
211+
const transformer = getMetaTagTransformer(bodyStream);
139212

140-
let outputData = '';
141-
outputStream.on('data', chunk => {
142-
outputData += chunk.toString();
143-
});
144-
145-
outputStream.on('end', () => {
146-
expect(outputData).toContain('<meta name="sentry-trace" content="test-trace-id"></head>');
147-
expect(outputData).not.toContain('</head></head>');
148-
done();
149-
});
213+
let outputData = '';
214+
bodyStream.on('data', chunk => {
215+
outputData += chunk.toString();
216+
});
150217

151-
transformer.pipe(outputStream);
218+
bodyStream.on('end', () => {
219+
expect(outputData).toContain('<meta name="sentry-trace" content="test-trace-id"></head>');
220+
expect(outputData).not.toContain('</head></head>');
221+
resolve();
222+
});
152223

153-
bodyStream.write('<html><head></head><body>Test</body></html>');
154-
bodyStream.end();
224+
transformer.write('<html><head></head><body>Test</body></html>');
225+
transformer.end();
226+
});
155227
});
156228

157-
test('should not modify chunks without head closing tag', done => {
158-
const outputStream = new PassThrough();
159-
const bodyStream = new PassThrough();
160-
const transformer = getMetaTagTransformer(bodyStream);
161-
162-
let outputData = '';
163-
outputStream.on('data', chunk => {
164-
outputData += chunk.toString();
165-
});
229+
test('should not modify chunks without head closing tag', () => {
230+
return new Promise<void>(resolve => {
231+
const bodyStream = new PassThrough();
232+
const transformer = getMetaTagTransformer(bodyStream);
166233

167-
outputStream.on('end', () => {
168-
expect(outputData).toBe('<html><body>Test</body></html>');
169-
expect(getTraceMetaTags).toHaveBeenCalled();
170-
done();
171-
});
234+
let outputData = '';
235+
bodyStream.on('data', chunk => {
236+
outputData += chunk.toString();
237+
});
172238

173-
transformer.pipe(outputStream);
239+
bodyStream.on('end', () => {
240+
expect(outputData).toBe('<html><body>Test</body></html>');
241+
resolve();
242+
});
174243

175-
bodyStream.write('<html><body>Test</body></html>');
176-
bodyStream.end();
244+
transformer.write('<html><body>Test</body></html>');
245+
transformer.end();
246+
});
177247
});
178248

179-
test('should handle buffer input', done => {
180-
const outputStream = new PassThrough();
181-
const bodyStream = new PassThrough();
182-
const transformer = getMetaTagTransformer(bodyStream);
183-
184-
let outputData = '';
185-
outputStream.on('data', chunk => {
186-
outputData += chunk.toString();
187-
});
249+
test('should handle buffer input', () => {
250+
return new Promise<void>(resolve => {
251+
const bodyStream = new PassThrough();
252+
const transformer = getMetaTagTransformer(bodyStream);
188253

189-
outputStream.on('end', () => {
190-
expect(outputData).toContain('<meta name="sentry-trace" content="test-trace-id"></head>');
191-
done();
192-
});
254+
let outputData = '';
255+
bodyStream.on('data', chunk => {
256+
outputData += chunk.toString();
257+
});
193258

194-
transformer.pipe(outputStream);
259+
bodyStream.on('end', () => {
260+
expect(outputData).toContain('<meta name="sentry-trace" content="test-trace-id"></head>');
261+
resolve();
262+
});
195263

196-
bodyStream.write(Buffer.from('<html><head></head><body>Test</body></html>'));
197-
bodyStream.end();
264+
transformer.write(Buffer.from('<html><head></head><body>Test</body></html>'));
265+
transformer.end();
266+
});
198267
});
199268
});

0 commit comments

Comments
 (0)