@errorgap/bun 0.1.0 → 0.2.0
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/README.md +57 -2
- package/package.json +13 -3
- package/src/apm.ts +104 -0
- package/src/backtrace.ts +91 -6
- package/src/breadcrumbs.ts +42 -0
- package/src/client.ts +82 -15
- package/src/configuration.ts +30 -1
- package/src/handlers.ts +6 -1
- package/src/index.ts +104 -7
- package/src/logs.ts +44 -0
- package/src/notice.ts +48 -1
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# @errorgap/bun
|
|
2
2
|
|
|
3
|
-
Bun notifier for [Errorgap](https://errorgap.com).
|
|
3
|
+
Bun notifier for [Errorgap](https://errorgap.com). Captures errors with
|
|
4
|
+
source-aware backtraces, nested `Error.cause` chains, breadcrumbs, structured
|
|
5
|
+
logs, and APM transactions.
|
|
6
|
+
|
|
7
|
+
Because Bun executes TypeScript directly, backtrace frames point at the
|
|
8
|
+
original source, so each frame ships a source excerpt (file, line, function,
|
|
9
|
+
and surrounding lines) read straight from disk — the dashboard renders
|
|
10
|
+
highlighted source with no build step or source maps.
|
|
4
11
|
|
|
5
12
|
Requires Bun 1.0+.
|
|
6
13
|
|
|
@@ -44,6 +51,48 @@ try {
|
|
|
44
51
|
`notify` returns a `DeliveryResult` (`status`, `body`, `error`, `queued`).
|
|
45
52
|
The SDK never throws.
|
|
46
53
|
|
|
54
|
+
Nested errors are captured automatically: pass a chain built with
|
|
55
|
+
`new Error("…", { cause })` and each cause's type/message lands in
|
|
56
|
+
`context.causes` while its frames merge into the backtrace.
|
|
57
|
+
|
|
58
|
+
## Breadcrumbs
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
Errorgap.addBreadcrumb("received GET /orders", { category: "http" });
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Recorded breadcrumbs attach to every notice as `context.breadcrumbs`.
|
|
65
|
+
|
|
66
|
+
## Structured logs
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
await Errorgap.log("payment gateway timeout", "error", { source: "payments" });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Levels are `trace < debug < info < warn < error < fatal`; anything below
|
|
73
|
+
`minimumLogLevel` is dropped client-side.
|
|
74
|
+
|
|
75
|
+
## Performance (APM)
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
await Errorgap.trackTransaction(
|
|
79
|
+
{ method: "GET", path: "/orders/{orderId}", pathRaw: "/orders/123" },
|
|
80
|
+
async (spans) => {
|
|
81
|
+
spans.database("SELECT * FROM orders WHERE id = 123", 4.2, { function: "OrderRepo.get" });
|
|
82
|
+
spans.external(88, { function: "PaymentGateway.charge" });
|
|
83
|
+
await handleRequest();
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
await Errorgap.trackJob("ReceiptJob", async (spans) => {
|
|
88
|
+
spans.database("INSERT INTO receipts …", 6);
|
|
89
|
+
}, { queue: "mailers" });
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`path` is the normalized route template used for grouping; `path_raw` is the
|
|
93
|
+
concrete URL. Both helpers time the callback and deliver on completion. Use
|
|
94
|
+
`Errorgap.notifyTransaction(...)` for a pre-measured transaction.
|
|
95
|
+
|
|
47
96
|
## Configuration reference
|
|
48
97
|
|
|
49
98
|
| Option | Default | Notes |
|
|
@@ -53,10 +102,16 @@ The SDK never throws.
|
|
|
53
102
|
| `projectId` | `ERRORGAP_PROJECT_ID` | |
|
|
54
103
|
| `apiKey` | `ERRORGAP_API_KEY` | Sent as `x-errorgap-project-key` |
|
|
55
104
|
| `environment` | `NODE_ENV` or `"production"` | |
|
|
56
|
-
| `release` |
|
|
105
|
+
| `release` | `ERRORGAP_RELEASE` | |
|
|
106
|
+
| `rootDirectory` | `process.cwd()` | Relativizes backtrace source paths |
|
|
57
107
|
| `async` | `true` | Fire-and-forget delivery |
|
|
58
108
|
| `logger` | `console` | Pass `null` to silence |
|
|
59
109
|
| `filterKeys` | `["password", "token", ...]` | Substring, case-insensitive |
|
|
110
|
+
| `apmEnabled` | `true` | Deliver APM transactions |
|
|
111
|
+
| `apmSampleRate` | `1.0` | Fraction (0..1) of transactions delivered |
|
|
112
|
+
| `logsEnabled` | `true` | Deliver structured logs |
|
|
113
|
+
| `minimumLogLevel` | `info` | Drop logs below this level |
|
|
114
|
+
| `maxBreadcrumbs` | `25` | Breadcrumbs retained per notice |
|
|
60
115
|
| `captureGlobals` | `true` | Install process error hooks |
|
|
61
116
|
|
|
62
117
|
## Graceful flush
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@errorgap/bun",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Bun notifier for Errorgap error tracking.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Errorgap",
|
|
@@ -9,7 +9,13 @@
|
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/errorgaphq/errorgap-bun.git"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"errorgap",
|
|
14
|
+
"bun",
|
|
15
|
+
"error-tracking",
|
|
16
|
+
"exceptions",
|
|
17
|
+
"monitoring"
|
|
18
|
+
],
|
|
13
19
|
"type": "module",
|
|
14
20
|
"main": "./src/index.ts",
|
|
15
21
|
"module": "./src/index.ts",
|
|
@@ -21,7 +27,11 @@
|
|
|
21
27
|
},
|
|
22
28
|
"./package.json": "./package.json"
|
|
23
29
|
},
|
|
24
|
-
"files": [
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
25
35
|
"engines": {
|
|
26
36
|
"bun": ">=1.0.0"
|
|
27
37
|
},
|
package/src/apm.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Configuration } from "./configuration.js";
|
|
2
|
+
|
|
3
|
+
export interface Span {
|
|
4
|
+
kind: string;
|
|
5
|
+
sql?: string;
|
|
6
|
+
file?: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
function?: string;
|
|
9
|
+
durationMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SpanLocation {
|
|
13
|
+
file?: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
function?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function databaseSpan(sql: string, durationMs: number, location: SpanLocation = {}): Span {
|
|
19
|
+
return { kind: "db", sql: normalizeSql(sql), durationMs, ...location };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function externalSpan(durationMs: number, location: SpanLocation = {}): Span {
|
|
23
|
+
return { kind: "http", durationMs, ...location };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Collects spans recorded while a transaction or job is in flight. */
|
|
27
|
+
export class SpanCollector {
|
|
28
|
+
private spans: Span[] = [];
|
|
29
|
+
|
|
30
|
+
add(span: Span): void {
|
|
31
|
+
this.spans.push(span);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
database(sql: string, durationMs: number, location: SpanLocation = {}): void {
|
|
35
|
+
this.add(databaseSpan(sql, durationMs, location));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
external(durationMs: number, location: SpanLocation = {}): void {
|
|
39
|
+
this.add(externalSpan(durationMs, location));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
snapshot(): Span[] {
|
|
43
|
+
return [...this.spans];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Transaction {
|
|
48
|
+
/** "web" for HTTP interactions, "job" for background work. */
|
|
49
|
+
kind?: string;
|
|
50
|
+
method?: string;
|
|
51
|
+
/** Normalized route template used for grouping, e.g. `/orders/{id}`. */
|
|
52
|
+
path?: string;
|
|
53
|
+
/** Concrete path for a single request, e.g. `/orders/123`. */
|
|
54
|
+
pathRaw?: string;
|
|
55
|
+
statusCode?: number;
|
|
56
|
+
durationMs: number;
|
|
57
|
+
environment?: string;
|
|
58
|
+
/** ISO-8601. Defaults to now. */
|
|
59
|
+
occurredAt?: string;
|
|
60
|
+
spans?: Span[];
|
|
61
|
+
jobClass?: string;
|
|
62
|
+
queue?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function transactionPayload(
|
|
66
|
+
transaction: Transaction,
|
|
67
|
+
configuration: Configuration,
|
|
68
|
+
): Record<string, unknown> {
|
|
69
|
+
const payload: Record<string, unknown> = {
|
|
70
|
+
kind: transaction.kind ?? "web",
|
|
71
|
+
duration_ms: transaction.durationMs,
|
|
72
|
+
environment: transaction.environment ?? configuration.environment,
|
|
73
|
+
occurred_at: transaction.occurredAt ?? new Date().toISOString(),
|
|
74
|
+
spans: (transaction.spans ?? []).map(spanPayload),
|
|
75
|
+
};
|
|
76
|
+
if (transaction.method !== undefined) payload.method = transaction.method;
|
|
77
|
+
if (transaction.path !== undefined) payload.path = transaction.path;
|
|
78
|
+
if (transaction.pathRaw !== undefined) payload.path_raw = transaction.pathRaw;
|
|
79
|
+
if (transaction.statusCode !== undefined) payload.status_code = transaction.statusCode;
|
|
80
|
+
if (transaction.jobClass !== undefined) payload.job_class = transaction.jobClass;
|
|
81
|
+
if (transaction.queue !== undefined) payload.queue = transaction.queue;
|
|
82
|
+
return payload;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function spanPayload(span: Span): Record<string, unknown> {
|
|
86
|
+
const payload: Record<string, unknown> = {
|
|
87
|
+
kind: span.kind,
|
|
88
|
+
duration_ms: span.durationMs,
|
|
89
|
+
};
|
|
90
|
+
if (span.sql !== undefined) payload.sql = span.sql;
|
|
91
|
+
if (span.file !== undefined) payload.file = span.file;
|
|
92
|
+
if (span.line !== undefined) payload.line = span.line;
|
|
93
|
+
if (span.function !== undefined) payload.fn_name = span.function;
|
|
94
|
+
return payload;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Strip literals so query shapes aggregate: '…' and numbers become ?. */
|
|
98
|
+
export function normalizeSql(sql: string): string {
|
|
99
|
+
return sql
|
|
100
|
+
.replace(/'(?:''|[^'])*'/g, "?")
|
|
101
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, "?")
|
|
102
|
+
.replace(/\s+/g, " ")
|
|
103
|
+
.trim();
|
|
104
|
+
}
|
package/src/backtrace.ts
CHANGED
|
@@ -1,19 +1,44 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export interface SourceExcerpt {
|
|
5
|
+
start_line: number;
|
|
6
|
+
lines: string[];
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
export interface BacktraceFrame {
|
|
2
10
|
file?: string;
|
|
3
11
|
line?: number;
|
|
12
|
+
column?: number;
|
|
4
13
|
function?: string;
|
|
5
14
|
in_app?: boolean;
|
|
6
15
|
index: number;
|
|
16
|
+
source?: SourceExcerpt;
|
|
7
17
|
}
|
|
8
18
|
|
|
9
19
|
const V8_AT = /^\s*at\s+(?:(.*?)\s+\()?(.+?)(?::(\d+))?(?::(\d+))?\)?$/;
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
const SOURCE_CONTEXT_RADIUS = 6;
|
|
22
|
+
const MAX_SOURCE_LINE_CHARS = 400;
|
|
23
|
+
const MAX_SOURCE_FILE_BYTES = 2 * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
/** Cache of file line arrays, keyed by resolved path, for one notice build. */
|
|
26
|
+
const fileCache = new Map<string, string[] | null>();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a V8-style `Error.stack` into Errorgap frames. Because Bun executes
|
|
30
|
+
* TypeScript directly, frame locations point at the original source files, so
|
|
31
|
+
* each frame's source excerpt is read straight from disk.
|
|
32
|
+
*/
|
|
33
|
+
export function parseBacktrace(error: Error, rootDirectory?: string): BacktraceFrame[] {
|
|
12
34
|
const stack = typeof error.stack === "string" ? error.stack : "";
|
|
13
35
|
if (!stack) return [];
|
|
36
|
+
fileCache.clear();
|
|
37
|
+
|
|
14
38
|
const lines = stack.split("\n");
|
|
15
39
|
const frames: BacktraceFrame[] = [];
|
|
16
40
|
let index = 0;
|
|
41
|
+
|
|
17
42
|
for (const raw of lines) {
|
|
18
43
|
const trimmed = raw.trim();
|
|
19
44
|
if (!trimmed.startsWith("at ")) continue;
|
|
@@ -21,15 +46,25 @@ export function parseBacktrace(error: Error): BacktraceFrame[] {
|
|
|
21
46
|
if (!m) continue;
|
|
22
47
|
const fnName = m[1];
|
|
23
48
|
const location = (m[2] ?? "").replace(/^file:\/\//, "");
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
49
|
+
const line = m[3] ? Number(m[3]) : undefined;
|
|
50
|
+
const column = m[4] ? Number(m[4]) : undefined;
|
|
51
|
+
|
|
52
|
+
const frame: BacktraceFrame = {
|
|
53
|
+
file: displayPath(location, rootDirectory),
|
|
54
|
+
line,
|
|
55
|
+
column,
|
|
28
56
|
function: fnName || undefined,
|
|
29
57
|
in_app: isInApp(location),
|
|
30
58
|
index: index++,
|
|
31
|
-
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const source = sourceExcerpt(location, line, rootDirectory);
|
|
62
|
+
if (source) frame.source = source;
|
|
63
|
+
|
|
64
|
+
frames.push(frame);
|
|
32
65
|
}
|
|
66
|
+
|
|
67
|
+
fileCache.clear();
|
|
33
68
|
return frames;
|
|
34
69
|
}
|
|
35
70
|
|
|
@@ -40,3 +75,53 @@ function isInApp(file: string): boolean {
|
|
|
40
75
|
if (file.startsWith("bun:")) return false; // Bun internal modules
|
|
41
76
|
return true;
|
|
42
77
|
}
|
|
78
|
+
|
|
79
|
+
/** Strip the app root prefix for a cleaner in-app display path. */
|
|
80
|
+
function displayPath(file: string, root?: string): string {
|
|
81
|
+
if (!file) return file;
|
|
82
|
+
if (root) {
|
|
83
|
+
const normalized = root.endsWith("/") ? root : root + "/";
|
|
84
|
+
if (file.startsWith(normalized)) return file.slice(normalized.length);
|
|
85
|
+
}
|
|
86
|
+
return file;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sourceExcerpt(
|
|
90
|
+
file: string,
|
|
91
|
+
line: number | undefined,
|
|
92
|
+
root?: string,
|
|
93
|
+
): SourceExcerpt | undefined {
|
|
94
|
+
if (!file || !line || line < 1) return undefined;
|
|
95
|
+
if (file.startsWith("node:") || file.startsWith("bun:")) return undefined;
|
|
96
|
+
|
|
97
|
+
const contents = readSourceLines(resolvePath(file, root));
|
|
98
|
+
if (!contents || line > contents.length) return undefined;
|
|
99
|
+
|
|
100
|
+
const startLine = Math.max(1, line - SOURCE_CONTEXT_RADIUS);
|
|
101
|
+
const endLine = Math.min(contents.length, line + SOURCE_CONTEXT_RADIUS);
|
|
102
|
+
return {
|
|
103
|
+
start_line: startLine,
|
|
104
|
+
lines: contents.slice(startLine - 1, endLine).map((l) => l.slice(0, MAX_SOURCE_LINE_CHARS)),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolvePath(file: string, root?: string): string {
|
|
109
|
+
if (isAbsolute(file)) return file;
|
|
110
|
+
if (root) return join(root, file);
|
|
111
|
+
return file;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readSourceLines(path: string): string[] | null {
|
|
115
|
+
if (fileCache.has(path)) return fileCache.get(path) ?? null;
|
|
116
|
+
let lines: string[] | null = null;
|
|
117
|
+
try {
|
|
118
|
+
const stat = statSync(path);
|
|
119
|
+
if (stat.isFile() && stat.size <= MAX_SOURCE_FILE_BYTES) {
|
|
120
|
+
lines = readFileSync(path, "utf8").split(/\r?\n/);
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
lines = null;
|
|
124
|
+
}
|
|
125
|
+
fileCache.set(path, lines);
|
|
126
|
+
return lines;
|
|
127
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface Breadcrumb {
|
|
2
|
+
message: string;
|
|
3
|
+
category?: string;
|
|
4
|
+
metadata?: Record<string, unknown>;
|
|
5
|
+
timestamp: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface BreadcrumbInput {
|
|
9
|
+
category?: string;
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Fixed-size ring of recent app events (requests, queries, jobs) attached to
|
|
15
|
+
* every notice as `context.breadcrumbs`.
|
|
16
|
+
*/
|
|
17
|
+
export class BreadcrumbBuffer {
|
|
18
|
+
private crumbs: Breadcrumb[] = [];
|
|
19
|
+
|
|
20
|
+
constructor(private capacity: number) {}
|
|
21
|
+
|
|
22
|
+
add(message: string, input: BreadcrumbInput = {}): void {
|
|
23
|
+
if (this.capacity <= 0) return;
|
|
24
|
+
this.crumbs.push({
|
|
25
|
+
message,
|
|
26
|
+
...(input.category !== undefined ? { category: input.category } : {}),
|
|
27
|
+
...(input.metadata !== undefined ? { metadata: input.metadata } : {}),
|
|
28
|
+
timestamp: new Date().toISOString(),
|
|
29
|
+
});
|
|
30
|
+
if (this.crumbs.length > this.capacity) {
|
|
31
|
+
this.crumbs.splice(0, this.crumbs.length - this.capacity);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
clear(): void {
|
|
36
|
+
this.crumbs = [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
snapshot(): Breadcrumb[] {
|
|
40
|
+
return [...this.crumbs];
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Configuration } from "./configuration.js";
|
|
2
2
|
import { buildNotice, type NoticeContext, type NoticePayload } from "./notice.js";
|
|
3
|
+
import { transactionPayload, type Transaction } from "./apm.js";
|
|
4
|
+
import { logLevelRank, normalizeLogLevel } from "./logs.js";
|
|
3
5
|
import { VERSION } from "./version.js";
|
|
4
6
|
|
|
5
7
|
export interface DeliveryResult {
|
|
@@ -9,6 +11,13 @@ export interface DeliveryResult {
|
|
|
9
11
|
queued?: boolean;
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
export interface LogOptions {
|
|
15
|
+
source?: string;
|
|
16
|
+
environment?: string;
|
|
17
|
+
occurredAt?: string;
|
|
18
|
+
sync?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export class Client {
|
|
13
22
|
private pending = new Set<Promise<unknown>>();
|
|
14
23
|
|
|
@@ -26,23 +35,80 @@ export class Client {
|
|
|
26
35
|
this.configuration.validate();
|
|
27
36
|
const err = coerceError(error);
|
|
28
37
|
const notice = buildNotice(err, this.configuration, options);
|
|
38
|
+
return this.submit("notices", notice, options.sync);
|
|
39
|
+
} catch (exception) {
|
|
40
|
+
this.log(exception);
|
|
41
|
+
return { error: exception };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
/** Deliver an APM transaction (HTTP interaction or background job). */
|
|
46
|
+
async notifyTransaction(
|
|
47
|
+
transaction: Transaction,
|
|
48
|
+
options: { sync?: boolean } = {},
|
|
49
|
+
): Promise<DeliveryResult> {
|
|
50
|
+
try {
|
|
51
|
+
this.configuration.validate();
|
|
52
|
+
} catch (exception) {
|
|
53
|
+
this.log(exception);
|
|
54
|
+
return { error: exception };
|
|
55
|
+
}
|
|
56
|
+
if (!this.configuration.apmEnabled) {
|
|
57
|
+
return { status: 204 };
|
|
58
|
+
}
|
|
59
|
+
const rate = this.configuration.apmSampleRate;
|
|
60
|
+
if (!(rate >= 1 || (rate > 0 && Math.random() < rate))) {
|
|
61
|
+
return { status: 204 };
|
|
62
|
+
}
|
|
63
|
+
return this.submit("transactions", transactionPayload(transaction, this.configuration), options.sync);
|
|
64
|
+
}
|
|
35
65
|
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
/** Deliver a structured log line. */
|
|
67
|
+
async notifyLog(
|
|
68
|
+
message: string,
|
|
69
|
+
level = "info",
|
|
70
|
+
options: LogOptions = {},
|
|
71
|
+
): Promise<DeliveryResult> {
|
|
72
|
+
try {
|
|
73
|
+
this.configuration.validate();
|
|
38
74
|
} catch (exception) {
|
|
39
75
|
this.log(exception);
|
|
40
76
|
return { error: exception };
|
|
41
77
|
}
|
|
78
|
+
const normalizedLevel = normalizeLogLevel(level);
|
|
79
|
+
if (
|
|
80
|
+
!this.configuration.logsEnabled ||
|
|
81
|
+
logLevelRank(normalizedLevel) <
|
|
82
|
+
logLevelRank(normalizeLogLevel(this.configuration.minimumLogLevel))
|
|
83
|
+
) {
|
|
84
|
+
return { status: 204 };
|
|
85
|
+
}
|
|
86
|
+
const payload: Record<string, unknown> = {
|
|
87
|
+
message,
|
|
88
|
+
level: normalizedLevel,
|
|
89
|
+
environment: options.environment ?? this.configuration.environment,
|
|
90
|
+
occurred_at: options.occurredAt ?? new Date().toISOString(),
|
|
91
|
+
};
|
|
92
|
+
if (options.source) payload.source = options.source;
|
|
93
|
+
return this.submit("logs", payload, options.sync);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private async submit(
|
|
97
|
+
resource: string,
|
|
98
|
+
payload: unknown,
|
|
99
|
+
sync?: boolean,
|
|
100
|
+
): Promise<DeliveryResult> {
|
|
101
|
+
if (sync || !this.configuration.async) {
|
|
102
|
+
const p = this.deliver(resource, payload);
|
|
103
|
+
this.track(p);
|
|
104
|
+
return await p;
|
|
105
|
+
}
|
|
106
|
+
this.track(this.deliver(resource, payload));
|
|
107
|
+
return { queued: true, status: 202 };
|
|
42
108
|
}
|
|
43
109
|
|
|
44
|
-
async deliver(
|
|
45
|
-
const url =
|
|
110
|
+
async deliver(resource: string, payload: unknown): Promise<DeliveryResult> {
|
|
111
|
+
const url = resourceUrl(this.configuration, resource);
|
|
46
112
|
const headers: Record<string, string> = {
|
|
47
113
|
"content-type": "application/json",
|
|
48
114
|
"user-agent": `errorgap-bun/${VERSION}`,
|
|
@@ -54,7 +120,7 @@ export class Client {
|
|
|
54
120
|
const response = await fetch(url, {
|
|
55
121
|
method: "POST",
|
|
56
122
|
headers,
|
|
57
|
-
body: JSON.stringify(
|
|
123
|
+
body: JSON.stringify(payload),
|
|
58
124
|
});
|
|
59
125
|
const body = await safeBody(response);
|
|
60
126
|
return { status: response.status, body };
|
|
@@ -79,18 +145,19 @@ export class Client {
|
|
|
79
145
|
private log(exception: unknown): void {
|
|
80
146
|
const logger = this.configuration.logger;
|
|
81
147
|
if (!logger) return;
|
|
82
|
-
const message =
|
|
83
|
-
|
|
84
|
-
|
|
148
|
+
const message =
|
|
149
|
+
exception instanceof Error
|
|
150
|
+
? `${exception.name}: ${exception.message}`
|
|
151
|
+
: String(exception);
|
|
85
152
|
logger.warn(`[errorgap] ${message}`);
|
|
86
153
|
}
|
|
87
154
|
}
|
|
88
155
|
|
|
89
|
-
function
|
|
156
|
+
function resourceUrl(configuration: Configuration, resource: string): string {
|
|
90
157
|
const base = configuration.endpoint.endsWith("/")
|
|
91
158
|
? configuration.endpoint.slice(0, -1)
|
|
92
159
|
: configuration.endpoint;
|
|
93
|
-
return `${base}/api/projects/${configuration.projectSlug}
|
|
160
|
+
return `${base}/api/projects/${configuration.projectSlug}/${resource}`;
|
|
94
161
|
}
|
|
95
162
|
|
|
96
163
|
async function safeBody(response: Response): Promise<string> {
|
package/src/configuration.ts
CHANGED
|
@@ -12,6 +12,18 @@ export interface ConfigurationInput {
|
|
|
12
12
|
async?: boolean;
|
|
13
13
|
logger?: Logger | null;
|
|
14
14
|
filterKeys?: string[];
|
|
15
|
+
/** Root directory used to relativize backtrace source paths. Default cwd. */
|
|
16
|
+
rootDirectory?: string;
|
|
17
|
+
/** Enable APM transaction delivery. Default true. */
|
|
18
|
+
apmEnabled?: boolean;
|
|
19
|
+
/** Fraction (0..1) of transactions to deliver. Default 1. */
|
|
20
|
+
apmSampleRate?: number;
|
|
21
|
+
/** Enable structured log delivery. Default true. */
|
|
22
|
+
logsEnabled?: boolean;
|
|
23
|
+
/** Drop logs below this level (trace<debug<info<warn<error<fatal). Default "info". */
|
|
24
|
+
minimumLogLevel?: string;
|
|
25
|
+
/** Number of breadcrumbs retained and attached to notices. Default 25. */
|
|
26
|
+
maxBreadcrumbs?: number;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
const DEFAULT_FILTER_KEYS = [
|
|
@@ -34,6 +46,12 @@ export class Configuration {
|
|
|
34
46
|
async: boolean;
|
|
35
47
|
logger: Logger | null;
|
|
36
48
|
filterKeys: string[];
|
|
49
|
+
rootDirectory: string;
|
|
50
|
+
apmEnabled: boolean;
|
|
51
|
+
apmSampleRate: number;
|
|
52
|
+
logsEnabled: boolean;
|
|
53
|
+
minimumLogLevel: string;
|
|
54
|
+
maxBreadcrumbs: number;
|
|
37
55
|
|
|
38
56
|
constructor(input: ConfigurationInput = {}) {
|
|
39
57
|
this.endpoint = input.endpoint ?? process.env.ERRORGAP_ENDPOINT ?? "http://127.0.0.1:3030";
|
|
@@ -41,10 +59,16 @@ export class Configuration {
|
|
|
41
59
|
this.projectId = input.projectId ?? process.env.ERRORGAP_PROJECT_ID;
|
|
42
60
|
this.apiKey = input.apiKey ?? process.env.ERRORGAP_API_KEY;
|
|
43
61
|
this.environment = input.environment ?? process.env.NODE_ENV ?? "production";
|
|
44
|
-
this.release = input.release;
|
|
62
|
+
this.release = input.release ?? process.env.ERRORGAP_RELEASE;
|
|
45
63
|
this.async = input.async ?? true;
|
|
46
64
|
this.logger = input.logger === undefined ? console : input.logger;
|
|
47
65
|
this.filterKeys = input.filterKeys ?? [...DEFAULT_FILTER_KEYS];
|
|
66
|
+
this.rootDirectory = input.rootDirectory ?? process.cwd();
|
|
67
|
+
this.apmEnabled = input.apmEnabled ?? true;
|
|
68
|
+
this.apmSampleRate = clampRate(input.apmSampleRate ?? 1);
|
|
69
|
+
this.logsEnabled = input.logsEnabled ?? true;
|
|
70
|
+
this.minimumLogLevel = input.minimumLogLevel ?? "info";
|
|
71
|
+
this.maxBreadcrumbs = Math.max(0, Math.trunc(input.maxBreadcrumbs ?? 25));
|
|
48
72
|
}
|
|
49
73
|
|
|
50
74
|
validate(): void {
|
|
@@ -56,3 +80,8 @@ export class Configuration {
|
|
|
56
80
|
}
|
|
57
81
|
}
|
|
58
82
|
}
|
|
83
|
+
|
|
84
|
+
function clampRate(rate: number): number {
|
|
85
|
+
if (!Number.isFinite(rate)) return 1;
|
|
86
|
+
return Math.min(1, Math.max(0, rate));
|
|
87
|
+
}
|
package/src/handlers.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Client } from "./client.js";
|
|
2
|
+
import type { BreadcrumbBuffer } from "./breadcrumbs.js";
|
|
2
3
|
|
|
3
4
|
let installed = false;
|
|
4
5
|
let uncaughtHandler: ((err: Error) => void) | null = null;
|
|
@@ -8,19 +9,23 @@ let rejectionHandler: ((reason: unknown) => void) | null = null;
|
|
|
8
9
|
* Hook Bun's `uncaughtException` and `unhandledRejection` events on the
|
|
9
10
|
* Node-compatible `process` global.
|
|
10
11
|
*/
|
|
11
|
-
export function installProcessHandlers(client: Client): void {
|
|
12
|
+
export function installProcessHandlers(client: Client, breadcrumbs?: BreadcrumbBuffer): void {
|
|
12
13
|
if (installed) return;
|
|
13
14
|
installed = true;
|
|
15
|
+
const snapshot = () => breadcrumbs?.snapshot() ?? [];
|
|
16
|
+
|
|
14
17
|
uncaughtHandler = (err: Error) => {
|
|
15
18
|
void client.notify(err, {
|
|
16
19
|
sync: true,
|
|
17
20
|
context: { source: "uncaughtException" },
|
|
21
|
+
breadcrumbs: snapshot(),
|
|
18
22
|
});
|
|
19
23
|
};
|
|
20
24
|
rejectionHandler = (reason: unknown) => {
|
|
21
25
|
void client.notify(reason, {
|
|
22
26
|
sync: true,
|
|
23
27
|
context: { source: "unhandledRejection" },
|
|
28
|
+
breadcrumbs: snapshot(),
|
|
24
29
|
});
|
|
25
30
|
};
|
|
26
31
|
process.on("uncaughtException", uncaughtHandler);
|
package/src/index.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import { Client, type DeliveryResult } from "./client.js";
|
|
1
|
+
import { Client, type DeliveryResult, type LogOptions } from "./client.js";
|
|
2
2
|
import { Configuration, type ConfigurationInput } from "./configuration.js";
|
|
3
3
|
import { installProcessHandlers, uninstallProcessHandlers } from "./handlers.js";
|
|
4
|
+
import { BreadcrumbBuffer, type BreadcrumbInput } from "./breadcrumbs.js";
|
|
5
|
+
import { SpanCollector, type Transaction } from "./apm.js";
|
|
4
6
|
import type { NoticeContext } from "./notice.js";
|
|
5
7
|
import { VERSION } from "./version.js";
|
|
6
8
|
|
|
7
9
|
export type { ConfigurationInput, Logger } from "./configuration.js";
|
|
8
|
-
export type { BacktraceFrame } from "./backtrace.js";
|
|
9
|
-
export type { NoticeContext, NoticePayload } from "./notice.js";
|
|
10
|
-
export type { DeliveryResult } from "./client.js";
|
|
10
|
+
export type { BacktraceFrame, SourceExcerpt } from "./backtrace.js";
|
|
11
|
+
export type { NoticeContext, NoticePayload, NoticeCause } from "./notice.js";
|
|
12
|
+
export type { DeliveryResult, LogOptions } from "./client.js";
|
|
13
|
+
export type { Breadcrumb, BreadcrumbInput } from "./breadcrumbs.js";
|
|
14
|
+
export type { Span, SpanLocation, Transaction } from "./apm.js";
|
|
11
15
|
export { Client } from "./client.js";
|
|
12
16
|
export { Configuration } from "./configuration.js";
|
|
17
|
+
export { SpanCollector, databaseSpan, externalSpan, normalizeSql } from "./apm.js";
|
|
18
|
+
export { BreadcrumbBuffer } from "./breadcrumbs.js";
|
|
13
19
|
export { VERSION };
|
|
14
20
|
|
|
15
21
|
let configuration = new Configuration();
|
|
16
22
|
let client = new Client(configuration);
|
|
23
|
+
let breadcrumbs = new BreadcrumbBuffer(configuration.maxBreadcrumbs);
|
|
17
24
|
|
|
18
25
|
export interface InitOptions extends ConfigurationInput {
|
|
19
26
|
/** Install process.uncaughtException / unhandledRejection handlers. */
|
|
@@ -24,8 +31,9 @@ function init(options: InitOptions = {}): void {
|
|
|
24
31
|
const { captureGlobals = true, ...rest } = options;
|
|
25
32
|
configuration = new Configuration(rest);
|
|
26
33
|
client.configure(configuration);
|
|
34
|
+
breadcrumbs = new BreadcrumbBuffer(configuration.maxBreadcrumbs);
|
|
27
35
|
if (captureGlobals) {
|
|
28
|
-
installProcessHandlers(client);
|
|
36
|
+
installProcessHandlers(client, breadcrumbs);
|
|
29
37
|
} else {
|
|
30
38
|
uninstallProcessHandlers();
|
|
31
39
|
}
|
|
@@ -35,7 +43,80 @@ function notify(
|
|
|
35
43
|
error: unknown,
|
|
36
44
|
options: NoticeContext & { sync?: boolean } = {},
|
|
37
45
|
): Promise<DeliveryResult> {
|
|
38
|
-
return client.notify(error, options);
|
|
46
|
+
return client.notify(error, { breadcrumbs: breadcrumbs.snapshot(), ...options });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Record a diagnostic breadcrumb attached to subsequent notices. */
|
|
50
|
+
function addBreadcrumb(message: string, input: BreadcrumbInput = {}): void {
|
|
51
|
+
breadcrumbs.add(message, input);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function clearBreadcrumbs(): void {
|
|
55
|
+
breadcrumbs.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Deliver a structured log line at the given level. */
|
|
59
|
+
function log(message: string, level = "info", options: LogOptions = {}): Promise<DeliveryResult> {
|
|
60
|
+
return client.notifyLog(message, level, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Deliver an APM transaction (HTTP interaction or background job). */
|
|
64
|
+
function notifyTransaction(
|
|
65
|
+
transaction: Transaction,
|
|
66
|
+
options: { sync?: boolean } = {},
|
|
67
|
+
): Promise<DeliveryResult> {
|
|
68
|
+
return client.notifyTransaction(transaction, options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Time an HTTP interaction and deliver it as a transaction. The callback
|
|
73
|
+
* receives a `SpanCollector` for recording DB/HTTP spans.
|
|
74
|
+
*/
|
|
75
|
+
async function trackTransaction<T>(
|
|
76
|
+
meta: Omit<Transaction, "durationMs" | "spans" | "kind"> & { kind?: string },
|
|
77
|
+
operation: (spans: SpanCollector) => Promise<T> | T,
|
|
78
|
+
): Promise<T> {
|
|
79
|
+
const spans = new SpanCollector();
|
|
80
|
+
const startedAt = new Date().toISOString();
|
|
81
|
+
const start = Date.now();
|
|
82
|
+
try {
|
|
83
|
+
return await operation(spans);
|
|
84
|
+
} finally {
|
|
85
|
+
void notifyTransaction({
|
|
86
|
+
kind: meta.kind ?? "web",
|
|
87
|
+
...meta,
|
|
88
|
+
occurredAt: meta.occurredAt ?? startedAt,
|
|
89
|
+
durationMs: Date.now() - start,
|
|
90
|
+
spans: spans.snapshot(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Time a background job and deliver it as a `job` transaction. The callback
|
|
97
|
+
* receives a `SpanCollector` for recording DB/HTTP spans.
|
|
98
|
+
*/
|
|
99
|
+
async function trackJob<T>(
|
|
100
|
+
jobClass: string,
|
|
101
|
+
operation: (spans: SpanCollector) => Promise<T> | T,
|
|
102
|
+
meta: { queue?: string; environment?: string } = {},
|
|
103
|
+
): Promise<T> {
|
|
104
|
+
const spans = new SpanCollector();
|
|
105
|
+
const startedAt = new Date().toISOString();
|
|
106
|
+
const start = Date.now();
|
|
107
|
+
try {
|
|
108
|
+
return await operation(spans);
|
|
109
|
+
} finally {
|
|
110
|
+
void notifyTransaction({
|
|
111
|
+
kind: "job",
|
|
112
|
+
jobClass,
|
|
113
|
+
queue: meta.queue ?? "default",
|
|
114
|
+
environment: meta.environment,
|
|
115
|
+
occurredAt: startedAt,
|
|
116
|
+
durationMs: Date.now() - start,
|
|
117
|
+
spans: spans.snapshot(),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
39
120
|
}
|
|
40
121
|
|
|
41
122
|
function flush(): Promise<void> {
|
|
@@ -53,10 +134,26 @@ function getClient(): Client {
|
|
|
53
134
|
export const Errorgap = {
|
|
54
135
|
init,
|
|
55
136
|
notify,
|
|
137
|
+
addBreadcrumb,
|
|
138
|
+
clearBreadcrumbs,
|
|
139
|
+
log,
|
|
140
|
+
notifyTransaction,
|
|
141
|
+
trackTransaction,
|
|
142
|
+
trackJob,
|
|
56
143
|
flush,
|
|
57
144
|
configuration: getConfiguration,
|
|
58
145
|
client: getClient,
|
|
59
146
|
VERSION,
|
|
60
147
|
};
|
|
61
148
|
|
|
62
|
-
export {
|
|
149
|
+
export {
|
|
150
|
+
init,
|
|
151
|
+
notify,
|
|
152
|
+
addBreadcrumb,
|
|
153
|
+
clearBreadcrumbs,
|
|
154
|
+
log,
|
|
155
|
+
notifyTransaction,
|
|
156
|
+
trackTransaction,
|
|
157
|
+
trackJob,
|
|
158
|
+
flush,
|
|
159
|
+
};
|
package/src/logs.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function normalizeLogLevel(level: string): string {
|
|
2
|
+
switch (level.trim().toLowerCase()) {
|
|
3
|
+
case "warning":
|
|
4
|
+
case "warn":
|
|
5
|
+
return "warn";
|
|
6
|
+
case "err":
|
|
7
|
+
case "severe":
|
|
8
|
+
case "critical":
|
|
9
|
+
return "error";
|
|
10
|
+
case "notice":
|
|
11
|
+
return "info";
|
|
12
|
+
case "fine":
|
|
13
|
+
case "finer":
|
|
14
|
+
case "finest":
|
|
15
|
+
return "debug";
|
|
16
|
+
case "trace":
|
|
17
|
+
case "debug":
|
|
18
|
+
case "info":
|
|
19
|
+
case "error":
|
|
20
|
+
case "fatal":
|
|
21
|
+
return level.trim().toLowerCase();
|
|
22
|
+
default:
|
|
23
|
+
return "info";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function logLevelRank(level: string): number {
|
|
28
|
+
switch (level) {
|
|
29
|
+
case "trace":
|
|
30
|
+
return 0;
|
|
31
|
+
case "debug":
|
|
32
|
+
return 10;
|
|
33
|
+
case "info":
|
|
34
|
+
return 20;
|
|
35
|
+
case "warn":
|
|
36
|
+
return 30;
|
|
37
|
+
case "error":
|
|
38
|
+
return 40;
|
|
39
|
+
case "fatal":
|
|
40
|
+
return 50;
|
|
41
|
+
default:
|
|
42
|
+
return 20;
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/notice.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import type { Configuration } from "./configuration.js";
|
|
2
2
|
import { parseBacktrace, type BacktraceFrame } from "./backtrace.js";
|
|
3
|
+
import type { Breadcrumb } from "./breadcrumbs.js";
|
|
3
4
|
import { filterParams } from "./filter.js";
|
|
4
5
|
import { VERSION } from "./version.js";
|
|
5
6
|
|
|
7
|
+
const MAX_CAUSE_DEPTH = 10;
|
|
8
|
+
|
|
6
9
|
export interface NoticeContext {
|
|
7
10
|
context?: Record<string, unknown>;
|
|
8
11
|
environment?: Record<string, unknown>;
|
|
9
12
|
session?: Record<string, unknown>;
|
|
10
13
|
params?: Record<string, unknown>;
|
|
14
|
+
breadcrumbs?: Breadcrumb[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface NoticeCause {
|
|
18
|
+
type: string;
|
|
19
|
+
message: string;
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export interface NoticePayload {
|
|
@@ -29,6 +38,9 @@ export function buildNotice(
|
|
|
29
38
|
configuration: Configuration,
|
|
30
39
|
options: NoticeContext = {},
|
|
31
40
|
): NoticePayload {
|
|
41
|
+
const causes = collectCauses(error);
|
|
42
|
+
const breadcrumbs = options.breadcrumbs ?? [];
|
|
43
|
+
|
|
32
44
|
return {
|
|
33
45
|
project_id: configuration.projectId,
|
|
34
46
|
received_at: new Date().toISOString(),
|
|
@@ -36,7 +48,7 @@ export function buildNotice(
|
|
|
36
48
|
{
|
|
37
49
|
type: errorType(error),
|
|
38
50
|
message: String(error.message ?? ""),
|
|
39
|
-
backtrace:
|
|
51
|
+
backtrace: flattenBacktrace(error, configuration.rootDirectory),
|
|
40
52
|
},
|
|
41
53
|
],
|
|
42
54
|
context: {
|
|
@@ -46,6 +58,8 @@ export function buildNotice(
|
|
|
46
58
|
release: configuration.release,
|
|
47
59
|
runtime: "bun",
|
|
48
60
|
runtime_version: tryBunVersion(),
|
|
61
|
+
...(causes.length > 0 ? { causes } : {}),
|
|
62
|
+
...(breadcrumbs.length > 0 ? { breadcrumbs } : {}),
|
|
49
63
|
...(options.context ?? {}),
|
|
50
64
|
},
|
|
51
65
|
environment: options.environment ?? {},
|
|
@@ -54,6 +68,39 @@ export function buildNotice(
|
|
|
54
68
|
};
|
|
55
69
|
}
|
|
56
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Walk `error.cause` (ES2022) and merge each cause's frames into a single
|
|
73
|
+
* backtrace, re-indexing so the dashboard renders the full chain in one view.
|
|
74
|
+
*/
|
|
75
|
+
function flattenBacktrace(error: Error, rootDirectory: string): BacktraceFrame[] {
|
|
76
|
+
const frames: BacktraceFrame[] = [];
|
|
77
|
+
let index = 0;
|
|
78
|
+
for (const link of errorChain(error)) {
|
|
79
|
+
for (const frame of parseBacktrace(link, rootDirectory)) {
|
|
80
|
+
frames.push({ ...frame, index: index++ });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return frames;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function collectCauses(error: Error): NoticeCause[] {
|
|
87
|
+
return errorChain(error)
|
|
88
|
+
.slice(1)
|
|
89
|
+
.map((link) => ({ type: errorType(link), message: String(link.message ?? "") }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function errorChain(error: Error): Error[] {
|
|
93
|
+
const chain: Error[] = [];
|
|
94
|
+
let current: unknown = error;
|
|
95
|
+
const seen = new Set<unknown>();
|
|
96
|
+
while (current instanceof Error && !seen.has(current) && chain.length < MAX_CAUSE_DEPTH) {
|
|
97
|
+
seen.add(current);
|
|
98
|
+
chain.push(current);
|
|
99
|
+
current = (current as { cause?: unknown }).cause;
|
|
100
|
+
}
|
|
101
|
+
return chain;
|
|
102
|
+
}
|
|
103
|
+
|
|
57
104
|
function tryBunVersion(): string | undefined {
|
|
58
105
|
try {
|
|
59
106
|
// @ts-ignore — Bun global is defined when running under Bun.
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.2.0";
|