@logtape/sentry 1.3.1 → 1.3.2
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 +5 -2
- package/deno.json +0 -14
- package/src/mod.test.ts +0 -270
- package/src/mod.ts +0 -365
- package/tsdown.config.ts +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logtape/sentry",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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.3.
|
|
48
|
+
"@logtape/logtape": "^1.3.2"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@sentry/core": "^9.41.0",
|
package/deno.json
DELETED
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
|
-
}
|