@errorgap/bun 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Errorgap
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # @errorgap/bun
2
+
3
+ Bun notifier for [Errorgap](https://errorgap.com). Errors only in v1.
4
+
5
+ Requires Bun 1.0+.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ bun add @errorgap/bun
11
+ ```
12
+
13
+ The package ships as TypeScript source — no build step. Bun loads it
14
+ directly.
15
+
16
+ ## Configure
17
+
18
+ ```ts
19
+ import { Errorgap } from "@errorgap/bun";
20
+
21
+ Errorgap.init({
22
+ endpoint: process.env.ERRORGAP_ENDPOINT,
23
+ projectSlug: process.env.ERRORGAP_PROJECT_SLUG,
24
+ apiKey: process.env.ERRORGAP_API_KEY,
25
+ environment: process.env.NODE_ENV ?? "production",
26
+ });
27
+ ```
28
+
29
+ `init` reads `ERRORGAP_*` env vars when fields are omitted. By default
30
+ it installs `process.on("uncaughtException")` and
31
+ `process.on("unhandledRejection")`; pass `captureGlobals: false` to skip.
32
+
33
+ ## Manual notification
34
+
35
+ ```ts
36
+ try {
37
+ await risky();
38
+ } catch (err) {
39
+ await Errorgap.notify(err, { context: { component: "checkout" } });
40
+ throw err;
41
+ }
42
+ ```
43
+
44
+ `notify` returns a `DeliveryResult` (`status`, `body`, `error`, `queued`).
45
+ The SDK never throws.
46
+
47
+ ## Configuration reference
48
+
49
+ | Option | Default | Notes |
50
+ |---|---|---|
51
+ | `endpoint` | `ERRORGAP_ENDPOINT` or `http://127.0.0.1:3030` | |
52
+ | `projectSlug` | `ERRORGAP_PROJECT_SLUG` | **Required** |
53
+ | `projectId` | `ERRORGAP_PROJECT_ID` | |
54
+ | `apiKey` | `ERRORGAP_API_KEY` | Sent as `x-errorgap-project-key` |
55
+ | `environment` | `NODE_ENV` or `"production"` | |
56
+ | `release` | — | |
57
+ | `async` | `true` | Fire-and-forget delivery |
58
+ | `logger` | `console` | Pass `null` to silence |
59
+ | `filterKeys` | `["password", "token", ...]` | Substring, case-insensitive |
60
+ | `captureGlobals` | `true` | Install process error hooks |
61
+
62
+ ## Graceful flush
63
+
64
+ ```ts
65
+ await Errorgap.flush();
66
+ ```
67
+
68
+ ## Development
69
+
70
+ ```sh
71
+ bun install
72
+ bun test
73
+ ```
74
+
75
+ ## License
76
+
77
+ MIT.
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@errorgap/bun",
3
+ "version": "0.1.0",
4
+ "description": "Bun notifier for Errorgap error tracking.",
5
+ "license": "MIT",
6
+ "author": "Errorgap",
7
+ "homepage": "https://github.com/errorgaphq/errorgap-bun",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/errorgaphq/errorgap-bun.git"
11
+ },
12
+ "keywords": ["errorgap", "bun", "error-tracking", "exceptions", "monitoring"],
13
+ "type": "module",
14
+ "main": "./src/index.ts",
15
+ "module": "./src/index.ts",
16
+ "types": "./src/index.ts",
17
+ "exports": {
18
+ ".": {
19
+ "import": "./src/index.ts",
20
+ "default": "./src/index.ts"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": ["src", "README.md", "LICENSE"],
25
+ "engines": {
26
+ "bun": ">=1.0.0"
27
+ },
28
+ "scripts": {
29
+ "test": "bun test",
30
+ "typecheck": "tsc --noEmit"
31
+ },
32
+ "devDependencies": {
33
+ "@types/bun": "^1.1.0",
34
+ "typescript": "^5.3.3"
35
+ }
36
+ }
@@ -0,0 +1,42 @@
1
+ export interface BacktraceFrame {
2
+ file?: string;
3
+ line?: number;
4
+ function?: string;
5
+ in_app?: boolean;
6
+ index: number;
7
+ }
8
+
9
+ const V8_AT = /^\s*at\s+(?:(.*?)\s+\()?(.+?)(?::(\d+))?(?::(\d+))?\)?$/;
10
+
11
+ export function parseBacktrace(error: Error): BacktraceFrame[] {
12
+ const stack = typeof error.stack === "string" ? error.stack : "";
13
+ if (!stack) return [];
14
+ const lines = stack.split("\n");
15
+ const frames: BacktraceFrame[] = [];
16
+ let index = 0;
17
+ for (const raw of lines) {
18
+ const trimmed = raw.trim();
19
+ if (!trimmed.startsWith("at ")) continue;
20
+ const m = trimmed.match(V8_AT);
21
+ if (!m) continue;
22
+ const fnName = m[1];
23
+ const location = (m[2] ?? "").replace(/^file:\/\//, "");
24
+ const lineNumber = m[3] ? Number(m[3]) : undefined;
25
+ frames.push({
26
+ file: location,
27
+ line: lineNumber,
28
+ function: fnName || undefined,
29
+ in_app: isInApp(location),
30
+ index: index++,
31
+ });
32
+ }
33
+ return frames;
34
+ }
35
+
36
+ function isInApp(file: string): boolean {
37
+ if (!file) return false;
38
+ if (file.includes("/node_modules/")) return false;
39
+ if (file.startsWith("node:")) return false;
40
+ if (file.startsWith("bun:")) return false; // Bun internal modules
41
+ return true;
42
+ }
package/src/client.ts ADDED
@@ -0,0 +1,116 @@
1
+ import type { Configuration } from "./configuration.js";
2
+ import { buildNotice, type NoticeContext, type NoticePayload } from "./notice.js";
3
+ import { VERSION } from "./version.js";
4
+
5
+ export interface DeliveryResult {
6
+ status?: number;
7
+ body?: string;
8
+ error?: unknown;
9
+ queued?: boolean;
10
+ }
11
+
12
+ export class Client {
13
+ private pending = new Set<Promise<unknown>>();
14
+
15
+ constructor(private configuration: Configuration) {}
16
+
17
+ configure(configuration: Configuration): void {
18
+ this.configuration = configuration;
19
+ }
20
+
21
+ async notify(
22
+ error: unknown,
23
+ options: NoticeContext & { sync?: boolean } = {},
24
+ ): Promise<DeliveryResult> {
25
+ try {
26
+ this.configuration.validate();
27
+ const err = coerceError(error);
28
+ const notice = buildNotice(err, this.configuration, options);
29
+
30
+ if (options.sync || !this.configuration.async) {
31
+ const p = this.deliver(notice);
32
+ this.track(p);
33
+ return await p;
34
+ }
35
+
36
+ this.track(this.deliver(notice));
37
+ return { queued: true, status: 202 };
38
+ } catch (exception) {
39
+ this.log(exception);
40
+ return { error: exception };
41
+ }
42
+ }
43
+
44
+ async deliver(notice: NoticePayload): Promise<DeliveryResult> {
45
+ const url = noticesUrl(this.configuration);
46
+ const headers: Record<string, string> = {
47
+ "content-type": "application/json",
48
+ "user-agent": `errorgap-bun/${VERSION}`,
49
+ };
50
+ if (this.configuration.apiKey) {
51
+ headers["x-errorgap-project-key"] = this.configuration.apiKey;
52
+ }
53
+ try {
54
+ const response = await fetch(url, {
55
+ method: "POST",
56
+ headers,
57
+ body: JSON.stringify(notice),
58
+ });
59
+ const body = await safeBody(response);
60
+ return { status: response.status, body };
61
+ } catch (exception) {
62
+ this.log(exception);
63
+ return { error: exception };
64
+ }
65
+ }
66
+
67
+ async flush(): Promise<void> {
68
+ while (this.pending.size > 0) {
69
+ await Promise.all(Array.from(this.pending));
70
+ }
71
+ }
72
+
73
+ private track(promise: Promise<unknown>): void {
74
+ const wrapped = promise.catch(() => undefined);
75
+ this.pending.add(wrapped);
76
+ void wrapped.finally(() => this.pending.delete(wrapped));
77
+ }
78
+
79
+ private log(exception: unknown): void {
80
+ const logger = this.configuration.logger;
81
+ if (!logger) return;
82
+ const message = exception instanceof Error
83
+ ? `${exception.name}: ${exception.message}`
84
+ : String(exception);
85
+ logger.warn(`[errorgap] ${message}`);
86
+ }
87
+ }
88
+
89
+ function noticesUrl(configuration: Configuration): string {
90
+ const base = configuration.endpoint.endsWith("/")
91
+ ? configuration.endpoint.slice(0, -1)
92
+ : configuration.endpoint;
93
+ return `${base}/api/projects/${configuration.projectSlug}/notices`;
94
+ }
95
+
96
+ async function safeBody(response: Response): Promise<string> {
97
+ try {
98
+ return await response.text();
99
+ } catch {
100
+ return "";
101
+ }
102
+ }
103
+
104
+ function coerceError(error: unknown): Error {
105
+ if (error instanceof Error) return error;
106
+ if (typeof error === "string") return new Error(error);
107
+ if (error && typeof error === "object") {
108
+ const obj = error as { message?: unknown; name?: unknown };
109
+ const err = new Error(
110
+ typeof obj.message === "string" ? obj.message : JSON.stringify(error),
111
+ );
112
+ if (typeof obj.name === "string") err.name = obj.name;
113
+ return err;
114
+ }
115
+ return new Error(String(error));
116
+ }
@@ -0,0 +1,58 @@
1
+ export interface Logger {
2
+ warn(message: string, ...args: unknown[]): void;
3
+ }
4
+
5
+ export interface ConfigurationInput {
6
+ endpoint?: string;
7
+ projectSlug?: string;
8
+ projectId?: string;
9
+ apiKey?: string;
10
+ environment?: string;
11
+ release?: string;
12
+ async?: boolean;
13
+ logger?: Logger | null;
14
+ filterKeys?: string[];
15
+ }
16
+
17
+ const DEFAULT_FILTER_KEYS = [
18
+ "password",
19
+ "password_confirmation",
20
+ "token",
21
+ "secret",
22
+ "api_key",
23
+ "authorization",
24
+ "cookie",
25
+ ];
26
+
27
+ export class Configuration {
28
+ endpoint: string;
29
+ projectSlug: string | undefined;
30
+ projectId: string | undefined;
31
+ apiKey: string | undefined;
32
+ environment: string;
33
+ release: string | undefined;
34
+ async: boolean;
35
+ logger: Logger | null;
36
+ filterKeys: string[];
37
+
38
+ constructor(input: ConfigurationInput = {}) {
39
+ this.endpoint = input.endpoint ?? process.env.ERRORGAP_ENDPOINT ?? "http://127.0.0.1:3030";
40
+ this.projectSlug = input.projectSlug ?? process.env.ERRORGAP_PROJECT_SLUG;
41
+ this.projectId = input.projectId ?? process.env.ERRORGAP_PROJECT_ID;
42
+ this.apiKey = input.apiKey ?? process.env.ERRORGAP_API_KEY;
43
+ this.environment = input.environment ?? process.env.NODE_ENV ?? "production";
44
+ this.release = input.release;
45
+ this.async = input.async ?? true;
46
+ this.logger = input.logger === undefined ? console : input.logger;
47
+ this.filterKeys = input.filterKeys ?? [...DEFAULT_FILTER_KEYS];
48
+ }
49
+
50
+ validate(): void {
51
+ if (!this.endpoint || this.endpoint.trim().length === 0) {
52
+ throw new Error("Errorgap endpoint is required");
53
+ }
54
+ if (!this.projectSlug || this.projectSlug.trim().length === 0) {
55
+ throw new Error("Errorgap projectSlug is required");
56
+ }
57
+ }
58
+ }
package/src/filter.ts ADDED
@@ -0,0 +1,31 @@
1
+ const FILTERED = "[FILTERED]";
2
+
3
+ export function filterParams(
4
+ params: Record<string, unknown>,
5
+ filterKeys: string[],
6
+ ): Record<string, unknown> {
7
+ const lowered = filterKeys.map((k) => k.toLowerCase());
8
+ return walk(params, lowered);
9
+ }
10
+
11
+ function walk(
12
+ value: Record<string, unknown>,
13
+ loweredKeys: string[],
14
+ ): Record<string, unknown> {
15
+ const out: Record<string, unknown> = {};
16
+ for (const [key, val] of Object.entries(value)) {
17
+ if (isSensitive(key, loweredKeys)) {
18
+ out[key] = FILTERED;
19
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
20
+ out[key] = walk(val as Record<string, unknown>, loweredKeys);
21
+ } else {
22
+ out[key] = val;
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function isSensitive(key: string, loweredKeys: string[]): boolean {
29
+ const k = key.toLowerCase();
30
+ return loweredKeys.some((needle) => k.includes(needle));
31
+ }
@@ -0,0 +1,37 @@
1
+ import type { Client } from "./client.js";
2
+
3
+ let installed = false;
4
+ let uncaughtHandler: ((err: Error) => void) | null = null;
5
+ let rejectionHandler: ((reason: unknown) => void) | null = null;
6
+
7
+ /**
8
+ * Hook Bun's `uncaughtException` and `unhandledRejection` events on the
9
+ * Node-compatible `process` global.
10
+ */
11
+ export function installProcessHandlers(client: Client): void {
12
+ if (installed) return;
13
+ installed = true;
14
+ uncaughtHandler = (err: Error) => {
15
+ void client.notify(err, {
16
+ sync: true,
17
+ context: { source: "uncaughtException" },
18
+ });
19
+ };
20
+ rejectionHandler = (reason: unknown) => {
21
+ void client.notify(reason, {
22
+ sync: true,
23
+ context: { source: "unhandledRejection" },
24
+ });
25
+ };
26
+ process.on("uncaughtException", uncaughtHandler);
27
+ process.on("unhandledRejection", rejectionHandler);
28
+ }
29
+
30
+ export function uninstallProcessHandlers(): void {
31
+ if (!installed) return;
32
+ if (uncaughtHandler) process.off("uncaughtException", uncaughtHandler);
33
+ if (rejectionHandler) process.off("unhandledRejection", rejectionHandler);
34
+ uncaughtHandler = null;
35
+ rejectionHandler = null;
36
+ installed = false;
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { Client, type DeliveryResult } from "./client.js";
2
+ import { Configuration, type ConfigurationInput } from "./configuration.js";
3
+ import { installProcessHandlers, uninstallProcessHandlers } from "./handlers.js";
4
+ import type { NoticeContext } from "./notice.js";
5
+ import { VERSION } from "./version.js";
6
+
7
+ 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";
11
+ export { Client } from "./client.js";
12
+ export { Configuration } from "./configuration.js";
13
+ export { VERSION };
14
+
15
+ let configuration = new Configuration();
16
+ let client = new Client(configuration);
17
+
18
+ export interface InitOptions extends ConfigurationInput {
19
+ /** Install process.uncaughtException / unhandledRejection handlers. */
20
+ captureGlobals?: boolean;
21
+ }
22
+
23
+ function init(options: InitOptions = {}): void {
24
+ const { captureGlobals = true, ...rest } = options;
25
+ configuration = new Configuration(rest);
26
+ client.configure(configuration);
27
+ if (captureGlobals) {
28
+ installProcessHandlers(client);
29
+ } else {
30
+ uninstallProcessHandlers();
31
+ }
32
+ }
33
+
34
+ function notify(
35
+ error: unknown,
36
+ options: NoticeContext & { sync?: boolean } = {},
37
+ ): Promise<DeliveryResult> {
38
+ return client.notify(error, options);
39
+ }
40
+
41
+ function flush(): Promise<void> {
42
+ return client.flush();
43
+ }
44
+
45
+ function getConfiguration(): Configuration {
46
+ return configuration;
47
+ }
48
+
49
+ function getClient(): Client {
50
+ return client;
51
+ }
52
+
53
+ export const Errorgap = {
54
+ init,
55
+ notify,
56
+ flush,
57
+ configuration: getConfiguration,
58
+ client: getClient,
59
+ VERSION,
60
+ };
61
+
62
+ export { init, notify, flush };
package/src/notice.ts ADDED
@@ -0,0 +1,71 @@
1
+ import type { Configuration } from "./configuration.js";
2
+ import { parseBacktrace, type BacktraceFrame } from "./backtrace.js";
3
+ import { filterParams } from "./filter.js";
4
+ import { VERSION } from "./version.js";
5
+
6
+ export interface NoticeContext {
7
+ context?: Record<string, unknown>;
8
+ environment?: Record<string, unknown>;
9
+ session?: Record<string, unknown>;
10
+ params?: Record<string, unknown>;
11
+ }
12
+
13
+ export interface NoticePayload {
14
+ project_id?: string;
15
+ received_at: string;
16
+ errors: Array<{
17
+ type: string;
18
+ message: string;
19
+ backtrace: BacktraceFrame[];
20
+ }>;
21
+ context: Record<string, unknown>;
22
+ environment: Record<string, unknown>;
23
+ session: Record<string, unknown>;
24
+ params: Record<string, unknown>;
25
+ }
26
+
27
+ export function buildNotice(
28
+ error: Error,
29
+ configuration: Configuration,
30
+ options: NoticeContext = {},
31
+ ): NoticePayload {
32
+ return {
33
+ project_id: configuration.projectId,
34
+ received_at: new Date().toISOString(),
35
+ errors: [
36
+ {
37
+ type: errorType(error),
38
+ message: String(error.message ?? ""),
39
+ backtrace: parseBacktrace(error),
40
+ },
41
+ ],
42
+ context: {
43
+ notifier: "errorgap-bun",
44
+ notifier_version: VERSION,
45
+ environment: configuration.environment,
46
+ release: configuration.release,
47
+ runtime: "bun",
48
+ runtime_version: tryBunVersion(),
49
+ ...(options.context ?? {}),
50
+ },
51
+ environment: options.environment ?? {},
52
+ session: options.session ?? {},
53
+ params: filterParams(options.params ?? {}, configuration.filterKeys),
54
+ };
55
+ }
56
+
57
+ function tryBunVersion(): string | undefined {
58
+ try {
59
+ // @ts-ignore — Bun global is defined when running under Bun.
60
+ return typeof Bun !== "undefined" ? Bun.version : undefined;
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ function errorType(error: Error): string {
67
+ if (typeof error.name === "string" && error.name.length > 0) {
68
+ return error.name;
69
+ }
70
+ return error.constructor?.name ?? "Error";
71
+ }
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export const VERSION = "0.1.0";