import * as Sentry from "@sentry/node"; import TransportStream = require("winston-transport"); import { LEVEL } from "triple-beam"; enum SentrySeverity { Debug = "debug", Log = "log", Info = "info", Warning = "warning", Error = "error", Fatal = "fatal", } const DEFAULT_LEVELS_MAP: SeverityOptions = { silly: SentrySeverity.Debug, verbose: SentrySeverity.Debug, info: SentrySeverity.Info, debug: SentrySeverity.Debug, warn: SentrySeverity.Warning, error: SentrySeverity.Error, }; export interface SentryTransportOptions extends TransportStream.TransportStreamOptions { sentry?: Sentry.NodeOptions; levelsMap?: SeverityOptions; skipSentryInit?: boolean; } interface SeverityOptions { [key: string]: Sentry.SeverityLevel; } class ExtendedError extends Error { constructor(info: any) { super(info.message); this.name = info.name || "Error"; if (info.stack && typeof info.stack === "string") { this.stack = info.stack; } } } export default class SentryTransport extends TransportStream { public silent = false; private levelsMap: SeverityOptions = {}; public constructor(opts?: SentryTransportOptions) { super(opts); this.levelsMap = this.setLevelsMap(opts && opts.levelsMap); this.silent = (opts && opts.silent) || false; if (!opts || !opts.skipSentryInit) { Sentry.init(SentryTransport.withDefaults((opts && opts.sentry) || {})); } } public log(info: any, callback: () => void) { setImmediate(() => { this.emit("logged", info); }); if (this.silent) return callback(); const { message, tags, user, ...meta } = info; const winstonLevel = info[LEVEL]; const sentryLevel = this.levelsMap[winstonLevel]; const scope = Sentry.getCurrentScope(); scope.clear(); if (tags !== undefined && SentryTransport.isObject(tags)) { scope.setTags(tags); } scope.setExtras(meta); if (user !== undefined && SentryTransport.isObject(user)) { scope.setUser(user); } // TODO: add fingerprints // scope.setFingerprint(['{{ default }}', path]); // fingerprint should be an array // scope.clear(); // TODO: add breadcrumbs // Sentry.addBreadcrumb({ // message: 'My Breadcrumb', // // ... // }); // Capturing Errors / Exceptions if (SentryTransport.shouldLogException(sentryLevel)) { const error = Object.values(info).find((value) => value instanceof Error) ?? new ExtendedError(info); Sentry.captureException(error, { tags, level: sentryLevel }); return callback(); } // Capturing Messages Sentry.captureMessage(message, sentryLevel); return callback(); } end(...args: any[]) { Sentry.flush().then(() => { super.end(...args); }); return this; } public get sentry() { return Sentry; } private setLevelsMap = (options?: SeverityOptions): SeverityOptions => { if (!options) { return DEFAULT_LEVELS_MAP; } const customLevelsMap = Object.keys(options).reduce( (acc: { [key: string]: any }, winstonSeverity: string) => { acc[winstonSeverity] = options[winstonSeverity]; return acc; }, {} ); return { ...DEFAULT_LEVELS_MAP, ...customLevelsMap, }; }; private static withDefaults(options: Sentry.NodeOptions) { return { ...options, dsn: (options && options.dsn) || process.env.SENTRY_DSN || "", serverName: (options && options.serverName) || "winston-transport-sentry-node", environment: (options && options.environment) || process.env.SENTRY_ENVIRONMENT || process.env.NODE_ENV || "production", debug: (options && options.debug) || !!process.env.SENTRY_DEBUG || false, sampleRate: (options && options.sampleRate) || 1.0, maxBreadcrumbs: (options && options.maxBreadcrumbs) || 100, }; } // private normalizeMessage(msg: any) { // return msg && msg.message ? msg.message : msg; // } private static isObject(obj: any) { const type = typeof obj; return type === "function" || (type === "object" && !!obj); } private static shouldLogException(level: Sentry.SeverityLevel) { return level === SentrySeverity.Fatal || level === SentrySeverity.Error; } }