1- import {
2- type Event ,
3- type SeverityLevel ,
4- captureEvent ,
5- } from '@sentry/cloudflare' ;
1+ import { processTraceItem } from './utils/processTailItem' ;
62
73export type SentryTailWorkerOptions = {
84 samplingRate : number ;
@@ -18,287 +14,3 @@ export function createSentryTail<Env = unknown>(
1814 }
1915 } ;
2016}
21-
22- function processTraceItem (
23- options : SentryTailWorkerOptions ,
24- item : TraceItem
25- ) : void {
26- const severityLevel = determineSeverityLevel ( item ) ;
27- if ( ! severityLevel ) {
28- // Not an error
29- return ;
30- }
31-
32- if (
33- options . samplingRate !== 1 &&
34- ! shouldSampleTraceItem ( options . samplingRate )
35- ) {
36- return ;
37- }
38-
39- const event : Event = {
40- level : severityLevel ,
41- timestamp : item . eventTimestamp ?? Date . now ( ) ,
42- logger : '@node-core/cloudflare-sentry-tail' ,
43- message : workerOutcomeToEventMessage ( item . outcome ) ,
44- fingerprint : [ ] ,
45- breadcrumbs : [ ] ,
46- exception : {
47- values : [ ] ,
48- } ,
49- tags : {
50- outcome : item . outcome ,
51- script_name : item . scriptName ,
52- script_version : item . scriptVersion ?. tag ,
53- cpu_time : item . cpuTime ,
54- wall_time : item . wallTime ,
55- } ,
56- } ;
57-
58- // Populate data specific to the type of trace event we got
59- handleTraceItemEvent ( options , item , event ) ;
60-
61- // Populate breadcrumbs with any relevant data
62- addRemainingBreadcrumbs ( item , event ) ;
63-
64- // Sort breadcrumbs by their timestamps
65- event . breadcrumbs ?. sort ( ( a , b ) => {
66- if ( ! a . timestamp || ! b . timestamp ) {
67- return 0 ;
68- }
69-
70- return a . timestamp - b . timestamp ;
71- } ) ;
72-
73- captureEvent ( event ) ;
74- }
75-
76- function determineSeverityLevel ( item : TraceItem ) : SeverityLevel | undefined {
77- // Two scenarios where we want to report back to Sentry:
78- // 1. Trace item outcome isn't 'ok'
79- // 2. We have a status code >= 500
80- //
81- // Note that outcome is determined by if the worker executed to completion,
82- // not if it returned a successful status code
83-
84- if ( item . outcome === 'ok' ) {
85- const response =
86- item . event && 'response' in item . event ? item . event . response : undefined ;
87-
88- if ( response ?. status && response ?. status >= 500 ) {
89- return 'error' ;
90- } else {
91- // Don't care
92- return undefined ;
93- }
94- }
95-
96- return workerOutcomeToSeverityLevel ( item . outcome ) ;
97- }
98-
99- /**
100- * Determines what kind of trace item event we received and adds any
101- * event-specific properties to the Sentry event to be reported.
102- */
103- function handleTraceItemEvent (
104- options : SentryTailWorkerOptions ,
105- item : TraceItem ,
106- sentryEvent : Event
107- ) : void {
108- if ( ! item . event ) {
109- return ;
110- }
111-
112- if ( 'request' in item . event ) {
113- const request = item . event . request ;
114- const response = item . event . response ;
115-
116- const redactedHeaders : Record < string , string > = { } ;
117- for ( let [ key , value ] of Object . entries ( request . headers ) ) {
118- key = key . toLowerCase ( ) ;
119-
120- if ( options . headersToRedact && options . headersToRedact . includes ( key ) ) {
121- value = 'redacted' ;
122- }
123-
124- redactedHeaders [ key ] = value ;
125- }
126-
127- sentryEvent . request = {
128- method : request . method ,
129- url : request . url ,
130- headers : redactedHeaders ,
131- env : {
132- asn : request . cf ?. asn ,
133- colo : request . cf ?. colo ,
134- continent : request . cf ?. continent ,
135- country : request . cf ?. country ,
136- timezone : request . cf ?. timezone ,
137- httpProtocol : request . cf ?. httpProtocol ,
138- requestPriority : request . cf ?. requestPriority ,
139- tlsCipher : request . cf ?. tlsCipher ,
140- tlsClientAuth : request . cf ?. tlsClientAuth ,
141- tlsExportedAuthenticator : request . cf ?. tlsExportedAuthenticator ,
142- tlsVersion : request . cf ?. tlsVersion ,
143- } ,
144- } ;
145-
146- const responseStatusCode = response ?. status ?? 'Unknown' ;
147-
148- sentryEvent . message = response
149- ? `${ responseStatusCode } Response`
150- : 'No response' ;
151-
152- sentryEvent . breadcrumbs ?. push ( {
153- type : 'http' ,
154- category : 'request' ,
155- timestamp : item . eventTimestamp ?? Date . now ( ) ,
156- data : {
157- url : request . url ,
158- method : request . method ,
159- status_code : responseStatusCode ,
160- } ,
161- } ) ;
162-
163- const requestUrl = new URL ( request . url ) ;
164- sentryEvent . fingerprint ?. push (
165- requestUrl . origin ,
166- requestUrl . pathname ,
167- request . method ,
168- `${ responseStatusCode } `
169- ) ;
170-
171- sentryEvent . tags ! . event = 'fetch' ;
172- sentryEvent . tags ! . ray_id = redactedHeaders [ 'cf-ray' ] ;
173- } else if ( 'rpcMethod' in item . event ) {
174- sentryEvent . tags ! . event = 'js-rpc' ;
175- sentryEvent . tags ! . rpc_method = item . event . rpcMethod ;
176- } else if ( 'scheduledTime' in item . event ) {
177- if ( 'cron' in item . event ) {
178- sentryEvent . tags ! . event = 'scheduled' ;
179- sentryEvent . tags ! . scheduled_time = item . event . scheduledTime ;
180- sentryEvent . tags ! . cron = item . event . cron ;
181- return ;
182- }
183-
184- sentryEvent . tags ! . event = 'alarm' ;
185- sentryEvent . tags ! . scheduled_time = item . event . scheduledTime . toUTCString ( ) ;
186- } else if ( 'queue' in item . event ) {
187- sentryEvent . tags ! . event = 'queue' ;
188- sentryEvent . tags ! . queue = item . event . queue ;
189- sentryEvent . tags ! . batchSize = item . event . batchSize ;
190- } else if ( 'mailFrom' in item . event ) {
191- sentryEvent . tags ! . event = 'email' ;
192- sentryEvent . tags ! . rawSize = item . event . rawSize ;
193- }
194- }
195-
196- function addRemainingBreadcrumbs ( item : TraceItem , sentryEvent : Event ) {
197- if ( ! sentryEvent . breadcrumbs ) {
198- return ;
199- }
200-
201- let breadcrumbsIdx = sentryEvent . breadcrumbs . length ;
202-
203- // Allocate space for the elements we're gonna add
204- sentryEvent . breadcrumbs . length +=
205- item . logs . length +
206- item . diagnosticsChannelEvents . length +
207- item . exceptions . length ;
208-
209- for ( const log of item . logs ) {
210- sentryEvent . breadcrumbs [ breadcrumbsIdx ++ ] = {
211- type : 'debug' ,
212- category : `console.${ log . level } ` ,
213- message : consoleLogToString ( log . message ) ,
214- level : consoleLogLevelToSentryLevel ( log . level ) ,
215- timestamp : log . timestamp ,
216- } ;
217- }
218-
219- for ( const payload of item . diagnosticsChannelEvents ) {
220- sentryEvent . breadcrumbs [ breadcrumbsIdx ++ ] = {
221- type : 'debug' ,
222- category : `channel.${ payload . channel } ` ,
223- message : consoleLogToString ( payload . message ) ,
224- level : 'debug' ,
225- timestamp : payload . timestamp ,
226- } ;
227- }
228-
229- let fingerprintIdx = sentryEvent . fingerprint ! . length ;
230- let exceptionValueIdx = sentryEvent . exception ! . values ! . length ;
231-
232- sentryEvent . fingerprint ! . length += item . exceptions . length ;
233- sentryEvent . exception ! . values ! . length += item . exceptions . length ;
234-
235- for ( const exception of item . exceptions ) {
236- sentryEvent . breadcrumbs [ breadcrumbsIdx ++ ] = {
237- type : 'error' ,
238- level : 'error' ,
239- category : exception . name ,
240- message : exception . message ,
241- timestamp : exception . timestamp ,
242- data : {
243- stack : exception . stack ,
244- } ,
245- } ;
246-
247- sentryEvent . fingerprint ! [ fingerprintIdx ++ ] = exception . name ;
248- sentryEvent . exception ! . values ! [ exceptionValueIdx ++ ] = {
249- type : exception . name ,
250- value : exception . message ,
251- } ;
252- }
253- }
254-
255- function shouldSampleTraceItem ( sampleRate : number ) {
256- const buffer = new Uint32Array ( 1 ) ;
257- crypto . getRandomValues ( buffer ) ;
258-
259- const random = buffer [ 0 ] / 4294967295 ;
260-
261- return random <= sampleRate ;
262- }
263-
264- function workerOutcomeToSeverityLevel ( outcome : string ) : SeverityLevel {
265- const map : Record < string , SeverityLevel > = {
266- exceededCpu : 'fatal' ,
267- exceededMemory : 'fatal' ,
268- exception : 'error' ,
269- ok : 'info' ,
270- } ;
271-
272- return map [ outcome ] ?? 'warning' ;
273- }
274-
275- function workerOutcomeToEventMessage ( outcome : string ) : string {
276- const map : Record < string , string > = {
277- exceededCpu : 'Exceeded CPU' ,
278- exceededMemory : 'Exceeded Memory' ,
279- exception : 'Script Threw Exception' ,
280- canceled : 'Client Disconnected' ,
281- ok : 'Success' ,
282- } ;
283-
284- return map [ outcome ] ?? 'Internal' ;
285- }
286-
287- function consoleLogLevelToSentryLevel ( logLevel : string ) : SeverityLevel {
288- const map : Record < string , SeverityLevel > = {
289- debug : 'debug' ,
290- log : 'info' ,
291- error : 'error' ,
292- warn : 'warning' ,
293- trace : 'debug' ,
294- } ;
295-
296- return map [ logLevel ] ?? 'debug' ;
297- }
298-
299- function consoleLogToString ( logMessage : unknown ) : string {
300- const pieces = Array . isArray ( logMessage ) ? logMessage : [ logMessage ] ;
301- return pieces
302- . map ( p => ( typeof p === 'string' ? p : JSON . stringify ( p ) ) )
303- . join ( ', ' ) ;
304- }
0 commit comments