@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 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`.
@@ -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
+ }
@@ -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";
@@ -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
+ }