@logtape/sentry 1.4.0-dev.409 → 1.4.0-dev.413

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logtape/sentry",
3
- "version": "1.4.0-dev.409+63c2cd45",
3
+ "version": "1.4.0-dev.413+1097da33",
4
4
  "description": "LogTape Sentry sink",
5
5
  "keywords": [
6
6
  "LogTape",
@@ -40,9 +40,12 @@
40
40
  "./package.json": "./package.json"
41
41
  },
42
42
  "sideEffects": false,
43
+ "files": [
44
+ "dist/"
45
+ ],
43
46
  "peerDependencies": {
44
47
  "@sentry/core": ">=8.0.0",
45
- "@logtape/logtape": "^1.4.0-dev.409+63c2cd45"
48
+ "@logtape/logtape": "^1.4.0-dev.413+1097da33"
46
49
  },
47
50
  "devDependencies": {
48
51
  "@sentry/core": "^9.41.0",
package/deno.json DELETED
@@ -1,14 +0,0 @@
1
- {
2
- "name": "@logtape/sentry",
3
- "version": "1.4.0-dev.409+63c2cd45",
4
- "license": "MIT",
5
- "exports": {
6
- ".": "./src/mod.ts"
7
- },
8
- "imports": {
9
- "@sentry/core": "npm:@sentry/core@^9.46.0"
10
- },
11
- "tasks": {
12
- "build": "pnpm build"
13
- }
14
- }
package/src/mod.test.ts DELETED
@@ -1,270 +0,0 @@
1
- import { suite } from "@alinea/suite";
2
- import { assertEquals, assertStringIncludes } from "@std/assert";
3
- import type { LogRecord } from "@logtape/logtape";
4
- import { getSentrySink } from "./mod.ts";
5
-
6
- const test = suite(import.meta);
7
-
8
- // Helper to create a mock log record
9
- function createMockLogRecord(overrides: Partial<LogRecord> = {}): LogRecord {
10
- return {
11
- category: ["test", "category"],
12
- level: "info",
13
- message: ["Hello, ", "world", "!"],
14
- rawMessage: "Hello, {name}!",
15
- timestamp: Date.now(),
16
- properties: {},
17
- ...overrides,
18
- };
19
- }
20
-
21
- // =============================================================================
22
- // Sink creation tests
23
- // =============================================================================
24
-
25
- test("getSentrySink() creates sink without parameters", () => {
26
- const sink = getSentrySink();
27
- assertEquals(typeof sink, "function");
28
- });
29
-
30
- test("getSentrySink() accepts deprecated client parameter", () => {
31
- // Deprecated client path still works (logs warning via meta logger)
32
- const mockClient = {
33
- captureMessage: () => "id",
34
- captureException: () => "id",
35
- };
36
- const sink = getSentrySink(mockClient);
37
- assertEquals(typeof sink, "function");
38
- });
39
-
40
- test("getSentrySink() throws on invalid parameter type", () => {
41
- let threw = false;
42
- try {
43
- // @ts-expect-error Testing invalid input
44
- getSentrySink("invalid");
45
- } catch (e) {
46
- threw = true;
47
- assertStringIncludes((e as Error).message, "Invalid parameter");
48
- }
49
- assertEquals(threw, true);
50
- });
51
-
52
- // =============================================================================
53
- // beforeSend hook tests
54
- // =============================================================================
55
-
56
- test("beforeSend can transform records", () => {
57
- const transformedRecords: LogRecord[] = [];
58
-
59
- const sink = getSentrySink({
60
- beforeSend: (record) => {
61
- const transformed = {
62
- ...record,
63
- properties: { ...record.properties, transformed: true },
64
- };
65
- transformedRecords.push(transformed);
66
- return transformed;
67
- },
68
- });
69
-
70
- sink(createMockLogRecord());
71
-
72
- assertEquals(transformedRecords.length, 1);
73
- assertEquals(transformedRecords[0].properties.transformed, true);
74
- });
75
-
76
- test("beforeSend can filter records by returning null", () => {
77
- let processedCount = 0;
78
-
79
- const sink = getSentrySink({
80
- beforeSend: (record) => {
81
- if (record.level === "debug") {
82
- return null;
83
- }
84
- processedCount++;
85
- return record;
86
- },
87
- });
88
-
89
- sink(createMockLogRecord({ level: "debug" }));
90
- sink(createMockLogRecord({ level: "info" }));
91
- sink(createMockLogRecord({ level: "debug" }));
92
- sink(createMockLogRecord({ level: "error" }));
93
-
94
- assertEquals(processedCount, 2);
95
- });
96
-
97
- // =============================================================================
98
- // Error resilience tests
99
- // =============================================================================
100
-
101
- test("sink never throws even when beforeSend throws", () => {
102
- const sink = getSentrySink({
103
- beforeSend: () => {
104
- throw new Error("beforeSend error");
105
- },
106
- });
107
-
108
- // Should not throw
109
- sink(createMockLogRecord());
110
- });
111
-
112
- test("sink handles circular references in properties", () => {
113
- const sink = getSentrySink();
114
-
115
- const circular: Record<string, unknown> = { name: "test" };
116
- circular.self = circular;
117
-
118
- // Should not throw
119
- sink(createMockLogRecord({
120
- properties: { data: circular },
121
- }));
122
- });
123
-
124
- // =============================================================================
125
- // Behavior verification tests
126
- // =============================================================================
127
-
128
- test("sink with Error at error level triggers exception path", () => {
129
- let sawError = false;
130
- const sink = getSentrySink({
131
- beforeSend: (record) => {
132
- if (record.properties.error instanceof Error) {
133
- sawError = true;
134
- }
135
- return record;
136
- },
137
- });
138
-
139
- sink(createMockLogRecord({
140
- level: "error",
141
- properties: { error: new Error("Test") },
142
- }));
143
-
144
- assertEquals(sawError, true);
145
- });
146
-
147
- test("sink without Error at error level does not trigger exception path", () => {
148
- let sawError = false;
149
- const sink = getSentrySink({
150
- beforeSend: (record) => {
151
- sawError = record.properties.error instanceof Error;
152
- return record;
153
- },
154
- });
155
-
156
- sink(createMockLogRecord({
157
- level: "error",
158
- message: ["Error without Error instance"],
159
- }));
160
-
161
- assertEquals(sawError, false);
162
- });
163
-
164
- test("sink processes all log levels without error", () => {
165
- const sink = getSentrySink();
166
- const levels: LogRecord["level"][] = [
167
- "trace",
168
- "debug",
169
- "info",
170
- "warning",
171
- "error",
172
- "fatal",
173
- ];
174
-
175
- for (const level of levels) {
176
- sink(createMockLogRecord({ level }));
177
- }
178
- });
179
-
180
- test("sink handles template messages correctly", () => {
181
- let capturedMessage: readonly unknown[] | null = null;
182
- const sink = getSentrySink({
183
- beforeSend: (record) => {
184
- capturedMessage = record.message;
185
- return record;
186
- },
187
- });
188
-
189
- sink(createMockLogRecord({
190
- message: ["User ", { id: 123 }, " logged in"],
191
- }));
192
-
193
- assertEquals(capturedMessage!.length, 3);
194
- assertEquals(capturedMessage![0], "User ");
195
- assertEquals((capturedMessage![1] as { id: number }).id, 123);
196
- });
197
-
198
- // =============================================================================
199
- // Options tests
200
- // =============================================================================
201
-
202
- test("sink accepts enableBreadcrumbs option", () => {
203
- const sink = getSentrySink({ enableBreadcrumbs: true });
204
-
205
- // Should not throw
206
- sink(createMockLogRecord({ level: "info" }));
207
- sink(createMockLogRecord({ level: "debug" }));
208
- });
209
-
210
- // =============================================================================
211
- // Meta logger filtering tests
212
- // =============================================================================
213
-
214
- test("sink ignores logs from logtape.meta.sentry category", () => {
215
- let processedCount = 0;
216
- const sink = getSentrySink({
217
- beforeSend: (record) => {
218
- processedCount++;
219
- return record;
220
- },
221
- });
222
-
223
- // This should be ignored (meta logger category)
224
- sink(createMockLogRecord({
225
- category: ["logtape", "meta", "sentry"],
226
- message: ["Meta log message"],
227
- }));
228
-
229
- // This should be processed
230
- sink(createMockLogRecord({
231
- category: ["app", "module"],
232
- message: ["Normal log message"],
233
- }));
234
-
235
- assertEquals(processedCount, 1);
236
- });
237
-
238
- test("sink does not ignore partial matches of meta category", () => {
239
- let processedCount = 0;
240
- const sink = getSentrySink({
241
- beforeSend: (record) => {
242
- processedCount++;
243
- return record;
244
- },
245
- });
246
-
247
- // These should NOT be ignored (partial matches or different third element)
248
- sink(createMockLogRecord({ category: ["logtape"] }));
249
- sink(createMockLogRecord({ category: ["logtape", "meta"] }));
250
- sink(createMockLogRecord({ category: ["logtape", "meta", "other"] }));
251
-
252
- assertEquals(processedCount, 3);
253
- });
254
-
255
- test("sink ignores logtape.meta.sentry with child categories", () => {
256
- let processedCount = 0;
257
- const sink = getSentrySink({
258
- beforeSend: (record) => {
259
- processedCount++;
260
- return record;
261
- },
262
- });
263
-
264
- // Child categories of logtape.meta.sentry should also be ignored
265
- sink(createMockLogRecord({
266
- category: ["logtape", "meta", "sentry", "child"],
267
- }));
268
-
269
- assertEquals(processedCount, 0);
270
- });
package/src/mod.ts DELETED
@@ -1,365 +0,0 @@
1
- import {
2
- compareLogLevel,
3
- getLogger,
4
- type LogLevel,
5
- type LogRecord,
6
- type Sink,
7
- } from "@logtape/logtape";
8
- import type {
9
- LogSeverityLevel,
10
- ParameterizedString,
11
- SeverityLevel,
12
- } from "@sentry/core";
13
- // Import namespace to safely check for public logger API (added in v9.41.0)
14
- import * as SentryCore from "@sentry/core";
15
- import {
16
- captureException as globalCaptureException,
17
- captureMessage as globalCaptureMessage,
18
- getActiveSpan as globalGetActiveSpan,
19
- getClient as globalGetClient,
20
- getIsolationScope as globalGetIsolationScope,
21
- } from "@sentry/core";
22
-
23
- /**
24
- * Converts a LogTape {@link LogRecord} into a Sentry {@link ParameterizedString}.
25
- *
26
- * This preserves the template structure for better message grouping in Sentry,
27
- * allowing similar messages with different values to be grouped together.
28
- *
29
- * @param record The log record to convert.
30
- * @returns A parameterized string with template and values.
31
- */
32
- function getParameterizedString(record: LogRecord): ParameterizedString {
33
- let result = "";
34
- let tplString = "";
35
- const tplValues: string[] = [];
36
- for (let i = 0; i < record.message.length; i++) {
37
- if (i % 2 === 0) {
38
- result += record.message[i];
39
- tplString += String(record.message[i]).replaceAll("%", "%%");
40
- } else {
41
- const value = inspect(record.message[i]);
42
- result += value;
43
- tplString += `%s`;
44
- tplValues.push(value);
45
- }
46
- }
47
- const paramStr = new String(result) as ParameterizedString;
48
- paramStr.__sentry_template_string__ = tplString;
49
- paramStr.__sentry_template_values__ = tplValues;
50
- return paramStr;
51
- }
52
-
53
- /**
54
- * A platform-specific inspect function. In Deno, this is {@link Deno.inspect},
55
- * and in Node.js/Bun it is {@link util.inspect}. If neither is available, it
56
- * falls back to {@link JSON.stringify}.
57
- *
58
- * @param value The value to inspect.
59
- * @returns The string representation of the value.
60
- */
61
- const inspect: (value: unknown) => string =
62
- // @ts-ignore: Deno global
63
- "Deno" in globalThis && "inspect" in globalThis.Deno &&
64
- // @ts-ignore: Deno global
65
- typeof globalThis.Deno.inspect === "function"
66
- // @ts-ignore: Deno global
67
- ? globalThis.Deno.inspect
68
- // @ts-ignore: Node.js global
69
- : "util" in globalThis && "inspect" in globalThis.util &&
70
- // @ts-ignore: Node.js global
71
- typeof globalThis.util.inspect === "function"
72
- // @ts-ignore: Node.js global
73
- ? globalThis.util.inspect
74
- : JSON.stringify;
75
-
76
- // Level normalization helpers
77
-
78
- function mapLevelForEvents(level: LogLevel): SeverityLevel {
79
- switch (level) {
80
- case "trace":
81
- return "debug";
82
- default:
83
- return level as SeverityLevel; // debug | info | error | fatal
84
- }
85
- }
86
-
87
- function mapLevelForLogs(level: LogLevel): LogSeverityLevel {
88
- switch (level) {
89
- case "trace":
90
- return "debug";
91
- case "warning":
92
- return "warn";
93
- case "debug":
94
- case "info":
95
- case "error":
96
- case "fatal":
97
- return level;
98
- default:
99
- return "info"; // fallback
100
- }
101
- }
102
-
103
- /**
104
- * A Sentry client instance type (used for v1.1.x backward compatibility).
105
- *
106
- * Client instances only support `captureMessage` and `captureException`.
107
- * For scope operations (breadcrumbs, user context, traces), the sink always
108
- * uses global functions from `@sentry/core`.
109
- *
110
- * @deprecated This is only used for backward compatibility with v1.1.x.
111
- * New code should use `getSentrySink()` without parameters, which automatically
112
- * uses Sentry's global functions.
113
- *
114
- * @since 1.3.0
115
- */
116
- export interface SentryInstance {
117
- captureMessage: (
118
- message: string,
119
- captureContext?: SeverityLevel | unknown,
120
- ) => string;
121
- captureException: (exception: unknown, hint?: unknown) => string;
122
- }
123
-
124
- /**
125
- * Options for configuring the Sentry sink.
126
- * @since 1.3.0
127
- */
128
- export interface SentrySinkOptions {
129
- /**
130
- * Enable automatic breadcrumb creation for log events.
131
- *
132
- * When enabled, all logs become breadcrumbs in Sentry's isolation scope,
133
- * providing a complete context trail when errors occur. Breadcrumbs are
134
- * lightweight and only appear in error reports for debugging.
135
- *
136
- * @default false
137
- * @since 1.3.0
138
- */
139
- enableBreadcrumbs?: boolean;
140
-
141
- /**
142
- * Optional hook to transform or filter records before sending to Sentry.
143
- * Return `null` to drop the record.
144
- *
145
- * @since 1.3.0
146
- */
147
- beforeSend?: (record: LogRecord) => LogRecord | null;
148
- }
149
-
150
- /**
151
- * Gets a LogTape sink that sends logs to Sentry.
152
- *
153
- * This sink uses Sentry's global capture functions from `@sentry/core`,
154
- * following Sentry v8+ best practices. Simply call `Sentry.init()` before
155
- * creating the sink, and it will automatically use your initialized client.
156
- *
157
- * @param optionsOrClient Optional configuration. Can be:
158
- * - Omitted: Uses global Sentry functions (recommended)
159
- * - Object with options: Configure sink behavior
160
- * - Sentry client instance: Backward compatibility (deprecated)
161
- * @returns A LogTape sink that sends logs to Sentry.
162
- *
163
- * @example Recommended usage - no parameters
164
- * ```typescript
165
- * import { configure } from "@logtape/logtape";
166
- * import { getSentrySink } from "@logtape/sentry";
167
- * import * as Sentry from "@sentry/node";
168
- *
169
- * Sentry.init({ dsn: process.env.SENTRY_DSN });
170
- *
171
- * await configure({
172
- * sinks: {
173
- * sentry: getSentrySink(), // That's it!
174
- * },
175
- * loggers: [
176
- * { category: [], sinks: ["sentry"], lowestLevel: "error" },
177
- * ],
178
- * });
179
- * ```
180
- *
181
- * @example With options
182
- * ```typescript
183
- * import * as Sentry from "@sentry/node";
184
- * Sentry.init({ dsn: process.env.SENTRY_DSN });
185
- *
186
- * await configure({
187
- * sinks: {
188
- * sentry: getSentrySink({
189
- * enableBreadcrumbs: true,
190
- * }),
191
- * },
192
- * loggers: [
193
- * { category: [], sinks: ["sentry"], lowestLevel: "info" },
194
- * ],
195
- * });
196
- * ```
197
- *
198
- * @example Edge functions - must flush before termination
199
- * ```typescript
200
- * // Cloudflare Workers
201
- * export default {
202
- * async fetch(request, env, ctx) {
203
- * logger.error("Something happened");
204
- * ctx.waitUntil(Sentry.flush(2000)); // Don't block response
205
- * return new Response("OK");
206
- * }
207
- * };
208
- * ```
209
- *
210
- * @example Legacy usage (v1.1.x - deprecated)
211
- * ```typescript
212
- * import { getClient } from "@sentry/node";
213
- * const client = getClient();
214
- * getSentrySink(client); // Still works but shows deprecation warning
215
- * ```
216
- *
217
- * @since 1.0.0
218
- */
219
- export function getSentrySink(
220
- optionsOrClient?: SentrySinkOptions | SentryInstance,
221
- ): Sink {
222
- let sentry: SentryInstance | undefined;
223
- let options: SentrySinkOptions = {};
224
-
225
- // Detect which API pattern is being used
226
- if (optionsOrClient == null) {
227
- // Pattern: getSentrySink() - no params (RECOMMENDED)
228
- // Use global functions
229
- } else if (
230
- typeof optionsOrClient === "object" &&
231
- "captureMessage" in optionsOrClient &&
232
- typeof optionsOrClient.captureMessage === "function"
233
- ) {
234
- // Pattern: getSentrySink(client) - DEPRECATED (v1.1.x backward compatibility)
235
- getLogger(["logtape", "meta", "sentry"]).warn(
236
- "Passing a client directly is deprecated and will be removed in v2.0.0. " +
237
- "Use getSentrySink() instead - simpler and recommended!",
238
- );
239
- sentry = optionsOrClient as SentryInstance;
240
- } else if (typeof optionsOrClient === "object") {
241
- // Pattern: getSentrySink({ options }) - options object
242
- options = optionsOrClient as SentrySinkOptions;
243
- } else {
244
- throw new Error(
245
- `[@logtape/sentry] Invalid parameter (type: ${typeof optionsOrClient}).\n\n` +
246
- "Expected one of:\n" +
247
- " getSentrySink() // Recommended\n" +
248
- " getSentrySink({ options }) // With options\n" +
249
- " getSentrySink(client) // Deprecated (v1.1.x compat)\n",
250
- );
251
- }
252
-
253
- // Choose which Sentry functions to use:
254
- // - For capture functions: use client if provided (v1.1.x compat), otherwise globals
255
- // - For scope operations: ALWAYS use globals (client doesn't have these methods)
256
- const captureMessage = sentry
257
- ? (msg: ParameterizedString, ctx?: unknown) =>
258
- sentry.captureMessage(String(msg), ctx)
259
- : globalCaptureMessage;
260
- const captureException = sentry
261
- ? (exception: unknown, hint?: unknown) =>
262
- sentry.captureException(exception, hint)
263
- : globalCaptureException;
264
-
265
- return (record: LogRecord) => {
266
- try {
267
- // Skip meta logger records to prevent infinite recursion
268
- const { category } = record;
269
- if (
270
- category[0] === "logtape" && category[1] === "meta" &&
271
- category[2] === "sentry"
272
- ) {
273
- return;
274
- }
275
-
276
- // Optional transformation/filtering
277
- const transformed = options.beforeSend
278
- ? options.beforeSend(record)
279
- : record;
280
- if (transformed == null) return;
281
-
282
- // Parameterized message for structured logging and events
283
- const paramMessage = getParameterizedString(transformed);
284
- const message = paramMessage.toString();
285
-
286
- // Level mapping
287
- const eventLevel = mapLevelForEvents(transformed.level);
288
-
289
- // Enriched structured attributes
290
- const attributes = {
291
- ...transformed.properties,
292
- "sentry.origin": "auto.logging.logtape",
293
- category: transformed.category.join("."),
294
- timestamp: transformed.timestamp,
295
- } as Record<string, unknown>;
296
-
297
- // After enriched attributes
298
- const activeSpan = globalGetActiveSpan();
299
- if (activeSpan) {
300
- const spanCtx = activeSpan.spanContext();
301
- attributes.trace_id = spanCtx.traceId;
302
- attributes.span_id = spanCtx.spanId;
303
- if ("parentSpanId" in spanCtx) {
304
- attributes.parent_span_id = spanCtx.parentSpanId; // Optional
305
- }
306
- }
307
-
308
- // Send structured log if Sentry logging is enabled (v9.41.0+)
309
- // Uses public logger API when available (SDK 9.41.0+)
310
- const client = globalGetClient();
311
- if (client) {
312
- const { enableLogs, _experiments } = client.getOptions();
313
- const loggingEnabled = enableLogs ?? _experiments?.enableLogs;
314
-
315
- if (loggingEnabled && "logger" in SentryCore) {
316
- const logLevel = mapLevelForLogs(transformed.level);
317
- const sentryLogger = SentryCore.logger as unknown as
318
- | Record<
319
- string,
320
- ((msg: ParameterizedString, attrs: unknown) => void)
321
- >
322
- | undefined;
323
- const logFn = sentryLogger?.[logLevel];
324
- if (typeof logFn === "function") {
325
- logFn(paramMessage, attributes);
326
- }
327
- }
328
- }
329
-
330
- // Capture as Sentry event (Issue) based on level and error presence
331
- // Use compareLogLevel() to handle future severity level additions
332
- const isErrorLevel = compareLogLevel(transformed.level, "error") >= 0;
333
-
334
- if (isErrorLevel && transformed.properties.error instanceof Error) {
335
- // Error instance at error/fatal level -> captureException for stack trace
336
- const { error, ...rest } = attributes;
337
- captureException(error as Error, {
338
- level: eventLevel,
339
- extra: { message, ...rest },
340
- });
341
- } else if (isErrorLevel) {
342
- // Error/fatal level without Error instance -> captureMessage as Issue
343
- captureMessage(paramMessage, {
344
- level: eventLevel,
345
- extra: attributes,
346
- });
347
- } else if (options.enableBreadcrumbs) {
348
- // Non-error levels -> breadcrumbs only (if enabled)
349
- const isolationScope = globalGetIsolationScope();
350
- isolationScope?.addBreadcrumb({
351
- category: transformed.category.join("."),
352
- level: eventLevel,
353
- message,
354
- timestamp: transformed.timestamp / 1000,
355
- data: attributes,
356
- });
357
- }
358
- } catch (err) {
359
- // Never throw from a sink; keep failures silent but visible in debug
360
- try {
361
- console.debug("[@logtape/sentry] sink error", err);
362
- } catch { /* ignore console errors */ }
363
- }
364
- };
365
- }
package/tsdown.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from "tsdown";
2
-
3
- export default defineConfig({
4
- entry: "src/mod.ts",
5
- dts: {
6
- sourcemap: true,
7
- },
8
- format: ["esm", "cjs"],
9
- platform: "node",
10
- unbundle: true,
11
- });