diff --git a/packages/react-router/src/server/wrapSentryHandleRequest.ts b/packages/react-router/src/server/wrapSentryHandleRequest.ts index df7d65109338..e5e10f2c05b2 100644 --- a/packages/react-router/src/server/wrapSentryHandleRequest.ts +++ b/packages/react-router/src/server/wrapSentryHandleRequest.ts @@ -2,6 +2,7 @@ import { context } from '@opentelemetry/api'; import { getRPCMetadata, RPCType } from '@opentelemetry/core'; import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import { + flushIfServerless, getActiveSpan, getRootSpan, getTraceMetaTags, @@ -58,10 +59,15 @@ export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest): }); } - return originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext); + try { + return await originalHandle(request, responseStatusCode, responseHeaders, routerContext, loadContext); + } finally { + await flushIfServerless(); + } }; } +// todo(v11): remove this /** @deprecated Use `wrapSentryHandleRequest` instead. */ export const sentryHandleRequest = wrapSentryHandleRequest; diff --git a/packages/react-router/test/server/wrapSentryHandleRequest.test.ts b/packages/react-router/test/server/wrapSentryHandleRequest.test.ts index 40dce7c83702..430972cb92e2 100644 --- a/packages/react-router/test/server/wrapSentryHandleRequest.test.ts +++ b/packages/react-router/test/server/wrapSentryHandleRequest.test.ts @@ -1,6 +1,7 @@ import { RPCType } from '@opentelemetry/core'; import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import { + flushIfServerless, getActiveSpan, getRootSpan, getTraceMetaTags, @@ -15,13 +16,13 @@ vi.mock('@opentelemetry/core', () => ({ RPCType: { HTTP: 'http' }, getRPCMetadata: vi.fn(), })); - vi.mock('@sentry/core', () => ({ SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'sentry.source', SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin', getActiveSpan: vi.fn(), getRootSpan: vi.fn(), getTraceMetaTags: vi.fn(), + flushIfServerless: vi.fn(), })); describe('wrapSentryHandleRequest', () => { @@ -62,7 +63,8 @@ describe('wrapSentryHandleRequest', () => { (getActiveSpan as unknown as ReturnType).mockReturnValue(mockActiveSpan); (getRootSpan as unknown as ReturnType).mockReturnValue(mockRootSpan); const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata); - vi.mocked(vi.importActual('@opentelemetry/core')).getRPCMetadata = getRPCMetadata; + (vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata = + getRPCMetadata; const routerContext = { staticHandlerContext: { @@ -110,7 +112,8 @@ describe('wrapSentryHandleRequest', () => { (getActiveSpan as unknown as ReturnType).mockReturnValue(null); const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata); - vi.mocked(vi.importActual('@opentelemetry/core')).getRPCMetadata = getRPCMetadata; + (vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata = + getRPCMetadata; const routerContext = { staticHandlerContext: { @@ -122,6 +125,76 @@ describe('wrapSentryHandleRequest', () => { expect(getRPCMetadata).not.toHaveBeenCalled(); }); + + test('should call flushIfServerless on successful execution', async () => { + const originalHandler = vi.fn().mockResolvedValue('success response'); + const wrappedHandler = wrapSentryHandleRequest(originalHandler); + + const request = new Request('https://example.com'); + const responseStatusCode = 200; + const responseHeaders = new Headers(); + const routerContext = { staticHandlerContext: { matches: [] } } as any; + const loadContext = {} as any; + + await wrappedHandler(request, responseStatusCode, responseHeaders, routerContext, loadContext); + + expect(flushIfServerless).toHaveBeenCalled(); + }); + + test('should call flushIfServerless even when original handler throws an error', async () => { + const mockError = new Error('Handler failed'); + const originalHandler = vi.fn().mockRejectedValue(mockError); + const wrappedHandler = wrapSentryHandleRequest(originalHandler); + + const request = new Request('https://example.com'); + const responseStatusCode = 200; + const responseHeaders = new Headers(); + const routerContext = { staticHandlerContext: { matches: [] } } as any; + const loadContext = {} as any; + + await expect( + wrappedHandler(request, responseStatusCode, responseHeaders, routerContext, loadContext), + ).rejects.toThrow('Handler failed'); + + expect(flushIfServerless).toHaveBeenCalled(); + }); + + test('should propagate errors from original handler', async () => { + const mockError = new Error('Test error'); + const originalHandler = vi.fn().mockRejectedValue(mockError); + const wrappedHandler = wrapSentryHandleRequest(originalHandler); + + const request = new Request('https://example.com'); + const responseStatusCode = 500; + const responseHeaders = new Headers(); + const routerContext = { staticHandlerContext: { matches: [] } } as any; + const loadContext = {} as any; + + await expect(wrappedHandler(request, responseStatusCode, responseHeaders, routerContext, loadContext)).rejects.toBe( + mockError, + ); + }); +}); + +test('should not set span attributes when parameterized path does not exist', async () => { + const mockActiveSpan = {}; + const mockRootSpan = { setAttributes: vi.fn() }; + + (getActiveSpan as unknown as ReturnType).mockReturnValue(mockActiveSpan); + (getRootSpan as unknown as ReturnType).mockReturnValue(mockRootSpan); + + const originalHandler = vi.fn().mockResolvedValue('test'); + const wrappedHandler = wrapSentryHandleRequest(originalHandler); + + const routerContext = { + staticHandlerContext: { + matches: [], + }, + } as any; + + await wrappedHandler(new Request('https://guapo.chulo'), 200, new Headers(), routerContext, {} as any); + + expect(mockRootSpan.setAttributes).not.toHaveBeenCalled(); }); describe('getMetaTagTransformer', () => { @@ -132,68 +205,64 @@ describe('getMetaTagTransformer', () => { ); }); - test('should inject meta tags before closing head tag', done => { - const outputStream = new PassThrough(); - const bodyStream = new PassThrough(); - const transformer = getMetaTagTransformer(bodyStream); + test('should inject meta tags before closing head tag', () => { + return new Promise(resolve => { + const bodyStream = new PassThrough(); + const transformer = getMetaTagTransformer(bodyStream); - let outputData = ''; - outputStream.on('data', chunk => { - outputData += chunk.toString(); - }); - - outputStream.on('end', () => { - expect(outputData).toContain(''); - expect(outputData).not.toContain(''); - done(); - }); + let outputData = ''; + bodyStream.on('data', chunk => { + outputData += chunk.toString(); + }); - transformer.pipe(outputStream); + bodyStream.on('end', () => { + expect(outputData).toContain(''); + expect(outputData).not.toContain(''); + resolve(); + }); - bodyStream.write('Test'); - bodyStream.end(); + transformer.write('Test'); + transformer.end(); + }); }); - test('should not modify chunks without head closing tag', done => { - const outputStream = new PassThrough(); - const bodyStream = new PassThrough(); - const transformer = getMetaTagTransformer(bodyStream); - - let outputData = ''; - outputStream.on('data', chunk => { - outputData += chunk.toString(); - }); + test('should not modify chunks without head closing tag', () => { + return new Promise(resolve => { + const bodyStream = new PassThrough(); + const transformer = getMetaTagTransformer(bodyStream); - outputStream.on('end', () => { - expect(outputData).toBe('Test'); - expect(getTraceMetaTags).toHaveBeenCalled(); - done(); - }); + let outputData = ''; + bodyStream.on('data', chunk => { + outputData += chunk.toString(); + }); - transformer.pipe(outputStream); + bodyStream.on('end', () => { + expect(outputData).toBe('Test'); + resolve(); + }); - bodyStream.write('Test'); - bodyStream.end(); + transformer.write('Test'); + transformer.end(); + }); }); - test('should handle buffer input', done => { - const outputStream = new PassThrough(); - const bodyStream = new PassThrough(); - const transformer = getMetaTagTransformer(bodyStream); - - let outputData = ''; - outputStream.on('data', chunk => { - outputData += chunk.toString(); - }); + test('should handle buffer input', () => { + return new Promise(resolve => { + const bodyStream = new PassThrough(); + const transformer = getMetaTagTransformer(bodyStream); - outputStream.on('end', () => { - expect(outputData).toContain(''); - done(); - }); + let outputData = ''; + bodyStream.on('data', chunk => { + outputData += chunk.toString(); + }); - transformer.pipe(outputStream); + bodyStream.on('end', () => { + expect(outputData).toContain(''); + resolve(); + }); - bodyStream.write(Buffer.from('Test')); - bodyStream.end(); + transformer.write(Buffer.from('Test')); + transformer.end(); + }); }); });