reportli 1.0.2 → 1.0.3

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/src/index.ts CHANGED
@@ -1,180 +1,425 @@
1
- // reportli.ts
1
+ // src/index.ts — Reportli SDK v1.0.3
2
2
 
3
3
  const ENDPOINT =
4
4
  "https://fahikyfmgdyzejdfftox.supabase.co/functions/v1/rapid-processor";
5
5
 
6
+ // ─── Types ────────────────────────────────────────────────────────────────────
7
+
6
8
  type Config = {
7
9
  apiKey: string;
8
10
  environment?: string;
9
11
  };
10
12
 
13
+ type QueueItem = Record<string, unknown>;
14
+
15
+ // ─── State ────────────────────────────────────────────────────────────────────
16
+
11
17
  let initialized = false;
12
- let config: Config;
13
-
14
- function post(payload: Record<string, unknown>) {
15
- fetch(ENDPOINT, {
16
- method: "POST",
17
- headers: {
18
- "Content-Type": "application/json",
19
- },
20
- body: JSON.stringify(payload),
21
- }).catch(() => {
22
- // Never throw from the SDK.
23
- });
18
+ let _config: Config;
19
+ const queue: QueueItem[] = [];
20
+ let flushTimer: ReturnType<typeof setTimeout> | null = null;
21
+ let isFlushing = false;
22
+
23
+ // ─── Queue & Batch Send ───────────────────────────────────────────────────────
24
+
25
+ function scheduleFlush() {
26
+ if (flushTimer) return;
27
+ flushTimer = setTimeout(() => {
28
+ flushTimer = null;
29
+ flush();
30
+ }, 2000); // batch every 2 seconds
31
+ }
32
+
33
+ async function flush() {
34
+ if (isFlushing || queue.length === 0) return;
35
+ isFlushing = true;
36
+
37
+ const batch = queue.splice(0, 10); // send max 10 at a time
38
+
39
+ for (const payload of batch) {
40
+ await sendNow(payload);
41
+ }
42
+
43
+ isFlushing = false;
44
+
45
+ if (queue.length > 0) flush(); // flush remaining
46
+ }
47
+
48
+ async function sendNow(payload: QueueItem, attempts = 3): Promise<void> {
49
+ for (let i = 0; i < attempts; i++) {
50
+ try {
51
+ const res = await fetch(ENDPOINT, {
52
+ method: "POST",
53
+ headers: {
54
+ "Content-Type": "application/json",
55
+ "x-api-key": _config?.apiKey ?? "",
56
+ },
57
+ body: JSON.stringify(payload),
58
+ });
59
+ if (res.ok) return; // success — stop retrying
60
+ } catch {
61
+ // network error — wait before retry
62
+ }
63
+ await sleep(1000 * (i + 1)); // 1s, 2s, 3s
64
+ }
65
+ // give up silently after 3 attempts — never crash user app
66
+ }
67
+
68
+ function sleep(ms: number) {
69
+ return new Promise((resolve) => setTimeout(resolve, ms));
70
+ }
71
+
72
+ // ─── Enqueue ──────────────────────────────────────────────────────────────────
73
+
74
+ function enqueue(payload: QueueItem) {
75
+ if (queue.length >= 100) return; // cap queue to prevent memory issues
76
+ queue.push(payload);
77
+ scheduleFlush();
78
+ }
79
+
80
+ // ─── Send immediately (for critical messages) ─────────────────────────────────
81
+
82
+ function sendImmediate(payload: QueueItem) {
83
+ sendNow(payload).catch(() => {}); // fire and forget
84
+ }
85
+
86
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
87
+
88
+ function getUrl(): string {
89
+ if (typeof window !== "undefined") return window.location.href;
90
+ try { return require("os").hostname(); } catch { return "server"; }
24
91
  }
25
92
 
26
- function sendError(data: {
27
- message: string;
28
- code?: string;
29
- stack?: string;
30
- file?: string;
31
- line?: number;
32
- column?: number;
33
- severity?: string;
34
- }) {
35
- post({
93
+ function getBrowser(): string {
94
+ if (typeof navigator !== "undefined") return navigator.userAgent;
95
+ return `Node.js ${typeof process !== "undefined" ? process.version : "unknown"}`;
96
+ }
97
+
98
+ function getEnvironment(): string {
99
+ return _config?.environment ?? (typeof window !== "undefined" ? "browser" : "server");
100
+ }
101
+
102
+ function parseStack(stack: string | undefined): { file: string; line: number; column: number } {
103
+ if (!stack) return { file: "unknown", line: 0, column: 0 };
104
+ const match =
105
+ stack.match(/at .+ \((.+):(\d+):(\d+)\)/) ||
106
+ stack.match(/at (.+):(\d+):(\d+)/);
107
+ return {
108
+ file: match?.[1]?.split("/")?.pop() || "unknown",
109
+ line: parseInt(match?.[2] || "0"),
110
+ column: parseInt(match?.[3] || "0"),
111
+ };
112
+ }
113
+
114
+ function getErrorCode(error: any): string {
115
+ return (
116
+ error?.code ||
117
+ error?.status?.toString() ||
118
+ error?.statusCode?.toString() ||
119
+ error?.name ||
120
+ "ERR_UNKNOWN"
121
+ );
122
+ }
123
+
124
+ function classifyError(error: any, context?: string): { category: string; severity: "low" | "medium" | "high" | "critical" } {
125
+ const msg = String(error?.message || error || "").toLowerCase();
126
+ const name = String(error?.name || "").toLowerCase();
127
+
128
+ // Payment — always critical
129
+ if (msg.includes("stripe") || msg.includes("payment") || msg.includes("card declined") || msg.includes("checkout") || msg.includes("refund"))
130
+ return { category: "Payment Error", severity: "critical" };
131
+
132
+ // Auth
133
+ if (msg.includes("jwt") || msg.includes("token expired") || msg.includes("unauthorized") || msg.includes("session") || msg.includes("oauth") || msg.includes("login failed"))
134
+ return { category: "Auth Error", severity: "high" };
135
+
136
+ // Database
137
+ if (msg.includes("supabase") || msg.includes("database") || msg.includes("query") || msg.includes("connection lost") || msg.includes("transaction") || msg.includes("duplicate key") || msg.includes("foreign key"))
138
+ return { category: "Database Error", severity: "critical" };
139
+
140
+ // React/Next.js
141
+ if (msg.includes("hydration") || msg.includes("does not match server"))
142
+ return { category: "Hydration Error", severity: "high" };
143
+ if (msg.includes("invalid hook") || msg.includes("rules of hooks"))
144
+ return { category: "Hook Error", severity: "critical" };
145
+ if (msg.includes("render") || msg.includes("error boundary"))
146
+ return { category: "Render Error", severity: "critical" };
147
+ if (msg.includes("dynamic import") || msg.includes("failed to fetch dynamically"))
148
+ return { category: "Import Error", severity: "high" };
149
+
150
+ // Network
151
+ if (msg.includes("cors") || msg.includes("cross-origin"))
152
+ return { category: "CORS Error", severity: "high" };
153
+ if (msg.includes("fetch failed") || msg.includes("failed to fetch") || name === "fetcherror")
154
+ return { category: "Network Error", severity: "high" };
155
+ if (msg.includes("timeout") || msg.includes("timed out") || msg.includes("etimedout"))
156
+ return { category: "Timeout Error", severity: "medium" };
157
+ if (msg.includes("websocket"))
158
+ return { category: "WebSocket Error", severity: "high" };
159
+
160
+ // HTTP status codes
161
+ if (msg.includes("http 401") || msg.includes("xhr 401"))
162
+ return { category: "Auth Error", severity: "high" };
163
+ if (msg.includes("http 403") || msg.includes("xhr 403"))
164
+ return { category: "Auth Error", severity: "high" };
165
+ if (msg.includes("http 404") || msg.includes("xhr 404"))
166
+ return { category: "Not Found Error", severity: "medium" };
167
+ if (msg.includes("http 500") || msg.includes("xhr 500"))
168
+ return { category: "Server Error", severity: "critical" };
169
+ if (msg.includes("http 503") || msg.includes("xhr 503"))
170
+ return { category: "Server Error", severity: "critical" };
171
+
172
+ // Memory
173
+ if (msg.includes("maximum call stack") || msg.includes("out of memory") || msg.includes("heap limit"))
174
+ return { category: "Memory Error", severity: "critical" };
175
+
176
+ // Server
177
+ if (msg.includes("cannot find module") || msg.includes("module not found"))
178
+ return { category: "Module Error", severity: "critical" };
179
+ if (msg.includes("econnrefused") || msg.includes("connection refused"))
180
+ return { category: "Connection Error", severity: "critical" };
181
+
182
+ // Email & Jobs
183
+ if (msg.includes("email") || msg.includes("smtp") || msg.includes("sendgrid"))
184
+ return { category: "Email Error", severity: "high" };
185
+ if (msg.includes("cron") || msg.includes("webhook") || msg.includes("queue"))
186
+ return { category: "Job Error", severity: "high" };
187
+
188
+ // Files
189
+ if (msg.includes("upload") || msg.includes("file size") || msg.includes("invalid file"))
190
+ return { category: "File Error", severity: "medium" };
191
+
192
+ // Storage
193
+ if (msg.includes("quota exceeded") || msg.includes("localstorage") || msg.includes("indexeddb"))
194
+ return { category: "Storage Error", severity: "medium" };
195
+
196
+ // JS core
197
+ if (name === "typeerror") return { category: "TypeError", severity: "high" };
198
+ if (name === "referenceerror") return { category: "ReferenceError", severity: "critical" };
199
+ if (name === "rangeerror") return { category: "RangeError", severity: "high" };
200
+ if (name === "syntaxerror") return { category: "SyntaxError", severity: "high" };
201
+
202
+ // Context based
203
+ if (context === "unhandledrejection") return { category: "Promise Error", severity: "medium" };
204
+ if (context === "express") return { category: "Server Error", severity: "high" };
205
+ if (context === "resource") return { category: "Resource Error", severity: "low" };
206
+
207
+ return { category: "Unknown Error", severity: "medium" };
208
+ }
209
+
210
+ function buildErrorPayload(error: any, context?: string): QueueItem {
211
+ const message = error?.message || String(error) || "Unknown error";
212
+ const stack = error?.stack || "";
213
+ const { file, line, column } = parseStack(stack);
214
+ const { category, severity } = classifyError(error, context);
215
+
216
+ return {
36
217
  type: "ERROR",
37
- apiKey: config.apiKey,
38
- message: data.message,
39
- code: data.code ?? "Error",
40
- stack: data.stack ?? null,
41
- file: data.file ?? null,
42
- line: data.line ?? null,
43
- column: data.column ?? null,
44
- url:
45
- typeof window !== "undefined" ? window.location.href : null,
218
+ apiKey: _config?.apiKey,
219
+ message,
220
+ code: getErrorCode(error),
221
+ stack,
222
+ file,
223
+ line,
224
+ column,
225
+ url: getUrl(),
46
226
  timestamp: new Date().toISOString(),
47
- environment: config.environment ?? "production",
48
- browser:
49
- typeof navigator !== "undefined"
50
- ? navigator.userAgent
51
- : "node",
52
- error_category: data.code ?? "Error",
53
- severity: data.severity ?? "high",
227
+ environment: getEnvironment(),
228
+ browser: getBrowser(),
229
+ error_category: category,
230
+ severity,
54
231
  status: "open",
55
- });
232
+ context: context || "auto",
233
+ };
56
234
  }
57
235
 
58
- export const Reportli = {
59
- init(cfg: Config) {
60
- if (initialized) return;
236
+ // ─── Browser Listeners ────────────────────────────────────────────────────────
61
237
 
62
- config = cfg;
63
- initialized = true;
238
+ function activateBrowserListeners() {
239
+ // JS errors + resource errors
240
+ window.addEventListener("error", (event) => {
241
+ // Resource load error (img, script, link, video)
242
+ const target = event.target as HTMLElement;
243
+ if (target && target.tagName && ["IMG", "SCRIPT", "LINK", "VIDEO", "AUDIO"].includes(target.tagName)) {
244
+ const src = (target as any).src || (target as any).href || "unknown";
245
+ enqueue(buildErrorPayload({ name: "ResourceError", message: `${target.tagName} failed to load: ${src}`, stack: "" }, "resource"));
246
+ return;
247
+ }
64
248
 
65
- console.log(
66
- "✅ Reportli initialized successfully. Error monitoring is active."
67
- );
68
-
69
- // JS errors
70
- window.addEventListener("error", (event: any) => {
71
- if (event.error instanceof Error) {
72
- sendError({
73
- message: event.error.message,
74
- code: event.error.name,
75
- stack: event.error.stack,
76
- file: event.filename,
77
- line: event.lineno,
78
- column: event.colno,
79
- });
80
- } else if (event.target) {
81
- // Resource load failure
82
- sendError({
83
- message: "Resource failed to load",
84
- code: "ResourceLoadError",
85
- });
86
- }
87
- }, true);
88
-
89
- // Promise rejections
90
- window.addEventListener("unhandledrejection", (event: any) => {
91
- const err = event.reason;
92
- sendError({
93
- message: err?.message ?? String(err),
94
- code: err?.name ?? "UnhandledPromiseRejection",
95
- stack: err?.stack,
96
- });
97
- });
249
+ const err = (event as ErrorEvent).error || {
250
+ name: "Error",
251
+ message: (event as ErrorEvent).message,
252
+ stack: `at ${(event as ErrorEvent).filename}:${(event as ErrorEvent).lineno}:${(event as ErrorEvent).colno}`,
253
+ };
254
+ enqueue(buildErrorPayload(err, "window"));
255
+ }, true);
98
256
 
99
- // fetch interception
100
- const originalFetch = window.fetch;
101
- window.fetch = async (...args) => {
102
- try {
103
- const response = await originalFetch(...args);
104
-
105
- if (!response.ok) {
106
- sendError({
107
- message: `Fetch HTTP ${response.status}`,
108
- code: `HTTP_${response.status}`,
109
- severity: "medium",
110
- });
111
- }
257
+ // Unhandled promise rejections
258
+ window.addEventListener("unhandledrejection", (event) => {
259
+ const err = event.reason instanceof Error
260
+ ? event.reason
261
+ : { name: "UnhandledRejection", message: String(event.reason || "Unhandled Promise Rejection"), stack: "" };
262
+ enqueue(buildErrorPayload(err, "unhandledrejection"));
263
+ });
264
+
265
+ // Intercept fetch — catches all API errors automatically
266
+ const originalFetch = window.fetch;
267
+ window.fetch = async function (...args: Parameters<typeof fetch>): Promise<Response> {
268
+ const url = typeof args[0] === "string"
269
+ ? args[0]
270
+ : args[0] instanceof URL
271
+ ? args[0].toString()
272
+ : (args[0] as Request)?.url ?? "";
273
+
274
+ // Never intercept our own requests
275
+ if (url.includes("rapid-processor")) return originalFetch(...args);
112
276
 
113
- return response;
114
- } catch (e: any) {
115
- sendError({
116
- message: e?.message ?? "Fetch failed",
117
- code: "FetchError",
118
- stack: e?.stack,
119
- });
120
- throw e;
277
+ try {
278
+ const response = await originalFetch(...args);
279
+ if (!response.ok) {
280
+ enqueue(buildErrorPayload({
281
+ name: `HTTP_${response.status}`,
282
+ message: `HTTP ${response.status}: ${(args[1] as RequestInit)?.method || "GET"} ${url}`,
283
+ stack: `${(args[1] as RequestInit)?.method || "GET"} ${url} → ${response.status} ${response.statusText}`,
284
+ status: response.status,
285
+ }, "fetch"));
121
286
  }
122
- };
287
+ return response;
288
+ } catch (err: any) {
289
+ enqueue(buildErrorPayload({
290
+ name: "FetchError",
291
+ message: `Fetch failed: ${(args[1] as RequestInit)?.method || "GET"} ${url} — ${err.message}`,
292
+ stack: err.stack,
293
+ }, "fetch"));
294
+ throw err;
295
+ }
296
+ };
123
297
 
124
- // XMLHttpRequest interception
125
- const open = XMLHttpRequest.prototype.open;
126
- const send = XMLHttpRequest.prototype.send;
127
-
128
- XMLHttpRequest.prototype.open = function (
129
- method,
130
- url,
131
- ...rest
132
- ) {
133
- (this as any).__reportli_url = url;
134
- return open.call(this, method, url, ...rest);
135
- };
298
+ // Intercept XHR
299
+ const originalOpen = XMLHttpRequest.prototype.open;
300
+ const originalSend = XMLHttpRequest.prototype.send;
301
+
302
+ XMLHttpRequest.prototype.open = function (method: string, url: string, ...rest: any[]) {
303
+ (this as any)._r_method = method;
304
+ (this as any)._r_url = url;
305
+ return originalOpen.call(this, method, url, ...rest);
306
+ } as any;
136
307
 
137
- XMLHttpRequest.prototype.send = function (...args) {
138
- this.addEventListener("load", function () {
139
- if (this.status >= 400) {
140
- sendError({
141
- message: `XHR HTTP ${this.status}`,
142
- code: `XHR_${this.status}`,
143
- });
308
+ XMLHttpRequest.prototype.send = function (...args: any[]) {
309
+ const url: string = (this as any)._r_url || "";
310
+ const method: string = (this as any)._r_method || "GET";
311
+
312
+ if (!url.includes("rapid-processor")) {
313
+ this.addEventListener("loadend", () => {
314
+ if (this.status >= 400 || this.status === 0) {
315
+ enqueue(buildErrorPayload({
316
+ name: `XHR_${this.status}`,
317
+ message: `XHR ${this.status}: ${method} ${url}`,
318
+ stack: `${method} ${url} → ${this.status} ${this.statusText}`,
319
+ status: this.status,
320
+ }, "xhr"));
144
321
  }
145
322
  });
323
+ }
146
324
 
147
- this.addEventListener("error", function () {
148
- sendError({
149
- message: "XHR connection failed",
150
- code: "XHRConnectionError",
151
- });
152
- });
325
+ return originalSend.apply(this, args);
326
+ } as any;
153
327
 
154
- return send.apply(this, args as any);
155
- };
328
+ // Disconnect when page closes
329
+ window.addEventListener("beforeunload", () => {
330
+ try {
331
+ navigator.sendBeacon(
332
+ ENDPOINT,
333
+ JSON.stringify({
334
+ type: "SDK_DISCONNECTED",
335
+ apiKey: _config?.apiKey,
336
+ timestamp: new Date().toISOString(),
337
+ environment: getEnvironment(),
338
+ url: getUrl(),
339
+ })
340
+ );
341
+ } catch { /* silent */ }
342
+ });
343
+ }
344
+
345
+ // ─── Server Listeners ─────────────────────────────────────────────────────────
346
+
347
+ function activateServerListeners() {
348
+ process.on("uncaughtException", (error: Error) => {
349
+ enqueue(buildErrorPayload(error, "uncaughtException"));
350
+ flush(); // flush immediately on crash
351
+ });
352
+
353
+ process.on("unhandledRejection", (reason: any) => {
354
+ enqueue(buildErrorPayload(
355
+ reason instanceof Error ? reason : { name: "UnhandledRejection", message: String(reason), stack: "" },
356
+ "unhandledrejection"
357
+ ));
358
+ });
359
+
360
+ const shutdown = async (signal: string) => {
361
+ await sendNow({
362
+ type: "SDK_DISCONNECTED",
363
+ apiKey: _config?.apiKey,
364
+ timestamp: new Date().toISOString(),
365
+ environment: getEnvironment(),
366
+ url: getUrl(),
367
+ signal,
368
+ });
369
+ process.exit(0);
370
+ };
371
+
372
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
373
+ process.on("SIGINT", () => shutdown("SIGINT"));
374
+ }
375
+
376
+ // ─── Public API ───────────────────────────────────────────────────────────────
377
+
378
+ export const Reportli = {
379
+ init(cfg: Config) {
380
+ if (initialized) return;
381
+ if (!cfg?.apiKey) return;
382
+
383
+ _config = cfg;
384
+ initialized = true;
385
+
386
+ // Send SDK_INITIALIZED immediately — not queued
387
+ sendImmediate({
388
+ type: "SDK_INITIALIZED",
389
+ apiKey: cfg.apiKey,
390
+ timestamp: new Date().toISOString(),
391
+ environment: getEnvironment(),
392
+ url: getUrl(),
393
+ browser: getBrowser(),
394
+ });
395
+
396
+ // Activate listeners
397
+ if (typeof window !== "undefined") {
398
+ activateBrowserListeners();
399
+ } else if (typeof process !== "undefined" && process.versions?.node) {
400
+ activateServerListeners();
401
+ }
156
402
  },
157
403
 
158
404
  capture(error: unknown) {
159
- if (error instanceof Error) {
160
- sendError({
161
- message: error.message,
162
- code: error.name,
163
- stack: error.stack,
164
- });
165
- } else {
166
- sendError({
167
- message: String(error),
168
- code: "ManualCapture",
169
- });
170
- }
405
+ if (!initialized) return;
406
+ const err = error instanceof Error
407
+ ? error
408
+ : { name: "ManualCapture", message: String(error), stack: new Error().stack };
409
+ enqueue(buildErrorPayload(err, "manual"));
171
410
  },
172
411
 
173
412
  captureMessage(message: string) {
174
- sendError({
175
- message,
176
- code: "Message",
177
- severity: "low",
178
- });
413
+ if (!initialized) return;
414
+ enqueue(buildErrorPayload({ name: "Message", message, stack: "" }, "manual"));
415
+ },
416
+
417
+ errorHandler() {
418
+ return function (err: any, _req: any, _res: any, next: any) {
419
+ enqueue(buildErrorPayload(err, "express"));
420
+ next(err);
421
+ };
179
422
  },
180
- };
423
+ };
424
+
425
+ export default Reportli;