Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/react-router/src/server/wrapSentryHandleRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;

Expand Down
163 changes: 154 additions & 9 deletions packages/react-router/test/server/wrapSentryHandleRequest.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RPCType } from '@opentelemetry/core';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import {
flushIfServerless,
getActiveSpan,
getRootSpan,
getTraceMetaTags,
Expand All @@ -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', () => {
Expand Down Expand Up @@ -62,7 +63,8 @@ describe('wrapSentryHandleRequest', () => {
(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockActiveSpan);
(getRootSpan as unknown as ReturnType<typeof vi.fn>).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: {
Expand Down Expand Up @@ -110,7 +112,8 @@ describe('wrapSentryHandleRequest', () => {
(getActiveSpan as unknown as ReturnType<typeof vi.fn>).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: {
Expand All @@ -122,6 +125,151 @@ 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 call original handler with same parameters', async () => {
const originalHandler = vi.fn().mockResolvedValue('original response');
const wrappedHandler = wrapSentryHandleRequest(originalHandler);

const request = new Request('https://taco.burrito');
const responseStatusCode = 200;
const responseHeaders = new Headers();
const routerContext = { staticHandlerContext: { matches: [] } } as any;
const loadContext = {} as any;

const result = await wrappedHandler(request, responseStatusCode, responseHeaders, routerContext, loadContext);

expect(originalHandler).toHaveBeenCalledWith(
request,
responseStatusCode,
responseHeaders,
routerContext,
loadContext,
);
expect(result).toBe('original response');
});

test('should set span attributes when parameterized path exists and active span exists', async () => {
const originalHandler = vi.fn().mockResolvedValue('test');
const wrappedHandler = wrapSentryHandleRequest(originalHandler);

const mockActiveSpan = {};
const mockRootSpan = { setAttributes: vi.fn() };
const mockRpcMetadata = { type: RPCType.HTTP, route: '/some-path' };

(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockActiveSpan);
(getRootSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockRootSpan);
const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata);
(vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata =
getRPCMetadata;

const routerContext = {
staticHandlerContext: {
matches: [{ route: { path: 'some-path' } }],
},
} as any;

await wrappedHandler(new Request('https://nacho.queso'), 200, new Headers(), routerContext, {} as any);

expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({
[ATTR_HTTP_ROUTE]: '/some-path',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router.request-handler',
});
expect(mockRpcMetadata.route).toBe('/some-path');
});

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<typeof vi.fn>).mockReturnValue(mockActiveSpan);
(getRootSpan as unknown as ReturnType<typeof vi.fn>).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();
});

test('should not set span attributes when active span does not exist', async () => {
const originalHandler = vi.fn().mockResolvedValue('test');
const wrappedHandler = wrapSentryHandleRequest(originalHandler);

const mockRpcMetadata = { type: RPCType.HTTP, route: '/some-path' };

(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(null);

const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata);
(vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata =
getRPCMetadata;

const routerContext = {
staticHandlerContext: {
matches: [{ route: { path: 'some-path' } }],
},
} as any;

await wrappedHandler(new Request('https://tio.pepe'), 200, new Headers(), routerContext, {} as any);

expect(getRPCMetadata).not.toHaveBeenCalled();
});

describe('getMetaTagTransformer', () => {
Expand All @@ -132,7 +280,7 @@ describe('getMetaTagTransformer', () => {
);
});

test('should inject meta tags before closing head tag', done => {
test('should inject meta tags before closing head tag', () => {
const outputStream = new PassThrough();
const bodyStream = new PassThrough();
const transformer = getMetaTagTransformer(bodyStream);
Expand All @@ -145,7 +293,6 @@ describe('getMetaTagTransformer', () => {
outputStream.on('end', () => {
expect(outputData).toContain('<meta name="sentry-trace" content="test-trace-id"></head>');
expect(outputData).not.toContain('</head></head>');
done();
});

transformer.pipe(outputStream);
Expand All @@ -154,7 +301,7 @@ describe('getMetaTagTransformer', () => {
bodyStream.end();
});

test('should not modify chunks without head closing tag', done => {
test('should not modify chunks without head closing tag', () => {
const outputStream = new PassThrough();
const bodyStream = new PassThrough();
const transformer = getMetaTagTransformer(bodyStream);
Expand All @@ -167,7 +314,6 @@ describe('getMetaTagTransformer', () => {
outputStream.on('end', () => {
expect(outputData).toBe('<html><body>Test</body></html>');
expect(getTraceMetaTags).toHaveBeenCalled();
done();
});

transformer.pipe(outputStream);
Expand All @@ -176,7 +322,7 @@ describe('getMetaTagTransformer', () => {
bodyStream.end();
});

test('should handle buffer input', done => {
test('should handle buffer input', () => {
const outputStream = new PassThrough();
const bodyStream = new PassThrough();
const transformer = getMetaTagTransformer(bodyStream);
Expand All @@ -188,7 +334,6 @@ describe('getMetaTagTransformer', () => {

outputStream.on('end', () => {
expect(outputData).toContain('<meta name="sentry-trace" content="test-trace-id"></head>');
done();
});

transformer.pipe(outputStream);
Expand Down
Loading