@xyph3r/faultline 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/README.md +35 -0
- package/dist/client.d.ts +19 -0
- package/dist/client.js +129 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +103 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.js +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# faultline
|
|
2
|
+
|
|
3
|
+
Self-hosted error tracking SDK. Zero dependencies, works everywhere.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Faultline } from "faultline"
|
|
7
|
+
|
|
8
|
+
Faultline.init({
|
|
9
|
+
dsn: process.env.FAULTLINE_DSN,
|
|
10
|
+
baseUrl: process.env.FAULTLINE_BASE_URL
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// Manual capture
|
|
14
|
+
Faultline.capture(err, { route: "/api/checkout", userId: "usr_123" })
|
|
15
|
+
|
|
16
|
+
// Wrap a handler
|
|
17
|
+
export const POST = Faultline.withCapture(async (req: Request) => {
|
|
18
|
+
// thrown errors are captured and rethrown
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Observer hooks
|
|
22
|
+
Faultline.on("beforeCapture", (payload) => {
|
|
23
|
+
delete payload.metadata?.password // strip PII
|
|
24
|
+
})
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install faultline
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
Node 18+, Bun, Deno, or any runtime with `fetch` and `crypto`.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CaptureContext, FaultlineOptions, IngestPayload } from "./types";
|
|
2
|
+
export declare class FaultlineClient {
|
|
3
|
+
readonly options: {
|
|
4
|
+
dsn: string | undefined;
|
|
5
|
+
baseUrl: string | undefined;
|
|
6
|
+
env: string;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
debug: boolean;
|
|
9
|
+
fetch: typeof fetch | undefined;
|
|
10
|
+
onBeforeCapture: ((payload: IngestPayload) => IngestPayload | void) | undefined;
|
|
11
|
+
onAfterCapture: ((payload: IngestPayload) => void) | undefined;
|
|
12
|
+
onCaptureError: ((error: Error, payload: IngestPayload) => void) | undefined;
|
|
13
|
+
};
|
|
14
|
+
constructor(options?: FaultlineOptions);
|
|
15
|
+
capture(error: unknown, context?: CaptureContext): Promise<void>;
|
|
16
|
+
private buildIngestUrl;
|
|
17
|
+
private send;
|
|
18
|
+
private warn;
|
|
19
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export class FaultlineClient {
|
|
2
|
+
options;
|
|
3
|
+
constructor(options = {}) {
|
|
4
|
+
this.options = {
|
|
5
|
+
dsn: options.dsn ?? process.env.FAULTLINE_DSN,
|
|
6
|
+
baseUrl: options.baseUrl ?? process.env.FAULTLINE_BASE_URL ?? "https://faultline.dev",
|
|
7
|
+
env: options.env ?? process.env.NODE_ENV ?? "production",
|
|
8
|
+
enabled: options.enabled ?? true,
|
|
9
|
+
debug: options.debug ?? false,
|
|
10
|
+
fetch: options.fetch,
|
|
11
|
+
onBeforeCapture: options.onBeforeCapture,
|
|
12
|
+
onAfterCapture: options.onAfterCapture,
|
|
13
|
+
onCaptureError: options.onCaptureError
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
capture(error, context = {}) {
|
|
17
|
+
if (!this.options.enabled) {
|
|
18
|
+
return Promise.resolve();
|
|
19
|
+
}
|
|
20
|
+
if (!this.options.dsn) {
|
|
21
|
+
this.warn("FAULTLINE_DSN is not set");
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
let payload = buildPayload(error, {
|
|
25
|
+
...context,
|
|
26
|
+
env: this.options.env
|
|
27
|
+
});
|
|
28
|
+
if (this.options.onBeforeCapture) {
|
|
29
|
+
const modified = this.options.onBeforeCapture(payload);
|
|
30
|
+
if (modified)
|
|
31
|
+
payload = modified;
|
|
32
|
+
}
|
|
33
|
+
return this.send(payload);
|
|
34
|
+
}
|
|
35
|
+
buildIngestUrl() {
|
|
36
|
+
return `${this.options.baseUrl}/ingest/${this.options.dsn}`;
|
|
37
|
+
}
|
|
38
|
+
async send(payload) {
|
|
39
|
+
const fetchImpl = this.options.fetch ?? globalThis.fetch;
|
|
40
|
+
if (typeof fetchImpl !== "function") {
|
|
41
|
+
this.warn("Global fetch is not available in this runtime");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
await fetchImpl(this.buildIngestUrl(), {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: {
|
|
48
|
+
"content-type": "application/json"
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(payload)
|
|
51
|
+
});
|
|
52
|
+
this.options.onAfterCapture?.(payload);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
56
|
+
this.options.onCaptureError?.(err, payload);
|
|
57
|
+
this.warn(`Faultline capture failed: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
warn(message) {
|
|
61
|
+
if (this.options.debug) {
|
|
62
|
+
console.warn(message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function buildPayload(error, context) {
|
|
67
|
+
const normalized = normalizeError(error);
|
|
68
|
+
return {
|
|
69
|
+
title: normalized.title,
|
|
70
|
+
message: normalized.message,
|
|
71
|
+
stack: normalized.stack,
|
|
72
|
+
file: normalized.file,
|
|
73
|
+
line: normalized.line,
|
|
74
|
+
col: normalized.col,
|
|
75
|
+
route: context.route,
|
|
76
|
+
env: context.env,
|
|
77
|
+
level: context.level ?? "error",
|
|
78
|
+
userId: context.userId,
|
|
79
|
+
metadata: context.metadata
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function normalizeError(error) {
|
|
83
|
+
if (error instanceof Error) {
|
|
84
|
+
const stackLocation = parseStackLocation(error.stack);
|
|
85
|
+
return {
|
|
86
|
+
title: error.name || "Error",
|
|
87
|
+
message: error.message,
|
|
88
|
+
stack: error.stack,
|
|
89
|
+
file: stackLocation?.file,
|
|
90
|
+
line: stackLocation?.line,
|
|
91
|
+
col: stackLocation?.col
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (typeof error === "string") {
|
|
95
|
+
return {
|
|
96
|
+
title: "Error",
|
|
97
|
+
message: error
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
title: "UnknownError",
|
|
102
|
+
message: safeJson(error)
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function parseStackLocation(stack) {
|
|
106
|
+
if (!stack) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const lines = stack.split("\n");
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
const match = line.match(/\(?([^\s()]+):(\d+):(\d+)\)?$/);
|
|
112
|
+
if (match) {
|
|
113
|
+
return {
|
|
114
|
+
file: match[1],
|
|
115
|
+
line: Number(match[2]),
|
|
116
|
+
col: Number(match[3])
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
function safeJson(value) {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.stringify(value);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return String(value);
|
|
128
|
+
}
|
|
129
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CaptureContext, FaultlineEvents, FaultlineOptions } from "./types";
|
|
2
|
+
type EventHandler<E extends keyof FaultlineEvents> = (payload: FaultlineEvents[E]) => void;
|
|
3
|
+
/**
|
|
4
|
+
* # Pattern: Singleton + Observer
|
|
5
|
+
* # Problem: Error capture must work from anywhere without passing an instance,
|
|
6
|
+
* and consumers need hooks to filter/enrich/log events.
|
|
7
|
+
* # Solution: Faultline.init() creates a global singleton; Faultline.on() registers
|
|
8
|
+
* typed observers; Faultline.capture() delegates to the singleton.
|
|
9
|
+
* # Trade-off: Global mutable state — justified because error tracking is
|
|
10
|
+
* a cross-cutting concern like logging. Multiple instances still work via constructor.
|
|
11
|
+
*/
|
|
12
|
+
export declare class Faultline {
|
|
13
|
+
private static client;
|
|
14
|
+
private static listeners;
|
|
15
|
+
static init(options?: FaultlineOptions): typeof Faultline;
|
|
16
|
+
static capture(error: unknown, context?: CaptureContext): Promise<void>;
|
|
17
|
+
static withCapture<TArgs extends unknown[], TResult>(handler: (...args: TArgs) => TResult | Promise<TResult>, getContext?: (error: unknown, args: TArgs) => CaptureContext): (...args: TArgs) => Promise<TResult>;
|
|
18
|
+
static expressHandler(): (error: unknown, req: {
|
|
19
|
+
originalUrl?: string;
|
|
20
|
+
url?: string;
|
|
21
|
+
route?: {
|
|
22
|
+
path?: string;
|
|
23
|
+
};
|
|
24
|
+
}, _res: unknown, next: (error?: unknown) => void) => Promise<void>;
|
|
25
|
+
static on<E extends keyof FaultlineEvents>(event: E, handler: EventHandler<E>): () => void;
|
|
26
|
+
static off<E extends keyof FaultlineEvents>(event: E, handler: EventHandler<E>): void;
|
|
27
|
+
private static emit;
|
|
28
|
+
private client;
|
|
29
|
+
constructor(options?: FaultlineOptions);
|
|
30
|
+
capture(error: unknown, context?: CaptureContext): Promise<void>;
|
|
31
|
+
withCapture: typeof Faultline.withCapture;
|
|
32
|
+
expressHandler: typeof Faultline.expressHandler;
|
|
33
|
+
}
|
|
34
|
+
export type { CaptureContext, FaultlineOptions };
|
|
35
|
+
export type { IngestPayload, FaultlineEvents } from "./types";
|
|
36
|
+
export { FaultlineClient } from "./client";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { FaultlineClient } from "./client";
|
|
2
|
+
/**
|
|
3
|
+
* # Pattern: Singleton + Observer
|
|
4
|
+
* # Problem: Error capture must work from anywhere without passing an instance,
|
|
5
|
+
* and consumers need hooks to filter/enrich/log events.
|
|
6
|
+
* # Solution: Faultline.init() creates a global singleton; Faultline.on() registers
|
|
7
|
+
* typed observers; Faultline.capture() delegates to the singleton.
|
|
8
|
+
* # Trade-off: Global mutable state — justified because error tracking is
|
|
9
|
+
* a cross-cutting concern like logging. Multiple instances still work via constructor.
|
|
10
|
+
*/
|
|
11
|
+
export class Faultline {
|
|
12
|
+
static client = null;
|
|
13
|
+
static listeners = new Map();
|
|
14
|
+
// ── Singleton ──
|
|
15
|
+
static init(options = {}) {
|
|
16
|
+
Faultline.client = new FaultlineClient({
|
|
17
|
+
...options,
|
|
18
|
+
onBeforeCapture: (payload) => {
|
|
19
|
+
Faultline.emit("beforeCapture", payload);
|
|
20
|
+
return payload;
|
|
21
|
+
},
|
|
22
|
+
onAfterCapture: (payload) => {
|
|
23
|
+
Faultline.emit("afterCapture", payload);
|
|
24
|
+
},
|
|
25
|
+
onCaptureError: (error, payload) => {
|
|
26
|
+
Faultline.emit("captureError", { error: error.message, ...payload });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return Faultline;
|
|
30
|
+
}
|
|
31
|
+
static capture(error, context = {}) {
|
|
32
|
+
if (!Faultline.client) {
|
|
33
|
+
if (typeof process !== "undefined" && process.env.NODE_ENV === "development") {
|
|
34
|
+
console.warn("Faultline not initialized — call Faultline.init() first");
|
|
35
|
+
}
|
|
36
|
+
return Promise.resolve();
|
|
37
|
+
}
|
|
38
|
+
return Faultline.client.capture(error, context);
|
|
39
|
+
}
|
|
40
|
+
static withCapture(handler, getContext) {
|
|
41
|
+
return async (...args) => {
|
|
42
|
+
try {
|
|
43
|
+
return await handler(...args);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const context = getContext ? getContext(error, args) : inferContext(args);
|
|
47
|
+
await Faultline.capture(error, context);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
static expressHandler() {
|
|
53
|
+
return async (error, req, _res, next) => {
|
|
54
|
+
await Faultline.capture(error, {
|
|
55
|
+
route: req.originalUrl ?? req.route?.path ?? req.url
|
|
56
|
+
});
|
|
57
|
+
next(error);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// ── Observer ──
|
|
61
|
+
static on(event, handler) {
|
|
62
|
+
if (!Faultline.listeners.has(event)) {
|
|
63
|
+
Faultline.listeners.set(event, new Set());
|
|
64
|
+
}
|
|
65
|
+
Faultline.listeners.get(event).add(handler);
|
|
66
|
+
return () => Faultline.off(event, handler);
|
|
67
|
+
}
|
|
68
|
+
static off(event, handler) {
|
|
69
|
+
Faultline.listeners.get(event)?.delete(handler);
|
|
70
|
+
}
|
|
71
|
+
static emit(event, payload) {
|
|
72
|
+
Faultline.listeners.get(event)?.forEach((fn) => {
|
|
73
|
+
try {
|
|
74
|
+
fn(payload);
|
|
75
|
+
}
|
|
76
|
+
catch { /* observer errors must never propagate */ }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// ── Instance (isolated, for testing or multi-project use) ──
|
|
80
|
+
client;
|
|
81
|
+
constructor(options = {}) {
|
|
82
|
+
this.client = new FaultlineClient(options);
|
|
83
|
+
}
|
|
84
|
+
capture(error, context = {}) {
|
|
85
|
+
return this.client.capture(error, context);
|
|
86
|
+
}
|
|
87
|
+
withCapture = Faultline.withCapture;
|
|
88
|
+
expressHandler = Faultline.expressHandler;
|
|
89
|
+
}
|
|
90
|
+
// ── Helpers ──
|
|
91
|
+
function inferContext(args) {
|
|
92
|
+
const [first] = args;
|
|
93
|
+
if (first && typeof first === "object" && "url" in first && typeof first.url === "string") {
|
|
94
|
+
try {
|
|
95
|
+
return { route: new URL(first.url).pathname };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { route: first.url };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
export { FaultlineClient } from "./client";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type CaptureContext = {
|
|
2
|
+
userId?: string;
|
|
3
|
+
route?: string;
|
|
4
|
+
metadata?: Record<string, unknown>;
|
|
5
|
+
level?: "error" | "warning" | "info";
|
|
6
|
+
};
|
|
7
|
+
export type IngestPayload = {
|
|
8
|
+
title: string;
|
|
9
|
+
message?: string;
|
|
10
|
+
stack?: string;
|
|
11
|
+
route?: string;
|
|
12
|
+
file?: string;
|
|
13
|
+
line?: number;
|
|
14
|
+
col?: number;
|
|
15
|
+
env?: string;
|
|
16
|
+
level?: "error" | "warning" | "info";
|
|
17
|
+
userId?: string;
|
|
18
|
+
metadata?: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
export type FaultlineOptions = {
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
dsn?: string;
|
|
23
|
+
env?: string;
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
debug?: boolean;
|
|
26
|
+
fetch?: typeof fetch;
|
|
27
|
+
onBeforeCapture?: (payload: IngestPayload) => IngestPayload | void;
|
|
28
|
+
onAfterCapture?: (payload: IngestPayload) => void;
|
|
29
|
+
onCaptureError?: (error: Error, payload: IngestPayload) => void;
|
|
30
|
+
};
|
|
31
|
+
export type FaultlineEvents = {
|
|
32
|
+
beforeCapture: IngestPayload;
|
|
33
|
+
afterCapture: IngestPayload;
|
|
34
|
+
captureError: IngestPayload & {
|
|
35
|
+
error: string;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type ExpressLikeRequest = {
|
|
39
|
+
originalUrl?: string;
|
|
40
|
+
url?: string;
|
|
41
|
+
route?: {
|
|
42
|
+
path?: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
export type ExpressLikeNext = (error?: unknown) => void;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xyph3r/faultline",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Self-hosted error tracking SDK. Zero dependencies, works everywhere.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/faisalahmedsifat/faultline.git",
|
|
9
|
+
"directory": "packages/sdk"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"error-tracking",
|
|
13
|
+
"error-monitoring",
|
|
14
|
+
"self-hosted",
|
|
15
|
+
"sentry-alternative",
|
|
16
|
+
"typescript"
|
|
17
|
+
],
|
|
18
|
+
"author": "Faisal Ahmed Sifat",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"main": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./types": {
|
|
28
|
+
"types": "./dist/types.d.ts",
|
|
29
|
+
"default": "./dist/types.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.json",
|
|
41
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
43
|
+
}
|
|
44
|
+
}
|