reportli 1.0.1 → 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,32 +1,104 @@
1
- /**
2
- * Reportli SDK v1.0.1
3
- * Your AI agent that never sleeps.
4
- * Watches your SaaS, catches every error, files the GitHub issue — automatically.
5
- */
1
+ // src/index.ts — Reportli SDK v1.0.3
6
2
 
7
- const ENDPOINT = "https://fahikyfmgdyzejdfftox.supabase.co/functions/v1/rapid-processor";
3
+ const ENDPOINT =
4
+ "https://fahikyfmgdyzejdfftox.supabase.co/functions/v1/rapid-processor";
8
5
 
9
- const isBrowser = typeof window !== "undefined";
10
- const isNode = typeof process !== "undefined" && !!(process.versions?.node);
6
+ // ─── Types ────────────────────────────────────────────────────────────────────
11
7
 
12
- let _apiKey = "";
8
+ type Config = {
9
+ apiKey: string;
10
+ environment?: string;
11
+ };
13
12
 
14
- // ─── Helpers ──────────────────────────────────────────────────────────────────
13
+ type QueueItem = Record<string, unknown>;
15
14
 
16
- function getEnvironment(): string {
17
- return isBrowser ? "browser" : "server";
15
+ // ─── State ────────────────────────────────────────────────────────────────────
16
+
17
+ let initialized = false;
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
18
46
  }
19
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
+
20
88
  function getUrl(): string {
21
- if (isBrowser) return window.location.href;
89
+ if (typeof window !== "undefined") return window.location.href;
22
90
  try { return require("os").hostname(); } catch { return "server"; }
23
91
  }
24
92
 
25
93
  function getBrowser(): string {
26
- if (isBrowser) return navigator.userAgent;
94
+ if (typeof navigator !== "undefined") return navigator.userAgent;
27
95
  return `Node.js ${typeof process !== "undefined" ? process.version : "unknown"}`;
28
96
  }
29
97
 
