Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64b63e1c52 | ||
|
|
6651b713db | ||
|
|
f464fa5017 |
+1
-1
@@ -1 +1 @@
|
|||||||
{ "singleQuote": false, "tabWidth": 4, "trailingComma": "all", "printWidth": 100, "semi": true }
|
{ "singleQuote": true, "tabWidth": 4, "trailingComma": "all", "printWidth": 100, "semi": true }
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ when:
|
|||||||
branch: main
|
branch: main
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
performance: low
|
performance: high
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
verify:
|
verify:
|
||||||
|
|||||||
+2
-3
@@ -1,10 +1,9 @@
|
|||||||
# [1.1.0](https://git.mifi.dev/mifi/logger/compare/v1.0.1...v1.1.0) (2026-08-21)
|
# [1.1.0](https://git.mifi.dev/mifi/logger/compare/v1.0.1...v1.1.0) (2026-08-21)
|
||||||
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* now supports `async` functions for logging data ([c2bcc6c](https://git.mifi.dev/mifi/logger/commit/c2bcc6c82c675640b85e97f1d78dd7c6e627d8f0))
|
- now supports `async` functions for logging data ([c2bcc6c](https://git.mifi.dev/mifi/logger/commit/c2bcc6c82c675640b85e97f1d78dd7c6e627d8f0))
|
||||||
* now supports `async` functions for logging data ([16550ec](https://git.mifi.dev/mifi/logger/commit/16550ec8bb7810ccc36c36cbaab5542e4ad66dae))
|
- now supports `async` functions for logging data ([16550ec](https://git.mifi.dev/mifi/logger/commit/16550ec8bb7810ccc36c36cbaab5542e4ad66dae))
|
||||||
|
|
||||||
## [1.0.1](https://git.mifi.dev/mifi/logger/compare/v1.0.0...v1.0.1) (2026-08-07)
|
## [1.0.1](https://git.mifi.dev/mifi/logger/compare/v1.0.0...v1.0.1) (2026-08-07)
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
Intentional, namespaced logging for TypeScript browser and Node.js applications.
|
Intentional, namespaced logging for TypeScript browser and Node.js applications.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createLogger } from "@mifi/logger";
|
import { createLogger } from '@mifi/logger';
|
||||||
|
|
||||||
const logger = createLogger({ environment: "production", namespace: "WIDGET" });
|
const logger = createLogger({ environment: 'production', namespace: 'WIDGET' });
|
||||||
logger.child("Button").debug("Rendered", () => ({ expensive: "only evaluated when shown" }));
|
logger.child('Button').debug('Rendered', () => ({ expensive: 'only evaluated when shown' }));
|
||||||
|
logger.debug('response', async () => ({ data: await fetchBody() })); // settled before sinks
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -20,22 +21,23 @@ Requires Node.js `>=24`. Published to the private `@mifi` registry.
|
|||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createLogger } from "@mifi/logger";
|
import { createLogger } from '@mifi/logger';
|
||||||
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "development", // "development" | "staging" | "production"
|
environment: 'development', // "development" | "staging" | "production"
|
||||||
namespace: "API", // optional; string or string[] joined with ":"
|
namespace: 'API', // optional; string or string[] joined with ":"
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.trace("very detailed");
|
logger.trace('very detailed');
|
||||||
logger.debug("diagnostic", () => ({ snapshot: heavyWork() }));
|
logger.debug('diagnostic', () => ({ snapshot: heavyWork() }));
|
||||||
logger.info("ready");
|
logger.debug('response', async () => ({ data: await res.json(), url: res.url }));
|
||||||
logger.warn("slow response", { ms: 1200 });
|
logger.info('ready');
|
||||||
logger.error("request failed", error);
|
logger.warn('slow response', { ms: 1200 });
|
||||||
logger.assert(userId, "missing user id");
|
logger.error('request failed', error);
|
||||||
|
logger.assert(userId, 'missing user id');
|
||||||
|
|
||||||
const auth = logger.child("auth"); // namespace → "API:auth"
|
const auth = logger.child('auth'); // namespace → "API:auth"
|
||||||
auth.info("token refreshed");
|
auth.info('token refreshed');
|
||||||
```
|
```
|
||||||
|
|
||||||
Console utility methods (`group`, `groupCollapsed`, `table`, `time`, `count`, `dir`, …) are available and gated by the same level policy as `info`.
|
Console utility methods (`group`, `groupCollapsed`, `table`, `time`, `count`, `dir`, …) are available and gated by the same level policy as `info`.
|
||||||
@@ -64,9 +66,9 @@ Highest wins:
|
|||||||
### Browser session override
|
### Browser session override
|
||||||
|
|
||||||
```js
|
```js
|
||||||
sessionStorage.setItem("showLoggingFor", "debug");
|
sessionStorage.setItem('showLoggingFor', 'debug');
|
||||||
// or restrict to namespaces (exact or descendants):
|
// or restrict to namespaces (exact or descendants):
|
||||||
sessionStorage.setItem("showLoggingFor", "debug:WIDGET,API");
|
sessionStorage.setItem('showLoggingFor', 'debug:WIDGET,API');
|
||||||
```
|
```
|
||||||
|
|
||||||
`WIDGET` matches `WIDGET` and `WIDGET:Button`.
|
`WIDGET` matches `WIDGET` and `WIDGET:Button`.
|
||||||
@@ -127,20 +129,20 @@ Override with `logLevel` on `createSentrySink`. Console policy stays independent
|
|||||||
`trace` / `debug` / `log` / `info` / `warn` / `error` (and `assert`) accept a trailing options object. Console utilities (`group`, `time`, …) do not.
|
`trace` / `debug` / `log` / `info` / `warn` / `error` (and `assert`) accept a trailing options object. Console utilities (`group`, `time`, …) do not.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from '@sentry/nextjs';
|
||||||
import { createLogger } from "@mifi/logger";
|
import { createLogger } from '@mifi/logger';
|
||||||
import { createSentrySink } from "@mifi/logger/sentry";
|
import { createSentrySink } from '@mifi/logger/sentry';
|
||||||
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
namespace: "API",
|
namespace: 'API',
|
||||||
sentry: createSentrySink(Sentry, { logs: true }),
|
sentry: createSentrySink(Sentry, { logs: true }),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.error("Request failed", error, { requestId: "req_1" });
|
logger.error('Request failed', error, { requestId: 'req_1' });
|
||||||
logger.warn("slow", { ms: 1200 }); // → Sentry Logs in production
|
logger.warn('slow', { ms: 1200 }); // → Sentry Logs in production
|
||||||
logger.info("investigating", { orderId }, { sentry: true }); // force Logs
|
logger.info('investigating', { orderId }, { sentry: true }); // force Logs
|
||||||
logger.error("expected", err, { suppressSentry: true }); // console only (when enabled)
|
logger.error('expected', err, { suppressSentry: true }); // console only (when enabled)
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | Effect |
|
| Option | Effect |
|
||||||
@@ -150,7 +152,7 @@ logger.error("expected", err, { suppressSentry: true }); // console only (when e
|
|||||||
|
|
||||||
A last argument is treated as options only when every key is `sentry` or `suppressSentry`. Prefer the third-argument form when you also pass data.
|
A last argument is treated as options only when every key is `sentry` or `suppressSentry`. Prefer the third-argument form when you also pass data.
|
||||||
|
|
||||||
Logs payload: string message + flat primitive attributes (including `logger.namespace`). Nested objects are not unfurled.
|
Logs payload: string message + flat primitive attributes (including `logger.namespace`). Nested objects are not unfurled. When emit was deferred for thenables, the payload also includes `async: true`.
|
||||||
|
|
||||||
Issue severities use Sentry’s union (`warning`, not logger `warn`). The sink maps `warn` → `warning` and `trace` → `debug` for Issues only; Logs keep logger level names (`sentry.logger.warn`).
|
Issue severities use Sentry’s union (`warning`, not logger `warn`). The sink maps `warn` → `warning` and `trace` → `debug` for Issues only; Logs keep logger level names (`sentry.logger.warn`).
|
||||||
|
|
||||||
@@ -159,16 +161,17 @@ Issue severities use Sentry’s union (`warning`, not logger `warn`). The sink m
|
|||||||
Replace the default destinations entirely with `sinks`:
|
Replace the default destinations entirely with `sinks`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { createLogger, createConsoleSink, type LogSink } from "@mifi/logger";
|
import { createLogger, createConsoleSink, type LogSink } from '@mifi/logger';
|
||||||
|
|
||||||
const analyticsSink: LogSink = {
|
const analyticsSink: LogSink = {
|
||||||
emit(event) {
|
emit(event) {
|
||||||
if (event.sendToConsole) analytics.track("log", event);
|
if (event.sendToConsole) analytics.track('log', event);
|
||||||
|
// event.async === true when args were settled asynchronously
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
sinks: [createConsoleSink(), analyticsSink],
|
sinks: [createConsoleSink(), analyticsSink],
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
@@ -192,16 +195,46 @@ All public APIs include JSDoc with parameter and example documentation in the Ty
|
|||||||
|
|
||||||
## Lazy arguments
|
## Lazy arguments
|
||||||
|
|
||||||
Any function passed **after** the first argument is invoked only if the event is emitted:
|
Any function passed **after** the first argument is invoked only if the event will actually be emitted (console and/or Sentry). That keeps expensive snapshots cheap when the level is suppressed.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
logger.debug("state", () => buildHugeSnapshot()); // skipped when debug is suppressed
|
logger.debug('state', () => buildHugeSnapshot()); // skipped when debug is suppressed
|
||||||
logger.debug("response", async () => ({ data: await res.json(), url }));
|
logger.error('failed', error, () => ({ body: pendingBody }));
|
||||||
```
|
```
|
||||||
|
|
||||||
Top-level thenables (including Promises returned from lazy factories, or a Promise passed directly as an argument) are settled before sinks run. The log call stays fire-and-forget (`void`). Settled events set `LogEvent.async`, and the console sink renders an `[async]` label suffix. Nested promises inside plain objects are not walked — compose them inside the async factory. Async emits may appear out of order relative to sync logs.
|
### Async factories and Promises
|
||||||
|
|
||||||
Rejection reasons replace rejected thenables in that argument slot; other arguments still emit.
|
Factories may be `async`, or you may pass a top-level `Promise` as an argument. Thenables are **settled before sinks run**; the log method stays fire-and-forget (`void`).
|
||||||
|
|
||||||
|
```ts
|
||||||
|
logger.debug('response', async () => ({
|
||||||
|
data: await res.json(),
|
||||||
|
url: res.url,
|
||||||
|
}));
|
||||||
|
|
||||||
|
logger.error('failed', { id }, responsePromise);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Behavior | Detail |
|
||||||
|
| ----------------- | -------------------------------------------------------------------------------------------- |
|
||||||
|
| Unwrap scope | **Shallow** — only top-level thenables in the argument list |
|
||||||
|
| Nested promises | Not walked; compose with `await` / `Promise.all` inside the factory |
|
||||||
|
| Call return type | Still `void` (no `await logger.debug(...)`) |
|
||||||
|
| Timestamp | Captured at call time (before settle), so async logs correlate with the failure moment |
|
||||||
|
| `LogEvent.async` | `true` when emit was deferred; omitted for fully sync emits |
|
||||||
|
| Console | Label gains an `[async]` suffix, e.g. `[API][async]` |
|
||||||
|
| Sentry Logs | Attribute `async: true` when set |
|
||||||
|
| Ordering | Async emits may appear after later sync logs |
|
||||||
|
| Rejections | Rejection `reason` replaces that argument slot; other args still emit (`Promise.allSettled`) |
|
||||||
|
| Console utilities | `group` / `time` / etc. do not settle thenables (emit path only) |
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Nested promises — compose inside the factory:
|
||||||
|
logger.debug('route', async () => {
|
||||||
|
const [body, next] = await Promise.all([res.json(), nextRoute]);
|
||||||
|
return { body, next };
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
|
|||||||
Generated
+962
-962
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -3,7 +3,7 @@ const releaseToken = process.env.GITEA_RELEASE_TOKEN;
|
|||||||
const repositoryUrl =
|
const repositoryUrl =
|
||||||
releaseUsername && releaseToken
|
releaseUsername && releaseToken
|
||||||
? `https://${encodeURIComponent(releaseUsername)}:${encodeURIComponent(releaseToken)}@git.mifi.dev/mifi/logger.git`
|
? `https://${encodeURIComponent(releaseUsername)}:${encodeURIComponent(releaseToken)}@git.mifi.dev/mifi/logger.git`
|
||||||
: "https://git.mifi.dev/mifi/logger.git";
|
: 'https://git.mifi.dev/mifi/logger.git';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default Angular preset rejects `feat!` / `fix!` headers entirely (no release).
|
* Default Angular preset rejects `feat!` / `fix!` headers entirely (no release).
|
||||||
@@ -12,23 +12,23 @@ const repositoryUrl =
|
|||||||
const conventionalCommitParserOpts = {
|
const conventionalCommitParserOpts = {
|
||||||
headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/,
|
headerPattern: /^(\w*)(?:\((.*)\))?!?: (.*)$/,
|
||||||
breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/,
|
breakingHeaderPattern: /^(\w*)(?:\((.*)\))?!: (.*)$/,
|
||||||
noteKeywords: ["BREAKING CHANGE", "BREAKING-CHANGE"],
|
noteKeywords: ['BREAKING CHANGE', 'BREAKING-CHANGE'],
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
branches: ["main"],
|
branches: ['main'],
|
||||||
repositoryUrl,
|
repositoryUrl,
|
||||||
tagFormat: "v${version}",
|
tagFormat: 'v${version}',
|
||||||
plugins: [
|
plugins: [
|
||||||
["@semantic-release/commit-analyzer", { parserOpts: conventionalCommitParserOpts }],
|
['@semantic-release/commit-analyzer', { parserOpts: conventionalCommitParserOpts }],
|
||||||
["@semantic-release/release-notes-generator", { parserOpts: conventionalCommitParserOpts }],
|
['@semantic-release/release-notes-generator', { parserOpts: conventionalCommitParserOpts }],
|
||||||
["@semantic-release/changelog", { changelogFile: "CHANGELOG.md" }],
|
['@semantic-release/changelog', { changelogFile: 'CHANGELOG.md' }],
|
||||||
"@semantic-release/npm",
|
'@semantic-release/npm',
|
||||||
[
|
[
|
||||||
"@semantic-release/git",
|
'@semantic-release/git',
|
||||||
{
|
{
|
||||||
assets: ["CHANGELOG.md", "package.json", "pnpm-lock.yaml"],
|
assets: ['CHANGELOG.md', 'package.json', 'pnpm-lock.yaml'],
|
||||||
message: "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
|
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
+12
-9
@@ -14,12 +14,15 @@
|
|||||||
* allows `warn` and `error`).
|
* allows `warn` and `error`).
|
||||||
*/
|
*/
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
TRACE = "trace",
|
TRACE = 'trace',
|
||||||
DEBUG = "debug",
|
DEBUG = 'debug',
|
||||||
INFO = "info",
|
FATAL = 'fatal',
|
||||||
WARN = "warn",
|
INFO = 'info',
|
||||||
ERROR = "error",
|
LOG = 'log',
|
||||||
SILENT = "silent",
|
WARN = 'warn',
|
||||||
|
WARNING = 'warning',
|
||||||
|
ERROR = 'error',
|
||||||
|
SILENT = 'silent',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +30,7 @@ export enum LogLevel {
|
|||||||
* explicitly or via runtime overrides.
|
* explicitly or via runtime overrides.
|
||||||
*/
|
*/
|
||||||
export enum LoggerEnvironment {
|
export enum LoggerEnvironment {
|
||||||
DEVELOPMENT = "development",
|
DEVELOPMENT = 'development',
|
||||||
STAGING = "staging",
|
STAGING = 'staging',
|
||||||
PRODUCTION = "production",
|
PRODUCTION = 'production',
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -11,6 +11,16 @@
|
|||||||
* logger.info("started");
|
* logger.info("started");
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export * from "./logger";
|
export * from './constants';
|
||||||
export * from "./sinks/console";
|
export * from './logger';
|
||||||
export * from "./types";
|
export * from './sinks/console';
|
||||||
|
export type {
|
||||||
|
LogLevel,
|
||||||
|
LoggerEnvironment,
|
||||||
|
LoggerOptions,
|
||||||
|
Logger,
|
||||||
|
LogData,
|
||||||
|
LogCallOptions,
|
||||||
|
LogEvent,
|
||||||
|
LogSink,
|
||||||
|
} from './types';
|
||||||
|
|||||||
+57
-57
@@ -1,4 +1,4 @@
|
|||||||
import { createConsoleSink } from "./sinks/console";
|
import { createConsoleSink } from './sinks/console';
|
||||||
import type {
|
import type {
|
||||||
LogCallOptions,
|
LogCallOptions,
|
||||||
LogEvent,
|
LogEvent,
|
||||||
@@ -7,24 +7,24 @@ import type {
|
|||||||
LoggerOptions,
|
LoggerOptions,
|
||||||
LogLevel,
|
LogLevel,
|
||||||
LogSink,
|
LogSink,
|
||||||
} from "./types";
|
} from './types';
|
||||||
|
|
||||||
/** Duck-type for sinks created by `createSentrySink` (avoids importing the sentry entry). */
|
/** Duck-type for sinks created by `createSentrySink` (avoids importing the sentry entry). */
|
||||||
type SentrySinkLike = LogSink & {
|
type SentrySinkLike = LogSink & {
|
||||||
kind: "sentry";
|
kind: 'sentry';
|
||||||
options: { logs?: boolean; logLevel?: LogLevel };
|
options: { logs?: boolean; logLevel?: LogLevel };
|
||||||
};
|
};
|
||||||
|
|
||||||
function isSentrySinkLike(sink: LogSink): sink is SentrySinkLike {
|
function isSentrySinkLike(sink: LogSink): sink is SentrySinkLike {
|
||||||
return "kind" in sink && (sink as { kind?: unknown }).kind === "sentry";
|
return 'kind' in sink && (sink as { kind?: unknown }).kind === 'sentry';
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEVELS: readonly Exclude<LogLevel, "silent">[] = ["trace", "debug", "info", "warn", "error"];
|
const LEVELS: readonly Exclude<LogLevel, 'silent'>[] = ['trace', 'debug', 'info', 'warn', 'error'];
|
||||||
const LOG_CALL_OPTION_KEYS = new Set(["sentry", "suppressSentry"]);
|
const LOG_CALL_OPTION_KEYS = new Set(['sentry', 'suppressSentry']);
|
||||||
|
|
||||||
/** Numeric rank for comparing levels; `silent` is above every emit level. */
|
/** Numeric rank for comparing levels; `silent` is above every emit level. */
|
||||||
const levelRank = (level: LogLevel): number =>
|
const levelRank = (level: LogLevel): number =>
|
||||||
level === "silent" ? Infinity : LEVELS.indexOf(level);
|
level === 'silent' ? Infinity : LEVELS.indexOf(level);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default {@link LogLevel} for a {@link LoggerEnvironment} when no override is set.
|
* Default {@link LogLevel} for a {@link LoggerEnvironment} when no override is set.
|
||||||
@@ -33,7 +33,7 @@ const levelRank = (level: LogLevel): number =>
|
|||||||
* @returns `trace` (development), `warn` (staging), or `error` (production).
|
* @returns `trace` (development), `warn` (staging), or `error` (production).
|
||||||
*/
|
*/
|
||||||
const defaultLevel = (environment: LoggerEnvironment): LogLevel =>
|
const defaultLevel = (environment: LoggerEnvironment): LogLevel =>
|
||||||
environment === "development" ? "trace" : environment === "staging" ? "warn" : "error";
|
environment === 'development' ? 'trace' : environment === 'staging' ? 'warn' : 'error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default Sentry Logs threshold when `createSentrySink(..., { logs: true })` is
|
* Default Sentry Logs threshold when `createSentrySink(..., { logs: true })` is
|
||||||
@@ -43,7 +43,7 @@ const defaultLevel = (environment: LoggerEnvironment): LogLevel =>
|
|||||||
* @returns `info` (staging) or `warn` (production). Development never sends.
|
* @returns `info` (staging) or `warn` (production). Development never sends.
|
||||||
*/
|
*/
|
||||||
export const defaultSentryLogLevel = (environment: LoggerEnvironment): LogLevel =>
|
export const defaultSentryLogLevel = (environment: LoggerEnvironment): LogLevel =>
|
||||||
environment === "staging" ? "info" : "warn";
|
environment === 'staging' ? 'info' : 'warn';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses a runtime logging override string used by browser session storage and
|
* Parses a runtime logging override string used by browser session storage and
|
||||||
@@ -73,13 +73,13 @@ export function parseLoggingOverride(
|
|||||||
value: string | null | undefined,
|
value: string | null | undefined,
|
||||||
): { level: LogLevel; namespaces: readonly string[] } | undefined {
|
): { level: LogLevel; namespaces: readonly string[] } | undefined {
|
||||||
if (!value) return undefined;
|
if (!value) return undefined;
|
||||||
const [rawLevel, rawNamespaces = ""] = value.trim().split(":", 2);
|
const [rawLevel, rawNamespaces = ''] = value.trim().split(':', 2);
|
||||||
if (!(["trace", "debug", "info", "warn", "error", "silent"] as string[]).includes(rawLevel))
|
if (!(['trace', 'debug', 'info', 'warn', 'error', 'silent'] as string[]).includes(rawLevel))
|
||||||
return undefined;
|
return undefined;
|
||||||
return {
|
return {
|
||||||
level: rawLevel as LogLevel,
|
level: rawLevel as LogLevel,
|
||||||
namespaces: rawNamespaces
|
namespaces: rawNamespaces
|
||||||
.split(",")
|
.split(',')
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
.filter(Boolean),
|
.filter(Boolean),
|
||||||
};
|
};
|
||||||
@@ -91,7 +91,7 @@ export function parseLoggingOverride(
|
|||||||
* @param value - Candidate last argument from a log call.
|
* @param value - Candidate last argument from a log call.
|
||||||
*/
|
*/
|
||||||
export function isLogCallOptions(value: unknown): value is LogCallOptions {
|
export function isLogCallOptions(value: unknown): value is LogCallOptions {
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value) || value instanceof Error)
|
if (!value || typeof value !== 'object' || Array.isArray(value) || value instanceof Error)
|
||||||
return false;
|
return false;
|
||||||
const keys = Object.keys(value);
|
const keys = Object.keys(value);
|
||||||
return keys.length > 0 && keys.every((key) => LOG_CALL_OPTION_KEYS.has(key));
|
return keys.length > 0 && keys.every((key) => LOG_CALL_OPTION_KEYS.has(key));
|
||||||
@@ -119,7 +119,7 @@ function splitLogCallArguments(arguments_: readonly unknown[]): {
|
|||||||
* @param provided - Optional storage; when omitted, uses `globalThis.sessionStorage`.
|
* @param provided - Optional storage; when omitted, uses `globalThis.sessionStorage`.
|
||||||
* @returns A `getItem`-compatible storage, or `undefined` if unavailable.
|
* @returns A `getItem`-compatible storage, or `undefined` if unavailable.
|
||||||
*/
|
*/
|
||||||
function resolveStorage(provided?: Pick<Storage, "getItem">): Pick<Storage, "getItem"> | undefined {
|
function resolveStorage(provided?: Pick<Storage, 'getItem'>): Pick<Storage, 'getItem'> | undefined {
|
||||||
if (provided) return provided;
|
if (provided) return provided;
|
||||||
try {
|
try {
|
||||||
return globalThis.sessionStorage;
|
return globalThis.sessionStorage;
|
||||||
@@ -150,9 +150,9 @@ function resolveEnv(
|
|||||||
* @param provided - Explicit runtime; when omitted, inferred from `globalThis.window`.
|
* @param provided - Explicit runtime; when omitted, inferred from `globalThis.window`.
|
||||||
* @returns `"browser"` if `window` is defined, otherwise `"node"`.
|
* @returns `"browser"` if `window` is defined, otherwise `"node"`.
|
||||||
*/
|
*/
|
||||||
function resolveRuntime(provided?: "browser" | "node"): "browser" | "node" {
|
function resolveRuntime(provided?: 'browser' | 'node'): 'browser' | 'node' {
|
||||||
if (provided) return provided;
|
if (provided) return provided;
|
||||||
return typeof (globalThis as { window?: unknown }).window === "undefined" ? "node" : "browser";
|
return typeof (globalThis as { window?: unknown }).window === 'undefined' ? 'node' : 'browser';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -184,7 +184,7 @@ function namespaceMatches(namespace: string | undefined, filters: readonly strin
|
|||||||
*/
|
*/
|
||||||
function evaluate(arguments_: readonly unknown[]): readonly unknown[] {
|
function evaluate(arguments_: readonly unknown[]): readonly unknown[] {
|
||||||
return arguments_.map((argument, index) =>
|
return arguments_.map((argument, index) =>
|
||||||
index > 0 && typeof argument === "function" ? (argument as () => unknown)() : argument,
|
index > 0 && typeof argument === 'function' ? (argument as () => unknown)() : argument,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,8 +192,8 @@ function evaluate(arguments_: readonly unknown[]): readonly unknown[] {
|
|||||||
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
||||||
return (
|
return (
|
||||||
value !== null &&
|
value !== null &&
|
||||||
(typeof value === "object" || typeof value === "function") &&
|
(typeof value === 'object' || typeof value === 'function') &&
|
||||||
typeof (value as { then?: unknown }).then === "function"
|
typeof (value as { then?: unknown }).then === 'function'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +205,7 @@ async function settleArguments(arguments_: readonly unknown[]): Promise<readonly
|
|||||||
const settled = await Promise.allSettled(
|
const settled = await Promise.allSettled(
|
||||||
arguments_.map((argument) => (isThenable(argument) ? argument : Promise.resolve(argument))),
|
arguments_.map((argument) => (isThenable(argument) ? argument : Promise.resolve(argument))),
|
||||||
);
|
);
|
||||||
return settled.map((result) => (result.status === "fulfilled" ? result.value : result.reason));
|
return settled.map((result) => (result.status === 'fulfilled' ? result.value : result.reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasThenable(arguments_: readonly unknown[]): boolean {
|
function hasThenable(arguments_: readonly unknown[]): boolean {
|
||||||
@@ -219,7 +219,7 @@ function hasThenable(arguments_: readonly unknown[]): boolean {
|
|||||||
function resolveSentrySinkOptions(
|
function resolveSentrySinkOptions(
|
||||||
options: LoggerOptions,
|
options: LoggerOptions,
|
||||||
): { logs: boolean; logLevel: LogLevel } | undefined {
|
): { logs: boolean; logLevel: LogLevel } | undefined {
|
||||||
if (options.environment === "development") return undefined;
|
if (options.environment === 'development') return undefined;
|
||||||
const candidate = options.sentry ? options.sentry : options.sinks?.find(isSentrySinkLike);
|
const candidate = options.sentry ? options.sentry : options.sinks?.find(isSentrySinkLike);
|
||||||
if (!candidate) return undefined;
|
if (!candidate) return undefined;
|
||||||
if (isSentrySinkLike(candidate)) {
|
if (isSentrySinkLike(candidate)) {
|
||||||
@@ -271,15 +271,15 @@ function resolveSentrySinkOptions(
|
|||||||
*/
|
*/
|
||||||
export function createLogger(options: LoggerOptions): Logger {
|
export function createLogger(options: LoggerOptions): Logger {
|
||||||
const namespace =
|
const namespace =
|
||||||
typeof options.namespace === "string"
|
typeof options.namespace === 'string'
|
||||||
? options.namespace
|
? options.namespace
|
||||||
: options.namespace?.filter(Boolean).join(":");
|
: options.namespace?.filter(Boolean).join(':');
|
||||||
const env = resolveEnv(options.env);
|
const env = resolveEnv(options.env);
|
||||||
const runtimeOverride =
|
const runtimeOverride =
|
||||||
parseLoggingOverride(resolveStorage(options.sessionStorage)?.getItem("showLoggingFor")) ??
|
parseLoggingOverride(resolveStorage(options.sessionStorage)?.getItem('showLoggingFor')) ??
|
||||||
parseLoggingOverride(
|
parseLoggingOverride(
|
||||||
env.MIFI_LOG_LEVEL
|
env.MIFI_LOG_LEVEL
|
||||||
? `${env.MIFI_LOG_LEVEL}${env.MIFI_LOG_NAMESPACES ? `:${env.MIFI_LOG_NAMESPACES}` : ""}`
|
? `${env.MIFI_LOG_LEVEL}${env.MIFI_LOG_NAMESPACES ? `:${env.MIFI_LOG_NAMESPACES}` : ''}`
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
);
|
||||||
const threshold = options.level ?? runtimeOverride?.level ?? defaultLevel(options.environment);
|
const threshold = options.level ?? runtimeOverride?.level ?? defaultLevel(options.environment);
|
||||||
@@ -287,26 +287,26 @@ export function createLogger(options: LoggerOptions): Logger {
|
|||||||
const sentrySinkOptions = resolveSentrySinkOptions(options);
|
const sentrySinkOptions = resolveSentrySinkOptions(options);
|
||||||
const sentryActive = sentrySinkOptions !== undefined;
|
const sentryActive = sentrySinkOptions !== undefined;
|
||||||
const sinks: readonly LogSink[] = options.sinks ?? [
|
const sinks: readonly LogSink[] = options.sinks ?? [
|
||||||
...(runtime === "node" || options.environment !== "production" || !options.sentry
|
...(runtime === 'node' || options.environment !== 'production' || !options.sentry
|
||||||
? [createConsoleSink()]
|
? [createConsoleSink()]
|
||||||
: []),
|
: []),
|
||||||
...(options.sentry && options.environment !== "development" ? [options.sentry] : []),
|
...(options.sentry && options.environment !== 'development' ? [options.sentry] : []),
|
||||||
];
|
];
|
||||||
const enabled = (level: Exclude<LogLevel, "silent">) =>
|
const enabled = (level: Exclude<LogLevel, 'silent'>) =>
|
||||||
levelRank(level) >= levelRank(threshold) &&
|
levelRank(level) >= levelRank(threshold) &&
|
||||||
(!runtimeOverride?.namespaces.length ||
|
(!runtimeOverride?.namespaces.length ||
|
||||||
namespaceMatches(namespace, runtimeOverride.namespaces));
|
namespaceMatches(namespace, runtimeOverride.namespaces));
|
||||||
const deliver = (event: LogEvent) => {
|
const deliver = (event: LogEvent) => {
|
||||||
sinks.forEach((sink) => sink.emit(event));
|
sinks.forEach((sink) => sink.emit(event));
|
||||||
};
|
};
|
||||||
const emit = (level: Exclude<LogLevel, "silent">, arguments_: readonly unknown[]) => {
|
const emit = (level: Exclude<LogLevel, 'silent'>, arguments_: readonly unknown[]) => {
|
||||||
const split = splitLogCallArguments(arguments_);
|
const split = splitLogCallArguments(arguments_);
|
||||||
const sendToConsole = enabled(level);
|
const sendToConsole = enabled(level);
|
||||||
const sendToSentryIssue =
|
const sendToSentryIssue =
|
||||||
sentryActive && level === "error" && !split.options.suppressSentry;
|
sentryActive && level === 'error' && !split.options.suppressSentry;
|
||||||
const sendToSentryLogs =
|
const sendToSentryLogs =
|
||||||
sentryActive &&
|
sentryActive &&
|
||||||
level !== "error" &&
|
level !== 'error' &&
|
||||||
(Boolean(split.options.sentry) ||
|
(Boolean(split.options.sentry) ||
|
||||||
(Boolean(sentrySinkOptions?.logs) &&
|
(Boolean(sentrySinkOptions?.logs) &&
|
||||||
levelRank(level) >= levelRank(sentrySinkOptions.logLevel)));
|
levelRank(level) >= levelRank(sentrySinkOptions.logLevel)));
|
||||||
@@ -333,14 +333,14 @@ export function createLogger(options: LoggerOptions): Logger {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const consoleMethod = (
|
const consoleMethod = (
|
||||||
method: keyof Console | "profile" | "profileEnd",
|
method: keyof Console | 'profile' | 'profileEnd',
|
||||||
level: Exclude<LogLevel, "silent">,
|
level: Exclude<LogLevel, 'silent'>,
|
||||||
arguments_: readonly unknown[],
|
arguments_: readonly unknown[],
|
||||||
) => {
|
) => {
|
||||||
if (!enabled(level)) return;
|
if (!enabled(level)) return;
|
||||||
const console_ = globalThis.console as Console & Record<string, unknown>;
|
const console_ = globalThis.console as Console & Record<string, unknown>;
|
||||||
const fn = console_[method];
|
const fn = console_[method];
|
||||||
if (typeof fn !== "function") return;
|
if (typeof fn !== 'function') return;
|
||||||
(fn as (...items: unknown[]) => void).apply(
|
(fn as (...items: unknown[]) => void).apply(
|
||||||
console_,
|
console_,
|
||||||
Array.from(prefix(namespace, evaluate(arguments_))),
|
Array.from(prefix(namespace, evaluate(arguments_))),
|
||||||
@@ -350,30 +350,30 @@ export function createLogger(options: LoggerOptions): Logger {
|
|||||||
namespace,
|
namespace,
|
||||||
child: (child) =>
|
child: (child) =>
|
||||||
createLogger({ ...options, namespace: [namespace, child].filter(Boolean) as string[] }),
|
createLogger({ ...options, namespace: [namespace, child].filter(Boolean) as string[] }),
|
||||||
trace: (...items) => emit("trace", items),
|
trace: (...items) => emit('trace', items),
|
||||||
debug: (...items) => emit("debug", items),
|
debug: (...items) => emit('debug', items),
|
||||||
log: (...items) => emit("info", items),
|
log: (...items) => emit('info', items),
|
||||||
info: (...items) => emit("info", items),
|
info: (...items) => emit('info', items),
|
||||||
warn: (...items) => emit("warn", items),
|
warn: (...items) => emit('warn', items),
|
||||||
error: (...items) => emit("error", items),
|
error: (...items) => emit('error', items),
|
||||||
assert: (condition, ...items) => {
|
assert: (condition, ...items) => {
|
||||||
if (!condition) emit("error", ["Assertion failed", ...items]);
|
if (!condition) emit('error', ['Assertion failed', ...items]);
|
||||||
},
|
},
|
||||||
group: (...items) => consoleMethod("group", "info", items),
|
group: (...items) => consoleMethod('group', 'info', items),
|
||||||
groupCollapsed: (...items) => consoleMethod("groupCollapsed", "info", items),
|
groupCollapsed: (...items) => consoleMethod('groupCollapsed', 'info', items),
|
||||||
groupEnd: () => consoleMethod("groupEnd", "info", []),
|
groupEnd: () => consoleMethod('groupEnd', 'info', []),
|
||||||
dir: (item, options_) => consoleMethod("dir", "info", [item, options_]),
|
dir: (item, options_) => consoleMethod('dir', 'info', [item, options_]),
|
||||||
dirxml: (...items) => consoleMethod("dirxml", "info", items),
|
dirxml: (...items) => consoleMethod('dirxml', 'info', items),
|
||||||
table: (data, properties) => consoleMethod("table", "info", [data, properties]),
|
table: (data, properties) => consoleMethod('table', 'info', [data, properties]),
|
||||||
clear: () => consoleMethod("clear", "info", []),
|
clear: () => consoleMethod('clear', 'info', []),
|
||||||
count: (label) => consoleMethod("count", "info", [label]),
|
count: (label) => consoleMethod('count', 'info', [label]),
|
||||||
countReset: (label) => consoleMethod("countReset", "info", [label]),
|
countReset: (label) => consoleMethod('countReset', 'info', [label]),
|
||||||
time: (label) => consoleMethod("time", "info", [label]),
|
time: (label) => consoleMethod('time', 'info', [label]),
|
||||||
timeLog: (label, ...items) => consoleMethod("timeLog", "info", [label, ...items]),
|
timeLog: (label, ...items) => consoleMethod('timeLog', 'info', [label, ...items]),
|
||||||
timeEnd: (label) => consoleMethod("timeEnd", "info", [label]),
|
timeEnd: (label) => consoleMethod('timeEnd', 'info', [label]),
|
||||||
timeStamp: (label) => consoleMethod("timeStamp", "info", [label]),
|
timeStamp: (label) => consoleMethod('timeStamp', 'info', [label]),
|
||||||
profile: (label) => consoleMethod("profile", "info", [label]),
|
profile: (label) => consoleMethod('profile', 'info', [label]),
|
||||||
profileEnd: (label) => consoleMethod("profileEnd", "info", [label]),
|
profileEnd: (label) => consoleMethod('profileEnd', 'info', [label]),
|
||||||
};
|
};
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
@@ -386,5 +386,5 @@ export function createLogger(options: LoggerOptions): Logger {
|
|||||||
* @returns Arguments with `[A][B]` prefix when namespaced.
|
* @returns Arguments with `[A][B]` prefix when namespaced.
|
||||||
*/
|
*/
|
||||||
function prefix(namespace: string | undefined, arguments_: readonly unknown[]): readonly unknown[] {
|
function prefix(namespace: string | undefined, arguments_: readonly unknown[]): readonly unknown[] {
|
||||||
return namespace ? [`[${namespace.split(":").join("][")}]`, ...arguments_] : arguments_;
|
return namespace ? [`[${namespace.split(':').join('][')}]`, ...arguments_] : arguments_;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
/** Public entry for `@mifi/logger/sentry`. Implementation: `src/sinks/sentry.ts`. */
|
/** Public entry for `@mifi/logger/sentry`. Implementation: `src/sinks/sentry.ts`. */
|
||||||
export * from "./sinks/sentry";
|
export * from './sinks/sentry';
|
||||||
|
|||||||
+24
-20
@@ -1,24 +1,28 @@
|
|||||||
import type { LogLevel, LogSink } from "../types";
|
import { LogLevel } from '../constants';
|
||||||
|
import type { LogSink } from '../types';
|
||||||
|
|
||||||
const ANSI_RESET = "\u001B[0m";
|
type ConsoleLogLevel =
|
||||||
const ANSI_BY_LEVEL: Record<Exclude<LogLevel, "silent">, string> = {
|
`${Exclude<LogLevel, LogLevel.FATAL | LogLevel.LOG | LogLevel.SILENT | LogLevel.WARNING>}`;
|
||||||
trace: "\u001B[90m",
|
|
||||||
debug: "\u001B[36m",
|
const ANSI_RESET = '\u001B[0m';
|
||||||
info: "\u001B[32m",
|
const ANSI_BY_LEVEL: Record<ConsoleLogLevel, string> = {
|
||||||
warn: "\u001B[33m",
|
trace: '\u001B[90m',
|
||||||
error: "\u001B[31m",
|
debug: '\u001B[36m',
|
||||||
|
info: '\u001B[32m',
|
||||||
|
warn: '\u001B[33m',
|
||||||
|
error: '\u001B[31m',
|
||||||
};
|
};
|
||||||
const BROWSER_STYLE_BY_LEVEL: Record<Exclude<LogLevel, "silent">, string> = {
|
const BROWSER_STYLE_BY_LEVEL: Record<ConsoleLogLevel, string> = {
|
||||||
trace: "color: #6b7280; font-weight: 600",
|
trace: 'color: #6b7280; font-weight: 600',
|
||||||
debug: "color: #0891b2; font-weight: 600",
|
debug: 'color: #0891b2; font-weight: 600',
|
||||||
info: "color: #15803d; font-weight: 600",
|
info: 'color: #15803d; font-weight: 600',
|
||||||
warn: "color: #a16207; font-weight: 700",
|
warn: 'color: #a16207; font-weight: 700',
|
||||||
error: "color: #dc2626; font-weight: 700",
|
error: 'color: #dc2626; font-weight: 700',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Whether the current global looks like a browser (`window` is defined). */
|
/** Whether the current global looks like a browser (`window` is defined). */
|
||||||
function isBrowser(): boolean {
|
function isBrowser(): boolean {
|
||||||
return typeof (globalThis as { window?: unknown }).window !== "undefined";
|
return typeof (globalThis as { window?: unknown }).window !== 'undefined';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether Node stdout is a TTY that can display ANSI colors. */
|
/** Whether Node stdout is a TTY that can display ANSI colors. */
|
||||||
@@ -51,14 +55,14 @@ function supportsAnsi(): boolean {
|
|||||||
export function createConsoleSink(): LogSink {
|
export function createConsoleSink(): LogSink {
|
||||||
return {
|
return {
|
||||||
emit: (event) => {
|
emit: (event) => {
|
||||||
if (!event.sendToConsole) return;
|
if (!event.sendToConsole || event.level === LogLevel.SILENT) return;
|
||||||
const method = event.level === "trace" ? "debug" : event.level;
|
const method = event.level === 'trace' ? 'debug' : event.level;
|
||||||
const console_ = globalThis.console as Console & Record<string, unknown>;
|
const console_ = globalThis.console as Console & Record<string, unknown>;
|
||||||
const fn = console_[method];
|
const fn = console_[method];
|
||||||
if (typeof fn !== "function") return;
|
if (typeof fn !== 'function') return;
|
||||||
const labelBase = event.namespace
|
const labelBase = event.namespace
|
||||||
? `[${event.namespace.split(":").join("][")}]`
|
? `[${event.namespace.split(':').join('][')}]`
|
||||||
: "[LOG]";
|
: '[LOG]';
|
||||||
const label = event.async ? `${labelBase}[async]` : labelBase;
|
const label = event.async ? `${labelBase}[async]` : labelBase;
|
||||||
const arguments_ = isBrowser()
|
const arguments_ = isBrowser()
|
||||||
? [`%c${label}`, BROWSER_STYLE_BY_LEVEL[event.level], ...event.arguments]
|
? [`%c${label}`, BROWSER_STYLE_BY_LEVEL[event.level], ...event.arguments]
|
||||||
|
|||||||
+23
-18
@@ -1,10 +1,12 @@
|
|||||||
import type { LogEvent, LogLevel, LogSink } from "../types";
|
import type { LogEvent, LogSink } from '../types';
|
||||||
|
import { LogLevel } from '../constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sentry Issue severity levels (`@sentry/core` `SeverityLevel`).
|
* Sentry Issue severity levels (`@sentry/core` `SeverityLevel`).
|
||||||
* Note: Issues use `"warning"`; Logs use `"warn"`.
|
* Note: Issues use `"warning"`; Logs use `"warn"`.
|
||||||
*/
|
*/
|
||||||
export type SentrySeverityLevel = "fatal" | "error" | "warning" | "log" | "info" | "debug";
|
export type SentrySeverityLevel =
|
||||||
|
`${Exclude<LogLevel, LogLevel.SILENT | LogLevel.TRACE | LogLevel.WARN>}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options for {@link createSentrySink}.
|
* Options for {@link createSentrySink}.
|
||||||
@@ -23,7 +25,7 @@ export interface SentrySinkOptions {
|
|||||||
* Minimum level for automatic Sentry Logs delivery when {@link SentrySinkOptions.logs}
|
* Minimum level for automatic Sentry Logs delivery when {@link SentrySinkOptions.logs}
|
||||||
* is enabled. Per-call `{ sentry: true }` bypasses this threshold.
|
* is enabled. Per-call `{ sentry: true }` bypasses this threshold.
|
||||||
*/
|
*/
|
||||||
logLevel?: LogLevel;
|
logLevel?: Exclude<LogLevel, LogLevel.SILENT>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,7 +104,7 @@ export interface SentryLike {
|
|||||||
|
|
||||||
/** Sink returned by {@link createSentrySink}. */
|
/** Sink returned by {@link createSentrySink}. */
|
||||||
export interface SentrySink extends LogSink {
|
export interface SentrySink extends LogSink {
|
||||||
readonly kind: "sentry";
|
readonly kind: 'sentry';
|
||||||
readonly options: SentrySinkOptions;
|
readonly options: SentrySinkOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +114,7 @@ export interface SentrySink extends LogSink {
|
|||||||
* @param sink - Any {@link LogSink}.
|
* @param sink - Any {@link LogSink}.
|
||||||
*/
|
*/
|
||||||
export function isSentrySink(sink: LogSink): sink is SentrySink {
|
export function isSentrySink(sink: LogSink): sink is SentrySink {
|
||||||
return "kind" in sink && (sink as { kind?: unknown }).kind === "sentry";
|
return 'kind' in sink && (sink as { kind?: unknown }).kind === 'sentry';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -124,14 +126,16 @@ export function isSentrySink(sink: LogSink): sink is SentrySink {
|
|||||||
*
|
*
|
||||||
* @param level - Logger emit level (never `silent`).
|
* @param level - Logger emit level (never `silent`).
|
||||||
*/
|
*/
|
||||||
export function toSentrySeverity(level: Exclude<LogLevel, "silent">): SentrySeverityLevel {
|
export function toSentrySeverity(
|
||||||
if (level === "warn") return "warning";
|
level: `${Exclude<LogLevel, LogLevel.SILENT>}`,
|
||||||
if (level === "trace") return "debug";
|
): SentrySeverityLevel {
|
||||||
|
if (level === LogLevel.WARN) return LogLevel.WARNING;
|
||||||
|
if (level === LogLevel.TRACE) return LogLevel.DEBUG;
|
||||||
return level;
|
return level;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAttributeValue(value: unknown): value is string | number | boolean {
|
function isAttributeValue(value: unknown): value is string | number | boolean {
|
||||||
return typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -144,12 +148,12 @@ export function toSentryLogPayload(event: LogEvent): {
|
|||||||
attributes: Record<string, string | number | boolean>;
|
attributes: Record<string, string | number | boolean>;
|
||||||
} {
|
} {
|
||||||
const message =
|
const message =
|
||||||
event.arguments.filter((item) => typeof item === "string").join(" ") || "Log event";
|
event.arguments.filter((item) => typeof item === 'string').join(' ') || 'Log event';
|
||||||
const attributes: Record<string, string | number | boolean> = {};
|
const attributes: Record<string, string | number | boolean> = {};
|
||||||
if (event.namespace) attributes["logger.namespace"] = event.namespace;
|
if (event.namespace) attributes['logger.namespace'] = event.namespace;
|
||||||
if (event.async) attributes.async = true;
|
if (event.async) attributes.async = true;
|
||||||
for (const item of event.arguments) {
|
for (const item of event.arguments) {
|
||||||
if (!item || typeof item !== "object" || item instanceof Error || Array.isArray(item))
|
if (!item || typeof item !== 'object' || item instanceof Error || Array.isArray(item))
|
||||||
continue;
|
continue;
|
||||||
for (const [key, value] of Object.entries(item as Record<string, unknown>)) {
|
for (const [key, value] of Object.entries(item as Record<string, unknown>)) {
|
||||||
if (isAttributeValue(value)) attributes[key] = value;
|
if (isAttributeValue(value)) attributes[key] = value;
|
||||||
@@ -198,17 +202,18 @@ export function toSentryLogPayload(event: LogEvent): {
|
|||||||
*/
|
*/
|
||||||
export function createSentrySink(sentry: SentryLike, options: SentrySinkOptions = {}): SentrySink {
|
export function createSentrySink(sentry: SentryLike, options: SentrySinkOptions = {}): SentrySink {
|
||||||
return {
|
return {
|
||||||
kind: "sentry",
|
kind: 'sentry',
|
||||||
options,
|
options,
|
||||||
emit(event: LogEvent) {
|
emit(event: LogEvent) {
|
||||||
if (event.environment === "development") return;
|
if (event.environment === 'development') return;
|
||||||
|
if (event.level === LogLevel.SILENT) return;
|
||||||
if (event.sendToSentryIssue) {
|
if (event.sendToSentryIssue) {
|
||||||
const error = event.arguments.find((item): item is Error => item instanceof Error);
|
const error = event.arguments.find((item): item is Error => item instanceof Error);
|
||||||
const { message, attributes } = toSentryLogPayload(event);
|
const { message, attributes } = toSentryLogPayload(event);
|
||||||
const severity = toSentrySeverity(event.level);
|
const severity = toSentrySeverity(event.level);
|
||||||
sentry.withScope((scope) => {
|
sentry.withScope((scope) => {
|
||||||
scope.setLevel(severity);
|
scope.setLevel(severity);
|
||||||
if (event.namespace) scope.setTag("logger.namespace", event.namespace);
|
if (event.namespace) scope.setTag('logger.namespace', event.namespace);
|
||||||
scope.setExtras(attributes);
|
scope.setExtras(attributes);
|
||||||
if (error) sentry.captureException(error);
|
if (error) sentry.captureException(error);
|
||||||
else sentry.captureMessage(message, severity);
|
else sentry.captureMessage(message, severity);
|
||||||
@@ -216,9 +221,9 @@ export function createSentrySink(sentry: SentryLike, options: SentrySinkOptions
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!event.sendToSentryLogs || !sentry.logger) return;
|
if (!event.sendToSentryLogs || !sentry.logger) return;
|
||||||
const method = event.level === "trace" ? "trace" : event.level;
|
const method = event.level === 'trace' ? 'trace' : event.level;
|
||||||
const log = sentry.logger[method];
|
const log = sentry.logger[method as keyof SentryLoggerApiLike];
|
||||||
if (typeof log !== "function") return;
|
if (typeof log !== 'function') return;
|
||||||
const { message, attributes } = toSentryLogPayload(event);
|
const { message, attributes } = toSentryLogPayload(event);
|
||||||
log.call(sentry.logger, message, attributes);
|
log.call(sentry.logger, message, attributes);
|
||||||
},
|
},
|
||||||
|
|||||||
+6
-5
@@ -1,4 +1,4 @@
|
|||||||
import { LogLevel as LogLevelEnum, LoggerEnvironment as LoggerEnvironmentEnum } from "./constants";
|
import { LogLevel as LogLevelEnum, LoggerEnvironment as LoggerEnvironmentEnum } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Severity threshold for log filtering, ordered from most to least verbose.
|
* Severity threshold for log filtering, ordered from most to least verbose.
|
||||||
@@ -15,7 +15,8 @@ import { LogLevel as LogLevelEnum, LoggerEnvironment as LoggerEnvironmentEnum }
|
|||||||
* A logger emits events at or above its configured threshold (e.g. `warn`
|
* A logger emits events at or above its configured threshold (e.g. `warn`
|
||||||
* allows `warn` and `error`).
|
* allows `warn` and `error`).
|
||||||
*/
|
*/
|
||||||
export type LogLevel = `${LogLevelEnum}`;
|
export type LogLevel =
|
||||||
|
`${Exclude<LogLevelEnum, LogLevelEnum.FATAL | LogLevelEnum.LOG | LogLevelEnum.WARNING>}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deployment stage used to pick a default log level when none is configured
|
* Deployment stage used to pick a default log level when none is configured
|
||||||
@@ -70,7 +71,7 @@ export interface LogCallOptions {
|
|||||||
*/
|
*/
|
||||||
export interface LogEvent {
|
export interface LogEvent {
|
||||||
/** Severity of this event. Never `silent`. */
|
/** Severity of this event. Never `silent`. */
|
||||||
level: Exclude<LogLevel, "silent">;
|
level: Exclude<LogLevel, LogLevelEnum.SILENT>;
|
||||||
/**
|
/**
|
||||||
* Colon-joined namespace for this logger (e.g. `"WIDGET:Button"`).
|
* Colon-joined namespace for this logger (e.g. `"WIDGET:Button"`).
|
||||||
* Omitted when the logger was created without a namespace.
|
* Omitted when the logger was created without a namespace.
|
||||||
@@ -165,7 +166,7 @@ export interface LoggerOptions {
|
|||||||
* Runtime used for the default destination policy (console vs Sentry-only
|
* Runtime used for the default destination policy (console vs Sentry-only
|
||||||
* in production browsers). Inferred from `globalThis.window` when omitted.
|
* in production browsers). Inferred from `globalThis.window` when omitted.
|
||||||
*/
|
*/
|
||||||
runtime?: "browser" | "node";
|
runtime?: 'browser' | 'node';
|
||||||
/**
|
/**
|
||||||
* Hard level override. Takes precedence over session storage, env vars,
|
* Hard level override. Takes precedence over session storage, env vars,
|
||||||
* and environment defaults.
|
* and environment defaults.
|
||||||
@@ -178,7 +179,7 @@ export interface LoggerOptions {
|
|||||||
* Expected value format matches {@link parseLoggingOverride}:
|
* Expected value format matches {@link parseLoggingOverride}:
|
||||||
* `debug` or `debug:WIDGET,API`.
|
* `debug` or `debug:WIDGET,API`.
|
||||||
*/
|
*/
|
||||||
sessionStorage?: Pick<Storage, "getItem">;
|
sessionStorage?: Pick<Storage, 'getItem'>;
|
||||||
/**
|
/**
|
||||||
* Node/container environment variable map. Defaults to `process.env`
|
* Node/container environment variable map. Defaults to `process.env`
|
||||||
* when available.
|
* when available.
|
||||||
|
|||||||
+15
-15
@@ -1,14 +1,14 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { createConsoleSink } from "../src/index";
|
import { createConsoleSink } from '../src/index';
|
||||||
import type { LogEvent } from "../src/index";
|
import type { LogEvent } from '../src/index';
|
||||||
|
|
||||||
function event(overrides: Partial<LogEvent> = {}): LogEvent {
|
function event(overrides: Partial<LogEvent> = {}): LogEvent {
|
||||||
return {
|
return {
|
||||||
level: "debug",
|
level: 'debug',
|
||||||
namespace: "API",
|
namespace: 'API',
|
||||||
arguments: ["hello"],
|
arguments: ['hello'],
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
environment: "development",
|
environment: 'development',
|
||||||
sendToSentryLogs: false,
|
sendToSentryLogs: false,
|
||||||
sendToSentryIssue: false,
|
sendToSentryIssue: false,
|
||||||
sendToConsole: true,
|
sendToConsole: true,
|
||||||
@@ -16,25 +16,25 @@ function event(overrides: Partial<LogEvent> = {}): LogEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("createConsoleSink", () => {
|
describe('createConsoleSink', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("appends [async] to the namespace label when event.async is set", () => {
|
it('appends [async] to the namespace label when event.async is set', () => {
|
||||||
const debug = vi.spyOn(console, "debug").mockImplementation(() => undefined);
|
const debug = vi.spyOn(console, 'debug').mockImplementation(() => undefined);
|
||||||
createConsoleSink().emit(event({ async: true }));
|
createConsoleSink().emit(event({ async: true }));
|
||||||
expect(debug).toHaveBeenCalled();
|
expect(debug).toHaveBeenCalled();
|
||||||
const first = debug.mock.calls[0]?.[0];
|
const first = debug.mock.calls[0]?.[0];
|
||||||
expect(String(first)).toContain("[API][async]");
|
expect(String(first)).toContain('[API][async]');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits [async] for synchronous events", () => {
|
it('omits [async] for synchronous events', () => {
|
||||||
const debug = vi.spyOn(console, "debug").mockImplementation(() => undefined);
|
const debug = vi.spyOn(console, 'debug').mockImplementation(() => undefined);
|
||||||
createConsoleSink().emit(event());
|
createConsoleSink().emit(event());
|
||||||
expect(debug).toHaveBeenCalled();
|
expect(debug).toHaveBeenCalled();
|
||||||
const first = debug.mock.calls[0]?.[0];
|
const first = debug.mock.calls[0]?.[0];
|
||||||
expect(String(first)).toContain("[API]");
|
expect(String(first)).toContain('[API]');
|
||||||
expect(String(first)).not.toContain("[async]");
|
expect(String(first)).not.toContain('[async]');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+96
-96
@@ -1,127 +1,127 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { createLogger, isLogCallOptions, parseLoggingOverride } from "../src/index";
|
import { createLogger, isLogCallOptions, parseLoggingOverride } from '../src/index';
|
||||||
import type { LogEvent, LogSink } from "../src/index";
|
import type { LogEvent, LogSink } from '../src/index';
|
||||||
import { createSentrySink } from "../src/sentry";
|
import { createSentrySink } from '../src/sentry';
|
||||||
|
|
||||||
function sink(): { sink: LogSink; events: LogEvent[] } {
|
function sink(): { sink: LogSink; events: LogEvent[] } {
|
||||||
const events: LogEvent[] = [];
|
const events: LogEvent[] = [];
|
||||||
return { events, sink: { emit: (event) => events.push(event) } };
|
return { events, sink: { emit: (event) => events.push(event) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("parseLoggingOverride", () => {
|
describe('parseLoggingOverride', () => {
|
||||||
it("accepts a global level and a namespace-filtered level", () => {
|
it('accepts a global level and a namespace-filtered level', () => {
|
||||||
expect(parseLoggingOverride("debug")).toEqual({ level: "debug", namespaces: [] });
|
expect(parseLoggingOverride('debug')).toEqual({ level: 'debug', namespaces: [] });
|
||||||
expect(parseLoggingOverride("debug:WIDGET, API")).toEqual({
|
expect(parseLoggingOverride('debug:WIDGET, API')).toEqual({
|
||||||
level: "debug",
|
level: 'debug',
|
||||||
namespaces: ["WIDGET", "API"],
|
namespaces: ['WIDGET', 'API'],
|
||||||
});
|
});
|
||||||
expect(parseLoggingOverride("verbose")).toBeUndefined();
|
expect(parseLoggingOverride('verbose')).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isLogCallOptions", () => {
|
describe('isLogCallOptions', () => {
|
||||||
it("accepts only known option keys", () => {
|
it('accepts only known option keys', () => {
|
||||||
expect(isLogCallOptions({ sentry: true })).toBe(true);
|
expect(isLogCallOptions({ sentry: true })).toBe(true);
|
||||||
expect(isLogCallOptions({ suppressSentry: true })).toBe(true);
|
expect(isLogCallOptions({ suppressSentry: true })).toBe(true);
|
||||||
expect(isLogCallOptions({ sentry: true, suppressSentry: false })).toBe(true);
|
expect(isLogCallOptions({ sentry: true, suppressSentry: false })).toBe(true);
|
||||||
expect(isLogCallOptions({ requestId: "req_1" })).toBe(false);
|
expect(isLogCallOptions({ requestId: 'req_1' })).toBe(false);
|
||||||
expect(isLogCallOptions({ sentry: true, requestId: "req_1" })).toBe(false);
|
expect(isLogCallOptions({ sentry: true, requestId: 'req_1' })).toBe(false);
|
||||||
expect(isLogCallOptions({})).toBe(false);
|
expect(isLogCallOptions({})).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createLogger", () => {
|
describe('createLogger', () => {
|
||||||
it("uses environment defaults and never evaluates suppressed lazy data", () => {
|
it('uses environment defaults and never evaluates suppressed lazy data', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const expensive = vi.fn(() => ({ huge: "snapshot" }));
|
const expensive = vi.fn(() => ({ huge: 'snapshot' }));
|
||||||
const logger = createLogger({ environment: "production", sinks: [destination.sink] });
|
const logger = createLogger({ environment: 'production', sinks: [destination.sink] });
|
||||||
logger.debug("ignored", expensive);
|
logger.debug('ignored', expensive);
|
||||||
logger.error("kept", expensive);
|
logger.error('kept', expensive);
|
||||||
expect(expensive).toHaveBeenCalledTimes(1);
|
expect(expensive).toHaveBeenCalledTimes(1);
|
||||||
expect(destination.events).toHaveLength(1);
|
expect(destination.events).toHaveLength(1);
|
||||||
expect(destination.events[0]?.arguments).toEqual(["kept", { huge: "snapshot" }]);
|
expect(destination.events[0]?.arguments).toEqual(['kept', { huge: 'snapshot' }]);
|
||||||
expect(destination.events[0]?.async).toBeUndefined();
|
expect(destination.events[0]?.async).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("never starts suppressed async lazy factories", () => {
|
it('never starts suppressed async lazy factories', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const expensive = vi.fn(async () => ({ huge: "snapshot" }));
|
const expensive = vi.fn(async () => ({ huge: 'snapshot' }));
|
||||||
const logger = createLogger({ environment: "production", sinks: [destination.sink] });
|
const logger = createLogger({ environment: 'production', sinks: [destination.sink] });
|
||||||
logger.debug("ignored", expensive);
|
logger.debug('ignored', expensive);
|
||||||
expect(expensive).not.toHaveBeenCalled();
|
expect(expensive).not.toHaveBeenCalled();
|
||||||
expect(destination.events).toHaveLength(0);
|
expect(destination.events).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("settles async lazy factories before emitting and sets async", async () => {
|
it('settles async lazy factories before emitting and sets async', async () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const factory = vi.fn(async () => ({ data: 1 }));
|
const factory = vi.fn(async () => ({ data: 1 }));
|
||||||
const logger = createLogger({ environment: "development", sinks: [destination.sink] });
|
const logger = createLogger({ environment: 'development', sinks: [destination.sink] });
|
||||||
logger.debug("response", factory);
|
logger.debug('response', factory);
|
||||||
expect(factory).toHaveBeenCalledTimes(1);
|
expect(factory).toHaveBeenCalledTimes(1);
|
||||||
expect(destination.events).toHaveLength(0);
|
expect(destination.events).toHaveLength(0);
|
||||||
await vi.waitFor(() => expect(destination.events).toHaveLength(1));
|
await vi.waitFor(() => expect(destination.events).toHaveLength(1));
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
arguments: ["response", { data: 1 }],
|
arguments: ['response', { data: 1 }],
|
||||||
async: true,
|
async: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unwraps direct Promise arguments and keeps rejection reasons", async () => {
|
it('unwraps direct Promise arguments and keeps rejection reasons', async () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const reason = new Error("boom");
|
const reason = new Error('boom');
|
||||||
const logger = createLogger({ environment: "development", sinks: [destination.sink] });
|
const logger = createLogger({ environment: 'development', sinks: [destination.sink] });
|
||||||
logger.error("failed", { id: 1 }, Promise.reject(reason));
|
logger.error('failed', { id: 1 }, Promise.reject(reason));
|
||||||
expect(destination.events).toHaveLength(0);
|
expect(destination.events).toHaveLength(0);
|
||||||
await vi.waitFor(() => expect(destination.events).toHaveLength(1));
|
await vi.waitFor(() => expect(destination.events).toHaveLength(1));
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
arguments: ["failed", { id: 1 }, reason],
|
arguments: ['failed', { id: 1 }, reason],
|
||||||
async: true,
|
async: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies session namespace filtering to namespace descendants", () => {
|
it('applies session namespace filtering to namespace descendants', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const storage = { getItem: () => "debug:WIDGET" };
|
const storage = { getItem: () => 'debug:WIDGET' };
|
||||||
createLogger({
|
createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
namespace: "WIDGET:Button",
|
namespace: 'WIDGET:Button',
|
||||||
sessionStorage: storage,
|
sessionStorage: storage,
|
||||||
sinks: [destination.sink],
|
sinks: [destination.sink],
|
||||||
}).debug("shown");
|
}).debug('shown');
|
||||||
createLogger({
|
createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
namespace: "API",
|
namespace: 'API',
|
||||||
sessionStorage: storage,
|
sessionStorage: storage,
|
||||||
sinks: [destination.sink],
|
sinks: [destination.sink],
|
||||||
}).debug("hidden");
|
}).debug('hidden');
|
||||||
expect(destination.events.map((event) => event.arguments[0])).toEqual(["shown"]);
|
expect(destination.events.map((event) => event.arguments[0])).toEqual(['shown']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("builds immutable child namespaces and reports failed assertions as errors", () => {
|
it('builds immutable child namespaces and reports failed assertions as errors', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const root = createLogger({
|
const root = createLogger({
|
||||||
environment: "development",
|
environment: 'development',
|
||||||
namespace: "WIDGET",
|
namespace: 'WIDGET',
|
||||||
sinks: [destination.sink],
|
sinks: [destination.sink],
|
||||||
});
|
});
|
||||||
root.child("Button").assert(false, "missing label");
|
root.child('Button').assert(false, 'missing label');
|
||||||
expect(root.namespace).toBe("WIDGET");
|
expect(root.namespace).toBe('WIDGET');
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
level: "error",
|
level: 'error',
|
||||||
namespace: "WIDGET:Button",
|
namespace: 'WIDGET:Button',
|
||||||
arguments: ["Assertion failed", "missing label"],
|
arguments: ['Assertion failed', 'missing label'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends production browser errors to Sentry without writing to the console", () => {
|
it('sends production browser errors to Sentry without writing to the console', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
runtime: "browser",
|
runtime: 'browser',
|
||||||
sentry: destination.sink,
|
sentry: destination.sink,
|
||||||
});
|
});
|
||||||
logger.error("captured");
|
logger.error('captured');
|
||||||
expect(destination.events).toHaveLength(1);
|
expect(destination.events).toHaveLength(1);
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
sendToSentryIssue: true,
|
sendToSentryIssue: true,
|
||||||
@@ -132,19 +132,19 @@ describe("createLogger", () => {
|
|||||||
consoleError.mockRestore();
|
consoleError.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forces selected events to Sentry Logs without writing them to the console", () => {
|
it('forces selected events to Sentry Logs without writing them to the console', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => undefined);
|
const consoleInfo = vi.spyOn(console, 'info').mockImplementation(() => undefined);
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
runtime: "browser",
|
runtime: 'browser',
|
||||||
sentry: destination.sink,
|
sentry: destination.sink,
|
||||||
});
|
});
|
||||||
logger.info("Cache warmed", { sentry: true });
|
logger.info('Cache warmed', { sentry: true });
|
||||||
expect(destination.events).toHaveLength(1);
|
expect(destination.events).toHaveLength(1);
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
level: "info",
|
level: 'info',
|
||||||
arguments: ["Cache warmed"],
|
arguments: ['Cache warmed'],
|
||||||
sendToSentryLogs: true,
|
sendToSentryLogs: true,
|
||||||
sendToSentryIssue: false,
|
sendToSentryIssue: false,
|
||||||
sendToConsole: false,
|
sendToConsole: false,
|
||||||
@@ -153,42 +153,42 @@ describe("createLogger", () => {
|
|||||||
consoleInfo.mockRestore();
|
consoleInfo.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("strips trailing call options from event arguments and supports data plus options", () => {
|
it('strips trailing call options from event arguments and supports data plus options', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
runtime: "browser",
|
runtime: 'browser',
|
||||||
sentry: destination.sink,
|
sentry: destination.sink,
|
||||||
});
|
});
|
||||||
logger.warn("slow", { ms: 1200 }, { sentry: true });
|
logger.warn('slow', { ms: 1200 }, { sentry: true });
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
level: "warn",
|
level: 'warn',
|
||||||
arguments: ["slow", { ms: 1200 }],
|
arguments: ['slow', { ms: 1200 }],
|
||||||
sendToSentryLogs: true,
|
sendToSentryLogs: true,
|
||||||
sendToConsole: false,
|
sendToConsole: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("suppresses Sentry Issues when suppressSentry is set", () => {
|
it('suppresses Sentry Issues when suppressSentry is set', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
runtime: "browser",
|
runtime: 'browser',
|
||||||
sentry: destination.sink,
|
sentry: destination.sink,
|
||||||
});
|
});
|
||||||
logger.error("expected", new Error("nope"), { suppressSentry: true });
|
logger.error('expected', new Error('nope'), { suppressSentry: true });
|
||||||
expect(destination.events).toHaveLength(1);
|
expect(destination.events).toHaveLength(1);
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
sendToSentryIssue: false,
|
sendToSentryIssue: false,
|
||||||
sendToSentryLogs: false,
|
sendToSentryLogs: false,
|
||||||
arguments: ["expected", expect.any(Error)],
|
arguments: ['expected', expect.any(Error)],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("never marks events for Sentry in development", () => {
|
it('never marks events for Sentry in development', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "development",
|
environment: 'development',
|
||||||
sinks: [
|
sinks: [
|
||||||
destination.sink,
|
destination.sink,
|
||||||
createSentrySink(
|
createSentrySink(
|
||||||
@@ -213,8 +213,8 @@ describe("createLogger", () => {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
logger.error("local only");
|
logger.error('local only');
|
||||||
logger.info("probe", { sentry: true });
|
logger.info('probe', { sentry: true });
|
||||||
expect(destination.events).toHaveLength(2);
|
expect(destination.events).toHaveLength(2);
|
||||||
expect(
|
expect(
|
||||||
destination.events.every(
|
destination.events.every(
|
||||||
@@ -223,25 +223,25 @@ describe("createLogger", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps production Node errors on stderr as well as sending them to Sentry", () => {
|
it('keeps production Node errors on stderr as well as sending them to Sentry', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
runtime: "node",
|
runtime: 'node',
|
||||||
sentry: destination.sink,
|
sentry: destination.sink,
|
||||||
});
|
});
|
||||||
logger.error("captured");
|
logger.error('captured');
|
||||||
expect(destination.events).toHaveLength(1);
|
expect(destination.events).toHaveLength(1);
|
||||||
expect(consoleError).toHaveBeenCalledTimes(1);
|
expect(consoleError).toHaveBeenCalledTimes(1);
|
||||||
consoleError.mockRestore();
|
consoleError.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mirrors warn to Sentry Logs in production when logs are enabled on the sink", () => {
|
it('mirrors warn to Sentry Logs in production when logs are enabled on the sink', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
runtime: "browser",
|
runtime: 'browser',
|
||||||
sinks: [
|
sinks: [
|
||||||
destination.sink,
|
destination.sink,
|
||||||
createSentrySink(
|
createSentrySink(
|
||||||
@@ -261,21 +261,21 @@ describe("createLogger", () => {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
logger.warn("noisy");
|
logger.warn('noisy');
|
||||||
logger.info("below default production logs threshold");
|
logger.info('below default production logs threshold');
|
||||||
expect(destination.events).toHaveLength(1);
|
expect(destination.events).toHaveLength(1);
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
level: "warn",
|
level: 'warn',
|
||||||
sendToSentryLogs: true,
|
sendToSentryLogs: true,
|
||||||
sendToConsole: false,
|
sendToConsole: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mirrors info to Sentry Logs in staging when logs are enabled", () => {
|
it('mirrors info to Sentry Logs in staging when logs are enabled', () => {
|
||||||
const destination = sink();
|
const destination = sink();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "staging",
|
environment: 'staging',
|
||||||
runtime: "browser",
|
runtime: 'browser',
|
||||||
sinks: [
|
sinks: [
|
||||||
destination.sink,
|
destination.sink,
|
||||||
createSentrySink(
|
createSentrySink(
|
||||||
@@ -295,10 +295,10 @@ describe("createLogger", () => {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
logger.info("visible in staging logs");
|
logger.info('visible in staging logs');
|
||||||
logger.debug("still below staging logs threshold");
|
logger.debug('still below staging logs threshold');
|
||||||
expect(destination.events.map((event) => event.arguments[0])).toEqual([
|
expect(destination.events.map((event) => event.arguments[0])).toEqual([
|
||||||
"visible in staging logs",
|
'visible in staging logs',
|
||||||
]);
|
]);
|
||||||
expect(destination.events[0]).toMatchObject({
|
expect(destination.events[0]).toMatchObject({
|
||||||
sendToSentryLogs: true,
|
sendToSentryLogs: true,
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* Compile-time regression: official Sentry SDKs must assign to {@link SentryLike}
|
* Compile-time regression: official Sentry SDKs must assign to {@link SentryLike}
|
||||||
* without casts or consumer adapters. Checked by `pnpm check` (`tsc --noEmit`).
|
* without casts or consumer adapters. Checked by `pnpm check` (`tsc --noEmit`).
|
||||||
*/
|
*/
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from '@sentry/node';
|
||||||
import { createSentrySink, type SentryLike } from "../src/sentry";
|
import { createSentrySink, type SentryLike } from '../src/sentry';
|
||||||
|
|
||||||
const _sdk: SentryLike = Sentry;
|
const _sdk: SentryLike = Sentry;
|
||||||
createSentrySink(Sentry);
|
createSentrySink(Sentry);
|
||||||
|
|||||||
+69
-69
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { createLogger, type LogEvent } from "../src/index";
|
import { createLogger, type LogEvent } from '../src/index';
|
||||||
import { createSentrySink, toSentryLogPayload, toSentrySeverity } from "../src/sentry";
|
import { createSentrySink, toSentryLogPayload, toSentrySeverity } from '../src/sentry';
|
||||||
|
|
||||||
function createFakeSentry() {
|
function createFakeSentry() {
|
||||||
const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() };
|
const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() };
|
||||||
@@ -23,10 +23,10 @@ function createFakeSentry() {
|
|||||||
|
|
||||||
function issueEvent(overrides: Partial<LogEvent> = {}): LogEvent {
|
function issueEvent(overrides: Partial<LogEvent> = {}): LogEvent {
|
||||||
return {
|
return {
|
||||||
level: "error",
|
level: 'error',
|
||||||
arguments: ["message"],
|
arguments: ['message'],
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
sendToSentryLogs: false,
|
sendToSentryLogs: false,
|
||||||
sendToSentryIssue: true,
|
sendToSentryIssue: true,
|
||||||
sendToConsole: false,
|
sendToConsole: false,
|
||||||
@@ -34,46 +34,46 @@ function issueEvent(overrides: Partial<LogEvent> = {}): LogEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("toSentrySeverity", () => {
|
describe('toSentrySeverity', () => {
|
||||||
it("maps logger levels to Sentry Issue severities", () => {
|
it('maps logger levels to Sentry Issue severities', () => {
|
||||||
expect(toSentrySeverity("warn")).toBe("warning");
|
expect(toSentrySeverity('warn')).toBe('warning');
|
||||||
expect(toSentrySeverity("trace")).toBe("debug");
|
expect(toSentrySeverity('trace')).toBe('debug');
|
||||||
expect(toSentrySeverity("error")).toBe("error");
|
expect(toSentrySeverity('error')).toBe('error');
|
||||||
expect(toSentrySeverity("info")).toBe("info");
|
expect(toSentrySeverity('info')).toBe('info');
|
||||||
expect(toSentrySeverity("debug")).toBe("debug");
|
expect(toSentrySeverity('debug')).toBe('debug');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("toSentryLogPayload", () => {
|
describe('toSentryLogPayload', () => {
|
||||||
it("joins string messages and flattens primitive object attributes", () => {
|
it('joins string messages and flattens primitive object attributes', () => {
|
||||||
expect(
|
expect(
|
||||||
toSentryLogPayload({
|
toSentryLogPayload({
|
||||||
level: "info",
|
level: 'info',
|
||||||
namespace: "API",
|
namespace: 'API',
|
||||||
arguments: ["Cache warmed", { entries: 42, ok: true, nested: { a: 1 } }],
|
arguments: ['Cache warmed', { entries: 42, ok: true, nested: { a: 1 } }],
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
sendToSentryLogs: true,
|
sendToSentryLogs: true,
|
||||||
sendToSentryIssue: false,
|
sendToSentryIssue: false,
|
||||||
sendToConsole: false,
|
sendToConsole: false,
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
message: "Cache warmed",
|
message: 'Cache warmed',
|
||||||
attributes: {
|
attributes: {
|
||||||
"logger.namespace": "API",
|
'logger.namespace': 'API',
|
||||||
entries: 42,
|
entries: 42,
|
||||||
ok: true,
|
ok: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes async when the event was deferred for thenables", () => {
|
it('includes async when the event was deferred for thenables', () => {
|
||||||
expect(
|
expect(
|
||||||
toSentryLogPayload({
|
toSentryLogPayload({
|
||||||
level: "debug",
|
level: 'debug',
|
||||||
arguments: ["response"],
|
arguments: ['response'],
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
environment: "development",
|
environment: 'development',
|
||||||
sendToSentryLogs: true,
|
sendToSentryLogs: true,
|
||||||
sendToSentryIssue: false,
|
sendToSentryIssue: false,
|
||||||
sendToConsole: true,
|
sendToConsole: true,
|
||||||
@@ -83,112 +83,112 @@ describe("toSentryLogPayload", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createSentrySink", () => {
|
describe('createSentrySink', () => {
|
||||||
it("captures errors as Issues with namespace and structured attributes", () => {
|
it('captures errors as Issues with namespace and structured attributes', () => {
|
||||||
const { scope, sentry } = createFakeSentry();
|
const { scope, sentry } = createFakeSentry();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
namespace: "API",
|
namespace: 'API',
|
||||||
sinks: [createSentrySink(sentry)],
|
sinks: [createSentrySink(sentry)],
|
||||||
});
|
});
|
||||||
const error = new Error("offline");
|
const error = new Error('offline');
|
||||||
logger.warn("ignored without logs");
|
logger.warn('ignored without logs');
|
||||||
logger.error("Request failed", error, { requestId: "req_1" });
|
logger.error('Request failed', error, { requestId: 'req_1' });
|
||||||
expect(sentry.captureException).toHaveBeenCalledWith(error);
|
expect(sentry.captureException).toHaveBeenCalledWith(error);
|
||||||
expect(sentry.captureMessage).not.toHaveBeenCalled();
|
expect(sentry.captureMessage).not.toHaveBeenCalled();
|
||||||
expect(sentry.logger.info).not.toHaveBeenCalled();
|
expect(sentry.logger.info).not.toHaveBeenCalled();
|
||||||
expect(scope.setLevel).toHaveBeenCalledWith("error");
|
expect(scope.setLevel).toHaveBeenCalledWith('error');
|
||||||
expect(scope.setTag).toHaveBeenCalledWith("logger.namespace", "API");
|
expect(scope.setTag).toHaveBeenCalledWith('logger.namespace', 'API');
|
||||||
expect(scope.setExtras).toHaveBeenCalledWith({
|
expect(scope.setExtras).toHaveBeenCalledWith({
|
||||||
"logger.namespace": "API",
|
'logger.namespace': 'API',
|
||||||
requestId: "req_1",
|
requestId: 'req_1',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes non-error events to Sentry Logs when enabled, not as Issues", () => {
|
it('writes non-error events to Sentry Logs when enabled, not as Issues', () => {
|
||||||
const { sentry } = createFakeSentry();
|
const { sentry } = createFakeSentry();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
namespace: "API",
|
namespace: 'API',
|
||||||
sinks: [createSentrySink(sentry, { logs: true })],
|
sinks: [createSentrySink(sentry, { logs: true })],
|
||||||
});
|
});
|
||||||
logger.warn("slow", { ms: 1200 });
|
logger.warn('slow', { ms: 1200 });
|
||||||
logger.info("forced", { id: 1 }, { sentry: true });
|
logger.info('forced', { id: 1 }, { sentry: true });
|
||||||
expect(sentry.captureMessage).not.toHaveBeenCalled();
|
expect(sentry.captureMessage).not.toHaveBeenCalled();
|
||||||
expect(sentry.captureException).not.toHaveBeenCalled();
|
expect(sentry.captureException).not.toHaveBeenCalled();
|
||||||
expect(sentry.logger.warn).toHaveBeenCalledWith("slow", {
|
expect(sentry.logger.warn).toHaveBeenCalledWith('slow', {
|
||||||
"logger.namespace": "API",
|
'logger.namespace': 'API',
|
||||||
ms: 1200,
|
ms: 1200,
|
||||||
});
|
});
|
||||||
expect(sentry.logger.info).toHaveBeenCalledWith("forced", {
|
expect(sentry.logger.info).toHaveBeenCalledWith('forced', {
|
||||||
"logger.namespace": "API",
|
'logger.namespace': 'API',
|
||||||
id: 1,
|
id: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not create an Issue when suppressSentry is set", () => {
|
it('does not create an Issue when suppressSentry is set', () => {
|
||||||
const { sentry } = createFakeSentry();
|
const { sentry } = createFakeSentry();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
runtime: "node",
|
runtime: 'node',
|
||||||
sinks: [createSentrySink(sentry, { logs: true })],
|
sinks: [createSentrySink(sentry, { logs: true })],
|
||||||
});
|
});
|
||||||
logger.error("expected", new Error("nope"), { suppressSentry: true });
|
logger.error('expected', new Error('nope'), { suppressSentry: true });
|
||||||
expect(sentry.captureException).not.toHaveBeenCalled();
|
expect(sentry.captureException).not.toHaveBeenCalled();
|
||||||
expect(sentry.logger.error).not.toHaveBeenCalled();
|
expect(sentry.logger.error).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("never sends in development", () => {
|
it('never sends in development', () => {
|
||||||
const { sentry } = createFakeSentry();
|
const { sentry } = createFakeSentry();
|
||||||
const logger = createLogger({
|
const logger = createLogger({
|
||||||
environment: "development",
|
environment: 'development',
|
||||||
sinks: [createSentrySink(sentry, { logs: true })],
|
sinks: [createSentrySink(sentry, { logs: true })],
|
||||||
});
|
});
|
||||||
logger.error("local", new Error("x"));
|
logger.error('local', new Error('x'));
|
||||||
logger.warn("local warn", { sentry: true });
|
logger.warn('local warn', { sentry: true });
|
||||||
expect(sentry.captureException).not.toHaveBeenCalled();
|
expect(sentry.captureException).not.toHaveBeenCalled();
|
||||||
expect(sentry.logger.warn).not.toHaveBeenCalled();
|
expect(sentry.logger.warn).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps warn → warning on the Issues path and keeps warn on the Logs path", () => {
|
it('maps warn → warning on the Issues path and keeps warn on the Logs path', () => {
|
||||||
const { scope, sentry } = createFakeSentry();
|
const { scope, sentry } = createFakeSentry();
|
||||||
const sink = createSentrySink(sentry, { logs: true });
|
const sink = createSentrySink(sentry, { logs: true });
|
||||||
|
|
||||||
sink.emit(
|
sink.emit(
|
||||||
issueEvent({
|
issueEvent({
|
||||||
level: "warn",
|
level: 'warn',
|
||||||
arguments: ["degraded"],
|
arguments: ['degraded'],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(scope.setLevel).toHaveBeenCalledWith("warning");
|
expect(scope.setLevel).toHaveBeenCalledWith('warning');
|
||||||
expect(sentry.captureMessage).toHaveBeenCalledWith("degraded", "warning");
|
expect(sentry.captureMessage).toHaveBeenCalledWith('degraded', 'warning');
|
||||||
expect(sentry.logger.warn).not.toHaveBeenCalled();
|
expect(sentry.logger.warn).not.toHaveBeenCalled();
|
||||||
|
|
||||||
sink.emit({
|
sink.emit({
|
||||||
level: "warn",
|
level: 'warn',
|
||||||
namespace: "API",
|
namespace: 'API',
|
||||||
arguments: ["slow", { ms: 10 }],
|
arguments: ['slow', { ms: 10 }],
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
environment: "production",
|
environment: 'production',
|
||||||
sendToSentryLogs: true,
|
sendToSentryLogs: true,
|
||||||
sendToSentryIssue: false,
|
sendToSentryIssue: false,
|
||||||
sendToConsole: false,
|
sendToConsole: false,
|
||||||
});
|
});
|
||||||
expect(sentry.logger.warn).toHaveBeenCalledWith("slow", {
|
expect(sentry.logger.warn).toHaveBeenCalledWith('slow', {
|
||||||
"logger.namespace": "API",
|
'logger.namespace': 'API',
|
||||||
ms: 10,
|
ms: 10,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("maps error Issue severity as error", () => {
|
it('maps error Issue severity as error', () => {
|
||||||
const { scope, sentry } = createFakeSentry();
|
const { scope, sentry } = createFakeSentry();
|
||||||
createSentrySink(sentry).emit(
|
createSentrySink(sentry).emit(
|
||||||
issueEvent({
|
issueEvent({
|
||||||
level: "error",
|
level: 'error',
|
||||||
arguments: ["failed"],
|
arguments: ['failed'],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(scope.setLevel).toHaveBeenCalledWith("error");
|
expect(scope.setLevel).toHaveBeenCalledWith('error');
|
||||||
expect(sentry.captureMessage).toHaveBeenCalledWith("failed", "error");
|
expect(sentry.captureMessage).toHaveBeenCalledWith('failed', 'error');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+5
-5
@@ -1,16 +1,16 @@
|
|||||||
import { defineConfig } from "tsup";
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: { index: "src/index.ts", sentry: "src/sentry.ts" },
|
entry: { index: 'src/index.ts', sentry: 'src/sentry.ts' },
|
||||||
format: ["esm", "cjs"],
|
format: ['esm', 'cjs'],
|
||||||
// tsup injects baseUrl for DTS; TS 6 treats that as an error until tsup stops
|
// tsup injects baseUrl for DTS; TS 6 treats that as an error until tsup stops
|
||||||
// (https://github.com/egoist/tsup/issues/1388)
|
// (https://github.com/egoist/tsup/issues/1388)
|
||||||
dts: {
|
dts: {
|
||||||
compilerOptions: {
|
compilerOptions: {
|
||||||
ignoreDeprecations: "6.0",
|
ignoreDeprecations: '6.0',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
clean: true,
|
clean: true,
|
||||||
target: "es2022",
|
target: 'es2022',
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: { environment: "node", coverage: { provider: "v8", reporter: ["text", "html"] } },
|
test: { environment: 'node', coverage: { provider: 'v8', reporter: ['text', 'html'] } },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user