Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ export function instrumentExportedHandlerFetch<T extends ExportedHandler<any, an
new Proxy(original, {
apply(target, thisArg, args: Parameters<NonNullable<T['fetch']>>) {
const [request, env, ctx] = args;

if (request.method === 'OPTIONS' || request.method === 'HEAD') {
return target.apply(thisArg, args);
}

const context = instrumentContext(ctx);
const options = getFinalOptions(optionsCallback(env), env);
args[1] = instrumentEnv(env, options);
Expand Down Expand Up @@ -53,6 +58,10 @@ export function instrumentWorkerEntrypointFetch<T extends WorkerEntrypoint>(
apply(target, thisArg, args: [Request]) {
const [request] = args;

if (request.method === 'OPTIONS' || request.method === 'HEAD') {
return Reflect.apply(target, thisArg, args);
}

return wrapRequestHandler({ options, request, context }, () => Reflect.apply(target, thisArg, args));
},
});
Expand Down
4 changes: 4 additions & 0 deletions packages/cloudflare/src/pages-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export function sentryPagesPlugin<
): PagesPluginFunction<Env, Params, Data, PluginParams> {
setAsyncLocalStorageAsyncContextStrategy();
return context => {
if (context.request.method === 'OPTIONS' || context.request.method === 'HEAD') {
return context.next();
}

const options = typeof handlerOrOptions === 'function' ? handlerOrOptions(context) : handlerOrOptions;
return wrapRequestHandler({ options, request: context.request, context: { ...context, props: {} } }, () =>
context.next(),
Expand Down
14 changes: 0 additions & 14 deletions packages/cloudflare/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,20 +85,6 @@ export function wrapRequestHandler(
}
}

// Do not capture spans for OPTIONS and HEAD requests
if (request.method === 'OPTIONS' || request.method === 'HEAD') {
try {
return await handler();
} catch (e) {
if (captureErrors) {
captureException(e, { mechanism: { handled: false, type: 'auto.http.cloudflare' } });
}
throw e;
} finally {
Comment thread
JPeer264 marked this conversation as resolved.
waitUntil?.(flushAndDispose(client));
}
}

if (client) {
Comment thread
JPeer264 marked this conversation as resolved.
await captureIncomingRequestBody(client, request);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,85 @@ describe('instrumentFetch', () => {
await Promise.all(waits);
expect(flush).toHaveBeenCalledOnce();
});

test('OPTIONS requests bypass SDK instrumentation', async () => {
const originalFetch = vi.fn().mockReturnValue(new Response('options response'));
const handler = {
fetch: originalFetch,
} satisfies ExportedHandler<typeof MOCK_ENV>;

const optionsCallback = vi.fn().mockReturnValue({ dsn: MOCK_ENV.SENTRY_DSN });

const wrappedHandler = withSentry(optionsCallback, handler);
const result = await wrappedHandler.fetch?.(
new Request('https://example.com', { method: 'OPTIONS' }),
MOCK_ENV,
createMockExecutionContext(),
);

expect(originalFetch).toHaveBeenCalledTimes(1);
expect(optionsCallback).not.toHaveBeenCalled();
expect(result?.status).toBe(200);
if (result) {
expect(await result.text()).toBe('options response');
}
});

test('HEAD requests bypass SDK instrumentation', async () => {
const originalFetch = vi.fn().mockReturnValue(new Response('head response'));
const handler = {
fetch: originalFetch,
} satisfies ExportedHandler<typeof MOCK_ENV>;

const optionsCallback = vi.fn().mockReturnValue({ dsn: MOCK_ENV.SENTRY_DSN });

const wrappedHandler = withSentry(optionsCallback, handler);
const result = await wrappedHandler.fetch?.(
new Request('https://example.com', { method: 'HEAD' }),
MOCK_ENV,
createMockExecutionContext(),
);

expect(originalFetch).toHaveBeenCalledTimes(1);
expect(optionsCallback).not.toHaveBeenCalled();
expect(result?.status).toBe(200);
});

test('GET requests are instrumented', async () => {
const originalFetch = vi.fn().mockReturnValue(new Response('get response'));
const handler = {
fetch: originalFetch,
} satisfies ExportedHandler<typeof MOCK_ENV>;

const optionsCallback = vi.fn().mockReturnValue({ dsn: MOCK_ENV.SENTRY_DSN });

const wrappedHandler = withSentry(optionsCallback, handler);
await wrappedHandler.fetch?.(
new Request('https://example.com', { method: 'GET' }),
MOCK_ENV,
createMockExecutionContext(),
);

expect(originalFetch).toHaveBeenCalledTimes(1);
expect(optionsCallback).toHaveBeenCalledTimes(1);
});

test('POST requests are instrumented', async () => {
const originalFetch = vi.fn().mockReturnValue(new Response('post response'));
const handler = {
fetch: originalFetch,
} satisfies ExportedHandler<typeof MOCK_ENV>;

const optionsCallback = vi.fn().mockReturnValue({ dsn: MOCK_ENV.SENTRY_DSN });

const wrappedHandler = withSentry(optionsCallback, handler);
await wrappedHandler.fetch?.(
new Request('https://example.com', { method: 'POST' }),
MOCK_ENV,
createMockExecutionContext(),
);

expect(originalFetch).toHaveBeenCalledTimes(1);
expect(optionsCallback).toHaveBeenCalledTimes(1);
});
});
64 changes: 64 additions & 0 deletions packages/cloudflare/test/pages-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,68 @@ describe('sentryPagesPlugin', () => {
expect(result.status).toBe(response.status);
expect(await result.text()).toBe('test');
});

test('OPTIONS requests bypass SDK instrumentation', async () => {
const mockOptionsHandler = vi.fn().mockReturnValue(MOCK_OPTIONS);
const mockOnRequest = sentryPagesPlugin(mockOptionsHandler);
const mockNext = vi.fn().mockResolvedValue(new Response('options response'));

const result = await mockOnRequest({
request: new Request('https://example.com', { method: 'OPTIONS' }),
functionPath: 'test',
waitUntil: vi.fn(),
passThroughOnException: vi.fn(),
next: mockNext,
env: { ASSETS: { fetch: vi.fn() } },
params: {},
data: {},
pluginArgs: MOCK_OPTIONS,
});

expect(mockOptionsHandler).not.toHaveBeenCalled();
expect(mockNext).toHaveBeenCalledTimes(1);
expect(result.status).toBe(200);
});

test('HEAD requests bypass SDK instrumentation', async () => {
const mockOptionsHandler = vi.fn().mockReturnValue(MOCK_OPTIONS);
const mockOnRequest = sentryPagesPlugin(mockOptionsHandler);
const mockNext = vi.fn().mockResolvedValue(new Response('head response'));

const result = await mockOnRequest({
request: new Request('https://example.com', { method: 'HEAD' }),
functionPath: 'test',
waitUntil: vi.fn(),
passThroughOnException: vi.fn(),
next: mockNext,
env: { ASSETS: { fetch: vi.fn() } },
params: {},
data: {},
pluginArgs: MOCK_OPTIONS,
});

expect(mockOptionsHandler).not.toHaveBeenCalled();
expect(mockNext).toHaveBeenCalledTimes(1);
expect(result.status).toBe(200);
});

test('GET requests are instrumented', async () => {
const mockOptionsHandler = vi.fn().mockReturnValue(MOCK_OPTIONS);
const mockOnRequest = sentryPagesPlugin(mockOptionsHandler);
const mockNext = vi.fn().mockResolvedValue(new Response('get response'));

await mockOnRequest({
request: new Request('https://example.com', { method: 'GET' }),
functionPath: 'test',
waitUntil: vi.fn(),
passThroughOnException: vi.fn(),
next: mockNext,
env: { ASSETS: { fetch: vi.fn() } },
params: {},
data: {},
pluginArgs: MOCK_OPTIONS,
});

expect(mockOptionsHandler).toHaveBeenCalledTimes(1);
});
});
54 changes: 0 additions & 54 deletions packages/cloudflare/test/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,60 +563,6 @@ describe('flushAndDispose', () => {
disposeSpy.mockRestore();
});

