Skip to content

Commit 459be04

Browse files
committed
test: Add tests for scheduled handler
1 parent ac4c2b3 commit 459be04

File tree

3 files changed

+219
-30
lines changed

3 files changed

+219
-30
lines changed

Diff for: packages/cloudflare/src/handler.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import type {
33
ExportedHandlerFetchHandler,
44
ExportedHandlerScheduledHandler,
55
} from '@cloudflare/workers-types';
6-
import { captureException, flush, startSpan, withIsolationScope } from '@sentry/core';
6+
import {
7+
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
8+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
9+
captureException,
10+
flush,
11+
startSpan,
12+
withIsolationScope,
13+
} from '@sentry/core';
714
import { setAsyncLocalStorageAsyncContextStrategy } from './async';
815
import type { CloudflareOptions } from './client';
916
import { wrapRequestHandler } from './request';
@@ -71,6 +78,8 @@ export function withSentry<E extends ExportedHandler<any>>(
7178
'faas.cron': event.cron,
7279
'faas.time': new Date(event.scheduledTime).toISOString(),
7380
'faas.trigger': 'timer',
81+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.faas.cloudflare',
82+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task',
7483
},
7584
},
7685
async () => {

Diff for: packages/cloudflare/src/request.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ExecutionContext, IncomingRequestCfProperties, Request, Response } from '@cloudflare/workers-types';
1+
import type { ExecutionContext, IncomingRequestCfProperties } from '@cloudflare/workers-types';
22

