@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 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 and tokens
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 (rough pattern)
149
+ // JWT tokens - matches eyJ... pattern (base64url encoded JSON starting with {"alg" or {"typ")
96
150
  {
97
- pattern: /\b[A-Za-z0-9-_]{36,}\.[A-Za-z0-9-_]{36,}\.[A-Za-z0-9-_]{36,}\b/g,
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 MAX_BODY_SIZE = 5e3;
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 > MAX_BODY_SIZE ? bodyStr.substring(0, MAX_BODY_SIZE) + "..." : bodyStr;
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 > MAX_BODY_SIZE ? text.substring(0, MAX_BODY_SIZE) + "..." : text;
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
- status: response.status,
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
- status: 0,
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 > MAX_BODY_SIZE ? bodyStr.substring(0, MAX_BODY_SIZE) + "..." : bodyStr;
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 > MAX_BODY_SIZE ? responseText.substring(0, MAX_BODY_SIZE) + "..." : responseText;
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
- status: xhr.status,
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: ${safeStringify(event.reason)}`,
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
- window.addEventListener("visibilitychange", () => {
1687
+ visibilityHandler = () => {
1349
1688
  if (document.visibilityState === "hidden") {
1350
1689
  sendWebVitals();
1351
1690
  }
1352
- });
1353
- window.addEventListener("load", () => {
1354
- setTimeout(() => {
1691
+ };
1692
+ window.addEventListener("visibilitychange", visibilityHandler);
1693
+ loadHandler = () => {
1694
+ navigationTimingTimeout = setTimeout(() => {
1355
1695
  sendNavigationTiming();
1356
1696
  }, 0);
1357
- });
1358
- setTimeout(() => {
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
- status: 200,
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 LEADER_KEY = "__logspace_leader";
13859
- const SESSION_KEY = "__logspace_session";
13860
- const CHANNEL_NAME = "logspace-multi-tab";
13861
- const HEARTBEAT_INTERVAL = 2e3;
13862
- const HEARTBEAT_STALE_THRESHOLD = 5e3;
13863
- const LEADER_CHECK_INTERVAL = 3e3;
13864
- class MultiTabCoordinator {
13865
- constructor() {
13866
- this.channel = null;
13867
- this.isLeader = false;
13868
- this.callbacks = null;
13869
- this.heartbeatTimer = null;
13870
- this.leaderCheckTimer = null;
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
- * Attempt to become leader or join as follower
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
- * Try to claim leadership
13923
- */
13924
- tryBecomeLeader() {
13925
- const existingSession = this.getSessionState();
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
- try {
13934
- localStorage.setItem(LEADER_KEY, JSON.stringify(leaderState));
13935
- const verified = this.getLeaderState();
13936
- if (verified?.tabId === this.tabId) {
13937
- this.becomeLeader(sessionId, isNewSession);
13938
- } else {
13939
- this.becomeFollower(verified.sessionId);
13940
- }
13941
- } catch (error) {
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
- }, HEARTBEAT_INTERVAL);
14042
- this.updateHeartbeat();
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 = "0.1.0";
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: 200,
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: 300,
14507
- // 5 minutes
14508
- idleTimeout: 30,
14509
- // 30 seconds
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: 15,
14524
- // 15 seconds before trigger
14525
- recordAfter: 15,
14526
- // 15 seconds after trigger
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
- if (rateLimiter.count >= config.limits.rateLimit) {
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 || samplingTriggered) return;
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
- console.log(`[LogSpace] Sampling triggered by: ${triggerType}`);
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
- LogSpace.stopSession();
14849
+ stopSessionInternal({ restartImmediately: true });
14683
14850
  }
14684
14851
  }, config.sampling.recordAfter * 1e3);
14685
14852
  }
14686
14853
  function checkSamplingTrigger(log) {
14687
- if (!config?.sampling.enabled || samplingTriggered) return;
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 = samplingTriggerTime - config.sampling.bufferBefore * 1e3;
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
- rrwebEvents = rrwebEvents.filter((event) => {
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
- LogSpace.stopSession();
14995
+ stopSessionInternal({ restartImmediately: true });
14735
14996
  }
14736
14997
  return;
14737
14998
  }
14738
- const tabId = multiTabCoordinator.isActive() ? multiTabCoordinator.getTabId() : "single";
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
- if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
14751
- multiTabCoordinator.sendLog(log);
14752
- resetIdleTimer();
14753
- if (config.debug) {
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(log);
14763
- config.hooks.onAction?.(log, session);
15015
+ checkSamplingTrigger(maskedLog);
15016
+ config.hooks.onAction?.(maskedLog, session);
14764
15017
  if (config.debug) {
14765
- console.log("[LogSpace] Captured:", log.type, log.data);
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
- if (multiTabCoordinator.isActive() && !multiTabCoordinator.isLeaderTab()) {
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 recovery on next visit");
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: multiTabCoordinator.isActive() ? multiTabCoordinator.getTabId() : "main",
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, ending session");
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 = multiTabCoordinator.isActive() ? multiTabCoordinator.getTabId() : "main";
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 not sent");
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 sendSession() {
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: session?.userId,
15120
- userTraits: session?.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 existingLogTimestamps = new Set(logs.map((l) => l.timestamp));
15219
- const newLogs = recoveredLogs.filter((l) => !existingLogTimestamps.has(l.timestamp));
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 existingEventTimestamps = new Set(rrwebEvents.map((e) => e.timestamp));
15237
- const newEvents = recoveredEvents.filter((e) => !existingEventTimestamps.has(e.timestamp));
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 (recoveryTargetSessionId && emergencyBackup.id === recoveryTargetSessionId) {
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 (config.debug) {
15297
- console.log("[LogSpace] Recovering crashed session:", backup.sessionId);
15298
- }
15299
- const recoveredPayload = {
15300
- id: backup.sessionId,
15301
- startTime: backup.startTime,
15302
- endTime: backup.lastCheckpoint,
15303
- duration: backup.lastCheckpoint - backup.startTime,
15304
- url: backup.url,
15305
- title: backup.title,
15306
- userId: backup.userId,
15307
- userTraits: backup.userTraits,
15308
- metadata: backup.metadata,
15309
- logs: backup.logs,
15310
- stats: {
15311
- logCount: backup.logs.length,
15312
- networkLogCount: backup.logs.filter((l) => l.type === "network").length,
15313
- consoleLogCount: backup.logs.filter((l) => l.type === "console").length,
15314
- errorCount: backup.logs.filter((l) => l.type === "error").length,
15315
- storageLogCount: backup.logs.filter((l) => l.type === "storage").length,
15316
- interactionLogCount: backup.logs.filter((l) => l.type === "interaction").length,
15317
- performanceLogCount: backup.logs.filter((l) => l.type === "performance").length,
15318
- annotationCount: backup.logs.filter((l) => l.type === "annotation").length,
15319
- tabCount: 1
15320
- // SDK always has single tab
15321
- },
15322
- endReason: "crash",
15323
- recordingType: "rrweb",
15324
- rrwebEvents: backup.rrwebEvents,
15325
- sdkVersion: SDK_VERSION
15326
- };
15327
- pendingSessions.push({
15328
- id: backup.sessionId,
15329
- data: recoveredPayload,
15330
- createdAt: backup.lastCheckpoint,
15331
- retryCount: 0
15332
- });
15333
- await clearCurrentSessionBackup();
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
- console.warn("[LogSpace] SDK already initialized");
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
- if (!multiTabCoordinator.isActive()) {
15485
- this.startSession();
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
- if (multiTabCoordinator.isActive() && multiTabCoordinator.getSessionId()) {
15526
- sessionId = multiTabCoordinator.getSessionId();
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: multiTabCoordinator.isActive() ? 0 : 1
15557
- // Will be updated by multi-tab coordinator
15957
+ tabCount: 1
15558
15958
  }
15559
15959
  };
15560
- const tabId = multiTabCoordinator.isActive() ? multiTabCoordinator.getTabId() : void 0;
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 && !sessionEndedByHardLimit) {
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
- LogSpace.stopSession();
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
- this.stopSession();
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
- if (!session || session.status === "stopped") {
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
- if (!session) return;
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
- if (multiTabCoordinator.isActive()) {
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");