Skip to content

Commit b608e71

Browse files
committed
Add Sentry error property option
Add SentrySinkOptions.errorPropertyNames so applications can choose which structured log properties are inspected for Error instances. The sink still defaults to the existing error and err property order, while custom lists replace that default and preserve their configured order. Document the option and cover custom property names and precedence with regression tests. #189 Assisted-by: Codex:gpt-5.5
1 parent d87464f commit b608e71

4 files changed

Lines changed: 115 additions & 9 deletions

File tree

CHANGES.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ To be released.
2929
test callback and reporting them only when the callback fails by default.
3030
The reporter uses scoped configuration, preserves wrapped callback
3131
parameters, and supports `mode: "on-failure"`, `mode: "always"`, and
32-
`mode: "never"`. [[#186], [#189]]
32+
`mode: "never"`. [[#186], [#190]]
3333

3434
- Added `FailureLogReporter` interface with `wrap()` and `run()`
3535
methods.
@@ -41,7 +41,7 @@ To be released.
4141
`@logtape/testing` export for compatibility.
4242

4343
[#186]: https://github.com/dahlia/logtape/issues/186
44-
[#189]: https://github.com/dahlia/logtape/pull/189
44+
[#190]: https://github.com/dahlia/logtape/pull/190
4545

4646
### @logtape/file
4747

@@ -69,6 +69,15 @@ To be released.
6969

7070
[#184]: https://github.com/dahlia/logtape/pull/184
7171

72+
### @logtape/sentry
73+
74+
- Added `SentrySinkOptions.errorPropertyNames` option to customize which
75+
structured log properties are checked for `Error` instances before the
76+
Sentry sink chooses `captureException()`. The default remains
77+
`["error", "err"]`. [[#189]]
78+
79+
[#189]: https://github.com/dahlia/logtape/issues/189
80+
7281

7382
Version 2.2.3
7483
-------------

docs/sinks/sentry.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,30 @@ logger.error("User not found: {userId}", { userId: 123 });
253253
logger.info("Request received", { path: "/api/users" });
254254
~~~~
255255

256+
You can customize which property names are checked for an `Error` instance with
257+
the `errorPropertyNames` option. Names are checked in order, and the first
258+
matching `Error` is sent to `captureException`:
259+
260+
~~~~ typescript twoslash
261+
// @noErrors: 2305 2307
262+
import * as Sentry from "@sentry/node";
263+
import { configure } from "@logtape/logtape";
264+
import { getSentrySink } from "@logtape/sentry";
265+
266+
Sentry.init({ dsn: process.env.SENTRY_DSN });
267+
268+
await configure({
269+
sinks: {
270+
sentry: getSentrySink({
271+
errorPropertyNames: ["exception", "error", "err"],
272+
}),
273+
},
274+
loggers: [
275+
{ category: [], sinks: ["sentry"], lowestLevel: "error" },
276+
],
277+
});
278+
~~~~
279+
256280
All logs are sent to Sentry's structured logging (when `enableLogs: true`) and
257281
can become breadcrumbs (when `enableBreadcrumbs: true`), providing full context
258282
when errors occur.

packages/sentry/src/mod.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,61 @@ test("sink prefers error property over err property", () => {
244244
assert.strictEqual(extra.err, err);
245245
});
246246

247+
test("sink uses configured error property names", () => {
248+
let capturedException: unknown;
249+
let capturedHint: unknown;
250+
const error = new Error("Default");
251+
const exception = new Error("Custom");
252+
const sink = getSentrySink({
253+
errorPropertyNames: ["exception"],
254+
sentry: createMockSentryNamespace({
255+
captureException: (capturedExceptionValue, hint) => {
256+
capturedException = capturedExceptionValue;
257+
capturedHint = hint;
258+
return "exception-id";
259+
},
260+
}),
261+
});
262+
263+
sink(createMockLogRecord({
264+
level: "error",
265+
properties: { error, exception, requestId: "request-1" },
266+
}));
267+
268+
const extra = (capturedHint as { extra: Record<string, unknown> }).extra;
269+
assert.strictEqual(capturedException, exception);
270+
assert.strictEqual(extra.error, error);
271+
assert.strictEqual("exception" in extra, false);
272+
assert.strictEqual(extra.requestId, "request-1");
273+
});
274+
275+
test("sink uses configured error property order", () => {
276+
let capturedException: unknown;
277+
let capturedHint: unknown;
278+
const error = new Error("Default");
279+
const exception = new Error("Custom");
280+
const sink = getSentrySink({
281+
errorPropertyNames: ["exception", "error"],
282+
sentry: createMockSentryNamespace({
283+
captureException: (capturedExceptionValue, hint) => {
284+
capturedException = capturedExceptionValue;
285+
capturedHint = hint;
286+
return "exception-id";
287+
},
288+
}),
289+
});
290+
291+
sink(createMockLogRecord({
292+
level: "error",
293+
properties: { error, exception },
294+
}));
295+
296+
const extra = (capturedHint as { extra: Record<string, unknown> }).extra;
297+
assert.strictEqual(capturedException, exception);
298+
assert.strictEqual(extra.error, error);
299+
assert.strictEqual("exception" in extra, false);
300+
});
301+
247302
test("sink without Error at error level does not trigger exception path", () => {
248303
let sawError = false;
249304
const sink = getSentrySink({

packages/sentry/src/mod.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,16 @@ function mapLevelForLogs(level: LogLevel): LogSeverityLevel {
7474
}
7575
}
7676

77+
const defaultErrorPropertyNames = ["error", "err"] as const;
78+
7779
function getErrorProperty(
7880
properties: Readonly<Record<string, unknown>>,
79-
): readonly [property: "error" | "err", error: Error] | undefined {
80-
if (properties.error instanceof Error) {
81-
return ["error", properties.error];
82-
}
83-
if (properties.err instanceof Error) {
84-
return ["err", properties.err];
81+
propertyNames: readonly string[],
82+
): readonly [property: string, error: Error] | undefined {
83+
for (const property of propertyNames) {
84+
if (properties[property] instanceof Error) {
85+
return [property, properties[property]];
86+
}
8587
}
8688
return undefined;
8789
}
@@ -223,6 +225,19 @@ export interface SentrySinkOptions {
223225
*/
224226
enableBreadcrumbs?: boolean;
225227

228+
/**
229+
* Property names to inspect for an `Error` instance when deciding whether
230+
* error-level records should be sent through Sentry's `captureException()`.
231+
*
232+
* Names are checked in order, and the first property containing an `Error`
233+
* instance is used as the captured exception. Set this to a custom list when
234+
* your application or logger stores the primary exception under another name.
235+
*
236+
* @default `["error", "err"]`
237+
* @since 2.3.0
238+
*/
239+
errorPropertyNames?: readonly string[];
240+
226241
/**
227242
* Optional hook to transform or filter records before sending to Sentry.
228243
* Return `null` to drop the record.
@@ -435,7 +450,10 @@ export function getSentrySink(
435450
// Capture as Sentry event (Issue) based on level and error presence
436451
// Use compareLogLevel() to handle future severity level additions
437452
const isErrorLevel = compareLogLevel(transformed.level, "error") >= 0;
438-
const errorProperty = getErrorProperty(transformed.properties);
453+
const errorProperty = getErrorProperty(
454+
transformed.properties,
455+
options.errorPropertyNames ?? defaultErrorPropertyNames,
456+
);
439457

440458
if (isErrorLevel && errorProperty != null) {
441459
// Error instance at error/fatal level -> captureException for stack trace

0 commit comments

Comments
 (0)