UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

265 lines (236 loc) 8.31 kB
// Load sentry.io so we get information about errors 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"; // Errors in files matching any entry in this array will always be reported const pathWhitelists = ["node_modules/iobroker.zwave2"]; // except if they are included in this array const pathBlacklists = ["node_modules/@serialport"]; // like pathBlacklists but for exact matches const pathBlacklistsExact = ["repl.js"]; function isZWaveError( err: Error | string | null | undefined, ): err is ZWaveError { if (!err || typeof err === "string") return false; return "code" in err && typeof (err as any).code === "number"; } /** @internal */ export interface SentryContext { isPartOfThisLib(filename: string): boolean; shouldIgnore(event: Sentry.Event, hint?: Sentry.EventHint): boolean; getFingerprint(): Promise<string>; } const errorMessageTests = (() => { const tests: ((msg: string) => boolean | undefined)[] = []; // For some reason, working with ZWaveError instances directly doesn't work always. // Therefore check for the error codes' string representations. tests.push((msg) => { for (const code of [ // we don't care about timeouts ZWaveErrorCodes.Controller_MessageDropped, // We don't care about failed node removal ZWaveErrorCodes.RemoveFailedNode_Failed, ZWaveErrorCodes.RemoveFailedNode_NodeOK, // Or failed inclusion processes: ZWaveErrorCodes.Controller_InclusionFailed, ZWaveErrorCodes.Controller_ExclusionFailed, // Or users that don't read the changelog: ZWaveErrorCodes.Driver_NoErrorHandler, // Or incorrect driver options: ZWaveErrorCodes.Driver_InvalidOptions, ] as const) { if (msg.includes(getErrorSuffix(code))) { return true; } } }); // Don't care about users that try to manage associations on nodes that don't support it tests.push( (msg) => msg.includes(getErrorSuffix(ZWaveErrorCodes.CC_NotSupported)) && /does not support.+associations/.test(msg), ); // No such file or directory, cannot open /dev/ttyACM0 // no such file or directory, rename '/usr/src/app/store/mqtt/incoming~' // Opening COM18: File not found // No such file or directory, cannot open Select Port 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), ); // EROFS: read-only file system, write // ENODEV: no such device, write // ENOSPC: no space left on device, write tests.push( (msg) => /(EROFS|ENODEV|ENOSPC)/i.test(msg) && /(read-only file system|no such device|no space left)/i.test(msg), ); // Unknown system error -116: Unknown system error -116, write tests.push((msg) => /unknown system error/i.test(msg)); // Input/output error setting custom baud rate of 115200 tests.push((msg) => /custom baud rate/i.test(msg)); // Could not locate the bindings file tests.push((msg) => /bindings\.node/i.test(msg)); return tests; })(); export function createSentryContext(libraryRootDir: string): SentryContext { /** Checks if a filename is part of this library. Paths outside will be excluded from Sentry error reporting */ function isPartOfThisLib(filename: string): boolean { 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(): Promise<string> { const fingerprintPath = path.join(libraryRootDir, "fingerprint.txt"); let fingerprint: string | undefined; 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 { /* ignore */ } } return fingerprint; } /** Returns whether any line in the given stacktrace is whitelisted and none is blacklisted */ function anyWhitelisted(filenames: string[]): boolean { 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: string[]): boolean { 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: Sentry.Event, hint?: Sentry.EventHint, ): boolean { // Sentry orders stack traces from outside (index 0) to inside (index 0). // In order to figure out if the error was caused inside zwave-js, we need to // ignore all traces without a filename or from Node.js internals const filenames = event.exception?.values?.[0]?.stacktrace?.frames ?.map((f) => f.filename) ?.filter( (f): f is string => !!f && !f.startsWith("internal/"), ) ?? []; // Definitely ignore errors which have nothing to do with this library, unless whitelisted if (!filenames.some((f) => isPartOfThisLib(f))) { return !anyWhitelisted(filenames); } // Definitely ignore errors where a part of the stack trace is blacklisted if (anyBlacklistedExact(filenames)) { return true; } let ignore = false; const culprit = filenames[filenames.length - 1]; const culpritIsPartOfThisLib = isPartOfThisLib(culprit); // Maybe ignore errors that are raised outside zwave-js if (!culpritIsPartOfThisLib) ignore = true; // Filter out specific errors that are raised by zwave-js, // but shouldn't create a report on Sentry because they should be // handled by the library user if (!ignore && culpritIsPartOfThisLib && hint) { if (hint.originalException) { try { const msg = hint.originalException.toString(); if (errorMessageTests.some((test) => test(msg))) { ignore = true; } } catch { // This doesn't seem to be representable as a string } // Try to attach transaction context if this is an actual ZWaveError instance if ( !ignore && isZWaveError(hint.originalException) && hint.originalException.transactionSource ) { event.contexts = { transaction: { stack: hint.originalException.transactionSource, }, }; } } } // Don't ignore explicitly whitelisted paths if (ignore && anyWhitelisted(filenames)) { ignore = false; } return ignore; } return { isPartOfThisLib, shouldIgnore, getFingerprint, }; } export async function initSentry( libraryRootDir: string, libName: string, libVersion: string, ): Promise<void> { const context: SentryContext = 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: "none", // Let applications take care of force-exiting }), new Sentry.Integrations.FunctionToString(), new Integrations.Dedupe() as any, ], maxBreadcrumbs: 30, beforeSend(event, hint) { return context.shouldIgnore(event, hint) ? null : event; }, }); // Try to group events by user (anonymously) try { const fingerprint = await context.getFingerprint(); Sentry.configureScope((scope) => { scope.setUser({ id: fingerprint }); }); } catch { /* ignore */ } }