UNPKG

@logtape/sentry

Version:

LogTape Sentry sink

212 lines (210 loc) 7.39 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")); //#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 = 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; } /** * A platform-specific inspect function. In Deno, this is {@link Deno.inspect}, * and in Node.js/Bun it is {@link util.inspect}. If neither is available, it * falls back to {@link JSON.stringify}. * * @param value The value to inspect. * @returns The string representation of the value. */ const inspect = "Deno" in globalThis && "inspect" in globalThis.Deno && typeof globalThis.Deno.inspect === "function" ? globalThis.Deno.inspect : "util" in globalThis && "inspect" in globalThis.util && typeof globalThis.util.inspect === "function" ? globalThis.util.inspect : JSON.stringify; 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"; } } /** * Gets a LogTape sink that sends logs to Sentry. * * This sink uses Sentry's global capture functions from `@sentry/core`, * following Sentry v8+ best practices. Simply call `Sentry.init()` before * creating the sink, and it will automatically use your initialized client. * * @param optionsOrClient Optional configuration. Can be: * - Omitted: Uses global Sentry functions (recommended) * - Object with options: Configure sink behavior * - 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 options * ```typescript * import * as Sentry from "@sentry/node"; * Sentry.init({ dsn: process.env.SENTRY_DSN }); * * await configure({ * sinks: { * sentry: getSentrySink({ * enableBreadcrumbs: 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 sentry; 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 and will be removed in v2.0.0. Use getSentrySink() instead - simpler and recommended!"); sentry = 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(client) // Deprecated (v1.1.x compat) `); const captureMessage = sentry ? (msg, ctx) => sentry.captureMessage(String(msg), ctx) : __sentry_core.captureMessage; const captureException = sentry ? (exception, hint) => sentry.captureException(exception, hint) : __sentry_core.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 = (0, __sentry_core.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 = (0, __sentry_core.getClient)(); if (client) { const { enableLogs, _experiments } = client.getOptions(); const loggingEnabled = enableLogs ?? _experiments?.enableLogs; if (loggingEnabled && "logger" in __sentry_core) { const logLevel = mapLevelForLogs(transformed.level); const sentryLogger = __sentry_core.logger; const logFn = sentryLogger?.[logLevel]; if (typeof logFn === "function") logFn(paramMessage, attributes); } } const isErrorLevel = (0, __logtape_logtape.compareLogLevel)(transformed.level, "error") >= 0; if (isErrorLevel && transformed.properties.error instanceof Error) { const { error,...rest } = attributes; captureException(error, { level: eventLevel, extra: { message, ...rest } }); } else if (isErrorLevel) captureMessage(paramMessage, { level: eventLevel, extra: attributes }); else if (options.enableBreadcrumbs) { const isolationScope = (0, __sentry_core.getIsolationScope)(); isolationScope?.addBreadcrumb({ category: transformed.category.join("."), level: eventLevel, message, timestamp: transformed.timestamp / 1e3, data: attributes }); } } catch (err) { try { console.debug("[@logtape/sentry] sink error", err); } catch {} } }; } //#endregion exports.getSentrySink = getSentrySink;