33
44import { 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' ;
610import { withSentry } from '../src/handler' ;
711
812const MOCK_ENV = {
913 SENTRY_DSN : 'https://public@dsn.ingest.sentry.io/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