reportli 1.0.0 → 1.0.1
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 +11 -76
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +322 -636
- package/dist/index.js.map +2 -2
- package/dist/index.mjs +330 -636
- package/dist/index.mjs.map +2 -2
- package/package.json +1 -1
- package/src/index.ts +342 -737
package/src/index.ts
CHANGED
|
@@ -1,782 +1,387 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Reportli SDK
|
|
3
|
-
*
|
|
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.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
apiKey: string;
|
|
8
|
-
projectId?: string;
|
|
9
|
-
projectName?: string;
|
|
10
|
-
environment?: string;
|
|
11
|
-
framework?: string;
|
|
12
|
-
disableHmrLogging?: boolean;
|
|
13
|
-
captureUnhandledRejections?: boolean;
|
|
14
|
-
autoCreateGitHubIssues?: boolean;
|
|
15
|
-
userEmail?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ExceptionPayload {
|
|
19
|
-
apiKey: string;
|
|
20
|
-
projectId?: string;
|
|
21
|
-
projectName?: string;
|
|
22
|
-
errorType: string;
|
|
23
|
-
errorMessage: string;
|
|
24
|
-
errorStack: string;
|
|
25
|
-
framework?: string;
|
|
26
|
-
user_email?: string;
|
|
27
|
-
severity?: "low" | "medium" | "high" | "critical";
|
|
28
|
-
status?: string;
|
|
29
|
-
}
|
|
7
|
+
const ENDPOINT = "https://fahikyfmgdyzejdfftox.supabase.co/functions/v1/rapid-processor";
|
|
30
8
|
|
|
31
|
-
const
|
|
9
|
+
const isBrowser = typeof window !== "undefined";
|
|
10
|
+
const isNode = typeof process !== "undefined" && !!(process.versions?.node);
|
|
32
11
|
|
|
33
|
-
|
|
34
|
-
private config: ReportliConfig | null = null;
|
|
35
|
-
private isInitialized = false;
|
|
12
|
+
let _apiKey = "";
|
|
36
13
|
|
|
37
|
-
|
|
38
|
-
* Initializes the Reportli tracker with your secure project credentials.
|
|
39
|
-
*/
|
|
40
|
-
public init(config: ReportliConfig): void {
|
|
41
|
-
if (!config || !config.apiKey) {
|
|
42
|
-
console.error("[Reportli SDK] Initialization failed: 'apiKey' is required.");
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (this.isInitialized) {
|
|
47
|
-
return; // Already active in thread
|
|
48
|
-
}
|
|
14
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
49
15
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
autoCreateGitHubIssues: true,
|
|
54
|
-
...config,
|
|
55
|
-
};
|
|
56
|
-
this.isInitialized = true;
|
|
57
|
-
|
|
58
|
-
// Detect execution runtime
|
|
59
|
-
const isBrowser = typeof window !== "undefined";
|
|
60
|
-
const isNode = typeof process !== "undefined" && process.versions && process.versions.node;
|
|
61
|
-
|
|
62
|
-
// 1. Send the installation/setup success message on initial setup block
|
|
63
|
-
this.sendRegistrationWebhook(isBrowser);
|
|
16
|
+
function getEnvironment(): string {
|
|
17
|
+
return isBrowser ? "browser" : "server";
|
|
18
|
+
}
|
|
64
19
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.setupNodeListeners();
|
|
70
|
-
}
|
|
20
|
+
function getUrl(): string {
|
|
21
|
+
if (isBrowser) return window.location.href;
|
|
22
|
+
try { return require("os").hostname(); } catch { return "server"; }
|
|
23
|
+
}
|
|
71
24
|
|
|
72
|
-
|
|
73
|
-
|
|
25
|
+
function getBrowser(): string {
|
|
26
|
+
if (isBrowser) return navigator.userAgent;
|
|
27
|
+
return `Node.js ${typeof process !== "undefined" ? process.version : "unknown"}`;
|
|
28
|
+
}
|
|
74
29
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
30
|
+
function parseStack(stack: string | undefined): { file: string; line: number; column: number } {
|
|
31
|
+
if (!stack) return { file: "unknown", line: 0, column: 0 };
|
|
32
|
+
const match =
|
|
33
|
+
stack.match(/at .+ \((.+):(\d+):(\d+)\)/) ||
|
|
34
|
+
stack.match(/at (.+):(\d+):(\d+)/);
|
|
35
|
+
return {
|
|
36
|
+
file: match?.[1]?.split("/")?.pop() || "unknown",
|
|
37
|
+
line: parseInt(match?.[2] || "0"),
|
|
38
|
+
column: parseInt(match?.[3] || "0"),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
83
41
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const payload: ExceptionPayload = {
|
|
88
|
-
apiKey: this.config.apiKey,
|
|
89
|
-
projectId: this.config.projectId || "proj-sandbox",
|
|
90
|
-
projectName: this.config.projectName || "SaaS App",
|
|
91
|
-
errorType: errorType,
|
|
92
|
-
errorMessage: errorObject.message || "Manual trace reported",
|
|
93
|
-
errorStack: errorObject.stack || "",
|
|
94
|
-
framework: this.config.framework || (typeof window !== "undefined" ? "React/Next.js Client" : "Node.js Server"),
|
|
95
|
-
user_email: this.config.userEmail || "anonymous",
|
|
96
|
-
severity: severityInput || severity,
|
|
97
|
-
status: "open"
|
|
98
|
-
};
|
|
42
|
+
function getErrorCode(error: any): string {
|
|
43
|
+
return error?.code || error?.status?.toString() || error?.statusCode?.toString() || error?.name || "ERR_UNKNOWN";
|
|
44
|
+
}
|
|
99
45
|
|
|
100
|
-
|
|
101
|
-
|
|
46
|
+
function classifyError(error: any, context?: string): { errorType: string; severity: "low" | "medium" | "high" | "critical" } {
|
|
47
|
+
const msg = String(error?.message || error || "").toLowerCase();
|
|
48
|
+
const name = String(error?.name || "").toLowerCase();
|
|
49
|
+
|
|
50
|
+
if (name.includes("quotaexceeded") || msg.includes("quota exceeded") || msg.includes("localstorage") || msg.includes("indexeddb"))
|
|
51
|
+
return { errorType: "localStorage quota exceeded", severity: "medium" };
|
|
52
|
+
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" };
|
|
130
|
+
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" };
|
|
134
|
+
if (msg.includes("timeout") || msg.includes("timed out") || msg.includes("etimedout"))
|
|
135
|
+
return { errorType: "Request timeout error", severity: "medium" };
|
|
136
|
+
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" };
|
|
145
|
+
}
|
|
102
146
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const { errorType, severity } = this.classifyError(errorObject, "express");
|
|
111
|
-
|
|
112
|
-
const payload: ExceptionPayload = {
|
|
113
|
-
apiKey: this.config.apiKey,
|
|
114
|
-
projectId: this.config.projectId || "express-server",
|
|
115
|
-
projectName: this.config.projectName || "Express App",
|
|
116
|
-
errorType: errorType || "Route handler error",
|
|
117
|
-
errorMessage: `${req.method} ${req.url} - ${errorObject.message}`,
|
|
118
|
-
errorStack: errorObject.stack || "",
|
|
119
|
-
framework: this.config.framework || "Express",
|
|
120
|
-
user_email: this.config.userEmail || req.user?.email || "anonymous",
|
|
121
|
-
severity: severity || "high",
|
|
122
|
-
status: "open"
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
this.sendCrashReport(payload);
|
|
126
|
-
} catch (e) {
|
|
127
|
-
console.error("[Reportli SDK] Failed to process express exception:", e);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
next(err);
|
|
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,
|
|
131
154
|
};
|
|
155
|
+
return { name: "Error", message: "Unknown error", stack: new Error().stack };
|
|
156
|
+
}
|
|
132
157
|
|
|
133
|
-
|
|
134
|
-
* Registers global window listeners for DOM-based exceptions.
|
|
135
|
-
*/
|
|
136
|
-
private setupBrowserListeners(): void {
|
|
137
|
-
if (typeof window === "undefined") return;
|
|
138
|
-
|
|
139
|
-
// A. Uncaught JavaScript Runtime Crashes
|
|
140
|
-
window.addEventListener("error", (event) => {
|
|
141
|
-
// Prevent loop tracing reportli requests
|
|
142
|
-
if (event.filename && event.filename.includes("rapid-processor")) return;
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const error = event.error || {
|
|
146
|
-
message: event.message,
|
|
147
|
-
filename: event.filename,
|
|
148
|
-
lineno: event.lineno,
|
|
149
|
-
colno: event.colno,
|
|
150
|
-
stack: `${event.message} at ${event.filename}:${event.lineno}:${event.colno}`,
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const errorObject = this.normalizeError(error);
|
|
154
|
-
const { errorType, severity } = this.classifyError(errorObject);
|
|
155
|
-
|
|
156
|
-
const payload: ExceptionPayload = {
|
|
157
|
-
apiKey: this.config?.apiKey || "",
|
|
158
|
-
projectId: this.config?.projectId || "proj-sandbox",
|
|
159
|
-
projectName: this.config?.projectName || "SaaS App",
|
|
160
|
-
errorType: errorType,
|
|
161
|
-
errorMessage: errorObject.message,
|
|
162
|
-
errorStack: errorObject.stack || "",
|
|
163
|
-
framework: this.config?.framework || "React/Next.js Client",
|
|
164
|
-
user_email: this.config?.userEmail || "anonymous",
|
|
165
|
-
severity: severity,
|
|
166
|
-
status: "open",
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
this.sendCrashReport(payload);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
console.error("[Reportli SDK] Inner listener exception:", err);
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// B. Unhandled Promise Rejections (e.g. async/await loops)
|
|
176
|
-
window.addEventListener("unhandledrejection", (event) => {
|
|
177
|
-
try {
|
|
178
|
-
const reason = event.reason;
|
|
179
|
-
const errorObject = this.normalizeError(reason);
|
|
180
|
-
const { errorType, severity } = this.classifyError(errorObject, "unhandledrejection");
|
|
181
|
-
|
|
182
|
-
const payload: ExceptionPayload = {
|
|
183
|
-
apiKey: this.config?.apiKey || "",
|
|
184
|
-
projectId: this.config?.projectId || "proj-sandbox",
|
|
185
|
-
projectName: this.config?.projectName || "SaaS App",
|
|
186
|
-
errorType: errorType,
|
|
187
|
-
errorMessage: `Unhandled Promise: ${errorObject.message}`,
|
|
188
|
-
errorStack: errorObject.stack || "",
|
|
189
|
-
framework: this.config?.framework || "React/Next.js Client",
|
|
190
|
-
user_email: this.config?.userEmail || "anonymous",
|
|
191
|
-
severity: severity,
|
|
192
|
-
status: "open",
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
this.sendCrashReport(payload);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
console.error("[Reportli SDK] Promise rejection listener exception:", err);
|
|
198
|
-
}
|
|
199
|
-
});
|
|
158
|
+
// ─── Send ─────────────────────────────────────────────────────────────────────
|
|
200
159
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const tagName = (target as HTMLElement).tagName;
|
|
210
|
-
if (!tagName) return;
|
|
211
|
-
|
|
212
|
-
let resourceType = "";
|
|
213
|
-
let sourceUrl = "";
|
|
214
|
-
|
|
215
|
-
if (tagName === "IMG") {
|
|
216
|
-
resourceType = "Image failed to load";
|
|
217
|
-
sourceUrl = (target as HTMLImageElement).src;
|
|
218
|
-
} else if (tagName === "SCRIPT") {
|
|
219
|
-
resourceType = "Script failed to load";
|
|
220
|
-
sourceUrl = (target as HTMLScriptElement).src;
|
|
221
|
-
} else if (tagName === "LINK") {
|
|
222
|
-
resourceType = "CSS failed to load";
|
|
223
|
-
sourceUrl = (target as HTMLLinkElement).href;
|
|
224
|
-
} else if (tagName === "VIDEO" || tagName === "AUDIO") {
|
|
225
|
-
resourceType = "Video failed to load";
|
|
226
|
-
sourceUrl = (target as HTMLVideoElement).src;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (resourceType) {
|
|
230
|
-
const payload: ExceptionPayload = {
|
|
231
|
-
apiKey: this.config?.apiKey || "",
|
|
232
|
-
projectId: this.config?.projectId || "proj-sandbox",
|
|
233
|
-
projectName: this.config?.projectName || "SaaS App",
|
|
234
|
-
errorType: resourceType,
|
|
235
|
-
errorMessage: `Failed to load static resource asset: ${sourceUrl}`,
|
|
236
|
-
errorStack: `HTML Tag: <${tagName.toLowerCase()}> failed to download at location ${window.location.href}`,
|
|
237
|
-
framework: this.config?.framework || "React/Next.js Client",
|
|
238
|
-
user_email: this.config?.userEmail || "anonymous",
|
|
239
|
-
severity: "low",
|
|
240
|
-
status: "open",
|
|
241
|
-
};
|
|
242
|
-
this.sendCrashReport(payload);
|
|
243
|
-
}
|
|
244
|
-
} catch (err) {
|
|
245
|
-
console.error("[Reportli SDK] Resource load listener failure:", err);
|
|
246
|
-
}
|
|
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,
|
|
247
168
|
},
|
|
248
|
-
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
// D. Network Fetch Interceptors (Monkey patch global fetch to trace bad status codes)
|
|
252
|
-
this.interceptFetch();
|
|
253
|
-
this.interceptXhr();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Registers node process listeners for server instances.
|
|
258
|
-
*/
|
|
259
|
-
private setupNodeListeners(): void {
|
|
260
|
-
if (typeof process === "undefined") return;
|
|
261
|
-
|
|
262
|
-
process.on("uncaughtException", (error) => {
|
|
263
|
-
try {
|
|
264
|
-
const errorObject = this.normalizeError(error);
|
|
265
|
-
const { errorType, severity } = this.classifyError(errorObject, "uncaughtexception");
|
|
266
|
-
|
|
267
|
-
const payload: ExceptionPayload = {
|
|
268
|
-
apiKey: this.config?.apiKey || "",
|
|
269
|
-
projectId: this.config?.projectId || "node-server",
|
|
270
|
-
projectName: this.config?.projectName || "Node Server",
|
|
271
|
-
errorType: errorType,
|
|
272
|
-
errorMessage: `Uncaught Exception: ${errorObject.message}`,
|
|
273
|
-
errorStack: errorObject.stack || "",
|
|
274
|
-
framework: this.config?.framework || "NodeJS backend",
|
|
275
|
-
user_email: this.config?.userEmail || "anonymous",
|
|
276
|
-
severity: severity,
|
|
277
|
-
status: "open",
|
|
278
|
-
};
|
|
279
|
-
|
|
280
|
-
// Send-sync block
|
|
281
|
-
this.sendCrashReport(payload);
|
|
282
|
-
} catch (err) {
|
|
283
|
-
console.error("[Reportli SDK] Node fatal uncaughtexception logging failed:", err);
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
process.on("unhandledRejection", (reason) => {
|
|
288
|
-
try {
|
|
289
|
-
const errorObject = this.normalizeError(reason);
|
|
290
|
-
const { errorType, severity } = this.classifyError(errorObject, "unhandledrejection");
|
|
291
|
-
|
|
292
|
-
const payload: ExceptionPayload = {
|
|
293
|
-
apiKey: this.config?.apiKey || "",
|
|
294
|
-
projectId: this.config?.projectId || "node-server",
|
|
295
|
-
projectName: this.config?.projectName || "Node Server",
|
|
296
|
-
errorType: errorType,
|
|
297
|
-
errorMessage: `Unhandled Rejection: ${errorObject.message}`,
|
|
298
|
-
errorStack: errorObject.stack || "",
|
|
299
|
-
framework: this.config?.framework || "NodeJS backend",
|
|
300
|
-
user_email: this.config?.userEmail || "anonymous",
|
|
301
|
-
severity: severity,
|
|
302
|
-
status: "open",
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
this.sendCrashReport(payload);
|
|
306
|
-
} catch (err) {
|
|
307
|
-
console.error("[Reportli SDK] Node fatal unhandledrejection logging failed:", err);
|
|
308
|
-
}
|
|
169
|
+
body: JSON.stringify(payload),
|
|
309
170
|
});
|
|
171
|
+
} catch {
|
|
172
|
+
// Fail silently — never break user app
|
|
310
173
|
}
|
|
174
|
+
}
|
|
311
175
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*/
|
|
338
|
-
private sendRegistrationWebhook(isBrowser: boolean): void {
|
|
339
|
-
if (!this.config) return;
|
|
340
|
-
|
|
341
|
-
const key = `reportli_initialized_success_${this.config.apiKey}`;
|
|
342
|
-
if (isBrowser && typeof localStorage !== "undefined") {
|
|
343
|
-
if (localStorage.getItem(key)) {
|
|
344
|
-
return; // Already verified registration autrefois
|
|
345
|
-
}
|
|
346
|
-
localStorage.setItem(key, "true");
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const deviceDetails = isBrowser
|
|
350
|
-
? typeof navigator !== "undefined" ? navigator.userAgent : "Browser sandbox"
|
|
351
|
-
: typeof process !== "undefined" ? `NodeJS environment: ${process.version}` : "Cloud instance";
|
|
352
|
-
|
|
353
|
-
const welcomePayload: ExceptionPayload = {
|
|
354
|
-
apiKey: this.config.apiKey,
|
|
355
|
-
projectId: this.config.projectId || "proj-sandbox",
|
|
356
|
-
projectName: this.config.projectName || "SaaS App Component",
|
|
357
|
-
errorType: "SDK_INITIALIZED",
|
|
358
|
-
errorMessage: "Reportli SDK successfully initialized! Error listener is active.",
|
|
359
|
-
errorStack: `SDK Active Diagnostics: Success registration from execution agent. Device context: ${deviceDetails}`,
|
|
360
|
-
framework: this.config.framework || (isBrowser ? "React/Next.js Client" : "Node.js Server"),
|
|
361
|
-
user_email: this.config.userEmail || "anonymous",
|
|
362
|
-
severity: "low",
|
|
363
|
-
status: "info"
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
this.sendCrashReport(welcomePayload);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Capture fetch network status deviations.
|
|
371
|
-
*/
|
|
372
|
-
private interceptFetch(): void {
|
|
373
|
-
if (typeof window === "undefined" || typeof window.fetch !== "function") return;
|
|
374
|
-
|
|
375
|
-
const originalFetch = window.fetch;
|
|
376
|
-
const self = this;
|
|
377
|
-
|
|
378
|
-
window.fetch = async function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
379
|
-
const url = typeof input === "string" ? input : (input instanceof URL ? input.toString() : input.url);
|
|
380
|
-
|
|
381
|
-
// Skip tracking of reportli's own API calls to avoid circular logging loop
|
|
382
|
-
if (url.includes("rapid-processor") || url.includes("supabase.co/functions")) {
|
|
383
|
-
return originalFetch.apply(this, arguments as any);
|
|
384
|
-
}
|
|
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);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
type: "ERROR",
|
|
185
|
+
apiKey: _apiKey,
|
|
186
|
+
message: normalized.message,
|
|
187
|
+
code: getErrorCode(error),
|
|
188
|
+
stack: normalized.stack || "",
|
|
189
|
+
file,
|
|
190
|
+
line,
|
|
191
|
+
column,
|
|
192
|
+
url: getUrl(),
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
environment: getEnvironment(),
|
|
195
|
+
browser: getBrowser(),
|
|
196
|
+
error_category: errorType,
|
|
197
|
+
severity,
|
|
198
|
+
status: "open",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
385
201
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
202
|
+
// ─── Browser listeners ────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
function activateBrowserListeners(): void {
|
|
205
|
+
// JS runtime errors + resource load errors
|
|
206
|
+
window.addEventListener("error", (event) => {
|
|
207
|
+
if ((event as any).filename?.includes("rapid-processor")) return;
|
|
208
|
+
|
|
209
|
+
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;
|
|
396
220
|
}
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/**
|
|
401
|
-
* Trace XHR status failures.
|
|
402
|
-
*/
|
|
403
|
-
private interceptXhr(): void {
|
|
404
|
-
if (typeof window === "undefined" || typeof XMLHttpRequest === "undefined") return;
|
|
405
|
-
|
|
406
|
-
const originalOpen = XMLHttpRequest.prototype.open;
|
|
407
|
-
const originalSend = XMLHttpRequest.prototype.send;
|
|
408
|
-
const self = this;
|
|
409
|
-
|
|
410
|
-
XMLHttpRequest.prototype.open = function (method: string, url: string) {
|
|
411
|
-
(this as any)._reportli_method = method;
|
|
412
|
-
(this as any)._reportli_url = url;
|
|
413
|
-
return originalOpen.apply(this, arguments as any);
|
|
414
|
-
} as any;
|
|
415
|
-
|
|
416
|
-
XMLHttpRequest.prototype.send = function () {
|
|
417
|
-
const xhrInstance = this;
|
|
418
|
-
|
|
419
|
-
xhrInstance.addEventListener("loadend", () => {
|
|
420
|
-
try {
|
|
421
|
-
const url = (xhrInstance as any)._reportli_url || "";
|
|
422
|
-
const method = (xhrInstance as any)._reportli_method || "GET";
|
|
423
|
-
const status = xhrInstance.status;
|
|
424
|
-
|
|
425
|
-
// Skip self calls
|
|
426
|
-
if (url.includes("rapid-processor") || url.includes("supabase.co/functions")) {
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
if (status >= 400 || status === 0) {
|
|
431
|
-
if (status === 0) {
|
|
432
|
-
self.logNetworkFailure(url, new Error("CORS or Connection Refused"), method);
|
|
433
|
-
} else {
|
|
434
|
-
self.logNetworkError(url, status, xhrInstance.statusText || "XHR Error", method);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
} catch (e) {
|
|
438
|
-
console.error("[Reportli SDK] Failed to extract XHR statistics:", e);
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
return originalSend.apply(this, arguments as any);
|
|
443
|
-
} as any;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
private logNetworkError(url: string, status: number, statusText: string, method: string) {
|
|
447
|
-
let errorType = `API ${status} Error`;
|
|
448
|
-
let severity: "medium" | "high" | "critical" = "medium";
|
|
449
|
-
|
|
450
|
-
if (status === 400) errorType = "API 400 Bad Request";
|
|
451
|
-
else if (status === 401) { errorType = "API 401 Unauthorized"; severity = "high"; }
|
|
452
|
-
else if (status === 403) { errorType = "API 403 Forbidden"; severity = "high"; }
|
|
453
|
-
else if (status === 404) errorType = "API 404 Not Found";
|
|
454
|
-
else if (status === 500) { errorType = "API 500 Internal Server Error"; severity = "high"; }
|
|
455
|
-
else if (status === 503) { errorType = "API 503 Service Unavailable"; severity = "high"; }
|
|
456
|
-
|
|
457
|
-
const payload: ExceptionPayload = {
|
|
458
|
-
apiKey: this.config?.apiKey || "",
|
|
459
|
-
projectId: this.config?.projectId || "proj-sandbox",
|
|
460
|
-
projectName: this.config?.projectName || "SaaS App",
|
|
461
|
-
errorType: errorType,
|
|
462
|
-
errorMessage: `HTTP API failed with code ${status}: ${method} ${url}`,
|
|
463
|
-
errorStack: `Network Request Header Context: [Method: ${method}] Url: ${url} Response Description: ${statusText || "Dev server returned error."}`,
|
|
464
|
-
framework: this.config?.framework || "React/Next.js Client",
|
|
465
|
-
user_email: this.config?.userEmail || "anonymous",
|
|
466
|
-
severity: severity,
|
|
467
|
-
status: "open",
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
this.sendCrashReport(payload);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
private logNetworkFailure(url: string, error: Error | any, method: string) {
|
|
474
|
-
const errorMsg = String(error.message || error || "");
|
|
475
|
-
let errorType = "Fetch failed error";
|
|
476
|
-
let severity: "medium" | "high" = "high";
|
|
477
|
-
|
|
478
|
-
if (errorMsg.includes("timeout") || errorMsg.includes("abort")) {
|
|
479
|
-
errorType = "Request timeout error";
|
|
480
|
-
severity = "medium";
|
|
481
|
-
} else if (errorMsg.includes("CORS") || errorMsg.includes("origin") || errorMsg.includes("Access-Control")) {
|
|
482
|
-
errorType = "CORS error";
|
|
483
221
|
}
|
|
484
222
|
|
|
485
|
-
const
|
|
486
|
-
apiKey: this.config?.apiKey || "",
|
|
487
|
-
projectId: this.config?.projectId || "proj-sandbox",
|
|
488
|
-
projectName: this.config?.projectName || "SaaS App",
|
|
489
|
-
errorType: errorType,
|
|
490
|
-
errorMessage: `Failed to complete HTTP request: [${method}] ${url}`,
|
|
491
|
-
errorStack: `Network crash context: ${errorMsg}\nConnection status: Failed to resolve.`,
|
|
492
|
-
framework: this.config?.framework || "React/Next.js Client",
|
|
493
|
-
user_email: this.config?.userEmail || "anonymous",
|
|
494
|
-
severity: severity,
|
|
495
|
-
status: "open",
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
this.sendCrashReport(payload);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
/**
|
|
502
|
-
* Helper to safely format raw objects / strings into structured Errors
|
|
503
|
-
*/
|
|
504
|
-
private normalizeError(err: any): { name: string; message: string; stack?: string } {
|
|
505
|
-
if (err instanceof Error) {
|
|
506
|
-
return {
|
|
507
|
-
name: err.name,
|
|
508
|
-
message: err.message,
|
|
509
|
-
stack: err.stack,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (typeof err === "string") {
|
|
514
|
-
return {
|
|
515
|
-
name: "Error",
|
|
516
|
-
message: err,
|
|
517
|
-
stack: new Error(err).stack,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
if (err && typeof err === "object") {
|
|
522
|
-
return {
|
|
523
|
-
name: err.name || err.code || "Error",
|
|
524
|
-
message: err.message || err.reason || JSON.stringify(err),
|
|
525
|
-
stack: err.stack || err.traceback || new Error(err.message).stack,
|
|
526
|
-
};
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return {
|
|
223
|
+
const err = (event as ErrorEvent).error || {
|
|
530
224
|
name: "Error",
|
|
531
|
-
message:
|
|
532
|
-
stack:
|
|
225
|
+
message: (event as ErrorEvent).message,
|
|
226
|
+
stack: `${(event as ErrorEvent).message} at ${(event as ErrorEvent).filename}:${(event as ErrorEvent).lineno}:${(event as ErrorEvent).colno}`,
|
|
533
227
|
};
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Comprehensive classification rules checking substring and classifications
|
|
538
|
-
*/
|
|
539
|
-
private classifyError(error: any, context?: string): { errorType: string; severity: "low" | "medium" | "high" | "critical" } {
|
|
540
|
-
const msg = String(error.message || error || "").toLowerCase();
|
|
541
|
-
const name = String(error.name || "").toLowerCase();
|
|
228
|
+
send(buildErrorPayload(err));
|
|
229
|
+
}, true);
|
|
542
230
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
231
|
+
// Unhandled promise rejections
|
|
232
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
233
|
+
send(buildErrorPayload(event.reason || { message: "Unhandled Promise Rejection" }, "unhandledrejection"));
|
|
234
|
+
});
|
|
547
235
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
553
|
-
return
|
|
554
|
-
}
|
|
555
|
-
if (msg.includes("failed prop type") || msg.includes("invalid prop")) {
|
|
556
|
-
return { errorType: "Props type error", severity: "low" };
|
|
557
|
-
}
|
|
558
|
-
if (msg.includes("render") || msg.includes("react error boundary") || msg.includes("component render")) {
|
|
559
|
-
return { errorType: "Component render error", severity: "critical" };
|
|
560
|
-
}
|
|
561
|
-
if (msg.includes("route not found") || msg.includes("cannot find route") || msg.includes("404 route")) {
|
|
562
|
-
return { errorType: "Route not found error", severity: "medium" };
|
|
563
|
-
}
|
|
564
|
-
if (msg.includes("failed to fetch dynamically imported") || msg.includes("dynamic import")) {
|
|
565
|
-
return { errorType: "Dynamic import error", severity: "high" };
|
|
236
|
+
// Intercept fetch
|
|
237
|
+
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);
|
|
566
242
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
return { errorType: "ReferenceError", severity: "critical" };
|
|
577
|
-
}
|
|
578
|
-
if (name === "rangeerror" || msg.startsWith("rangeerror")) {
|
|
579
|
-
if (msg.includes("maximum call stack") || msg.includes("stack overflow")) {
|
|
580
|
-
return { errorType: "Stack overflow error", severity: "critical" };
|
|
243
|
+
try {
|
|
244
|
+
const response = await originalFetch.apply(this, arguments as any);
|
|
245
|
+
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}`,
|
|
250
|
+
status: response.status,
|
|
251
|
+
}));
|
|
581
252
|
}
|
|
582
|
-
return
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (name === "urierror") {
|
|
591
|
-
return { errorType: "URIError", severity: "medium" };
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// 4. Payment & Billing Errors (Stripe/Paypal)
|
|
595
|
-
if (msg.includes("stripe") && (msg.includes("key") || msg.includes("init") || msg.includes("key is required") || msg.includes("is not defined"))) {
|
|
596
|
-
return { errorType: "Stripe initialization error", severity: "critical" };
|
|
597
|
-
}
|
|
598
|
-
if (msg.includes("stripe") && (msg.includes("payment") || msg.includes("processing") || msg.includes("charge"))) {
|
|
599
|
-
return { errorType: "Payment processing error", severity: "critical" };
|
|
600
|
-
}
|
|
601
|
-
if (msg.includes("card declined") || msg.includes("card_declined") || msg.includes("declined")) {
|
|
602
|
-
return { errorType: "Card declined error", severity: "high" };
|
|
603
|
-
}
|
|
604
|
-
if (msg.includes("checkout session") || msg.includes("create checkout")) {
|
|
605
|
-
return { errorType: "Checkout session error", severity: "high" };
|
|
606
|
-
}
|
|
607
|
-
if (msg.includes("refund failed") || msg.includes("refund_failed")) {
|
|
608
|
-
return { errorType: "Refund failed error", severity: "high" };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// 5. Database Errors (Supabase / PG / Mongo)
|
|
612
|
-
if (msg.includes("supabase query") || msg.includes("postgresterror")) {
|
|
613
|
-
return { errorType: "Supabase query error", severity: "critical" };
|
|
614
|
-
}
|
|
615
|
-
if (msg.includes("unique constraint") || msg.includes("duplicate key")) {
|
|
616
|
-
return { errorType: "Unique constraint error", severity: "high" };
|
|
617
|
-
}
|
|
618
|
-
if (msg.includes("foreign key")) {
|
|
619
|
-
return { errorType: "Foreign key error", severity: "high" };
|
|
620
|
-
}
|
|
621
|
-
if (msg.includes("transaction failed") || msg.includes("rollback")) {
|
|
622
|
-
return { errorType: "Transaction failed error", severity: "critical" };
|
|
623
|
-
}
|
|
624
|
-
if (msg.includes("connection lost") || msg.includes("db connection") || msg.includes("connection refused") || msg.includes("econnrefused")) {
|
|
625
|
-
return { errorType: "Connection lost error", severity: "critical" };
|
|
626
|
-
}
|
|
627
|
-
if (msg.includes("query timeout") || msg.includes("statement timeout")) {
|
|
628
|
-
return { errorType: "Query timeout error", severity: "high" };
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// 6. JWT & Auth Server Errors
|
|
632
|
-
if (msg.includes("jwt expired") || msg.includes("token expired") || msg.includes("jsonwebtokenexpired")) {
|
|
633
|
-
return { errorType: "JWT expired error", severity: "high" };
|
|
634
|
-
}
|
|
635
|
-
if (msg.includes("jwt verification") || msg.includes("invalid signature") || msg.includes("jwt malformed")) {
|
|
636
|
-
return { errorType: "JWT verification error", severity: "high" };
|
|
637
|
-
}
|
|
638
|
-
if (msg.includes("firebase-admin") || msg.includes("firebase admin")) {
|
|
639
|
-
return { errorType: "Firebase admin error", severity: "critical" };
|
|
640
|
-
}
|
|
641
|
-
if (msg.includes("session creation") || msg.includes("create session")) {
|
|
642
|
-
return { errorType: "Session creation error", severity: "high" };
|
|
643
|
-
}
|
|
644
|
-
if (msg.includes("password hash") || msg.includes("bcrypt") || msg.includes("argon2")) {
|
|
645
|
-
return { errorType: "Password hash error", severity: "critical" };
|
|
253
|
+
return response;
|
|
254
|
+
} catch (err: any) {
|
|
255
|
+
send(buildErrorPayload({
|
|
256
|
+
name: "FetchError",
|
|
257
|
+
message: `Fetch failed: ${init?.method || "GET"} ${url} — ${err.message}`,
|
|
258
|
+
stack: err.stack,
|
|
259
|
+
}));
|
|
260
|
+
throw err;
|
|
646
261
|
}
|
|
262
|
+
};
|
|
647
263
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
264
|
+
// Intercept XHR
|
|
265
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
266
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
267
|
+
|
|
268
|
+
XMLHttpRequest.prototype.open = function (method: string, url: string) {
|
|
269
|
+
(this as any)._r_method = method;
|
|
270
|
+
(this as any)._r_url = url;
|
|
271
|
+
return originalOpen.apply(this, arguments as any);
|
|
272
|
+
} as any;
|
|
273
|
+
|
|
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);
|
|
289
|
+
} as any;
|
|
667
290
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
291
|
+
// Disconnect when page closes
|
|
292
|
+
window.addEventListener("beforeunload", () => {
|
|
293
|
+
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
|
+
}));
|
|
301
|
+
} catch { /* silent */ }
|
|
302
|
+
});
|
|
303
|
+
}
|
|
681
304
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
305
|
+
// ─── Server listeners ─────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function activateServerListeners(): void {
|
|
308
|
+
process.on("uncaughtException", (error: Error) => {
|
|
309
|
+
send(buildErrorPayload(error, "uncaughtexception"));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
process.on("unhandledRejection", (reason: any) => {
|
|
313
|
+
send(buildErrorPayload(
|
|
314
|
+
reason instanceof Error ? reason : { name: "UnhandledRejection", message: String(reason), stack: "" },
|
|
315
|
+
"unhandledrejection"
|
|
316
|
+
));
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const shutdown = async () => {
|
|
320
|
+
await send({
|
|
321
|
+
type: "SDK_DISCONNECTED",
|
|
322
|
+
apiKey: _apiKey,
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
environment: "server",
|
|
325
|
+
url: getUrl(),
|
|
326
|
+
});
|
|
327
|
+
process.exit(0);
|
|
328
|
+
};
|
|
692
329
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
if (msg.includes("middleware")) {
|
|
698
|
-
return { errorType: "Middleware error", severity: "high" };
|
|
699
|
-
}
|
|
700
|
-
if (msg.includes("validation error") || msg.includes("zoderror") || msg.includes("joi validation")) {
|
|
701
|
-
return { errorType: "Request validation error", severity: "medium" };
|
|
702
|
-
}
|
|
703
|
-
if (msg.includes("body parser") || msg.includes("multer") || msg.includes("multipart") || msg.includes("body-parser")) {
|
|
704
|
-
return { errorType: "Body parser error", severity: "high" };
|
|
705
|
-
}
|
|
706
|
-
if (msg.includes("too many requests") || msg.includes("rate limit") || msg.includes("429")) {
|
|
707
|
-
return { errorType: "Rate limit error", severity: "high" };
|
|
708
|
-
}
|
|
330
|
+
process.on("SIGTERM", shutdown);
|
|
331
|
+
process.on("SIGINT", shutdown);
|
|
332
|
+
}
|
|
709
333
|
|
|
710
|
-
|
|
711
|
-
if (msg.includes("file upload") || msg.includes("upload failed") || msg.includes("cloudinary") || msg.includes("s3 upload") || msg.includes("multipart upload")) {
|
|
712
|
-
return { errorType: "File upload failed", severity: "high" };
|
|
713
|
-
}
|
|
714
|
-
if (msg.includes("file size exceeded") || msg.includes("payload too large") || msg.includes("size validation")) {
|
|
715
|
-
return { errorType: "File size exceeded", severity: "medium" };
|
|
716
|
-
}
|
|
717
|
-
if (msg.includes("invalid file type") || msg.includes("mime type mismatch") || msg.includes("file format")) {
|
|
718
|
-
return { errorType: "Invalid file type", severity: "medium" };
|
|
719
|
-
}
|
|
720
|
-
if (msg.includes("storage quota exceeded") || msg.includes("bucket quota") || msg.includes("exhausted quota")) {
|
|
721
|
-
return { errorType: "Storage quota exceeded", severity: "high" };
|
|
722
|
-
}
|
|
723
|
-
if (msg.includes("cdn ") || msg.includes("upload to cdn") || msg.includes("cdn distribution")) {
|
|
724
|
-
return { errorType: "CDN upload failed", severity: "high" };
|
|
725
|
-
}
|
|
334
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
726
335
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
if (msg.includes("smtp connection") || msg.includes("smtp error") || msg.includes("mail connection")) {
|
|
732
|
-
return { errorType: "SMTP connection error", severity: "high" };
|
|
733
|
-
}
|
|
734
|
-
if (msg.includes("template rendering") || msg.includes("mjml") || msg.includes("pug render")) {
|
|
735
|
-
return { errorType: "Template rendering error", severity: "medium" };
|
|
736
|
-
}
|
|
737
|
-
if (msg.includes("onesignal") || msg.includes("push notification") || msg.includes("fcm payload")) {
|
|
738
|
-
return { errorType: "OneSignal API error", severity: "high" };
|
|
739
|
-
}
|
|
336
|
+
const Reportli = {
|
|
337
|
+
init({ apiKey }: { apiKey: string }): void {
|
|
338
|
+
if (!apiKey || _apiKey) return;
|
|
339
|
+
_apiKey = apiKey;
|
|
740
340
|
|
|
741
|
-
//
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
+
});
|
|
756
364
|
}
|
|
757
365
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (msg.includes("async") && msg.includes("await")) {
|
|
763
|
-
return { errorType: "Async await failure", severity: "high" };
|
|
764
|
-
}
|
|
765
|
-
if (msg.includes("promise chain") || msg.includes("promise.all") || msg.includes("promise.race")) {
|
|
766
|
-
return { errorType: "Promise chain error", severity: "high" };
|
|
767
|
-
}
|
|
768
|
-
if (msg.includes("promise timed out") || msg.includes("promise timeout")) {
|
|
769
|
-
return { errorType: "Promise timeout error", severity: "medium" };
|
|
366
|
+
if (isBrowser) {
|
|
367
|
+
activateBrowserListeners();
|
|
368
|
+
} else if (isNode) {
|
|
369
|
+
activateServerListeners();
|
|
770
370
|
}
|
|
371
|
+
},
|
|
771
372
|
|
|
772
|
-
|
|
773
|
-
if (
|
|
774
|
-
|
|
775
|
-
|
|
373
|
+
capture(error: any): void {
|
|
374
|
+
if (!_apiKey) return;
|
|
375
|
+
send(buildErrorPayload(error, "manual"));
|
|
376
|
+
},
|
|
776
377
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
378
|
+
errorHandler() {
|
|
379
|
+
return function (err: any, _req: any, _res: any, next: any) {
|
|
380
|
+
send(buildErrorPayload(err, "express"));
|
|
381
|
+
next(err);
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
};
|
|
780
385
|
|
|
781
|
-
export const Reportli = new ReportliTracker();
|
|
782
386
|
export default Reportli;
|
|
387
|
+
export { Reportli };
|