feat!: API options object support to control sentry handling; removal of logger.sentry.
This commit is contained in:
+160
-5
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+98
-12
@@ -1,28 +1,114 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createLogger } from "../src/index.js";
|
||||
import { createSentrySink } from "../src/sentry.js";
|
||||
import { createLogger } from "../src/index";
|
||||
import { createSentrySink, toSentryLogPayload } from "../src/sentry";
|
||||
|
||||
describe("createSentrySink", () => {
|
||||
it("captures errors and explicitly selected events with namespace and structured context", () => {
|
||||
const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() };
|
||||
const sentry = {
|
||||
function createFakeSentry() {
|
||||
const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() };
|
||||
return {
|
||||
scope,
|
||||
sentry: {
|
||||
withScope: (callback: (value: typeof scope) => void) => callback(scope),
|
||||
captureException: vi.fn(),
|
||||
captureMessage: vi.fn(),
|
||||
};
|
||||
logger: {
|
||||
trace: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("toSentryLogPayload", () => {
|
||||
it("joins string messages and flattens primitive object attributes", () => {
|
||||
expect(
|
||||
toSentryLogPayload({
|
||||
level: "info",
|
||||
namespace: "API",
|
||||
arguments: ["Cache warmed", { entries: 42, ok: true, nested: { a: 1 } }],
|
||||
timestamp: new Date(),
|
||||
environment: "production",
|
||||
sendToSentryLogs: true,
|
||||
sendToSentryIssue: false,
|
||||
sendToConsole: false,
|
||||
}),
|
||||
).toEqual({
|
||||
message: "Cache warmed",
|
||||
attributes: {
|
||||
"logger.namespace": "API",
|
||||
entries: 42,
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createSentrySink", () => {
|
||||
it("captures errors as Issues with namespace and structured attributes", () => {
|
||||
const { scope, sentry } = createFakeSentry();
|
||||
const logger = createLogger({
|
||||
environment: "production",
|
||||
namespace: "API",
|
||||
sinks: [createSentrySink(sentry)],
|
||||
});
|
||||
const error = new Error("offline");
|
||||
logger.warn("ignored");
|
||||
logger.sentry.info("Cache warmed");
|
||||
logger.warn("ignored without logs");
|
||||
logger.error("Request failed", error, { requestId: "req_1" });
|
||||
expect(sentry.captureMessage).toHaveBeenCalledWith("Cache warmed", "info");
|
||||
expect(sentry.captureException).toHaveBeenCalledWith(error);
|
||||
expect(scope.setLevel).toHaveBeenCalledWith("info");
|
||||
expect(sentry.captureMessage).not.toHaveBeenCalled();
|
||||
expect(sentry.logger.info).not.toHaveBeenCalled();
|
||||
expect(scope.setLevel).toHaveBeenCalledWith("error");
|
||||
expect(scope.setTag).toHaveBeenCalledWith("logger.namespace", "API");
|
||||
expect(scope.setExtras).toHaveBeenCalledWith({ context_0: { requestId: "req_1" } });
|
||||
expect(scope.setExtras).toHaveBeenCalledWith({
|
||||
"logger.namespace": "API",
|
||||
requestId: "req_1",
|
||||
});
|
||||
});
|
||||
|
||||
it("writes non-error events to Sentry Logs when enabled, not as Issues", () => {
|
||||
const { sentry } = createFakeSentry();
|
||||
const logger = createLogger({
|
||||
environment: "production",
|
||||
namespace: "API",
|
||||
sinks: [createSentrySink(sentry, { logs: true })],
|
||||
});
|
||||
logger.warn("slow", { ms: 1200 });
|
||||
logger.info("forced", { id: 1 }, { sentry: true });
|
||||
expect(sentry.captureMessage).not.toHaveBeenCalled();
|
||||
expect(sentry.captureException).not.toHaveBeenCalled();
|
||||
expect(sentry.logger.warn).toHaveBeenCalledWith("slow", {
|
||||
"logger.namespace": "API",
|
||||
ms: 1200,
|
||||
});
|
||||
expect(sentry.logger.info).toHaveBeenCalledWith("forced", {
|
||||
"logger.namespace": "API",
|
||||
id: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not create an Issue when suppressSentry is set", () => {
|
||||
const { sentry } = createFakeSentry();
|
||||
const logger = createLogger({
|
||||
environment: "production",
|
||||
runtime: "node",
|
||||
sinks: [createSentrySink(sentry, { logs: true })],
|
||||
});
|
||||
logger.error("expected", new Error("nope"), { suppressSentry: true });
|
||||
expect(sentry.captureException).not.toHaveBeenCalled();
|
||||
expect(sentry.logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("never sends in development", () => {
|
||||
const { sentry } = createFakeSentry();
|
||||
const logger = createLogger({
|
||||
environment: "development",
|
||||
sinks: [createSentrySink(sentry, { logs: true })],
|
||||
});
|
||||
logger.error("local", new Error("x"));
|
||||
logger.warn("local warn", { sentry: true });
|
||||
expect(sentry.captureException).not.toHaveBeenCalled();
|
||||
expect(sentry.logger.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user