@techts/sdk 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/index.d.mts +118 -0
- package/dist/index.d.ts +118 -0
- package/dist/index.js +708 -0
- package/dist/index.mjs +683 -0
- package/package.json +24 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DebuggerSDK: () => DebuggerSDK
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var DebuggerSDK = class {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.queue = [];
|
|
29
|
+
this.batchTimer = null;
|
|
30
|
+
this.metricsTimer = null;
|
|
31
|
+
this.navigationCount = 0;
|
|
32
|
+
this.errorCount = 0;
|
|
33
|
+
this.networkFailCount = 0;
|
|
34
|
+
this.longTaskCount = 0;
|
|
35
|
+
this.originalFetch = null;
|
|
36
|
+
this.originalXhrOpen = null;
|
|
37
|
+
this.originalXhrSend = null;
|
|
38
|
+
this.originalWsConstructor = null;
|
|
39
|
+
this.config = {
|
|
40
|
+
platform: "Browser",
|
|
41
|
+
version: "1.0.0",
|
|
42
|
+
batchInterval: 2e3,
|
|
43
|
+
maxBatchSize: 50,
|
|
44
|
+
captureConsole: true,
|
|
45
|
+
captureErrors: true,
|
|
46
|
+
captureNetwork: true,
|
|
47
|
+
captureResources: true,
|
|
48
|
+
captureWebVitals: true,
|
|
49
|
+
captureLongTasks: true,
|
|
50
|
+
captureNavigation: true,
|
|
51
|
+
captureVisibility: true,
|
|
52
|
+
captureCSP: true,
|
|
53
|
+
captureWebSockets: true,
|
|
54
|
+
slowRequestThresholdMs: 3e3,
|
|
55
|
+
metricsInterval: 3e4,
|
|
56
|
+
sessionId: "",
|
|
57
|
+
userId: "",
|
|
58
|
+
...config
|
|
59
|
+
};
|
|
60
|
+
this.sessionId = this.config.sessionId || this.generateSessionId();
|
|
61
|
+
this.pageLoadTime = performance.now();
|
|
62
|
+
this.originalConsole = {};
|
|
63
|
+
for (const m of ["log", "warn", "error", "debug", "info", "trace", "assert"]) {
|
|
64
|
+
this.originalConsole[m] = console[m]?.bind(console);
|
|
65
|
+
}
|
|
66
|
+
if (this.config.captureConsole) this.interceptConsole();
|
|
67
|
+
if (this.config.captureErrors) this.captureGlobalErrors();
|
|
68
|
+
if (this.config.captureNetwork) this.interceptNetwork();
|
|
69
|
+
if (this.config.captureResources) this.captureResourceErrors();
|
|
70
|
+
if (this.config.captureWebVitals) this.captureWebVitals();
|
|
71
|
+
if (this.config.captureLongTasks) this.captureLongTasks();
|
|
72
|
+
if (this.config.captureNavigation) this.captureNavigation();
|
|
73
|
+
if (this.config.captureVisibility) this.captureVisibilityChanges();
|
|
74
|
+
if (this.config.captureCSP) this.captureCSPViolations();
|
|
75
|
+
if (this.config.captureWebSockets) this.interceptWebSockets();
|
|
76
|
+
this.batchTimer = setInterval(() => this.flush(), this.config.batchInterval);
|
|
77
|
+
if (this.config.metricsInterval > 0) {
|
|
78
|
+
this.metricsTimer = setInterval(
|
|
79
|
+
() => this.collectBrowserMetrics(),
|
|
80
|
+
this.config.metricsInterval
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
this.heartbeat();
|
|
84
|
+
this.log("info", `debugger.help SDK v3 initialized (session: ${this.sessionId})`, {
|
|
85
|
+
userAgent: navigator.userAgent,
|
|
86
|
+
url: location.href,
|
|
87
|
+
viewport: `${innerWidth}x${innerHeight}`,
|
|
88
|
+
devicePixelRatio,
|
|
89
|
+
language: navigator.language,
|
|
90
|
+
cookiesEnabled: navigator.cookieEnabled,
|
|
91
|
+
onLine: navigator.onLine
|
|
92
|
+
});
|
|
93
|
+
if (typeof window !== "undefined") {
|
|
94
|
+
window.addEventListener("beforeunload", () => this.flush());
|
|
95
|
+
window.addEventListener("online", () => this.log("info", "Network: back online"));
|
|
96
|
+
window.addEventListener("offline", () => this.log("warn", "Network: went offline"));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ── Public API ───────────────────────────────────────────
|
|
100
|
+
log(level, message, context) {
|
|
101
|
+
this.enqueue({
|
|
102
|
+
type: "log",
|
|
103
|
+
level,
|
|
104
|
+
message,
|
|
105
|
+
context: { ...context, sessionId: this.sessionId, userId: this.config.userId || void 0 }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
debug(msg, ctx) {
|
|
109
|
+
this.log("debug", msg, ctx);
|
|
110
|
+
}
|
|
111
|
+
info(msg, ctx) {
|
|
112
|
+
this.log("info", msg, ctx);
|
|
113
|
+
}
|
|
114
|
+
warn(msg, ctx) {
|
|
115
|
+
this.log("warn", msg, ctx);
|
|
116
|
+
}
|
|
117
|
+
error(msg, ctx) {
|
|
118
|
+
this.log("error", msg, ctx);
|
|
119
|
+
}
|
|
120
|
+
critical(msg, ctx) {
|
|
121
|
+
this.log("critical", msg, ctx);
|
|
122
|
+
}
|
|
123
|
+
captureError(err, context) {
|
|
124
|
+
this.errorCount++;
|
|
125
|
+
this.enqueue({
|
|
126
|
+
type: "error",
|
|
127
|
+
title: err.message,
|
|
128
|
+
stackTrace: err.stack || "",
|
|
129
|
+
context: {
|
|
130
|
+
name: err.name,
|
|
131
|
+
sessionId: this.sessionId,
|
|
132
|
+
userId: this.config.userId || void 0,
|
|
133
|
+
errorCount: this.errorCount,
|
|
134
|
+
...context
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Capture React error boundary errors.
|
|
140
|
+
* Call from componentDidCatch or ErrorBoundary fallback.
|
|
141
|
+
*/
|
|
142
|
+
captureReactError(error, errorInfo) {
|
|
143
|
+
this.captureError(error, {
|
|
144
|
+
capturedFrom: "react_error_boundary",
|
|
145
|
+
componentStack: errorInfo.componentStack?.slice(0, 3e3)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
metric(data) {
|
|
149
|
+
this.enqueue({
|
|
150
|
+
type: "metric",
|
|
151
|
+
...data,
|
|
152
|
+
custom: { ...data.custom || {}, sessionId: this.sessionId }
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
inspect(variables) {
|
|
156
|
+
this.enqueue({ type: "inspect", variables });
|
|
157
|
+
}
|
|
158
|
+
heartbeat() {
|
|
159
|
+
this.enqueue({ type: "heartbeat" });
|
|
160
|
+
}
|
|
161
|
+
setUserId(userId) {
|
|
162
|
+
this.config.userId = userId;
|
|
163
|
+
this.log("info", `User identified: ${userId}`);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Track image generation results from the frontend
|
|
167
|
+
*/
|
|
168
|
+
captureImageGenResult(jobId, model, params, result, error, durationMs) {
|
|
169
|
+
const ctx = { jobId, model, params, durationMs };
|
|
170
|
+
if (error) {
|
|
171
|
+
this.enqueue({
|
|
172
|
+
type: "error",
|
|
173
|
+
title: `Image gen failed: ${model} \u2014 ${error}`,
|
|
174
|
+
context: { ...ctx, capturedFrom: "image_gen" }
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
this.log("info", `Image gen complete: ${model} (${durationMs}ms)`, {
|
|
178
|
+
...ctx,
|
|
179
|
+
result,
|
|
180
|
+
capturedFrom: "image_gen"
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// ── Console Interception ─────────────────────────────────
|
|
185
|
+
interceptConsole() {
|
|
186
|
+
const self = this;
|
|
187
|
+
const levelMap = {
|
|
188
|
+
log: "info",
|
|
189
|
+
info: "info",
|
|
190
|
+
warn: "warn",
|
|
191
|
+
error: "error",
|
|
192
|
+
debug: "debug",
|
|
193
|
+
trace: "debug"
|
|
194
|
+
};
|
|
195
|
+
for (const [method, level] of Object.entries(levelMap)) {
|
|
196
|
+
const original = self.originalConsole[method];
|
|
197
|
+
if (!original) continue;
|
|
198
|
+
console[method] = function(...args) {
|
|
199
|
+
original(...args);
|
|
200
|
+
const message = args.map((a) => {
|
|
201
|
+
if (a instanceof Error) return `${a.name}: ${a.message}
|
|
202
|
+
${a.stack}`;
|
|
203
|
+
if (typeof a === "object") {
|
|
204
|
+
try {
|
|
205
|
+
return JSON.stringify(a, null, 0);
|
|
206
|
+
} catch {
|
|
207
|
+
return String(a);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return String(a);
|
|
211
|
+
}).join(" ");
|
|
212
|
+
self.enqueue({
|
|
213
|
+
type: "log",
|
|
214
|
+
level,
|
|
215
|
+
message: `[console.${method}] ${message.slice(0, 3e3)}`,
|
|
216
|
+
context: { capturedFrom: "console", sessionId: self.sessionId }
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const origAssert = self.originalConsole.assert;
|
|
221
|
+
if (origAssert) {
|
|
222
|
+
console.assert = function(condition, ...args) {
|
|
223
|
+
origAssert(condition, ...args);
|
|
224
|
+
if (!condition) {
|
|
225
|
+
const message = args.map((a) => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" ");
|
|
226
|
+
self.enqueue({
|
|
227
|
+
type: "error",
|
|
228
|
+
title: `Assertion failed: ${message.slice(0, 500)}`,
|
|
229
|
+
context: { capturedFrom: "console.assert", sessionId: self.sessionId }
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ── Global Error Capture ─────────────────────────────────
|
|
236
|
+
captureGlobalErrors() {
|
|
237
|
+
if (typeof window === "undefined") return;
|
|
238
|
+
window.addEventListener("error", (event) => {
|
|
239
|
+
if (event.target && event.target.tagName) return;
|
|
240
|
+
this.captureError(event.error || new Error(event.message), {
|
|
241
|
+
filename: event.filename,
|
|
242
|
+
lineno: event.lineno,
|
|
243
|
+
colno: event.colno,
|
|
244
|
+
capturedFrom: "window.onerror"
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
window.addEventListener("unhandledrejection", (event) => {
|
|
248
|
+
const err = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
|
|
249
|
+
this.captureError(err, { capturedFrom: "unhandledrejection" });
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
// ── Resource Error Capture ───────────────────────────────
|
|
253
|
+
captureResourceErrors() {
|
|
254
|
+
if (typeof window === "undefined") return;
|
|
255
|
+
window.addEventListener("error", (event) => {
|
|
256
|
+
const target = event.target;
|
|
257
|
+
if (!target?.tagName) return;
|
|
258
|
+
const tag = target.tagName.toLowerCase();
|
|
259
|
+
const src = target.src || target.href || target.src;
|
|
260
|
+
if (src) {
|
|
261
|
+
this.enqueue({
|
|
262
|
+
type: "error",
|
|
263
|
+
title: `Resource failed to load: <${tag}> ${src}`,
|
|
264
|
+
context: {
|
|
265
|
+
capturedFrom: "resource_error",
|
|
266
|
+
tagName: tag,
|
|
267
|
+
src,
|
|
268
|
+
sessionId: this.sessionId
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}, true);
|
|
273
|
+
}
|
|
274
|
+
// ── Network Interception (Fetch + XHR) ───────────────────
|
|
275
|
+
interceptNetwork() {
|
|
276
|
+
if (typeof window === "undefined") return;
|
|
277
|
+
this.interceptFetch();
|
|
278
|
+
this.interceptXHR();
|
|
279
|
+
}
|
|
280
|
+
interceptFetch() {
|
|
281
|
+
if (typeof fetch === "undefined") return;
|
|
282
|
+
this.originalFetch = fetch.bind(window);
|
|
283
|
+
const self = this;
|
|
284
|
+
const originalFetch = this.originalFetch;
|
|
285
|
+
window.fetch = async function(input, init) {
|
|
286
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
287
|
+
const method = init?.method || "GET";
|
|
288
|
+
const start = performance.now();
|
|
289
|
+
try {
|
|
290
|
+
const response = await originalFetch(input, init);
|
|
291
|
+
const duration = Math.round(performance.now() - start);
|
|
292
|
+
if (!url.includes("/ingest") && !url.includes("/debug")) {
|
|
293
|
+
if (response.status >= 400) {
|
|
294
|
+
self.networkFailCount++;
|
|
295
|
+
let responseBody = "";
|
|
296
|
+
try {
|
|
297
|
+
const cloned = response.clone();
|
|
298
|
+
responseBody = (await cloned.text()).slice(0, 1e3);
|
|
299
|
+
} catch {
|
|
300
|
+
}
|
|
301
|
+
self.enqueue({
|
|
302
|
+
type: response.status >= 500 ? "error" : "log",
|
|
303
|
+
...response.status >= 500 ? { title: `[fetch] ${method} ${url} \u2192 ${response.status}` } : {},
|
|
304
|
+
level: response.status >= 500 ? "error" : "warn",
|
|
305
|
+
message: `[fetch] ${method} ${url} \u2192 ${response.status} (${duration}ms)`,
|
|
306
|
+
context: {
|
|
307
|
+
capturedFrom: "fetch",
|
|
308
|
+
url,
|
|
309
|
+
method,
|
|
310
|
+
status: response.status,
|
|
311
|
+
statusText: response.statusText,
|
|
312
|
+
duration,
|
|
313
|
+
responseBody,
|
|
314
|
+
requestHeaders: self.sanitizeHeaders(init?.headers),
|
|
315
|
+
sessionId: self.sessionId
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
} else if (duration > self.config.slowRequestThresholdMs) {
|
|
319
|
+
self.log("warn", `[fetch] Slow request: ${method} ${url} (${duration}ms)`, {
|
|
320
|
+
capturedFrom: "fetch",
|
|
321
|
+
url,
|
|
322
|
+
method,
|
|
323
|
+
status: response.status,
|
|
324
|
+
duration
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return response;
|
|
329
|
+
} catch (err) {
|
|
330
|
+
const duration = Math.round(performance.now() - start);
|
|
331
|
+
if (!url.includes("/ingest") && !url.includes("/debug")) {
|
|
332
|
+
self.networkFailCount++;
|
|
333
|
+
self.enqueue({
|
|
334
|
+
type: "error",
|
|
335
|
+
title: `Network request failed: ${method} ${url}`,
|
|
336
|
+
stackTrace: err instanceof Error ? err.stack || "" : "",
|
|
337
|
+
context: {
|
|
338
|
+
capturedFrom: "fetch",
|
|
339
|
+
url,
|
|
340
|
+
method,
|
|
341
|
+
duration,
|
|
342
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
343
|
+
sessionId: self.sessionId
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
throw err;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
interceptXHR() {
|
|
352
|
+
if (typeof XMLHttpRequest === "undefined") return;
|
|
353
|
+
const self = this;
|
|
354
|
+
const origOpen = XMLHttpRequest.prototype.open;
|
|
355
|
+
const origSend = XMLHttpRequest.prototype.send;
|
|
356
|
+
this.originalXhrOpen = origOpen;
|
|
357
|
+
this.originalXhrSend = origSend;
|
|
358
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
359
|
+
this.__ys_method = method;
|
|
360
|
+
this.__ys_url = typeof url === "string" ? url : url.href;
|
|
361
|
+
return origOpen.apply(this, [method, url, ...rest]);
|
|
362
|
+
};
|
|
363
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
364
|
+
const xhrUrl = this.__ys_url;
|
|
365
|
+
const xhrMethod = this.__ys_method;
|
|
366
|
+
if (xhrUrl?.includes("/ingest") || xhrUrl?.includes("/debug")) {
|
|
367
|
+
return origSend.call(this, body);
|
|
368
|
+
}
|
|
369
|
+
const start = performance.now();
|
|
370
|
+
this.addEventListener("loadend", function() {
|
|
371
|
+
const duration = Math.round(performance.now() - start);
|
|
372
|
+
if (this.status >= 400) {
|
|
373
|
+
self.networkFailCount++;
|
|
374
|
+
self.enqueue({
|
|
375
|
+
type: this.status >= 500 ? "error" : "log",
|
|
376
|
+
...this.status >= 500 ? { title: `[xhr] ${xhrMethod} ${xhrUrl} \u2192 ${this.status}` } : {},
|
|
377
|
+
level: this.status >= 500 ? "error" : "warn",
|
|
378
|
+
message: `[xhr] ${xhrMethod} ${xhrUrl} \u2192 ${this.status} (${duration}ms)`,
|
|
379
|
+
context: {
|
|
380
|
+
capturedFrom: "xhr",
|
|
381
|
+
url: xhrUrl,
|
|
382
|
+
method: xhrMethod,
|
|
383
|
+
status: this.status,
|
|
384
|
+
duration,
|
|
385
|
+
responseBody: (this.responseText || "").slice(0, 500),
|
|
386
|
+
sessionId: self.sessionId
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
} else if (duration > self.config.slowRequestThresholdMs) {
|
|
390
|
+
self.log("warn", `[xhr] Slow request: ${xhrMethod} ${xhrUrl} (${duration}ms)`, {
|
|
391
|
+
capturedFrom: "xhr",
|
|
392
|
+
url: xhrUrl,
|
|
393
|
+
method: xhrMethod,
|
|
394
|
+
status: this.status,
|
|
395
|
+
duration
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
this.addEventListener("error", function() {
|
|
400
|
+
self.networkFailCount++;
|
|
401
|
+
self.enqueue({
|
|
402
|
+
type: "error",
|
|
403
|
+
title: `XHR request failed: ${xhrMethod} ${xhrUrl}`,
|
|
404
|
+
context: { capturedFrom: "xhr", url: xhrUrl, method: xhrMethod, sessionId: self.sessionId }
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
return origSend.call(this, body);
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
// ── Web Vitals ───────────────────────────────────────────
|
|
411
|
+
captureWebVitals() {
|
|
412
|
+
if (typeof PerformanceObserver === "undefined") return;
|
|
413
|
+
try {
|
|
414
|
+
const lcpObserver = new PerformanceObserver((list) => {
|
|
415
|
+
const entries = list.getEntries();
|
|
416
|
+
const last = entries[entries.length - 1];
|
|
417
|
+
if (last) {
|
|
418
|
+
this.log("info", `[perf] LCP: ${Math.round(last.startTime)}ms`, {
|
|
419
|
+
capturedFrom: "web_vitals",
|
|
420
|
+
metric: "LCP",
|
|
421
|
+
value: Math.round(last.startTime),
|
|
422
|
+
element: last.element?.tagName
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
|
|
427
|
+
} catch {
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
const fidObserver = new PerformanceObserver((list) => {
|
|
431
|
+
for (const entry of list.getEntries()) {
|
|
432
|
+
const fid = entry.processingStart - entry.startTime;
|
|
433
|
+
this.log("info", `[perf] FID: ${Math.round(fid)}ms`, {
|
|
434
|
+
capturedFrom: "web_vitals",
|
|
435
|
+
metric: "FID",
|
|
436
|
+
value: Math.round(fid)
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
fidObserver.observe({ type: "first-input", buffered: true });
|
|
441
|
+
} catch {
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
let clsValue = 0;
|
|
445
|
+
const clsObserver = new PerformanceObserver((list) => {
|
|
446
|
+
for (const entry of list.getEntries()) {
|
|
447
|
+
if (!entry.hadRecentInput) {
|
|
448
|
+
clsValue += entry.value;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
clsObserver.observe({ type: "layout-shift", buffered: true });
|
|
453
|
+
if (typeof document !== "undefined") {
|
|
454
|
+
document.addEventListener("visibilitychange", () => {
|
|
455
|
+
if (document.visibilityState === "hidden") {
|
|
456
|
+
this.log("info", `[perf] CLS: ${clsValue.toFixed(4)}`, {
|
|
457
|
+
capturedFrom: "web_vitals",
|
|
458
|
+
metric: "CLS",
|
|
459
|
+
value: clsValue
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
} catch {
|
|
465
|
+
}
|
|
466
|
+
try {
|
|
467
|
+
const fcpObserver = new PerformanceObserver((list) => {
|
|
468
|
+
for (const entry of list.getEntries()) {
|
|
469
|
+
if (entry.name === "first-contentful-paint") {
|
|
470
|
+
this.log("info", `[perf] FCP: ${Math.round(entry.startTime)}ms`, {
|
|
471
|
+
capturedFrom: "web_vitals",
|
|
472
|
+
metric: "FCP",
|
|
473
|
+
value: Math.round(entry.startTime)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
fcpObserver.observe({ type: "paint", buffered: true });
|
|
479
|
+
} catch {
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
483
|
+
if (nav) {
|
|
484
|
+
const ttfb = Math.round(nav.responseStart - nav.requestStart);
|
|
485
|
+
this.log("info", `[perf] TTFB: ${ttfb}ms`, {
|
|
486
|
+
capturedFrom: "web_vitals",
|
|
487
|
+
metric: "TTFB",
|
|
488
|
+
value: ttfb
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// ── Long Tasks ───────────────────────────────────────────
|
|
495
|
+
captureLongTasks() {
|
|
496
|
+
if (typeof PerformanceObserver === "undefined") return;
|
|
497
|
+
try {
|
|
498
|
+
const observer = new PerformanceObserver((list) => {
|
|
499
|
+
for (const entry of list.getEntries()) {
|
|
500
|
+
this.longTaskCount++;
|
|
501
|
+
if (entry.duration > 100) {
|
|
502
|
+
this.log("warn", `[perf] Long task: ${Math.round(entry.duration)}ms`, {
|
|
503
|
+
capturedFrom: "long_task",
|
|
504
|
+
duration: Math.round(entry.duration),
|
|
505
|
+
startTime: Math.round(entry.startTime),
|
|
506
|
+
longTaskCount: this.longTaskCount
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
observer.observe({ type: "longtask", buffered: true });
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
// ── Navigation Tracking ──────────────────────────────────
|
|
516
|
+
captureNavigation() {
|
|
517
|
+
if (typeof window === "undefined") return;
|
|
518
|
+
window.addEventListener("popstate", () => {
|
|
519
|
+
this.navigationCount++;
|
|
520
|
+
this.log("info", `[nav] popstate \u2192 ${location.href}`, {
|
|
521
|
+
capturedFrom: "navigation",
|
|
522
|
+
type: "popstate",
|
|
523
|
+
navigationCount: this.navigationCount
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
const origPush = history.pushState.bind(history);
|
|
527
|
+
const origReplace = history.replaceState.bind(history);
|
|
528
|
+
const self = this;
|
|
529
|
+
history.pushState = function(...args) {
|
|
530
|
+
origPush(...args);
|
|
531
|
+
self.navigationCount++;
|
|
532
|
+
self.log("info", `[nav] pushState \u2192 ${location.href}`, {
|
|
533
|
+
capturedFrom: "navigation",
|
|
534
|
+
type: "pushState",
|
|
535
|
+
navigationCount: self.navigationCount
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
history.replaceState = function(...args) {
|
|
539
|
+
origReplace(...args);
|
|
540
|
+
self.log("info", `[nav] replaceState \u2192 ${location.href}`, {
|
|
541
|
+
capturedFrom: "navigation",
|
|
542
|
+
type: "replaceState"
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
// ── Visibility Changes ───────────────────────────────────
|
|
547
|
+
captureVisibilityChanges() {
|
|
548
|
+
if (typeof document === "undefined") return;
|
|
549
|
+
document.addEventListener("visibilitychange", () => {
|
|
550
|
+
const state = document.visibilityState;
|
|
551
|
+
this.log("info", `[visibility] Tab ${state}`, {
|
|
552
|
+
capturedFrom: "visibility",
|
|
553
|
+
state,
|
|
554
|
+
hiddenDuration: state === "visible" ? Math.round(performance.now() - this.pageLoadTime) : void 0
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// ── CSP Violations ───────────────────────────────────────
|
|
559
|
+
captureCSPViolations() {
|
|
560
|
+
if (typeof document === "undefined") return;
|
|
561
|
+
document.addEventListener("securitypolicyviolation", (e) => {
|
|
562
|
+
this.enqueue({
|
|
563
|
+
type: "error",
|
|
564
|
+
title: `CSP violation: ${e.violatedDirective}`,
|
|
565
|
+
context: {
|
|
566
|
+
capturedFrom: "csp_violation",
|
|
567
|
+
blockedURI: e.blockedURI,
|
|
568
|
+
violatedDirective: e.violatedDirective,
|
|
569
|
+
effectiveDirective: e.effectiveDirective,
|
|
570
|
+
originalPolicy: e.originalPolicy?.slice(0, 500),
|
|
571
|
+
sourceFile: e.sourceFile,
|
|
572
|
+
lineNumber: e.lineNumber,
|
|
573
|
+
sessionId: this.sessionId
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
// ── WebSocket Interception ───────────────────────────────
|
|
579
|
+
interceptWebSockets() {
|
|
580
|
+
if (typeof WebSocket === "undefined") return;
|
|
581
|
+
const self = this;
|
|
582
|
+
const OriginalWebSocket = WebSocket;
|
|
583
|
+
this.originalWsConstructor = OriginalWebSocket;
|
|
584
|
+
window.WebSocket = function(url, protocols) {
|
|
585
|
+
const ws = new OriginalWebSocket(url, protocols);
|
|
586
|
+
const wsUrl = typeof url === "string" ? url : url.href;
|
|
587
|
+
ws.addEventListener("error", () => {
|
|
588
|
+
self.enqueue({
|
|
589
|
+
type: "error",
|
|
590
|
+
title: `WebSocket error: ${wsUrl}`,
|
|
591
|
+
context: { capturedFrom: "websocket", url: wsUrl, readyState: ws.readyState }
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
ws.addEventListener("close", (event) => {
|
|
595
|
+
if (!event.wasClean) {
|
|
596
|
+
self.log("warn", `[ws] Unclean close: ${wsUrl} (code: ${event.code})`, {
|
|
597
|
+
capturedFrom: "websocket",
|
|
598
|
+
url: wsUrl,
|
|
599
|
+
code: event.code,
|
|
600
|
+
reason: event.reason
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
return ws;
|
|
605
|
+
};
|
|
606
|
+
window.WebSocket.prototype = OriginalWebSocket.prototype;
|
|
607
|
+
window.WebSocket.CONNECTING = OriginalWebSocket.CONNECTING;
|
|
608
|
+
window.WebSocket.OPEN = OriginalWebSocket.OPEN;
|
|
609
|
+
window.WebSocket.CLOSING = OriginalWebSocket.CLOSING;
|
|
610
|
+
window.WebSocket.CLOSED = OriginalWebSocket.CLOSED;
|
|
611
|
+
}
|
|
612
|
+
// ── Browser Metrics ──────────────────────────────────────
|
|
613
|
+
collectBrowserMetrics() {
|
|
614
|
+
if (typeof window === "undefined") return;
|
|
615
|
+
const perf = performance;
|
|
616
|
+
const memory = perf.memory ? {
|
|
617
|
+
usedJSHeapSize: Math.round(perf.memory.usedJSHeapSize / 1e6),
|
|
618
|
+
totalJSHeapSize: Math.round(perf.memory.totalJSHeapSize / 1e6),
|
|
619
|
+
jsHeapSizeLimit: Math.round(perf.memory.jsHeapSizeLimit / 1e6)
|
|
620
|
+
} : null;
|
|
621
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
622
|
+
const domNodeCount = document.querySelectorAll("*").length;
|
|
623
|
+
const resourceEntries = performance.getEntriesByType("resource");
|
|
624
|
+
const failedResources = resourceEntries.filter((e) => e.transferSize === 0 && e.decodedBodySize === 0);
|
|
625
|
+
this.metric({
|
|
626
|
+
memory: memory?.usedJSHeapSize ?? null,
|
|
627
|
+
custom: {
|
|
628
|
+
...memory || {},
|
|
629
|
+
loadTime: nav ? Math.round(nav.loadEventEnd - nav.startTime) : null,
|
|
630
|
+
domInteractive: nav ? Math.round(nav.domInteractive - nav.startTime) : null,
|
|
631
|
+
domContentLoaded: nav ? Math.round(nav.domContentLoadedEventEnd - nav.startTime) : null,
|
|
632
|
+
domNodeCount,
|
|
633
|
+
resourceCount: resourceEntries.length,
|
|
634
|
+
failedResourceCount: failedResources.length,
|
|
635
|
+
url: location.href,
|
|
636
|
+
sessionId: this.sessionId,
|
|
637
|
+
errorCount: this.errorCount,
|
|
638
|
+
networkFailCount: this.networkFailCount,
|
|
639
|
+
longTaskCount: this.longTaskCount,
|
|
640
|
+
navigationCount: this.navigationCount,
|
|
641
|
+
sessionDurationMs: Math.round(performance.now() - this.pageLoadTime),
|
|
642
|
+
onLine: navigator.onLine
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
// ── Internal ─────────────────────────────────────────────
|
|
647
|
+
enqueue(item) {
|
|
648
|
+
item.source = this.config.source;
|
|
649
|
+
item.platform = this.config.platform;
|
|
650
|
+
item.version = this.config.version;
|
|
651
|
+
this.queue.push(item);
|
|
652
|
+
if (this.queue.length >= this.config.maxBatchSize) {
|
|
653
|
+
this.flush();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
async flush() {
|
|
657
|
+
if (this.queue.length === 0) return;
|
|
658
|
+
const batch = this.queue.splice(0, this.config.maxBatchSize);
|
|
659
|
+
const doFetch = this.originalFetch || fetch;
|
|
660
|
+
const promises = batch.map(
|
|
661
|
+
(item) => doFetch(this.config.ingestUrl, {
|
|
662
|
+
method: "POST",
|
|
663
|
+
headers: {
|
|
664
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
665
|
+
"Content-Type": "application/json"
|
|
666
|
+
},
|
|
667
|
+
body: JSON.stringify(item)
|
|
668
|
+
}).catch(() => {
|
|
669
|
+
})
|
|
670
|
+
);
|
|
671
|
+
await Promise.allSettled(promises);
|
|
672
|
+
}
|
|
673
|
+
generateSessionId() {
|
|
674
|
+
return "ses_" + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
675
|
+
}
|
|
676
|
+
sanitizeHeaders(headers) {
|
|
677
|
+
if (!headers) return void 0;
|
|
678
|
+
const result = {};
|
|
679
|
+
const h = headers instanceof Headers ? headers : new Headers(headers);
|
|
680
|
+
h.forEach((value, key) => {
|
|
681
|
+
const lower = key.toLowerCase();
|
|
682
|
+
if (lower.includes("auth") || lower.includes("cookie") || lower.includes("token")) {
|
|
683
|
+
result[key] = "[REDACTED]";
|
|
684
|
+
} else {
|
|
685
|
+
result[key] = value;
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
destroy() {
|
|
691
|
+
if (this.batchTimer) clearInterval(this.batchTimer);
|
|
692
|
+
if (this.metricsTimer) clearInterval(this.metricsTimer);
|
|
693
|
+
if (this.config.captureConsole) {
|
|
694
|
+
for (const [method, original] of Object.entries(this.originalConsole)) {
|
|
695
|
+
if (original) console[method] = original;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (this.originalFetch) window.fetch = this.originalFetch;
|
|
699
|
+
if (this.originalXhrOpen) XMLHttpRequest.prototype.open = this.originalXhrOpen;
|
|
700
|
+
if (this.originalXhrSend) XMLHttpRequest.prototype.send = this.originalXhrSend;
|
|
701
|
+
if (this.originalWsConstructor) window.WebSocket = this.originalWsConstructor;
|
|
702
|
+
this.flush();
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
706
|
+
0 && (module.exports = {
|
|
707
|
+
DebuggerSDK
|
|
708
|
+
});
|