test('dispose is called for OPTIONS requests', async () => {
const context = createMockExecutionContext();
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
(context as any).waitUntil = waitUntil;

const disposeSpy = vi.spyOn(CloudflareClient.prototype, 'dispose');
const flushSpy = vi.spyOn(SentryCore.Client.prototype, 'flush').mockResolvedValue(true);

await wrapRequestHandler(
{
options: MOCK_OPTIONS,
request: new Request('https://example.com', { method: 'OPTIONS' }),
context,
},
() => new Response('', { status: 200 }),
);

// Wait for all waitUntil promises to resolve
await Promise.all(waits);

expect(disposeSpy).toHaveBeenCalled();

flushSpy.mockRestore();
disposeSpy.mockRestore();
});

test('dispose is called for HEAD requests', async () => {
const context = createMockExecutionContext();
const waits: Promise<unknown>[] = [];
const waitUntil = vi.fn(promise => waits.push(promise));
(context as any).waitUntil = waitUntil;

const disposeSpy = vi.spyOn(CloudflareClient.prototype, 'dispose');
const flushSpy = vi.spyOn(SentryCore.Client.prototype, 'flush').mockResolvedValue(true);

await wrapRequestHandler(
{
options: MOCK_OPTIONS,
request: new Request('https://example.com', { method: 'HEAD' }),
context,
},
() => new Response('', { status: 200 }),
);

// Wait for all waitUntil promises to resolve
await Promise.all(waits);

expect(disposeSpy).toHaveBeenCalled();

flushSpy.mockRestore();
disposeSpy.mockRestore();
});

test('dispose is called after streaming response completes', async () => {
const context = createMockExecutionContext();
const waits: Promise<unknown>[] = [];
Expand Down
Loading