- `logger.debug('message', async () => ({ asyncReturn: await asyncFn() }))` is now supported
- NOTE: this may result in logging occurring out of band for these calls if other events fire befor they settle
Update CI labels to direct jobs to correct servers
195 lines
6.8 KiB
TypeScript
195 lines
6.8 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { createLogger, type LogEvent } from "../src/index";
|
|
import { createSentrySink, toSentryLogPayload, toSentrySeverity } from "../src/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(),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function issueEvent(overrides: Partial<LogEvent> = {}): LogEvent {
|
|
return {
|
|
level: "error",
|
|
arguments: ["message"],
|
|
timestamp: new Date(),
|
|
environment: "production",
|
|
sendToSentryLogs: false,
|
|
sendToSentryIssue: true,
|
|
sendToConsole: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("toSentrySeverity", () => {
|
|
it("maps logger levels to Sentry Issue severities", () => {
|
|
expect(toSentrySeverity("warn")).toBe("warning");
|
|
expect(toSentrySeverity("trace")).toBe("debug");
|
|
expect(toSentrySeverity("error")).toBe("error");
|
|
expect(toSentrySeverity("info")).toBe("info");
|
|
expect(toSentrySeverity("debug")).toBe("debug");
|
|
});
|
|
});
|
|
|
|
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,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("includes async when the event was deferred for thenables", () => {
|
|
expect(
|
|
toSentryLogPayload({
|
|
level: "debug",
|
|
arguments: ["response"],
|
|
timestamp: new Date(),
|
|
environment: "development",
|
|
sendToSentryLogs: true,
|
|
sendToSentryIssue: false,
|
|
sendToConsole: true,
|
|
async: true,
|
|
}).attributes,
|
|
).toEqual({ async: 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 without logs");
|
|
logger.error("Request failed", error, { requestId: "req_1" });
|
|
expect(sentry.captureException).toHaveBeenCalledWith(error);
|
|
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({
|
|
"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();
|
|
});
|
|
|
|
it("maps warn → warning on the Issues path and keeps warn on the Logs path", () => {
|
|
const { scope, sentry } = createFakeSentry();
|
|
const sink = createSentrySink(sentry, { logs: true });
|
|
|
|
sink.emit(
|
|
issueEvent({
|
|
level: "warn",
|
|
arguments: ["degraded"],
|
|
}),
|
|
);
|
|
expect(scope.setLevel).toHaveBeenCalledWith("warning");
|
|
expect(sentry.captureMessage).toHaveBeenCalledWith("degraded", "warning");
|
|
expect(sentry.logger.warn).not.toHaveBeenCalled();
|
|
|
|
sink.emit({
|
|
level: "warn",
|
|
namespace: "API",
|
|
arguments: ["slow", { ms: 10 }],
|
|
timestamp: new Date(),
|
|
environment: "production",
|
|
sendToSentryLogs: true,
|
|
sendToSentryIssue: false,
|
|
sendToConsole: false,
|
|
});
|
|
expect(sentry.logger.warn).toHaveBeenCalledWith("slow", {
|
|
"logger.namespace": "API",
|
|
ms: 10,
|
|
});
|
|
});
|
|
|
|
it("maps error Issue severity as error", () => {
|
|
const { scope, sentry } = createFakeSentry();
|
|
createSentrySink(sentry).emit(
|
|
issueEvent({
|
|
level: "error",
|
|
arguments: ["failed"],
|
|
}),
|
|
);
|
|
expect(scope.setLevel).toHaveBeenCalledWith("error");
|
|
expect(sentry.captureMessage).toHaveBeenCalledWith("failed", "error");
|
|
});
|
|
});
|