diff --git a/README.md b/README.md index ffb2234..2183ae7 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Copy [`env.example`](./env.example) as a starting point for local or container c ## Sentry -The Sentry adapter has **no SDK dependency**. Provide the SDK your application already uses via `@mifi/logger/sentry`. +The Sentry adapter has **no SDK dependency**. Pass your official `@sentry/node`, `@sentry/browser`, or `@sentry/nextjs` module directly to `createSentrySink` — no consumer adapter or cast is required. **Development never sends to Sentry.** Staging and production attach the sink when `sentry` is configured. @@ -152,6 +152,8 @@ A last argument is treated as options only when every key is `sentry` or `suppre Logs payload: string message + flat primitive attributes (including `logger.namespace`). Nested objects are not unfurled. +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`). + ## Custom sinks Replace the default destinations entirely with `sinks`: @@ -182,7 +184,7 @@ When `sinks` is provided, the `sentry` option is not used for default sink selec | `parseLoggingOverride` | `@mifi/logger` | Parse `debug` / `debug:NS1,NS2` override strings | | `createSentrySink` | `@mifi/logger/sentry` | Adapter for any Sentry-like SDK | -Types: `Logger`, `LoggerOptions`, `LogLevel`, `LoggerEnvironment`, `LogEvent`, `LogSink`, `LogData`, `LogCallOptions`, and (from `/sentry`) `SentryLike`, `SentryScopeLike`, `SentrySink`, `SentrySinkOptions`. +Types: `Logger`, `LoggerOptions`, `LogLevel`, `LoggerEnvironment`, `LogEvent`, `LogSink`, `LogData`, `LogCallOptions`, and (from `/sentry`) `SentryLike`, `SentryScopeLike`, `SentrySeverityLevel`, `SentrySink`, `SentrySinkOptions`. Optional destinations live under `src/sinks/` (console, Sentry today; New Relic / Datadog-style adapters can land beside them later). Public import paths stay `@mifi/logger` and `@mifi/logger/sentry`. diff --git a/package.json b/package.json index e2f0be9..7155004 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "devDependencies": { "@semantic-release/changelog": "^7.0.0", "@semantic-release/git": "^11.0.1", + "@sentry/node": "^10.69.0", "@types/node": "^24.0.0", "@vitest/coverage-v8": "^3.0.0", "oxlint": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0458f62..8d49a80 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ importers: "@semantic-release/git": specifier: ^11.0.1 version: 11.0.1(semantic-release@25.0.8(typescript@6.0.3)) + "@sentry/node": + specifier: ^10.69.0 + version: 10.69.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)) "@types/node": specifier: ^24.0.0 version: 24.13.3 @@ -70,6 +73,26 @@ packages: } engines: { node: ">=6.0.0" } + "@apm-js-collab/code-transformer-bundler-plugins@0.7.4": + resolution: + { + integrity: sha512-nAfOeZPSUAQvJa1iFT/5oCrTm5YQhMMrfCNthNnaXHZiOQhu1KGuLoIx7HtbAi3wfwaBYLaICPIeenIaEwcXIg==, + } + engines: { node: ">=18.0.0" } + + "@apm-js-collab/code-transformer@0.18.1": + resolution: + { + integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==, + } + hasBin: true + + "@apm-js-collab/tracing-hooks@0.13.0": + resolution: + { + integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==, + } + "@babel/code-frame@7.29.7": resolution: { @@ -730,6 +753,72 @@ packages: integrity: sha512-ByP1v7YL5SMveFPP7+sj0/ZuWCOOg/Chs4NafOMpq6WNIM/hdGY0S7C0TCGDBWu1aGmOxmUIhMx3cO+IdwYZ1Q==, } + "@opentelemetry/api-logs@0.220.0": + resolution: + { + integrity: sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==, + } + engines: { node: ">=8.0.0" } + + "@opentelemetry/api@1.9.1": + resolution: + { + integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==, + } + engines: { node: ">=8.0.0" } + + "@opentelemetry/core@2.10.0": + resolution: + { + integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==, + } + engines: { node: ^18.19.0 || >=20.6.0 } + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + + "@opentelemetry/instrumentation@0.220.0": + resolution: + { + integrity: sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==, + } + engines: { node: ^18.19.0 || >=20.6.0 } + peerDependencies: + "@opentelemetry/api": ^1.3.0 + + "@opentelemetry/resources@2.10.0": + resolution: + { + integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==, + } + engines: { node: ^18.19.0 || >=20.6.0 } + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + + "@opentelemetry/sdk-trace-base@2.10.0": + resolution: + { + integrity: sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==, + } + engines: { node: ^18.19.0 || >=20.6.0 } + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + + "@opentelemetry/sdk-trace@2.10.0": + resolution: + { + integrity: sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==, + } + engines: { node: ^18.19.0 || >=20.6.0 } + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + + "@opentelemetry/semantic-conventions@1.43.0": + resolution: + { + integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==, + } + engines: { node: ">=14" } + "@oxlint/binding-android-arm-eabi@1.77.0": resolution: { @@ -1217,6 +1306,69 @@ packages: peerDependencies: semantic-release: ">=20.1.0" + "@sentry/conventions@0.16.0": + resolution: + { + integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==, + } + engines: { node: ">=14" } + + "@sentry/core@10.69.0": + resolution: + { + integrity: sha512-+uuqVEeiDzYuAKjZLqsROKXvRTbl/QeH0gfGRtpYib1cud4rAFWRIkFmcR7Jb7JGFYwmReyQotiTj/hcDszTZg==, + } + engines: { node: ">=18" } + + "@sentry/node-core@10.69.0": + resolution: + { + integrity: sha512-IgArHczrZJxkgxoffHscj0NxQrG6kCazgmGQnlf3j58J1ec21YaUu8Tu+7G4Lo5tCiW3teQnwlKW1ttMXSqWRw==, + } + engines: { node: ">=18" } + peerDependencies: + "@opentelemetry/api": ^1.9.0 + "@opentelemetry/core": ^1.30.1 || ^2.1.0 + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1" + "@opentelemetry/instrumentation": ">=0.57.1 <1" + "@opentelemetry/sdk-trace-base": ^1.30.1 || ^2.1.0 + peerDependenciesMeta: + "@opentelemetry/api": + optional: true + "@opentelemetry/core": + optional: true + "@opentelemetry/exporter-trace-otlp-http": + optional: true + "@opentelemetry/instrumentation": + optional: true + "@opentelemetry/sdk-trace-base": + optional: true + + "@sentry/node@10.69.0": + resolution: + { + integrity: sha512-xEXA1YGIiTZbrW6MWV34uS6JGQuQg2ijTI0zed+FsJb9JZKPYel/GZK8Km26vfTVb+yCXFmWZNBesKegNcVdzg==, + } + engines: { node: ">=18" } + + "@sentry/opentelemetry@10.69.0": + resolution: + { + integrity: sha512-3FyWV6YcEJuvLrlaKGE1dHXCI+1YO0a62w7PkwlRg8yp6K6YXkmdwu9GjqaYD+Ju4tm7uC7mHIsGFQMm0M7pqQ==, + } + engines: { node: ">=18" } + peerDependencies: + "@opentelemetry/api": ^1.9.0 + "@opentelemetry/core": ^1.30.1 || ^2.1.0 + "@opentelemetry/sdk-trace-base": ^1.30.1 || ^2.1.0 + + "@sentry/server-utils@10.69.0": + resolution: + { + integrity: sha512-0MwHrA8+nNvMIsqf8m3cXwCBlUjr6AS7N6CZvHJtY1DkqEvQqEbD5VIrhzEyHN/KMZIgQ8XeDCQRhjnXFQGRhg==, + } + engines: { node: ">=18" } + "@simple-libs/stream-utils@1.2.0": resolution: { @@ -1431,6 +1583,13 @@ packages: integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==, } + astring@1.9.0: + resolution: + { + integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==, + } + hasBin: true + balanced-match@1.0.2: resolution: { @@ -1548,6 +1707,12 @@ packages: } engines: { node: ">= 14.16.0" } + cjs-module-lexer@2.2.0: + resolution: + { + integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==, + } + clean-stack@5.3.0: resolution: { @@ -1825,6 +1990,12 @@ packages: integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==, } + es-module-lexer@2.3.1: + resolution: + { + integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==, + } + esbuild@0.27.7: resolution: { @@ -1862,6 +2033,20 @@ packages: } engines: { node: ">=12" } + esquery@1.7.0: + resolution: + { + integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, + } + engines: { node: ">=0.10" } + + estraverse@5.3.0: + resolution: + { + integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==, + } + engines: { node: ">=4.0" } + estree-walker@3.0.3: resolution: { @@ -2143,6 +2328,13 @@ packages: } engines: { node: ">=18.20" } + import-in-the-middle@3.3.3: + resolution: + { + integrity: sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==, + } + engines: { node: ">=18" } + import-meta-resolve@4.2.0: resolution: { @@ -2491,6 +2683,13 @@ packages: integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==, } + meriyah@6.1.4: + resolution: + { + integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==, + } + engines: { node: ">=18.0.0" } + micromatch@4.0.8: resolution: { @@ -2546,6 +2745,12 @@ packages: integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==, } + module-details-from-path@1.0.4: + resolution: + { + integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==, + } + ms@2.1.3: resolution: { @@ -3070,6 +3275,13 @@ packages: } engines: { node: ">=0.10.0" } + require-in-the-middle@8.0.1: + resolution: + { + integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==, + } + engines: { node: ">=9.3.0 || >=8.10.0 <9.0.0" } + resolve-from@4.0.0: resolution: { @@ -3106,6 +3318,12 @@ packages: engines: { node: ^22.14.0 || >= 24.10.0 } hasBin: true + semifies@1.0.0: + resolution: + { + integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==, + } + semver-regex@4.0.5: resolution: { @@ -3852,6 +4070,30 @@ snapshots: "@jridgewell/gen-mapping": 0.3.13 "@jridgewell/trace-mapping": 0.3.31 + "@apm-js-collab/code-transformer-bundler-plugins@0.7.4": + dependencies: + "@apm-js-collab/code-transformer": 0.18.1 + es-module-lexer: 2.3.1 + magic-string: 0.30.21 + module-details-from-path: 1.0.4 + + "@apm-js-collab/code-transformer@0.18.1": + dependencies: + "@types/estree": 1.0.9 + astring: 1.9.0 + esquery: 1.7.0 + meriyah: 6.1.4 + semifies: 1.0.0 + source-map: 0.6.1 + + "@apm-js-collab/tracing-hooks@0.13.0": + dependencies: + "@apm-js-collab/code-transformer": 0.18.1 + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + "@babel/code-frame@7.29.7": dependencies: "@babel/helper-validator-identifier": 7.29.7 @@ -4126,6 +4368,49 @@ snapshots: dependencies: "@octokit/openapi-types": 28.0.0 + "@opentelemetry/api-logs@0.220.0": + dependencies: + "@opentelemetry/api": 1.9.1 + + "@opentelemetry/api@1.9.1": {} + + "@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)": + dependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/semantic-conventions": 1.43.0 + + "@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)": + dependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/api-logs": 0.220.0 + import-in-the-middle: 3.3.3 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + "@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)": + dependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/core": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/semantic-conventions": 1.43.0 + + "@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)": + dependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/core": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/resources": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/sdk-trace": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/semantic-conventions": 1.43.0 + + "@opentelemetry/sdk-trace@2.10.0(@opentelemetry/api@1.9.1)": + dependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/core": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/resources": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/semantic-conventions": 1.43.0 + + "@opentelemetry/semantic-conventions@1.43.0": {} + "@oxlint/binding-android-arm-eabi@1.77.0": optional: true @@ -4369,6 +4654,58 @@ snapshots: transitivePeerDependencies: - supports-color + "@sentry/conventions@0.16.0": {} + + "@sentry/core@10.69.0": + dependencies: + "@sentry/conventions": 0.16.0 + + "@sentry/node-core@10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))": + dependencies: + "@sentry/conventions": 0.16.0 + "@sentry/core": 10.69.0 + "@sentry/opentelemetry": 10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + import-in-the-middle: 3.3.3 + optionalDependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/core": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/instrumentation": 0.220.0(@opentelemetry/api@1.9.1) + "@opentelemetry/sdk-trace-base": 2.10.0(@opentelemetry/api@1.9.1) + + "@sentry/node@10.69.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))": + dependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/instrumentation": 0.220.0(@opentelemetry/api@1.9.1) + "@opentelemetry/sdk-trace-base": 2.10.0(@opentelemetry/api@1.9.1) + "@sentry/conventions": 0.16.0 + "@sentry/core": 10.69.0 + "@sentry/node-core": 10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + "@sentry/opentelemetry": 10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + "@sentry/server-utils": 10.69.0 + import-in-the-middle: 3.3.3 + transitivePeerDependencies: + - "@opentelemetry/core" + - "@opentelemetry/exporter-trace-otlp-http" + - supports-color + + "@sentry/opentelemetry@10.69.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))": + dependencies: + "@opentelemetry/api": 1.9.1 + "@opentelemetry/core": 2.10.0(@opentelemetry/api@1.9.1) + "@opentelemetry/sdk-trace-base": 2.10.0(@opentelemetry/api@1.9.1) + "@sentry/conventions": 0.16.0 + "@sentry/core": 10.69.0 + + "@sentry/server-utils@10.69.0": + dependencies: + "@apm-js-collab/code-transformer-bundler-plugins": 0.7.4 + "@apm-js-collab/tracing-hooks": 0.13.0 + "@sentry/conventions": 0.16.0 + "@sentry/core": 10.69.0 + meriyah: 6.1.4 + transitivePeerDependencies: + - supports-color + "@simple-libs/stream-utils@1.2.0": {} "@sindresorhus/is@4.6.0": {} @@ -4494,6 +4831,8 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 + astring@1.9.0: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -4552,6 +4891,8 @@ snapshots: dependencies: readdirp: 4.1.2 + cjs-module-lexer@2.2.0: {} + clean-stack@5.3.0: dependencies: escape-string-regexp: 5.0.0 @@ -4700,6 +5041,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-module-lexer@2.3.1: {} + esbuild@0.27.7: optionalDependencies: "@esbuild/aix-ppc64": 0.27.7 @@ -4764,6 +5107,12 @@ snapshots: escape-string-regexp@5.0.0: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + estree-walker@3.0.3: dependencies: "@types/estree": 1.0.9 @@ -4957,6 +5306,12 @@ snapshots: transitivePeerDependencies: - supports-color + import-in-the-middle@3.3.3: + dependencies: + cjs-module-lexer: 2.2.0 + es-module-lexer: 2.3.1 + module-details-from-path: 1.0.4 + import-meta-resolve@4.2.0: {} indent-string@5.0.0: {} @@ -5121,6 +5476,8 @@ snapshots: merge-stream@2.0.0: {} + meriyah@6.1.4: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -5149,6 +5506,8 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 + module-details-from-path@1.0.4: {} + ms@2.1.3: {} mz@2.7.0: @@ -5398,6 +5757,13 @@ snapshots: require-directory@2.1.1: {} + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -5471,6 +5837,8 @@ snapshots: - supports-color - typescript + semifies@1.0.0: {} + semver-regex@4.0.5: {} semver@7.8.5: {} diff --git a/src/sinks/sentry.ts b/src/sinks/sentry.ts index 0204a38..1ce182b 100644 --- a/src/sinks/sentry.ts +++ b/src/sinks/sentry.ts @@ -1,5 +1,11 @@ import type { LogEvent, LogLevel, LogSink } from "../types"; +/** + * Sentry Issue severity levels (`@sentry/core` `SeverityLevel`). + * Note: Issues use `"warning"`; Logs use `"warn"`. + */ +export type SentrySeverityLevel = "fatal" | "error" | "warning" | "log" | "info" | "debug"; + /** * Options for {@link createSentrySink}. * @@ -23,13 +29,16 @@ export interface SentrySinkOptions { /** * Minimal Sentry scope surface required by {@link createSentrySink}. * Satisfied by `@sentry/browser`, `@sentry/node`, `@sentry/nextjs`, etc. + * + * Method parameter types are intentionally aligned with Sentry's public API + * (contravariance) so `typeof Sentry` assigns to {@link SentryLike} without adapters. */ export interface SentryScopeLike { /** * Sets the severity for the event about to be captured. - * @param level - Logger level string (e.g. `"error"`, `"info"`). + * @param level - Sentry Issue severity (`"warning"`, not logger `"warn"`). */ - setLevel(level: string): void; + setLevel(level: SentrySeverityLevel): void; /** * Attaches a string tag to the event. * @param key - Tag name (this package uses `"logger.namespace"`). @@ -59,6 +68,8 @@ export interface SentryLoggerApiLike { * Minimal Sentry client surface required by {@link createSentrySink}. * Pass your app's Sentry SDK module; this package does not depend on Sentry. * + * Structural typing matches official `@sentry/*` exports — no consumer adapter needed. + * * @example * ```ts * import * as Sentry from "@sentry/nextjs"; @@ -80,10 +91,11 @@ export interface SentryLike { captureException(error: Error): void; /** * Captures a string message when no `Error` is present in the arguments. + * Second argument matches Sentry's optional severity / capture-context slot. * @param message - Joined string arguments, or a fallback. - * @param level - Event severity. + * @param captureContext - Issue severity (use {@link toSentrySeverity} mapping). */ - captureMessage(message: string, level: string): void; + captureMessage(message: string, captureContext?: SentrySeverityLevel): void | string; /** Structured Logs API. Required when using `{ logs: true }` or `{ sentry: true }`. */ logger?: SentryLoggerApiLike; } @@ -103,6 +115,21 @@ export function isSentrySink(sink: LogSink): sink is SentrySink { return "kind" in sink && (sink as { kind?: unknown }).kind === "sentry"; } +/** + * Maps a logger level to a Sentry **Issue** {@link SentrySeverityLevel}. + * + * - `"warn"` → `"warning"` (Issues API; Logs keep `"warn"`) + * - `"trace"` → `"debug"` (closest Issue severity) + * - other levels map by name (`error`, `info`, `debug`) + * + * @param level - Logger emit level (never `silent`). + */ +export function toSentrySeverity(level: Exclude): SentrySeverityLevel { + if (level === "warn") return "warning"; + if (level === "trace") return "debug"; + return level; +} + function isAttributeValue(value: unknown): value is string | number | boolean { return typeof value === "string" || typeof value === "number" || typeof value === "boolean"; } @@ -143,6 +170,9 @@ export function toSentryLogPayload(event: LogEvent): { * event with `{ sentry: true }` on the log call. Suppress an Issue with * `{ suppressSentry: true }`. * + * Official `@sentry/node`, `@sentry/browser`, and `@sentry/nextjs` modules are + * assignable to {@link SentryLike} directly — no adapter object required. + * * @param sentry - Any object implementing {@link SentryLike} (typically the Sentry SDK). * @param options - Optional Logs configuration. * @returns A sink to pass as `sentry` or inside `sinks` on {@link createLogger}. @@ -174,12 +204,13 @@ export function createSentrySink(sentry: SentryLike, options: SentrySinkOptions if (event.sendToSentryIssue) { const error = event.arguments.find((item): item is Error => item instanceof Error); const { message, attributes } = toSentryLogPayload(event); + const severity = toSentrySeverity(event.level); sentry.withScope((scope) => { - scope.setLevel(event.level); + scope.setLevel(severity); if (event.namespace) scope.setTag("logger.namespace", event.namespace); scope.setExtras(attributes); if (error) sentry.captureException(error); - else sentry.captureMessage(message, event.level); + else sentry.captureMessage(message, severity); }); return; } diff --git a/test/sentry-assignability.ts b/test/sentry-assignability.ts new file mode 100644 index 0000000..daf25c7 --- /dev/null +++ b/test/sentry-assignability.ts @@ -0,0 +1,30 @@ +/** + * Compile-time regression: official Sentry SDKs must assign to {@link SentryLike} + * without casts or consumer adapters. Checked by `pnpm check` (`tsc --noEmit`). + */ +import * as Sentry from "@sentry/node"; +import { createSentrySink, type SentryLike } from "../src/sentry"; + +const _sdk: SentryLike = Sentry; +createSentrySink(Sentry); +createSentrySink(Sentry, { logs: true }); + +const stub: SentryLike = { + withScope: (callback) => + callback({ + setLevel: () => undefined, + setTag: () => undefined, + setExtras: () => undefined, + }), + captureException: () => undefined, + captureMessage: () => undefined, + logger: { + trace: () => undefined, + debug: () => undefined, + info: () => undefined, + warn: () => undefined, + error: () => undefined, + }, +}; +createSentrySink(stub); +createSentrySink(stub, { logs: true }); diff --git a/test/sentry.test.ts b/test/sentry.test.ts index 906fd27..49e7637 100644 --- a/test/sentry.test.ts +++ b/test/sentry.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { createLogger } from "../src/index"; -import { createSentrySink, toSentryLogPayload } from "../src/sentry"; +import { createLogger, type LogEvent } from "../src/index"; +import { createSentrySink, toSentryLogPayload, toSentrySeverity } from "../src/sentry"; function createFakeSentry() { const scope = { setLevel: vi.fn(), setTag: vi.fn(), setExtras: vi.fn() }; @@ -21,6 +21,29 @@ function createFakeSentry() { }; } +function issueEvent(overrides: Partial = {}): LogEvent { + return { + level: "error", + arguments: ["message"], + timestamp: new Date(), + environment: "production", + sendToSentryLogs: false, + sendToSentryIssue: true, + sendToConsole: false, + ...overrides, + }; +} + +describe("toSentrySeverity", () => { + it("maps logger levels to Sentry Issue severities", () => { + expect(toSentrySeverity("warn")).toBe("warning"); + expect(toSentrySeverity("trace")).toBe("debug"); + expect(toSentrySeverity("error")).toBe("error"); + expect(toSentrySeverity("info")).toBe("info"); + expect(toSentrySeverity("debug")).toBe("debug"); + }); +}); + describe("toSentryLogPayload", () => { it("joins string messages and flattens primitive object attributes", () => { expect( @@ -111,4 +134,46 @@ describe("createSentrySink", () => { expect(sentry.captureException).not.toHaveBeenCalled(); expect(sentry.logger.warn).not.toHaveBeenCalled(); }); + + it("maps warn → warning on the Issues path and keeps warn on the Logs path", () => { + const { scope, sentry } = createFakeSentry(); + const sink = createSentrySink(sentry, { logs: true }); + + sink.emit( + issueEvent({ + level: "warn", + arguments: ["degraded"], + }), + ); + expect(scope.setLevel).toHaveBeenCalledWith("warning"); + expect(sentry.captureMessage).toHaveBeenCalledWith("degraded", "warning"); + expect(sentry.logger.warn).not.toHaveBeenCalled(); + + sink.emit({ + level: "warn", + namespace: "API", + arguments: ["slow", { ms: 10 }], + timestamp: new Date(), + environment: "production", + sendToSentryLogs: true, + sendToSentryIssue: false, + sendToConsole: false, + }); + expect(sentry.logger.warn).toHaveBeenCalledWith("slow", { + "logger.namespace": "API", + ms: 10, + }); + }); + + it("maps error Issue severity as error", () => { + const { scope, sentry } = createFakeSentry(); + createSentrySink(sentry).emit( + issueEvent({ + level: "error", + arguments: ["failed"], + }), + ); + expect(scope.setLevel).toHaveBeenCalledWith("error"); + expect(sentry.captureMessage).toHaveBeenCalledWith("failed", "error"); + }); });