@infinitewatch/web-core 0.0.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/dist/index.js ADDED
@@ -0,0 +1,1681 @@
1
+ // src/core.ts
2
+ var isRecord = (value) => typeof value === "object" && value !== null;
3
+ var getMutationData = (event) => {
4
+ if (!event || event.type !== 3) return null;
5
+ const data = event.data;
6
+ if (!data || data.source !== 0) return null;
7
+ return data;
8
+ };
9
+ var LIB_VERSION = "v43.1";
10
+ var EXCLUDED_ORGANIZATIONS = {
11
+ NORTHIUS: "68f7bc5a815d853ecb2ce914"
12
+ };
13
+ var EXCLUDED_ORG_IDS = new Set(Object.values(EXCLUDED_ORGANIZATIONS));
14
+ var DEFAULT_CONFIG = {
15
+ baseUrl: "https://ingest.infinitewatch.ai",
16
+ endpoint: "https://ingest.infinitewatch.ai/v1/ingest",
17
+ endpointConfig: "https://ingest.infinitewatch.ai/v1/ingest/config",
18
+ sessionId: null,
19
+ userId: null,
20
+ organizationId: null,
21
+ debug: false,
22
+ defaultSamplingPercent: 100,
23
+ maskAllInputs: true,
24
+ maskInputOptions: {
25
+ password: true,
26
+ email: true,
27
+ text: true,
28
+ tel: true,
29
+ url: true,
30
+ textarea: true,
31
+ select: true,
32
+ checkbox: true,
33
+ radio: true,
34
+ file: true
35
+ },
36
+ blockClass: "",
37
+ blockSelector: "",
38
+ maskTextSelector: "",
39
+ maskTextClass: "",
40
+ recordIframe: false,
41
+ recordCanvas: false,
42
+ recordCrossOriginIframes: false,
43
+ sessionStorageKey: "iw_session",
44
+ autoResumeSession: true,
45
+ batchSize: 200,
46
+ flushInterval: 1e4,
47
+ sessionTimeout: 30 * 60 * 1e3,
48
+ sessionHeartbeatInterval: null,
49
+ refresh: false,
50
+ inlineStylesheet: false,
51
+ generateInsights: false,
52
+ blackList: ["yui_"]
53
+ };
54
+ var BYTES_IN_KILOBYTE = 1024;
55
+ var URL_PARAMS = {
56
+ VERSION: "v",
57
+ REQUEST_TYPE: "t",
58
+ KEEPALIVE: "k",
59
+ SIZE: "s",
60
+ COMPRESSION: "c",
61
+ SESSION_ID: "i"
62
+ };
63
+ var getWindow = () => {
64
+ if (typeof window !== "undefined") {
65
+ return window;
66
+ }
67
+ if (typeof globalThis !== "undefined" && "window" in globalThis) {
68
+ return globalThis.window;
69
+ }
70
+ const doc = typeof document !== "undefined" ? document : typeof globalThis !== "undefined" && "document" in globalThis ? (
71
+ // biome-ignore lint/suspicious/noExplicitAny: globalThis typing for non-standard environments
72
+ globalThis.document
73
+ ) : void 0;
74
+ return doc?.defaultView ?? void 0;
75
+ };
76
+ var getDocument = () => {
77
+ if (typeof document !== "undefined") {
78
+ return document;
79
+ }
80
+ if (typeof globalThis !== "undefined" && "document" in globalThis) {
81
+ return globalThis.document;
82
+ }
83
+ return void 0;
84
+ };
85
+ var isBrowser = () => !!(getWindow() && getDocument());
86
+ var now = () => Date.now();
87
+ var isOrganizationExcluded = (orgId) => {
88
+ return !!orgId && EXCLUDED_ORG_IDS.has(orgId);
89
+ };
90
+ var createWebCore = () => {
91
+ let config = { ...DEFAULT_CONFIG };
92
+ let isInitialized = false;
93
+ let eventBuffer = [];
94
+ let flushTimer = null;
95
+ let stopRecording = null;
96
+ let sessionId = null;
97
+ let userId = null;
98
+ let externalId = null;
99
+ let organizationId = null;
100
+ let sessionHeartbeatTimer = null;
101
+ let sessionExpiryTimer = null;
102
+ let isSessionActive = false;
103
+ let sessionStartTime = null;
104
+ let isSampledIn = true;
105
+ let hardStop = false;
106
+ let contractStatus = null;
107
+ let quotaStatus = null;
108
+ let lastRecordedUrl = "";
109
+ let urlChangeCleanup = null;
110
+ const log = (message, ...args) => {
111
+ if (config.debug) {
112
+ console.log(
113
+ "[InfiniteWatch]",
114
+ (/* @__PURE__ */ new Date()).toISOString(),
115
+ message,
116
+ ...args
117
+ );
118
+ }
119
+ };
120
+ const error = (message, ...args) => {
121
+ if (config.debug) {
122
+ console.error(
123
+ "[InfiniteWatch]",
124
+ (/* @__PURE__ */ new Date()).toISOString(),
125
+ "ERROR:",
126
+ message,
127
+ ...args
128
+ );
129
+ if (args.length > 0 && args[0] instanceof Error) {
130
+ console.error("[InfiniteWatch] Stack trace:", args[0].stack);
131
+ }
132
+ }
133
+ };
134
+ const generateSessionId = () => {
135
+ const nextId = `session_${now()}_${Math.random().toString(36).slice(2, 11)}`;
136
+ log("Generated new session ID:", nextId);
137
+ return nextId;
138
+ };
139
+ const generateUserId = () => {
140
+ const nextId = `user_${Math.random().toString(36).slice(2, 11)}`;
141
+ log("Generated new user ID:", nextId);
142
+ return nextId;
143
+ };
144
+ const getSamplingPercentForOrg = (orgId) => {
145
+ if (!orgId) {
146
+ return typeof config.defaultSamplingPercent === "number" ? config.defaultSamplingPercent : 50;
147
+ }
148
+ const base = typeof config.defaultSamplingPercent === "number" ? config.defaultSamplingPercent : 50;
149
+ return Math.max(0, Math.min(100, base));
150
+ };
151
+ const hashStringToPercent = (value) => {
152
+ let hash = 5381;
153
+ const input = String(value);
154
+ for (let i = 0; i < input.length; i += 1) {
155
+ hash = (hash << 5) + hash + input.charCodeAt(i);
156
+ hash |= 0;
157
+ }
158
+ return Math.abs(hash) % 100;
159
+ };
160
+ const shouldSample = (orgId, sessId) => {
161
+ const percent = getSamplingPercentForOrg(orgId);
162
+ const bucket = hashStringToPercent(sessId || "");
163
+ return bucket < percent;
164
+ };
165
+ const compressGzip = async (data) => {
166
+ if (typeof CompressionStream !== "undefined") {
167
+ try {
168
+ const originalSize = new Blob([data]).size;
169
+ const stream = new Blob([data]).stream();
170
+ const compressedStream = stream.pipeThrough(
171
+ new CompressionStream("gzip")
172
+ );
173
+ const compressedBlob = await new Response(compressedStream).blob();
174
+ return { body: compressedBlob, compressed: true, originalSize };
175
+ } catch (err) {
176
+ log("Compression failed, sending uncompressed:", err);
177
+ const fallbackBlob2 = new Blob([data]);
178
+ return {
179
+ body: fallbackBlob2,
180
+ compressed: false,
181
+ originalSize: fallbackBlob2.size
182
+ };
183
+ }
184
+ }
185
+ log("CompressionStream not available, sending uncompressed");
186
+ const fallbackBlob = new Blob([data]);
187
+ return {
188
+ body: fallbackBlob,
189
+ compressed: false,
190
+ originalSize: fallbackBlob.size
191
+ };
192
+ };
193
+ const getCookieDomain = () => {
194
+ const win = getWindow();
195
+ if (!win) return "";
196
+ const hostname = win.location.hostname;
197
+ if (hostname === "localhost" || hostname === "127.0.0.1" || /^\d+\.\d+\.\d+\.\d+$/.test(hostname)) {
198
+ return "";
199
+ }
200
+ const parts = hostname.split(".");
201
+ if (parts.length >= 2) {
202
+ return `.${parts.slice(-2).join(".")}`;
203
+ }
204
+ return hostname;
205
+ };
206
+ const getCookieSecurityAttrs = () => {
207
+ return "; Secure; SameSite=None";
208
+ };
209
+ const setCookie = (name, value, days = 365) => {
210
+ const doc = getDocument();
211
+ if (!doc) return;
212
+ const expires = /* @__PURE__ */ new Date();
213
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1e3);
214
+ const encodedValue = encodeURIComponent(value);
215
+ const domain = getCookieDomain();
216
+ const domainAttr = domain ? `; Domain=${domain}` : "";
217
+ const securityAttrs = getCookieSecurityAttrs();
218
+ const cookieBase = `${name}=${encodedValue}`;
219
+ const cookieWithAttrs = `${cookieBase}; expires=${expires.toUTCString()}; Path=/${domainAttr}${securityAttrs}`;
220
+ doc.cookie = cookieWithAttrs;
221
+ if (!doc.cookie.includes(`${name}=`)) {
222
+ doc.cookie = cookieBase;
223
+ }
224
+ };
225
+ const getCookie = (name) => {
226
+ const doc = getDocument();
227
+ if (!doc) return null;
228
+ const nameEQ = `${name}=`;
229
+ const ca = doc.cookie.split(";");
230
+ for (let i = 0; i < ca.length; i += 1) {
231
+ let c = ca[i] ?? "";
232
+ while (c.charAt(0) === " ") c = c.substring(1, c.length);
233
+ if (c.indexOf(nameEQ) === 0) {
234
+ return decodeURIComponent(c.substring(nameEQ.length, c.length));
235
+ }
236
+ }
237
+ return null;
238
+ };
239
+ const deleteCookie = (name) => {
240
+ const doc = getDocument();
241
+ if (!doc) return;
242
+ const domain = getCookieDomain();
243
+ const domainAttr = domain ? `; Domain=${domain}` : "";
244
+ const securityAttrs = getCookieSecurityAttrs();
245
+ doc.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; Path=/${domainAttr}${securityAttrs}`;
246
+ };
247
+ const saveSessionToStorage = () => {
248
+ const storageKey = config.sessionStorageKey || "iw_session";
249
+ if (!storageKey) {
250
+ log("No session storage key configured, skipping save");
251
+ return;
252
+ }
253
+ const sessionData = {
254
+ sessionId,
255
+ userId,
256
+ organizationId,
257
+ lastActivity: now(),
258
+ sessionStartTime,
259
+ isActive: isSessionActive,
260
+ config: {
261
+ endpoint: config.endpoint,
262
+ sessionTimeout: config.sessionTimeout
263
+ }
264
+ };
265
+ if (externalId) {
266
+ sessionData.externalId = externalId;
267
+ }
268
+ try {
269
+ setCookie(storageKey, JSON.stringify(sessionData), 730);
270
+ log("Saved session to storage with user identity");
271
+ } catch (err) {
272
+ error("Failed to save session to storage:", err);
273
+ }
274
+ };
275
+ const clearSessionFromStorage = () => {
276
+ const storageKey = config.sessionStorageKey || "iw_session";
277
+ if (!storageKey) {
278
+ log("No session storage key configured, skipping clear");
279
+ return;
280
+ }
281
+ try {
282
+ deleteCookie(storageKey);
283
+ } catch (err) {
284
+ error("Failed to clear session from storage:", err);
285
+ }
286
+ };
287
+ const loadSessionFromStorage = () => {
288
+ const storageKey = config.sessionStorageKey || "iw_session";
289
+ if (!storageKey) {
290
+ log("No session storage key configured, skipping load");
291
+ return null;
292
+ }
293
+ try {
294
+ const sessionData = getCookie(storageKey);
295
+ if (!sessionData) {
296
+ log("No session data found in storage");
297
+ return null;
298
+ }
299
+ const parsed = JSON.parse(sessionData);
300
+ const timeSinceLastActivity = now() - parsed.lastActivity;
301
+ const totalSessionDuration = parsed.sessionStartTime ? now() - parsed.sessionStartTime : 0;
302
+ if (timeSinceLastActivity > config.sessionTimeout || totalSessionDuration > config.sessionTimeout) {
303
+ clearSessionFromStorage();
304
+ return null;
305
+ }
306
+ log("Session loaded from storage successfully:", parsed);
307
+ return parsed;
308
+ } catch (err) {
309
+ error("Failed to load session from storage:", err);
310
+ clearSessionFromStorage();
311
+ return null;
312
+ }
313
+ };
314
+ const getOrCreateUserId = () => {
315
+ const existingSession = loadSessionFromStorage();
316
+ if (existingSession?.userId) {
317
+ log("Using existing userId from cookie:", existingSession.userId);
318
+ return existingSession.userId;
319
+ }
320
+ const newUserId = generateUserId();
321
+ log("Generated new userId:", newUserId);
322
+ return newUserId;
323
+ };
324
+ const updateSessionActivity = () => {
325
+ if (isSessionActive) {
326
+ saveSessionToStorage();
327
+ if (config.refresh) {
328
+ resetSessionExpiryTimer();
329
+ }
330
+ }
331
+ };
332
+ const startSessionHeartbeat = () => {
333
+ if (sessionHeartbeatTimer) {
334
+ clearInterval(sessionHeartbeatTimer);
335
+ }
336
+ if (config.sessionHeartbeatInterval) {
337
+ sessionHeartbeatTimer = setInterval(() => {
338
+ if (isSessionActive) {
339
+ if (config.refresh) {
340
+ updateSessionActivity();
341
+ } else {
342
+ saveSessionToStorage();
343
+ }
344
+ }
345
+ }, config.sessionHeartbeatInterval);
346
+ }
347
+ };
348
+ const stopSessionHeartbeat = () => {
349
+ if (sessionHeartbeatTimer) {
350
+ clearInterval(sessionHeartbeatTimer);
351
+ sessionHeartbeatTimer = null;
352
+ } else {
353
+ log("No heartbeat timer to stop");
354
+ }
355
+ };
356
+ const startSessionExpiryTimer = () => {
357
+ if (sessionExpiryTimer) {
358
+ clearTimeout(sessionExpiryTimer);
359
+ }
360
+ let timeoutDuration = config.sessionTimeout;
361
+ if (sessionStartTime) {
362
+ const elapsed = now() - sessionStartTime;
363
+ const remaining = config.sessionTimeout - elapsed;
364
+ if (remaining <= 0) {
365
+ clearSessionFromStorage();
366
+ sessionId = generateSessionId();
367
+ sessionStartTime = now();
368
+ timeoutDuration = config.sessionTimeout;
369
+ } else {
370
+ timeoutDuration = remaining;
371
+ }
372
+ }
373
+ sessionExpiryTimer = setTimeout(() => {
374
+ stopRecordingSession();
375
+ }, timeoutDuration);
376
+ };
377
+ const resetSessionExpiryTimer = () => {
378
+ if (sessionExpiryTimer) {
379
+ clearTimeout(sessionExpiryTimer);
380
+ }
381
+ startSessionExpiryTimer();
382
+ };
383
+ const stopSessionExpiryTimer = () => {
384
+ if (sessionExpiryTimer) {
385
+ clearTimeout(sessionExpiryTimer);
386
+ sessionExpiryTimer = null;
387
+ } else {
388
+ log("No session expiry timer to stop");
389
+ }
390
+ };
391
+ const isSessionExpired = (sessionData) => {
392
+ if (!sessionData) {
393
+ log("No session data provided, considering expired");
394
+ return true;
395
+ }
396
+ const timeSinceLastActivity = now() - sessionData.lastActivity;
397
+ const totalSessionDuration = sessionData.sessionStartTime ? now() - sessionData.sessionStartTime : 0;
398
+ return timeSinceLastActivity > config.sessionTimeout || totalSessionDuration > config.sessionTimeout;
399
+ };
400
+ const fetchWithTimeout = async (url, options = {}, timeoutMs = 2e4) => {
401
+ const controller = new AbortController();
402
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
403
+ const mergedOptions = { ...options, signal: controller.signal };
404
+ try {
405
+ return await fetch(url, mergedOptions);
406
+ } catch (err) {
407
+ if (err instanceof DOMException && (err.name === "AbortError" || err.name === "TimeoutError")) {
408
+ return { ok: false, status: 0, statusText: "Aborted" };
409
+ }
410
+ if (typeof err === "object") {
411
+ const message = err.message || "";
412
+ if (message.includes("aborted") || message.includes("cancelled")) {
413
+ return { ok: false, status: 0, statusText: "Aborted" };
414
+ }
415
+ }
416
+ return { ok: false, status: 0, statusText: "Aborted" };
417
+ } finally {
418
+ clearTimeout(timeoutId);
419
+ }
420
+ };
421
+ const buildEndpointUrl = (baseEndpoint, requestType, keepalive, payloadSizeKB, isCompressed = false, sessionIdParam = null) => {
422
+ let url;
423
+ try {
424
+ url = new URL(baseEndpoint);
425
+ } catch {
426
+ return baseEndpoint;
427
+ }
428
+ const currentSessionId = sessionIdParam !== null ? sessionIdParam : sessionId;
429
+ url.searchParams.set(URL_PARAMS.VERSION, LIB_VERSION);
430
+ url.searchParams.set(URL_PARAMS.REQUEST_TYPE, requestType);
431
+ url.searchParams.set(
432
+ URL_PARAMS.KEEPALIVE,
433
+ keepalive === null ? "null" : String(keepalive)
434
+ );
435
+ url.searchParams.set(URL_PARAMS.SIZE, Math.round(payloadSizeKB).toString());
436
+ url.searchParams.set(URL_PARAMS.COMPRESSION, isCompressed ? "1" : "0");
437
+ if (currentSessionId) {
438
+ url.searchParams.set(URL_PARAMS.SESSION_ID, currentSessionId);
439
+ }
440
+ return url.toString();
441
+ };
442
+ const sendEvents = async (events) => {
443
+ if (!events || events.length === 0) {
444
+ log("No events to send, skipping network request");
445
+ return;
446
+ }
447
+ if (contractStatus) {
448
+ log("Events not sent - Contract issue:", contractStatus.message);
449
+ return;
450
+ }
451
+ if (quotaStatus) {
452
+ log("Events not sent - Quota exceeded:", quotaStatus.message);
453
+ return;
454
+ }
455
+ if (config.defaultSamplingPercent === 0) {
456
+ log("Events not sent - samplingPercent set to 0");
457
+ return;
458
+ }
459
+ if (!sessionId || !userId) {
460
+ error("Cannot send events: sessionId or userId not set");
461
+ return;
462
+ }
463
+ const capturedSessionId = sessionId;
464
+ const capturedUserId = userId;
465
+ const capturedExternalId = externalId;
466
+ const capturedOrganizationId = organizationId;
467
+ const eventsToSend = [...events];
468
+ const MAX_CHUNK_SIZE = 40 * BYTES_IN_KILOBYTE;
469
+ const basePayload = {
470
+ session_id: capturedSessionId,
471
+ user_id: capturedUserId,
472
+ organization_id: capturedOrganizationId,
473
+ events: [],
474
+ client_version: LIB_VERSION
475
+ };
476
+ if (capturedExternalId) {
477
+ basePayload.external_id = capturedExternalId;
478
+ }
479
+ const basePayloadString = JSON.stringify(basePayload);
480
+ const basePayloadSize = new Blob([basePayloadString]).size;
481
+ const fullPayloadString = JSON.stringify({
482
+ ...basePayload,
483
+ events: eventsToSend
484
+ });
485
+ const fullPayloadSize = new Blob([fullPayloadString]).size;
486
+ let chunks = [];
487
+ if (fullPayloadSize > MAX_CHUNK_SIZE) {
488
+ let currentChunk = [];
489
+ let currentChunkSize = basePayloadSize;
490
+ for (let i = 0; i < eventsToSend.length; i += 1) {
491
+ const testChunk = [...currentChunk, eventsToSend[i]];
492
+ const testPayloadSize = new Blob([
493
+ JSON.stringify({ ...basePayload, events: testChunk })
494
+ ]).size;
495
+ if (testPayloadSize > MAX_CHUNK_SIZE && currentChunk.length > 0) {
496
+ chunks.push([...currentChunk]);
497
+ currentChunk = [eventsToSend[i]];
498
+ currentChunkSize = new Blob([
499
+ JSON.stringify({ ...basePayload, events: currentChunk })
500
+ ]).size;
501
+ } else {
502
+ currentChunk = testChunk;
503
+ currentChunkSize = testPayloadSize;
504
+ }
505
+ void currentChunkSize;
506
+ }
507
+ if (currentChunk.length > 0) {
508
+ chunks.push(currentChunk);
509
+ }
510
+ } else {
511
+ chunks = [eventsToSend];
512
+ }
513
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
514
+ const chunk = chunks[chunkIndex];
515
+ const chunkPayload = {
516
+ session_id: capturedSessionId,
517
+ user_id: capturedUserId,
518
+ organization_id: capturedOrganizationId,
519
+ events: chunk,
520
+ client_version: LIB_VERSION
521
+ };
522
+ if (capturedExternalId) {
523
+ chunkPayload.external_id = capturedExternalId;
524
+ }
525
+ const chunkPayloadString = JSON.stringify(chunkPayload);
526
+ try {
527
+ const compressionResult = config.debug ? {
528
+ body: chunkPayloadString,
529
+ compressed: false,
530
+ originalSize: new Blob([chunkPayloadString]).size
531
+ } : await compressGzip(chunkPayloadString);
532
+ const bodyToSend = compressionResult.body;
533
+ const bodySizeBytes = typeof bodyToSend === "string" ? new Blob([bodyToSend]).size : bodyToSend instanceof Blob ? bodyToSend.size : new Blob([bodyToSend]).size;
534
+ const payloadSizeKB = bodySizeBytes / BYTES_IN_KILOBYTE;
535
+ if (payloadSizeKB * BYTES_IN_KILOBYTE > MAX_CHUNK_SIZE) {
536
+ const endpointUrl = buildEndpointUrl(
537
+ config.endpoint,
538
+ "f",
539
+ false,
540
+ payloadSizeKB,
541
+ compressionResult.compressed,
542
+ capturedSessionId
543
+ );
544
+ const headers = {
545
+ "Content-Type": compressionResult.compressed ? "application/gzip" : "text/plain"
546
+ };
547
+ const response = await fetchWithTimeout(
548
+ endpointUrl,
549
+ {
550
+ method: "POST",
551
+ headers,
552
+ body: bodyToSend,
553
+ keepalive: false
554
+ },
555
+ 2e4
556
+ );
557
+ if (response?.status === 0) {
558
+ continue;
559
+ }
560
+ if (response && !response.ok && response.status >= 500) {
561
+ error(`Failed to send chunk ${chunkIndex + 1}:`, response.status);
562
+ }
563
+ } else {
564
+ let sent = false;
565
+ if (isBrowser() && navigator.sendBeacon) {
566
+ try {
567
+ const endpointUrl2 = buildEndpointUrl(
568
+ config.endpoint,
569
+ "b",
570
+ null,
571
+ payloadSizeKB,
572
+ compressionResult.compressed,
573
+ capturedSessionId
574
+ );
575
+ sent = navigator.sendBeacon(endpointUrl2, bodyToSend);
576
+ if (sent) {
577
+ continue;
578
+ }
579
+ } catch (err) {
580
+ log(
581
+ "Compression failed for sendBeacon, falling back to fetch:",
582
+ err
583
+ );
584
+ }
585
+ } else {
586
+ log(
587
+ `sendBeacon not supported for chunk ${chunkIndex + 1}, using fetch`
588
+ );
589
+ }
590
+ const endpointUrl = buildEndpointUrl(
591
+ config.endpoint,
592
+ "f",
593
+ false,
594
+ payloadSizeKB,
595
+ compressionResult.compressed,
596
+ capturedSessionId
597
+ );
598
+ const headers = {
599
+ "Content-Type": compressionResult.compressed ? "application/gzip" : "text/plain"
600
+ };
601
+ fetchWithTimeout(
602
+ endpointUrl,
603
+ {
604
+ method: "POST",
605
+ headers,
606
+ body: bodyToSend,
607
+ keepalive: false
608
+ },
609
+ 2e4
610
+ ).catch(() => {
611
+ });
612
+ }
613
+ } catch (err) {
614
+ const errorMessage = err?.message || "";
615
+ const isBlocked2 = errorMessage.includes("Failed to fetch") || errorMessage.includes("NetworkError") || errorMessage.includes("ERR_BLOCKED_BY_CLIENT") || errorMessage.includes("ERR_BLOCKED_BY_RESPONSE") || errorMessage.includes("net::ERR_FAILED");
616
+ const isAborted = err?.name === "AbortError" || err?.name === "TimeoutError" || errorMessage.includes("aborted") || errorMessage.includes("cancelled");
617
+ if (isBlocked2 || isAborted) {
618
+ return;
619
+ }
620
+ return;
621
+ }
622
+ }
623
+ };
624
+ const flushEvents = () => {
625
+ if (eventBuffer.length === 0) {
626
+ log("No events in buffer to flush");
627
+ return;
628
+ }
629
+ const eventsToSend = [...eventBuffer];
630
+ eventBuffer = [];
631
+ void sendEvents(eventsToSend);
632
+ };
633
+ const startFlushTimer = () => {
634
+ if (flushTimer) {
635
+ log("Clearing existing flush timer");
636
+ clearInterval(flushTimer);
637
+ }
638
+ flushTimer = setInterval(() => {
639
+ flushEvents();
640
+ }, config.flushInterval);
641
+ };
642
+ const stopFlushTimer = () => {
643
+ if (flushTimer) {
644
+ clearInterval(flushTimer);
645
+ flushTimer = null;
646
+ } else {
647
+ log("No flush timer to stop");
648
+ }
649
+ };
650
+ const loadRrweb = () => new Promise((resolve, reject) => {
651
+ if (!isBrowser()) {
652
+ reject(new Error("rrweb requires a browser environment"));
653
+ return;
654
+ }
655
+ if (window.rrweb) {
656
+ resolve(window.rrweb);
657
+ return;
658
+ }
659
+ const script = document.createElement("script");
660
+ script.src = "https://app.infinitewatch.ai/rrweb.min.js";
661
+ script.onload = () => {
662
+ if (window.rrweb) {
663
+ resolve(window.rrweb);
664
+ } else {
665
+ reject(new Error("rrweb failed to load"));
666
+ }
667
+ };
668
+ script.onerror = () => reject(new Error("Failed to load rrweb script"));
669
+ document.head.appendChild(script);
670
+ });
671
+ const loadRrwebPacker = () => new Promise((resolve, reject) => {
672
+ try {
673
+ if (!isBrowser()) {
674
+ reject(new Error("rrweb packer requires a browser environment"));
675
+ return;
676
+ }
677
+ const existing = window.rrwebPack?.pack || window.pack;
678
+ if (typeof existing === "function") {
679
+ resolve(existing);
680
+ return;
681
+ }
682
+ const script = document.createElement("script");
683
+ script.src = "https://app.infinitewatch.ai/rrweb-record-pack.min.js";
684
+ script.async = true;
685
+ script.onload = () => {
686
+ const packFn = window.rrwebRecord?.pack || window.pack;
687
+ if (typeof packFn === "function") {
688
+ resolve(packFn);
689
+ } else {
690
+ reject(
691
+ new Error("rrweb packer loaded but pack function not found")
692
+ );
693
+ }
694
+ };
695
+ script.onerror = () => reject(new Error("Failed to load rrweb packer script"));
696
+ document.head.appendChild(script);
697
+ } catch (e) {
698
+ reject(e);
699
+ }
700
+ });
701
+ const addEvent = async (event) => {
702
+ if (hardStop) return;
703
+ if (isSessionActive && sessionStartTime) {
704
+ const totalSessionDuration = now() - sessionStartTime;
705
+ if (totalSessionDuration > config.sessionTimeout) {
706
+ log("Session expired, stopping and restarting with new session");
707
+ stopRecordingSession();
708
+ }
709
+ }
710
+ if (!isSessionActive) {
711
+ if (stopRecording) {
712
+ stopRecording();
713
+ stopRecording = null;
714
+ }
715
+ sessionId = config.sessionId || generateSessionId();
716
+ userId = config.userId || getOrCreateUserId();
717
+ const previousSession = loadSessionFromStorage();
718
+ if (previousSession?.externalId) {
719
+ externalId = previousSession.externalId || null;
720
+ }
721
+ organizationId = config.organizationId;
722
+ sessionStartTime = now();
723
+ await startRecording();
724
+ return;
725
+ }
726
+ eventBuffer.push(event);
727
+ updateSessionActivity();
728
+ if (eventBuffer.length >= config.batchSize) {
729
+ log("Buffer size reached, triggering flush");
730
+ flushEvents();
731
+ } else {
732
+ log("Buffer not full yet, continuing to collect events");
733
+ }
734
+ };
735
+ const generateInsights = async (hardSessionId = null) => {
736
+ if (!config.generateInsights) {
737
+ return;
738
+ }
739
+ sessionId = hardSessionId || sessionId;
740
+ const event = {
741
+ type: 9999,
742
+ timestamp: now(),
743
+ data: {}
744
+ };
745
+ const payload = {
746
+ session_id: sessionId,
747
+ user_id: userId,
748
+ organization_id: organizationId,
749
+ events: [event],
750
+ client_version: LIB_VERSION
751
+ };
752
+ if (externalId) {
753
+ payload.external_id = externalId;
754
+ }
755
+ const payloadString = JSON.stringify(payload);
756
+ const payloadSizeKB = new Blob([payloadString]).size / BYTES_IN_KILOBYTE;
757
+ try {
758
+ const endpointUrl = buildEndpointUrl(
759
+ config.endpoint,
760
+ "f",
761
+ false,
762
+ payloadSizeKB,
763
+ false
764
+ );
765
+ const response = await fetch(endpointUrl, {
766
+ method: "POST",
767
+ headers: { "Content-Type": "text/plain" },
768
+ body: payloadString,
769
+ mode: "cors",
770
+ credentials: "omit"
771
+ });
772
+ if (response.status === 0) {
773
+ log("Insights request aborted (status 0)");
774
+ } else if (!response.ok && response.status >= 500) {
775
+ error("Failed to send insights with fetch:", response.status);
776
+ }
777
+ } catch (err) {
778
+ const errorMessage = err?.message || "";
779
+ const isBlocked2 = errorMessage.includes("Failed to fetch") || errorMessage.includes("NetworkError") || errorMessage.includes("ERR_BLOCKED_BY_CLIENT") || errorMessage.includes("ERR_BLOCKED_BY_RESPONSE") || errorMessage.includes("net::ERR_FAILED");
780
+ const isAborted = err?.name === "AbortError" || err?.name === "TimeoutError" || errorMessage.includes("aborted") || errorMessage.includes("cancelled");
781
+ if (isBlocked2 || isAborted) {
782
+ return;
783
+ }
784
+ return;
785
+ }
786
+ };
787
+ const startRecording = async () => {
788
+ if (isSessionActive) {
789
+ log("Recording already active, skipping startRecording");
790
+ return;
791
+ }
792
+ isSessionActive = true;
793
+ try {
794
+ const [packFn, rrweb] = await Promise.all([
795
+ loadRrwebPacker(),
796
+ loadRrweb()
797
+ ]);
798
+ const filterBlacklistedAttrMutations = (event) => {
799
+ const data = getMutationData(event);
800
+ if (!data) return event;
801
+ if (!Array.isArray(data.attributes)) {
802
+ return event;
803
+ }
804
+ const prefixes = Array.isArray(config.blackList) ? config.blackList.filter(
805
+ (p) => typeof p === "string" && p.length > 0
806
+ ) : [];
807
+ if (prefixes.length === 0) {
808
+ return event;
809
+ }
810
+ const startsWithBlacklisted = (idValue) => {
811
+ if (typeof idValue !== "string") return false;
812
+ for (const prefix of prefixes) {
813
+ if (idValue.startsWith(prefix)) return true;
814
+ }
815
+ return false;
816
+ };
817
+ const nextAttributes = data.attributes.filter((attrChange) => {
818
+ const elemId = attrChange?.attributes?.id;
819
+ return !startsWithBlacklisted(elemId);
820
+ });
821
+ const baseEvent = event;
822
+ const nextData = { ...data, attributes: nextAttributes };
823
+ const hasAdds = Array.isArray(nextData.adds) && nextData.adds.length > 0;
824
+ const hasRemoves = Array.isArray(nextData.removes) && nextData.removes.length > 0;
825
+ const hasTexts = Array.isArray(nextData.texts) && nextData.texts.length > 0;
826
+ const hasAttributes = Array.isArray(nextData.attributes) && nextData.attributes.length > 0;
827
+ if (!hasAdds && !hasRemoves && !hasTexts && !hasAttributes) {
828
+ return null;
829
+ }
830
+ return {
831
+ ...baseEvent,
832
+ data: nextData
833
+ };
834
+ };
835
+ const filterMutations = (event) => {
836
+ const data = getMutationData(event);
837
+ if (data) {
838
+ const attrs = data.attributes;
839
+ if (Array.isArray(attrs) && attrs.length) {
840
+ const filtered = attrs.filter((a) => {
841
+ const at = isRecord(a?.attributes) ? a.attributes : {};
842
+ const keys = Object.keys(at);
843
+ const onlyTransformOpacity = keys.length > 0 && keys.every((k) => k === "transform" || k === "opacity");
844
+ if (!onlyTransformOpacity) return true;
845
+ const id = at.id;
846
+ if (isInteractiveId(id)) return true;
847
+ return false;
848
+ });
849
+ if (filtered.length === 0) return null;
850
+ data.attributes = filtered;
851
+ }
852
+ }
853
+ return event;
854
+ };
855
+ const isNoisySvgAdd = (add) => {
856
+ const n = add?.node;
857
+ if (!n) return false;
858
+ if (!n.isSVG) return false;
859
+ const tag = (n.tagName || "").toLowerCase();
860
+ if (tag === "path" && typeof (isRecord(n.attributes) ? n.attributes.d : void 0) === "string" && (isRecord(n.attributes) ? String(n.attributes.d).length : 0) > 200)
861
+ return true;
862
+ if (tag === "style") return true;
863
+ return false;
864
+ };
865
+ const isUselessTextAdd = (add) => {
866
+ const n = add?.node;
867
+ return n?.type === 3 && (n.textContent === "" || n.textContent == null);
868
+ };
869
+ const filterAdds = (event) => {
870
+ const data = getMutationData(event);
871
+ if (!data) return event;
872
+ if (Array.isArray(data.adds) && data.adds.length) {
873
+ data.adds = data.adds.filter(
874
+ (add) => !isNoisySvgAdd(add) && !isUselessTextAdd(add)
875
+ );
876
+ }
877
+ const hasAdds = Array.isArray(data.adds) && data.adds.length > 0;
878
+ const hasRemoves = Array.isArray(data.removes) && data.removes.length > 0;
879
+ const hasTexts = Array.isArray(data.texts) && data.texts.length > 0;
880
+ const hasAttributes = Array.isArray(data.attributes) && data.attributes.length > 0;
881
+ if (!hasAdds && !hasRemoves && !hasTexts && !hasAttributes) {
882
+ return null;
883
+ }
884
+ return event;
885
+ };
886
+ const NET_SAMPLE_MS = 1e4;
887
+ const IDLE_MS = 15e3;
888
+ const BLOCKED_NET_HOSTS = /* @__PURE__ */ new Set([
889
+ "ingest.infinitewatch.ai",
890
+ "ingest.humanbehavior.co"
891
+ ]);
892
+ let __iw_lastUserIntentAt = now();
893
+ let __iw_intentInstalled = false;
894
+ const __iw_lastNetSentAt = /* @__PURE__ */ new Map();
895
+ const iwInstallUserIntentListenersOnce = () => {
896
+ if (__iw_intentInstalled || !isBrowser()) return;
897
+ __iw_intentInstalled = true;
898
+ const mark = () => {
899
+ __iw_lastUserIntentAt = now();
900
+ };
901
+ [
902
+ "pointerdown",
903
+ "mousedown",
904
+ "touchstart",
905
+ "keydown",
906
+ "wheel",
907
+ "scroll",
908
+ "input"
909
+ ].forEach((t) => {
910
+ window.addEventListener(t, mark, { capture: true, passive: true });
911
+ });
912
+ };
913
+ const iwKeyForRequest = (name) => {
914
+ try {
915
+ const u = new URL(name);
916
+ return u.hostname + u.pathname;
917
+ } catch {
918
+ return String(name || "");
919
+ }
920
+ };
921
+ const iwIsBlockedHost = (name) => {
922
+ try {
923
+ const host = new URL(name).hostname;
924
+ return BLOCKED_NET_HOSTS.has(host);
925
+ } catch {
926
+ return false;
927
+ }
928
+ };
929
+ const filterNetworkNoise = (event) => {
930
+ if (!event || event.type !== 6 || !event.data?.plugin || event.data.plugin !== "rrweb/network@1") {
931
+ return event;
932
+ }
933
+ if (now() - __iw_lastUserIntentAt > IDLE_MS) {
934
+ return null;
935
+ }
936
+ const payload = event.data?.payload;
937
+ const reqs = payload?.requests;
938
+ if (!Array.isArray(reqs) || reqs.length === 0) return event;
939
+ const timestamp = event.timestamp || now();
940
+ const filteredReqs = [];
941
+ for (const r of reqs) {
942
+ const name = r?.name;
943
+ if (typeof name !== "string") continue;
944
+ if (iwIsBlockedHost(name)) continue;
945
+ const key = iwKeyForRequest(name);
946
+ const last = __iw_lastNetSentAt.get(key) || 0;
947
+ if (timestamp - last < NET_SAMPLE_MS) continue;
948
+ __iw_lastNetSentAt.set(key, timestamp);
949
+ filteredReqs.push(r);
950
+ }
951
+ if (filteredReqs.length === 0) return null;
952
+ return {
953
+ ...event,
954
+ data: {
955
+ ...event.data,
956
+ payload: {
957
+ ...payload,
958
+ requests: filteredReqs
959
+ }
960
+ }
961
+ };
962
+ };
963
+ const ANIM_ATTR_KEYS = /* @__PURE__ */ new Set(["d"]);
964
+ const ANIM_STYLE_KEYS = /* @__PURE__ */ new Set([
965
+ "transform",
966
+ "opacity",
967
+ "filter",
968
+ "backdrop-filter",
969
+ "will-change",
970
+ "translate",
971
+ "scale",
972
+ "rotate"
973
+ ]);
974
+ const isAnimationOnlyAttrChange = (attrChange) => {
975
+ const attrs = attrChange?.attributes;
976
+ if (!attrs || typeof attrs !== "object") return false;
977
+ const keys = Object.keys(attrs);
978
+ const firstKey = keys[0];
979
+ if (keys.length === 1 && firstKey && ANIM_ATTR_KEYS.has(firstKey))
980
+ return true;
981
+ if (keys.length === 1 && firstKey === "style") {
982
+ const style = attrs.style;
983
+ if (!style || typeof style !== "object") return false;
984
+ const styleKeys = Object.keys(style);
985
+ if (styleKeys.length === 0) return false;
986
+ return styleKeys.every((k) => ANIM_STYLE_KEYS.has(k));
987
+ }
988
+ return keys.every((k) => ANIM_STYLE_KEYS.has(k));
989
+ };
990
+ const isPureAnimationMutationEvent = (event) => {
991
+ const data = getMutationData(event);
992
+ if (!data) return false;
993
+ const hasAdds = Array.isArray(data.adds) && data.adds.length > 0;
994
+ const hasRemoves = Array.isArray(data.removes) && data.removes.length > 0;
995
+ const hasTexts = Array.isArray(data.texts) && data.texts.length > 0;
996
+ if (hasAdds || hasRemoves || hasTexts) return false;
997
+ const attrs = data.attributes;
998
+ if (!Array.isArray(attrs) || attrs.length === 0) return false;
999
+ return attrs.every(isAnimationOnlyAttrChange);
1000
+ };
1001
+ const IDLE_MUTATION_SAMPLE_MS = 1500;
1002
+ let __iw_lastKeptIdleAnimMutationAt = 0;
1003
+ const sampleAnimationMutationsWhenIdle = (event) => {
1004
+ if (!event || event.type !== 3 || event?.data?.source !== 0)
1005
+ return event;
1006
+ const idleFor = now() - __iw_lastUserIntentAt;
1007
+ if (idleFor <= IDLE_MS) return event;
1008
+ if (!isPureAnimationMutationEvent(event)) return event;
1009
+ const ts = event.timestamp || now();
1010
+ if (ts - __iw_lastKeptIdleAnimMutationAt < IDLE_MUTATION_SAMPLE_MS) {
1011
+ return null;
1012
+ }
1013
+ __iw_lastKeptIdleAnimMutationAt = ts;
1014
+ return event;
1015
+ };
1016
+ const dropIdleAnimationMutations = (event) => {
1017
+ const data = getMutationData(event);
1018
+ if (!data) return event;
1019
+ const idleFor = now() - __iw_lastUserIntentAt;
1020
+ if (idleFor > IDLE_MS && isPureAnimationMutationEvent(event))
1021
+ return null;
1022
+ return event;
1023
+ };
1024
+ const DROP_ATTR_KEYS = /* @__PURE__ */ new Set(["decoding", "data-hydrated"]);
1025
+ const filterLowValueAttributes = (event) => {
1026
+ const data = getMutationData(event);
1027
+ if (!data) return event;
1028
+ const attrs = data.attributes;
1029
+ if (!Array.isArray(attrs) || attrs.length === 0) return event;
1030
+ const next = [];
1031
+ for (const a of attrs) {
1032
+ const at = a?.attributes;
1033
+ if (!at || typeof at !== "object") {
1034
+ next.push(a);
1035
+ continue;
1036
+ }
1037
+ const newAttrs = { ...at };
1038
+ for (const k of Object.keys(newAttrs)) {
1039
+ if (DROP_ATTR_KEYS.has(k)) delete newAttrs[k];
1040
+ }
1041
+ if ("preserveAspectRatio" in newAttrs && String(newAttrs.preserveAspectRatio).toLowerCase() === "none") {
1042
+ delete newAttrs.preserveAspectRatio;
1043
+ }
1044
+ if ("width" in newAttrs && String(newAttrs.width) === "100%")
1045
+ delete newAttrs.width;
1046
+ if ("height" in newAttrs && String(newAttrs.height) === "100%")
1047
+ delete newAttrs.height;
1048
+ if (Object.keys(newAttrs).length === 0) continue;
1049
+ next.push({ ...a, attributes: newAttrs });
1050
+ }
1051
+ data.attributes = next;
1052
+ const empty = (!data.adds || data.adds.length === 0) && (!data.attributes || data.attributes.length === 0) && (!data.texts || data.texts.length === 0) && (!data.removes || data.removes.length === 0);
1053
+ return empty ? null : event;
1054
+ };
1055
+ const filterIdleEvents = (event) => {
1056
+ if (now() - __iw_lastUserIntentAt > IDLE_MS) {
1057
+ if (event?.type === 3 && event?.data?.source === 0) {
1058
+ const hasAdds = Array.isArray(event.data.adds) && event.data.adds.length > 0;
1059
+ const hasRemoves = Array.isArray(event.data.removes) && event.data.removes.length > 0;
1060
+ const hasTexts = Array.isArray(event.data.texts) && event.data.texts.length > 0;
1061
+ if (hasAdds || hasRemoves || hasTexts) {
1062
+ return event;
1063
+ }
1064
+ }
1065
+ return null;
1066
+ }
1067
+ return event;
1068
+ };
1069
+ const filterEvent = (event) => {
1070
+ let filtered = event;
1071
+ if (filtered?.type === 5) return filtered;
1072
+ filtered = filterIdleEvents(filtered);
1073
+ if (!filtered) return null;
1074
+ filtered = filterBlacklistedAttrMutations(filtered);
1075
+ if (!filtered) return null;
1076
+ filtered = filterMutations(filtered);
1077
+ if (!filtered) return null;
1078
+ filtered = filterAdds(filtered);
1079
+ if (!filtered) return null;
1080
+ filtered = filterNetworkNoise(filtered);
1081
+ if (!filtered) return null;
1082
+ filtered = sampleAnimationMutationsWhenIdle(filtered);
1083
+ if (!filtered) return null;
1084
+ filtered = dropIdleAnimationMutations(filtered);
1085
+ if (!filtered) return null;
1086
+ filtered = filterLowValueAttributes(filtered);
1087
+ if (!filtered) return null;
1088
+ return filtered;
1089
+ };
1090
+ const INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,label,[role="button"],[role="link"],[tabindex],[contenteditable="true"]';
1091
+ const isInteractiveEl = (el) => {
1092
+ if (!el || el.nodeType !== 1) return false;
1093
+ return !!(el.matches?.(INTERACTIVE_SELECTOR) || el.closest?.(INTERACTIVE_SELECTOR));
1094
+ };
1095
+ const onlyAnimationStyleChanged = (oldStyle = "", newStyle = "") => {
1096
+ const toMap = (s) => s.split(";").map((x) => x.trim()).filter(Boolean).map((x) => x.split(":").map((y) => y.trim())).reduce((acc, [k, v]) => {
1097
+ if (k) acc[k] = v ?? "";
1098
+ return acc;
1099
+ }, {});
1100
+ const oldMap = toMap(oldStyle);
1101
+ const newMap = toMap(newStyle);
1102
+ const changedKeys = /* @__PURE__ */ new Set();
1103
+ for (const k of /* @__PURE__ */ new Set([
1104
+ ...Object.keys(oldMap),
1105
+ ...Object.keys(newMap)
1106
+ ])) {
1107
+ if ((oldMap[k] ?? "") !== (newMap[k] ?? "")) changedKeys.add(k);
1108
+ }
1109
+ if (changedKeys.size === 0) return false;
1110
+ const ANIM_STYLE_PROPS = [
1111
+ "transform",
1112
+ "opacity",
1113
+ "will-change",
1114
+ "filter",
1115
+ "backdrop-filter",
1116
+ "translate",
1117
+ "scale",
1118
+ "rotate"
1119
+ ];
1120
+ return [...changedKeys].every(
1121
+ (k) => ANIM_STYLE_PROPS.some((p) => k.includes(p))
1122
+ );
1123
+ };
1124
+ const INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
1125
+ "a",
1126
+ "button",
1127
+ "input",
1128
+ "select",
1129
+ "textarea",
1130
+ "label"
1131
+ ]);
1132
+ const INTERACTIVE_ROLES = /* @__PURE__ */ new Set(["button", "link"]);
1133
+ let __iw_idIsInteractive = /* @__PURE__ */ new Map();
1134
+ const rebuildInteractiveIdMapFromFullSnapshot = (fsEvent) => {
1135
+ __iw_idIsInteractive = /* @__PURE__ */ new Map();
1136
+ const root = fsEvent?.data?.node;
1137
+ if (!root) return;
1138
+ const walk = (node, interactiveAncestor = false) => {
1139
+ if (!node || typeof node !== "object") return;
1140
+ if (node.type !== 2) {
1141
+ if (node.childNodes) {
1142
+ node.childNodes.forEach((ch) => {
1143
+ walk(ch, interactiveAncestor);
1144
+ });
1145
+ }
1146
+ return;
1147
+ }
1148
+ const tag = String(node.tagName || "").toLowerCase();
1149
+ const attrs = node.attributes || {};
1150
+ const id = attrs.id;
1151
+ const selfInteractive = INTERACTIVE_TAGS.has(tag) || typeof attrs.role === "string" && INTERACTIVE_ROLES.has(String(attrs.role).toLowerCase()) || attrs.tabindex != null || attrs.href != null || String(attrs.contenteditable || "").toLowerCase() === "true";
1152
+ const isInteractive = interactiveAncestor || selfInteractive;
1153
+ if (typeof id === "string") {
1154
+ __iw_idIsInteractive.set(id, isInteractive);
1155
+ }
1156
+ if (node.childNodes) {
1157
+ node.childNodes.forEach((ch) => {
1158
+ walk(ch, isInteractive);
1159
+ });
1160
+ }
1161
+ };
1162
+ walk(root, false);
1163
+ };
1164
+ const isInteractiveId = (id) => {
1165
+ if (typeof id !== "string") return false;
1166
+ return __iw_idIsInteractive.get(id) === true;
1167
+ };
1168
+ const emitUrlChangeEvent = () => {
1169
+ const win = getWindow();
1170
+ if (!win) return;
1171
+ const url = win.location.href;
1172
+ if (url === lastRecordedUrl) return;
1173
+ lastRecordedUrl = url;
1174
+ const customEvent = {
1175
+ type: 5,
1176
+ data: {
1177
+ tag: "url-change",
1178
+ url,
1179
+ pathname: win.location.pathname,
1180
+ search: win.location.search,
1181
+ hash: win.location.hash
1182
+ },
1183
+ timestamp: Date.now(),
1184
+ w_v: LIB_VERSION
1185
+ };
1186
+ void addEvent(customEvent);
1187
+ };
1188
+ const installUrlChangeListeners = () => {
1189
+ const win = getWindow();
1190
+ if (!win) return;
1191
+ const originalPushState = win.history.pushState.bind(win.history);
1192
+ const originalReplaceState = win.history.replaceState.bind(win.history);
1193
+ win.history.pushState = (...args) => {
1194
+ originalPushState(...args);
1195
+ emitUrlChangeEvent();
1196
+ };
1197
+ win.history.replaceState = (...args) => {
1198
+ originalReplaceState(...args);
1199
+ emitUrlChangeEvent();
1200
+ };
1201
+ const popstateHandler = () => emitUrlChangeEvent();
1202
+ win.addEventListener("popstate", popstateHandler);
1203
+ urlChangeCleanup = () => {
1204
+ win.history.pushState = originalPushState;
1205
+ win.history.replaceState = originalReplaceState;
1206
+ win.removeEventListener("popstate", popstateHandler);
1207
+ };
1208
+ };
1209
+ const recordOptions = {
1210
+ emit: (event) => {
1211
+ if (event?.type === 2) {
1212
+ rebuildInteractiveIdMapFromFullSnapshot(event);
1213
+ }
1214
+ const filtered = filterEvent(event);
1215
+ if (filtered) {
1216
+ try {
1217
+ const win = getWindow();
1218
+ const iwConfig = win ? win.__IW_CONFIG__ : void 0;
1219
+ if (typeof iwConfig?.onEvent === "function") {
1220
+ iwConfig.onEvent(filtered);
1221
+ }
1222
+ } catch {
1223
+ }
1224
+ filtered.w_v = LIB_VERSION;
1225
+ const eventToAdd = config.debug ? filtered : packFn(filtered);
1226
+ void addEvent(eventToAdd);
1227
+ if (filtered.type === 2 && !lastRecordedUrl) {
1228
+ const win = getWindow();
1229
+ const url = win?.location?.href ?? "";
1230
+ lastRecordedUrl = url;
1231
+ const sessionStartedEvent = {
1232
+ type: 5,
1233
+ data: {
1234
+ tag: "session-started",
1235
+ url,
1236
+ pathname: win?.location?.pathname ?? "",
1237
+ search: win?.location?.search ?? "",
1238
+ hash: win?.location?.hash ?? ""
1239
+ },
1240
+ timestamp: Date.now(),
1241
+ w_v: LIB_VERSION
1242
+ };
1243
+ void addEvent(sessionStartedEvent);
1244
+ }
1245
+ }
1246
+ },
1247
+ maskAllInputs: config.maskAllInputs,
1248
+ maskInputOptions: config.maskInputOptions,
1249
+ recordCanvas: config.recordCanvas,
1250
+ recordIframe: config.recordIframe,
1251
+ recordCrossOriginIframes: config.recordCrossOriginIframes,
1252
+ collectFonts: false,
1253
+ inlineStylesheet: config.inlineStylesheet,
1254
+ slimDOM: true,
1255
+ slimDOMOptions: {
1256
+ script: true,
1257
+ comment: true,
1258
+ headFavicon: true,
1259
+ headMETA: true,
1260
+ headLink: true,
1261
+ headScript: true,
1262
+ headStyle: true,
1263
+ iframe: true,
1264
+ svg: true
1265
+ },
1266
+ sampling: {
1267
+ mousemove: 300,
1268
+ scroll: 300,
1269
+ media: 3e3,
1270
+ input: "last"
1271
+ },
1272
+ ignoreMutation: (m) => {
1273
+ const t = m.target || null;
1274
+ if (!t || t.nodeType !== 1) return false;
1275
+ if (m.type === "attributes" && m.attributeName === "style") {
1276
+ const oldStyle = m.oldValue ?? "";
1277
+ const newStyle = t.getAttribute("style") ?? "";
1278
+ if (onlyAnimationStyleChanged(oldStyle, newStyle) && !isInteractiveEl(t)) {
1279
+ return true;
1280
+ }
1281
+ }
1282
+ if (m.type === "childList" && m.addedNodes.length === 0 && m.removedNodes.length === 0) {
1283
+ return true;
1284
+ }
1285
+ return false;
1286
+ }
1287
+ };
1288
+ if (config.blockClass) {
1289
+ recordOptions.blockClass = config.blockClass;
1290
+ }
1291
+ if (config.blockSelector) {
1292
+ recordOptions.blockSelector = config.blockSelector;
1293
+ }
1294
+ if (config.maskTextSelector) {
1295
+ recordOptions.maskTextSelector = config.maskTextSelector;
1296
+ }
1297
+ if (config.maskTextClass) {
1298
+ recordOptions.maskTextClass = config.maskTextClass;
1299
+ }
1300
+ iwInstallUserIntentListenersOnce();
1301
+ stopRecording = rrweb.record(recordOptions);
1302
+ installUrlChangeListeners();
1303
+ initSessionRecording();
1304
+ } catch (err) {
1305
+ error("Failed to start recording:", err);
1306
+ }
1307
+ };
1308
+ const initSessionRecording = () => {
1309
+ startSessionHeartbeat();
1310
+ startSessionExpiryTimer();
1311
+ saveSessionToStorage();
1312
+ startFlushTimer();
1313
+ };
1314
+ const stopRecordingSession = () => {
1315
+ stopSessionHeartbeat();
1316
+ stopSessionExpiryTimer();
1317
+ stopFlushTimer();
1318
+ flushEvents();
1319
+ clearSessionFromStorage();
1320
+ if (urlChangeCleanup) {
1321
+ urlChangeCleanup();
1322
+ urlChangeCleanup = null;
1323
+ }
1324
+ lastRecordedUrl = "";
1325
+ sessionId = null;
1326
+ sessionStartTime = null;
1327
+ isSessionActive = false;
1328
+ };
1329
+ const init = async (options) => {
1330
+ if (!isBrowser()) {
1331
+ return null;
1332
+ }
1333
+ if (isInitialized) {
1334
+ log("Tracker already initialized");
1335
+ return client;
1336
+ }
1337
+ if (!options.organizationId) {
1338
+ console.error(
1339
+ "[InfiniteWatch]",
1340
+ "organizationId is required. Provide it as a prop/parameter or via an environment variable (e.g. NEXT_PUBLIC_INFINITEWATCH_ORG_ID or VITE_INFINITEWATCH_ORG_ID)."
1341
+ );
1342
+ return null;
1343
+ }
1344
+ if (isOrganizationExcluded(options.organizationId)) {
1345
+ log("Organization excluded from tracking:", options.organizationId);
1346
+ return null;
1347
+ }
1348
+ const initialConfig = { ...DEFAULT_CONFIG, ...options };
1349
+ let recordingConfig = {};
1350
+ if (initialConfig.endpointConfig && initialConfig.organizationId) {
1351
+ try {
1352
+ const response = await fetch(initialConfig.endpointConfig, {
1353
+ method: "GET",
1354
+ headers: {
1355
+ "X-Organization": initialConfig.organizationId
1356
+ },
1357
+ credentials: "omit",
1358
+ keepalive: false
1359
+ });
1360
+ if (response.status === 403) {
1361
+ const errorData = await response.json();
1362
+ contractStatus = {
1363
+ error: errorData.error || "No active contract",
1364
+ message: errorData.message || "Your contract has expired",
1365
+ contract_expired: errorData.contract_expired || true
1366
+ };
1367
+ log(
1368
+ "Contract expired or no active contract:",
1369
+ contractStatus.message
1370
+ );
1371
+ return null;
1372
+ } else if (response.status === 429) {
1373
+ const errorData = await response.json();
1374
+ quotaStatus = {
1375
+ error: errorData.error || "Quota exceeded",
1376
+ message: errorData.message || "You have reached your usage limit",
1377
+ usage: errorData.usage || null,
1378
+ upgrade_url: errorData.upgrade_url || null
1379
+ };
1380
+ log("Quota exceeded:", quotaStatus.message);
1381
+ return null;
1382
+ } else if (response.ok) {
1383
+ const configData = await response.json();
1384
+ const booleanFromConfig = (value, fallback) => {
1385
+ if (value == null) return fallback;
1386
+ if (value === "true") return true;
1387
+ if (value === "false") return false;
1388
+ return Boolean(value);
1389
+ };
1390
+ recordingConfig = {
1391
+ defaultSamplingPercent: configData.samplingPercent ?? initialConfig.defaultSamplingPercent,
1392
+ autoResumeSession: booleanFromConfig(
1393
+ configData.autoResumeSession,
1394
+ initialConfig.autoResumeSession
1395
+ ),
1396
+ batchSize: configData.batchSize ?? initialConfig.batchSize,
1397
+ flushInterval: configData.flushInterval ?? initialConfig.flushInterval,
1398
+ sessionTimeout: configData.sessionTimeout ?? initialConfig.sessionTimeout,
1399
+ sessionHeartbeatInterval: configData.sessionHeartbeatInterval ?? initialConfig.sessionHeartbeatInterval,
1400
+ inlineStylesheet: booleanFromConfig(
1401
+ configData.inlineStylesheet,
1402
+ initialConfig.inlineStylesheet
1403
+ ),
1404
+ generateInsights: booleanFromConfig(
1405
+ configData.generateInsights,
1406
+ initialConfig.generateInsights
1407
+ )
1408
+ };
1409
+ options.defaultSamplingPercent = recordingConfig.defaultSamplingPercent;
1410
+ options.autoResumeSession = recordingConfig.autoResumeSession;
1411
+ options.batchSize = recordingConfig.batchSize;
1412
+ options.flushInterval = recordingConfig.flushInterval;
1413
+ options.sessionTimeout = recordingConfig.sessionTimeout;
1414
+ options.sessionHeartbeatInterval = recordingConfig.sessionHeartbeatInterval;
1415
+ options.inlineStylesheet = recordingConfig.inlineStylesheet;
1416
+ options.generateInsights = recordingConfig.generateInsights;
1417
+ options.blackList = recordingConfig.blackList;
1418
+ switch (configData.masking) {
1419
+ case "All":
1420
+ options.maskTextSelector = "body";
1421
+ break;
1422
+ case "OnlyPasswords":
1423
+ options.maskAllInputs = false;
1424
+ options.maskInputOptions = {
1425
+ password: true,
1426
+ email: false,
1427
+ text: false,
1428
+ tel: false,
1429
+ url: false,
1430
+ textarea: false,
1431
+ select: false,
1432
+ checkbox: false,
1433
+ radio: false,
1434
+ file: false
1435
+ };
1436
+ break;
1437
+ }
1438
+ } else {
1439
+ log("Config endpoint returned error:", response.status);
1440
+ }
1441
+ } catch (err) {
1442
+ log("Failed to fetch endpoint config, using defaults:", err);
1443
+ }
1444
+ }
1445
+ config = { ...DEFAULT_CONFIG, ...options };
1446
+ if (config.autoResumeSession) {
1447
+ const existingSession = loadSessionFromStorage();
1448
+ if (existingSession && !isSessionExpired(existingSession)) {
1449
+ sessionId = existingSession.sessionId;
1450
+ userId = existingSession.userId;
1451
+ externalId = existingSession.externalId || null;
1452
+ organizationId = existingSession.organizationId || config.organizationId;
1453
+ sessionStartTime = existingSession.sessionStartTime || now();
1454
+ isSampledIn = true;
1455
+ } else {
1456
+ sessionId = config.sessionId || generateSessionId();
1457
+ userId = config.userId || getOrCreateUserId();
1458
+ const previousSession = loadSessionFromStorage();
1459
+ if (previousSession?.externalId) {
1460
+ externalId = previousSession.externalId || null;
1461
+ }
1462
+ organizationId = config.organizationId;
1463
+ sessionStartTime = now();
1464
+ isSampledIn = shouldSample(organizationId, sessionId);
1465
+ }
1466
+ } else {
1467
+ sessionId = config.sessionId || generateSessionId();
1468
+ userId = config.userId || getOrCreateUserId();
1469
+ const previousSession = loadSessionFromStorage();
1470
+ if (previousSession?.externalId) {
1471
+ externalId = previousSession.externalId || null;
1472
+ }
1473
+ organizationId = config.organizationId;
1474
+ sessionStartTime = now();
1475
+ isSampledIn = shouldSample(organizationId, sessionId);
1476
+ }
1477
+ if (isSampledIn) {
1478
+ startSessionExpiryTimer();
1479
+ } else {
1480
+ log("Session not sampled in; not starting expiry timer");
1481
+ }
1482
+ isInitialized = true;
1483
+ window.addEventListener("beforeunload", () => {
1484
+ flushEvents();
1485
+ updateSessionActivity();
1486
+ void generateInsights().catch(() => {
1487
+ });
1488
+ });
1489
+ document.addEventListener("visibilitychange", () => {
1490
+ if (document.visibilityState === "visible") {
1491
+ if (isSessionActive && sessionStartTime) {
1492
+ const totalSessionDuration = now() - sessionStartTime;
1493
+ if (totalSessionDuration > config.sessionTimeout) {
1494
+ log("Session expired while tab was backgrounded, stopping session");
1495
+ stopRecordingSession();
1496
+ return;
1497
+ }
1498
+ }
1499
+ updateSessionActivity();
1500
+ } else if (document.visibilityState === "hidden") {
1501
+ void generateInsights().catch(() => {
1502
+ });
1503
+ }
1504
+ });
1505
+ return client;
1506
+ };
1507
+ const start = async () => {
1508
+ if (!isInitialized) {
1509
+ error("Tracker not initialized. Call init() first.");
1510
+ return client;
1511
+ }
1512
+ if (contractStatus) {
1513
+ log("Recording blocked - Contract issue:", contractStatus.message);
1514
+ return client;
1515
+ }
1516
+ if (quotaStatus) {
1517
+ log("Recording blocked - Quota exceeded:", quotaStatus.message);
1518
+ return client;
1519
+ }
1520
+ if (config.defaultSamplingPercent === 0) {
1521
+ log("Recording blocked - samplingPercent set to 0");
1522
+ return client;
1523
+ }
1524
+ if (!isSampledIn) {
1525
+ return client;
1526
+ }
1527
+ try {
1528
+ await startRecording();
1529
+ } catch (err) {
1530
+ error("Failed to start recording:", err);
1531
+ }
1532
+ return client;
1533
+ };
1534
+ const stop = () => {
1535
+ stopRecordingSession();
1536
+ return client;
1537
+ };
1538
+ const hardStopSession = () => {
1539
+ hardStop = true;
1540
+ stopRecordingSession();
1541
+ return client;
1542
+ };
1543
+ const flush = () => {
1544
+ flushEvents();
1545
+ return client;
1546
+ };
1547
+ const identify = (userIdentifier) => {
1548
+ if (!userIdentifier || !userIdentifier.external_id) {
1549
+ error("InfiniteWatch.identify: external_id is required");
1550
+ return client;
1551
+ }
1552
+ if (!sessionId || !userId) {
1553
+ error(
1554
+ "InfiniteWatch.identify: No active session. Call init() and start() first."
1555
+ );
1556
+ return client;
1557
+ }
1558
+ if (!organizationId) {
1559
+ organizationId = config.organizationId;
1560
+ }
1561
+ if (!organizationId) {
1562
+ error(
1563
+ "InfiniteWatch.identify: No organization ID set. Cannot identify user."
1564
+ );
1565
+ return client;
1566
+ }
1567
+ const doc = getDocument();
1568
+ const win = getWindow();
1569
+ if (!win) {
1570
+ return client;
1571
+ }
1572
+ const nav = win.navigator ?? (typeof navigator !== "undefined" ? navigator : void 0);
1573
+ const userLanguage = nav && "userLanguage" in nav ? nav.userLanguage : void 0;
1574
+ const browserMetadata = {
1575
+ user_agent: nav?.userAgent,
1576
+ language: nav?.language || userLanguage,
1577
+ languages: nav?.languages ? Array.from(nav.languages) : [],
1578
+ platform: nav?.platform,
1579
+ screen_width: win.screen?.width,
1580
+ screen_height: win.screen?.height,
1581
+ viewport_width: win.innerWidth,
1582
+ viewport_height: win.innerHeight,
1583
+ color_depth: win.screen?.colorDepth,
1584
+ pixel_ratio: win.devicePixelRatio || 1,
1585
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
1586
+ timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
1587
+ url: win.location?.href,
1588
+ pathname: win.location?.pathname,
1589
+ referrer: doc?.referrer || null,
1590
+ online: nav?.onLine,
1591
+ cookie_enabled: nav?.cookieEnabled,
1592
+ do_not_track: nav?.doNotTrack || null,
1593
+ touch_support: "ontouchstart" in win || (nav?.maxTouchPoints ?? 0) > 0
1594
+ };
1595
+ const requestBody = {
1596
+ external_id: userIdentifier.external_id,
1597
+ session_id: sessionId,
1598
+ user_id: userId,
1599
+ organization_id: organizationId,
1600
+ client_version: LIB_VERSION,
1601
+ client_metadata: browserMetadata
1602
+ };
1603
+ for (const key in userIdentifier) {
1604
+ if (key !== "external_id" && Object.hasOwn(userIdentifier, key)) {
1605
+ requestBody[key] = userIdentifier[key];
1606
+ }
1607
+ }
1608
+ const payloadString = JSON.stringify(requestBody);
1609
+ const payloadSizeKB = new Blob([payloadString]).size / BYTES_IN_KILOBYTE;
1610
+ const endpointUrl = buildEndpointUrl(
1611
+ `${config.baseUrl}/v1/users`,
1612
+ "f",
1613
+ true,
1614
+ payloadSizeKB,
1615
+ false,
1616
+ sessionId
1617
+ );
1618
+ fetch(endpointUrl, {
1619
+ method: "POST",
1620
+ headers: {
1621
+ "Content-Type": "text/plain"
1622
+ },
1623
+ body: payloadString,
1624
+ keepalive: true
1625
+ }).then((response) => {
1626
+ if (response.status === 0) {
1627
+ log("Identify request aborted (status 0)");
1628
+ } else if (response.ok) {
1629
+ externalId = userIdentifier.external_id;
1630
+ saveSessionToStorage();
1631
+ log("User identified successfully:", userIdentifier.external_id);
1632
+ } else if (response.status >= 500) {
1633
+ error("Failed to identify user:", response.status);
1634
+ }
1635
+ }).catch((err) => {
1636
+ const errorMessage = err?.message || "";
1637
+ const isBlocked2 = errorMessage.includes("Failed to fetch") || errorMessage.includes("NetworkError") || errorMessage.includes("ERR_BLOCKED_BY_CLIENT") || errorMessage.includes("ERR_BLOCKED_BY_RESPONSE") || errorMessage.includes("net::ERR_FAILED");
1638
+ const isAborted = err?.name === "AbortError" || err?.name === "TimeoutError" || errorMessage.includes("aborted") || errorMessage.includes("cancelled");
1639
+ if (!isBlocked2 && !isAborted) {
1640
+ error("InfiniteWatch identify failed:", err);
1641
+ }
1642
+ });
1643
+ return client;
1644
+ };
1645
+ const getSessionInfo = () => {
1646
+ return {
1647
+ sessionId,
1648
+ userId,
1649
+ organizationId,
1650
+ sessionStartTime,
1651
+ isActive: isSessionActive,
1652
+ hasStoredSession: !!loadSessionFromStorage()
1653
+ };
1654
+ };
1655
+ const isBlocked = () => {
1656
+ return contractStatus !== null || quotaStatus !== null || config.defaultSamplingPercent === 0;
1657
+ };
1658
+ const getContractStatus = () => contractStatus;
1659
+ const getQuotaStatus = () => quotaStatus;
1660
+ const client = {
1661
+ init,
1662
+ start,
1663
+ stop,
1664
+ hardStop: hardStopSession,
1665
+ flush,
1666
+ identify,
1667
+ getSessionInfo,
1668
+ isBlocked,
1669
+ getContractStatus,
1670
+ getQuotaStatus
1671
+ };
1672
+ return client;
1673
+ };
1674
+
1675
+ // src/index.ts
1676
+ var webCore = createWebCore();
1677
+ export {
1678
+ createWebCore,
1679
+ webCore
1680
+ };
1681
+ //# sourceMappingURL=index.js.map