/**
 * Sentry client for Cloudflare Workers.
 * Adheres to https://docs.sentry.io/development/sdk-dev/overview/
 */ import { User , Request , Stacktrace , StackFrame , Extra , Extras , Primitive , } from '@sentry/types' ; import { Options , Event , Breadcrumb , Level , RewriteFrames } from './types' ; import { API } from '@sentry/core' ; import { isError , isPlainObject , extractExceptionKeysForMessage , normalizeToSize , } from '@sentry/utils' ; import { v4 as uuidv4 } from 'uuid' ; import { parse } from 'cookie' ; import { fromError } from 'stacktrace-js' ; import { Scope } from './scope' ; export default class Toucan { /**
     * Default maximum number of breadcrumbs added to an event. Can be overwritten
     * with  Options . maxBreadcrumbs .
      private readonly DEFAULT_BREADCRUMBS = 100 ; /**
     * Absolute maximum number of breadcrumbs added to an event. The
     * `maxBreadcrumbs` option cannot be higher than this value.
     */ private readonly MAX_BREADCRUMBS = 100 ; /**
     * Is a  Scope []
      private scopes : Scope [ ] ; /**
     * If an empty DSN is passed, we should treat it as valid option which signifies disabling the SDK.
     */ private disabled : boolean ; /**
     * Options passed to constructor. See Options type.
     */ private options : Options ; /**
     * Full store endpoint with auth search params. Parsed from options.dsn.
     */ private url : string ; /**
     * Sentry request object transformed from incoming event.request.
     */ private request : Request | undefined ; constructor ( options ) { this . scopes = [ new Scope ( ) ] ; this . options = options ; if ( ! options . dsn || options . dsn . length === 0 ) { this . url = '' ; this . disabled = true ; this . debug ( ( ) => this . log ( 'dsn missing, SDK is disabled' ) ) ; } else { this . url = new API ( options . dsn ) . getStoreEndpointWithUrlEncodedAuth ( ) ; this . disabled = false ; this . debug ( ( ) => this . log ( `dsn parsed, full store endpoint: ${ this . url }` ) ) ; } if ( 'context' in options && options . context . request ) { this . request = this . toSentryRequest ( options . context . request ) ; } else if ( 'request' in options && options . request ) { this . request = this . toSentryRequest ( options . request ) ; } else if ( 'event' in options && 'request' in options . event ) { this . request = this . toSentryRequest ( options . event . request ) ; } this . beforeSend = this . beforeSend . bind ( this ) ; /**
         * Wrap all class methods in a proxy that:
         * 1. Wraps all code in try/catch to handle internal erros gracefully.
         * 2. Prevents execution if disabled = true
         */ return new Proxy ( this , { get : ( target , key , receiver ) => { return ( ... args ) => { if ( this . disabled ) return ; try { return Reflect . get ( target , key , receiver ) . apply ( target , args ) ; } catch ( err ) { this . debug ( ( ) => this . error ( err ) ) ; } } ; } , } ) ; } param key param extra setExtra ( key , extra ) { this . getScope ( ) . setExtra ( key , extra ) ; } param extras setExtras ( extras ) { this . getScope ( ) . setExtras ( extras ) ; } param key param value setTag ( key , value ) { this . getScope ( ) . setTag ( key , value ) ; } param tags setTags ( tags ) { this . getScope ( ) . setTags ( tags ) ; } param fingerprint setFingerprint ( fingerprint ) { this . getScope ( ) . setFingerprint ( fingerprint ) ; } param breadcrumb addBreadcrumb ( breadcrumb ) { const maxBreadcrumbs = this . options . maxBreadcrumbs ?? this . DEFAULT_BREADCRUMBS ; const numberOfBreadcrumbs = Math . min ( maxBreadcrumbs , this . MAX_BREADCRUMBS ) ; if ( numberOfBreadcrumbs <= 0 ) return ; if ( ! breadcrumb . timestamp ) { breadcrumb . timestamp = this . timestamp ( ) ; } this . getScope ( ) . addBreadcrumb ( breadcrumb , numberOfBreadcrumbs ) ; } param exception returns captureException ( exception ) { this . debug ( ( ) => this . log ( `calling captureException` ) ) ; const event = this . buildEvent ( { } ) ; if ( ! event ) return ; if ( 'context' in this . options ) { this . options . context . waitUntil ( this . reportException ( event , exception ) ) ; } else if ( 'event' in this . options ) { this . options . event . waitUntil ( this . reportException ( event , exception ) ) ; } else { this . reportException ( event , exception ) ; } return event . event_id ; } param message param level returns captureMessage ( message , level = 'info' ) { this . debug ( ( ) => this . log ( `calling captureMessage` ) ) ; const event = this . buildEvent ( { level , message } ) ; if ( ! event ) return ; if ( 'context' in this . options ) { this . options . context . waitUntil ( this . reportMessage ( event ) ) ; } else if ( 'event' in this . options ) { this . options . event . waitUntil ( this . reportMessage ( event ) ) ; } else { this . reportMessage ( event ) ; } return event . event_id ; } param user setUser ( user ) { this . getScope ( ) . setUser ( user ) ; } param body setRequestBody ( body ) { if ( this . request ) { this . request . data = body ; } else { this . request = { data : body } ; } } example withScope ( callback ) { const scope = this . pushScope ( ) ; try { callback ( scope ) ; } finally { this . popScope ( ) ; } } param data private async postEvent ( data ) { let headers = { 'Content-Type' : 'application/json' , 'User-Agent' : '__name__/__version__' , } ; if ( this . options ?. transportOptions ?. headers ) { headers = { ... headers , ... this . options . transportOptions . headers , } ; } const body = JSON . stringify ( data ) ; this . debug ( ( ) => { this . log ( `sending request to Sentry with headers: ${ JSON . stringify ( headers ) } and body: ${ body }` ) ; } ) ; const response = await fetch ( this . url , { method : 'POST' , body , headers , } ) ; await this . debug ( ( ) => { return this . logResponse ( response ) ; } ) ; return response ; } param additionalData returns private buildEvent ( additionalData ) { const sampleRate = this . options . sampleRate ; if ( typeof sampleRate === 'number' && Math . random ( ) > sampleRate ) { this . debug ( ( ) => this . log ( `skipping this event (sampleRate === ${ sampleRate })` ) ) ; return ; } const pkg = this . options . pkg ; const release = this . options . release ? this . options . release : pkg ? `${ pkg . name }-${ pkg . version }` : undefined ; const scope = this . getScope ( ) ; const payload = { event_id : uuidv4 ( ) . replace ( /-/g , '' ) , logger : 'EdgeWorker' , platform : 'node' , release , environment : this . options . environment , timestamp : this . timestamp ( ) , level : 'error' , modules : pkg ? { ... pkg . dependencies , ... pkg . devDependencies , } : undefined , ... additionalData , request : this . request , sdk : { name : '__name__' , version : '__version__' , } , } ; scope . applyToEvent ( payload ) ; const beforeSend = this . options . beforeSend ?? this . beforeSend ; return beforeSend ( payload ) ; } param request returns private toSentryRequest ( request ) { const cookieString = request . headers . get ( 'cookie' ) ; let cookies = undefined ; if ( cookieString ) { try { cookies = parse ( cookieString ) ; } catch ( e ) { } } const headers = { } ; for ( const [ k , v ] of ( request . headers as any ) . entries ( ) ) { if ( k !== 'cookie' ) { headers [ k ] = v ; } } const rv = { method : request . method , cookies , headers , } ; try { const url = new URL ( request . url ) ; rv . url = `${ url . protocol }//${ url . hostname }${ url . pathname }` ; rv . query_string = url . search ; } catch ( e ) { const qi = request . url . indexOf ( '?' ) ; if ( qi < 0 ) { rv . url = request . url ; } else { rv . url = request . url . substr ( 0 , qi ) ; rv . query_string = request . url . substr ( qi + 1 ) ; } } return rv ; } param event returns private beforeSend ( event ) { const request = event . request ; if ( request ) { const allowedHeaders = this . options . allowedHeaders ; const allowedCookies = this . options . allowedCookies ; const allowedSearchParams = this . options . allowedSearchParams ; if ( allowedHeaders ) { request . headers = this . applyAllowlist ( request . headers , allowedHeaders ) ; } else { delete request . headers ; } if ( allowedCookies ) { request . cookies = this . applyAllowlist ( request . cookies , allowedCookies ) ; } else { delete request . cookies ; } if ( allowedSearchParams ) { const params = Object . fromEntries ( new URLSearchParams ( request . query_string ) as any ) ; const allowedParams = new URLSearchParams ( ) ; Object . keys ( this . applyAllowlist ( params , allowedSearchParams ) ) . forEach ( ( allowedKey ) => { allowedParams . set ( allowedKey , params [ allowedKey ] ) ; } ) ; request . query_string = allowedParams . toString ( ) ; } else { delete request . query_string ; } } event . request = request ; return event ; } param obj param allowlist returns private applyAllowlist ( obj = { } , allowlist ) { let predicate = ( item ) => false ; if ( allowlist instanceof RegExp ) { predicate = ( item ) => allowlist . test ( item ) ; } else if ( Array . isArray ( allowlist ) ) { const allowlistLowercased = allowlist . map ( ( item ) => item . toLowerCase ( ) ) ; predicate = ( item ) => allowlistLowercased . includes ( item ) ; } else { this . debug ( ( ) => this . warn ( 'allowlist must be an array of strings, or a regular expression.' ) ) ; return { } ; } return Object . keys ( obj ) . map ( ( key ) => key . toLowerCase ( ) ) . filter ( ( key ) => predicate ( key ) ) . reduce < Record < string , string > > ( ( allowed , key ) => { allowed [ key ] = obj [ key ] ; return allowed ; } , { } ) ; } /**
     * A number representing the seconds elapsed since the UNIX epoch.
     */ private timestamp ( ) { return Date . now ( ) / 1000 ; } param event private async reportMessage ( event ) { return this . postEvent ( event ) ; } param event param error private async reportException ( event , maybeError ) { let error ; if ( isError ( maybeError ) ) { error = maybeError as Error ; } else if ( isPlainObject ( maybeError ) ) { const message = `Non-Error exception captured with keys: ${ extractExceptionKeysForMessage ( maybeError ) }` ; this . setExtra ( '__serialized__' , normalizeToSize ( maybeError as { } ) ) ; error = new Error ( message ) ; } else { error = new Error ( maybeError as string ) ; } const stacktrace = await this . buildStackTrace ( error ) ; event . exception = { values : [ { type : error . name , value : error . message , stacktrace } ] , } ; return this . postEvent ( event ) ; } param error returns private async buildStackTrace ( error ) { if ( this . options . attachStacktrace === false ) { return undefined ; } try { const stack = await fromError ( error ) ; /**
             * sentry-cli and webpack-sentry-plugin upload the source-maps named by their path with a ~/ prefix.
             * Lets adhere to this behavior.
             */ const rewriteFrames = this . options . rewriteFrames ?? { root : '~/' , iteratee : ( frame ) => frame , } ; return { frames : stack . map < StackFrame > ( ( frame ) => { const filename = frame . fileName ?? '' ; const stackFrame = { colno : frame . columnNumber , lineno : frame . lineNumber , filename , function : frame . functionName , } ; if ( ! ! rewriteFrames . root ) { stackFrame . filename = `${ rewriteFrames . root }${ stackFrame . filename }` ; } return ! ! rewriteFrames . iteratee ? rewriteFrames . iteratee ( stackFrame ) : stackFrame ; } ) . reverse ( ) , } ; } catch ( e ) { return undefined ; } } param callback private debug < ReturnType > ( callback ) { if ( this . options . debug ) { return callback ( ) ; } } private log ( message ) { console . log ( `toucan-js: ${ message }` ) ; } private warn ( message ) { console . warn ( `toucan-js: ${ message }` ) ; } private error ( message ) { console . error ( `toucan-js: ${ message }` ) ; } param response private async logResponse ( response ) { let responseText = '' ; try { responseText = await response . text ( ) ; } catch ( e ) { responseText += '' ; } let origin = 'Sentry' ; try { const originUrl = new URL ( response . url ) ; origin = originUrl . origin ; } catch ( e ) { origin = response . url ?? 'Sentry' ; } const msg = `${ origin } responded with [${ response . status } ${ response . statusText }]: ${ responseText }` ; if ( response . ok ) { this . log ( msg ) ; } else { this . error ( msg ) ; } } /** Returns the scope of the top stack. */ private getScope ( ) { return this . scopes [ this . scopes . length - 1 ] ; } /**
     * Create a new scope to store context information.
     * The scope will be layered on top of the current one. It is isolated, i.e. all breadcrumbs and context information added to this scope will be removed once the scope ends.  * Be sure to always remove this scope with  this . popScope when the operation finishes or throws.
      private pushScope ( ) { const scope = Scope . clone ( this . getScope ( ) ) ; this . scopes . push ( scope ) ; return scope ; } /**
     * Removes a previously pushed scope from the stack.
     * This restores the state before the scope was pushed. All breadcrumbs and context information added since the last call to  this . pushScope are discarded.
      private popScope ( ) { if ( this . scopes . length <= 1 ) return false ; return ! ! this . scopes . pop ( ) ; } }