3
3
4
4
import { beforeEach , describe , expect , test , vi } from 'vitest' ;
5
5
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' ;
6
10
import { withSentry } from '../src/handler' ;
7
11
8
12
const MOCK_ENV = {
9
13
SENTRY_DSN :
'https://[email protected] /1337' ,
10
14
} ;
11
15
12
- describe ( 'sentryPagesPlugin ' , ( ) => {
16
+ describe ( 'withSentry ' , ( ) => {
13
17
beforeEach ( ( ) => {
14
18
vi . clearAllMocks ( ) ;
15
19
} ) ;
16
20
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 > ;
23
28
24
- const optionsCallback = vi . fn ( ) . mockReturnValue ( { } ) ;
29
+ const optionsCallback = vi . fn ( ) . mockReturnValue ( { } ) ;
25
30
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 ( ) ) ;
28
33
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
+ } ) ;
31
55
} ) ;
32
56
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
+ } ) ;
49
221
} ) ;
50
222
} ) ;
51
223
@@ -55,3 +227,11 @@ function createMockExecutionContext(): ExecutionContext {
55
227
passThroughOnException : vi . fn ( ) ,
56
228
} ;
57
229
}
230
+
231
+ function createMockScheduledController ( ) : ScheduledController {
232
+ return {
233
+ scheduledTime : 123 ,
234
+ cron : '0 0 0 * * *' ,
235
+ noRetry : vi . fn ( ) ,
236
+ } ;
237
+ }
0 commit comments