33
import {
44
SEMANTIC_ATTRIBUTE_SENTRY_OP,

Diff for: packages/cloudflare/test/handler.test.ts

+208-28
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,221 @@
33

44
import { beforeEach, describe, expect, test, vi } from 'vitest';
55

6+
import type { ScheduledController } from '@cloudflare/workers-types';
7+
import * as SentryCore from '@sentry/core';
8+
import type { Event } from '@sentry/types';
9+
import { CloudflareClient } from '../src/client';
610
import { withSentry } from '../src/handler';
711

812
const MOCK_ENV = {
913
SENTRY_DSN: 'https://[email protected]/1337',
1014
};
1115

12-
describe('sentryPagesPlugin', () => {
16+
describe('withSentry', () => {
1317
beforeEach(() => {
1418
vi.clearAllMocks();
1519
});
1620

17-
test('gets env from handler', async () => {
18-
const handler = {
19-
fetch(_request, _env, _context) {
20-
return new Response('test');
21-
},
22-
} satisfies ExportedHandler;
21+
describe('fetch handler', () => {
22+
test('executes options callback with env', async () => {
23+
const handler = {
24+
fetch(_request, _env, _context) {
25+
return new Response('test');
26+
},
27+
} satisfies ExportedHandler<typeof MOCK_ENV>;
2328

24-
const optionsCallback = vi.fn().mockReturnValue({});
29+
const optionsCallback = vi.fn().mockReturnValue({});
2530

26-
const wrappedHandler = withSentry(optionsCallback, handler);
27-
await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
31+
const wrappedHandler = withSentry(optionsCallback, handler);
32+
await wrappedHandler.fetch(new Request('https://example.com'), MOCK_ENV, createMockExecutionContext());
2833

29-
expect(optionsCallback).toHaveBeenCalledTimes(1);
30-
expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV);
34+
expect(optionsCallback).toHaveBeenCalledTimes(1);
35+
expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV);
36+
});
37+
38+
test('passes through the handler response', async () => {
39+
const response = new Response('test');
40+
const handler = {
41+
async fetch(_request, _env, _context) {
42+
return response;
43+
},
44+
} satisfies ExportedHandler<typeof MOCK_ENV>;
45+
46+
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
47+
const result = await wrappedHandler.fetch(
48+
new Request('https://example.com'),
49+
MOCK_ENV,
50+
createMockExecutionContext(),
51+
);
52+
53+
expect(result).toBe(response);
54+
});
3155
});
3256

33-
test('passes through the response from the handler', async () => {
34-
const response = new Response('test');
35-
const handler = {
36-
async fetch(_request, _env, _context) {
37-
return response;
38-
},
39-
} satisfies ExportedHandler;
40-
41-
const wrappedHandler = withSentry(() => ({}), handler);
42-
const result = await wrappedHandler.fetch(
43-
new Request('https://example.com'),
44-
MOCK_ENV,
45-
createMockExecutionContext(),
46-
);
47-
48-
expect(result).toBe(response);
57+
describe('scheduled handler', () => {
58+
test('executes options callback with env', async () => {
59+
const handler = {
60+
scheduled(_controller, _env, _context) {
61+
return;
62+
},
63+
} satisfies ExportedHandler<typeof MOCK_ENV>;
64+
65+
const optionsCallback = vi.fn().mockReturnValue({});
66+
67+
const wrappedHandler = withSentry(optionsCallback, handler);
68+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
69+
70+
expect(optionsCallback).toHaveBeenCalledTimes(1);
71+
expect(optionsCallback).toHaveBeenLastCalledWith(MOCK_ENV);
72+
});
73+
74+
test('flushes the event after the handler is done using the cloudflare context.waitUntil', async () => {
75+
const handler = {
76+
scheduled(_controller, _env, _context) {
77+
return;
78+
},
79+
} satisfies ExportedHandler<typeof MOCK_ENV>;
80+
81+
const context = createMockExecutionContext();
82+
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
83+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, context);
84+
85+
// eslint-disable-next-line @typescript-eslint/unbound-method
86+
expect(context.waitUntil).toHaveBeenCalledTimes(1);
87+
// eslint-disable-next-line @typescript-eslint/unbound-method
88+
expect(context.waitUntil).toHaveBeenLastCalledWith(expect.any(Promise));
89+
});
90+
91+
test('creates a cloudflare client and sets it on the handler', async () => {
92+
const initAndBindSpy = vi.spyOn(SentryCore, 'initAndBind');
93+
const handler = {
94+
scheduled(_controller, _env, _context) {
95+
return;
96+
},
97+
} satisfies ExportedHandler<typeof MOCK_ENV>;
98+
99+
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
100+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
101+
102+
expect(initAndBindSpy).toHaveBeenCalledTimes(1);
103+
expect(initAndBindSpy).toHaveBeenLastCalledWith(CloudflareClient, expect.any(Object));
104+
});
105+
106+
describe('scope instrumentation', () => {
107+
test('adds cloud resource context', async () => {
108+
const handler = {
109+
scheduled(_controller, _env, _context) {
110+
SentryCore.captureMessage('cloud_resource');
111+
return;
112+
},
113+
} satisfies ExportedHandler<typeof MOCK_ENV>;
114+
115+
let sentryEvent: Event = {};
116+
const wrappedHandler = withSentry(
117+
env => ({
118+
dsn: env.SENTRY_DSN,
119+
beforeSend(event) {
120+
sentryEvent = event;
121+
return null;
122+
},
123+
}),
124+
handler,
125+
);
126+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
127+
128+
expect(sentryEvent.contexts?.cloud_resource).toEqual({ 'cloud.provider': 'cloudflare' });
129+
});
130+
});
131+
132+
describe('error instrumentation', () => {
133+
test('captures errors thrown by the handler', async () => {
134+
const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException');
135+
const error = new Error('test');
136+
137+
expect(captureExceptionSpy).not.toHaveBeenCalled();
138+
139+
const handler = {
140+
scheduled(_controller, _env, _context) {
141+
throw error;
142+
},
143+
} satisfies ExportedHandler<typeof MOCK_ENV>;
144+
145+
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
146+
try {
147+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
148+
} catch {
149+
// ignore
150+
}
151+
152+
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
153+
expect(captureExceptionSpy).toHaveBeenLastCalledWith(error, {
154+
mechanism: { handled: false, type: 'cloudflare' },
155+
});
156+
});
157+
158+
test('re-throws the error after capturing', async () => {
159+
const error = new Error('test');
160+
const handler = {
161+
scheduled(_controller, _env, _context) {
162+
throw error;
163+
},
164+
} satisfies ExportedHandler<typeof MOCK_ENV>;
165+
166+
const wrappedHandler = withSentry(env => ({ dsn: env.SENTRY_DSN }), handler);
167+
168+
let thrownError: Error | undefined;
169+
try {
170+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
171+
} catch (e: any) {
172+
thrownError = e;
173+
}
174+
175+
expect(thrownError).toBe(error);
176+
});
177+
});
178+
179+
describe('tracing instrumentation', () => {
180+
test('creates a span that wraps scheduled invocation', async () => {
181+
const handler = {
182+
scheduled(_controller, _env, _context) {
183+
return;
184+
},
185+
} satisfies ExportedHandler<typeof MOCK_ENV>;
186+
187+
let sentryEvent: Event = {};
188+
const wrappedHandler = withSentry(
189+
env => ({
190+
dsn: env.SENTRY_DSN,
191+
tracesSampleRate: 1,
192+
beforeSendTransaction(event) {
193+
sentryEvent = event;
194+
return null;
195+
},
196+
}),
197+
handler,
198+
);
199+
200+
await wrappedHandler.scheduled(createMockScheduledController(), MOCK_ENV, createMockExecutionContext());
201+
202+
expect(sentryEvent.transaction).toEqual('Scheduled Cron 0 0 0 * * *');
203+
expect(sentryEvent.spans).toHaveLength(0);
204+
expect(sentryEvent.contexts?.trace).toEqual({
205+
data: {
206+
'sentry.origin': 'auto.faas.cloudflare',
207+
'sentry.op': 'faas.cron',
208+
'faas.cron': '0 0 0 * * *',
209+
'faas.time': expect.any(String),
210+
'faas.trigger': 'timer',
211+
'sentry.sample_rate': 1,
212+
'sentry.source': 'task',
213+
},
214+
op: 'faas.cron',
215+
origin: 'auto.faas.cloudflare',
216+
span_id: expect.any(String),
217+
trace_id: expect.any(String),
218+
});
219+
});
220+
});
49221
});
50222
});
51223

@@ -55,3 +227,11 @@ function createMockExecutionContext(): ExecutionContext {
55227
passThroughOnException: vi.fn(),
56228
};
57229
}
230+
231+
function createMockScheduledController(): ScheduledController {
232+
return {
233+
scheduledTime: 123,
234+
cron: '0 0 0 * * *',
235+
noRetry: vi.fn(),
236+
};
237+
}

0 commit comments

Comments
 (0)