ray-logger-core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/collectors/console.d.ts +9 -0
  2. package/collectors/console.d.ts.map +1 -0
  3. package/collectors/console.js +51 -0
  4. package/collectors/console.js.map +1 -0
  5. package/collectors/cookie.d.ts +9 -0
  6. package/collectors/cookie.d.ts.map +1 -0
  7. package/collectors/cookie.js +162 -0
  8. package/collectors/cookie.js.map +1 -0
  9. package/collectors/error.d.ts +8 -0
  10. package/collectors/error.d.ts.map +1 -0
  11. package/collectors/error.js +47 -0
  12. package/collectors/error.js.map +1 -0
  13. package/collectors/network/fetch.d.ts +9 -0
  14. package/collectors/network/fetch.d.ts.map +1 -0
  15. package/collectors/network/fetch.js +182 -0
  16. package/collectors/network/fetch.js.map +1 -0
  17. package/collectors/network/xhr.d.ts +8 -0
  18. package/collectors/network/xhr.d.ts.map +1 -0
  19. package/collectors/network/xhr.js +187 -0
  20. package/collectors/network/xhr.js.map +1 -0
  21. package/collectors/rrweb.d.ts +8 -0
  22. package/collectors/rrweb.d.ts.map +1 -0
  23. package/collectors/rrweb.js +41 -0
  24. package/collectors/rrweb.js.map +1 -0
  25. package/collectors/storage.d.ts +9 -0
  26. package/collectors/storage.d.ts.map +1 -0
  27. package/collectors/storage.js +139 -0
  28. package/collectors/storage.js.map +1 -0
  29. package/collectors/types.d.ts +2 -0
  30. package/collectors/types.d.ts.map +1 -0
  31. package/collectors/types.js +2 -0
  32. package/collectors/types.js.map +1 -0
  33. package/controller.d.ts +22 -0
  34. package/controller.d.ts.map +1 -0
  35. package/controller.js +218 -0
  36. package/controller.js.map +1 -0
  37. package/index.d.ts +10 -0
  38. package/index.d.ts.map +1 -0
  39. package/index.js +1559 -0
  40. package/index.js.map +1 -0
  41. package/package.json +20 -0
  42. package/session.d.ts +13 -0
  43. package/session.d.ts.map +1 -0
  44. package/session.js +77 -0
  45. package/session.js.map +1 -0
  46. package/storage/idb.d.ts +40 -0
  47. package/storage/idb.d.ts.map +1 -0
  48. package/storage/idb.js +182 -0
  49. package/storage/idb.js.map +1 -0
  50. package/timeline.d.ts +23 -0
  51. package/timeline.d.ts.map +1 -0
  52. package/timeline.js +64 -0
  53. package/timeline.js.map +1 -0
  54. package/types.d.ts +181 -0
  55. package/types.d.ts.map +1 -0
  56. package/types.js +28 -0
  57. package/types.js.map +1 -0
  58. package/utils/console-stack.d.ts +12 -0
  59. package/utils/console-stack.d.ts.map +1 -0
  60. package/utils/console-stack.js +141 -0
  61. package/utils/console-stack.js.map +1 -0
  62. package/utils/serialize.d.ts +4 -0
  63. package/utils/serialize.d.ts.map +1 -0
  64. package/utils/serialize.js +57 -0
  65. package/utils/serialize.js.map +1 -0
