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/dist/index.d.ts +8 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +210 -227
- package/dist/index.js.map +2 -2
- package/dist/index.mjs +210 -227
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
- package/src/index.ts +288 -250
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 =
|
|
3
|
+
const ENDPOINT =
|
|
4
|
+
"https://fahikyfmgdyzejdfftox.supabase.co/functions/v1/rapid-processor";
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
const isNode = typeof process !== "undefined" && !!(process.versions?.node);
|
|
6
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
11
7
|
|
|
12
|
-
|
|
8
|
+
type Config = {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
environment?: string;
|
|
11
|
+
};
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
type QueueItem = Record<string, unknown>;
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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): {
|
|
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
|
-
|
|
51
|
-
|
|
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 {
|
|
54
|
-
if (msg.includes("invalid hook
|
|
55
|
-
return {
|
|
56
|
-
if (msg.includes("
|
|
57
|
-
return {
|
|
58
|
-
if (msg.includes("
|
|
59
|
-
return {
|
|
60
|
-
|
|
61
|
-
|
|
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 {
|
|
132
|
-
if (msg.includes("fetch failed") || msg.includes("failed to fetch"))
|
|
133
|
-
return {
|
|
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 {
|
|
156
|
+
return { category: "Timeout Error", severity: "medium" };
|
|
136
157
|
if (msg.includes("websocket"))
|
|
137
|
-
return {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
return {
|
|
142
|
-
if (
|
|
143
|
-
return {
|
|
144
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
186
|
-
message
|
|
218
|
+
apiKey: _config?.apiKey,
|
|
219
|
+
message,
|
|
187
220
|
code: getErrorCode(error),
|
|
188
|
-
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:
|
|
229
|
+
error_category: category,
|
|
197
230
|
severity,
|
|
198
231
|
status: "open",
|
|
232
|
+
context: context || "auto",
|
|
199
233
|
};
|
|
200
234
|
}
|
|
201
235
|
|
|
202
|
-
// ─── Browser
|
|
236
|
+
// ─── Browser Listeners ────────────────────────────────────────────────────────
|
|
203
237
|
|
|
204
|
-
function activateBrowserListeners()
|
|
205
|
-
// JS
|
|
238
|
+
function activateBrowserListeners() {
|
|
239
|
+
// JS errors + resource errors
|
|
206
240
|
window.addEventListener("error", (event) => {
|
|
207
|
-
|
|
208
|
-
|
|
241
|
+
// Resource load error (img, script, link, video)
|
|
209
242
|
const target = event.target as HTMLElement;
|
|
210
|
-
if (target && target.tagName) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
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:
|
|
252
|
+
stack: `at ${(event as ErrorEvent).filename}:${(event as ErrorEvent).lineno}:${(event as ErrorEvent).colno}`,
|
|
227
253
|
};
|
|
228
|
-
|
|
254
|
+
enqueue(buildErrorPayload(err, "window"));
|
|
229
255
|
}, true);
|
|
230
256
|
|
|
231
257
|
// Unhandled promise rejections
|
|
232
258
|
window.addEventListener("unhandledrejection", (event) => {
|
|
233
|
-
|
|
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 (
|
|
239
|
-
const url = typeof
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
278
|
+
const response = await originalFetch(...args);
|
|
245
279
|
if (!response.ok) {
|
|
246
|
-
|
|
247
|
-
name: `
|
|
248
|
-
message: `HTTP ${response.status}: ${
|
|
249
|
-
stack: `${
|
|
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
|
-
|
|
289
|
+
enqueue(buildErrorPayload({
|
|
256
290
|
name: "FetchError",
|
|
257
|
-
message: `Fetch failed: ${
|
|
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.
|
|
305
|
+
return originalOpen.call(this, method, url, ...rest);
|
|
272
306
|
} as any;
|
|
273
307
|
|
|
274
|
-
XMLHttpRequest.prototype.send = function () {
|
|
275
|
-
this
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
345
|
+
// ─── Server Listeners ─────────────────────────────────────────────────────────
|
|
306
346
|
|
|
307
|
-
function activateServerListeners()
|
|
347
|
+
function activateServerListeners() {
|
|
308
348
|
process.on("uncaughtException", (error: Error) => {
|
|
309
|
-
|
|
349
|
+
enqueue(buildErrorPayload(error, "uncaughtException"));
|
|
350
|
+
flush(); // flush immediately on crash
|
|
310
351
|
});
|
|
311
352
|
|
|
312
353
|
process.on("unhandledRejection", (reason: any) => {
|
|
313
|
-
|
|
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
|
|
360
|
+
const shutdown = async (signal: string) => {
|
|
361
|
+
await sendNow({
|
|
321
362
|
type: "SDK_DISCONNECTED",
|
|
322
|
-
apiKey:
|
|
363
|
+
apiKey: _config?.apiKey,
|
|
323
364
|
timestamp: new Date().toISOString(),
|
|
324
|
-
environment:
|
|
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(
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
396
|
+
// Activate listeners
|
|
397
|
+
if (typeof window !== "undefined") {
|
|
367
398
|
activateBrowserListeners();
|
|
368
|
-
} else if (
|
|
399
|
+
} else if (typeof process !== "undefined" && process.versions?.node) {
|
|
369
400
|
activateServerListeners();
|
|
370
401
|
}
|
|
371
402
|
},
|
|
372
403
|
|
|
373
|
-
capture(error:
|
|
374
|
-
if (!
|
|
375
|
-
|
|
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
|
-
|
|
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 };
|