import * as Integrations from "@sentry/integrations" ; import * as Sentry from "@sentry/node" ; import { getErrorSuffix , ZWaveError , ZWaveErrorCodes } from "@zwave-js/core" ; import { randomBytes } from "crypto" ; import * as fs from "fs-extra" ; import * as path from "path" ; const pathWhitelists = [ "node_modules/iobroker.zwave2" ] ; const pathBlacklists = [ "node_modules/@serialport" ] ; const pathBlacklistsExact = [ "repl.js" ] ; function isZWaveError ( err ) { if ( ! err || typeof err === "string" ) return false ; return "code" in err && typeof ( err as any ) . code === "number" ; } internal export interface SentryContext { isPartOfThisLib ( filename ) : boolean ; shouldIgnore ( event , hint ? ) : boolean ; getFingerprint ( ) : Promise < string > ; } const errorMessageTests = ( ( ) => { const tests = [ ] ; tests . push ( ( msg ) => { for ( const code of [ ZWaveErrorCodes . Controller_MessageDropped , ZWaveErrorCodes . RemoveFailedNode_Failed , ZWaveErrorCodes . RemoveFailedNode_NodeOK , ZWaveErrorCodes . Controller_InclusionFailed , ZWaveErrorCodes . Controller_ExclusionFailed , ZWaveErrorCodes . Driver_NoErrorHandler , ZWaveErrorCodes . Driver_InvalidOptions , ] as const ) { if ( msg . includes ( getErrorSuffix ( code ) ) ) { return true ; } } } ) ; tests . push ( ( msg ) => msg . includes ( getErrorSuffix ( ZWaveErrorCodes . CC_NotSupported ) ) && /does not support.+associations/ . test ( msg ) ) ; tests . push ( ( msg ) => /(no such file|permission denied|cannot open|file not found)/i . test ( msg ) && /(\/dev\/|\/mqtt\/|COM\d+|Select Port)/i . test ( msg ) ) ; tests . push ( ( msg ) => /(EROFS|ENODEV|ENOSPC)/i . test ( msg ) && /(read-only file system|no such device|no space left)/i . test ( msg ) ) ; tests . push ( ( msg ) => /unknown system error/i . test ( msg ) ) ; tests . push ( ( msg ) => /custom baud rate/i . test ( msg ) ) ; tests . push ( ( msg ) => /bindings\.node/i . test ( msg ) ) ; return tests ; } ) ( ) ; export function createSentryContext ( libraryRootDir ) { /** Checks if a filename is part of this library. Paths outside will be excluded from Sentry error reporting */ function isPartOfThisLib ( filename ) { const relative = path . relative ( libraryRootDir , filename ) ; return ( ! ! relative && ! relative . startsWith ( ".." ) && ! path . isAbsolute ( relative ) ) ; } /** Creates a new fingerprint or retrieves a previously-generated one */ async function getFingerprint ( ) { const fingerprintPath = path . join ( libraryRootDir , "fingerprint.txt" ) ; let fingerprint ; if ( await fs . pathExists ( fingerprintPath ) ) { fingerprint = await fs . readFile ( fingerprintPath , "utf8" ) ; } if ( ! fingerprint || fingerprint . length < 8 ) { fingerprint = randomBytes ( 8 ) . toString ( "hex" ) ; try { await fs . writeFile ( fingerprintPath , fingerprint , "utf8" ) ; } catch { } } return fingerprint ; } /** Returns whether any line in the given stacktrace is whitelisted and none is blacklisted */ function anyWhitelisted ( filenames ) { const normalizedFilenames = filenames . map ( ( f ) => path . normalize ( f ) ) ; const normalizedWhitelists = pathWhitelists . map ( ( w ) => path . normalize ( w ) ) ; const normalizedBlacklists = pathBlacklists . map ( ( b ) => path . normalize ( b ) ) ; return ( normalizedFilenames . some ( ( f ) => normalizedWhitelists . some ( ( w ) => f . includes ( w ) ) ) && ! normalizedFilenames . some ( ( f ) => normalizedBlacklists . some ( ( b ) => f . includes ( b ) ) ) ) ; } /** Returns whether any line in the given stacktrace is blacklisted */ function anyBlacklistedExact ( filenames ) { const normalizedFilenames = filenames . map ( ( f ) => path . normalize ( f ) ) ; const normalizedBlacklistsExact = pathBlacklistsExact . map ( ( b ) => path . normalize ( b ) ) ; return normalizedFilenames . some ( ( f ) => normalizedBlacklistsExact . some ( ( b ) => f === b ) ) ; } /** Returns whether the given Sentry event should be ignored */ function shouldIgnore ( event , hint ? ) { const filenames = event . exception ?. values ?. [ 0 ] ?. stacktrace ?. frames ?. map ( ( f ) => f . filename ) ?. filter ( ( f ) : f is string => ! ! f && ! f . startsWith ( "internal/" ) ) ?? [ ] ; if ( ! filenames . some ( ( f ) => isPartOfThisLib ( f ) ) ) { return ! anyWhitelisted ( filenames ) ; } if ( anyBlacklistedExact ( filenames ) ) { return true ; } let ignore = false ; const culprit = filenames [ filenames . length - 1 ] ; const culpritIsPartOfThisLib = isPartOfThisLib ( culprit ) ; if ( ! culpritIsPartOfThisLib ) ignore = true ; if ( ! ignore && culpritIsPartOfThisLib && hint ) { if ( hint . originalException ) { try { const msg = hint . originalException . toString ( ) ; if ( errorMessageTests . some ( ( test ) => test ( msg ) ) ) { ignore = true ; } } catch { } if ( ! ignore && isZWaveError ( hint . originalException ) && hint . originalException . transactionSource ) { event . contexts = { transaction : { stack : hint . originalException . transactionSource } } ; } } } if ( ignore && anyWhitelisted ( filenames ) ) { ignore = false ; } return ignore ; } return { isPartOfThisLib , shouldIgnore , getFingerprint } ; } export async function initSentry ( libraryRootDir , libName , libVersion ) { const context = createSentryContext ( libraryRootDir ) ; Sentry . init ( { release : `${ libName }@${ libVersion }` , dsn : "https://a66de07edd064106853cc639407ebe64@sentry.iobroker.net/119" , defaultIntegrations : false , autoSessionTracking : false , integrations : [ new Sentry . Integrations . OnUncaughtException ( ) , new Sentry . Integrations . OnUnhandledRejection ( { mode : "strict" } ) , new Sentry . Integrations . FunctionToString ( ) , new Integrations . Dedupe ( ) as any , ] , maxBreadcrumbs : 30 , beforeSend ( event , hint ) { return context . shouldIgnore ( event , hint ) ? null : event ; } } ) ; try { const fingerprint = await context . getFingerprint ( ) ; Sentry . configureScope ( ( scope ) => { scope . setUser ( { id : fingerprint } ) ; } ) ; } catch { } }