package/index.js ADDED
@@ -0,0 +1,1559 @@
1
+ import { record } from 'rrweb';
2
+
3
+ const PACKAGE_NAME = '@ray-web-logger/core';
4
+ const SESSION_EXPORT_SCHEMA_VERSION = 1;
5
+ const DEFAULT_MEMORY_FLUSH_THRESHOLD_BYTES = 1_048_576;
6
+ const DEFAULT_RRWEB_CHECKOUT_EVERY_NMS = 30_000;
7
+ const DEFAULT_INDEXED_DB_NAME = 'ray-web-logger-webLogger';
8
+ const DEFAULT_SESSION_STORAGE_KEY = '__web_logger_session__';
9
+ /** 与规格 DevTools 对齐的 `console.*` 方法名;采集时写入 `ConsolePayload.level`,宿主无实现则跳过 patch。 */
10
+ const CONSOLE_CAPTURE_METHODS = [
11
+ 'debug',
12
+ 'log',
13
+ 'info',
14
+ 'warn',
15
+ 'error',
16
+ 'trace',
17
+ 'dir',
18
+ 'dirxml',
19
+ 'table',
20
+ 'group',
21
+ 'groupCollapsed',
22
+ 'groupEnd',
23
+ 'assert',
24
+ 'count',
25
+ 'countReset',
26
+ 'time',
27
+ 'timeLog',
28
+ 'timeEnd',
29
+ ];
30
+
31
+ const MAX_DEPTH = 5;
32
+ const MAX_ARRAY_ITEMS = 50;
33
+ const MAX_OBJECT_KEYS = 50;
34
+ function serializeValue(value) {
35
+ return serializeUnknown(value, new WeakSet(), 0);
36
+ }
37
+ function serializeArgs(args) {
38
+ return args.map((arg) => serializeValue(arg));
39
+ }
40
+ function serializeUnknown(value, seen, depth) {
41
+ if (value === null ||
42
+ typeof value === 'string' ||
43
+ typeof value === 'number' ||
44
+ typeof value === 'boolean') {
45
+ return value;
46
+ }
47
+ if (typeof value === 'undefined') {
48
+ return { type: 'unserializable', reason: 'undefined' };
49
+ }
50
+ if (typeof value === 'bigint') {
51
+ return { type: 'unserializable', reason: 'bigint', preview: value.toString() };
52
+ }
53
+ if (typeof value === 'symbol' || typeof value === 'function') {
54
+ return { type: 'unserializable', reason: typeof value, preview: String(value) };
55
+ }
56
+ if (depth >= MAX_DEPTH) {
57
+ return { type: 'unserializable', reason: 'max-depth', preview: previewValue(value) };
58
+ }
59
+ if (seen.has(value)) {
60
+ return { type: 'circular', reason: 'circular-reference', preview: previewValue(value) };
61
+ }
62
+ seen.add(value);
63
+ if (value instanceof Error) {
64
+ return {
65
+ name: value.name,
66
+ message: value.message,
67
+ stack: value.stack ?? null,
68
+ };
69
+ }
70
+ if (Array.isArray(value)) {
71
+ return value.slice(0, MAX_ARRAY_ITEMS).map((item) => serializeUnknown(item, seen, depth + 1));
72
+ }
73
+ const result = {};
74
+ for (const key of Object.keys(value).slice(0, MAX_OBJECT_KEYS)) {
75
+ result[key] = serializeUnknown(value[key], seen, depth + 1);
76
+ }
77
+ return result;
78
+ }
79
+ function previewValue(value) {
80
+ try {
81
+ return String(value);
82
+ }
83
+ catch {
84
+ return Object.prototype.toString.call(value);
85
+ }
86
+ }
87
+
88
+ /** Collector 与其它 web-logger 内部帧需在展示前跳过 */
89
+ const INTERNAL_STACK_MARKERS = [
90
+ '/collectors/console',
91
+ '\\collectors\\console',
92
+ 'collectors/console.ts',
93
+ 'collectors\\console.ts',
94
+ 'collectors/console.js',
95
+ 'collectors\\console.js',
96
+ '@ray-web-logger/core',
97
+ 'packages/core/src/collectors/console',
98
+ 'packages\\core\\src\\collectors\\console',
99
+ ];
100
+ /** 从同步 `new Error().stack` 推导 `ConsolePayload` 中与调用位置相关的可选字段(非 trace 不写 `stack/stackFrames`,控制体积)。 */
101
+ function deriveConsoleLocationFields(options) {
102
+ const { stack, method } = options;
103
+ if (!stack?.trim()) {
104
+ return {};
105
+ }
106
+ const rawLines = splitStackLines(stack);
107
+ const parsedFrames = [];
108
+ for (const raw of rawLines) {
109
+ if (!raw.trim()) {
110
+ continue;
111
+ }
112
+ const frame = parseStackLine(raw);
113
+ if (frame.url !== undefined || frame.line !== undefined || frame.file !== undefined) {
114
+ parsedFrames.push({ raw, frame });
115
+ }
116
+ }
117
+ const userFrames = dropInternalPrefix(parsedFrames);
118
+ if (userFrames.length === 0) {
119
+ return method === 'trace' ? { stack } : {};
120
+ }
121
+ const callSite = normalizeFrame(userFrames[0].frame);
122
+ if (method !== 'trace') {
123
+ return { callSite };
124
+ }
125
+ return {
126
+ callSite,
127
+ stack,
128
+ stackFrames: userFrames.map((f) => normalizeFrame(f.frame)),
129
+ };
130
+ }
131
+ function splitStackLines(stack) {
132
+ return stack.split(/\r?\n/);
133
+ }
134
+ function parseStackLine(line) {
135
+ const trimmed = line.trim();
136
+ const ff = /^(.+?)@(.+):(\d+):(\d+)$/.exec(trimmed);
137
+ if (ff && !trimmed.startsWith('at ')) {
138
+ const url = ff[2];
139
+ return {
140
+ functionName: ff[1] || undefined,
141
+ url,
142
+ line: Number(ff[3]),
143
+ column: Number(ff[4]),
144
+ file: basenameFromUrl(url),
145
+ };
146
+ }
147
+ const parenOpen = trimmed.lastIndexOf('(');
148
+ const parenClose = trimmed.lastIndexOf(')');
149
+ if (trimmed.startsWith('at ') && parenOpen !== -1 && parenClose > parenOpen) {
150
+ const inside = trimmed.slice(parenOpen + 1, parenClose);
151
+ const loc = parseUrlLineColumn(inside);
152
+ if (loc) {
153
+ const head = trimmed.slice(3, parenOpen).trim();
154
+ return {
155
+ functionName: head || undefined,
156
+ url: loc.url,
157
+ line: loc.line,
158
+ column: loc.column,
159
+ file: basenameFromUrl(loc.url),
160
+ };
161
+ }
162
+ }
163
+ if (trimmed.startsWith('at ')) {
164
+ const rest = trimmed.slice(3).trim();
165
+ const loc = parseUrlLineColumn(rest);
166
+ if (loc) {
167
+ return {
168
+ url: loc.url,
169
+ line: loc.line,
170
+ column: loc.column,
171
+ file: basenameFromUrl(loc.url),
172
+ };
173
+ }
174
+ }
175
+ return {};
176
+ }
177
+ function parseUrlLineColumn(s) {
178
+ const m = /^(.*):(\d+):(\d+)$/.exec(s);
179
+ if (!m) {
180
+ return undefined;
181
+ }
182
+ return { url: m[1], line: Number(m[2]), column: Number(m[3]) };
183
+ }
184
+ function basenameFromUrl(url) {
185
+ const trimmed = url.trim();
186
+ if (!trimmed || trimmed === '(native)') {
187
+ return undefined;
188
+ }
189
+ try {
190
+ const u = new URL(trimmed);
191
+ const seg = u.pathname.split('/').filter(Boolean);
192
+ return seg[seg.length - 1] || trimmed;
193
+ }
194
+ catch {
195
+ const parts = trimmed.split(/[/\\]/);
196
+ const last = parts[parts.length - 1];
197
+ return last || trimmed;
198
+ }
199
+ }
200
+ function isInternalFrame(raw, frame) {
201
+ if (raw.includes('(native)')) {
202
+ return true;
203
+ }
204
+ const blob = `${raw} ${frame.url ?? ''}`;
205
+ for (const marker of INTERNAL_STACK_MARKERS) {
206
+ if (blob.includes(marker)) {
207
+ return true;
208
+ }
209
+ }
210
+ return false;
211
+ }
212
+ function dropInternalPrefix(frames) {
213
+ let i = 0;
214
+ while (i < frames.length && isInternalFrame(frames[i].raw, frames[i].frame)) {
215
+ i += 1;
216
+ }
217
+ return frames.slice(i);
218
+ }
219
+ function normalizeFrame(frame) {
220
+ const url = frame.url;
221
+ const file = frame.file ?? (url ? basenameFromUrl(url) : undefined);
222
+ return {
223
+ ...frame,
224
+ file,
225
+ ...(url ? { url } : {}),
226
+ };
227
+ }
228
+
229
+ function installConsoleCollector(options) {
230
+ const consoleRef = options.consoleRef ?? globalThis.console;
231
+ if (!consoleRef) {
232
+ return noop$6;
233
+ }
234
+ const sink = consoleRef;
235
+ const originals = new Map();
236
+ for (const method of CONSOLE_CAPTURE_METHODS) {
237
+ const native = sink[method];
238
+ if (typeof native !== 'function') {
239
+ continue;
240
+ }
241
+ const impl = native;
242
+ originals.set(method, impl);
243
+ sink[method] = (...args) => {
244
+ const stack = options.recordConsoleCallSite !== false ? captureStackString() : undefined;
245
+ void impl.apply(consoleRef, args);
246
+ try {
247
+ const locationFields = stack !== undefined ? deriveConsoleLocationFields({ stack, method }) : {};
248
+ options.record({
249
+ type: 'console',
250
+ payload: {
251
+ level: method,
252
+ args: serializeArgs(args),
253
+ ...locationFields,
254
+ },
255
+ });
256
+ }
257
+ catch {
258
+ // Console 行为不应因采集失败而受影响。
259
+ }
260
+ };
261
+ }
262
+ return () => {
263
+ for (const [method, impl] of originals) {
264
+ sink[method] = impl;
265
+ }
266
+ };
267
+ }
268
+ function noop$6() {
269
+ // noop
270
+ }
271
+ /** 在调用原生 console 方法**之前**截取栈,才能包含业务调用者;实现需极轻量。 */
272
+ function captureStackString() {
273
+ const err = new Error();
274
+ return err.stack;
275
+ }
276
+
277
+ function installCookieCollector(options) {
278
+ const documentRef = options.documentRef ?? (typeof document !== 'undefined' ? document : undefined);
279
+ if (!documentRef) {
280
+ return noop$5;
281
+ }
282
+ recordCookieSnapshot(options.record, documentRef, options.config);
283
+ const descriptorOwner = findCookieDescriptorOwner(documentRef);
284
+ const descriptor = descriptorOwner
285
+ ? Object.getOwnPropertyDescriptor(descriptorOwner, 'cookie')
286
+ : undefined;
287
+ if (!descriptorOwner || !descriptor?.set || !descriptor.configurable) {
288
+ return noop$5;
289
+ }
290
+ Object.defineProperty(descriptorOwner, 'cookie', {
291
+ configurable: descriptor.configurable,
292
+ enumerable: descriptor.enumerable,
293
+ get: descriptor.get,
294
+ set(value) {
295
+ descriptor.set?.call(this, value);
296
+ recordCookieWrite(options.record, value, options.config);
297
+ },
298
+ });
299
+ return () => {
300
+ Object.defineProperty(descriptorOwner, 'cookie', descriptor);
301
+ };
302
+ }
303
+ function recordCookieSnapshot(record, documentRef, config) {
304
+ const snapshot = readCookieSnapshot(documentRef, config);
305
+ if (!snapshot || (snapshot.cookies.length === 0 && snapshot.skipped.length === 0)) {
306
+ return;
307
+ }
308
+ record({
309
+ type: 'cookie',
310
+ payload: {
311
+ op: 'snapshot',
312
+ cookies: snapshot.cookies,
313
+ ...(snapshot.skipped.length > 0 ? { skipped: snapshot.skipped } : {}),
314
+ },
315
+ });
316
+ }
317
+ function readCookieSnapshot(documentRef, config) {
318
+ let rawCookie = '';
319
+ try {
320
+ rawCookie = documentRef.cookie;
321
+ }
322
+ catch {
323
+ return undefined;
324
+ }
325
+ if (!rawCookie) {
326
+ return { cookies: [], skipped: [] };
327
+ }
328
+ const cookies = [];
329
+ const skipped = [];
330
+ for (const rawPair of rawCookie.split(';')) {
331
+ const pair = rawPair.trim();
332
+ const separatorIndex = pair.indexOf('=');
333
+ if (separatorIndex <= 0) {
334
+ continue;
335
+ }
336
+ const name = pair.slice(0, separatorIndex).trim();
337
+ const value = pair.slice(separatorIndex + 1).trim();
338
+ if (!name) {
339
+ continue;
340
+ }
341
+ if (shouldSkipCookie(name, config)) {
342
+ skipped.push({ name, reason: 'disallowed' });
343
+ continue;
344
+ }
345
+ cookies.push({ name, value });
346
+ }
347
+ return { cookies, skipped };
348
+ }
349
+ function recordCookieWrite(record, rawCookie, config) {
350
+ const parsed = parseCookieWrite(rawCookie);
351
+ if (!parsed || shouldSkipCookie(parsed.name, config)) {
352
+ return;
353
+ }
354
+ record({
355
+ type: 'cookie',
356
+ payload: {
357
+ name: parsed.name,
358
+ value: parsed.value,
359
+ op: parsed.isDelete ? 'delete' : 'set',
360
+ ...(parsed.domain ? { domain: parsed.domain } : {}),
361
+ ...(parsed.expires ? { expires: parsed.expires } : {}),
362
+ ...(parsed.maxAge ? { maxAge: parsed.maxAge } : {}),
363
+ },
364
+ });
365
+ }
366
+ function parseCookieWrite(rawCookie) {
367
+ const [pair = '', ...attrs] = rawCookie.split(';');
368
+ const separatorIndex = pair.indexOf('=');
369
+ if (separatorIndex <= 0) {
370
+ return undefined;
371
+ }
372
+ const name = pair.slice(0, separatorIndex).trim();
373
+ const value = pair.slice(separatorIndex + 1).trim();
374
+ if (!name) {
375
+ return undefined;
376
+ }
377
+ return {
378
+ name,
379
+ value,
380
+ isDelete: isDeleteCookie(value, attrs),
381
+ ...parseCookieAttrs(attrs),
382
+ };
383
+ }
384
+ function parseCookieAttrs(attrs) {
385
+ const parsed = {};
386
+ for (const attr of attrs) {
387
+ const separatorIndex = attr.indexOf('=');
388
+ const rawKey = separatorIndex >= 0 ? attr.slice(0, separatorIndex) : attr;
389
+ const rawValue = separatorIndex >= 0 ? attr.slice(separatorIndex + 1) : '';
390
+ const key = rawKey.trim().toLowerCase();
391
+ const value = rawValue.trim();
392
+ if (key === 'domain' && value) {
393
+ parsed.domain = value;
394
+ }
395
+ else if (key === 'expires' && value) {
396
+ parsed.expires = value;
397
+ }
398
+ else if (key === 'max-age' && value) {
399
+ parsed.maxAge = value;
400
+ }
401
+ }
402
+ return parsed;
403
+ }
404
+ function isDeleteCookie(value, attrs) {
405
+ for (const attr of attrs) {
406
+ const [rawKey = '', rawValue = ''] = attr.split('=');
407
+ const key = rawKey.trim().toLowerCase();
408
+ const attrValue = rawValue.trim();
409
+ if (key === 'max-age' && Number(attrValue) <= 0) {
410
+ return true;
411
+ }
412
+ if (key === 'expires') {
413
+ const expiresAt = Date.parse(attrValue);
414
+ if (Number.isFinite(expiresAt) && expiresAt <= Date.now()) {
415
+ return true;
416
+ }
417
+ }
418
+ }
419
+ return value === '' && attrs.length > 0;
420
+ }
421
+ function shouldSkipCookie(name, config) {
422
+ return new Set(config.disallowRecordCookieKeys ?? []).has(name);
423
+ }
424
+ function findCookieDescriptorOwner(documentRef) {
425
+ let proto = Object.getPrototypeOf(documentRef);
426
+ while (proto) {
427
+ const descriptor = Object.getOwnPropertyDescriptor(proto, 'cookie');
428
+ if (descriptor) {
429
+ return proto;
430
+ }
431
+ proto = Object.getPrototypeOf(proto);
432
+ }
433
+ return undefined;
434
+ }
435
+ function noop$5() {
436
+ // noop
437
+ }
438
+
439
+ function installErrorCollector(options) {
440
+ const windowRef = options.windowRef ?? (typeof window !== 'undefined' ? window : undefined);
441
+ if (!windowRef) {
442
+ return noop$4;
443
+ }
444
+ const previousOnError = windowRef.onerror;
445
+ const onErrorEvent = (event) => {
446
+ recordError(options.record, event.error, event.message, event.filename);
447
+ };
448
+ const onUnhandledRejection = (event) => {
449
+ recordError(options.record, event.reason, 'Unhandled promise rejection');
450
+ };
451
+ windowRef.onerror = (message, source, lineno, colno, error) => {
452
+ recordError(options.record, error, String(message), source ? String(source) : undefined);
453
+ if (typeof previousOnError === 'function') {
454
+ return previousOnError.call(windowRef, message, source, lineno, colno, error);
455
+ }
456
+ return previousOnError ?? false;
457
+ };
458
+ windowRef.addEventListener('error', onErrorEvent);
459
+ windowRef.addEventListener('unhandledrejection', onUnhandledRejection);
460
+ return () => {
461
+ windowRef.onerror = previousOnError;
462
+ windowRef.removeEventListener('error', onErrorEvent);
463
+ windowRef.removeEventListener('unhandledrejection', onUnhandledRejection);
464
+ };
465
+ }
466
+ function recordError(record, errorLike, fallbackMessage, source) {
467
+ try {
468
+ const error = errorLike instanceof Error ? errorLike : undefined;
469
+ record({
470
+ type: 'error',
471
+ payload: {
472
+ message: error?.message || fallbackMessage,
473
+ ...(error?.stack ? { stack: error.stack } : {}),
474
+ ...(source ? { source } : {}),
475
+ },
476
+ });
477
+ }
478
+ catch {
479
+ // 全局错误处理不能再抛异常,避免影响宿主错误链路。
480
+ }
481
+ }
482
+ function noop$4() {
483
+ // noop
484
+ }
485
+
486
+ function installFetchCollector(options) {
487
+ const originalFetch = options.fetchRef ?? globalThis.fetch;
488
+ if (typeof originalFetch !== 'function' || typeof globalThis.fetch !== 'function') {
489
+ return noop$3;
490
+ }
491
+ globalThis.fetch = (async (input, init) => {
492
+ const startedAt = Date.now();
493
+ const requestInfo = await createRequestInfo(input, init, options.config);
494
+ try {
495
+ const response = await originalFetch.call(globalThis, input, init);
496
+ const durationMs = Date.now() - startedAt;
497
+ void recordSuccess(options.record, requestInfo, response, durationMs, options.config);
498
+ return response;
499
+ }
500
+ catch (error) {
501
+ options.record({
502
+ type: 'network',
503
+ payload: {
504
+ outcome: 'failed',
505
+ url: requestInfo.url,
506
+ method: requestInfo.method,
507
+ durationMs: Date.now() - startedAt,
508
+ error: normalizeNetworkError(error),
509
+ ...(requestInfo.bodySkippedReason
510
+ ? { bodySkippedReason: requestInfo.bodySkippedReason }
511
+ : {}),
512
+ },
513
+ });
514
+ throw error;
515
+ }
516
+ });
517
+ return () => {
518
+ if (globalThis.fetch === originalFetch) {
519
+ return;
520
+ }
521
+ globalThis.fetch = originalFetch;
522
+ };
523
+ }
524
+ async function createRequestInfo(input, init, config) {
525
+ const request = input instanceof Request ? input : undefined;
526
+ const method = normalizeMethod(init?.method ?? request?.method ?? 'GET');
527
+ const url = sanitizeUrl$1(getInputUrl(input), config.disallowRecordSearches);
528
+ const headers = config.enableRecordRequestHeaders === false
529
+ ? undefined
530
+ : headersToRecord(init?.headers ?? request?.headers, config.disallowRecordRequestHeaders);
531
+ const bodyInfo = await readRequestBody(input, init, config);
532
+ return {
533
+ url,
534
+ method,
535
+ ...(headers ? { headers } : {}),
536
+ ...bodyInfo,
537
+ };
538
+ }
539
+ async function recordSuccess(record, requestInfo, response, durationMs, config) {
540
+ const responseBody = await readResponseBody(response, config);
541
+ const bodySkippedReason = requestInfo.bodySkippedReason ?? responseBody.bodySkippedReason;
542
+ record({
543
+ type: 'network',
544
+ payload: {
545
+ outcome: 'success',
546
+ url: requestInfo.url,
547
+ method: requestInfo.method,
548
+ status: response.status,
549
+ durationMs,
550
+ ...(requestInfo.headers ? { requestHeaders: requestInfo.headers } : {}),
551
+ ...(config.enableRecordResponseHeaders === false
552
+ ? {}
553
+ : {
554
+ responseHeaders: headersToRecord(response.headers, config.disallowRecordResponseHeaders),
555
+ }),
556
+ ...(requestInfo.body !== undefined ? { requestBody: requestInfo.body } : {}),
557
+ ...(responseBody.body !== undefined ? { responseBody: responseBody.body } : {}),
558
+ ...(bodySkippedReason ? { bodySkippedReason } : {}),
559
+ },
560
+ });
561
+ }
562
+ function getInputUrl(input) {
563
+ if (typeof input === 'string') {
564
+ return input;
565
+ }
566
+ if (input instanceof URL) {
567
+ return input.toString();
568
+ }
569
+ return input.url;
570
+ }
571
+ function sanitizeUrl$1(url, disallowSearches) {
572
+ if (!disallowSearches?.length) {
573
+ return url;
574
+ }
575
+ try {
576
+ const parsed = new URL(url, globalThis.location?.href);
577
+ for (const key of disallowSearches) {
578
+ parsed.searchParams.delete(key);
579
+ }
580
+ return parsed.toString();
581
+ }
582
+ catch {
583
+ return url;
584
+ }
585
+ }
586
+ function headersToRecord(headersInit, disallowHeaders) {
587
+ if (!headersInit) {
588
+ return undefined;
589
+ }
590
+ const disallowed = new Set((disallowHeaders ?? []).map((header) => header.toLowerCase()));
591
+ const headers = new Headers(headersInit);
592
+ const result = {};
593
+ headers.forEach((value, key) => {
594
+ if (!disallowed.has(key.toLowerCase())) {
595
+ result[key] = value;
596
+ }
597
+ });
598
+ return result;
599
+ }
600
+ async function readRequestBody(input, init, config) {
601
+ if (config.enableRecordRequestBody === false) {
602
+ return { bodySkippedReason: 'request-body-disabled' };
603
+ }
604
+ if (init?.body) {
605
+ return bodyInitToString(init.body);
606
+ }
607
+ if (input instanceof Request && input.body) {
608
+ try {
609
+ return { body: await input.clone().text() };
610
+ }
611
+ catch {
612
+ return { bodySkippedReason: 'request-body-unreadable' };
613
+ }
614
+ }
615
+ return { body: null };
616
+ }
617
+ async function readResponseBody(response, config) {
618
+ if (config.enableRecordResponseBody === false) {
619
+ return { bodySkippedReason: 'response-body-disabled' };
620
+ }
621
+ try {
622
+ return { body: await response.clone().text() };
623
+ }
624
+ catch {
625
+ return { bodySkippedReason: 'response-body-unreadable' };
626
+ }
627
+ }
628
+ async function bodyInitToString(body) {
629
+ if (typeof body === 'string') {
630
+ return { body };
631
+ }
632
+ if (body instanceof URLSearchParams) {
633
+ return { body: body.toString() };
634
+ }
635
+ if (body instanceof Blob) {
636
+ return { body: await body.text() };
637
+ }
638
+ if (body instanceof FormData) {
639
+ return { bodySkippedReason: 'request-body-form-data' };
640
+ }
641
+ if (body instanceof ArrayBuffer) {
642
+ return { bodySkippedReason: 'request-body-binary' };
643
+ }
644
+ if (ArrayBuffer.isView(body)) {
645
+ return { bodySkippedReason: 'request-body-binary' };
646
+ }
647
+ if (body instanceof ReadableStream) {
648
+ return { bodySkippedReason: 'request-body-stream' };
649
+ }
650
+ return { bodySkippedReason: 'request-body-unknown' };
651
+ }
652
+ function normalizeMethod(method) {
653
+ return method.toUpperCase();
654
+ }
655
+ function normalizeNetworkError(error) {
656
+ if (error instanceof DOMException) {
657
+ return { message: error.message, name: error.name, code: error.code };
658
+ }
659
+ if (error instanceof Error) {
660
+ return { message: error.message, name: error.name };
661
+ }
662
+ return { message: String(error) };
663
+ }
664
+ function noop$3() {
665
+ // noop
666
+ }
667
+
668
+ function installXhrCollector(options) {
669
+ if (typeof globalThis.XMLHttpRequest === 'undefined') {
670
+ return noop$2;
671
+ }
672
+ const prototype = globalThis.XMLHttpRequest.prototype;
673
+ const originalOpen = prototype.open;
674
+ const originalSend = prototype.send;
675
+ const originalSetRequestHeader = prototype.setRequestHeader;
676
+ const states = new WeakMap();
677
+ prototype.open = function patchedOpen(method, url, async, username, password) {
678
+ states.set(this, {
679
+ method: method.toUpperCase(),
680
+ url: sanitizeUrl(String(url), options.config.disallowRecordSearches),
681
+ startedAt: 0,
682
+ recorded: false,
683
+ });
684
+ return originalOpen.call(this, method, url, async ?? true, username, password);
685
+ };
686
+ prototype.setRequestHeader = function patchedSetRequestHeader(name, value) {
687
+ const state = states.get(this);
688
+ if (state && options.config.enableRecordRequestHeaders !== false) {
689
+ const disallowed = new Set((options.config.disallowRecordRequestHeaders ?? []).map((header) => header.toLowerCase()));
690
+ if (!disallowed.has(name.toLowerCase())) {
691
+ state.requestHeaders ??= {};
692
+ state.requestHeaders[name] = value;
693
+ }
694
+ }
695
+ return originalSetRequestHeader.call(this, name, value);
696
+ };
697
+ prototype.send = function patchedSend(body) {
698
+ const state = states.get(this);
699
+ if (state) {
700
+ state.startedAt = Date.now();
701
+ const bodyInfo = requestBodyToString(body ?? null, options.config);
702
+ state.requestBody = bodyInfo.body;
703
+ state.bodySkippedReason = bodyInfo.bodySkippedReason;
704
+ const onError = () => {
705
+ state.failure = { message: 'XMLHttpRequest error', name: 'error' };
706
+ };
707
+ const onAbort = () => {
708
+ state.failure = { message: 'XMLHttpRequest aborted', name: 'abort' };
709
+ };
710
+ const onTimeout = () => {
711
+ state.failure = { message: 'XMLHttpRequest timeout', name: 'timeout' };
712
+ };
713
+ const onLoadEnd = () => {
714
+ recordXhr(options.record, this, state, options.config);
715
+ this.removeEventListener('error', onError);
716
+ this.removeEventListener('abort', onAbort);
717
+ this.removeEventListener('timeout', onTimeout);
718
+ this.removeEventListener('loadend', onLoadEnd);
719
+ };
720
+ this.addEventListener('error', onError);
721
+ this.addEventListener('abort', onAbort);
722
+ this.addEventListener('timeout', onTimeout);
723
+ this.addEventListener('loadend', onLoadEnd);
724
+ }
725
+ return originalSend.call(this, body ?? null);
726
+ };
727
+ return () => {
728
+ prototype.open = originalOpen;
729
+ prototype.send = originalSend;
730
+ prototype.setRequestHeader = originalSetRequestHeader;
731
+ };
732
+ }
733
+ function recordXhr(record, xhr, state, config) {
734
+ if (state.recorded) {
735
+ return;
736
+ }
737
+ state.recorded = true;
738
+ try {
739
+ if (state.failure) {
740
+ record({
741
+ type: 'network',
742
+ payload: {
743
+ outcome: 'failed',
744
+ url: state.url,
745
+ method: state.method,
746
+ durationMs: Date.now() - state.startedAt,
747
+ error: state.failure,
748
+ ...(state.bodySkippedReason ? { bodySkippedReason: state.bodySkippedReason } : {}),
749
+ },
750
+ });
751
+ return;
752
+ }
753
+ const responseBody = readXhrResponseBody(xhr, config);
754
+ const bodySkippedReason = state.bodySkippedReason ?? responseBody.bodySkippedReason;
755
+ record({
756
+ type: 'network',
757
+ payload: {
758
+ outcome: 'success',
759
+ url: state.url,
760
+ method: state.method,
761
+ status: xhr.status,
762
+ durationMs: Date.now() - state.startedAt,
763
+ ...(state.requestHeaders ? { requestHeaders: state.requestHeaders } : {}),
764
+ ...(config.enableRecordResponseHeaders === false
765
+ ? {}
766
+ : {
767
+ responseHeaders: parseResponseHeaders(xhr.getAllResponseHeaders(), config.disallowRecordResponseHeaders),
768
+ }),
769
+ ...(state.requestBody !== undefined ? { requestBody: state.requestBody } : {}),
770
+ ...(responseBody.body !== undefined ? { responseBody: responseBody.body } : {}),
771
+ ...(bodySkippedReason ? { bodySkippedReason } : {}),
772
+ },
773
+ });
774
+ }
775
+ catch {
776
+ // XHR 回调链不能因采集异常影响宿主监听器。
777
+ }
778
+ }
779
+ function sanitizeUrl(url, disallowSearches) {
780
+ if (!disallowSearches?.length) {
781
+ return url;
782
+ }
783
+ try {
784
+ const parsed = new URL(url, globalThis.location?.href);
785
+ for (const key of disallowSearches) {
786
+ parsed.searchParams.delete(key);
787
+ }
788
+ return parsed.toString();
789
+ }
790
+ catch {
791
+ return url;
792
+ }
793
+ }
794
+ function requestBodyToString(body, config) {
795
+ if (config.enableRecordRequestBody === false) {
796
+ return { bodySkippedReason: 'request-body-disabled' };
797
+ }
798
+ if (body === null) {
799
+ return { body: null };
800
+ }
801
+ if (typeof body === 'string') {
802
+ return { body };
803
+ }
804
+ if (body instanceof URLSearchParams) {
805
+ return { body: body.toString() };
806
+ }
807
+ if (body instanceof FormData) {
808
+ return { bodySkippedReason: 'request-body-form-data' };
809
+ }
810
+ if (body instanceof Blob) {
811
+ return { bodySkippedReason: 'request-body-blob' };
812
+ }
813
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
814
+ return { bodySkippedReason: 'request-body-binary' };
815
+ }
816
+ return { bodySkippedReason: 'request-body-unknown' };
817
+ }
818
+ function readXhrResponseBody(xhr, config) {
819
+ if (config.enableRecordResponseBody === false) {
820
+ return { bodySkippedReason: 'response-body-disabled' };
821
+ }
822
+ if (xhr.responseType && xhr.responseType !== 'text') {
823
+ return { bodySkippedReason: `response-body-${xhr.responseType}` };
824
+ }
825
+ try {
826
+ return { body: xhr.responseText };
827
+ }
828
+ catch {
829
+ return { bodySkippedReason: 'response-body-unreadable' };
830
+ }
831
+ }
832
+ function parseResponseHeaders(rawHeaders, disallowHeaders) {
833
+ const disallowed = new Set((disallowHeaders ?? []).map((header) => header.toLowerCase()));
834
+ const result = {};
835
+ for (const line of rawHeaders.trim().split(/[\r\n]+/)) {
836
+ if (!line) {
837
+ continue;
838
+ }
839
+ const separatorIndex = line.indexOf(':');
840
+ if (separatorIndex <= 0) {
841
+ continue;
842
+ }
843
+ const key = line.slice(0, separatorIndex).trim();
844
+ const value = line.slice(separatorIndex + 1).trim();
845
+ if (!disallowed.has(key.toLowerCase())) {
846
+ result[key] = value;
847
+ }
848
+ }
849
+ return result;
850
+ }
851
+ function noop$2() {
852
+ // noop
853
+ }
854
+
855
+ function installRrwebCollector(options) {
856
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
857
+ return noop$1;
858
+ }
859
+ try {
860
+ const stop = record({
861
+ emit(event) {
862
+ options.record({
863
+ type: 'rrweb',
864
+ timestamp: event.timestamp,
865
+ payload: {
866
+ rrwebEvent: event,
867
+ },
868
+ });
869
+ },
870
+ recordCanvas: true,
871
+ sampling: {
872
+ canvas: 2,
873
+ media: 2,
874
+ },
875
+ collectFonts: true,
876
+ checkoutEveryNms: normalizeCheckoutEveryNms(options.checkoutEveryNms),
877
+ });
878
+ return stop ?? noop$1;
879
+ }
880
+ catch {
881
+ return noop$1;
882
+ }
883
+ }
884
+ function noop$1() {
885
+ // noop
886
+ }
887
+ function normalizeCheckoutEveryNms(checkoutEveryNms) {
888
+ if (checkoutEveryNms === undefined) {
889
+ return DEFAULT_RRWEB_CHECKOUT_EVERY_NMS;
890
+ }
891
+ return checkoutEveryNms > 0 ? checkoutEveryNms : undefined;
892
+ }
893
+
894
+ function installStorageCollector(options) {
895
+ const windowRef = options.windowRef ?? (typeof window !== 'undefined' ? window : undefined);
896
+ if (!windowRef || typeof globalThis.Storage === 'undefined') {
897
+ return noop;
898
+ }
899
+ const prototype = globalThis.Storage.prototype;
900
+ const originalSetItem = prototype.setItem;
901
+ const originalRemoveItem = prototype.removeItem;
902
+ const originalClear = prototype.clear;
903
+ prototype.setItem = function patchedSetItem(key, value) {
904
+ originalSetItem.call(this, key, value);
905
+ const area = resolveStorageArea(windowRef, this);
906
+ if (!area || shouldSkipStorageKey(key, options.config)) {
907
+ return;
908
+ }
909
+ options.record({
910
+ type: 'storage',
911
+ payload: {
912
+ area,
913
+ op: 'set',
914
+ key,
915
+ value: String(value),
916
+ },
917
+ });
918
+ };
919
+ prototype.removeItem = function patchedRemoveItem(key) {
920
+ originalRemoveItem.call(this, key);
921
+ const area = resolveStorageArea(windowRef, this);
922
+ if (!area || shouldSkipStorageKey(key, options.config)) {
923
+ return;
924
+ }
925
+ options.record({
926
+ type: 'storage',
927
+ payload: {
928
+ area,
929
+ op: 'remove',
930
+ key,
931
+ value: null,
932
+ },
933
+ });
934
+ };
935
+ prototype.clear = function patchedClear() {
936
+ originalClear.call(this);
937
+ const area = resolveStorageArea(windowRef, this);
938
+ if (!area) {
939
+ return;
940
+ }
941
+ options.record({
942
+ type: 'storage',
943
+ payload: {
944
+ area,
945
+ op: 'clear',
946
+ },
947
+ });
948
+ };
949
+ recordStorageSnapshot(options.record, windowRef, 'local', options.config);
950
+ recordStorageSnapshot(options.record, windowRef, 'session', options.config);
951
+ return () => {
952
+ prototype.setItem = originalSetItem;
953
+ prototype.removeItem = originalRemoveItem;
954
+ prototype.clear = originalClear;
955
+ };
956
+ }
957
+ function resolveStorageArea(windowRef, storage) {
958
+ try {
959
+ if (storage === windowRef.localStorage) {
960
+ return 'local';
961
+ }
962
+ if (storage === windowRef.sessionStorage) {
963
+ return 'session';
964
+ }
965
+ }
966
+ catch {
967
+ return undefined;
968
+ }
969
+ return undefined;
970
+ }
971
+ function recordStorageSnapshot(record, windowRef, area, config) {
972
+ const storage = getStorageArea(windowRef, area);
973
+ if (!storage) {
974
+ return;
975
+ }
976
+ const snapshot = readStorageSnapshot(storage, config);
977
+ if (Object.keys(snapshot.entries).length === 0 && snapshot.skipped.length === 0) {
978
+ return;
979
+ }
980
+ record({
981
+ type: 'storage',
982
+ payload: {
983
+ area,
984
+ op: 'snapshot',
985
+ entries: snapshot.entries,
986
+ ...(snapshot.skipped.length > 0 ? { skipped: snapshot.skipped } : {}),
987
+ },
988
+ });
989
+ }
990
+ function getStorageArea(windowRef, area) {
991
+ try {
992
+ return area === 'local' ? windowRef.localStorage : windowRef.sessionStorage;
993
+ }
994
+ catch {
995
+ return undefined;
996
+ }
997
+ }
998
+ function readStorageSnapshot(storage, config) {
999
+ const entries = {};
1000
+ const skipped = [];
1001
+ try {
1002
+ for (let index = 0; index < storage.length; index += 1) {
1003
+ const key = storage.key(index);
1004
+ if (!key) {
1005
+ continue;
1006
+ }
1007
+ if (shouldSkipStorageKey(key, config)) {
1008
+ skipped.push({ key, reason: 'disallowed' });
1009
+ continue;
1010
+ }
1011
+ const value = storage.getItem(key);
1012
+ if (value === null) {
1013
+ continue;
1014
+ }
1015
+ entries[key] = value;
1016
+ }
1017
+ }
1018
+ catch {
1019
+ return { entries, skipped };
1020
+ }
1021
+ return { entries, skipped };
1022
+ }
1023
+ function shouldSkipStorageKey(key, config) {
1024
+ const disallowed = new Set(config.disallowRecordStorageKeys ?? []);
1025
+ disallowed.add(config.sessionStorageKey ?? DEFAULT_SESSION_STORAGE_KEY);
1026
+ return disallowed.has(key);
1027
+ }
1028
+ function noop() {
1029
+ // noop
1030
+ }
1031
+
1032
+ class Timeline {
1033
+ sessionId;
1034
+ #events = [];
1035
+ #nextSeq;
1036
+ #now;
1037
+ constructor(options) {
1038
+ this.sessionId = options.sessionId;
1039
+ this.#nextSeq = options.initialSeq ?? 0;
1040
+ this.#now = options.now ?? Date.now;
1041
+ }
1042
+ append(event) {
1043
+ const normalized = {
1044
+ ...event,
1045
+ sessionId: this.sessionId,
1046
+ seq: this.#nextSeq,
1047
+ timestamp: event.timestamp ?? this.#now(),
1048
+ };
1049
+ this.#nextSeq += 1;
1050
+ this.#events.push(normalized);
1051
+ return normalized;
1052
+ }
1053
+ all() {
1054
+ return [...this.#events].sort(compareTimelineEvents);
1055
+ }
1056
+ pending() {
1057
+ return [...this.#events];
1058
+ }
1059
+ ensureNextSeqAtLeast(nextSeq) {
1060
+ this.#nextSeq = Math.max(this.#nextSeq, nextSeq);
1061
+ }
1062
+ since(sinceMs, untilMs = this.#now()) {
1063
+ return this.all().filter((event) => event.timestamp >= sinceMs && event.timestamp <= untilMs);
1064
+ }
1065
+ latestRrwebFullSnapshotBefore(beforeMs) {
1066
+ return latestRrwebFullSnapshotBefore(this.#events, beforeMs);
1067
+ }
1068
+ clear(events) {
1069
+ if (!events) {
1070
+ this.#events = [];
1071
+ return;
1072
+ }
1073
+ const flushedSeq = new Set(events.map((event) => event.seq));
1074
+ this.#events = this.#events.filter((event) => !flushedSeq.has(event.seq));
1075
+ }
1076
+ }
1077
+ function compareTimelineEvents(left, right) {
1078
+ return left.timestamp - right.timestamp || left.seq - right.seq;
1079
+ }
1080
+ function latestRrwebFullSnapshotBefore(events, beforeMs) {
1081
+ return events
1082
+ .filter((event) => isRrwebFullSnapshotBefore(event, beforeMs))
1083
+ .sort(compareTimelineEvents)
1084
+ .at(-1);
1085
+ }
1086
+ function hasRrwebFullSnapshot(events) {
1087
+ return events.some(isRrwebFullSnapshot);
1088
+ }
1089
+ function isRrwebFullSnapshot(event) {
1090
+ return event.type === 'rrweb' && event.payload.rrwebEvent.type === 2;
1091
+ }
1092
+ function isRrwebFullSnapshotBefore(event, beforeMs) {
1093
+ return isRrwebFullSnapshot(event) && event.timestamp < beforeMs;
1094
+ }
1095
+
1096
+ const IDB_VERSION = 2;
1097
+ const SESSION_META_STORE = 'sessionMeta';
1098
+ const EVENT_CHUNKS_STORE = 'eventChunks';
1099
+ const EVENT_CHUNKS_BY_NAMESPACE_MAX_TIMESTAMP_INDEX = 'byNamespaceMaxTimestamp';
1100
+ const EVENT_CHUNKS_BY_NAMESPACE_MIN_TIMESTAMP_INDEX = 'byNamespaceMinTimestamp';
1101
+ class IndexedDbEventStore {
1102
+ namespaceKey;
1103
+ dbName;
1104
+ #dbPromise;
1105
+ #now;
1106
+ constructor(options) {
1107
+ this.namespaceKey = options.namespaceKey;
1108
+ this.dbName = DEFAULT_INDEXED_DB_NAME;
1109
+ this.#now = options.now ?? Date.now;
1110
+ }
1111
+ static isSupported() {
1112
+ return typeof globalThis.indexedDB !== 'undefined';
1113
+ }
1114
+ async appendChunk(events) {
1115
+ if (events.length === 0) {
1116
+ return undefined;
1117
+ }
1118
+ const db = await this.#open();
1119
+ const chunkSeq = await this.#nextChunkSeq(db);
1120
+ const ordered = [...events].sort(compareTimelineEvents);
1121
+ const seqList = ordered.map((event) => event.seq);
1122
+ const timestampList = ordered.map((event) => event.timestamp);
1123
+ const chunk = {
1124
+ namespaceKey: this.namespaceKey,
1125
+ chunkSeq,
1126
+ events: ordered,
1127
+ minSeq: Math.min(...seqList),
1128
+ maxSeq: Math.max(...seqList),
1129
+ minTimestamp: Math.min(...timestampList),
1130
+ maxTimestamp: Math.max(...timestampList),
1131
+ };
1132
+ const tx = db.transaction([SESSION_META_STORE, EVENT_CHUNKS_STORE], 'readwrite');
1133
+ tx.objectStore(EVENT_CHUNKS_STORE).put(chunk);
1134
+ tx.objectStore(SESSION_META_STORE).put({
1135
+ namespaceKey: this.namespaceKey,
1136
+ lastSeq: chunk.maxSeq,
1137
+ updatedAt: this.#now(),
1138
+ schemaVersion: SESSION_EXPORT_SCHEMA_VERSION,
1139
+ });
1140
+ await transactionDone(tx);
1141
+ return chunk;
1142
+ }
1143
+ async readEvents(options = {}) {
1144
+ const db = await this.#open();
1145
+ const events = await this.#readEventsFromChunks(db, options);
1146
+ return events.sort(compareTimelineEvents);
1147
+ }
1148
+ async readMeta() {
1149
+ const db = await this.#open();
1150
+ return requestToPromise(db
1151
+ .transaction(SESSION_META_STORE, 'readonly')
1152
+ .objectStore(SESSION_META_STORE)
1153
+ .get(this.namespaceKey));
1154
+ }
1155
+ async readLatestRrwebFullSnapshotBefore(beforeMs) {
1156
+ const db = await this.#open();
1157
+ const range = IDBKeyRange.bound([this.namespaceKey, 0], [this.namespaceKey, beforeMs], false, true);
1158
+ const tx = db.transaction(EVENT_CHUNKS_STORE, 'readonly');
1159
+ const cursor = tx
1160
+ .objectStore(EVENT_CHUNKS_STORE)
1161
+ .index(EVENT_CHUNKS_BY_NAMESPACE_MIN_TIMESTAMP_INDEX)
1162
+ .openCursor(range, 'prev');
1163
+ return findLatestRrwebFullSnapshotCursor(cursor, beforeMs);
1164
+ }
1165
+ async #nextChunkSeq(db) {
1166
+ const tx = db.transaction(EVENT_CHUNKS_STORE, 'readonly');
1167
+ const store = tx.objectStore(EVENT_CHUNKS_STORE);
1168
+ const range = IDBKeyRange.bound([this.namespaceKey, 0], [this.namespaceKey, Number.MAX_SAFE_INTEGER]);
1169
+ const cursor = await requestToPromise(store.openCursor(range, 'prev'));
1170
+ return cursor ? cursor.value.chunkSeq + 1 : 0;
1171
+ }
1172
+ async #readEventsFromChunks(db, options) {
1173
+ const tx = db.transaction(EVENT_CHUNKS_STORE, 'readonly');
1174
+ const store = tx.objectStore(EVENT_CHUNKS_STORE);
1175
+ if (options.sinceMs !== undefined) {
1176
+ const range = IDBKeyRange.bound([this.namespaceKey, options.sinceMs], [this.namespaceKey, Number.MAX_SAFE_INTEGER]);
1177
+ return collectEventsCursor(store.index(EVENT_CHUNKS_BY_NAMESPACE_MAX_TIMESTAMP_INDEX).openCursor(range), options);
1178
+ }
1179
+ const range = IDBKeyRange.bound([this.namespaceKey, 0], [this.namespaceKey, Number.MAX_SAFE_INTEGER]);
1180
+ return collectEventsCursor(store.openCursor(range), options);
1181
+ }
1182
+ #open() {
1183
+ this.#dbPromise ??= openDatabase(this.dbName);
1184
+ return this.#dbPromise;
1185
+ }
1186
+ }
1187
+ function openDatabase(dbName) {
1188
+ return new Promise((resolve, reject) => {
1189
+ const request = globalThis.indexedDB.open(dbName, IDB_VERSION);
1190
+ request.onupgradeneeded = () => {
1191
+ const db = request.result;
1192
+ if (!db.objectStoreNames.contains(SESSION_META_STORE)) {
1193
+ db.createObjectStore(SESSION_META_STORE, { keyPath: 'namespaceKey' });
1194
+ }
1195
+ let eventChunksStore;
1196
+ if (!db.objectStoreNames.contains(EVENT_CHUNKS_STORE)) {
1197
+ eventChunksStore = db.createObjectStore(EVENT_CHUNKS_STORE, {
1198
+ keyPath: ['namespaceKey', 'chunkSeq'],
1199
+ });
1200
+ }
1201
+ else {
1202
+ eventChunksStore = request.transaction.objectStore(EVENT_CHUNKS_STORE);
1203
+ }
1204
+ if (!eventChunksStore.indexNames.contains(EVENT_CHUNKS_BY_NAMESPACE_MAX_TIMESTAMP_INDEX)) {
1205
+ eventChunksStore.createIndex(EVENT_CHUNKS_BY_NAMESPACE_MAX_TIMESTAMP_INDEX, [
1206
+ 'namespaceKey',
1207
+ 'maxTimestamp',
1208
+ ]);
1209
+ }
1210
+ if (!eventChunksStore.indexNames.contains(EVENT_CHUNKS_BY_NAMESPACE_MIN_TIMESTAMP_INDEX)) {
1211
+ eventChunksStore.createIndex(EVENT_CHUNKS_BY_NAMESPACE_MIN_TIMESTAMP_INDEX, [
1212
+ 'namespaceKey',
1213
+ 'minTimestamp',
1214
+ ]);
1215
+ }
1216
+ };
1217
+ request.onerror = () => reject(request.error ?? new Error('Failed to open IndexedDB.'));
1218
+ request.onsuccess = () => resolve(request.result);
1219
+ });
1220
+ }
1221
+ function requestToPromise(request) {
1222
+ return new Promise((resolve, reject) => {
1223
+ request.onerror = () => reject(request.error ?? new Error('IndexedDB request failed.'));
1224
+ request.onsuccess = () => resolve(request.result);
1225
+ });
1226
+ }
1227
+ function transactionDone(transaction) {
1228
+ return new Promise((resolve, reject) => {
1229
+ transaction.oncomplete = () => resolve();
1230
+ transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB transaction failed.'));
1231
+ transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB transaction aborted.'));
1232
+ });
1233
+ }
1234
+ function collectEventsCursor(request, options) {
1235
+ return new Promise((resolve, reject) => {
1236
+ const values = [];
1237
+ const sinceMs = options.sinceMs ?? Number.NEGATIVE_INFINITY;
1238
+ const untilMs = options.untilMs ?? Number.POSITIVE_INFINITY;
1239
+ request.onerror = () => reject(request.error ?? new Error('IndexedDB cursor failed.'));
1240
+ request.onsuccess = () => {
1241
+ const cursor = request.result;
1242
+ if (!cursor) {
1243
+ resolve(values);
1244
+ return;
1245
+ }
1246
+ const chunk = cursor.value;
1247
+ for (const event of chunk.events) {
1248
+ if (event.timestamp >= sinceMs && event.timestamp <= untilMs) {
1249
+ values.push(event);
1250
+ }
1251
+ }
1252
+ cursor.continue();
1253
+ };
1254
+ });
1255
+ }
1256
+ function findLatestRrwebFullSnapshotCursor(request, beforeMs) {
1257
+ return new Promise((resolve, reject) => {
1258
+ request.onerror = () => reject(request.error ?? new Error('IndexedDB cursor failed.'));
1259
+ request.onsuccess = () => {
1260
+ const cursor = request.result;
1261
+ if (!cursor) {
1262
+ resolve(undefined);
1263
+ return;
1264
+ }
1265
+ const chunk = cursor.value;
1266
+ const snapshot = latestRrwebFullSnapshotBefore(chunk.events, beforeMs);
1267
+ if (snapshot) {
1268
+ resolve(snapshot);
1269
+ return;
1270
+ }
1271
+ cursor.continue();
1272
+ };
1273
+ });
1274
+ }
1275
+
1276
+ function resolveSession(options) {
1277
+ const storage = getSessionStorage();
1278
+ if (!storage) {
1279
+ return memoryOnlySession(options.sessionId);
1280
+ }
1281
+ const sessionStorageKey = options.sessionStorageKey ?? DEFAULT_SESSION_STORAGE_KEY;
1282
+ const stored = readStoredSessionState(storage, sessionStorageKey);
1283
+ const persistentSessionId = options.persistentSessionId ?? stored?.persistentSessionId ?? createPersistentSessionId();
1284
+ const initialSeq = Math.max(0, (stored?.lastSeq ?? -1) + 1);
1285
+ writeStoredSessionState(storage, sessionStorageKey, {
1286
+ persistentSessionId,
1287
+ lastSeq: initialSeq - 1,
1288
+ });
1289
+ return {
1290
+ persistentSessionId,
1291
+ namespaceKey: persistentSessionId,
1292
+ initialSeq,
1293
+ updateLastSeq(seq) {
1294
+ writeStoredSessionState(storage, sessionStorageKey, {
1295
+ persistentSessionId,
1296
+ lastSeq: seq,
1297
+ });
1298
+ },
1299
+ };
1300
+ }
1301
+ function memoryOnlySession(sessionId) {
1302
+ return {
1303
+ namespaceKey: sessionId,
1304
+ initialSeq: 0,
1305
+ updateLastSeq() {
1306
+ // sessionStorage 不可用时,按 Spec §1.3.3 仅保留当前运行期会话。
1307
+ },
1308
+ };
1309
+ }
1310
+ function getSessionStorage() {
1311
+ try {
1312
+ return globalThis.sessionStorage;
1313
+ }
1314
+ catch {
1315
+ return undefined;
1316
+ }
1317
+ }
1318
+ function readStoredSessionState(storage, sessionStorageKey) {
1319
+ try {
1320
+ const raw = storage.getItem(sessionStorageKey);
1321
+ if (!raw) {
1322
+ return undefined;
1323
+ }
1324
+ const parsed = JSON.parse(raw);
1325
+ if (typeof parsed.persistentSessionId !== 'string') {
1326
+ return undefined;
1327
+ }
1328
+ return {
1329
+ persistentSessionId: parsed.persistentSessionId,
1330
+ lastSeq: typeof parsed.lastSeq === 'number' ? parsed.lastSeq : undefined,
1331
+ };
1332
+ }
1333
+ catch {
1334
+ return undefined;
1335
+ }
1336
+ }
1337
+ function writeStoredSessionState(storage, sessionStorageKey, state) {
1338
+ try {
1339
+ storage.setItem(sessionStorageKey, JSON.stringify(state));
1340
+ }
1341
+ catch {
1342
+ // 写入失败等同 sessionStorage 不可用;当前运行期仍可继续采集与导出内存/IDB 数据。
1343
+ }
1344
+ }
1345
+ function createPersistentSessionId() {
1346
+ if (globalThis.crypto?.randomUUID) {
1347
+ return globalThis.crypto.randomUUID();
1348
+ }
1349
+ return `persistent-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1350
+ }
1351
+
1352
+ class WebLoggerCoreController {
1353
+ sessionId;
1354
+ persistentSessionId;
1355
+ namespaceKey;
1356
+ config;
1357
+ #timeline;
1358
+ #store;
1359
+ #flushPromise;
1360
+ #estimatedBufferBytes = 0;
1361
+ #destroyed = false;
1362
+ #now;
1363
+ #sessionState;
1364
+ #readyPromise;
1365
+ #collectorDisposers = [];
1366
+ constructor(options = {}) {
1367
+ this.sessionId = options.sessionId ?? createSessionId();
1368
+ this.#sessionState = resolveSession({
1369
+ sessionId: this.sessionId,
1370
+ persistentSessionId: options.persistentSessionId,
1371
+ sessionStorageKey: options.sessionStorageKey,
1372
+ });
1373
+ this.persistentSessionId = this.#sessionState.persistentSessionId;
1374
+ this.namespaceKey = this.#sessionState.namespaceKey;
1375
+ this.#now = options.now ?? Date.now;
1376
+ this.config = {
1377
+ ...options,
1378
+ memoryFlushThresholdBytes: options.memoryFlushThresholdBytes ?? DEFAULT_MEMORY_FLUSH_THRESHOLD_BYTES,
1379
+ };
1380
+ this.#timeline = new Timeline({
1381
+ sessionId: this.sessionId,
1382
+ now: this.#now,
1383
+ initialSeq: this.#sessionState.initialSeq,
1384
+ });
1385
+ if (IndexedDbEventStore.isSupported()) {
1386
+ this.#store = new IndexedDbEventStore({
1387
+ namespaceKey: this.namespaceKey,
1388
+ now: this.#now,
1389
+ });
1390
+ }
1391
+ this.#readyPromise = this.#hydrateSeqFromIdb();
1392
+ this.#collectorDisposers = [
1393
+ installConsoleCollector({
1394
+ record: (event) => this.record(event),
1395
+ recordConsoleCallSite: options.recordConsoleCallSite !== false,
1396
+ }),
1397
+ installErrorCollector({ record: (event) => this.record(event) }),
1398
+ installFetchCollector({ config: this.config, record: (event) => this.record(event) }),
1399
+ installXhrCollector({ config: this.config, record: (event) => this.record(event) }),
1400
+ installStorageCollector({ config: this.config, record: (event) => this.record(event) }),
1401
+ installCookieCollector({ config: this.config, record: (event) => this.record(event) }),
1402
+ installRrwebCollector({
1403
+ record: (event) => this.record(event),
1404
+ checkoutEveryNms: this.config.rrwebCheckoutEveryNms,
1405
+ }),
1406
+ ];
1407
+ }
1408
+ record(event) {
1409
+ this.#assertActive();
1410
+ const recorded = this.#timeline.append(event);
1411
+ this.#sessionState.updateLastSeq(recorded.seq);
1412
+ this.#estimatedBufferBytes += estimateEventBytes(recorded);
1413
+ if (this.#estimatedBufferBytes >= this.config.memoryFlushThresholdBytes) {
1414
+ void this.flushToIdb().catch(() => {
1415
+ // Export still includes memory events if background persistence fails.
1416
+ });
1417
+ }
1418
+ return recorded;
1419
+ }
1420
+ async flushToIdb() {
1421
+ this.#assertActive();
1422
+ if (!this.#store) {
1423
+ return;
1424
+ }
1425
+ await this.#waitUntilReady();
1426
+ if (this.#flushPromise) {
1427
+ return this.#flushPromise;
1428
+ }
1429
+ const pendingEvents = this.#timeline.pending();
1430
+ if (pendingEvents.length === 0) {
1431
+ return;
1432
+ }
1433
+ this.#flushPromise = this.#store
1434
+ .appendChunk(pendingEvents)
1435
+ .then(() => {
1436
+ this.#timeline.clear(pendingEvents);
1437
+ this.#estimatedBufferBytes = estimateEventsBytes(this.#timeline.pending());
1438
+ })
1439
+ .finally(() => {
1440
+ this.#flushPromise = undefined;
1441
+ });
1442
+ return this.#flushPromise;
1443
+ }
1444
+ async exportFull() {
1445
+ this.#assertActive();
1446
+ await this.#waitUntilReady();
1447
+ await this.#waitForFlush();
1448
+ const persistedEvents = await this.#readPersistedEvents();
1449
+ return this.#createExport('full', mergeEvents(persistedEvents, this.#timeline.all()));
1450
+ }
1451
+ async exportSince(sinceMs) {
1452
+ this.#assertActive();
1453
+ const untilMs = this.#now();
1454
+ await this.#waitUntilReady();
1455
+ await this.#waitForFlush();
1456
+ const persistedEvents = await this.#readPersistedEvents({ sinceMs, untilMs });
1457
+ const memoryEvents = this.#timeline.since(sinceMs, untilMs);
1458
+ const windowEvents = mergeEvents(persistedEvents, memoryEvents);
1459
+ const rrwebAnchor = hasRrwebFullSnapshot(windowEvents)
1460
+ ? undefined
1461
+ : await this.#readLatestRrwebFullSnapshotBefore(sinceMs);
1462
+ const events = rrwebAnchor ? mergeEvents([rrwebAnchor], windowEvents) : windowEvents;
1463
+ const includesRrwebBaseline = hasRrwebFullSnapshot(events);
1464
+ return this.#createExport('since', events, {
1465
+ sinceMs,
1466
+ rrwebAnchorSeq: rrwebAnchor?.seq,
1467
+ includesRrwebBaseline,
1468
+ replayableStandalone: includesRrwebBaseline,
1469
+ });
1470
+ }
1471
+ destroy() {
1472
+ for (const dispose of this.#collectorDisposers.splice(0)) {
1473
+ dispose();
1474
+ }
1475
+ this.#timeline.clear();
1476
+ this.#destroyed = true;
1477
+ }
1478
+ #createExport(kind, events, exportMeta = {}) {
1479
+ return {
1480
+ schemaVersion: SESSION_EXPORT_SCHEMA_VERSION,
1481
+ sessionId: this.sessionId,
1482
+ ...(this.persistentSessionId ? { persistentSessionId: this.persistentSessionId } : {}),
1483
+ exportedAt: new Date(this.#now()).toISOString(),
1484
+ export: { kind, ...exportMeta },
1485
+ events,
1486
+ };
1487
+ }
1488
+ #assertActive() {
1489
+ if (this.#destroyed) {
1490
+ throw new Error('WebLoggerController has been destroyed.');
1491
+ }
1492
+ }
1493
+ async #waitForFlush() {
1494
+ if (this.#flushPromise) {
1495
+ await this.#flushPromise;
1496
+ }
1497
+ }
1498
+ async #waitUntilReady() {
1499
+ await this.#readyPromise;
1500
+ }
1501
+ async #hydrateSeqFromIdb() {
1502
+ if (!this.#store) {
1503
+ return;
1504
+ }
1505
+ try {
1506
+ const meta = await this.#store.readMeta();
1507
+ if (!meta) {
1508
+ return;
1509
+ }
1510
+ const nextSeq = meta.lastSeq + 1;
1511
+ this.#timeline.ensureNextSeqAtLeast(nextSeq);
1512
+ this.#sessionState.updateLastSeq(meta.lastSeq);
1513
+ }
1514
+ catch {
1515
+ // IDB 元数据校准失败不阻断当前运行期采集;导出仍会包含内存事件。
1516
+ }
1517
+ }
1518
+ async #readPersistedEvents(options = {}) {
1519
+ if (!this.#store) {
1520
+ return [];
1521
+ }
1522
+ return this.#store.readEvents(options);
1523
+ }
1524
+ async #readLatestRrwebFullSnapshotBefore(beforeMs) {
1525
+ const anchors = [
1526
+ await this.#store?.readLatestRrwebFullSnapshotBefore(beforeMs),
1527
+ this.#timeline.latestRrwebFullSnapshotBefore(beforeMs),
1528
+ ].filter((event) => event !== undefined);
1529
+ return anchors.sort(compareTimelineEvents).at(-1);
1530
+ }
1531
+ }
1532
+ function initWebLogger(options = {}) {
1533
+ return new WebLoggerCoreController(options);
1534
+ }
1535
+ function createSessionId() {
1536
+ if (globalThis.crypto?.randomUUID) {
1537
+ return globalThis.crypto.randomUUID();
1538
+ }
1539
+ return `session-${Date.now()}-${Math.random().toString(36).slice(2)}`;
1540
+ }
1541
+ function estimateEventBytes(event) {
1542
+ return JSON.stringify(event).length * 2;
1543
+ }
1544
+ function estimateEventsBytes(events) {
1545
+ return events.reduce((total, event) => total + estimateEventBytes(event), 0);
1546
+ }
1547
+ function mergeEvents(persistedEvents, memoryEvents) {
1548
+ const bySeq = new Map();
1549
+ for (const event of persistedEvents) {
1550
+ bySeq.set(event.seq, event);
1551
+ }
1552
+ for (const event of memoryEvents) {
1553
+ bySeq.set(event.seq, event);
1554
+ }
1555
+ return [...bySeq.values()].sort(compareTimelineEvents);
1556
+ }
1557
+
1558
+ export { CONSOLE_CAPTURE_METHODS, DEFAULT_INDEXED_DB_NAME, DEFAULT_MEMORY_FLUSH_THRESHOLD_BYTES, DEFAULT_RRWEB_CHECKOUT_EVERY_NMS, DEFAULT_SESSION_STORAGE_KEY, EVENT_CHUNKS_STORE, IDB_VERSION, WebLoggerCoreController as InMemoryWebLoggerController, IndexedDbEventStore, PACKAGE_NAME, SESSION_EXPORT_SCHEMA_VERSION, SESSION_META_STORE, Timeline, WebLoggerCoreController, compareTimelineEvents, initWebLogger, resolveSession };
1559
+ //# sourceMappingURL=index.js.map