feat!: API options object support to control sentry handling; removal of logger.sentry.
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish unknown status

This commit is contained in:
2026-08-07 12:07:29 -03:00
parent 9f18e0855e
commit 6bee3a801d
11 changed files with 1170 additions and 161 deletions
+160 -5
View File
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { createLogger, parseLoggingOverride } from "../src/index.js";
import type { LogEvent, LogSink } from "../src/index.js";
import { createLogger, isLogCallOptions, parseLoggingOverride } from "../src/index";
import type { LogEvent, LogSink } from "../src/index";
import { createSentrySink } from "../src/sentry";
function sink(): { sink: LogSink; events: LogEvent[] } {
const events: LogEvent[] = [];
@@ -18,6 +19,17 @@ describe("parseLoggingOverride", () => {
});
});
describe("isLogCallOptions", () => {
it("accepts only known option keys", () => {
expect(isLogCallOptions({ sentry: true })).toBe(true);
expect(isLogCallOptions({ suppressSentry: true })).toBe(true);
expect(isLogCallOptions({ sentry: true, suppressSentry: false })).toBe(true);
expect(isLogCallOptions({ requestId: "req_1" })).toBe(false);
expect(isLogCallOptions({ sentry: true, requestId: "req_1" })).toBe(false);
expect(isLogCallOptions({})).toBe(false);
});
});
describe("createLogger", () => {
it("uses environment defaults and never evaluates suppressed lazy data", () => {
const destination = sink();
@@ -74,11 +86,16 @@ describe("createLogger", () => {
});
logger.error("captured");
expect(destination.events).toHaveLength(1);
expect(destination.events[0]).toMatchObject({
sendToSentryIssue: true,
sendToSentryLogs: false,
sendToConsole: true,
});
expect(consoleError).not.toHaveBeenCalled();
consoleError.mockRestore();
});
it("sends explicitly selected production events to Sentry without writing them to the console", () => {
it("forces selected events to Sentry Logs without writing them to the console", () => {
const destination = sink();
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
const logger = createLogger({
@@ -86,18 +103,87 @@ describe("createLogger", () => {
runtime: "browser",
sentry: destination.sink,
});
logger.sentry.info("Cache warmed");
logger.info("Cache warmed", { sentry: true });
expect(destination.events).toHaveLength(1);
expect(destination.events[0]).toMatchObject({
level: "info",
arguments: ["Cache warmed"],
sendToSentry: true,
sendToSentryLogs: true,
sendToSentryIssue: false,
sendToConsole: false,
});
expect(consoleInfo).not.toHaveBeenCalled();
consoleInfo.mockRestore();
});
it("strips trailing call options from event arguments and supports data plus options", () => {
const destination = sink();
const logger = createLogger({
environment: "production",
runtime: "browser",
sentry: destination.sink,
});
logger.warn("slow", { ms: 1200 }, { sentry: true });
expect(destination.events[0]).toMatchObject({
level: "warn",
arguments: ["slow", { ms: 1200 }],
sendToSentryLogs: true,
sendToConsole: false,
});
});
it("suppresses Sentry Issues when suppressSentry is set", () => {
const destination = sink();
const logger = createLogger({
environment: "production",
runtime: "browser",
sentry: destination.sink,
});
logger.error("expected", new Error("nope"), { suppressSentry: true });
expect(destination.events).toHaveLength(1);
expect(destination.events[0]).toMatchObject({
sendToSentryIssue: false,
sendToSentryLogs: false,
arguments: ["expected", expect.any(Error)],
});
});
it("never marks events for Sentry in development", () => {
const destination = sink();
const logger = createLogger({
environment: "development",
sinks: [
destination.sink,
createSentrySink(
{
withScope: (callback) =>
callback({
setLevel: () => undefined,
setTag: () => undefined,
setExtras: () => undefined,
}),
captureException: vi.fn(),
captureMessage: vi.fn(),
logger: {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
},
{ logs: true },
),
],
});
logger.error("local only");
logger.info("probe", { sentry: true });
expect(destination.events).toHaveLength(2);
expect(
destination.events.every((event) => !event.sendToSentryIssue && !event.sendToSentryLogs),
).toBe(true);
});
it("keeps production Node errors on stderr as well as sending them to Sentry", () => {
const destination = sink();
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
@@ -111,4 +197,73 @@ describe("createLogger", () => {
expect(consoleError).toHaveBeenCalledTimes(1);
consoleError.mockRestore();
});
it("mirrors warn to Sentry Logs in production when logs are enabled on the sink", () => {
const destination = sink();
const logger = createLogger({
environment: "production",
runtime: "browser",
sinks: [
destination.sink,
createSentrySink(
{
withScope: () => undefined,
captureException: vi.fn(),
captureMessage: vi.fn(),
logger: {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
},
{ logs: true },
),
],
});
logger.warn("noisy");
logger.info("below default production logs threshold");
expect(destination.events).toHaveLength(1);
expect(destination.events[0]).toMatchObject({
level: "warn",
sendToSentryLogs: true,
sendToConsole: false,
});
});
it("mirrors info to Sentry Logs in staging when logs are enabled", () => {
const destination = sink();
const logger = createLogger({
environment: "staging",
runtime: "browser",
sinks: [
destination.sink,
createSentrySink(
{
withScope: () => undefined,
captureException: vi.fn(),
captureMessage: vi.fn(),
logger: {
trace: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
},
},
{ logs: true },
),
],
});
logger.info("visible in staging logs");
logger.debug("still below staging logs threshold");
expect(destination.events.map((event) => event.arguments[0])).toEqual([
"visible in staging logs",
]);
expect(destination.events[0]).toMatchObject({
sendToSentryLogs: true,
sendToConsole: false,
});
});
});