@logspace/sdk 1.0.2 → 1.1.0
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/logspace.esm.js +1130 -702
- package/logspace.esm.js.map +1 -1
- package/logspace.iife.js +1 -1
- package/logspace.iife.js.map +1 -1
- package/logspace.umd.js +1 -1
- package/logspace.umd.js.map +1 -1
- package/package.json +5 -3
- package/sdk/capture/base.d.ts +5 -3
- package/sdk/capture/sse.d.ts +0 -3
- package/sdk/capture/websocket.d.ts +0 -3
- package/sdk/index.d.ts +25 -1
- package/sdk/storage/indexed-db.d.ts +4 -0
- package/sdk/types.d.ts +15 -6
- package/shared/types.d.ts +108 -1
- package/shared/utils.d.ts +8 -1
- package/README.md +0 -269
package/logspace.esm.js
CHANGED
|
@@ -27,6 +27,50 @@ function safeStringify(value) {
|
|
|
27
27
|
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
28
28
|
if (value === null) return "null";
|
|
29
29
|
if (value === void 0) return "undefined";
|
|
30
|
+
if (typeof value === "function") {
|
|
31
|
+
const name = value.name || "anonymous";
|
|
32
|
+
return `[Function: ${name}]`;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "symbol") {
|
|
35
|
+
return value.toString();
|
|
36
|
+
}
|
|
37
|
+
if (value instanceof Error) {
|
|
38
|
+
return JSON.stringify({
|
|
39
|
+
name: value.name,
|
|
40
|
+
message: value.message,
|
|
41
|
+
stack: value.stack
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (typeof FormData !== "undefined" && value instanceof FormData) {
|
|
45
|
+
const formDataObj = {};
|
|
46
|
+
value.forEach((val, key) => {
|
|
47
|
+
if (typeof Blob !== "undefined" && val instanceof Blob) {
|
|
48
|
+
const blobVal = val;
|
|
49
|
+
const fileInfo = typeof File !== "undefined" && val instanceof File ? `File(${val.name}, ${blobVal.size}b)` : `Blob(${blobVal.size}b)`;
|
|
50
|
+
if (formDataObj[key]) {
|
|
51
|
+
const existing = formDataObj[key];
|
|
52
|
+
formDataObj[key] = Array.isArray(existing) ? [...existing, fileInfo] : [existing, fileInfo];
|
|
53
|
+
} else {
|
|
54
|
+
formDataObj[key] = fileInfo;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const strVal = String(val).length > 1e3 ? String(val).substring(0, 1e3) + "..." : String(val);
|
|
58
|
+
if (formDataObj[key]) {
|
|
59
|
+
const existing = formDataObj[key];
|
|
60
|
+
formDataObj[key] = Array.isArray(existing) ? [...existing, strVal] : [existing, strVal];
|
|
61
|
+
} else {
|
|
62
|
+
formDataObj[key] = strVal;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return JSON.stringify(formDataObj);
|
|
67
|
+
}
|
|
68
|
+
if (typeof Blob !== "undefined" && value instanceof Blob) {
|
|
69
|
+
if (value instanceof File) {
|
|
70
|
+
return `File(${value.name}, ${value.size}b, ${value.type})`;
|
|
71
|
+
}
|
|
72
|
+
return `Blob(${value.size}b, ${value.type})`;
|
|
73
|
+
}
|
|
30
74
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
31
75
|
return JSON.stringify(value, (key, val) => {
|
|
32
76
|
if (typeof val === "object" && val !== null) {
|
|
@@ -65,6 +109,11 @@ function maskSensitiveData(text) {
|
|
|
65
109
|
pattern: /\b(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}\b/g,
|
|
66
110
|
replacement: "[PHONE_REDACTED]"
|
|
67
111
|
},
|
|
112
|
+
// Passwords in plain text logs (e.g., "Password: value", "password: value")
|
|
113
|
+
{
|
|
114
|
+
pattern: /\b(password|pwd|passwd)\s*[:=]\s*[^\s,;}\]]+/gi,
|
|
115
|
+
replacement: "$1: [REDACTED]"
|
|
116
|
+
},
|
|
68
117
|
// Passwords in JSON (case insensitive)
|
|
69
118
|
{
|
|
70
119
|
pattern: /"password"\s*:\s*"[^"]*"/gi,
|
|
@@ -74,7 +123,12 @@ function maskSensitiveData(text) {
|
|
|
74
123
|
pattern: /'password'\s*:\s*'[^']*'/gi,
|
|
75
124
|
replacement: "'password': '[REDACTED]'"
|
|
76
125
|
},
|
|
77
|
-
// API keys
|
|
126
|
+
// API keys (common prefixes: sk_, pk_, api_key_, etc.)
|
|
127
|
+
{
|
|
128
|
+
pattern: /\b(?:sk|pk|api|auth|secret)_(?:live|test|prod|dev)?_[A-Za-z0-9_-]{10,}/g,
|
|
129
|
+
replacement: "[API_KEY_REDACTED]"
|
|
130
|
+
},
|
|
131
|
+
// API keys and tokens in JSON
|
|
78
132
|
{
|
|
79
133
|
pattern: /"(?:api_?key|token|secret|auth_?token|access_?token)"\s*:\s*"[^"]*"/gi,
|
|
80
134
|
replacement: '"$1": "[REDACTED]"'
|
|
@@ -92,9 +146,9 @@ function maskSensitiveData(text) {
|
|
|
92
146
|
pattern: /Authorization:\s*Basic\s+[^\s]+/gi,
|
|
93
147
|
replacement: "Authorization: Basic [REDACTED]"
|
|
94
148
|
},
|
|
95
|
-
// JWT tokens (
|
|
149
|
+
// JWT tokens - matches eyJ... pattern (base64url encoded JSON starting with {"alg" or {"typ")
|
|
96
150
|
{
|
|
97
|
-
pattern: /\
|
|
151
|
+
pattern: /\beyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\b/g,
|
|
98
152
|
replacement: "[JWT_TOKEN_REDACTED]"
|
|
99
153
|
}
|
|
100
154
|
];
|
|
@@ -103,6 +157,52 @@ function maskSensitiveData(text) {
|
|
|
103
157
|
}
|
|
104
158
|
return masked;
|
|
105
159
|
}
|
|
160
|
+
const SENSITIVE_LABELS = /^(password|pwd|passwd|secret|token|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key|secret[_-]?key)\s*[:=]?\s*$/i;
|
|
161
|
+
function maskConsoleArgs(args) {
|
|
162
|
+
const result2 = [];
|
|
163
|
+
for (let i = 0; i < args.length; i++) {
|
|
164
|
+
const arg = args[i];
|
|
165
|
+
const nextArg = args[i + 1];
|
|
166
|
+
if (typeof arg === "string" && SENSITIVE_LABELS.test(arg.trim()) && nextArg !== void 0) {
|
|
167
|
+
result2.push(arg);
|
|
168
|
+
result2.push("[REDACTED]");
|
|
169
|
+
i++;
|
|
170
|
+
} else if (arg !== void 0) {
|
|
171
|
+
result2.push(maskSensitiveData(arg));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result2;
|
|
175
|
+
}
|
|
176
|
+
function maskHeaders(headers, additionalHeaders) {
|
|
177
|
+
if (!headers || typeof headers !== "object") {
|
|
178
|
+
return headers;
|
|
179
|
+
}
|
|
180
|
+
const maskedHeaders = {};
|
|
181
|
+
const defaultSensitiveHeaders = [
|
|
182
|
+
"authorization",
|
|
183
|
+
"cookie",
|
|
184
|
+
"set-cookie",
|
|
185
|
+
"x-api-key",
|
|
186
|
+
"x-auth-token",
|
|
187
|
+
"x-access-token",
|
|
188
|
+
"x-csrf-token",
|
|
189
|
+
"csrf-token"
|
|
190
|
+
];
|
|
191
|
+
const allSensitiveHeaders = [...defaultSensitiveHeaders, ...(additionalHeaders || []).map((h) => h.toLowerCase())];
|
|
192
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
193
|
+
let maskedValue = value;
|
|
194
|
+
const isSensitiveHeader = allSensitiveHeaders.some(
|
|
195
|
+
(sensitiveName) => name.toLowerCase().includes(sensitiveName.toLowerCase())
|
|
196
|
+
);
|
|
197
|
+
if (isSensitiveHeader) {
|
|
198
|
+
maskedValue = "[REDACTED]";
|
|
199
|
+
} else {
|
|
200
|
+
maskedValue = maskSensitiveData(value);
|
|
201
|
+
}
|
|
202
|
+
maskedHeaders[name] = maskedValue;
|
|
203
|
+
}
|
|
204
|
+
return maskedHeaders;
|
|
205
|
+
}
|
|
106
206
|
function isBrowser() {
|
|
107
207
|
return typeof window !== "undefined" && typeof document !== "undefined";
|
|
108
208
|
}
|
|
@@ -239,7 +339,59 @@ function createLogEntry(type, data, severity = "info") {
|
|
|
239
339
|
severity
|
|
240
340
|
};
|
|
241
341
|
}
|
|
342
|
+
function applyPrivacy(log, privacy) {
|
|
343
|
+
if (!privacy.maskSensitiveData) {
|
|
344
|
+
return log;
|
|
345
|
+
}
|
|
346
|
+
const masked = { ...log, data: { ...log.data } };
|
|
347
|
+
switch (log.type) {
|
|
348
|
+
case "network":
|
|
349
|
+
if (masked.data.requestHeaders) {
|
|
350
|
+
masked.data.requestHeaders = maskHeaders(masked.data.requestHeaders, privacy.redactHeaders);
|
|
351
|
+
}
|
|
352
|
+
if (masked.data.responseHeaders) {
|
|
353
|
+
masked.data.responseHeaders = maskHeaders(masked.data.responseHeaders, privacy.redactHeaders);
|
|
354
|
+
}
|
|
355
|
+
if (masked.data.requestBody) {
|
|
356
|
+
masked.data.requestBody = maskSensitiveData(masked.data.requestBody);
|
|
357
|
+
}
|
|
358
|
+
if (masked.data.responseBody) {
|
|
359
|
+
masked.data.responseBody = maskSensitiveData(masked.data.responseBody);
|
|
360
|
+
}
|
|
361
|
+
break;
|
|
362
|
+
case "console":
|
|
363
|
+
if (masked.data.args && Array.isArray(masked.data.args)) {
|
|
364
|
+
masked.data.args = maskConsoleArgs(masked.data.args);
|
|
365
|
+
}
|
|
366
|
+
break;
|
|
367
|
+
case "websocket":
|
|
368
|
+
case "sse":
|
|
369
|
+
if (masked.data.message) {
|
|
370
|
+
masked.data.message = maskSensitiveData(masked.data.message);
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
case "storage":
|
|
374
|
+
if (masked.data.value) {
|
|
375
|
+
masked.data.value = maskSensitiveData(masked.data.value);
|
|
376
|
+
}
|
|
377
|
+
if (masked.data.oldValue) {
|
|
378
|
+
masked.data.oldValue = maskSensitiveData(masked.data.oldValue);
|
|
379
|
+
}
|
|
380
|
+
break;
|
|
381
|
+
case "interaction":
|
|
382
|
+
if (masked.data.value) {
|
|
383
|
+
masked.data.value = maskSensitiveData(masked.data.value);
|
|
384
|
+
}
|
|
385
|
+
if (masked.data.element?.text) {
|
|
386
|
+
masked.data.element.text = maskSensitiveData(masked.data.element.text);
|
|
387
|
+
}
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
return masked;
|
|
391
|
+
}
|
|
242
392
|
let originalConsole = null;
|
|
393
|
+
const timerMap = /* @__PURE__ */ new Map();
|
|
394
|
+
const counterMap = /* @__PURE__ */ new Map();
|
|
243
395
|
function shouldIgnoreMessage(args) {
|
|
244
396
|
if (!args || args.length === 0) return false;
|
|
245
397
|
for (const arg of args) {
|
|
@@ -275,7 +427,15 @@ const consoleCapture = {
|
|
|
275
427
|
info: console.info,
|
|
276
428
|
warn: console.warn,
|
|
277
429
|
error: console.error,
|
|
278
|
-
debug: console.debug
|
|
430
|
+
debug: console.debug,
|
|
431
|
+
time: console.time,
|
|
432
|
+
timeEnd: console.timeEnd,
|
|
433
|
+
timeLog: console.timeLog,
|
|
434
|
+
trace: console.trace,
|
|
435
|
+
table: console.table,
|
|
436
|
+
count: console.count,
|
|
437
|
+
countReset: console.countReset,
|
|
438
|
+
assert: console.assert
|
|
279
439
|
};
|
|
280
440
|
levels.forEach((level) => {
|
|
281
441
|
console[level] = function(...args) {
|
|
@@ -303,6 +463,124 @@ const consoleCapture = {
|
|
|
303
463
|
handler2(log);
|
|
304
464
|
};
|
|
305
465
|
});
|
|
466
|
+
console.time = function(label = "default") {
|
|
467
|
+
originalConsole.time.call(console, label);
|
|
468
|
+
timerMap.set(label, performance.now());
|
|
469
|
+
};
|
|
470
|
+
console.timeLog = function(label = "default", ...args) {
|
|
471
|
+
originalConsole.timeLog.apply(console, [label, ...args]);
|
|
472
|
+
const startTime = timerMap.get(label);
|
|
473
|
+
if (startTime === void 0) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const duration = performance.now() - startTime;
|
|
477
|
+
const serializedArgs = args.map((arg) => safeStringify(arg));
|
|
478
|
+
const log = createLogEntry(
|
|
479
|
+
"console",
|
|
480
|
+
{
|
|
481
|
+
level: "timeLog",
|
|
482
|
+
args: [`${label}: ${duration.toFixed(3)}ms`, ...serializedArgs],
|
|
483
|
+
timerLabel: label,
|
|
484
|
+
duration
|
|
485
|
+
},
|
|
486
|
+
"info"
|
|
487
|
+
);
|
|
488
|
+
handler2(log);
|
|
489
|
+
};
|
|
490
|
+
console.timeEnd = function(label = "default") {
|
|
491
|
+
originalConsole.timeEnd.call(console, label);
|
|
492
|
+
const startTime = timerMap.get(label);
|
|
493
|
+
if (startTime === void 0) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const duration = performance.now() - startTime;
|
|
497
|
+
timerMap.delete(label);
|
|
498
|
+
const log = createLogEntry(
|
|
499
|
+
"console",
|
|
500
|
+
{
|
|
501
|
+
level: "timeEnd",
|
|
502
|
+
args: [`${label}: ${duration.toFixed(3)}ms`],
|
|
503
|
+
timerLabel: label,
|
|
504
|
+
duration
|
|
505
|
+
},
|
|
506
|
+
"info"
|
|
507
|
+
);
|
|
508
|
+
handler2(log);
|
|
509
|
+
};
|
|
510
|
+
console.trace = function(...args) {
|
|
511
|
+
originalConsole.trace.apply(console, args);
|
|
512
|
+
const serializedArgs = args.map((arg) => safeStringify(arg));
|
|
513
|
+
const stackTrace = new Error().stack || "";
|
|
514
|
+
const log = createLogEntry(
|
|
515
|
+
"console",
|
|
516
|
+
{
|
|
517
|
+
level: "trace",
|
|
518
|
+
args: serializedArgs.length > 0 ? serializedArgs : ["Trace"],
|
|
519
|
+
stackTrace
|
|
520
|
+
},
|
|
521
|
+
"info"
|
|
522
|
+
);
|
|
523
|
+
handler2(log);
|
|
524
|
+
};
|
|
525
|
+
console.table = function(data, columns) {
|
|
526
|
+
originalConsole.table.call(console, data, columns);
|
|
527
|
+
const log = createLogEntry(
|
|
528
|
+
"console",
|
|
529
|
+
{
|
|
530
|
+
level: "table",
|
|
531
|
+
args: [safeStringify(data)],
|
|
532
|
+
columns
|
|
533
|
+
},
|
|
534
|
+
"info"
|
|
535
|
+
);
|
|
536
|
+
handler2(log);
|
|
537
|
+
};
|
|
538
|
+
console.count = function(label = "default") {
|
|
539
|
+
originalConsole.count.call(console, label);
|
|
540
|
+
const count = (counterMap.get(label) || 0) + 1;
|
|
541
|
+
counterMap.set(label, count);
|
|
542
|
+
const log = createLogEntry(
|
|
543
|
+
"console",
|
|
544
|
+
{
|
|
545
|
+
level: "count",
|
|
546
|
+
args: [`${label}: ${count}`],
|
|
547
|
+
counterLabel: label,
|
|
548
|
+
count
|
|
549
|
+
},
|
|
550
|
+
"info"
|
|
551
|
+
);
|
|
552
|
+
handler2(log);
|
|
553
|
+
};
|
|
554
|
+
console.countReset = function(label = "default") {
|
|
555
|
+
originalConsole.countReset.call(console, label);
|
|
556
|
+
counterMap.set(label, 0);
|
|
557
|
+
const log = createLogEntry(
|
|
558
|
+
"console",
|
|
559
|
+
{
|
|
560
|
+
level: "countReset",
|
|
561
|
+
args: [`${label}: 0`],
|
|
562
|
+
counterLabel: label
|
|
563
|
+
},
|
|
564
|
+
"info"
|
|
565
|
+
);
|
|
566
|
+
handler2(log);
|
|
567
|
+
};
|
|
568
|
+
console.assert = function(condition, ...args) {
|
|
569
|
+
originalConsole.assert.apply(console, [condition, ...args]);
|
|
570
|
+
if (!condition) {
|
|
571
|
+
const serializedArgs = args.map((arg) => safeStringify(arg));
|
|
572
|
+
const log = createLogEntry(
|
|
573
|
+
"console",
|
|
574
|
+
{
|
|
575
|
+
level: "assert",
|
|
576
|
+
args: ["Assertion failed:", ...serializedArgs],
|
|
577
|
+
stackTrace: new Error().stack
|
|
578
|
+
},
|
|
579
|
+
"warn"
|
|
580
|
+
);
|
|
581
|
+
handler2(log);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
306
584
|
},
|
|
307
585
|
uninstall() {
|
|
308
586
|
if (!isBrowser() || !originalConsole) return;
|
|
@@ -311,10 +589,21 @@ const consoleCapture = {
|
|
|
311
589
|
console.warn = originalConsole.warn;
|
|
312
590
|
console.error = originalConsole.error;
|
|
313
591
|
console.debug = originalConsole.debug;
|
|
592
|
+
console.time = originalConsole.time;
|
|
593
|
+
console.timeEnd = originalConsole.timeEnd;
|
|
594
|
+
console.timeLog = originalConsole.timeLog;
|
|
595
|
+
console.trace = originalConsole.trace;
|
|
596
|
+
console.table = originalConsole.table;
|
|
597
|
+
console.count = originalConsole.count;
|
|
598
|
+
console.countReset = originalConsole.countReset;
|
|
599
|
+
console.assert = originalConsole.assert;
|
|
600
|
+
timerMap.clear();
|
|
601
|
+
counterMap.clear();
|
|
314
602
|
originalConsole = null;
|
|
315
603
|
}
|
|
316
604
|
};
|
|
317
|
-
const
|
|
605
|
+
const DEFAULT_MAX_BODY_SIZE = 10 * 1024;
|
|
606
|
+
let maxBodySize$2 = DEFAULT_MAX_BODY_SIZE;
|
|
318
607
|
let originalFetch = null;
|
|
319
608
|
let OriginalXHR = null;
|
|
320
609
|
let excludeUrls$3 = [];
|
|
@@ -409,12 +698,13 @@ function interceptSSEStream(response, url, handler2) {
|
|
|
409
698
|
}
|
|
410
699
|
const networkCapture = {
|
|
411
700
|
name: "network",
|
|
412
|
-
install(handler2, privacy) {
|
|
701
|
+
install(handler2, privacy, _onActivity, limits) {
|
|
413
702
|
if (!isBrowser()) return;
|
|
414
703
|
if (originalFetch) return;
|
|
415
704
|
captureHandler$1 = handler2;
|
|
416
705
|
excludeUrls$3 = privacy.excludeUrls || [];
|
|
417
706
|
const blockBodies = privacy.blockNetworkBodies || [];
|
|
707
|
+
maxBodySize$2 = limits?.maxNetworkBodySize ? limits.maxNetworkBodySize * 1024 : DEFAULT_MAX_BODY_SIZE;
|
|
418
708
|
originalFetch = window.fetch;
|
|
419
709
|
window.fetch = async function(...args) {
|
|
420
710
|
const startTime = performance.now();
|
|
@@ -444,7 +734,7 @@ const networkCapture = {
|
|
|
444
734
|
let requestBody;
|
|
445
735
|
if (!shouldBlockBody && options.body) {
|
|
446
736
|
const bodyStr = safeStringify(options.body);
|
|
447
|
-
requestBody = bodyStr.length >
|
|
737
|
+
requestBody = bodyStr.length > maxBodySize$2 ? bodyStr.substring(0, maxBodySize$2) + "..." : bodyStr;
|
|
448
738
|
}
|
|
449
739
|
try {
|
|
450
740
|
const response = await originalFetch(...args);
|
|
@@ -455,9 +745,9 @@ const networkCapture = {
|
|
|
455
745
|
let responseBody;
|
|
456
746
|
let responseSize;
|
|
457
747
|
try {
|
|
458
|
-
if (!shouldBlockBody && (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html"))) {
|
|
748
|
+
if (!shouldBlockBody && (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html") || contentType.includes("text/x-component"))) {
|
|
459
749
|
const text = await clonedResponse.text();
|
|
460
|
-
responseBody = text.length >
|
|
750
|
+
responseBody = text.length > maxBodySize$2 ? text.substring(0, maxBodySize$2) + "..." : text;
|
|
461
751
|
responseSize = new Blob([responseBody]).size;
|
|
462
752
|
} else if (contentLength) {
|
|
463
753
|
responseSize = parseInt(contentLength, 10);
|
|
@@ -478,7 +768,7 @@ const networkCapture = {
|
|
|
478
768
|
{
|
|
479
769
|
method,
|
|
480
770
|
url,
|
|
481
|
-
|
|
771
|
+
statusCode: response.status,
|
|
482
772
|
requestHeaders,
|
|
483
773
|
responseHeaders: Object.fromEntries(response.headers.entries()),
|
|
484
774
|
requestBody,
|
|
@@ -524,7 +814,7 @@ const networkCapture = {
|
|
|
524
814
|
{
|
|
525
815
|
method,
|
|
526
816
|
url,
|
|
527
|
-
|
|
817
|
+
statusCode: 0,
|
|
528
818
|
requestHeaders,
|
|
529
819
|
responseHeaders: {},
|
|
530
820
|
requestBody,
|
|
@@ -563,7 +853,7 @@ const networkCapture = {
|
|
|
563
853
|
xhr.send = function(body) {
|
|
564
854
|
if (body && !shouldExcludeUrl(url, excludeUrls$3)) {
|
|
565
855
|
const bodyStr = safeStringify(body);
|
|
566
|
-
requestBody = bodyStr.length >
|
|
856
|
+
requestBody = bodyStr.length > maxBodySize$2 ? bodyStr.substring(0, maxBodySize$2) + "..." : bodyStr;
|
|
567
857
|
}
|
|
568
858
|
return originalSend.apply(this, [body]);
|
|
569
859
|
};
|
|
@@ -596,7 +886,7 @@ const networkCapture = {
|
|
|
596
886
|
}
|
|
597
887
|
let responseBody;
|
|
598
888
|
let responseSize;
|
|
599
|
-
if (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html")) {
|
|
889
|
+
if (contentType.includes("application/json") || contentType.includes("text/plain") || contentType.includes("text/html") || contentType.includes("text/x-component")) {
|
|
600
890
|
try {
|
|
601
891
|
let responseText;
|
|
602
892
|
if (xhr.responseType === "" || xhr.responseType === "text") {
|
|
@@ -607,7 +897,7 @@ const networkCapture = {
|
|
|
607
897
|
responseText = "";
|
|
608
898
|
}
|
|
609
899
|
if (responseText) {
|
|
610
|
-
responseBody = responseText.length >
|
|
900
|
+
responseBody = responseText.length > maxBodySize$2 ? responseText.substring(0, maxBodySize$2) + "..." : responseText;
|
|
611
901
|
responseSize = new Blob([responseBody]).size;
|
|
612
902
|
}
|
|
613
903
|
} catch {
|
|
@@ -628,7 +918,7 @@ const networkCapture = {
|
|
|
628
918
|
{
|
|
629
919
|
method,
|
|
630
920
|
url,
|
|
631
|
-
|
|
921
|
+
statusCode: xhr.status,
|
|
632
922
|
requestHeaders,
|
|
633
923
|
responseHeaders,
|
|
634
924
|
requestBody,
|
|
@@ -684,10 +974,20 @@ const errorCapture = {
|
|
|
684
974
|
};
|
|
685
975
|
window.addEventListener("error", errorHandler$1, true);
|
|
686
976
|
rejectionHandler = (event) => {
|
|
977
|
+
let reasonMessage;
|
|
978
|
+
if (event.reason instanceof Error) {
|
|
979
|
+
reasonMessage = event.reason.message || event.reason.toString();
|
|
980
|
+
} else if (typeof event.reason === "string") {
|
|
981
|
+
reasonMessage = event.reason;
|
|
982
|
+
} else if (event.reason && typeof event.reason === "object") {
|
|
983
|
+
reasonMessage = event.reason.message || safeStringify(event.reason);
|
|
984
|
+
} else {
|
|
985
|
+
reasonMessage = safeStringify(event.reason);
|
|
986
|
+
}
|
|
687
987
|
const log = createLogEntry(
|
|
688
988
|
"error",
|
|
689
989
|
{
|
|
690
|
-
message: `Unhandled Promise Rejection: ${
|
|
990
|
+
message: `Unhandled Promise Rejection: ${reasonMessage}`,
|
|
691
991
|
stack: event.reason?.stack || "",
|
|
692
992
|
context: {},
|
|
693
993
|
isUncaught: true
|
|
@@ -712,6 +1012,7 @@ const errorCapture = {
|
|
|
712
1012
|
};
|
|
713
1013
|
let OriginalWebSocket = null;
|
|
714
1014
|
let excludeUrls$2 = [];
|
|
1015
|
+
let maxBodySize$1 = 10 * 1024;
|
|
715
1016
|
const wsThrottleMap = /* @__PURE__ */ new Map();
|
|
716
1017
|
const THROTTLE_WINDOW = 1e3;
|
|
717
1018
|
const MIN_THROTTLE_MS = 100;
|
|
@@ -747,12 +1048,15 @@ function shouldCaptureMessage$1(ws, direction) {
|
|
|
747
1048
|
}
|
|
748
1049
|
return shouldCapture;
|
|
749
1050
|
}
|
|
1051
|
+
let activityCallback$1;
|
|
750
1052
|
const websocketCapture = {
|
|
751
1053
|
name: "websocket",
|
|
752
|
-
install(handler2, privacy) {
|
|
1054
|
+
install(handler2, privacy, onActivity, limits) {
|
|
753
1055
|
if (!isBrowser()) return;
|
|
754
1056
|
if (OriginalWebSocket) return;
|
|
755
1057
|
excludeUrls$2 = privacy.excludeUrls || [];
|
|
1058
|
+
activityCallback$1 = onActivity;
|
|
1059
|
+
maxBodySize$1 = limits?.maxNetworkBodySize ? limits.maxNetworkBodySize * 1024 : 10 * 1024;
|
|
756
1060
|
OriginalWebSocket = window.WebSocket;
|
|
757
1061
|
window.WebSocket = function(url, protocols) {
|
|
758
1062
|
const ws = new OriginalWebSocket(url, protocols);
|
|
@@ -770,9 +1074,10 @@ const websocketCapture = {
|
|
|
770
1074
|
handler2(log);
|
|
771
1075
|
});
|
|
772
1076
|
ws.addEventListener("message", (event) => {
|
|
1077
|
+
activityCallback$1?.();
|
|
773
1078
|
if (!shouldCaptureMessage$1(ws, "received")) return;
|
|
774
1079
|
const isBinary = event.data instanceof ArrayBuffer || event.data instanceof Blob;
|
|
775
|
-
const { content, size } = truncateMessage(event.data, isBinary);
|
|
1080
|
+
const { content, size } = truncateMessage(event.data, isBinary, maxBodySize$1);
|
|
776
1081
|
const log = createLogEntry("websocket", {
|
|
777
1082
|
url: wsUrl,
|
|
778
1083
|
event: "message",
|
|
@@ -787,9 +1092,10 @@ const websocketCapture = {
|
|
|
787
1092
|
});
|
|
788
1093
|
const originalSend = ws.send.bind(ws);
|
|
789
1094
|
ws.send = function(data) {
|
|
1095
|
+
activityCallback$1?.();
|
|
790
1096
|
if (shouldCaptureMessage$1(ws, "sent")) {
|
|
791
1097
|
const isBinary = data instanceof ArrayBuffer || data instanceof Blob;
|
|
792
|
-
const { content, size } = truncateMessage(data, isBinary);
|
|
1098
|
+
const { content, size } = truncateMessage(data, isBinary, maxBodySize$1);
|
|
793
1099
|
const log = createLogEntry("websocket", {
|
|
794
1100
|
url: wsUrl,
|
|
795
1101
|
event: "message",
|
|
@@ -852,6 +1158,7 @@ const websocketCapture = {
|
|
|
852
1158
|
};
|
|
853
1159
|
let OriginalEventSource = null;
|
|
854
1160
|
let excludeUrls$1 = [];
|
|
1161
|
+
let maxBodySize = 10 * 1024;
|
|
855
1162
|
const sseThrottleMap = /* @__PURE__ */ new Map();
|
|
856
1163
|
const SSE_THROTTLE_MS = 100;
|
|
857
1164
|
function shouldCaptureMessage(eventSource) {
|
|
@@ -868,13 +1175,16 @@ function shouldCaptureMessage(eventSource) {
|
|
|
868
1175
|
}
|
|
869
1176
|
return false;
|
|
870
1177
|
}
|
|
1178
|
+
let activityCallback;
|
|
871
1179
|
const sseCapture = {
|
|
872
1180
|
name: "sse",
|
|
873
|
-
install(handler2, privacy) {
|
|
1181
|
+
install(handler2, privacy, onActivity, limits) {
|
|
874
1182
|
if (!isBrowser()) return;
|
|
875
1183
|
if (!window.EventSource) return;
|
|
876
1184
|
if (OriginalEventSource) return;
|
|
877
1185
|
excludeUrls$1 = privacy.excludeUrls || [];
|
|
1186
|
+
activityCallback = onActivity;
|
|
1187
|
+
maxBodySize = limits?.maxNetworkBodySize ? limits.maxNetworkBodySize * 1024 : 10 * 1024;
|
|
878
1188
|
OriginalEventSource = window.EventSource;
|
|
879
1189
|
window.EventSource = function(url, eventSourceInitDict) {
|
|
880
1190
|
const eventSource = new OriginalEventSource(url, eventSourceInitDict);
|
|
@@ -892,8 +1202,9 @@ const sseCapture = {
|
|
|
892
1202
|
handler2(log);
|
|
893
1203
|
});
|
|
894
1204
|
eventSource.addEventListener("message", (event) => {
|
|
1205
|
+
activityCallback?.();
|
|
895
1206
|
if (!shouldCaptureMessage(eventSource)) return;
|
|
896
|
-
const { content, size } = truncateMessage(event.data, false);
|
|
1207
|
+
const { content, size } = truncateMessage(event.data, false, maxBodySize);
|
|
897
1208
|
const log = createLogEntry("sse", {
|
|
898
1209
|
url: sseUrl,
|
|
899
1210
|
event: "message",
|
|
@@ -971,10 +1282,33 @@ const storageCapture = {
|
|
|
971
1282
|
if (originalStorage) return;
|
|
972
1283
|
captureHandler = handler2;
|
|
973
1284
|
originalStorage = {
|
|
1285
|
+
getItem: Storage.prototype.getItem,
|
|
974
1286
|
setItem: Storage.prototype.setItem,
|
|
975
1287
|
removeItem: Storage.prototype.removeItem,
|
|
976
1288
|
clear: Storage.prototype.clear
|
|
977
1289
|
};
|
|
1290
|
+
Storage.prototype.getItem = function(key) {
|
|
1291
|
+
const isSessionStorage = this === sessionStorage;
|
|
1292
|
+
if (isLogging || key.startsWith("__logspace")) {
|
|
1293
|
+
return originalStorage.getItem.call(this, key);
|
|
1294
|
+
}
|
|
1295
|
+
const value = originalStorage.getItem.call(this, key);
|
|
1296
|
+
try {
|
|
1297
|
+
isLogging = true;
|
|
1298
|
+
const storageType = isSessionStorage ? "sessionStorage" : "localStorage";
|
|
1299
|
+
const log = createLogEntry("storage", {
|
|
1300
|
+
storageType,
|
|
1301
|
+
operation: "getItem",
|
|
1302
|
+
key,
|
|
1303
|
+
value: value ? truncateValue(value).content : null,
|
|
1304
|
+
valueSize: value ? new Blob([value]).size : 0
|
|
1305
|
+
});
|
|
1306
|
+
handler2(log);
|
|
1307
|
+
} finally {
|
|
1308
|
+
isLogging = false;
|
|
1309
|
+
}
|
|
1310
|
+
return value;
|
|
1311
|
+
};
|
|
978
1312
|
Storage.prototype.setItem = function(key, value) {
|
|
979
1313
|
const isSessionStorage = this === sessionStorage;
|
|
980
1314
|
if (isLogging || key.startsWith("__logspace")) {
|
|
@@ -1063,6 +1397,7 @@ const storageCapture = {
|
|
|
1063
1397
|
uninstall() {
|
|
1064
1398
|
if (!isBrowser()) return;
|
|
1065
1399
|
if (originalStorage) {
|
|
1400
|
+
Storage.prototype.getItem = originalStorage.getItem;
|
|
1066
1401
|
Storage.prototype.setItem = originalStorage.setItem;
|
|
1067
1402
|
Storage.prototype.removeItem = originalStorage.removeItem;
|
|
1068
1403
|
Storage.prototype.clear = originalStorage.clear;
|
|
@@ -1200,6 +1535,10 @@ let fidObserver = null;
|
|
|
1200
1535
|
let clsObserver = null;
|
|
1201
1536
|
let fcpObserver = null;
|
|
1202
1537
|
let inpObserver = null;
|
|
1538
|
+
let visibilityHandler = null;
|
|
1539
|
+
let loadHandler = null;
|
|
1540
|
+
let sendVitalsTimeout = null;
|
|
1541
|
+
let navigationTimingTimeout = null;
|
|
1203
1542
|
const metrics = {
|
|
1204
1543
|
lcp: null,
|
|
1205
1544
|
fid: null,
|
|
@@ -1345,17 +1684,19 @@ const performanceCapture = {
|
|
|
1345
1684
|
inpObserver.observe({ type: "event", buffered: true, durationThreshold: 16 });
|
|
1346
1685
|
} catch {
|
|
1347
1686
|
}
|
|
1348
|
-
|
|
1687
|
+
visibilityHandler = () => {
|
|
1349
1688
|
if (document.visibilityState === "hidden") {
|
|
1350
1689
|
sendWebVitals();
|
|
1351
1690
|
}
|
|
1352
|
-
}
|
|
1353
|
-
window.addEventListener("
|
|
1354
|
-
|
|
1691
|
+
};
|
|
1692
|
+
window.addEventListener("visibilitychange", visibilityHandler);
|
|
1693
|
+
loadHandler = () => {
|
|
1694
|
+
navigationTimingTimeout = setTimeout(() => {
|
|
1355
1695
|
sendNavigationTiming();
|
|
1356
1696
|
}, 0);
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1697
|
+
};
|
|
1698
|
+
window.addEventListener("load", loadHandler);
|
|
1699
|
+
sendVitalsTimeout = setTimeout(() => {
|
|
1359
1700
|
sendWebVitals();
|
|
1360
1701
|
}, 1e4);
|
|
1361
1702
|
} catch {
|
|
@@ -1368,6 +1709,22 @@ const performanceCapture = {
|
|
|
1368
1709
|
observer.disconnect();
|
|
1369
1710
|
}
|
|
1370
1711
|
});
|
|
1712
|
+
if (visibilityHandler) {
|
|
1713
|
+
window.removeEventListener("visibilitychange", visibilityHandler);
|
|
1714
|
+
visibilityHandler = null;
|
|
1715
|
+
}
|
|
1716
|
+
if (loadHandler) {
|
|
1717
|
+
window.removeEventListener("load", loadHandler);
|
|
1718
|
+
loadHandler = null;
|
|
1719
|
+
}
|
|
1720
|
+
if (sendVitalsTimeout) {
|
|
1721
|
+
clearTimeout(sendVitalsTimeout);
|
|
1722
|
+
sendVitalsTimeout = null;
|
|
1723
|
+
}
|
|
1724
|
+
if (navigationTimingTimeout) {
|
|
1725
|
+
clearTimeout(navigationTimingTimeout);
|
|
1726
|
+
navigationTimingTimeout = null;
|
|
1727
|
+
}
|
|
1371
1728
|
lcpObserver = null;
|
|
1372
1729
|
fidObserver = null;
|
|
1373
1730
|
clsObserver = null;
|
|
@@ -1388,6 +1745,71 @@ let resourceObserver = null;
|
|
|
1388
1745
|
let excludeUrls = [];
|
|
1389
1746
|
const loggedResources = /* @__PURE__ */ new Set();
|
|
1390
1747
|
let handler$1 = null;
|
|
1748
|
+
const fontUrlToFamilyMap = /* @__PURE__ */ new Map();
|
|
1749
|
+
function buildFontFamilyMap() {
|
|
1750
|
+
if (!isBrowser()) return;
|
|
1751
|
+
try {
|
|
1752
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
1753
|
+
try {
|
|
1754
|
+
if (!sheet.cssRules) continue;
|
|
1755
|
+
for (const rule2 of Array.from(sheet.cssRules)) {
|
|
1756
|
+
if (rule2 instanceof CSSFontFaceRule) {
|
|
1757
|
+
const fontFamily = rule2.style.getPropertyValue("font-family").replace(/["']/g, "").trim();
|
|
1758
|
+
const src = rule2.style.getPropertyValue("src");
|
|
1759
|
+
if (fontFamily && src) {
|
|
1760
|
+
const urlMatches = src.matchAll(/url\(["']?([^"')]+)["']?\)/g);
|
|
1761
|
+
for (const match of urlMatches) {
|
|
1762
|
+
if (match[1]) {
|
|
1763
|
+
try {
|
|
1764
|
+
const absoluteUrl = new URL(match[1], sheet.href || window.location.href).href;
|
|
1765
|
+
fontUrlToFamilyMap.set(absoluteUrl, fontFamily);
|
|
1766
|
+
fontUrlToFamilyMap.set(match[1], fontFamily);
|
|
1767
|
+
} catch {
|
|
1768
|
+
fontUrlToFamilyMap.set(match[1], fontFamily);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
} catch {
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function getFontFamilyForUrl(url) {
|
|
1782
|
+
if (fontUrlToFamilyMap.has(url)) {
|
|
1783
|
+
return fontUrlToFamilyMap.get(url);
|
|
1784
|
+
}
|
|
1785
|
+
try {
|
|
1786
|
+
const urlObj = new URL(url);
|
|
1787
|
+
const filename = urlObj.pathname.split("/").pop();
|
|
1788
|
+
if (filename) {
|
|
1789
|
+
for (const [mapUrl, family] of fontUrlToFamilyMap.entries()) {
|
|
1790
|
+
if (mapUrl.endsWith(filename) || mapUrl.includes(filename)) {
|
|
1791
|
+
return family;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
} catch {
|
|
1796
|
+
}
|
|
1797
|
+
if ("fonts" in document) {
|
|
1798
|
+
try {
|
|
1799
|
+
const urlLower = url.toLowerCase();
|
|
1800
|
+
for (const font of document.fonts) {
|
|
1801
|
+
if (font.status === "loaded") {
|
|
1802
|
+
const family = font.family.replace(/["']/g, "");
|
|
1803
|
+
if (urlLower.includes(family.toLowerCase().replace(/\s+/g, ""))) {
|
|
1804
|
+
return family;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
} catch {
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return void 0;
|
|
1812
|
+
}
|
|
1391
1813
|
function getResourceType(entry) {
|
|
1392
1814
|
const initiatorType = entry.initiatorType || "other";
|
|
1393
1815
|
const url = entry.name;
|
|
@@ -1442,13 +1864,14 @@ function processResourceEntry(entry) {
|
|
|
1442
1864
|
} else if (duration > 1e3) {
|
|
1443
1865
|
severity = "warn";
|
|
1444
1866
|
}
|
|
1867
|
+
const fontFamily = resourceType === "font" ? getFontFamilyForUrl(entry.name) : void 0;
|
|
1445
1868
|
const log = createLogEntry(
|
|
1446
1869
|
"network",
|
|
1447
1870
|
{
|
|
1448
1871
|
method: "GET",
|
|
1449
1872
|
// Resource loads are always GET
|
|
1450
1873
|
url: entry.name,
|
|
1451
|
-
|
|
1874
|
+
statusCode: 200,
|
|
1452
1875
|
// Resources that successfully load return 200
|
|
1453
1876
|
resourceType,
|
|
1454
1877
|
initiatorType: entry.initiatorType,
|
|
@@ -1457,6 +1880,8 @@ function processResourceEntry(entry) {
|
|
|
1457
1880
|
decodedBodySize,
|
|
1458
1881
|
encodedBodySize,
|
|
1459
1882
|
cached,
|
|
1883
|
+
// Font family name (only for font resources)
|
|
1884
|
+
...fontFamily && { fontFamily },
|
|
1460
1885
|
// Timing breakdown
|
|
1461
1886
|
timing: {
|
|
1462
1887
|
dns: Math.round(entry.domainLookupEnd - entry.domainLookupStart),
|
|
@@ -1480,11 +1905,13 @@ const resourceCapture = {
|
|
|
1480
1905
|
handler$1 = captureHandler2;
|
|
1481
1906
|
excludeUrls = privacy.excludeUrls || [];
|
|
1482
1907
|
try {
|
|
1908
|
+
buildFontFamilyMap();
|
|
1483
1909
|
const existingResources = performance.getEntriesByType("resource");
|
|
1484
1910
|
existingResources.forEach((entry) => {
|
|
1485
1911
|
processResourceEntry(entry);
|
|
1486
1912
|
});
|
|
1487
1913
|
resourceObserver = new PerformanceObserver((list2) => {
|
|
1914
|
+
buildFontFamilyMap();
|
|
1488
1915
|
const entries = list2.getEntries();
|
|
1489
1916
|
entries.forEach((entry) => {
|
|
1490
1917
|
processResourceEntry(entry);
|
|
@@ -1507,6 +1934,7 @@ const resourceCapture = {
|
|
|
1507
1934
|
*/
|
|
1508
1935
|
reset() {
|
|
1509
1936
|
loggedResources.clear();
|
|
1937
|
+
fontUrlToFamilyMap.clear();
|
|
1510
1938
|
}
|
|
1511
1939
|
};
|
|
1512
1940
|
let originalPushState = null;
|
|
@@ -13758,7 +14186,7 @@ const state = {
|
|
|
13758
14186
|
stopFn: null,
|
|
13759
14187
|
startTime: null
|
|
13760
14188
|
};
|
|
13761
|
-
let currentTabId = null;
|
|
14189
|
+
let currentTabId$1 = null;
|
|
13762
14190
|
const DEFAULT_CONFIG$1 = {
|
|
13763
14191
|
maskAllInputs: false,
|
|
13764
14192
|
inlineStylesheet: true,
|
|
@@ -13786,7 +14214,7 @@ function startRRWebRecording(config2 = {}, onEvent, tabId) {
|
|
|
13786
14214
|
return false;
|
|
13787
14215
|
}
|
|
13788
14216
|
const mergedConfig = { ...DEFAULT_CONFIG$1, ...config2 };
|
|
13789
|
-
currentTabId = tabId || null;
|
|
14217
|
+
currentTabId$1 = tabId || null;
|
|
13790
14218
|
try {
|
|
13791
14219
|
state.events = [];
|
|
13792
14220
|
state.startTime = Date.now();
|
|
@@ -13795,7 +14223,7 @@ function startRRWebRecording(config2 = {}, onEvent, tabId) {
|
|
|
13795
14223
|
emit: (event, isCheckout) => {
|
|
13796
14224
|
const tabAwareEvent = {
|
|
13797
14225
|
...event,
|
|
13798
|
-
tabId: currentTabId || void 0
|
|
14226
|
+
tabId: currentTabId$1 || void 0
|
|
13799
14227
|
};
|
|
13800
14228
|
state.events.push(tabAwareEvent);
|
|
13801
14229
|
onEvent?.(tabAwareEvent);
|
|
@@ -13855,427 +14283,41 @@ function addRRWebCustomEvent(tag, payload) {
|
|
|
13855
14283
|
console.warn("[LogSpace] Failed to add custom event:", error);
|
|
13856
14284
|
}
|
|
13857
14285
|
}
|
|
13858
|
-
const
|
|
13859
|
-
const
|
|
13860
|
-
const
|
|
13861
|
-
|
|
13862
|
-
|
|
13863
|
-
|
|
13864
|
-
|
|
13865
|
-
|
|
13866
|
-
|
|
13867
|
-
|
|
13868
|
-
|
|
13869
|
-
|
|
13870
|
-
|
|
13871
|
-
this.currentSessionId = null;
|
|
13872
|
-
this.initialized = false;
|
|
13873
|
-
this.debug = false;
|
|
13874
|
-
this.tabId = this.generateTabId();
|
|
13875
|
-
}
|
|
13876
|
-
/**
|
|
13877
|
-
* Generate unique tab identifier
|
|
13878
|
-
*/
|
|
13879
|
-
generateTabId() {
|
|
13880
|
-
const timestamp = Date.now().toString(36);
|
|
13881
|
-
const random = Math.random().toString(36).substring(2, 8);
|
|
13882
|
-
return `tab_${timestamp}_${random}`;
|
|
13883
|
-
}
|
|
13884
|
-
/**
|
|
13885
|
-
* Check if BroadcastChannel is supported
|
|
13886
|
-
*/
|
|
13887
|
-
isSupported() {
|
|
13888
|
-
return typeof BroadcastChannel !== "undefined" && typeof localStorage !== "undefined";
|
|
13889
|
-
}
|
|
13890
|
-
/**
|
|
13891
|
-
* Initialize the multi-tab coordinator
|
|
13892
|
-
*/
|
|
13893
|
-
init(callbacks, debug = false) {
|
|
13894
|
-
if (this.initialized) return;
|
|
13895
|
-
if (!this.isSupported()) {
|
|
13896
|
-
if (debug) console.log("[LogSpace MultiTab] BroadcastChannel not supported, running single-tab mode");
|
|
13897
|
-
return;
|
|
13898
|
-
}
|
|
13899
|
-
this.callbacks = callbacks;
|
|
13900
|
-
this.debug = debug;
|
|
13901
|
-
this.initialized = true;
|
|
13902
|
-
this.channel = new BroadcastChannel(CHANNEL_NAME);
|
|
13903
|
-
this.channel.onmessage = (event) => this.handleMessage(event.data);
|
|
13904
|
-
window.addEventListener("beforeunload", () => this.handleUnload());
|
|
13905
|
-
this.electLeader();
|
|
13906
|
-
if (this.debug) {
|
|
13907
|
-
console.log("[LogSpace MultiTab] Initialized", { tabId: this.tabId, isLeader: this.isLeader });
|
|
13908
|
-
}
|
|
14286
|
+
const DB_NAME = "logspace-sdk-db";
|
|
14287
|
+
const DB_VERSION = 1;
|
|
14288
|
+
const STORES = {
|
|
14289
|
+
PENDING_SESSIONS: "pending_sessions",
|
|
14290
|
+
// Sessions waiting to be sent
|
|
14291
|
+
CURRENT_SESSION: "current_session"
|
|
14292
|
+
// Current active session backup
|
|
14293
|
+
};
|
|
14294
|
+
let db = null;
|
|
14295
|
+
let dbInitPromise = null;
|
|
14296
|
+
async function initDB() {
|
|
14297
|
+
if (dbInitPromise) {
|
|
14298
|
+
return dbInitPromise;
|
|
13909
14299
|
}
|
|
13910
|
-
|
|
13911
|
-
|
|
13912
|
-
*/
|
|
13913
|
-
electLeader() {
|
|
13914
|
-
const existingLeader = this.getLeaderState();
|
|
13915
|
-
if (existingLeader && !this.isLeaderStale(existingLeader)) {
|
|
13916
|
-
this.becomeFollower(existingLeader.sessionId);
|
|
13917
|
-
} else {
|
|
13918
|
-
this.tryBecomeLeader();
|
|
13919
|
-
}
|
|
14300
|
+
if (db) {
|
|
14301
|
+
return db;
|
|
13920
14302
|
}
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
13925
|
-
|
|
13926
|
-
const sessionId = existingSession?.id || this.generateSessionId();
|
|
13927
|
-
const isNewSession = !existingSession;
|
|
13928
|
-
const leaderState = {
|
|
13929
|
-
tabId: this.tabId,
|
|
13930
|
-
sessionId,
|
|
13931
|
-
heartbeat: Date.now()
|
|
14303
|
+
dbInitPromise = new Promise((resolve2, reject) => {
|
|
14304
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
14305
|
+
request.onerror = () => {
|
|
14306
|
+
dbInitPromise = null;
|
|
14307
|
+
reject(new Error("Failed to open IndexedDB"));
|
|
13932
14308
|
};
|
|
13933
|
-
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
if (this.debug) console.warn("[LogSpace MultiTab] Failed to claim leadership:", error);
|
|
13943
|
-
}
|
|
13944
|
-
}
|
|
13945
|
-
/**
|
|
13946
|
-
* Become the leader tab
|
|
13947
|
-
*/
|
|
13948
|
-
becomeLeader(sessionId, isNewSession) {
|
|
13949
|
-
this.isLeader = true;
|
|
13950
|
-
this.currentSessionId = sessionId;
|
|
13951
|
-
if (isNewSession) {
|
|
13952
|
-
const sessionState = {
|
|
13953
|
-
id: sessionId,
|
|
13954
|
-
startTime: Date.now()
|
|
13955
|
-
};
|
|
13956
|
-
localStorage.setItem(SESSION_KEY, JSON.stringify(sessionState));
|
|
13957
|
-
}
|
|
13958
|
-
this.startHeartbeat();
|
|
13959
|
-
this.broadcast({ type: "leader-announce", tabId: this.tabId, sessionId });
|
|
13960
|
-
this.callbacks?.onBecomeLeader(sessionId, isNewSession);
|
|
13961
|
-
if (this.debug) {
|
|
13962
|
-
console.log("[LogSpace MultiTab] Became leader", { sessionId, isNewSession });
|
|
13963
|
-
}
|
|
13964
|
-
}
|
|
13965
|
-
/**
|
|
13966
|
-
* Become a follower tab
|
|
13967
|
-
*/
|
|
13968
|
-
becomeFollower(sessionId) {
|
|
13969
|
-
this.isLeader = false;
|
|
13970
|
-
this.currentSessionId = sessionId;
|
|
13971
|
-
this.stopHeartbeat();
|
|
13972
|
-
this.startLeaderCheck();
|
|
13973
|
-
this.callbacks?.onBecomeFollower(sessionId);
|
|
13974
|
-
if (this.debug) {
|
|
13975
|
-
console.log("[LogSpace MultiTab] Became follower", { sessionId });
|
|
13976
|
-
}
|
|
13977
|
-
}
|
|
13978
|
-
/**
|
|
13979
|
-
* Handle incoming broadcast messages
|
|
13980
|
-
*/
|
|
13981
|
-
handleMessage(message) {
|
|
13982
|
-
if (this.debug) {
|
|
13983
|
-
console.log("[LogSpace MultiTab] Received message:", message.type);
|
|
13984
|
-
}
|
|
13985
|
-
switch (message.type) {
|
|
13986
|
-
case "leader-announce":
|
|
13987
|
-
if (message.tabId !== this.tabId) {
|
|
13988
|
-
if (this.isLeader) {
|
|
13989
|
-
this.becomeFollower(message.sessionId);
|
|
13990
|
-
}
|
|
13991
|
-
this.callbacks?.onLeaderChanged(message.tabId);
|
|
13992
|
-
}
|
|
13993
|
-
break;
|
|
13994
|
-
case "leader-leaving":
|
|
13995
|
-
if (!this.isLeader) {
|
|
13996
|
-
if (this.debug) {
|
|
13997
|
-
console.log("[LogSpace MultiTab] Leader leaving, attempting to claim leadership");
|
|
13998
|
-
}
|
|
13999
|
-
setTimeout(() => this.tryBecomeLeader(), Math.random() * 100);
|
|
14000
|
-
}
|
|
14001
|
-
break;
|
|
14002
|
-
case "log":
|
|
14003
|
-
if (this.isLeader && message.tabId !== this.tabId) {
|
|
14004
|
-
this.callbacks?.onReceiveLog(message.log);
|
|
14005
|
-
}
|
|
14006
|
-
break;
|
|
14007
|
-
case "rrweb-event":
|
|
14008
|
-
if (this.isLeader && message.tabId !== this.tabId) {
|
|
14009
|
-
this.callbacks?.onReceiveRRWebEvent(message.event);
|
|
14010
|
-
}
|
|
14011
|
-
break;
|
|
14012
|
-
case "request-session":
|
|
14013
|
-
if (this.isLeader && message.tabId !== this.tabId) {
|
|
14014
|
-
const session2 = this.getSessionState();
|
|
14015
|
-
if (session2) {
|
|
14016
|
-
this.broadcast({
|
|
14017
|
-
type: "session-info",
|
|
14018
|
-
sessionId: session2.id,
|
|
14019
|
-
startTime: session2.startTime
|
|
14020
|
-
});
|
|
14021
|
-
}
|
|
14022
|
-
}
|
|
14023
|
-
break;
|
|
14024
|
-
case "session-info":
|
|
14025
|
-
if (!this.isLeader) {
|
|
14026
|
-
this.currentSessionId = message.sessionId;
|
|
14027
|
-
}
|
|
14028
|
-
break;
|
|
14029
|
-
}
|
|
14030
|
-
}
|
|
14031
|
-
/**
|
|
14032
|
-
* Start heartbeat for leader
|
|
14033
|
-
*/
|
|
14034
|
-
startHeartbeat() {
|
|
14035
|
-
this.stopHeartbeat();
|
|
14036
|
-
this.stopLeaderCheck();
|
|
14037
|
-
this.heartbeatTimer = setInterval(() => {
|
|
14038
|
-
if (this.isLeader) {
|
|
14039
|
-
this.updateHeartbeat();
|
|
14309
|
+
request.onsuccess = () => {
|
|
14310
|
+
db = request.result;
|
|
14311
|
+
resolve2(db);
|
|
14312
|
+
};
|
|
14313
|
+
request.onupgradeneeded = (event) => {
|
|
14314
|
+
const database = event.target.result;
|
|
14315
|
+
if (!database.objectStoreNames.contains(STORES.PENDING_SESSIONS)) {
|
|
14316
|
+
const pendingStore = database.createObjectStore(STORES.PENDING_SESSIONS, { keyPath: "id" });
|
|
14317
|
+
pendingStore.createIndex("by-created", "createdAt");
|
|
14040
14318
|
}
|
|
14041
|
-
|
|
14042
|
-
|
|
14043
|
-
}
|
|
14044
|
-
/**
|
|
14045
|
-
* Stop heartbeat timer
|
|
14046
|
-
*/
|
|
14047
|
-
stopHeartbeat() {
|
|
14048
|
-
if (this.heartbeatTimer) {
|
|
14049
|
-
clearInterval(this.heartbeatTimer);
|
|
14050
|
-
this.heartbeatTimer = null;
|
|
14051
|
-
}
|
|
14052
|
-
}
|
|
14053
|
-
/**
|
|
14054
|
-
* Update heartbeat in localStorage
|
|
14055
|
-
*/
|
|
14056
|
-
updateHeartbeat() {
|
|
14057
|
-
const state2 = this.getLeaderState();
|
|
14058
|
-
if (state2 && state2.tabId === this.tabId) {
|
|
14059
|
-
state2.heartbeat = Date.now();
|
|
14060
|
-
try {
|
|
14061
|
-
localStorage.setItem(LEADER_KEY, JSON.stringify(state2));
|
|
14062
|
-
} catch (error) {
|
|
14063
|
-
}
|
|
14064
|
-
}
|
|
14065
|
-
}
|
|
14066
|
-
/**
|
|
14067
|
-
* Start checking if leader is alive (for followers)
|
|
14068
|
-
*/
|
|
14069
|
-
startLeaderCheck() {
|
|
14070
|
-
this.stopLeaderCheck();
|
|
14071
|
-
this.leaderCheckTimer = setInterval(() => {
|
|
14072
|
-
if (!this.isLeader) {
|
|
14073
|
-
const leader = this.getLeaderState();
|
|
14074
|
-
if (!leader || this.isLeaderStale(leader)) {
|
|
14075
|
-
if (this.debug) {
|
|
14076
|
-
console.log("[LogSpace MultiTab] Leader appears dead, attempting takeover");
|
|
14077
|
-
}
|
|
14078
|
-
this.tryBecomeLeader();
|
|
14079
|
-
}
|
|
14080
|
-
}
|
|
14081
|
-
}, LEADER_CHECK_INTERVAL);
|
|
14082
|
-
}
|
|
14083
|
-
/**
|
|
14084
|
-
* Stop leader check timer
|
|
14085
|
-
*/
|
|
14086
|
-
stopLeaderCheck() {
|
|
14087
|
-
if (this.leaderCheckTimer) {
|
|
14088
|
-
clearInterval(this.leaderCheckTimer);
|
|
14089
|
-
this.leaderCheckTimer = null;
|
|
14090
|
-
}
|
|
14091
|
-
}
|
|
14092
|
-
/**
|
|
14093
|
-
* Check if leader heartbeat is stale
|
|
14094
|
-
*/
|
|
14095
|
-
isLeaderStale(leader) {
|
|
14096
|
-
return Date.now() - leader.heartbeat > HEARTBEAT_STALE_THRESHOLD;
|
|
14097
|
-
}
|
|
14098
|
-
/**
|
|
14099
|
-
* Get leader state from localStorage
|
|
14100
|
-
*/
|
|
14101
|
-
getLeaderState() {
|
|
14102
|
-
try {
|
|
14103
|
-
const data = localStorage.getItem(LEADER_KEY);
|
|
14104
|
-
return data ? JSON.parse(data) : null;
|
|
14105
|
-
} catch {
|
|
14106
|
-
return null;
|
|
14107
|
-
}
|
|
14108
|
-
}
|
|
14109
|
-
/**
|
|
14110
|
-
* Get session state from localStorage
|
|
14111
|
-
*/
|
|
14112
|
-
getSessionState() {
|
|
14113
|
-
try {
|
|
14114
|
-
const data = localStorage.getItem(SESSION_KEY);
|
|
14115
|
-
return data ? JSON.parse(data) : null;
|
|
14116
|
-
} catch {
|
|
14117
|
-
return null;
|
|
14118
|
-
}
|
|
14119
|
-
}
|
|
14120
|
-
/**
|
|
14121
|
-
* Generate session ID
|
|
14122
|
-
*/
|
|
14123
|
-
generateSessionId() {
|
|
14124
|
-
const timestamp = Date.now().toString(36);
|
|
14125
|
-
const random = Math.random().toString(36).substring(2, 10);
|
|
14126
|
-
return `sdk_${timestamp}_${random}`;
|
|
14127
|
-
}
|
|
14128
|
-
/**
|
|
14129
|
-
* Broadcast message to other tabs
|
|
14130
|
-
*/
|
|
14131
|
-
broadcast(message) {
|
|
14132
|
-
if (this.channel) {
|
|
14133
|
-
this.channel.postMessage(message);
|
|
14134
|
-
}
|
|
14135
|
-
}
|
|
14136
|
-
/**
|
|
14137
|
-
* Handle page unload - clean leader handoff
|
|
14138
|
-
*/
|
|
14139
|
-
handleUnload() {
|
|
14140
|
-
if (this.isLeader) {
|
|
14141
|
-
this.broadcast({ type: "leader-leaving", tabId: this.tabId });
|
|
14142
|
-
try {
|
|
14143
|
-
localStorage.removeItem(LEADER_KEY);
|
|
14144
|
-
} catch {
|
|
14145
|
-
}
|
|
14146
|
-
}
|
|
14147
|
-
this.cleanup();
|
|
14148
|
-
}
|
|
14149
|
-
/**
|
|
14150
|
-
* Send log to leader (for follower tabs)
|
|
14151
|
-
*/
|
|
14152
|
-
sendLog(log) {
|
|
14153
|
-
if (!this.initialized || !this.channel) return;
|
|
14154
|
-
if (this.isLeader) {
|
|
14155
|
-
this.callbacks?.onReceiveLog(log);
|
|
14156
|
-
} else {
|
|
14157
|
-
this.broadcast({ type: "log", tabId: this.tabId, log });
|
|
14158
|
-
}
|
|
14159
|
-
}
|
|
14160
|
-
/**
|
|
14161
|
-
* Send rrweb event to leader (for follower tabs)
|
|
14162
|
-
*/
|
|
14163
|
-
sendRRWebEvent(event) {
|
|
14164
|
-
if (!this.initialized || !this.channel) return;
|
|
14165
|
-
if (this.isLeader) {
|
|
14166
|
-
this.callbacks?.onReceiveRRWebEvent(event);
|
|
14167
|
-
} else {
|
|
14168
|
-
this.broadcast({ type: "rrweb-event", tabId: this.tabId, event });
|
|
14169
|
-
}
|
|
14170
|
-
}
|
|
14171
|
-
/**
|
|
14172
|
-
* Update session user info (stored in localStorage for all tabs)
|
|
14173
|
-
*/
|
|
14174
|
-
updateSessionUser(userId, traits) {
|
|
14175
|
-
const session2 = this.getSessionState();
|
|
14176
|
-
if (session2) {
|
|
14177
|
-
session2.userId = userId;
|
|
14178
|
-
session2.userTraits = traits;
|
|
14179
|
-
try {
|
|
14180
|
-
localStorage.setItem(SESSION_KEY, JSON.stringify(session2));
|
|
14181
|
-
} catch {
|
|
14182
|
-
}
|
|
14183
|
-
}
|
|
14184
|
-
}
|
|
14185
|
-
/**
|
|
14186
|
-
* End session and clear shared state
|
|
14187
|
-
*/
|
|
14188
|
-
endSession() {
|
|
14189
|
-
try {
|
|
14190
|
-
localStorage.removeItem(SESSION_KEY);
|
|
14191
|
-
if (this.isLeader) {
|
|
14192
|
-
localStorage.removeItem(LEADER_KEY);
|
|
14193
|
-
}
|
|
14194
|
-
} catch {
|
|
14195
|
-
}
|
|
14196
|
-
}
|
|
14197
|
-
/**
|
|
14198
|
-
* Get current tab ID
|
|
14199
|
-
*/
|
|
14200
|
-
getTabId() {
|
|
14201
|
-
return this.tabId;
|
|
14202
|
-
}
|
|
14203
|
-
/**
|
|
14204
|
-
* Get current session ID
|
|
14205
|
-
*/
|
|
14206
|
-
getSessionId() {
|
|
14207
|
-
return this.currentSessionId;
|
|
14208
|
-
}
|
|
14209
|
-
/**
|
|
14210
|
-
* Check if this tab is the leader
|
|
14211
|
-
*/
|
|
14212
|
-
isLeaderTab() {
|
|
14213
|
-
return this.isLeader;
|
|
14214
|
-
}
|
|
14215
|
-
/**
|
|
14216
|
-
* Check if multi-tab mode is active
|
|
14217
|
-
*/
|
|
14218
|
-
isActive() {
|
|
14219
|
-
return this.initialized && this.channel !== null;
|
|
14220
|
-
}
|
|
14221
|
-
/**
|
|
14222
|
-
* Clean up resources
|
|
14223
|
-
*/
|
|
14224
|
-
cleanup() {
|
|
14225
|
-
this.stopHeartbeat();
|
|
14226
|
-
this.stopLeaderCheck();
|
|
14227
|
-
if (this.channel) {
|
|
14228
|
-
this.channel.close();
|
|
14229
|
-
this.channel = null;
|
|
14230
|
-
}
|
|
14231
|
-
this.initialized = false;
|
|
14232
|
-
}
|
|
14233
|
-
/**
|
|
14234
|
-
* Force become leader (for testing or recovery)
|
|
14235
|
-
*/
|
|
14236
|
-
forceLeadership() {
|
|
14237
|
-
if (!this.initialized) return;
|
|
14238
|
-
const session2 = this.getSessionState();
|
|
14239
|
-
const sessionId = session2?.id || this.generateSessionId();
|
|
14240
|
-
this.becomeLeader(sessionId, !session2);
|
|
14241
|
-
}
|
|
14242
|
-
}
|
|
14243
|
-
const multiTabCoordinator = new MultiTabCoordinator();
|
|
14244
|
-
const DB_NAME = "logspace-sdk-db";
|
|
14245
|
-
const DB_VERSION = 1;
|
|
14246
|
-
const STORES = {
|
|
14247
|
-
PENDING_SESSIONS: "pending_sessions",
|
|
14248
|
-
// Sessions waiting to be sent
|
|
14249
|
-
CURRENT_SESSION: "current_session"
|
|
14250
|
-
// Current active session backup
|
|
14251
|
-
};
|
|
14252
|
-
let db = null;
|
|
14253
|
-
let dbInitPromise = null;
|
|
14254
|
-
async function initDB() {
|
|
14255
|
-
if (dbInitPromise) {
|
|
14256
|
-
return dbInitPromise;
|
|
14257
|
-
}
|
|
14258
|
-
if (db) {
|
|
14259
|
-
return db;
|
|
14260
|
-
}
|
|
14261
|
-
dbInitPromise = new Promise((resolve2, reject) => {
|
|
14262
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
14263
|
-
request.onerror = () => {
|
|
14264
|
-
dbInitPromise = null;
|
|
14265
|
-
reject(new Error("Failed to open IndexedDB"));
|
|
14266
|
-
};
|
|
14267
|
-
request.onsuccess = () => {
|
|
14268
|
-
db = request.result;
|
|
14269
|
-
resolve2(db);
|
|
14270
|
-
};
|
|
14271
|
-
request.onupgradeneeded = (event) => {
|
|
14272
|
-
const database = event.target.result;
|
|
14273
|
-
if (!database.objectStoreNames.contains(STORES.PENDING_SESSIONS)) {
|
|
14274
|
-
const pendingStore = database.createObjectStore(STORES.PENDING_SESSIONS, { keyPath: "id" });
|
|
14275
|
-
pendingStore.createIndex("by-created", "createdAt");
|
|
14276
|
-
}
|
|
14277
|
-
if (!database.objectStoreNames.contains(STORES.CURRENT_SESSION)) {
|
|
14278
|
-
database.createObjectStore(STORES.CURRENT_SESSION, { keyPath: "id" });
|
|
14319
|
+
if (!database.objectStoreNames.contains(STORES.CURRENT_SESSION)) {
|
|
14320
|
+
database.createObjectStore(STORES.CURRENT_SESSION, { keyPath: "id" });
|
|
14279
14321
|
}
|
|
14280
14322
|
};
|
|
14281
14323
|
});
|
|
@@ -14344,6 +14386,29 @@ async function clearCurrentSessionBackup() {
|
|
|
14344
14386
|
console.warn("[LogSpace] Failed to clear session backup:", error);
|
|
14345
14387
|
}
|
|
14346
14388
|
}
|
|
14389
|
+
async function clearCurrentSessionBackupFor(sessionId) {
|
|
14390
|
+
if (!isIndexedDBAvailable()) return;
|
|
14391
|
+
try {
|
|
14392
|
+
const database = await initDB();
|
|
14393
|
+
const transaction = database.transaction(STORES.CURRENT_SESSION, "readwrite");
|
|
14394
|
+
const store = transaction.objectStore(STORES.CURRENT_SESSION);
|
|
14395
|
+
const current = await new Promise((resolve2, reject) => {
|
|
14396
|
+
const request = store.get("current");
|
|
14397
|
+
request.onsuccess = () => resolve2(request.result || null);
|
|
14398
|
+
request.onerror = () => reject(request.error);
|
|
14399
|
+
});
|
|
14400
|
+
if (!current || current.sessionId !== sessionId) {
|
|
14401
|
+
return;
|
|
14402
|
+
}
|
|
14403
|
+
await new Promise((resolve2, reject) => {
|
|
14404
|
+
const request = store.delete("current");
|
|
14405
|
+
request.onsuccess = () => resolve2();
|
|
14406
|
+
request.onerror = () => reject(request.error);
|
|
14407
|
+
});
|
|
14408
|
+
} catch (error) {
|
|
14409
|
+
console.warn("[LogSpace] Failed to clear session backup:", error);
|
|
14410
|
+
}
|
|
14411
|
+
}
|
|
14347
14412
|
async function addPendingSession(payload) {
|
|
14348
14413
|
if (!isIndexedDBAvailable()) return;
|
|
14349
14414
|
try {
|
|
@@ -14447,14 +14512,13 @@ function closeDB() {
|
|
|
14447
14512
|
dbInitPromise = null;
|
|
14448
14513
|
}
|
|
14449
14514
|
}
|
|
14450
|
-
const SDK_VERSION = "
|
|
14515
|
+
const SDK_VERSION = "1.1.0";
|
|
14451
14516
|
let config = null;
|
|
14452
14517
|
let session = null;
|
|
14453
14518
|
let logs = [];
|
|
14454
14519
|
let rrwebEvents = [];
|
|
14455
14520
|
let initialized = false;
|
|
14456
14521
|
let endReason = "manual";
|
|
14457
|
-
let sessionEndedByHardLimit = false;
|
|
14458
14522
|
let sessionEndedByIdle = false;
|
|
14459
14523
|
let rateLimiter = { count: 0, windowStart: 0 };
|
|
14460
14524
|
let deduplication = { lastLogHash: null, lastLogCount: 0 };
|
|
@@ -14467,11 +14531,16 @@ let visibilityTimer = null;
|
|
|
14467
14531
|
let lastMouseMoveTime = 0;
|
|
14468
14532
|
let samplingTriggered = false;
|
|
14469
14533
|
let samplingTriggerType = null;
|
|
14534
|
+
let samplingFirstTriggerTime = null;
|
|
14470
14535
|
let samplingTriggerTime = null;
|
|
14471
14536
|
let samplingEndTimer = null;
|
|
14537
|
+
let samplingTrimTimer = null;
|
|
14472
14538
|
let continuingSessionId = null;
|
|
14473
14539
|
let continuingSessionStartTime = null;
|
|
14540
|
+
let pendingIdentity = null;
|
|
14541
|
+
let currentIdentity = null;
|
|
14474
14542
|
let recoveryTargetSessionId = null;
|
|
14543
|
+
const processedRecoverySessionIds = /* @__PURE__ */ new Set();
|
|
14475
14544
|
const DEFAULT_CONFIG = {
|
|
14476
14545
|
capture: {
|
|
14477
14546
|
rrweb: true,
|
|
@@ -14488,7 +14557,7 @@ const DEFAULT_CONFIG = {
|
|
|
14488
14557
|
},
|
|
14489
14558
|
rrweb: {
|
|
14490
14559
|
maskAllInputs: false,
|
|
14491
|
-
checkoutEveryNth:
|
|
14560
|
+
checkoutEveryNth: 150,
|
|
14492
14561
|
recordCanvas: false
|
|
14493
14562
|
},
|
|
14494
14563
|
privacy: {
|
|
@@ -14503,15 +14572,17 @@ const DEFAULT_CONFIG = {
|
|
|
14503
14572
|
maxLogs: 1e4,
|
|
14504
14573
|
maxSize: 10 * 1024 * 1024,
|
|
14505
14574
|
// 10MB
|
|
14506
|
-
maxDuration:
|
|
14507
|
-
//
|
|
14508
|
-
idleTimeout:
|
|
14509
|
-
//
|
|
14575
|
+
maxDuration: 1800,
|
|
14576
|
+
// 30 minutes
|
|
14577
|
+
idleTimeout: 120,
|
|
14578
|
+
// 2 minutes
|
|
14510
14579
|
navigateAwayTimeout: 120,
|
|
14511
14580
|
// 2 minutes grace period
|
|
14512
14581
|
rateLimit: 100,
|
|
14513
14582
|
// 100 logs/second
|
|
14514
|
-
deduplicate: true
|
|
14583
|
+
deduplicate: true,
|
|
14584
|
+
maxNetworkBodySize: 10
|
|
14585
|
+
// 10KB (in KB, will be multiplied by 1024)
|
|
14515
14586
|
},
|
|
14516
14587
|
autoEnd: {
|
|
14517
14588
|
continueOnRefresh: true,
|
|
@@ -14520,10 +14591,10 @@ const DEFAULT_CONFIG = {
|
|
|
14520
14591
|
},
|
|
14521
14592
|
sampling: {
|
|
14522
14593
|
enabled: false,
|
|
14523
|
-
bufferBefore:
|
|
14524
|
-
//
|
|
14525
|
-
recordAfter:
|
|
14526
|
-
//
|
|
14594
|
+
bufferBefore: 30,
|
|
14595
|
+
// 30 seconds before trigger
|
|
14596
|
+
recordAfter: 30,
|
|
14597
|
+
// 30 seconds after trigger
|
|
14527
14598
|
triggers: {
|
|
14528
14599
|
onError: true,
|
|
14529
14600
|
onConsoleError: true,
|
|
@@ -14559,6 +14630,87 @@ function generateSessionId() {
|
|
|
14559
14630
|
const random = Math.random().toString(36).substring(2, 10);
|
|
14560
14631
|
return `sdk_${timestamp}_${random}`;
|
|
14561
14632
|
}
|
|
14633
|
+
const TAB_ID_KEY = "__logspace_tab_id";
|
|
14634
|
+
const SESSION_ID_KEY = "__logspace_session_id";
|
|
14635
|
+
let currentTabId = null;
|
|
14636
|
+
function generateTabId() {
|
|
14637
|
+
const timestamp = Date.now().toString(36);
|
|
14638
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
14639
|
+
return `tab_${timestamp}_${random}`;
|
|
14640
|
+
}
|
|
14641
|
+
function getOrCreateTabId() {
|
|
14642
|
+
if (currentTabId) return currentTabId;
|
|
14643
|
+
if (typeof sessionStorage !== "undefined") {
|
|
14644
|
+
const stored = sessionStorage.getItem(TAB_ID_KEY);
|
|
14645
|
+
if (stored) {
|
|
14646
|
+
currentTabId = stored;
|
|
14647
|
+
return stored;
|
|
14648
|
+
}
|
|
14649
|
+
}
|
|
14650
|
+
const created = generateTabId();
|
|
14651
|
+
currentTabId = created;
|
|
14652
|
+
try {
|
|
14653
|
+
sessionStorage.setItem(TAB_ID_KEY, created);
|
|
14654
|
+
} catch {
|
|
14655
|
+
}
|
|
14656
|
+
return created;
|
|
14657
|
+
}
|
|
14658
|
+
function setSessionContext(sessionId, tabId) {
|
|
14659
|
+
try {
|
|
14660
|
+
sessionStorage.setItem(SESSION_ID_KEY, sessionId);
|
|
14661
|
+
sessionStorage.setItem(TAB_ID_KEY, tabId);
|
|
14662
|
+
} catch {
|
|
14663
|
+
}
|
|
14664
|
+
try {
|
|
14665
|
+
window.__logspace_session_id = sessionId;
|
|
14666
|
+
window.__logspace_tab_id = tabId;
|
|
14667
|
+
} catch {
|
|
14668
|
+
}
|
|
14669
|
+
}
|
|
14670
|
+
function getSessionOrigin() {
|
|
14671
|
+
if (typeof document === "undefined") return null;
|
|
14672
|
+
const referrer = document.referrer || void 0;
|
|
14673
|
+
let openerSessionId;
|
|
14674
|
+
let openerTabId;
|
|
14675
|
+
let openerUrl;
|
|
14676
|
+
if (typeof window !== "undefined" && window.opener) {
|
|
14677
|
+
try {
|
|
14678
|
+
const opener = window.opener;
|
|
14679
|
+
openerSessionId = opener.sessionStorage?.getItem(SESSION_ID_KEY) ?? opener.__logspace_session_id;
|
|
14680
|
+
openerTabId = opener.sessionStorage?.getItem(TAB_ID_KEY) ?? opener.__logspace_tab_id;
|
|
14681
|
+
openerUrl = opener.location?.href;
|
|
14682
|
+
} catch {
|
|
14683
|
+
}
|
|
14684
|
+
}
|
|
14685
|
+
if (!referrer && !openerSessionId && !openerTabId && !openerUrl) {
|
|
14686
|
+
return null;
|
|
14687
|
+
}
|
|
14688
|
+
return {
|
|
14689
|
+
referrer,
|
|
14690
|
+
openerSessionId,
|
|
14691
|
+
openerTabId,
|
|
14692
|
+
openerUrl
|
|
14693
|
+
};
|
|
14694
|
+
}
|
|
14695
|
+
function buildSessionMetadata(metadata) {
|
|
14696
|
+
const origin = getSessionOrigin();
|
|
14697
|
+
if (!origin && !metadata) return void 0;
|
|
14698
|
+
if (!origin) return metadata;
|
|
14699
|
+
const existingOrigin = metadata?.logspaceOrigin;
|
|
14700
|
+
if (existingOrigin && typeof existingOrigin === "object" && !Array.isArray(existingOrigin)) {
|
|
14701
|
+
return {
|
|
14702
|
+
...metadata,
|
|
14703
|
+
logspaceOrigin: {
|
|
14704
|
+
...origin,
|
|
14705
|
+
...existingOrigin
|
|
14706
|
+
}
|
|
14707
|
+
};
|
|
14708
|
+
}
|
|
14709
|
+
return {
|
|
14710
|
+
...metadata || {},
|
|
14711
|
+
logspaceOrigin: origin
|
|
14712
|
+
};
|
|
14713
|
+
}
|
|
14562
14714
|
function hashLog(log) {
|
|
14563
14715
|
return `${log.type}:${JSON.stringify(log.data)}`;
|
|
14564
14716
|
}
|
|
@@ -14571,13 +14723,13 @@ function checkRateLimit() {
|
|
|
14571
14723
|
rateLimiter.count = 1;
|
|
14572
14724
|
return true;
|
|
14573
14725
|
}
|
|
14574
|
-
|
|
14726
|
+
rateLimiter.count++;
|
|
14727
|
+
if (rateLimiter.count > config.limits.rateLimit) {
|
|
14575
14728
|
if (config.debug) {
|
|
14576
14729
|
console.warn("[LogSpace] Rate limit exceeded, dropping log");
|
|
14577
14730
|
}
|
|
14578
14731
|
return false;
|
|
14579
14732
|
}
|
|
14580
|
-
rateLimiter.count++;
|
|
14581
14733
|
return true;
|
|
14582
14734
|
}
|
|
14583
14735
|
function checkDeduplication(log) {
|
|
@@ -14610,9 +14762,6 @@ function checkLimits() {
|
|
|
14610
14762
|
}
|
|
14611
14763
|
function resetIdleTimer() {
|
|
14612
14764
|
if (!config?.autoEnd.onIdle) return;
|
|
14613
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
14614
|
-
return;
|
|
14615
|
-
}
|
|
14616
14765
|
if (idleTimer) {
|
|
14617
14766
|
clearTimeout(idleTimer);
|
|
14618
14767
|
}
|
|
@@ -14630,9 +14779,6 @@ function resetIdleTimer() {
|
|
|
14630
14779
|
}
|
|
14631
14780
|
function handleUserActivity() {
|
|
14632
14781
|
if (!config || !initialized) return;
|
|
14633
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
14634
|
-
return;
|
|
14635
|
-
}
|
|
14636
14782
|
if (sessionEndedByIdle && (!session || session.status === "stopped")) {
|
|
14637
14783
|
if (config.debug) {
|
|
14638
14784
|
console.log("[LogSpace] User activity detected - starting new session");
|
|
@@ -14665,33 +14811,54 @@ function removeActivityListeners() {
|
|
|
14665
14811
|
window.removeEventListener("mousemove", handleMouseMove, { capture: true });
|
|
14666
14812
|
}
|
|
14667
14813
|
function triggerSampling(triggerType, triggerLog) {
|
|
14668
|
-
if (!config?.sampling.enabled
|
|
14814
|
+
if (!config?.sampling.enabled) return;
|
|
14815
|
+
if (unloadHandled || isNavigatingAway) {
|
|
14816
|
+
if (config.debug) {
|
|
14817
|
+
console.log(`[LogSpace] Ignoring sampling trigger during unload: ${triggerType}`);
|
|
14818
|
+
}
|
|
14819
|
+
return;
|
|
14820
|
+
}
|
|
14821
|
+
const isFirstTrigger = !samplingTriggered;
|
|
14669
14822
|
samplingTriggered = true;
|
|
14670
14823
|
samplingTriggerType = triggerType;
|
|
14824
|
+
if (isFirstTrigger) {
|
|
14825
|
+
samplingFirstTriggerTime = Date.now();
|
|
14826
|
+
}
|
|
14671
14827
|
samplingTriggerTime = Date.now();
|
|
14828
|
+
stopSamplingTrimTimer();
|
|
14672
14829
|
if (config.debug) {
|
|
14673
|
-
|
|
14830
|
+
if (isFirstTrigger) {
|
|
14831
|
+
console.log(`[LogSpace] Sampling triggered by: ${triggerType}`);
|
|
14832
|
+
} else {
|
|
14833
|
+
console.log(`[LogSpace] Sampling window extended by: ${triggerType}`);
|
|
14834
|
+
}
|
|
14835
|
+
}
|
|
14836
|
+
if (isFirstTrigger) {
|
|
14837
|
+
config.hooks.onSamplingTrigger?.(triggerType, triggerLog);
|
|
14838
|
+
}
|
|
14839
|
+
if (samplingEndTimer) {
|
|
14840
|
+
clearTimeout(samplingEndTimer);
|
|
14841
|
+
samplingEndTimer = null;
|
|
14674
14842
|
}
|
|
14675
|
-
config.hooks.onSamplingTrigger?.(triggerType, triggerLog);
|
|
14676
14843
|
samplingEndTimer = setTimeout(() => {
|
|
14677
14844
|
if (session?.status === "recording") {
|
|
14678
14845
|
if (config?.debug) {
|
|
14679
14846
|
console.log("[LogSpace] Sampling recordAfter period complete, ending session");
|
|
14680
14847
|
}
|
|
14681
14848
|
endReason = "samplingTriggered";
|
|
14682
|
-
|
|
14849
|
+
stopSessionInternal({ restartImmediately: true });
|
|
14683
14850
|
}
|
|
14684
14851
|
}, config.sampling.recordAfter * 1e3);
|
|
14685
14852
|
}
|
|
14686
14853
|
function checkSamplingTrigger(log) {
|
|
14687
|
-
if (!config?.sampling.enabled
|
|
14854
|
+
if (!config?.sampling.enabled) return;
|
|
14688
14855
|
if (config.sampling.triggers.onError && log.type === "error") {
|
|
14689
14856
|
triggerSampling("error", log);
|
|
14690
14857
|
return;
|
|
14691
14858
|
}
|
|
14692
14859
|
if (config.sampling.triggers.onConsoleError && log.type === "console") {
|
|
14693
14860
|
const consoleData = log.data;
|
|
14694
|
-
if (consoleData.level === "error") {
|
|
14861
|
+
if (consoleData.level === "error" || consoleData.level === "assert") {
|
|
14695
14862
|
triggerSampling("consoleError", log);
|
|
14696
14863
|
return;
|
|
14697
14864
|
}
|
|
@@ -14705,19 +14872,114 @@ function checkSamplingTrigger(log) {
|
|
|
14705
14872
|
}
|
|
14706
14873
|
}
|
|
14707
14874
|
function trimLogsToSamplingWindow() {
|
|
14708
|
-
if (!config?.sampling.enabled || !samplingTriggerTime || !session) return;
|
|
14709
|
-
const bufferStartTime =
|
|
14875
|
+
if (!config?.sampling.enabled || !samplingFirstTriggerTime || !samplingTriggerTime || !session) return;
|
|
14876
|
+
const bufferStartTime = samplingFirstTriggerTime - config.sampling.bufferBefore * 1e3;
|
|
14710
14877
|
const bufferEndTime = samplingTriggerTime + config.sampling.recordAfter * 1e3;
|
|
14711
14878
|
logs = logs.filter((log) => {
|
|
14712
14879
|
return log.timestamp >= bufferStartTime && log.timestamp <= bufferEndTime;
|
|
14713
14880
|
});
|
|
14714
|
-
|
|
14881
|
+
const recentEvents = rrwebEvents.filter((event) => {
|
|
14715
14882
|
return event.timestamp >= bufferStartTime && event.timestamp <= bufferEndTime;
|
|
14716
14883
|
});
|
|
14884
|
+
const hasFullSnapshot = recentEvents.some((event) => event.type === 2);
|
|
14885
|
+
if (config.debug) {
|
|
14886
|
+
console.log(
|
|
14887
|
+
`[LogSpace] trimLogsToSamplingWindow - before: ${rrwebEvents.length} events, recentEvents: ${recentEvents.length}, hasFullSnapshot in recent: ${hasFullSnapshot}`
|
|
14888
|
+
);
|
|
14889
|
+
console.log(
|
|
14890
|
+
`[LogSpace] trimLogsToSamplingWindow - rrweb types before: ${rrwebEvents.map((e) => e.type).join(",")}`
|
|
14891
|
+
);
|
|
14892
|
+
}
|
|
14893
|
+
if (hasFullSnapshot) {
|
|
14894
|
+
rrwebEvents = recentEvents;
|
|
14895
|
+
} else {
|
|
14896
|
+
const fullSnapshotsBeforeWindow = [];
|
|
14897
|
+
for (let i = 0; i < rrwebEvents.length; i++) {
|
|
14898
|
+
const event = rrwebEvents[i];
|
|
14899
|
+
if (event && event.type === 2 && event.timestamp < bufferStartTime) {
|
|
14900
|
+
fullSnapshotsBeforeWindow.push(event);
|
|
14901
|
+
}
|
|
14902
|
+
}
|
|
14903
|
+
if (config.debug) {
|
|
14904
|
+
console.log(
|
|
14905
|
+
`[LogSpace] trimLogsToSamplingWindow - found ${fullSnapshotsBeforeWindow.length} snapshots before window`
|
|
14906
|
+
);
|
|
14907
|
+
}
|
|
14908
|
+
if (fullSnapshotsBeforeWindow.length > 0) {
|
|
14909
|
+
rrwebEvents = [...fullSnapshotsBeforeWindow, ...recentEvents];
|
|
14910
|
+
} else {
|
|
14911
|
+
rrwebEvents = recentEvents;
|
|
14912
|
+
}
|
|
14913
|
+
}
|
|
14914
|
+
if (config.debug) {
|
|
14915
|
+
console.log(
|
|
14916
|
+
`[LogSpace] trimLogsToSamplingWindow - after: ${rrwebEvents.length} events, types: ${rrwebEvents.map((e) => e.type).join(",")}`
|
|
14917
|
+
);
|
|
14918
|
+
}
|
|
14919
|
+
rebuildSessionStats();
|
|
14717
14920
|
if (config.debug) {
|
|
14718
14921
|
console.log(`[LogSpace] Trimmed to sampling window: ${logs.length} logs, ${rrwebEvents.length} rrweb events`);
|
|
14719
14922
|
}
|
|
14720
14923
|
}
|
|
14924
|
+
const SAMPLING_TRIM_INTERVAL = 5 * 1e3;
|
|
14925
|
+
function trimRollingBuffer() {
|
|
14926
|
+
if (!config?.sampling.enabled || samplingTriggered || !session) return;
|
|
14927
|
+
const now = Date.now();
|
|
14928
|
+
const cutoffTime = now - config.sampling.bufferBefore * 1e3;
|
|
14929
|
+
const prevLogCount = logs.length;
|
|
14930
|
+
const prevRrwebCount = rrwebEvents.length;
|
|
14931
|
+
logs = logs.filter((log) => log.timestamp >= cutoffTime);
|
|
14932
|
+
const recentEvents = rrwebEvents.filter((event) => event.timestamp >= cutoffTime);
|
|
14933
|
+
const hasFullSnapshot = recentEvents.some((event) => event.type === 2);
|
|
14934
|
+
if (hasFullSnapshot) {
|
|
14935
|
+
rrwebEvents = recentEvents;
|
|
14936
|
+
} else {
|
|
14937
|
+
let firstFullSnapshotIndex = -1;
|
|
14938
|
+
const fullSnapshotIndices = [];
|
|
14939
|
+
for (let i = 0; i < rrwebEvents.length; i++) {
|
|
14940
|
+
const event = rrwebEvents[i];
|
|
14941
|
+
if (event && event.type === 2 && event.timestamp < cutoffTime) {
|
|
14942
|
+
if (firstFullSnapshotIndex === -1) {
|
|
14943
|
+
firstFullSnapshotIndex = i;
|
|
14944
|
+
}
|
|
14945
|
+
fullSnapshotIndices.push(i);
|
|
14946
|
+
}
|
|
14947
|
+
}
|
|
14948
|
+
if (firstFullSnapshotIndex >= 0) {
|
|
14949
|
+
const snapshotsToKeep = fullSnapshotIndices.map((i) => rrwebEvents[i]).filter((e) => e !== void 0);
|
|
14950
|
+
rrwebEvents = [...snapshotsToKeep, ...recentEvents];
|
|
14951
|
+
} else {
|
|
14952
|
+
rrwebEvents = recentEvents;
|
|
14953
|
+
}
|
|
14954
|
+
}
|
|
14955
|
+
currentSize = logs.reduce((sum, log) => sum + JSON.stringify(log).length, 0);
|
|
14956
|
+
rrwebSize = rrwebEvents.reduce((sum, event) => sum + JSON.stringify(event).length, 0);
|
|
14957
|
+
rebuildSessionStats();
|
|
14958
|
+
const trimmedLogs = prevLogCount - logs.length;
|
|
14959
|
+
const trimmedRrweb = prevRrwebCount - rrwebEvents.length;
|
|
14960
|
+
if (config.debug && (trimmedLogs > 0 || trimmedRrweb > 0)) {
|
|
14961
|
+
console.log(
|
|
14962
|
+
`[LogSpace] Rolling buffer trimmed: ${trimmedLogs} logs, ${trimmedRrweb} rrweb events (keeping last ${config.sampling.bufferBefore}s: ${logs.length} logs, ${rrwebEvents.length} rrweb events)`
|
|
14963
|
+
);
|
|
14964
|
+
}
|
|
14965
|
+
}
|
|
14966
|
+
function startSamplingTrimTimer() {
|
|
14967
|
+
if (samplingTrimTimer || !config?.sampling.enabled) return;
|
|
14968
|
+
samplingTrimTimer = setInterval(() => {
|
|
14969
|
+
trimRollingBuffer();
|
|
14970
|
+
}, SAMPLING_TRIM_INTERVAL);
|
|
14971
|
+
if (config.debug) {
|
|
14972
|
+
console.log(
|
|
14973
|
+
`[LogSpace] Sampling rolling buffer started (trimming every ${SAMPLING_TRIM_INTERVAL / 1e3}s, keeping ${config.sampling.bufferBefore}s)`
|
|
14974
|
+
);
|
|
14975
|
+
}
|
|
14976
|
+
}
|
|
14977
|
+
function stopSamplingTrimTimer() {
|
|
14978
|
+
if (samplingTrimTimer) {
|
|
14979
|
+
clearInterval(samplingTrimTimer);
|
|
14980
|
+
samplingTrimTimer = null;
|
|
14981
|
+
}
|
|
14982
|
+
}
|
|
14721
14983
|
const handleLog = (logData) => {
|
|
14722
14984
|
if (!config || !session || session.status !== "recording") return;
|
|
14723
14985
|
if (!checkRateLimit()) return;
|
|
@@ -14729,40 +14991,31 @@ const handleLog = (logData) => {
|
|
|
14729
14991
|
console.log(`[LogSpace] Session auto-ended: ${limitReached}`);
|
|
14730
14992
|
}
|
|
14731
14993
|
endReason = limitReached;
|
|
14732
|
-
sessionEndedByHardLimit = true;
|
|
14733
14994
|
config.hooks.onLimitReached?.(limitReached);
|
|
14734
|
-
|
|
14995
|
+
stopSessionInternal({ restartImmediately: true });
|
|
14735
14996
|
}
|
|
14736
14997
|
return;
|
|
14737
14998
|
}
|
|
14738
|
-
const tabId =
|
|
14999
|
+
const tabId = getOrCreateTabId();
|
|
14739
15000
|
const log = {
|
|
14740
15001
|
id: `${logs.length + 1}`,
|
|
14741
15002
|
timestamp: Date.now(),
|
|
14742
15003
|
// Use absolute timestamp (consistent with rrweb events)
|
|
14743
15004
|
tabId,
|
|
14744
|
-
// Tab ID from multi-tab coordinator
|
|
14745
15005
|
tabUrl: typeof window !== "undefined" ? window.location.href : "",
|
|
14746
15006
|
type: logData.type,
|
|
14747
15007
|
data: logData.data,
|
|
14748
15008
|
severity: logData.severity
|
|
14749
15009
|
};
|
|
14750
|
-
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
console.log("[LogSpace] Captured (sent to leader):", log.type, log.data);
|
|
14755
|
-
}
|
|
14756
|
-
return;
|
|
14757
|
-
}
|
|
14758
|
-
logs.push(log);
|
|
14759
|
-
currentSize += JSON.stringify(log).length;
|
|
14760
|
-
updateStats(log);
|
|
15010
|
+
const maskedLog = applyPrivacy(log, config.privacy);
|
|
15011
|
+
logs.push(maskedLog);
|
|
15012
|
+
currentSize += JSON.stringify(maskedLog).length;
|
|
15013
|
+
updateStats(maskedLog);
|
|
14761
15014
|
resetIdleTimer();
|
|
14762
|
-
checkSamplingTrigger(
|
|
14763
|
-
config.hooks.onAction?.(
|
|
15015
|
+
checkSamplingTrigger(maskedLog);
|
|
15016
|
+
config.hooks.onAction?.(maskedLog, session);
|
|
14764
15017
|
if (config.debug) {
|
|
14765
|
-
console.log("[LogSpace] Captured:",
|
|
15018
|
+
console.log("[LogSpace] Captured:", maskedLog.type, maskedLog.data);
|
|
14766
15019
|
}
|
|
14767
15020
|
};
|
|
14768
15021
|
function updateStats(log) {
|
|
@@ -14795,6 +15048,24 @@ function updateStats(log) {
|
|
|
14795
15048
|
break;
|
|
14796
15049
|
}
|
|
14797
15050
|
}
|
|
15051
|
+
function rebuildSessionStats() {
|
|
15052
|
+
if (!session) return;
|
|
15053
|
+
const tabCount = session.stats.tabCount || 1;
|
|
15054
|
+
session.stats = {
|
|
15055
|
+
logCount: 0,
|
|
15056
|
+
networkLogCount: 0,
|
|
15057
|
+
consoleLogCount: 0,
|
|
15058
|
+
errorCount: 0,
|
|
15059
|
+
storageLogCount: 0,
|
|
15060
|
+
interactionLogCount: 0,
|
|
15061
|
+
performanceLogCount: 0,
|
|
15062
|
+
annotationCount: 0,
|
|
15063
|
+
tabCount
|
|
15064
|
+
};
|
|
15065
|
+
for (const log of logs) {
|
|
15066
|
+
updateStats(log);
|
|
15067
|
+
}
|
|
15068
|
+
}
|
|
14798
15069
|
function installCaptures() {
|
|
14799
15070
|
if (!config) return;
|
|
14800
15071
|
const privacyWithSdkExclusions = { ...config.privacy };
|
|
@@ -14806,16 +15077,16 @@ function installCaptures() {
|
|
|
14806
15077
|
consoleCapture.install(handleLog, config.privacy);
|
|
14807
15078
|
}
|
|
14808
15079
|
if (config.capture.network) {
|
|
14809
|
-
networkCapture.install(handleLog, privacyWithSdkExclusions);
|
|
15080
|
+
networkCapture.install(handleLog, privacyWithSdkExclusions, resetIdleTimer, config.limits);
|
|
14810
15081
|
}
|
|
14811
15082
|
if (config.capture.errors) {
|
|
14812
15083
|
errorCapture.install(handleLog, config.privacy);
|
|
14813
15084
|
}
|
|
14814
15085
|
if (config.capture.websocket) {
|
|
14815
|
-
websocketCapture.install(handleLog, config.privacy);
|
|
15086
|
+
websocketCapture.install(handleLog, config.privacy, resetIdleTimer, config.limits);
|
|
14816
15087
|
}
|
|
14817
15088
|
if (config.capture.sse) {
|
|
14818
|
-
sseCapture.install(handleLog, config.privacy);
|
|
15089
|
+
sseCapture.install(handleLog, config.privacy, resetIdleTimer, config.limits);
|
|
14819
15090
|
}
|
|
14820
15091
|
if (config.capture.storage) {
|
|
14821
15092
|
storageCapture.install(handleLog, config.privacy);
|
|
@@ -14846,12 +15117,10 @@ function uninstallCaptures() {
|
|
|
14846
15117
|
spaNavigationCapture.uninstall();
|
|
14847
15118
|
}
|
|
14848
15119
|
let unloadHandled = false;
|
|
15120
|
+
let isNavigatingAway = false;
|
|
14849
15121
|
const CHECKPOINT_INTERVAL = 15 * 1e3;
|
|
14850
15122
|
async function saveCheckpoint() {
|
|
14851
15123
|
if (!session || session.status !== "recording" || !isIndexedDBAvailable()) return;
|
|
14852
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
14853
|
-
return;
|
|
14854
|
-
}
|
|
14855
15124
|
try {
|
|
14856
15125
|
await saveSessionCheckpoint(session.id, logs, rrwebEvents, {
|
|
14857
15126
|
startTime: session.startTime,
|
|
@@ -14886,6 +15155,37 @@ function stopCheckpointTimer() {
|
|
|
14886
15155
|
}
|
|
14887
15156
|
}
|
|
14888
15157
|
const EMERGENCY_BACKUP_KEY = "__logspace_emergency_backup";
|
|
15158
|
+
const SAMPLING_STATE_KEY = "__logspace_sampling_state";
|
|
15159
|
+
function saveSamplingState(sessionId) {
|
|
15160
|
+
if (!samplingTriggered) return;
|
|
15161
|
+
try {
|
|
15162
|
+
const state2 = {
|
|
15163
|
+
triggered: samplingTriggered,
|
|
15164
|
+
triggerType: samplingTriggerType,
|
|
15165
|
+
firstTriggerTime: samplingFirstTriggerTime,
|
|
15166
|
+
triggerTime: samplingTriggerTime,
|
|
15167
|
+
sessionId
|
|
15168
|
+
};
|
|
15169
|
+
localStorage.setItem(SAMPLING_STATE_KEY, JSON.stringify(state2));
|
|
15170
|
+
} catch (error) {
|
|
15171
|
+
}
|
|
15172
|
+
}
|
|
15173
|
+
function getSamplingState() {
|
|
15174
|
+
try {
|
|
15175
|
+
const state2 = localStorage.getItem(SAMPLING_STATE_KEY);
|
|
15176
|
+
if (state2) {
|
|
15177
|
+
return JSON.parse(state2);
|
|
15178
|
+
}
|
|
15179
|
+
} catch (error) {
|
|
15180
|
+
}
|
|
15181
|
+
return null;
|
|
15182
|
+
}
|
|
15183
|
+
function clearSamplingState() {
|
|
15184
|
+
try {
|
|
15185
|
+
localStorage.removeItem(SAMPLING_STATE_KEY);
|
|
15186
|
+
} catch (error) {
|
|
15187
|
+
}
|
|
15188
|
+
}
|
|
14889
15189
|
function saveEmergencyBackup(payload) {
|
|
14890
15190
|
try {
|
|
14891
15191
|
if (payload.logs.length === 0 && (!payload.rrwebEvents || payload.rrwebEvents.length < 3)) {
|
|
@@ -14915,16 +15215,9 @@ function clearEmergencyBackup() {
|
|
|
14915
15215
|
function handleUnload() {
|
|
14916
15216
|
if (unloadHandled) return;
|
|
14917
15217
|
if (!config || !session || session.status !== "recording") return;
|
|
14918
|
-
|
|
14919
|
-
if (config.debug) {
|
|
14920
|
-
console.log("[LogSpace] Follower tab unloading - no session to save (data is with leader)");
|
|
14921
|
-
}
|
|
14922
|
-
return;
|
|
14923
|
-
}
|
|
15218
|
+
isNavigatingAway = true;
|
|
14924
15219
|
unloadHandled = true;
|
|
14925
15220
|
endReason = "unload";
|
|
14926
|
-
const payload = buildPayload();
|
|
14927
|
-
if (!payload) return;
|
|
14928
15221
|
if (config.autoEnd.continueOnRefresh) {
|
|
14929
15222
|
try {
|
|
14930
15223
|
sessionStorage.setItem("logspace_continuing_session_id", session.id);
|
|
@@ -14935,6 +15228,13 @@ function handleUnload() {
|
|
|
14935
15228
|
}
|
|
14936
15229
|
} catch {
|
|
14937
15230
|
}
|
|
15231
|
+
if (samplingTriggered) {
|
|
15232
|
+
saveSamplingState(session.id);
|
|
15233
|
+
}
|
|
15234
|
+
}
|
|
15235
|
+
const payload = buildPayload();
|
|
15236
|
+
if (!payload) {
|
|
15237
|
+
return;
|
|
14938
15238
|
}
|
|
14939
15239
|
saveEmergencyBackup(payload);
|
|
14940
15240
|
if (isIndexedDBAvailable()) {
|
|
@@ -14942,7 +15242,7 @@ function handleUnload() {
|
|
|
14942
15242
|
});
|
|
14943
15243
|
}
|
|
14944
15244
|
if (config.debug) {
|
|
14945
|
-
console.log("[LogSpace] Session saved for
|
|
15245
|
+
console.log("[LogSpace] Session paused and saved for continuation");
|
|
14946
15246
|
}
|
|
14947
15247
|
}
|
|
14948
15248
|
function handleVisibilityChange() {
|
|
@@ -14954,7 +15254,7 @@ function handleVisibilityChange() {
|
|
|
14954
15254
|
}
|
|
14955
15255
|
if (config.capture.rrweb) {
|
|
14956
15256
|
addRRWebCustomEvent("tab-hidden", {
|
|
14957
|
-
tabId:
|
|
15257
|
+
tabId: getOrCreateTabId(),
|
|
14958
15258
|
timestamp: Date.now()
|
|
14959
15259
|
});
|
|
14960
15260
|
}
|
|
@@ -14963,14 +15263,8 @@ function handleVisibilityChange() {
|
|
|
14963
15263
|
}
|
|
14964
15264
|
visibilityTimer = setTimeout(() => {
|
|
14965
15265
|
if (session?.status === "recording" && document.visibilityState === "hidden") {
|
|
14966
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
14967
|
-
if (config?.debug) {
|
|
14968
|
-
console.log("[LogSpace] Grace period expired, but follower tab - not ending session");
|
|
14969
|
-
}
|
|
14970
|
-
return;
|
|
14971
|
-
}
|
|
14972
15266
|
if (config?.debug) {
|
|
14973
|
-
console.log("[LogSpace] Grace period expired,
|
|
15267
|
+
console.log("[LogSpace] Grace period expired, saving session for continuation");
|
|
14974
15268
|
}
|
|
14975
15269
|
endReason = "navigateAway";
|
|
14976
15270
|
handleUnload();
|
|
@@ -14984,8 +15278,20 @@ function handleVisibilityChange() {
|
|
|
14984
15278
|
clearTimeout(visibilityTimer);
|
|
14985
15279
|
visibilityTimer = null;
|
|
14986
15280
|
}
|
|
15281
|
+
if (isNavigatingAway || unloadHandled) {
|
|
15282
|
+
const wasNavigating = isNavigatingAway;
|
|
15283
|
+
const wasUnloadHandled = unloadHandled;
|
|
15284
|
+
isNavigatingAway = false;
|
|
15285
|
+
unloadHandled = false;
|
|
15286
|
+
if (config?.debug) {
|
|
15287
|
+
console.log("[LogSpace] Navigation flags reset - page is visible", {
|
|
15288
|
+
wasNavigating,
|
|
15289
|
+
wasUnloadHandled
|
|
15290
|
+
});
|
|
15291
|
+
}
|
|
15292
|
+
}
|
|
14987
15293
|
if (config?.capture.rrweb && session?.status === "recording") {
|
|
14988
|
-
const tabId =
|
|
15294
|
+
const tabId = getOrCreateTabId();
|
|
14989
15295
|
if (config.debug) {
|
|
14990
15296
|
console.log("[LogSpace] Tab visible, forcing full rrweb snapshot for:", tabId);
|
|
14991
15297
|
}
|
|
@@ -15005,8 +15311,13 @@ function buildPayload() {
|
|
|
15005
15311
|
if (!session) return null;
|
|
15006
15312
|
if (config?.sampling.enabled && !samplingTriggered) {
|
|
15007
15313
|
if (config.debug) {
|
|
15008
|
-
console.log("[LogSpace] Sampling enabled but not triggered - session
|
|
15314
|
+
console.log("[LogSpace] Sampling enabled but not triggered - session discarded");
|
|
15009
15315
|
}
|
|
15316
|
+
let discardReason = "noTrigger";
|
|
15317
|
+
if (endReason === "idle") discardReason = "idle";
|
|
15318
|
+
else if (endReason === "maxDuration") discardReason = "maxDuration";
|
|
15319
|
+
else if (endReason === "manual") discardReason = "manual";
|
|
15320
|
+
config.hooks.onSessionDiscarded?.(session, discardReason);
|
|
15010
15321
|
return null;
|
|
15011
15322
|
}
|
|
15012
15323
|
if (config?.sampling.enabled && samplingTriggered) {
|
|
@@ -15038,6 +15349,65 @@ function buildPayload() {
|
|
|
15038
15349
|
}
|
|
15039
15350
|
return payload;
|
|
15040
15351
|
}
|
|
15352
|
+
async function stopSessionInternal(options) {
|
|
15353
|
+
if (!session || session.status === "stopped") {
|
|
15354
|
+
return;
|
|
15355
|
+
}
|
|
15356
|
+
const shouldRestart = options?.restartImmediately === true;
|
|
15357
|
+
const endedSessionId = session.id;
|
|
15358
|
+
session.status = "stopped";
|
|
15359
|
+
session.endTime = Date.now();
|
|
15360
|
+
continuingSessionId = null;
|
|
15361
|
+
if (idleTimer) {
|
|
15362
|
+
clearTimeout(idleTimer);
|
|
15363
|
+
idleTimer = null;
|
|
15364
|
+
}
|
|
15365
|
+
if (durationTimer) {
|
|
15366
|
+
clearTimeout(durationTimer);
|
|
15367
|
+
durationTimer = null;
|
|
15368
|
+
}
|
|
15369
|
+
if (visibilityTimer) {
|
|
15370
|
+
clearTimeout(visibilityTimer);
|
|
15371
|
+
visibilityTimer = null;
|
|
15372
|
+
}
|
|
15373
|
+
stopCheckpointTimer();
|
|
15374
|
+
stopSamplingTrimTimer();
|
|
15375
|
+
if (config?.capture.rrweb) {
|
|
15376
|
+
const events = stopRRWebRecording();
|
|
15377
|
+
if (config.debug) {
|
|
15378
|
+
console.log("[LogSpace] rrweb recording stopped, events:", events.length);
|
|
15379
|
+
}
|
|
15380
|
+
}
|
|
15381
|
+
uninstallCaptures();
|
|
15382
|
+
config?.hooks.onSessionEnd?.(session, logs);
|
|
15383
|
+
if (config?.debug) {
|
|
15384
|
+
console.log("[LogSpace] Session stopped:", session.id, {
|
|
15385
|
+
duration: session.endTime - session.startTime,
|
|
15386
|
+
logs: logs.length,
|
|
15387
|
+
rrwebEvents: rrwebEvents.length,
|
|
15388
|
+
reason: endReason
|
|
15389
|
+
});
|
|
15390
|
+
}
|
|
15391
|
+
const payload = buildPayload();
|
|
15392
|
+
if (shouldRestart) {
|
|
15393
|
+
LogSpace.startSession();
|
|
15394
|
+
}
|
|
15395
|
+
if (!payload) {
|
|
15396
|
+
await clearCurrentSessionBackupFor(endedSessionId);
|
|
15397
|
+
clearEmergencyBackup();
|
|
15398
|
+
clearSamplingState();
|
|
15399
|
+
return;
|
|
15400
|
+
}
|
|
15401
|
+
try {
|
|
15402
|
+
await sendPayload(payload);
|
|
15403
|
+
await clearCurrentSessionBackupFor(endedSessionId);
|
|
15404
|
+
clearEmergencyBackup();
|
|
15405
|
+
clearSamplingState();
|
|
15406
|
+
} catch (error) {
|
|
15407
|
+
await addPendingSession(payload);
|
|
15408
|
+
throw error;
|
|
15409
|
+
}
|
|
15410
|
+
}
|
|
15041
15411
|
async function compressPayload(data) {
|
|
15042
15412
|
const encoder = new TextEncoder();
|
|
15043
15413
|
const inputData = encoder.encode(data);
|
|
@@ -15064,9 +15434,7 @@ async function compressPayload(data) {
|
|
|
15064
15434
|
}
|
|
15065
15435
|
return new Blob([inputData], { type: "application/json" });
|
|
15066
15436
|
}
|
|
15067
|
-
async function
|
|
15068
|
-
const payload = buildPayload();
|
|
15069
|
-
if (!payload) return;
|
|
15437
|
+
async function sendPayload(payload) {
|
|
15070
15438
|
if (config?.dryRun) {
|
|
15071
15439
|
console.log("[LogSpace] 🔶 DRY RUN - Session payload:", {
|
|
15072
15440
|
id: payload.id,
|
|
@@ -15116,8 +15484,8 @@ async function sendSession() {
|
|
|
15116
15484
|
// Use actual session start time for correct video seek calculation
|
|
15117
15485
|
// SDK-specific fields for identify() and session info
|
|
15118
15486
|
url: payload.url,
|
|
15119
|
-
userId:
|
|
15120
|
-
userTraits:
|
|
15487
|
+
userId: payload.userId,
|
|
15488
|
+
userTraits: payload.userTraits,
|
|
15121
15489
|
duration: payload.duration,
|
|
15122
15490
|
stats: payload.stats
|
|
15123
15491
|
};
|
|
@@ -15207,6 +15575,27 @@ async function sendPayloadToServer(payload) {
|
|
|
15207
15575
|
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
|
15208
15576
|
}
|
|
15209
15577
|
}
|
|
15578
|
+
function serializeDedupValue(value) {
|
|
15579
|
+
try {
|
|
15580
|
+
return JSON.stringify(value);
|
|
15581
|
+
} catch {
|
|
15582
|
+
return String(value);
|
|
15583
|
+
}
|
|
15584
|
+
}
|
|
15585
|
+
function getLogDedupKey(log) {
|
|
15586
|
+
if (log.id) {
|
|
15587
|
+
return `id:${log.id}|ts:${log.timestamp}|type:${log.type}`;
|
|
15588
|
+
}
|
|
15589
|
+
return `ts:${log.timestamp}|type:${log.type}|data:${serializeDedupValue(log.data)}`;
|
|
15590
|
+
}
|
|
15591
|
+
function getEventDedupKey(event) {
|
|
15592
|
+
const eventId = event.id ?? event.data?.id;
|
|
15593
|
+
if (eventId !== void 0 && eventId !== null) {
|
|
15594
|
+
return `id:${String(eventId)}|ts:${event.timestamp}|type:${event.type}`;
|
|
15595
|
+
}
|
|
15596
|
+
const data = event.data;
|
|
15597
|
+
return `ts:${event.timestamp}|type:${event.type}|data:${serializeDedupValue(data)}`;
|
|
15598
|
+
}
|
|
15210
15599
|
function mergeRecoveredData(recoveredLogs, recoveredEvents) {
|
|
15211
15600
|
if (!session || session.status !== "recording") {
|
|
15212
15601
|
if (config?.debug) {
|
|
@@ -15215,8 +15604,16 @@ function mergeRecoveredData(recoveredLogs, recoveredEvents) {
|
|
|
15215
15604
|
return;
|
|
15216
15605
|
}
|
|
15217
15606
|
if (recoveredLogs.length > 0) {
|
|
15218
|
-
const
|
|
15219
|
-
const newLogs =
|
|
15607
|
+
const existingLogKeys = new Set(logs.map((l) => getLogDedupKey(l)));
|
|
15608
|
+
const newLogs = [];
|
|
15609
|
+
for (const log of recoveredLogs) {
|
|
15610
|
+
const key = getLogDedupKey(log);
|
|
15611
|
+
if (existingLogKeys.has(key)) {
|
|
15612
|
+
continue;
|
|
15613
|
+
}
|
|
15614
|
+
existingLogKeys.add(key);
|
|
15615
|
+
newLogs.push(log);
|
|
15616
|
+
}
|
|
15220
15617
|
if (newLogs.length > 0) {
|
|
15221
15618
|
logs = [...newLogs, ...logs];
|
|
15222
15619
|
for (const log of newLogs) {
|
|
@@ -15233,8 +15630,16 @@ function mergeRecoveredData(recoveredLogs, recoveredEvents) {
|
|
|
15233
15630
|
}
|
|
15234
15631
|
}
|
|
15235
15632
|
if (recoveredEvents.length > 0) {
|
|
15236
|
-
const
|
|
15237
|
-
const newEvents =
|
|
15633
|
+
const existingEventKeys = new Set(rrwebEvents.map((e) => getEventDedupKey(e)));
|
|
15634
|
+
const newEvents = [];
|
|
15635
|
+
for (const event of recoveredEvents) {
|
|
15636
|
+
const key = getEventDedupKey(event);
|
|
15637
|
+
if (existingEventKeys.has(key)) {
|
|
15638
|
+
continue;
|
|
15639
|
+
}
|
|
15640
|
+
existingEventKeys.add(key);
|
|
15641
|
+
newEvents.push(event);
|
|
15642
|
+
}
|
|
15238
15643
|
if (newEvents.length > 0) {
|
|
15239
15644
|
rrwebEvents = [...newEvents, ...rrwebEvents];
|
|
15240
15645
|
for (const event of newEvents) {
|
|
@@ -15255,11 +15660,17 @@ async function recoverPendingSessions() {
|
|
|
15255
15660
|
try {
|
|
15256
15661
|
const emergencyBackup = getEmergencyBackup();
|
|
15257
15662
|
if (emergencyBackup) {
|
|
15258
|
-
if (
|
|
15663
|
+
if (processedRecoverySessionIds.has(emergencyBackup.id)) {
|
|
15664
|
+
if (config.debug) {
|
|
15665
|
+
console.log("[LogSpace] Skipping already processed emergency backup:", emergencyBackup.id);
|
|
15666
|
+
}
|
|
15667
|
+
clearEmergencyBackup();
|
|
15668
|
+
} else if (recoveryTargetSessionId && emergencyBackup.id === recoveryTargetSessionId) {
|
|
15259
15669
|
if (config.debug) {
|
|
15260
15670
|
console.log("[LogSpace] Merging emergency backup into continuing session:", emergencyBackup.id);
|
|
15261
15671
|
}
|
|
15262
15672
|
mergeRecoveredData(emergencyBackup.logs, emergencyBackup.rrwebEvents || []);
|
|
15673
|
+
processedRecoverySessionIds.add(emergencyBackup.id);
|
|
15263
15674
|
clearEmergencyBackup();
|
|
15264
15675
|
await clearCurrentSessionBackup();
|
|
15265
15676
|
} else {
|
|
@@ -15293,44 +15704,59 @@ async function recoverPendingSessions() {
|
|
|
15293
15704
|
if (pendingSessions.length === 0) {
|
|
15294
15705
|
const backup = await getCurrentSessionBackup();
|
|
15295
15706
|
if (backup && backup.logs.length > 0) {
|
|
15296
|
-
if (
|
|
15297
|
-
|
|
15298
|
-
|
|
15299
|
-
|
|
15300
|
-
|
|
15301
|
-
|
|
15302
|
-
|
|
15303
|
-
|
|
15304
|
-
|
|
15305
|
-
|
|
15306
|
-
|
|
15307
|
-
|
|
15308
|
-
|
|
15309
|
-
|
|
15310
|
-
|
|
15311
|
-
|
|
15312
|
-
|
|
15313
|
-
|
|
15314
|
-
|
|
15315
|
-
|
|
15316
|
-
|
|
15317
|
-
|
|
15318
|
-
|
|
15319
|
-
|
|
15320
|
-
|
|
15321
|
-
|
|
15322
|
-
|
|
15323
|
-
|
|
15324
|
-
|
|
15325
|
-
|
|
15326
|
-
|
|
15327
|
-
|
|
15328
|
-
|
|
15329
|
-
|
|
15330
|
-
|
|
15331
|
-
|
|
15332
|
-
|
|
15333
|
-
|
|
15707
|
+
if (recoveryTargetSessionId && backup.sessionId === recoveryTargetSessionId) {
|
|
15708
|
+
if (config.debug) {
|
|
15709
|
+
console.log("[LogSpace] Merging checkpoint backup into continuing session:", backup.sessionId);
|
|
15710
|
+
}
|
|
15711
|
+
mergeRecoveredData(backup.logs, backup.rrwebEvents || []);
|
|
15712
|
+
processedRecoverySessionIds.add(backup.sessionId);
|
|
15713
|
+
await clearCurrentSessionBackup();
|
|
15714
|
+
} else if (processedRecoverySessionIds.has(backup.sessionId)) {
|
|
15715
|
+
if (config.debug) {
|
|
15716
|
+
console.log("[LogSpace] Skipping already processed backup:", backup.sessionId);
|
|
15717
|
+
}
|
|
15718
|
+
await clearCurrentSessionBackup();
|
|
15719
|
+
} else {
|
|
15720
|
+
if (config.debug) {
|
|
15721
|
+
console.log("[LogSpace] Recovering crashed session:", backup.sessionId);
|
|
15722
|
+
}
|
|
15723
|
+
const recoveredPayload = {
|
|
15724
|
+
id: backup.sessionId,
|
|
15725
|
+
startTime: backup.startTime,
|
|
15726
|
+
endTime: backup.lastCheckpoint,
|
|
15727
|
+
duration: backup.lastCheckpoint - backup.startTime,
|
|
15728
|
+
url: backup.url,
|
|
15729
|
+
title: backup.title,
|
|
15730
|
+
userId: backup.userId,
|
|
15731
|
+
userTraits: backup.userTraits,
|
|
15732
|
+
metadata: backup.metadata,
|
|
15733
|
+
logs: backup.logs,
|
|
15734
|
+
stats: {
|
|
15735
|
+
logCount: backup.logs.length,
|
|
15736
|
+
networkLogCount: backup.logs.filter((l) => l.type === "network").length,
|
|
15737
|
+
consoleLogCount: backup.logs.filter((l) => l.type === "console").length,
|
|
15738
|
+
errorCount: backup.logs.filter((l) => l.type === "error").length,
|
|
15739
|
+
storageLogCount: backup.logs.filter((l) => l.type === "storage").length,
|
|
15740
|
+
interactionLogCount: backup.logs.filter((l) => l.type === "interaction").length,
|
|
15741
|
+
performanceLogCount: backup.logs.filter((l) => l.type === "performance").length,
|
|
15742
|
+
annotationCount: backup.logs.filter((l) => l.type === "annotation").length,
|
|
15743
|
+
tabCount: 1
|
|
15744
|
+
// SDK always has single tab
|
|
15745
|
+
},
|
|
15746
|
+
endReason: "crash",
|
|
15747
|
+
recordingType: "rrweb",
|
|
15748
|
+
rrwebEvents: backup.rrwebEvents,
|
|
15749
|
+
sdkVersion: SDK_VERSION
|
|
15750
|
+
};
|
|
15751
|
+
pendingSessions.push({
|
|
15752
|
+
id: backup.sessionId,
|
|
15753
|
+
data: recoveredPayload,
|
|
15754
|
+
createdAt: backup.lastCheckpoint,
|
|
15755
|
+
retryCount: 0
|
|
15756
|
+
});
|
|
15757
|
+
await addPendingSession(recoveredPayload);
|
|
15758
|
+
await clearCurrentSessionBackup();
|
|
15759
|
+
}
|
|
15334
15760
|
}
|
|
15335
15761
|
}
|
|
15336
15762
|
if (pendingSessions.length === 0) return;
|
|
@@ -15338,16 +15764,25 @@ async function recoverPendingSessions() {
|
|
|
15338
15764
|
console.log(`[LogSpace] Recovering ${pendingSessions.length} pending session(s)`);
|
|
15339
15765
|
}
|
|
15340
15766
|
for (const stored of pendingSessions) {
|
|
15767
|
+
if (processedRecoverySessionIds.has(stored.id)) {
|
|
15768
|
+
if (config.debug) {
|
|
15769
|
+
console.log("[LogSpace] Skipping already processed pending session:", stored.id);
|
|
15770
|
+
}
|
|
15771
|
+
await removePendingSession(stored.id);
|
|
15772
|
+
continue;
|
|
15773
|
+
}
|
|
15341
15774
|
if (recoveryTargetSessionId && stored.id === recoveryTargetSessionId) {
|
|
15342
15775
|
if (config.debug) {
|
|
15343
15776
|
console.log("[LogSpace] Merging pending session into continuing session:", stored.id);
|
|
15344
15777
|
}
|
|
15345
15778
|
mergeRecoveredData(stored.data.logs, stored.data.rrwebEvents || []);
|
|
15779
|
+
processedRecoverySessionIds.add(stored.id);
|
|
15346
15780
|
await removePendingSession(stored.id);
|
|
15347
15781
|
continue;
|
|
15348
15782
|
}
|
|
15349
15783
|
try {
|
|
15350
15784
|
await sendPayloadToServer(stored.data);
|
|
15785
|
+
processedRecoverySessionIds.add(stored.id);
|
|
15351
15786
|
await removePendingSession(stored.id);
|
|
15352
15787
|
if (config.debug) {
|
|
15353
15788
|
console.log("[LogSpace] Recovered session sent:", stored.id);
|
|
@@ -15370,11 +15805,11 @@ async function recoverPendingSessions() {
|
|
|
15370
15805
|
const LogSpace = {
|
|
15371
15806
|
/**
|
|
15372
15807
|
* Initialize the SDK
|
|
15808
|
+
* @throws Error if SDK is already initialized
|
|
15373
15809
|
*/
|
|
15374
15810
|
init(userConfig) {
|
|
15375
15811
|
if (initialized) {
|
|
15376
|
-
|
|
15377
|
-
return;
|
|
15812
|
+
throw new Error("[LogSpace] SDK already initialized. Call destroy() first if you need to reinitialize.");
|
|
15378
15813
|
}
|
|
15379
15814
|
if (typeof window === "undefined") {
|
|
15380
15815
|
console.warn("[LogSpace] SDK requires browser environment");
|
|
@@ -15387,54 +15822,9 @@ const LogSpace = {
|
|
|
15387
15822
|
version: SDK_VERSION,
|
|
15388
15823
|
serverUrl: config.serverUrl,
|
|
15389
15824
|
limits: config.limits,
|
|
15390
|
-
persistence: isIndexedDBAvailable() ? "enabled" : "disabled"
|
|
15391
|
-
multiTab: multiTabCoordinator.isSupported() ? "enabled" : "disabled"
|
|
15825
|
+
persistence: isIndexedDBAvailable() ? "enabled" : "disabled"
|
|
15392
15826
|
});
|
|
15393
15827
|
}
|
|
15394
|
-
if (multiTabCoordinator.isSupported()) {
|
|
15395
|
-
multiTabCoordinator.init(
|
|
15396
|
-
{
|
|
15397
|
-
onBecomeLeader: (sessionId, isNewSession) => {
|
|
15398
|
-
if (config?.debug) {
|
|
15399
|
-
console.log("[LogSpace] This tab is now the leader", { sessionId, isNewSession });
|
|
15400
|
-
}
|
|
15401
|
-
if (!isNewSession) {
|
|
15402
|
-
continuingSessionId = sessionId;
|
|
15403
|
-
}
|
|
15404
|
-
if (!session) {
|
|
15405
|
-
LogSpace.startSession();
|
|
15406
|
-
}
|
|
15407
|
-
},
|
|
15408
|
-
onBecomeFollower: (sessionId) => {
|
|
15409
|
-
if (config?.debug) {
|
|
15410
|
-
console.log("[LogSpace] This tab is now a follower", { sessionId });
|
|
15411
|
-
}
|
|
15412
|
-
if (!session) {
|
|
15413
|
-
LogSpace.startSession();
|
|
15414
|
-
}
|
|
15415
|
-
},
|
|
15416
|
-
onReceiveLog: (log) => {
|
|
15417
|
-
if (session?.status === "recording") {
|
|
15418
|
-
logs.push(log);
|
|
15419
|
-
currentSize += JSON.stringify(log).length;
|
|
15420
|
-
updateStats(log);
|
|
15421
|
-
}
|
|
15422
|
-
},
|
|
15423
|
-
onReceiveRRWebEvent: (event) => {
|
|
15424
|
-
if (session?.status === "recording") {
|
|
15425
|
-
rrwebEvents.push(event);
|
|
15426
|
-
rrwebSize += JSON.stringify(event).length;
|
|
15427
|
-
}
|
|
15428
|
-
},
|
|
15429
|
-
onLeaderChanged: (newLeaderTabId) => {
|
|
15430
|
-
if (config?.debug) {
|
|
15431
|
-
console.log("[LogSpace] Leader changed to:", newLeaderTabId);
|
|
15432
|
-
}
|
|
15433
|
-
}
|
|
15434
|
-
},
|
|
15435
|
-
config.debug
|
|
15436
|
-
);
|
|
15437
|
-
}
|
|
15438
15828
|
if (config.autoEnd.continueOnRefresh) {
|
|
15439
15829
|
try {
|
|
15440
15830
|
const savedSessionId = sessionStorage.getItem("logspace_continuing_session_id");
|
|
@@ -15481,9 +15871,8 @@ const LogSpace = {
|
|
|
15481
15871
|
installActivityListeners();
|
|
15482
15872
|
}
|
|
15483
15873
|
unloadHandled = false;
|
|
15484
|
-
|
|
15485
|
-
|
|
15486
|
-
}
|
|
15874
|
+
isNavigatingAway = false;
|
|
15875
|
+
this.startSession();
|
|
15487
15876
|
recoverPendingSessions().catch(() => {
|
|
15488
15877
|
});
|
|
15489
15878
|
},
|
|
@@ -15499,20 +15888,16 @@ const LogSpace = {
|
|
|
15499
15888
|
console.warn("[LogSpace] Session already recording");
|
|
15500
15889
|
return;
|
|
15501
15890
|
}
|
|
15502
|
-
if (sessionEndedByHardLimit) {
|
|
15503
|
-
if (config.debug) {
|
|
15504
|
-
console.log("[LogSpace] Session not restarted - previous session ended by hard limit");
|
|
15505
|
-
}
|
|
15506
|
-
return;
|
|
15507
|
-
}
|
|
15508
15891
|
sessionEndedByIdle = false;
|
|
15509
15892
|
samplingTriggered = false;
|
|
15510
15893
|
samplingTriggerType = null;
|
|
15894
|
+
samplingFirstTriggerTime = null;
|
|
15511
15895
|
samplingTriggerTime = null;
|
|
15512
15896
|
if (samplingEndTimer) {
|
|
15513
15897
|
clearTimeout(samplingEndTimer);
|
|
15514
15898
|
samplingEndTimer = null;
|
|
15515
15899
|
}
|
|
15900
|
+
stopSamplingTrimTimer();
|
|
15516
15901
|
logs = [];
|
|
15517
15902
|
rrwebEvents = [];
|
|
15518
15903
|
currentSize = 0;
|
|
@@ -15522,28 +15907,44 @@ const LogSpace = {
|
|
|
15522
15907
|
endReason = "manual";
|
|
15523
15908
|
let sessionId;
|
|
15524
15909
|
let sessionStartTime;
|
|
15525
|
-
|
|
15526
|
-
|
|
15527
|
-
sessionStartTime = Date.now();
|
|
15528
|
-
} else if (continuingSessionId) {
|
|
15910
|
+
let restoredSamplingState = false;
|
|
15911
|
+
if (continuingSessionId) {
|
|
15529
15912
|
sessionId = continuingSessionId;
|
|
15530
15913
|
sessionStartTime = continuingSessionStartTime || Date.now();
|
|
15531
15914
|
if (config.debug) {
|
|
15532
15915
|
console.log("[LogSpace] Using continuing session ID:", sessionId, "with original startTime:", sessionStartTime);
|
|
15533
15916
|
}
|
|
15917
|
+
const savedSamplingState = getSamplingState();
|
|
15918
|
+
if (savedSamplingState && savedSamplingState.sessionId === sessionId && savedSamplingState.triggered) {
|
|
15919
|
+
samplingTriggered = savedSamplingState.triggered;
|
|
15920
|
+
samplingTriggerType = savedSamplingState.triggerType;
|
|
15921
|
+
samplingFirstTriggerTime = savedSamplingState.firstTriggerTime;
|
|
15922
|
+
samplingTriggerTime = savedSamplingState.triggerTime;
|
|
15923
|
+
restoredSamplingState = true;
|
|
15924
|
+
if (config.debug) {
|
|
15925
|
+
console.log("[LogSpace] Restored sampling state from before refresh:", savedSamplingState.triggerType);
|
|
15926
|
+
}
|
|
15927
|
+
}
|
|
15928
|
+
clearSamplingState();
|
|
15534
15929
|
continuingSessionId = null;
|
|
15535
15930
|
continuingSessionStartTime = null;
|
|
15536
15931
|
} else {
|
|
15537
15932
|
sessionId = generateSessionId();
|
|
15538
15933
|
sessionStartTime = Date.now();
|
|
15539
15934
|
}
|
|
15935
|
+
const identityToApply = pendingIdentity || currentIdentity;
|
|
15936
|
+
const tabId = getOrCreateTabId();
|
|
15937
|
+
const sessionMetadata = buildSessionMetadata(metadata);
|
|
15540
15938
|
session = {
|
|
15541
15939
|
id: sessionId,
|
|
15940
|
+
tabId,
|
|
15542
15941
|
startTime: sessionStartTime,
|
|
15543
15942
|
status: "recording",
|
|
15544
15943
|
url: window.location.href,
|
|
15545
15944
|
title: document.title,
|
|
15546
|
-
metadata,
|
|
15945
|
+
metadata: sessionMetadata,
|
|
15946
|
+
userId: identityToApply?.userId,
|
|
15947
|
+
userTraits: identityToApply?.traits,
|
|
15547
15948
|
stats: {
|
|
15548
15949
|
logCount: 0,
|
|
15549
15950
|
networkLogCount: 0,
|
|
@@ -15553,11 +15954,10 @@ const LogSpace = {
|
|
|
15553
15954
|
interactionLogCount: 0,
|
|
15554
15955
|
performanceLogCount: 0,
|
|
15555
15956
|
annotationCount: 0,
|
|
15556
|
-
tabCount:
|
|
15557
|
-
// Will be updated by multi-tab coordinator
|
|
15957
|
+
tabCount: 1
|
|
15558
15958
|
}
|
|
15559
15959
|
};
|
|
15560
|
-
|
|
15960
|
+
setSessionContext(sessionId, tabId);
|
|
15561
15961
|
if (config.capture.rrweb) {
|
|
15562
15962
|
const rrwebStarted = startRRWebRecording(
|
|
15563
15963
|
{
|
|
@@ -15570,23 +15970,17 @@ const LogSpace = {
|
|
|
15570
15970
|
},
|
|
15571
15971
|
(event) => {
|
|
15572
15972
|
if (!session || session.status !== "recording") return;
|
|
15573
|
-
if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
|
|
15574
|
-
multiTabCoordinator.sendRRWebEvent(event);
|
|
15575
|
-
resetIdleTimer();
|
|
15576
|
-
return;
|
|
15577
|
-
}
|
|
15578
15973
|
const eventSize = JSON.stringify(event).length;
|
|
15579
15974
|
rrwebSize += eventSize;
|
|
15580
15975
|
const totalSize = currentSize + rrwebSize;
|
|
15581
15976
|
if (config && totalSize >= config.limits.maxSize) {
|
|
15582
|
-
if (config.autoEnd.onLimitReached
|
|
15583
|
-
sessionEndedByHardLimit = true;
|
|
15977
|
+
if (config.autoEnd.onLimitReached) {
|
|
15584
15978
|
if (config.debug) {
|
|
15585
15979
|
console.log("[LogSpace] Session auto-ended: maxSize (rrweb)");
|
|
15586
15980
|
}
|
|
15587
15981
|
endReason = "maxSize";
|
|
15588
15982
|
config.hooks.onLimitReached?.("maxSize");
|
|
15589
|
-
|
|
15983
|
+
stopSessionInternal({ restartImmediately: true });
|
|
15590
15984
|
}
|
|
15591
15985
|
return;
|
|
15592
15986
|
}
|
|
@@ -15607,13 +16001,47 @@ const LogSpace = {
|
|
|
15607
16001
|
}
|
|
15608
16002
|
endReason = "maxDuration";
|
|
15609
16003
|
config?.hooks.onLimitReached?.("maxDuration");
|
|
15610
|
-
|
|
16004
|
+
stopSessionInternal({ restartImmediately: true });
|
|
15611
16005
|
}
|
|
15612
16006
|
}, config.limits.maxDuration * 1e3);
|
|
15613
16007
|
}
|
|
15614
16008
|
resetIdleTimer();
|
|
15615
16009
|
installCaptures();
|
|
15616
16010
|
startCheckpointTimer();
|
|
16011
|
+
if (config.sampling.enabled && !restoredSamplingState) {
|
|
16012
|
+
startSamplingTrimTimer();
|
|
16013
|
+
} else if (config.sampling.enabled && restoredSamplingState && samplingTriggerTime) {
|
|
16014
|
+
const elapsed = Date.now() - samplingTriggerTime;
|
|
16015
|
+
const remainingTime = Math.max(0, config.sampling.recordAfter * 1e3 - elapsed);
|
|
16016
|
+
if (config.debug) {
|
|
16017
|
+
console.log(`[LogSpace] Sampling was triggered before refresh, ${remainingTime}ms remaining in recordAfter`);
|
|
16018
|
+
}
|
|
16019
|
+
if (remainingTime > 0) {
|
|
16020
|
+
samplingEndTimer = setTimeout(() => {
|
|
16021
|
+
if (session?.status === "recording") {
|
|
16022
|
+
if (config?.debug) {
|
|
16023
|
+
console.log("[LogSpace] Sampling recordAfter period complete, ending session");
|
|
16024
|
+
}
|
|
16025
|
+
endReason = "samplingTriggered";
|
|
16026
|
+
stopSessionInternal({ restartImmediately: true });
|
|
16027
|
+
}
|
|
16028
|
+
}, remainingTime);
|
|
16029
|
+
} else {
|
|
16030
|
+
if (config.debug) {
|
|
16031
|
+
console.log("[LogSpace] Sampling recordAfter period already elapsed, ending session");
|
|
16032
|
+
}
|
|
16033
|
+
endReason = "samplingTriggered";
|
|
16034
|
+
stopSessionInternal({ restartImmediately: true });
|
|
16035
|
+
}
|
|
16036
|
+
}
|
|
16037
|
+
if (session && identityToApply) {
|
|
16038
|
+
if (pendingIdentity && config.debug) {
|
|
16039
|
+
console.log("[LogSpace] Applied pending user identity:", identityToApply.userId);
|
|
16040
|
+
}
|
|
16041
|
+
}
|
|
16042
|
+
if (pendingIdentity) {
|
|
16043
|
+
pendingIdentity = null;
|
|
16044
|
+
}
|
|
15617
16045
|
if (session) {
|
|
15618
16046
|
config.hooks.onSessionStart?.(session);
|
|
15619
16047
|
if (config.debug) {
|
|
@@ -15625,68 +16053,23 @@ const LogSpace = {
|
|
|
15625
16053
|
* Stop recording and send session to server
|
|
15626
16054
|
*/
|
|
15627
16055
|
async stopSession() {
|
|
15628
|
-
|
|
15629
|
-
return;
|
|
15630
|
-
}
|
|
15631
|
-
session.status = "stopped";
|
|
15632
|
-
session.endTime = Date.now();
|
|
15633
|
-
continuingSessionId = null;
|
|
15634
|
-
if (idleTimer) {
|
|
15635
|
-
clearTimeout(idleTimer);
|
|
15636
|
-
idleTimer = null;
|
|
15637
|
-
}
|
|
15638
|
-
if (durationTimer) {
|
|
15639
|
-
clearTimeout(durationTimer);
|
|
15640
|
-
durationTimer = null;
|
|
15641
|
-
}
|
|
15642
|
-
if (visibilityTimer) {
|
|
15643
|
-
clearTimeout(visibilityTimer);
|
|
15644
|
-
visibilityTimer = null;
|
|
15645
|
-
}
|
|
15646
|
-
stopCheckpointTimer();
|
|
15647
|
-
if (config?.capture.rrweb) {
|
|
15648
|
-
const events = stopRRWebRecording();
|
|
15649
|
-
if (config.debug) {
|
|
15650
|
-
console.log("[LogSpace] rrweb recording stopped, events:", events.length);
|
|
15651
|
-
}
|
|
15652
|
-
}
|
|
15653
|
-
uninstallCaptures();
|
|
15654
|
-
config?.hooks.onSessionEnd?.(session, logs);
|
|
15655
|
-
if (config?.debug) {
|
|
15656
|
-
console.log("[LogSpace] Session stopped:", session.id, {
|
|
15657
|
-
duration: session.endTime - session.startTime,
|
|
15658
|
-
logs: logs.length,
|
|
15659
|
-
rrwebEvents: rrwebEvents.length,
|
|
15660
|
-
reason: endReason
|
|
15661
|
-
});
|
|
15662
|
-
}
|
|
15663
|
-
if (!multiTabCoordinator.isActive() || multiTabCoordinator.isLeaderTab()) {
|
|
15664
|
-
try {
|
|
15665
|
-
await sendSession();
|
|
15666
|
-
await clearCurrentSessionBackup();
|
|
15667
|
-
clearEmergencyBackup();
|
|
15668
|
-
if (multiTabCoordinator.isActive()) {
|
|
15669
|
-
multiTabCoordinator.endSession();
|
|
15670
|
-
}
|
|
15671
|
-
} catch (error) {
|
|
15672
|
-
const payload = buildPayload();
|
|
15673
|
-
if (payload) {
|
|
15674
|
-
await addPendingSession(payload);
|
|
15675
|
-
}
|
|
15676
|
-
throw error;
|
|
15677
|
-
}
|
|
15678
|
-
}
|
|
16056
|
+
await stopSessionInternal();
|
|
15679
16057
|
},
|
|
15680
16058
|
/**
|
|
15681
16059
|
* Identify user
|
|
16060
|
+
* If called before session starts, the identity will be queued and applied when session starts
|
|
15682
16061
|
*/
|
|
15683
16062
|
identify(userId, traits) {
|
|
15684
|
-
|
|
16063
|
+
currentIdentity = { userId, traits };
|
|
16064
|
+
if (!session) {
|
|
16065
|
+
pendingIdentity = { userId, traits };
|
|
16066
|
+
if (config?.debug) {
|
|
16067
|
+
console.log("[LogSpace] User identity queued (session not started yet):", userId);
|
|
16068
|
+
}
|
|
16069
|
+
return;
|
|
16070
|
+
}
|
|
15685
16071
|
session.userId = userId;
|
|
15686
16072
|
session.userTraits = traits;
|
|
15687
|
-
if (multiTabCoordinator.isActive()) {
|
|
15688
|
-
multiTabCoordinator.updateSessionUser(userId, traits);
|
|
15689
|
-
}
|
|
15690
16073
|
if (config?.debug) {
|
|
15691
16074
|
console.log("[LogSpace] User identified:", userId);
|
|
15692
16075
|
}
|
|
@@ -15699,7 +16082,7 @@ const LogSpace = {
|
|
|
15699
16082
|
type: "annotation",
|
|
15700
16083
|
data: {
|
|
15701
16084
|
annotationType: "event",
|
|
15702
|
-
event,
|
|
16085
|
+
eventName: event,
|
|
15703
16086
|
properties
|
|
15704
16087
|
}
|
|
15705
16088
|
});
|
|
@@ -15780,6 +16163,51 @@ const LogSpace = {
|
|
|
15780
16163
|
getRRWebEvents() {
|
|
15781
16164
|
return [...rrwebEvents];
|
|
15782
16165
|
},
|
|
16166
|
+
/**
|
|
16167
|
+
* Get current configuration (read-only)
|
|
16168
|
+
* Returns the full NormalizedConfig plus convenience top-level fields
|
|
16169
|
+
* that match the setConfig() interface for easy verification
|
|
16170
|
+
*/
|
|
16171
|
+
getConfig() {
|
|
16172
|
+
if (!config) return null;
|
|
16173
|
+
return {
|
|
16174
|
+
...config,
|
|
16175
|
+
// Expose convenience top-level fields that match setConfig() interface
|
|
16176
|
+
rateLimit: config.limits.rateLimit,
|
|
16177
|
+
maxLogs: config.limits.maxLogs,
|
|
16178
|
+
maxSize: config.limits.maxSize,
|
|
16179
|
+
idleTimeout: config.limits.idleTimeout
|
|
16180
|
+
};
|
|
16181
|
+
},
|
|
16182
|
+
/**
|
|
16183
|
+
* Update configuration at runtime
|
|
16184
|
+
* Only certain settings can be changed after initialization
|
|
16185
|
+
*/
|
|
16186
|
+
setConfig(updates) {
|
|
16187
|
+
if (!config) {
|
|
16188
|
+
console.warn("[LogSpace] SDK not initialized, cannot update config");
|
|
16189
|
+
return;
|
|
16190
|
+
}
|
|
16191
|
+
if (updates.rateLimit !== void 0) {
|
|
16192
|
+
config.limits.rateLimit = updates.rateLimit;
|
|
16193
|
+
}
|
|
16194
|
+
if (updates.maxLogs !== void 0) {
|
|
16195
|
+
config.limits.maxLogs = updates.maxLogs;
|
|
16196
|
+
}
|
|
16197
|
+
if (updates.maxSize !== void 0) {
|
|
16198
|
+
config.limits.maxSize = updates.maxSize;
|
|
16199
|
+
}
|
|
16200
|
+
if (updates.idleTimeout !== void 0) {
|
|
16201
|
+
config.limits.idleTimeout = updates.idleTimeout;
|
|
16202
|
+
resetIdleTimer();
|
|
16203
|
+
}
|
|
16204
|
+
if (updates.debug !== void 0) {
|
|
16205
|
+
config.debug = updates.debug;
|
|
16206
|
+
}
|
|
16207
|
+
if (config.debug) {
|
|
16208
|
+
console.log("[LogSpace] Config updated:", updates);
|
|
16209
|
+
}
|
|
16210
|
+
},
|
|
15783
16211
|
/**
|
|
15784
16212
|
* Destroy SDK
|
|
15785
16213
|
*/
|
|
@@ -15802,9 +16230,7 @@ const LogSpace = {
|
|
|
15802
16230
|
visibilityTimer = null;
|
|
15803
16231
|
}
|
|
15804
16232
|
stopCheckpointTimer();
|
|
15805
|
-
|
|
15806
|
-
multiTabCoordinator.cleanup();
|
|
15807
|
-
}
|
|
16233
|
+
stopSamplingTrimTimer();
|
|
15808
16234
|
if (config?.capture.rrweb) {
|
|
15809
16235
|
stopRRWebRecording();
|
|
15810
16236
|
}
|
|
@@ -15823,14 +16249,16 @@ const LogSpace = {
|
|
|
15823
16249
|
rrwebEvents = [];
|
|
15824
16250
|
currentSize = 0;
|
|
15825
16251
|
rrwebSize = 0;
|
|
15826
|
-
sessionEndedByHardLimit = false;
|
|
15827
16252
|
sessionEndedByIdle = false;
|
|
15828
16253
|
samplingTriggered = false;
|
|
15829
16254
|
samplingTriggerType = null;
|
|
16255
|
+
samplingFirstTriggerTime = null;
|
|
15830
16256
|
samplingTriggerTime = null;
|
|
15831
16257
|
recoveryTargetSessionId = null;
|
|
15832
16258
|
unloadHandled = false;
|
|
16259
|
+
isNavigatingAway = false;
|
|
15833
16260
|
lastMouseMoveTime = 0;
|
|
16261
|
+
currentTabId = null;
|
|
15834
16262
|
initialized = false;
|
|
15835
16263
|
if (wasDebug) {
|
|
15836
16264
|
console.log("[LogSpace] SDK destroyed");
|