@logtape/sentry
Version:
LogTape Sentry sink
212 lines (210 loc) • 7.39 kB
JavaScript
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;