2 Commits
Author SHA1 Message Date
semantic-release-bot 9f18e0855e chore(release): 0.10.0 [skip ci]
# [0.10.0](https://git.mifi.dev/mifi/logger/compare/v0.9.3...v0.10.0) (2026-08-05)

### Features

* **logger:** add explicit Sentry event channel ([44f4716](44f471653e))
2026-08-05 16:06:59 +00:00
mifi 44f471653e feat(logger): add explicit Sentry event channel
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
2026-08-05 13:05:06 -03:00
8 changed files with 74 additions and 11 deletions
+9 -3
View File
@@ -1,10 +1,16 @@
## [0.9.3](https://git.mifi.dev/mifi/logger/compare/v0.9.2...v0.9.3) (2026-08-05)
# [0.10.0](https://git.mifi.dev/mifi/logger/compare/v0.9.3...v0.10.0) (2026-08-05)
### Features
* **logger:** add explicit Sentry event channel ([44f4716](https://git.mifi.dev/mifi/logger/commit/44f471653e8d4045e87886da1e1bbe04edbd60af))
## [0.9.3](https://git.mifi.dev/mifi/logger/compare/v0.9.2...v0.9.3) (2026-08-05)
### Bug Fixes
* **release:** build package before publishing ([e0b4501](https://git.mifi.dev/mifi/logger/commit/e0b450157df7a8aa1ff8bf5299140b0e7b52a7f7))
* **release:** build package before publishing ([cebe107](https://git.mifi.dev/mifi/logger/commit/cebe107034a15e9fd1517d872037e97337426b29))
- **release:** build package before publishing ([e0b4501](https://git.mifi.dev/mifi/logger/commit/e0b450157df7a8aa1ff8bf5299140b0e7b52a7f7))
- **release:** build package before publishing ([cebe107](https://git.mifi.dev/mifi/logger/commit/cebe107034a15e9fd1517d872037e97337426b29))
## [0.9.2](https://git.mifi.dev/mifi/logger/compare/v0.9.1...v0.9.2) (2026-08-05)
+3 -1
View File
@@ -22,7 +22,7 @@ Containers can use `MIFI_LOG_LEVEL` and `MIFI_LOG_NAMESPACES`. Explicit `level`
## Sentry
The Sentry adapter has no SDK dependency. Provide the SDK that your application already uses. In a production browser, errors go to Sentry without also appearing in the console. In Node, they go to both Sentry and stderr.
The Sentry adapter has no SDK dependency. Provide the SDK that your application already uses. In a production browser, errors go to Sentry without also appearing in the console. In Node, they go to both Sentry and stderr. Use `logger.sentry` to explicitly capture selected non-error events without changing the default policy or writing them to the console when they are otherwise suppressed.
```ts
import * as Sentry from "@sentry/nextjs";
@@ -30,6 +30,8 @@ import { createLogger } from "@mifi/logger";
import { createSentrySink } from "@mifi/logger/sentry";
const logger = createLogger({ environment: "production", sentry: createSentrySink(Sentry) });
logger.sentry.info("Cache warmed", { entries: 42 });
```
## Releases
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mifi/logger",
"version": "0.9.3",
"version": "0.10.0",
"description": "Intentional, namespaced logging for browser and Node.js TypeScript applications.",
"repository": {
"type": "git",
+18 -2
View File
@@ -97,13 +97,20 @@ export function createLogger(options: LoggerOptions): Logger {
levelRank(level) >= levelRank(threshold) &&
(!runtimeOverride?.namespaces.length ||
namespaceMatches(namespace, runtimeOverride.namespaces));
const emit = (level: Exclude<LogLevel, "silent">, arguments_: readonly unknown[]) => {
if (!enabled(level)) return;
const emit = (
level: Exclude<LogLevel, "silent">,
arguments_: readonly unknown[],
sendToSentry = false,
) => {
const sendToConsole = enabled(level);
if (!sendToConsole && !sendToSentry) return;
const event: LogEvent = {
level,
namespace,
arguments: evaluate(arguments_),
timestamp: new Date(),
sendToSentry,
sendToConsole,
};
sinks.forEach((sink) => sink.emit(event));
};
@@ -123,6 +130,14 @@ export function createLogger(options: LoggerOptions): Logger {
};
const api: Logger = {
namespace,
sentry: {
trace: (...items) => emit("trace", items, true),
debug: (...items) => emit("debug", items, true),
log: (...items) => emit("info", items, true),
info: (...items) => emit("info", items, true),
warn: (...items) => emit("warn", items, true),
error: (...items) => emit("error", items, true),
},
child: (child) =>
createLogger({ ...options, namespace: [namespace, child].filter(Boolean) as string[] }),
trace: (...items) => emit("trace", items),
@@ -187,6 +202,7 @@ function supportsAnsi(): boolean {
export function createConsoleSink(): LogSink {
return {
emit: (event) => {
if (!event.sendToConsole) return;
const method = event.level === "trace" ? "debug" : event.level;
const console_ = globalThis.console as Console & Record<string, unknown>;
const fn = console_[method];
+3 -3
View File
@@ -15,7 +15,7 @@ export interface SentryLike {
export function createSentrySink(sentry: SentryLike): LogSink {
return {
emit(event: LogEvent) {
if (event.level !== "error") return;
if (event.level !== "error" && !event.sendToSentry) return;
const error = event.arguments.find((item): item is Error => item instanceof Error);
const extras = Object.fromEntries(
event.arguments
@@ -26,11 +26,11 @@ export function createSentrySink(sentry: SentryLike): LogSink {
event.arguments.filter((item) => typeof item === "string").join(" ") ||
"Logger error";
sentry.withScope((scope) => {
scope.setLevel("error");
scope.setLevel(event.level);
if (event.namespace) scope.setTag("logger.namespace", event.namespace);
scope.setExtras(extras);
if (error) sentry.captureException(error);
else sentry.captureMessage(message, "error");
else sentry.captureMessage(message, event.level);
});
},
};
+16
View File
@@ -9,6 +9,10 @@ export interface LogEvent {
namespace?: string;
arguments: readonly unknown[];
timestamp: Date;
/** Whether this event was explicitly selected for Sentry capture. */
sendToSentry: boolean;
/** Whether the default console sink should render this event. */
sendToConsole: boolean;
}
/** A destination for enabled log events. */
@@ -33,8 +37,20 @@ export interface LoggerOptions {
sinks?: readonly LogSink[];
}
/** Explicit Sentry reporting methods. These bypass the logger's normal level threshold. */
export interface SentryLogger {
trace(...arguments_: readonly unknown[]): void;
debug(...arguments_: readonly unknown[]): void;
log(...arguments_: readonly unknown[]): void;
info(...arguments_: readonly unknown[]): void;
warn(...arguments_: readonly unknown[]): void;
error(...arguments_: readonly unknown[]): void;
}
export interface Logger {
readonly namespace?: string;
/** Sends selected events to a configured Sentry sink without changing the default policy for other logs. */
readonly sentry: SentryLogger;
child(namespace: string): Logger;
trace(...arguments_: readonly unknown[]): void;
debug(...arguments_: readonly unknown[]): void;
+20
View File
@@ -78,6 +78,26 @@ describe("createLogger", () => {
consoleError.mockRestore();
});
it("sends explicitly selected production events to Sentry 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.sentry.info("Cache warmed");
expect(destination.events).toHaveLength(1);
expect(destination.events[0]).toMatchObject({
level: "info",
arguments: ["Cache warmed"],
sendToSentry: true,
sendToConsole: false,
});
expect(consoleInfo).not.toHaveBeenCalled();
consoleInfo.mockRestore();
});
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);
+4 -1
View File
@@ -3,7 +3,7 @@ import { createLogger } from "../src/index.js";
import { createSentrySink } from "../src/sentry.js";
describe("createSentrySink", () => {
it("captures errors with namespace and structured context, but ignores warnings", () => {
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 = {
withScope: (callback: (value: typeof scope) => void) => callback(scope),
@@ -17,8 +17,11 @@ describe("createSentrySink", () => {
});
const error = new Error("offline");
logger.warn("ignored");
logger.sentry.info("Cache warmed");
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(scope.setTag).toHaveBeenCalledWith("logger.namespace", "API");
expect(scope.setExtras).toHaveBeenCalledWith({ context_0: { requestId: "req_1" } });
});