98
+ function getEnvironment(): string {
99
+ return _config?.environment ?? (typeof window !== "undefined" ? "browser" : "server");
100
+ }
101
+
30
102
  function parseStack(stack: string | undefined): { file: string; line: number; column: number } {
31
103
  if (!stack) return { file: "unknown", line: 0, column: 0 };
32
104
  const match =
@@ -40,152 +112,113 @@ function parseStack(stack: string | undefined): { file: string; line: number; co
40
112
  }
41
113
 
42
114
  function getErrorCode(error: any): string {
43
- return error?.code || error?.status?.toString() || error?.statusCode?.toString() || error?.name || "ERR_UNKNOWN";
115
+ return (
116
+ error?.code ||
117
+ error?.status?.toString() ||
118
+ error?.statusCode?.toString() ||
119
+ error?.name ||
120
+ "ERR_UNKNOWN"
121
+ );
44
122
  }
45
123
 
46
- function classifyError(error: any, context?: string): { errorType: string; severity: "low" | "medium" | "high" | "critical" } {
124
+ function classifyError(error: any, context?: string): { category: string; severity: "low" | "medium" | "high" | "critical" } {
47
125
  const msg = String(error?.message || error || "").toLowerCase();
48
126
  const name = String(error?.name || "").toLowerCase();
49
127
 
50
- if (name.includes("quotaexceeded") || msg.includes("quota exceeded") || msg.includes("localstorage") || msg.includes("indexeddb"))
51
- return { errorType: "localStorage quota exceeded", severity: "medium" };
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
52
141
  if (msg.includes("hydration") || msg.includes("does not match server"))
53
- return { errorType: "React hydration error", severity: "high" };
54
- if (msg.includes("invalid hook call") || msg.includes("rules of hooks"))
55
- return { errorType: "Invalid hook call error", severity: "critical" };
56
- if (msg.includes("failed prop type") || msg.includes("invalid prop"))
57
- return { errorType: "Props type error", severity: "low" };
58
- if (msg.includes("render") || msg.includes("react error boundary"))
59
- return { errorType: "Component render error", severity: "critical" };
60
- if (msg.includes("route not found") || msg.includes("404 route"))
61
- return { errorType: "Route not found error", severity: "medium" };
62
- if (msg.includes("failed to fetch dynamically imported") || msg.includes("dynamic import"))
63
- return { errorType: "Dynamic import error", severity: "high" };
64
- if (msg.includes("suspense"))
65
- return { errorType: "Suspense boundary error", severity: "medium" };
66
- if (name === "typeerror" || msg.startsWith("typeerror"))
67
- return { errorType: "TypeError", severity: "high" };
68
- if (name === "referenceerror")
69
- return { errorType: "ReferenceError", severity: "critical" };
70
- if (name === "rangeerror" || msg.includes("maximum call stack"))
71
- return { errorType: "Stack overflow error", severity: "critical" };
72
- if (name === "syntaxerror")
73
- return { errorType: "SyntaxError", severity: "high" };
74
- if (msg.includes("stripe") && (msg.includes("init") || msg.includes("key")))
75
- return { errorType: "Stripe initialization error", severity: "critical" };
76
- if (msg.includes("payment") || msg.includes("card declined"))
77
- return { errorType: "Payment processing error", severity: "critical" };
78
- if (msg.includes("checkout session"))
79
- return { errorType: "Checkout session error", severity: "high" };
80
- if (msg.includes("refund failed"))
81
- return { errorType: "Refund failed error", severity: "high" };
82
- if (msg.includes("supabase") || msg.includes("postgresterror"))
83
- return { errorType: "Supabase query error", severity: "critical" };
84
- if (msg.includes("unique constraint") || msg.includes("duplicate key"))
85
- return { errorType: "Unique constraint error", severity: "high" };
86
- if (msg.includes("foreign key"))
87
- return { errorType: "Foreign key error", severity: "high" };
88
- if (msg.includes("transaction failed") || msg.includes("rollback"))
89
- return { errorType: "Transaction failed error", severity: "critical" };
90
- if (msg.includes("connection lost") || msg.includes("econnrefused"))
91
- return { errorType: "Connection lost error", severity: "critical" };
92
- if (msg.includes("query timeout"))
93
- return { errorType: "Query timeout error", severity: "high" };
94
- if (msg.includes("jwt expired") || msg.includes("token expired"))
95
- return { errorType: "JWT expired error", severity: "high" };
96
- if (msg.includes("jwt") && msg.includes("invalid"))
97
- return { errorType: "JWT verification error", severity: "high" };
98
- if (msg.includes("firebase admin"))
99
- return { errorType: "Firebase admin error", severity: "critical" };
100
- if (msg.includes("unauthorized") || msg.includes("permission denied"))
101
- return { errorType: "Unauthorized access error", severity: "high" };
102
- if (msg.includes("login failed") || msg.includes("invalid credentials"))
103
- return { errorType: "Login failed error", severity: "medium" };
104
- if (msg.includes("session ended") || msg.includes("session expired"))
105
- return { errorType: "Session ended error", severity: "medium" };
106
- if (msg.includes("oauth"))
107
- return { errorType: "OAuth callback error", severity: "high" };
108
- if (msg.includes("cron job") || msg.includes("cron failed"))
109
- return { errorType: "Cron job failed", severity: "high" };
110
- if (msg.includes("queue processing") || msg.includes("bullmq"))
111
- return { errorType: "Queue processing error", severity: "high" };
112
- if (msg.includes("webhook"))
113
- return { errorType: "Webhook delivery failed", severity: "high" };
114
- if (msg.includes("retry limit"))
115
- return { errorType: "Retry limit exceeded", severity: "high" };
116
- if (msg.includes("out of memory") || msg.includes("heap limit"))
117
- return { errorType: "Out of memory error", severity: "critical" };
118
- if (msg.includes("cannot find module"))
119
- return { errorType: "Module not found error", severity: "critical" };
120
- if (msg.includes("email") || msg.includes("smtp") || msg.includes("sendgrid") || msg.includes("nodemailer"))
121
- return { errorType: "Email sending failed", severity: "high" };
122
- if (msg.includes("onesignal") || msg.includes("push notification"))
123
- return { errorType: "OneSignal API error", severity: "high" };
124
- if (msg.includes("file upload") || msg.includes("upload failed"))
125
- return { errorType: "File upload failed", severity: "high" };
126
- if (msg.includes("file size exceeded") || msg.includes("payload too large"))
127
- return { errorType: "File size exceeded", severity: "medium" };
128
- if (msg.includes("invalid file type") || msg.includes("mime type"))
129
- return { errorType: "Invalid file type", severity: "medium" };
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
130
151
  if (msg.includes("cors") || msg.includes("cross-origin"))
131
- return { errorType: "CORS error", severity: "high" };
132
- if (msg.includes("fetch failed") || msg.includes("failed to fetch"))
133
- return { errorType: "Fetch failed error", severity: "high" };
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" };
134
155
  if (msg.includes("timeout") || msg.includes("timed out") || msg.includes("etimedout"))
135
- return { errorType: "Request timeout error", severity: "medium" };
156
+ return { category: "Timeout Error", severity: "medium" };
136
157
  if (msg.includes("websocket"))
137
- return { errorType: "WebSocket connection error", severity: "high" };
138
- if (context === "unhandledrejection")
139
- return { errorType: "Unhandled promise rejection", severity: "medium" };
140
- if (context === "uncaughtexception")
141
- return { errorType: "Uncaught exception", severity: "high" };
142
- if (context === "express")
143
- return { errorType: "Route handler error", severity: "high" };
144
- return { errorType: "Uncaught exception", severity: "medium" };
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" };
145
208
  }
146
209
 
147
- function normalizeError(err: any): { name: string; message: string; stack?: string } {
148
- if (err instanceof Error) return { name: err.name, message: err.message, stack: err.stack };
149
- if (typeof err === "string") return { name: "Error", message: err, stack: new Error(err).stack };
150
- if (err && typeof err === "object") return {
151
- name: err.name || err.code || "Error",
152
- message: err.message || err.reason || JSON.stringify(err),
153
- stack: err.stack || new Error(err.message).stack,
154
- };
155
- return { name: "Error", message: "Unknown error", stack: new Error().stack };
156
- }
157
-
158
- // ─── Send ─────────────────────────────────────────────────────────────────────
159
-
160
- async function send(payload: object): Promise<void> {
161
- try {
162
- if ((payload as any).message?.includes("rapid-processor")) return;
163
- await fetch(ENDPOINT, {
164
- method: "POST",
165
- headers: {
166
- "Content-Type": "application/json",
167
- "x-api-key": _apiKey,
168
- },
169
- body: JSON.stringify(payload),
170
- });
171
- } catch {
172
- // Fail silently — never break user app
173
- }
174
- }
175
-
176
- // ─── Build error payload ──────────────────────────────────────────────────────
177
-
178
- function buildErrorPayload(error: any, context?: string): object {
179
- const normalized = normalizeError(error);
180
- const { file, line, column } = parseStack(normalized.stack);
181
- const { errorType, severity } = classifyError(normalized, context);
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);
182
215
 
183
216
  return {
184
217
  type: "ERROR",
185
- apiKey: _apiKey,
186
- message: normalized.message,
218
+ apiKey: _config?.apiKey,
219
+ message,
187
220
  code: getErrorCode(error),
188
- stack: normalized.stack || "",
221
+ stack,
189
222
  file,
190
223
  line,
191
224
  column,
@@ -193,70 +226,71 @@ function buildErrorPayload(error: any, context?: string): object {
193
226
  timestamp: new Date().toISOString(),
194
227
  environment: getEnvironment(),
195
228
  browser: getBrowser(),
196
- error_category: errorType,
229
+ error_category: category,
197
230
  severity,
198
231
  status: "open",
232
+ context: context || "auto",
199
233
  };
200
234
  }
201
235
 
202
- // ─── Browser listeners ────────────────────────────────────────────────────────
236
+ // ─── Browser Listeners ────────────────────────────────────────────────────────
203
237
 
204
- function activateBrowserListeners(): void {
205
- // JS runtime errors + resource load errors
238
+ function activateBrowserListeners() {
239
+ // JS errors + resource errors
206
240
  window.addEventListener("error", (event) => {
207
- if ((event as any).filename?.includes("rapid-processor")) return;
208
-
241
+ // Resource load error (img, script, link, video)
209
242
  const target = event.target as HTMLElement;
210
- if (target && target.tagName) {
211
- const tag = target.tagName;
212
- const src = (target as any).src || (target as any).href || "";
213
- if (["IMG", "SCRIPT", "LINK", "VIDEO", "AUDIO", "SOURCE"].includes(tag)) {
214
- send(buildErrorPayload({
215
- name: "ResourceError",
216
- message: `${tag} failed to load: ${src}`,
217
- stack: `at ${window.location.href}`,
218
- }));
219
- return;
220
- }
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;
221
247
  }
222
248
 
223
249
  const err = (event as ErrorEvent).error || {
224
250
  name: "Error",
225
251
  message: (event as ErrorEvent).message,
226
- stack: `${(event as ErrorEvent).message} at ${(event as ErrorEvent).filename}:${(event as ErrorEvent).lineno}:${(event as ErrorEvent).colno}`,
252
+ stack: `at ${(event as ErrorEvent).filename}:${(event as ErrorEvent).lineno}:${(event as ErrorEvent).colno}`,
227
253
  };
228
- send(buildErrorPayload(err));
254
+ enqueue(buildErrorPayload(err, "window"));
229
255
  }, true);
230
256
 
231
257
  // Unhandled promise rejections
232
258
  window.addEventListener("unhandledrejection", (event) => {
233
- send(buildErrorPayload(event.reason || { message: "Unhandled Promise Rejection" }, "unhandledrejection"));
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"));
234
263
  });
235
264
 
236
- // Intercept fetch
265
+ // Intercept fetch — catches all API errors automatically
237
266
  const originalFetch = window.fetch;
238
- window.fetch = async function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
239
- const url = typeof input === "string" ? input : (input instanceof URL ? input.toString() : (input as Request).url);
240
- if (url.includes("rapid-processor") || url.includes("supabase.co/functions")) {
241
- return originalFetch.apply(this, arguments as any);
242
- }
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);
276
+
243
277
  try {
244
- const response = await originalFetch.apply(this, arguments as any);
278
+ const response = await originalFetch(...args);
245
279
  if (!response.ok) {
246
- send(buildErrorPayload({
247
- name: `API ${response.status} Error`,
248
- message: `HTTP ${response.status}: ${init?.method || "GET"} ${url}`,
249
- stack: `${init?.method || "GET"} ${url} → ${response.status} ${response.statusText}`,
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}`,
250
284
  status: response.status,
251
- }));
285
+ }, "fetch"));
252
286
  }
253
287
  return response;
254
288
  } catch (err: any) {
255
- send(buildErrorPayload({
289
+ enqueue(buildErrorPayload({
256
290
  name: "FetchError",
257
- message: `Fetch failed: ${init?.method || "GET"} ${url} — ${err.message}`,
291
+ message: `Fetch failed: ${(args[1] as RequestInit)?.method || "GET"} ${url} — ${err.message}`,
258
292
  stack: err.stack,
259
- }));
293
+ }, "fetch"));
260
294
  throw err;
261
295
  }
262
296
  };
@@ -265,123 +299,127 @@ function activateBrowserListeners(): void {
265
299
  const originalOpen = XMLHttpRequest.prototype.open;
266
300
  const originalSend = XMLHttpRequest.prototype.send;
267
301
 
268
- XMLHttpRequest.prototype.open = function (method: string, url: string) {
302
+ XMLHttpRequest.prototype.open = function (method: string, url: string, ...rest: any[]) {
269
303
  (this as any)._r_method = method;
270
304
  (this as any)._r_url = url;
271
- return originalOpen.apply(this, arguments as any);
305
+ return originalOpen.call(this, method, url, ...rest);
272
306
  } as any;
273
307
 
274
- XMLHttpRequest.prototype.send = function () {
275
- this.addEventListener("loadend", () => {
276
- const url: string = (this as any)._r_url || "";
277
- const method: string = (this as any)._r_method || "GET";
278
- if (url.includes("rapid-processor")) return;
279
- if (this.status >= 400 || this.status === 0) {
280
- send(buildErrorPayload({
281
- name: `XHR ${this.status} Error`,
282
- message: `XHR ${this.status}: ${method} ${url}`,
283
- stack: `${method} ${url} ${this.status} ${this.statusText}`,
284
- status: this.status,
285
- }));
286
- }
287
- });
288
- return originalSend.apply(this, arguments as any);
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"));
321
+ }
322
+ });
323
+ }
324
+
325
+ return originalSend.apply(this, args);
289
326
  } as any;
290
327
 
291
328
  // Disconnect when page closes
292
329
  window.addEventListener("beforeunload", () => {
293
330
  try {
294
- navigator.sendBeacon(ENDPOINT, JSON.stringify({
295
- type: "SDK_DISCONNECTED",
296
- apiKey: _apiKey,
297
- timestamp: new Date().toISOString(),
298
- environment: "browser",
299
- url: getUrl(),
300
- }));
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
+ );
301
341
  } catch { /* silent */ }
302
342
  });
303
343
  }
304
344
 
305
- // ─── Server listeners ─────────────────────────────────────────────────────────
345
+ // ─── Server Listeners ─────────────────────────────────────────────────────────
306
346
 
307
- function activateServerListeners(): void {
347
+ function activateServerListeners() {
308
348
  process.on("uncaughtException", (error: Error) => {
309
- send(buildErrorPayload(error, "uncaughtexception"));
349
+ enqueue(buildErrorPayload(error, "uncaughtException"));
350
+ flush(); // flush immediately on crash
310
351
  });
311
352
 
312
353
  process.on("unhandledRejection", (reason: any) => {
313
- send(buildErrorPayload(
354
+ enqueue(buildErrorPayload(
314
355
  reason instanceof Error ? reason : { name: "UnhandledRejection", message: String(reason), stack: "" },
315
356
  "unhandledrejection"
316
357
  ));
317
358
  });
318
359
 
319
- const shutdown = async () => {
320
- await send({
360
+ const shutdown = async (signal: string) => {
361
+ await sendNow({
321
362
  type: "SDK_DISCONNECTED",
322
- apiKey: _apiKey,
363
+ apiKey: _config?.apiKey,
323
364
  timestamp: new Date().toISOString(),
324
- environment: "server",
365
+ environment: getEnvironment(),
325
366
  url: getUrl(),
367
+ signal,
326
368
  });
327
369
  process.exit(0);
328
370
  };
329
371
 
330
- process.on("SIGTERM", shutdown);
331
- process.on("SIGINT", shutdown);
372
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
373
+ process.on("SIGINT", () => shutdown("SIGINT"));
332
374
  }
333
375
 
334
376
  // ─── Public API ───────────────────────────────────────────────────────────────
335
377
 
336
- const Reportli = {
337
- init({ apiKey }: { apiKey: string }): void {
338
- if (!apiKey || _apiKey) return;
339
- _apiKey = apiKey;
340
-
341
- // Send SDK_INITIALIZED once only
342
- if (isBrowser && typeof localStorage !== "undefined") {
343
- const key = `reportli_init_${apiKey}`;
344
- if (!localStorage.getItem(key)) {
345
- localStorage.setItem(key, "true");
346
- send({
347
- type: "SDK_INITIALIZED",
348
- apiKey: _apiKey,
349
- timestamp: new Date().toISOString(),
350
- environment: getEnvironment(),
351
- url: getUrl(),
352
- browser: getBrowser(),
353
- });
354
- }
355
- } else {
356
- send({
357
- type: "SDK_INITIALIZED",
358
- apiKey: _apiKey,
359
- timestamp: new Date().toISOString(),
360
- environment: getEnvironment(),
361
- url: getUrl(),
362
- browser: getBrowser(),
363
- });
364
- }
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
+ });
365
395
 
366
- if (isBrowser) {
396
+ // Activate listeners
397
+ if (typeof window !== "undefined") {
367
398
  activateBrowserListeners();
368
- } else if (isNode) {
399
+ } else if (typeof process !== "undefined" && process.versions?.node) {
369
400
  activateServerListeners();
370
401
  }
371
402
  },
372
403
 
373
- capture(error: any): void {
374
- if (!_apiKey) return;
375
- send(buildErrorPayload(error, "manual"));
404
+ capture(error: unknown) {
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"));
410
+ },
411
+
412
+ captureMessage(message: string) {
413
+ if (!initialized) return;
414
+ enqueue(buildErrorPayload({ name: "Message", message, stack: "" }, "manual"));
376
415
  },
377
416
 
378
417
  errorHandler() {
379
418
  return function (err: any, _req: any, _res: any, next: any) {
380
- send(buildErrorPayload(err, "express"));
419
+ enqueue(buildErrorPayload(err, "express"));
381
420
  next(err);
382
421
  };
383
422
  },
384
423
  };
385
424
 
386
425
  export default Reportli;
387
- export { Reportli };