UNPKG

@logtape/sentry

Version:

LogTape Sentry sink

247 lines (245 loc) 8.77 kB
const require_rolldown_runtime = require('./_virtual/rolldown_runtime.cjs'); const __logtape_logtape = require_rolldown_runtime.__toESM(require("@logtape/logtape")); const __sentry_core = require_rolldown_runtime.__toESM(require("@sentry/core")); const __util = require_rolldown_runtime.__toESM(require("#util")); //#region src/mod.ts /** * Converts a LogTape {@link LogRecord} into a Sentry {@link ParameterizedString}. * * This preserves the template structure for better message grouping in Sentry, * allowing similar messages with different values to be grouped together. * * @param record The log record to convert. * @returns A parameterized string with template and values. */ function getParameterizedString(record) { let result = ""; let tplString = ""; const tplValues = []; for (let i = 0; i < record.message.length; i++) if (i % 2 === 0) { result += record.message[i]; tplString += String(record.message[i]).replaceAll("%", "%%"); } else { const value = (0, __util.inspect)(record.message[i]); result += value; tplString += `%s`; tplValues.push(value); } const paramStr = new String(result); paramStr.__sentry_template_string__ = tplString; paramStr.__sentry_template_values__ = tplValues; return paramStr; } function mapLevelForEvents(level) { switch (level) { case "trace": return "debug"; default: return level; } } function mapLevelForLogs(level) { switch (level) { case "trace": return "debug"; case "warning": return "warn"; case "debug": case "info": case "error": case "fatal": return level; default: return "info"; } } const defaultErrorPropertyNames = ["error", "err"]; function getErrorProperty(properties, propertyNames) { for (const property of propertyNames) if (properties[property] instanceof Error) return [property, properties[property]]; return void 0; } /** * Gets a LogTape sink that sends logs to Sentry. * * This sink uses Sentry's global capture functions from `@sentry/core` by * default, following Sentry v8+ best practices. Simply call `Sentry.init()` * before creating the sink, and it will automatically use your initialized * client when both packages resolve the same Sentry module instance. * * @param optionsOrClient Optional configuration. Can be: * - Omitted: Uses global Sentry functions (recommended) * - Object with options: Configure sink behavior * - Object with `sentry`: Use an application-provided Sentry SDK namespace * - Sentry client instance: Backward compatibility (deprecated) * @returns A LogTape sink that sends logs to Sentry. * * @example Recommended usage - no parameters * ```typescript * import { configure } from "@logtape/logtape"; * import { getSentrySink } from "@logtape/sentry"; * import * as Sentry from "@sentry/node"; * * Sentry.init({ dsn: process.env.SENTRY_DSN }); * * await configure({ * sinks: { * sentry: getSentrySink(), // That's it! * }, * loggers: [ * { category: [], sinks: ["sentry"], lowestLevel: "error" }, * ], * }); * ``` * * @example With an application-provided Sentry namespace * ```typescript * import { configure } from "@logtape/logtape"; * import { getSentrySink } from "@logtape/sentry"; * import * as Sentry from "@sentry/nextjs"; * * Sentry.init({ dsn: process.env.SENTRY_DSN }); * * await configure({ * sinks: { * sentry: getSentrySink({ sentry: Sentry }), * }, * loggers: [ * { category: [], sinks: ["sentry"], lowestLevel: "error" }, * ], * }); * ``` * * @example With options * ```typescript * import * as Sentry from "@sentry/node"; * Sentry.init({ dsn: process.env.SENTRY_DSN }); * * await configure({ * sinks: { * sentry: getSentrySink({ * breadcrumbs: true, * }), * }, * loggers: [ * { category: [], sinks: ["sentry"], lowestLevel: "info" }, * ], * }); * ``` * * @example Edge functions - must flush before termination * ```typescript * // Cloudflare Workers * export default { * async fetch(request, env, ctx) { * logger.error("Something happened"); * ctx.waitUntil(Sentry.flush(2000)); // Don't block response * return new Response("OK"); * } * }; * ``` * * @example Legacy usage (v1.1.x - deprecated) * ```typescript * import { getClient } from "@sentry/node"; * const client = getClient(); * getSentrySink(client); // Still works but shows deprecation warning * ``` * * @since 1.0.0 */ function getSentrySink(optionsOrClient) { let legacyClient; let options = {}; if (optionsOrClient == null) {} else if (typeof optionsOrClient === "object" && "captureMessage" in optionsOrClient && typeof optionsOrClient.captureMessage === "function") { (0, __logtape_logtape.getLogger)([ "logtape", "meta", "sentry" ]).warn("Passing a client directly is deprecated. Use getSentrySink({ sentry: Sentry }) instead."); legacyClient = optionsOrClient; } else if (typeof optionsOrClient === "object") options = optionsOrClient; else throw new Error(`[@logtape/sentry] Invalid parameter (type: ${typeof optionsOrClient}).\n\nExpected one of: getSentrySink() // Recommended getSentrySink({ options }) // With options getSentrySink({ sentry }) // With a Sentry SDK namespace getSentrySink(client) // Deprecated (v1.1.x compat) `); const sentry = options.sentry ?? __sentry_core; const captureMessage = legacyClient ? (msg, ctx) => legacyClient.captureMessage(String(msg), ctx) : sentry.captureMessage; const captureException = legacyClient ? (exception, hint) => legacyClient.captureException(exception, hint) : sentry.captureException; return (record) => { try { const { category } = record; if (category[0] === "logtape" && category[1] === "meta" && category[2] === "sentry") return; const transformed = options.beforeSend ? options.beforeSend(record) : record; if (transformed == null) return; const paramMessage = getParameterizedString(transformed); const message = paramMessage.toString(); const eventLevel = mapLevelForEvents(transformed.level); const attributes = { ...transformed.properties, "sentry.origin": "auto.logging.logtape", category: transformed.category.join("."), timestamp: transformed.timestamp }; const activeSpan = sentry.getActiveSpan(); if (activeSpan) { const spanCtx = activeSpan.spanContext(); attributes.trace_id = spanCtx.traceId; attributes.span_id = spanCtx.spanId; if ("parentSpanId" in spanCtx) attributes.parent_span_id = spanCtx.parentSpanId; } const client = sentry.getClient(); if (client && shouldSendToLogs(transformed, options.logs)) { const { enableLogs, _experiments } = client.getOptions(); const loggingEnabled = enableLogs ?? _experiments?.enableLogs; const sentryLogger = sentry.logger; if (loggingEnabled && sentryLogger != null) { const logLevel = mapLevelForLogs(transformed.level); const logFn = sentryLogger[logLevel]; if (typeof logFn === "function") logFn(paramMessage, attributes); } } const isErrorLevel = (0, __logtape_logtape.compareLogLevel)(transformed.level, "error") >= 0; const errorProperty = getErrorProperty(transformed.properties, options.errorPropertyNames ?? defaultErrorPropertyNames); if (isErrorLevel && errorProperty != null) { const [property, error] = errorProperty; const rest = { ...attributes }; delete rest[property]; captureException(error, { level: eventLevel, extra: { message, ...rest } }); } else if (isErrorLevel) captureMessage(paramMessage, { level: eventLevel, extra: attributes }); else if (shouldAddBreadcrumb(transformed, options)) { const isolationScope = sentry.getIsolationScope(); isolationScope?.addBreadcrumb({ category: transformed.category.join("."), level: eventLevel, message, timestamp: transformed.timestamp / 1e3, data: attributes }); } } catch (error) { (0, __logtape_logtape.getLogger)([ "logtape", "meta", "sentry" ]).error("Failed to send log events to Sentry", { error }); } }; } function shouldSendToLogs(record, options) { return options?.level == null || (0, __logtape_logtape.compareLogLevel)(record.level, options.level) >= 0; } function shouldAddBreadcrumb(record, options) { const breadcrumbOptions = options.breadcrumbs; if (breadcrumbOptions === false) return false; if (breadcrumbOptions === true) return true; if (breadcrumbOptions == null) return options.enableBreadcrumbs === true; if (breadcrumbOptions.level != null && (0, __logtape_logtape.compareLogLevel)(record.level, breadcrumbOptions.level) < 0) return false; return breadcrumbOptions.maxLevel == null || (0, __logtape_logtape.compareLogLevel)(record.level, breadcrumbOptions.maxLevel) <= 0; } //#endregion exports.getSentrySink = getSentrySink;