270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
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[] = [];
|
|
return { events, sink: { emit: (event) => events.push(event) } };
|
|
}
|
|
|
|
describe("parseLoggingOverride", () => {
|
|
it("accepts a global level and a namespace-filtered level", () => {
|
|
expect(parseLoggingOverride("debug")).toEqual({ level: "debug", namespaces: [] });
|
|
expect(parseLoggingOverride("debug:WIDGET, API")).toEqual({
|
|
level: "debug",
|
|
namespaces: ["WIDGET", "API"],
|
|
});
|
|
expect(parseLoggingOverride("verbose")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
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();
|
|
const expensive = vi.fn(() => ({ huge: "snapshot" }));
|
|
const logger = createLogger({ environment: "production", sinks: [destination.sink] });
|
|
logger.debug("ignored", expensive);
|
|
logger.error("kept", expensive);
|
|
expect(expensive).toHaveBeenCalledTimes(1);
|
|
expect(destination.events).toHaveLength(1);
|
|
expect(destination.events[0]?.arguments).toEqual(["kept", { huge: "snapshot" }]);
|
|
});
|
|
|
|
it("applies session namespace filtering to namespace descendants", () => {
|
|
const destination = sink();
|
|
const storage = { getItem: () => "debug:WIDGET" };
|
|
createLogger({
|
|
environment: "production",
|
|
namespace: "WIDGET:Button",
|
|
sessionStorage: storage,
|
|
sinks: [destination.sink],
|
|
}).debug("shown");
|
|
createLogger({
|
|
environment: "production",
|
|
namespace: "API",
|
|
sessionStorage: storage,
|
|
sinks: [destination.sink],
|
|
}).debug("hidden");
|
|
expect(destination.events.map((event) => event.arguments[0])).toEqual(["shown"]);
|
|
});
|
|
|
|
it("builds immutable child namespaces and reports failed assertions as errors", () => {
|
|
const destination = sink();
|
|
const root = createLogger({
|
|
environment: "development",
|
|
namespace: "WIDGET",
|
|
sinks: [destination.sink],
|
|
});
|
|
root.child("Button").assert(false, "missing label");
|
|
expect(root.namespace).toBe("WIDGET");
|
|
expect(destination.events[0]).toMatchObject({
|
|
level: "error",
|
|
namespace: "WIDGET:Button",
|
|
arguments: ["Assertion failed", "missing label"],
|
|
});
|
|
});
|
|
|
|
it("sends production browser errors to Sentry without writing to the console", () => {
|
|
const destination = sink();
|
|
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
|
const logger = createLogger({
|
|
environment: "production",
|
|
runtime: "browser",
|
|
sentry: destination.sink,
|
|
});
|
|
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("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({
|
|
environment: "production",
|
|
runtime: "browser",
|
|
sentry: destination.sink,
|
|
});
|
|
logger.info("Cache warmed", { sentry: true });
|
|
expect(destination.events).toHaveLength(1);
|
|
expect(destination.events[0]).toMatchObject({
|
|
level: "info",
|
|
arguments: ["Cache warmed"],
|
|
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);
|
|
const logger = createLogger({
|
|
environment: "production",
|
|
runtime: "node",
|
|
sentry: destination.sink,
|
|
});
|
|
logger.error("captured");
|
|
expect(destination.events).toHaveLength(1);
|
|
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,
|
|
});
|
|
});
|
|
});
|