@sessionvision/core 0.2.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.
Files changed (104) hide show
  1. package/README.md +361 -0
  2. package/dist/react/capture/autocapture.d.ts +45 -0
  3. package/dist/react/capture/event.d.ts +29 -0
  4. package/dist/react/capture/pageview.d.ts +27 -0
  5. package/dist/react/capture/properties.d.ts +25 -0
  6. package/dist/react/core/config.d.ts +21 -0
  7. package/dist/react/core/init.d.ts +53 -0
  8. package/dist/react/core/queue.d.ts +22 -0
  9. package/dist/react/identity/anonymous.d.ts +30 -0
  10. package/dist/react/identity/identify.d.ts +46 -0
  11. package/dist/react/identity/session.d.ts +49 -0
  12. package/dist/react/index.cjs.js +48 -0
  13. package/dist/react/index.cjs.js.map +1 -0
  14. package/dist/react/index.d.ts +13 -0
  15. package/dist/react/index.esm.js +42 -0
  16. package/dist/react/index.esm.js.map +1 -0
  17. package/dist/react/react/SessionVisionProvider.d.ts +8 -0
  18. package/dist/react/react/context.d.ts +3 -0
  19. package/dist/react/react/hooks.d.ts +3 -0
  20. package/dist/react/react/index.d.ts +4 -0
  21. package/dist/react/stub.d.ts +9 -0
  22. package/dist/react/transport/buffer.d.ts +50 -0
  23. package/dist/react/transport/compress.d.ts +22 -0
  24. package/dist/react/transport/send.d.ts +30 -0
  25. package/dist/react/types.d.ts +228 -0
  26. package/dist/react/utils/dom.d.ts +51 -0
  27. package/dist/react/utils/pii.d.ts +26 -0
  28. package/dist/react/utils/selector.d.ts +12 -0
  29. package/dist/react/utils/storage.d.ts +44 -0
  30. package/dist/react/utils/uuid.d.ts +13 -0
  31. package/dist/react/vue/composables.d.ts +4 -0
  32. package/dist/react/vue/index.d.ts +2 -0
  33. package/dist/react/vue/plugin.d.ts +4 -0
  34. package/dist/sessionvision.cjs.js +1903 -0
  35. package/dist/sessionvision.cjs.js.map +1 -0
  36. package/dist/sessionvision.esm.js +1901 -0
  37. package/dist/sessionvision.esm.js.map +1 -0
  38. package/dist/sessionvision.js +1909 -0
  39. package/dist/sessionvision.js.map +1 -0
  40. package/dist/sessionvision.min.js +7 -0
  41. package/dist/sessionvision.min.js.map +1 -0
  42. package/dist/stub-template.ts +41 -0
  43. package/dist/stub.min.js +1 -0
  44. package/dist/types/capture/autocapture.d.ts +45 -0
  45. package/dist/types/capture/event.d.ts +29 -0
  46. package/dist/types/capture/pageview.d.ts +27 -0
  47. package/dist/types/capture/properties.d.ts +25 -0
  48. package/dist/types/core/config.d.ts +21 -0
  49. package/dist/types/core/init.d.ts +53 -0
  50. package/dist/types/core/queue.d.ts +22 -0
  51. package/dist/types/identity/anonymous.d.ts +30 -0
  52. package/dist/types/identity/identify.d.ts +46 -0
  53. package/dist/types/identity/session.d.ts +49 -0
  54. package/dist/types/index.d.ts +13 -0
  55. package/dist/types/react/SessionVisionProvider.d.ts +8 -0
  56. package/dist/types/react/context.d.ts +3 -0
  57. package/dist/types/react/hooks.d.ts +3 -0
  58. package/dist/types/react/index.d.ts +4 -0
  59. package/dist/types/stub.d.ts +9 -0
  60. package/dist/types/transport/buffer.d.ts +50 -0
  61. package/dist/types/transport/compress.d.ts +22 -0
  62. package/dist/types/transport/send.d.ts +30 -0
  63. package/dist/types/types.d.ts +228 -0
  64. package/dist/types/utils/dom.d.ts +51 -0
  65. package/dist/types/utils/pii.d.ts +26 -0
  66. package/dist/types/utils/selector.d.ts +12 -0
  67. package/dist/types/utils/storage.d.ts +44 -0
  68. package/dist/types/utils/uuid.d.ts +13 -0
  69. package/dist/types/vue/composables.d.ts +4 -0
  70. package/dist/types/vue/index.d.ts +2 -0
  71. package/dist/types/vue/plugin.d.ts +4 -0
  72. package/dist/vue/capture/autocapture.d.ts +45 -0
  73. package/dist/vue/capture/event.d.ts +29 -0
  74. package/dist/vue/capture/pageview.d.ts +27 -0
  75. package/dist/vue/capture/properties.d.ts +25 -0
  76. package/dist/vue/core/config.d.ts +21 -0
  77. package/dist/vue/core/init.d.ts +53 -0
  78. package/dist/vue/core/queue.d.ts +22 -0
  79. package/dist/vue/identity/anonymous.d.ts +30 -0
  80. package/dist/vue/identity/identify.d.ts +46 -0
  81. package/dist/vue/identity/session.d.ts +49 -0
  82. package/dist/vue/index.cjs.js +43 -0
  83. package/dist/vue/index.cjs.js.map +1 -0
  84. package/dist/vue/index.d.ts +13 -0
  85. package/dist/vue/index.esm.js +38 -0
  86. package/dist/vue/index.esm.js.map +1 -0
  87. package/dist/vue/react/SessionVisionProvider.d.ts +8 -0
  88. package/dist/vue/react/context.d.ts +3 -0
  89. package/dist/vue/react/hooks.d.ts +3 -0
  90. package/dist/vue/react/index.d.ts +4 -0
  91. package/dist/vue/stub.d.ts +9 -0
  92. package/dist/vue/transport/buffer.d.ts +50 -0
  93. package/dist/vue/transport/compress.d.ts +22 -0
  94. package/dist/vue/transport/send.d.ts +30 -0
  95. package/dist/vue/types.d.ts +228 -0
  96. package/dist/vue/utils/dom.d.ts +51 -0
  97. package/dist/vue/utils/pii.d.ts +26 -0
  98. package/dist/vue/utils/selector.d.ts +12 -0
  99. package/dist/vue/utils/storage.d.ts +44 -0
  100. package/dist/vue/utils/uuid.d.ts +13 -0
  101. package/dist/vue/vue/composables.d.ts +4 -0
  102. package/dist/vue/vue/index.d.ts +2 -0
  103. package/dist/vue/vue/plugin.d.ts +4 -0
  104. package/package.json +109 -0
@@ -0,0 +1,1909 @@
1
+ /*!
2
+ * Session Vision Core v0.2.0
3
+ * (c) 2026 Session Vision
4
+ * Released under the MIT License
5
+ */
6
+ (function (global, factory) {
7
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
+ typeof define === 'function' && define.amd ? define(factory) :
9
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.sessionvision = factory());
10
+ })(this, (function () { 'use strict';
11
+
12
+ /**
13
+ * Session Vision Snippet Type Definitions
14
+ */
15
+ /**
16
+ * Storage key constants
17
+ */
18
+ const STORAGE_KEYS = {
19
+ ANONYMOUS_ID: 'sessionvision_anonymous_id',
20
+ USER_ID: 'sessionvision_user_id',
21
+ SESSION: 'sessionvision_session',
22
+ REGISTERED_PROPS: 'sessionvision_registered',
23
+ REGISTERED_ONCE_PROPS: 'sessionvision_registered_once',
24
+ CONFIG_CACHE: 'sessionvision_config',
25
+ };
26
+ /**
27
+ * Default configuration values
28
+ */
29
+ const DEFAULT_CONFIG = {
30
+ apiHost: 'https://cdn.sessionvision.com',
31
+ ingestHost: 'https://app.sessionvision.com',
32
+ version: 'latest',
33
+ debug: false,
34
+ optOut: false,
35
+ maskAllInputs: true,
36
+ autocapture: {
37
+ pageview: true,
38
+ clicks: true,
39
+ formSubmit: true,
40
+ },
41
+ };
42
+ /**
43
+ * Session timeout in milliseconds (30 minutes)
44
+ */
45
+ const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
46
+ /**
47
+ * Event buffer configuration
48
+ */
49
+ const BUFFER_CONFIG = {
50
+ MAX_EVENTS: 10,
51
+ FLUSH_INTERVAL_MS: 5000,
52
+ MAX_RETRIES: 3,
53
+ RETRY_DELAYS_MS: [1000, 2000, 4000],
54
+ };
55
+ /**
56
+ * Config cache TTL in milliseconds (1 hour)
57
+ */
58
+ const CONFIG_CACHE_TTL_MS = 60 * 60 * 1000;
59
+
60
+ /**
61
+ * Storage utility for localStorage and sessionStorage operations
62
+ * Provides safe access with error handling for environments where storage is unavailable
63
+ */
64
+ /**
65
+ * Check if localStorage is available
66
+ */
67
+ function isLocalStorageAvailable() {
68
+ try {
69
+ const testKey = '__sessionvision_test__';
70
+ window.localStorage.setItem(testKey, testKey);
71
+ window.localStorage.removeItem(testKey);
72
+ return true;
73
+ }
74
+ catch {
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Check if sessionStorage is available
80
+ */
81
+ function isSessionStorageAvailable() {
82
+ try {
83
+ const testKey = '__sessionvision_test__';
84
+ window.sessionStorage.setItem(testKey, testKey);
85
+ window.sessionStorage.removeItem(testKey);
86
+ return true;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ /**
93
+ * Get a value from localStorage
94
+ */
95
+ function getLocalStorage(key) {
96
+ if (!isLocalStorageAvailable()) {
97
+ return null;
98
+ }
99
+ try {
100
+ const value = window.localStorage.getItem(key);
101
+ if (value === null) {
102
+ return null;
103
+ }
104
+ return JSON.parse(value);
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /**
111
+ * Set a value in localStorage
112
+ */
113
+ function setLocalStorage(key, value) {
114
+ if (!isLocalStorageAvailable()) {
115
+ return false;
116
+ }
117
+ try {
118
+ window.localStorage.setItem(key, JSON.stringify(value));
119
+ return true;
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ }
125
+ /**
126
+ * Remove a value from localStorage
127
+ */
128
+ function removeLocalStorage(key) {
129
+ if (!isLocalStorageAvailable()) {
130
+ return false;
131
+ }
132
+ try {
133
+ window.localStorage.removeItem(key);
134
+ return true;
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
140
+ /**
141
+ * Get a value from sessionStorage
142
+ */
143
+ function getSessionStorage(key) {
144
+ if (!isSessionStorageAvailable()) {
145
+ return null;
146
+ }
147
+ try {
148
+ const value = window.sessionStorage.getItem(key);
149
+ if (value === null) {
150
+ return null;
151
+ }
152
+ return JSON.parse(value);
153
+ }
154
+ catch {
155
+ return null;
156
+ }
157
+ }
158
+ /**
159
+ * Set a value in sessionStorage
160
+ */
161
+ function setSessionStorage(key, value) {
162
+ if (!isSessionStorageAvailable()) {
163
+ return false;
164
+ }
165
+ try {
166
+ window.sessionStorage.setItem(key, JSON.stringify(value));
167
+ return true;
168
+ }
169
+ catch {
170
+ return false;
171
+ }
172
+ }
173
+ /**
174
+ * Remove a value from sessionStorage
175
+ */
176
+ function removeSessionStorage(key) {
177
+ if (!isSessionStorageAvailable()) {
178
+ return false;
179
+ }
180
+ try {
181
+ window.sessionStorage.removeItem(key);
182
+ return true;
183
+ }
184
+ catch {
185
+ return false;
186
+ }
187
+ }
188
+ /**
189
+ * Get a raw string value from localStorage (without JSON parsing)
190
+ */
191
+ function getLocalStorageRaw(key) {
192
+ if (!isLocalStorageAvailable()) {
193
+ return null;
194
+ }
195
+ try {
196
+ return window.localStorage.getItem(key);
197
+ }
198
+ catch {
199
+ return null;
200
+ }
201
+ }
202
+ /**
203
+ * Set a raw string value in localStorage (without JSON stringifying)
204
+ */
205
+ function setLocalStorageRaw(key, value) {
206
+ if (!isLocalStorageAvailable()) {
207
+ return false;
208
+ }
209
+ try {
210
+ window.localStorage.setItem(key, value);
211
+ return true;
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * UUID v4 generation utility
220
+ * Generates cryptographically random UUIDs when available, falls back to Math.random
221
+ */
222
+ /**
223
+ * Generate a UUID v4 string
224
+ * Uses crypto.randomUUID() when available, falls back to manual generation
225
+ */
226
+ function generateUUID() {
227
+ // Use native crypto.randomUUID if available (modern browsers)
228
+ if (typeof crypto !== 'undefined' &&
229
+ typeof crypto.randomUUID === 'function') {
230
+ return crypto.randomUUID();
231
+ }
232
+ // Fallback using crypto.getRandomValues
233
+ if (typeof crypto !== 'undefined' &&
234
+ typeof crypto.getRandomValues === 'function') {
235
+ const bytes = new Uint8Array(16);
236
+ crypto.getRandomValues(bytes);
237
+ // Set version (4) and variant (RFC 4122)
238
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
239
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
240
+ return formatUUID(bytes);
241
+ }
242
+ // Final fallback using Math.random (less secure but works everywhere)
243
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
244
+ const r = (Math.random() * 16) | 0;
245
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
246
+ return v.toString(16);
247
+ });
248
+ }
249
+ /**
250
+ * Format a Uint8Array as a UUID string
251
+ */
252
+ function formatUUID(bytes) {
253
+ const hex = [];
254
+ for (let i = 0; i < bytes.length; i++) {
255
+ hex.push(bytes[i].toString(16).padStart(2, '0'));
256
+ }
257
+ return [
258
+ hex.slice(0, 4).join(''),
259
+ hex.slice(4, 6).join(''),
260
+ hex.slice(6, 8).join(''),
261
+ hex.slice(8, 10).join(''),
262
+ hex.slice(10, 16).join(''),
263
+ ].join('-');
264
+ }
265
+ /**
266
+ * Validate a UUID string format
267
+ */
268
+ function isValidUUID(uuid) {
269
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
270
+ return uuidRegex.test(uuid);
271
+ }
272
+
273
+ /**
274
+ * Session ID management
275
+ * Manages session lifecycle with configurable inactivity timeout
276
+ */
277
+ let cachedSessionData = null;
278
+ let sessionTimeoutMs = SESSION_TIMEOUT_MS;
279
+ /**
280
+ * Set the session timeout duration (for server-side configuration)
281
+ */
282
+ function setSessionTimeout(timeoutMinutes) {
283
+ sessionTimeoutMs = timeoutMinutes * 60 * 1000;
284
+ }
285
+ /**
286
+ * Get or create a session ID
287
+ * Creates a new session if:
288
+ * - No existing session
289
+ * - Session has timed out (30 minutes of inactivity by default)
290
+ */
291
+ function getSessionId() {
292
+ const now = Date.now();
293
+ // Check cached session first
294
+ if (cachedSessionData) {
295
+ if (now - cachedSessionData.lastActivity < sessionTimeoutMs) {
296
+ // Session still active, update activity
297
+ updateSessionActivity(cachedSessionData.id);
298
+ return cachedSessionData.id;
299
+ }
300
+ }
301
+ // Check stored session
302
+ const stored = getSessionStorage(STORAGE_KEYS.SESSION);
303
+ if (stored && isValidUUID(stored.id)) {
304
+ if (now - stored.lastActivity < sessionTimeoutMs) {
305
+ // Session still active, update activity and cache
306
+ updateSessionActivity(stored.id);
307
+ return stored.id;
308
+ }
309
+ }
310
+ // Create new session
311
+ return createNewSession();
312
+ }
313
+ /**
314
+ * Create a new session
315
+ */
316
+ function createNewSession() {
317
+ const sessionId = generateUUID();
318
+ cachedSessionData = {
319
+ id: sessionId,
320
+ lastActivity: Date.now(),
321
+ };
322
+ setSessionStorage(STORAGE_KEYS.SESSION, cachedSessionData);
323
+ return sessionId;
324
+ }
325
+ /**
326
+ * Update the session's last activity timestamp
327
+ */
328
+ function updateSessionActivity(sessionId) {
329
+ cachedSessionData = {
330
+ id: sessionId,
331
+ lastActivity: Date.now(),
332
+ };
333
+ setSessionStorage(STORAGE_KEYS.SESSION, cachedSessionData);
334
+ }
335
+ /**
336
+ * Reset the session (clear storage and cache)
337
+ */
338
+ function resetSession() {
339
+ cachedSessionData = null;
340
+ removeSessionStorage(STORAGE_KEYS.SESSION);
341
+ return createNewSession();
342
+ }
343
+
344
+ /**
345
+ * Configuration management module
346
+ * Handles fetching, caching, and merging configuration
347
+ */
348
+ /**
349
+ * Resolve user config with defaults
350
+ */
351
+ function resolveConfig(projectToken, userConfig) {
352
+ // Parse autocapture config
353
+ let autocapture = { ...DEFAULT_CONFIG.autocapture };
354
+ if (userConfig?.autocapture !== undefined) {
355
+ if (typeof userConfig.autocapture === 'boolean') {
356
+ // Boolean: enable or disable all
357
+ autocapture = {
358
+ pageview: userConfig.autocapture,
359
+ clicks: userConfig.autocapture,
360
+ formSubmit: userConfig.autocapture,
361
+ };
362
+ }
363
+ else if (typeof userConfig.autocapture === 'object') {
364
+ // Object: merge with defaults
365
+ const userAutocapture = userConfig.autocapture;
366
+ autocapture = {
367
+ pageview: userAutocapture.pageview ?? DEFAULT_CONFIG.autocapture.pageview,
368
+ clicks: userAutocapture.clicks ?? DEFAULT_CONFIG.autocapture.clicks,
369
+ formSubmit: userAutocapture.formSubmit ?? DEFAULT_CONFIG.autocapture.formSubmit,
370
+ };
371
+ }
372
+ }
373
+ return {
374
+ projectToken,
375
+ apiHost: userConfig?.apiHost ?? DEFAULT_CONFIG.apiHost,
376
+ ingestHost: userConfig?.ingestHost ?? DEFAULT_CONFIG.ingestHost,
377
+ version: userConfig?.version ?? DEFAULT_CONFIG.version,
378
+ debug: userConfig?.debug ?? DEFAULT_CONFIG.debug,
379
+ optOut: userConfig?.optOut ?? DEFAULT_CONFIG.optOut,
380
+ maskAllInputs: userConfig?.maskAllInputs ?? DEFAULT_CONFIG.maskAllInputs,
381
+ autocapture,
382
+ };
383
+ }
384
+ /**
385
+ * Get the cache key for a project's config
386
+ */
387
+ function getCacheKey(projectToken) {
388
+ return `${STORAGE_KEYS.CONFIG_CACHE}_${projectToken}`;
389
+ }
390
+ /**
391
+ * Get cached remote config if valid
392
+ */
393
+ function getCachedConfig(projectToken) {
394
+ const cached = getLocalStorage(getCacheKey(projectToken));
395
+ if (!cached) {
396
+ return null;
397
+ }
398
+ // Check if cache is expired
399
+ if (Date.now() - cached.timestamp > CONFIG_CACHE_TTL_MS) {
400
+ return null;
401
+ }
402
+ return cached.config;
403
+ }
404
+ /**
405
+ * Save remote config to cache
406
+ */
407
+ function setCachedConfig(projectToken, config) {
408
+ const cached = {
409
+ config,
410
+ timestamp: Date.now(),
411
+ };
412
+ setLocalStorage(getCacheKey(projectToken), cached);
413
+ }
414
+ /**
415
+ * Fetch remote configuration from server
416
+ */
417
+ async function fetchRemoteConfig(resolvedConfig) {
418
+ const { projectToken, debug } = resolvedConfig;
419
+ // Check cache first
420
+ const cached = getCachedConfig(projectToken);
421
+ if (cached) {
422
+ if (debug) {
423
+ console.log('[SessionVision] Using cached config');
424
+ }
425
+ return cached;
426
+ }
427
+ try {
428
+ // Fetch config from CDN (static file hosted at apiHost)
429
+ const url = `${resolvedConfig.apiHost}/static/config/${projectToken}`;
430
+ const response = await fetch(url, {
431
+ method: 'GET',
432
+ headers: {
433
+ Accept: 'application/json',
434
+ },
435
+ });
436
+ if (!response.ok) {
437
+ throw new Error(`Config fetch failed: ${response.status}`);
438
+ }
439
+ const config = await response.json();
440
+ // Cache the config
441
+ setCachedConfig(projectToken, config);
442
+ if (debug) {
443
+ console.log('[SessionVision] Remote config fetched:', config);
444
+ }
445
+ return config;
446
+ }
447
+ catch (error) {
448
+ if (debug) {
449
+ console.warn('[SessionVision] Failed to fetch remote config:', error);
450
+ }
451
+ // Try to use stale cache as fallback
452
+ const staleCache = getLocalStorage(getCacheKey(projectToken));
453
+ if (staleCache) {
454
+ if (debug) {
455
+ console.log('[SessionVision] Using stale cached config');
456
+ }
457
+ return staleCache.config;
458
+ }
459
+ return null;
460
+ }
461
+ }
462
+ /**
463
+ * Apply remote config settings
464
+ */
465
+ function applyRemoteConfig(remoteConfig) {
466
+ // Apply session timeout if specified
467
+ if (remoteConfig.session?.timeoutMinutes) {
468
+ setSessionTimeout(remoteConfig.session.timeoutMinutes);
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Anonymous ID management
474
+ * Generates and persists anonymous user identifiers using UUID v4
475
+ */
476
+ let cachedAnonymousId = null;
477
+ /**
478
+ * Get the current anonymous ID, creating one if it doesn't exist
479
+ */
480
+ function getAnonymousId() {
481
+ // Return cached value if available
482
+ if (cachedAnonymousId) {
483
+ return cachedAnonymousId;
484
+ }
485
+ // Try to load from localStorage
486
+ const stored = getLocalStorageRaw(STORAGE_KEYS.ANONYMOUS_ID);
487
+ if (stored && isValidUUID(stored)) {
488
+ cachedAnonymousId = stored;
489
+ return cachedAnonymousId;
490
+ }
491
+ // Generate new anonymous ID
492
+ cachedAnonymousId = generateUUID();
493
+ setLocalStorageRaw(STORAGE_KEYS.ANONYMOUS_ID, cachedAnonymousId);
494
+ return cachedAnonymousId;
495
+ }
496
+ /**
497
+ * Reset the anonymous ID (generates a new one)
498
+ * Called during reset() when user logs out
499
+ */
500
+ function resetAnonymousId() {
501
+ cachedAnonymousId = generateUUID();
502
+ setLocalStorageRaw(STORAGE_KEYS.ANONYMOUS_ID, cachedAnonymousId);
503
+ return cachedAnonymousId;
504
+ }
505
+
506
+ /**
507
+ * User identification management
508
+ * Handles identify() calls and user ID persistence
509
+ */
510
+ let cachedUserId = null;
511
+ let identifyCallback = null;
512
+ /**
513
+ * Set the callback to be called when identify() is invoked
514
+ * Used by the SDK to capture an $identify event
515
+ */
516
+ function setIdentifyCallback(callback) {
517
+ identifyCallback = callback;
518
+ }
519
+ /**
520
+ * Get the current user ID, or null if not identified
521
+ */
522
+ function getUserId() {
523
+ // Return cached value if available
524
+ if (cachedUserId !== null) {
525
+ return cachedUserId;
526
+ }
527
+ // Try to load from localStorage
528
+ const stored = getLocalStorageRaw(STORAGE_KEYS.USER_ID);
529
+ if (stored) {
530
+ cachedUserId = stored;
531
+ return cachedUserId;
532
+ }
533
+ return null;
534
+ }
535
+ /**
536
+ * Identify a user with an ID and optional traits
537
+ * - Sets the user ID in localStorage
538
+ * - Triggers an $identify event with traits
539
+ * - Forward-only: does not retroactively link past anonymous events
540
+ */
541
+ function identify$1(userId, traits) {
542
+ if (!userId || typeof userId !== 'string') {
543
+ console.warn('[SessionVision] identify() requires a non-empty user ID string');
544
+ return;
545
+ }
546
+ // Normalize userId
547
+ const normalizedUserId = userId.trim();
548
+ if (!normalizedUserId) {
549
+ console.warn('[SessionVision] identify() requires a non-empty user ID string');
550
+ return;
551
+ }
552
+ // Set user ID
553
+ cachedUserId = normalizedUserId;
554
+ setLocalStorageRaw(STORAGE_KEYS.USER_ID, normalizedUserId);
555
+ // Trigger $identify event callback
556
+ if (identifyCallback) {
557
+ identifyCallback(normalizedUserId, traits);
558
+ }
559
+ }
560
+ /**
561
+ * Get the distinct ID (user ID if identified, anonymous ID otherwise)
562
+ */
563
+ function getDistinctId() {
564
+ const userId = getUserId();
565
+ return userId || getAnonymousId();
566
+ }
567
+ /**
568
+ * Reset user identity
569
+ * - Clears user ID
570
+ * - Generates new anonymous ID
571
+ * - Starts new session
572
+ * Used on logout
573
+ */
574
+ function reset$1() {
575
+ // Clear user ID
576
+ cachedUserId = null;
577
+ removeLocalStorage(STORAGE_KEYS.USER_ID);
578
+ // Generate new anonymous ID
579
+ resetAnonymousId();
580
+ // Start new session
581
+ resetSession();
582
+ // Clear registered properties
583
+ removeLocalStorage(STORAGE_KEYS.REGISTERED_PROPS);
584
+ removeLocalStorage(STORAGE_KEYS.REGISTERED_ONCE_PROPS);
585
+ }
586
+
587
+ /**
588
+ * DOM utility functions for element inspection and event handling
589
+ */
590
+ /**
591
+ * Get the visible text content of an element, truncated to maxLength
592
+ */
593
+ function getElementText(element, maxLength = 100) {
594
+ // Check for data-sessionvision-mask attribute
595
+ if (element.hasAttribute('data-sessionvision-mask')) {
596
+ return '[masked]';
597
+ }
598
+ const text = element.textContent?.trim().replace(/\s+/g, ' ').slice(0, maxLength) || '';
599
+ return text;
600
+ }
601
+ /**
602
+ * Get CSS classes of an element as a space-separated string
603
+ */
604
+ function getElementClasses(element) {
605
+ if (element.classList && element.classList.length > 0) {
606
+ return Array.from(element.classList).join(' ');
607
+ }
608
+ return '';
609
+ }
610
+ /**
611
+ * Get the tag name of an element in lowercase
612
+ */
613
+ function getElementTag(element) {
614
+ return element.tagName?.toLowerCase() || '';
615
+ }
616
+ /**
617
+ * Get the ID of an element, or null if not present
618
+ */
619
+ function getElementId(element) {
620
+ return element.id || null;
621
+ }
622
+ /**
623
+ * Get the href attribute for anchor elements
624
+ */
625
+ function getElementHref(element) {
626
+ if (element instanceof HTMLAnchorElement) {
627
+ return element.href || null;
628
+ }
629
+ return element.getAttribute('href');
630
+ }
631
+ /**
632
+ * Check if an element should be ignored for tracking
633
+ */
634
+ function shouldIgnoreElement(element) {
635
+ // Check for explicit ignore attribute
636
+ if (element.hasAttribute('data-sessionvision-ignore')) {
637
+ return true;
638
+ }
639
+ // Ignore elements inside sessionvision-ignore containers
640
+ const closestIgnore = element.closest('[data-sessionvision-ignore]');
641
+ if (closestIgnore) {
642
+ return true;
643
+ }
644
+ // Ignore hidden elements
645
+ if (element instanceof HTMLElement) {
646
+ const style = window.getComputedStyle(element);
647
+ if (style.display === 'none' || style.visibility === 'hidden') {
648
+ return true;
649
+ }
650
+ }
651
+ return false;
652
+ }
653
+ /**
654
+ * Check if an element is interactive (clickable, input, etc.)
655
+ */
656
+ function isInteractiveElement(element) {
657
+ const tag = getElementTag(element);
658
+ const interactiveTags = [
659
+ 'a',
660
+ 'button',
661
+ 'input',
662
+ 'select',
663
+ 'textarea',
664
+ 'label',
665
+ ];
666
+ if (interactiveTags.includes(tag)) {
667
+ return true;
668
+ }
669
+ // Check for role attributes
670
+ const role = element.getAttribute('role');
671
+ if (role && ['button', 'link', 'checkbox', 'radio', 'tab'].includes(role)) {
672
+ return true;
673
+ }
674
+ // Check for click handlers or tabindex
675
+ if (element.hasAttribute('onclick') ||
676
+ element.hasAttribute('tabindex') ||
677
+ (element instanceof HTMLElement && element.contentEditable === 'true')) {
678
+ return true;
679
+ }
680
+ return false;
681
+ }
682
+ /**
683
+ * Get the closest interactive parent element
684
+ */
685
+ function getInteractiveParent(element) {
686
+ let current = element;
687
+ while (current) {
688
+ if (isInteractiveElement(current)) {
689
+ return current;
690
+ }
691
+ current = current.parentElement;
692
+ }
693
+ return null;
694
+ }
695
+ /**
696
+ * Safe document ready check
697
+ */
698
+ function onDocumentReady(callback) {
699
+ if (document.readyState === 'loading') {
700
+ document.addEventListener('DOMContentLoaded', callback);
701
+ }
702
+ else {
703
+ callback();
704
+ }
705
+ }
706
+ /**
707
+ * Get the current page URL
708
+ */
709
+ function getCurrentUrl() {
710
+ return window.location.href;
711
+ }
712
+ /**
713
+ * Get the document referrer
714
+ */
715
+ function getReferrer() {
716
+ return document.referrer || '';
717
+ }
718
+ /**
719
+ * Get the document title
720
+ */
721
+ function getDocumentTitle() {
722
+ return document.title || '';
723
+ }
724
+
725
+ /**
726
+ * Automatic properties module
727
+ * Collects browser, device, and environment information
728
+ */
729
+ /**
730
+ * Parse browser and version from user agent
731
+ */
732
+ function parseBrowser() {
733
+ const ua = navigator.userAgent;
734
+ // Order matters - check more specific browsers first
735
+ const browsers = [
736
+ { name: 'Edge', pattern: /Edg(?:e|A|iOS)?\/(\d+(?:\.\d+)*)/ },
737
+ { name: 'Chrome', pattern: /Chrome\/(\d+(?:\.\d+)*)/ },
738
+ { name: 'Firefox', pattern: /Firefox\/(\d+(?:\.\d+)*)/ },
739
+ { name: 'Safari', pattern: /Version\/(\d+(?:\.\d+)*).*Safari/ },
740
+ { name: 'Opera', pattern: /OPR\/(\d+(?:\.\d+)*)/ },
741
+ { name: 'IE', pattern: /MSIE (\d+(?:\.\d+)*)|Trident.*rv:(\d+(?:\.\d+)*)/ },
742
+ ];
743
+ for (const browser of browsers) {
744
+ const match = ua.match(browser.pattern);
745
+ if (match) {
746
+ return {
747
+ name: browser.name,
748
+ version: match[1] || match[2] || 'unknown',
749
+ };
750
+ }
751
+ }
752
+ return { name: 'unknown', version: 'unknown' };
753
+ }
754
+ /**
755
+ * Parse operating system from user agent
756
+ */
757
+ function parseOS() {
758
+ const ua = navigator.userAgent;
759
+ const osPatterns = [
760
+ { name: 'iOS', pattern: /iPad|iPhone|iPod/ },
761
+ { name: 'Android', pattern: /Android/ },
762
+ { name: 'Windows', pattern: /Windows NT/ },
763
+ { name: 'macOS', pattern: /Mac OS X/ },
764
+ { name: 'Linux', pattern: /Linux/ },
765
+ { name: 'Chrome OS', pattern: /CrOS/ },
766
+ ];
767
+ for (const os of osPatterns) {
768
+ if (os.pattern.test(ua)) {
769
+ return os.name;
770
+ }
771
+ }
772
+ return 'unknown';
773
+ }
774
+ /**
775
+ * Determine device type from user agent and screen size
776
+ */
777
+ function parseDeviceType() {
778
+ const ua = navigator.userAgent;
779
+ // Check for tablet patterns first
780
+ if (/iPad|tablet|playbook|silk/i.test(ua)) {
781
+ return 'tablet';
782
+ }
783
+ // Check for mobile patterns
784
+ if (/Mobile|Android|iP(hone|od)|IEMobile|BlackBerry|Opera Mini/i.test(ua)) {
785
+ return 'mobile';
786
+ }
787
+ // Fallback to screen width heuristic
788
+ const screenWidth = window.screen?.width || window.innerWidth;
789
+ if (screenWidth < 768) {
790
+ return 'mobile';
791
+ }
792
+ if (screenWidth < 1024) {
793
+ return 'tablet';
794
+ }
795
+ return 'desktop';
796
+ }
797
+ /**
798
+ * Get the user's timezone
799
+ */
800
+ function getTimezone() {
801
+ try {
802
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'unknown';
803
+ }
804
+ catch {
805
+ return 'unknown';
806
+ }
807
+ }
808
+ /**
809
+ * Get the user's locale
810
+ */
811
+ function getLocale() {
812
+ return navigator.language || 'unknown';
813
+ }
814
+ /**
815
+ * Get the connection type if available
816
+ */
817
+ function getConnectionType() {
818
+ const connection = navigator
819
+ .connection;
820
+ return connection?.effectiveType || null;
821
+ }
822
+ /**
823
+ * Get all automatic properties
824
+ */
825
+ function getAutomaticProperties() {
826
+ const browser = parseBrowser();
827
+ return {
828
+ $current_url: getCurrentUrl(),
829
+ $referrer: getReferrer(),
830
+ $browser: browser.name,
831
+ $browser_version: browser.version,
832
+ $os: parseOS(),
833
+ $device_type: parseDeviceType(),
834
+ $screen_width: window.screen?.width || 0,
835
+ $screen_height: window.screen?.height || 0,
836
+ $viewport_width: window.innerWidth || 0,
837
+ $viewport_height: window.innerHeight || 0,
838
+ $timezone: getTimezone(),
839
+ $locale: getLocale(),
840
+ $connection_type: getConnectionType(),
841
+ $lib_version: "0.2.0"
842
+ ,
843
+ };
844
+ }
845
+ /**
846
+ * Get registered properties (properties sent with every event)
847
+ */
848
+ function getRegisteredProperties() {
849
+ const props = getLocalStorage(STORAGE_KEYS.REGISTERED_PROPS);
850
+ const onceProps = getLocalStorage(STORAGE_KEYS.REGISTERED_ONCE_PROPS);
851
+ return {
852
+ ...onceProps,
853
+ ...props,
854
+ };
855
+ }
856
+ /**
857
+ * Register properties to be sent with every event
858
+ */
859
+ function registerProperties(properties) {
860
+ const existing = getLocalStorage(STORAGE_KEYS.REGISTERED_PROPS) || {};
861
+ setLocalStorage(STORAGE_KEYS.REGISTERED_PROPS, {
862
+ ...existing,
863
+ ...properties,
864
+ });
865
+ }
866
+ /**
867
+ * Register properties only if they don't already exist
868
+ */
869
+ function registerOnceProperties(properties) {
870
+ const existing = getLocalStorage(STORAGE_KEYS.REGISTERED_ONCE_PROPS) || {};
871
+ const merged = { ...existing };
872
+ for (const key of Object.keys(properties)) {
873
+ if (!(key in merged)) {
874
+ merged[key] = properties[key];
875
+ }
876
+ }
877
+ setLocalStorage(STORAGE_KEYS.REGISTERED_ONCE_PROPS, merged);
878
+ }
879
+
880
+ /**
881
+ * Event capture module
882
+ * Core event capture functionality
883
+ */
884
+ let eventCallback = null;
885
+ let config$3 = null;
886
+ /**
887
+ * Set the configuration
888
+ */
889
+ function setConfig(cfg) {
890
+ config$3 = cfg;
891
+ }
892
+ /**
893
+ * Set the callback to be called when an event is captured
894
+ */
895
+ function setEventCallback(callback) {
896
+ eventCallback = callback;
897
+ }
898
+ /**
899
+ * Capture an event
900
+ */
901
+ function captureEvent(eventName, properties = {}, options = {}) {
902
+ // Check if opted out
903
+ if (config$3?.optOut) {
904
+ if (config$3.debug) {
905
+ console.log('[SessionVision] Tracking disabled (optOut: true)');
906
+ }
907
+ return;
908
+ }
909
+ // Validate event name
910
+ if (!eventName || typeof eventName !== 'string') {
911
+ console.warn('[SessionVision] capture() requires a non-empty event name');
912
+ return;
913
+ }
914
+ const { includeAutoProperties = true } = options;
915
+ // Build event
916
+ const event = {
917
+ event: eventName,
918
+ timestamp: Date.now(),
919
+ properties: {
920
+ ...properties,
921
+ ...(includeAutoProperties ? getAutomaticProperties() : {}),
922
+ ...getRegisteredProperties(),
923
+ },
924
+ anonymousId: getAnonymousId(),
925
+ userId: getUserId(),
926
+ sessionId: getSessionId(),
927
+ };
928
+ // Debug logging
929
+ if (config$3?.debug) {
930
+ console.log('[SessionVision] Event captured:', event);
931
+ }
932
+ // Call the callback to buffer the event
933
+ if (eventCallback) {
934
+ eventCallback(event);
935
+ }
936
+ }
937
+ /**
938
+ * Capture an internal/system event (like $identify)
939
+ */
940
+ function captureSystemEvent(eventName, properties = {}) {
941
+ captureEvent(eventName, properties, { includeAutoProperties: true });
942
+ }
943
+
944
+ /**
945
+ * Pageview tracking module
946
+ * Handles initial page load and SPA navigation
947
+ */
948
+ let isInitialized$1 = false;
949
+ let lastUrl = null;
950
+ /**
951
+ * Capture a pageview event
952
+ */
953
+ function capturePageview(customProperties = {}) {
954
+ const url = getCurrentUrl();
955
+ // Avoid duplicate pageviews for the same URL
956
+ if (url === lastUrl) {
957
+ return;
958
+ }
959
+ lastUrl = url;
960
+ const properties = {
961
+ $current_url: url,
962
+ $referrer: getReferrer(),
963
+ $title: getDocumentTitle(),
964
+ ...customProperties,
965
+ };
966
+ captureEvent('$pageview', properties);
967
+ }
968
+ /**
969
+ * Handle history state changes for SPA navigation
970
+ */
971
+ function handleHistoryChange() {
972
+ // Small delay to ensure URL has updated
973
+ setTimeout(() => {
974
+ capturePageview();
975
+ }, 0);
976
+ }
977
+ /**
978
+ * Initialize pageview tracking
979
+ * - Captures initial pageview
980
+ * - Listens for SPA navigation (pushState, replaceState, popstate)
981
+ */
982
+ function initPageviewTracking() {
983
+ if (isInitialized$1) {
984
+ return;
985
+ }
986
+ isInitialized$1 = true;
987
+ // Capture initial pageview
988
+ capturePageview();
989
+ // Monkey-patch history.pushState
990
+ const originalPushState = history.pushState;
991
+ history.pushState = function (...args) {
992
+ const result = originalPushState.apply(this, args);
993
+ handleHistoryChange();
994
+ return result;
995
+ };
996
+ // Monkey-patch history.replaceState
997
+ const originalReplaceState = history.replaceState;
998
+ history.replaceState = function (...args) {
999
+ const result = originalReplaceState.apply(this, args);
1000
+ handleHistoryChange();
1001
+ return result;
1002
+ };
1003
+ // Listen for popstate (back/forward navigation)
1004
+ window.addEventListener('popstate', handleHistoryChange);
1005
+ }
1006
+
1007
+ /**
1008
+ * CSS Selector generation utility
1009
+ * Generates stable, unique selectors for DOM elements
1010
+ */
1011
+ /**
1012
+ * Maximum depth for selector generation
1013
+ */
1014
+ const MAX_SELECTOR_DEPTH = 5;
1015
+ /**
1016
+ * Generate a CSS selector for an element
1017
+ * Priority:
1018
+ * 1. data-sessionvision-id attribute
1019
+ * 2. ID attribute (if unique)
1020
+ * 3. Path-based selector with classes
1021
+ */
1022
+ function generateSelector(element) {
1023
+ // Priority 1: Custom sessionvision ID
1024
+ const customId = element.getAttribute('data-sessionvision-id');
1025
+ if (customId) {
1026
+ return `[data-sessionvision-id="${escapeAttributeValue(customId)}"]`;
1027
+ }
1028
+ // Priority 2: Element ID (if unique in document)
1029
+ const id = getElementId(element);
1030
+ if (id && isUniqueId(id)) {
1031
+ return `#${escapeCssIdentifier(id)}`;
1032
+ }
1033
+ // Priority 3: Build path-based selector
1034
+ return buildPathSelector(element);
1035
+ }
1036
+ /**
1037
+ * Check if an ID is unique in the document
1038
+ */
1039
+ function isUniqueId(id) {
1040
+ try {
1041
+ const elements = document.querySelectorAll(`#${escapeCssIdentifier(id)}`);
1042
+ return elements.length === 1;
1043
+ }
1044
+ catch {
1045
+ // Invalid ID (e.g., starts with number)
1046
+ return false;
1047
+ }
1048
+ }
1049
+ /**
1050
+ * Build a path-based selector walking up the DOM tree
1051
+ */
1052
+ function buildPathSelector(element) {
1053
+ const parts = [];
1054
+ let current = element;
1055
+ let depth = 0;
1056
+ while (current && depth < MAX_SELECTOR_DEPTH) {
1057
+ const part = buildElementPart(current);
1058
+ parts.unshift(part);
1059
+ // Stop at unique ID
1060
+ const id = getElementId(current);
1061
+ if (id && isUniqueId(id)) {
1062
+ break;
1063
+ }
1064
+ // Stop at body
1065
+ if (current === document.body) {
1066
+ break;
1067
+ }
1068
+ current = current.parentElement;
1069
+ depth++;
1070
+ }
1071
+ return parts.join(' > ');
1072
+ }
1073
+ /**
1074
+ * Build the selector part for a single element
1075
+ */
1076
+ function buildElementPart(element) {
1077
+ const tag = getElementTag(element);
1078
+ const id = getElementId(element);
1079
+ // Use ID if unique
1080
+ if (id && isUniqueId(id)) {
1081
+ return `#${escapeCssIdentifier(id)}`;
1082
+ }
1083
+ let selector = tag;
1084
+ // Add significant classes (up to 2)
1085
+ const classes = getElementClasses(element);
1086
+ if (classes) {
1087
+ const significantClasses = classes
1088
+ .split(' ')
1089
+ .filter(isSignificantClass)
1090
+ .slice(0, 2);
1091
+ if (significantClasses.length > 0) {
1092
+ selector += significantClasses
1093
+ .map((c) => `.${escapeCssIdentifier(c)}`)
1094
+ .join('');
1095
+ }
1096
+ }
1097
+ // Add nth-child if needed for uniqueness among siblings
1098
+ const nthChild = getNthChildIndex(element);
1099
+ if (nthChild !== null && needsNthChild(element, selector)) {
1100
+ selector += `:nth-child(${nthChild})`;
1101
+ }
1102
+ return selector;
1103
+ }
1104
+ /**
1105
+ * Check if a class name is significant (not utility/generated)
1106
+ */
1107
+ function isSignificantClass(className) {
1108
+ // Skip empty
1109
+ if (!className) {
1110
+ return false;
1111
+ }
1112
+ // Skip common utility framework classes
1113
+ const utilityPatterns = [
1114
+ /^(mt|mb|ml|mr|mx|my|pt|pb|pl|pr|px|py)-/, // Tailwind spacing
1115
+ /^(w|h|min-w|min-h|max-w|max-h)-/, // Tailwind sizing
1116
+ /^(text|font|bg|border|rounded)-/, // Tailwind common
1117
+ /^(flex|grid|block|inline|hidden)$/, // Tailwind display
1118
+ /^(col|row)-/, // Grid/flex
1119
+ /^(sm|md|lg|xl|2xl):/, // Responsive prefixes
1120
+ /^hover:/, // State prefixes
1121
+ /^[a-z]{1,3}-\d+$/, // Generic utility (e.g., p-4, m-2)
1122
+ /^css-[a-z0-9]+$/i, // CSS-in-JS generated
1123
+ /^_[a-zA-Z0-9]+$/, // CSS modules
1124
+ /^svelte-[a-z0-9]+$/i, // Svelte generated
1125
+ ];
1126
+ return !utilityPatterns.some((pattern) => pattern.test(className));
1127
+ }
1128
+ /**
1129
+ * Get the nth-child index of an element (1-based)
1130
+ */
1131
+ function getNthChildIndex(element) {
1132
+ const parent = element.parentElement;
1133
+ if (!parent) {
1134
+ return null;
1135
+ }
1136
+ const siblings = Array.from(parent.children);
1137
+ const index = siblings.indexOf(element);
1138
+ return index >= 0 ? index + 1 : null;
1139
+ }
1140
+ /**
1141
+ * Check if nth-child is needed to make selector unique among siblings
1142
+ */
1143
+ function needsNthChild(element, baseSelector) {
1144
+ const parent = element.parentElement;
1145
+ if (!parent) {
1146
+ return false;
1147
+ }
1148
+ try {
1149
+ const matches = parent.querySelectorAll(`:scope > ${baseSelector}`);
1150
+ return matches.length > 1;
1151
+ }
1152
+ catch {
1153
+ return false;
1154
+ }
1155
+ }
1156
+ /**
1157
+ * Escape a string for use in a CSS identifier (class, id)
1158
+ */
1159
+ function escapeCssIdentifier(str) {
1160
+ return str.replace(/([^\w-])/g, '\\$1').replace(/^(\d)/, '\\3$1 ');
1161
+ }
1162
+ /**
1163
+ * Escape a string for use in an attribute value selector
1164
+ */
1165
+ function escapeAttributeValue(str) {
1166
+ return str.replace(/"/g, '\\"').replace(/\\/g, '\\\\');
1167
+ }
1168
+
1169
+ /**
1170
+ * PII (Personally Identifiable Information) detection utility
1171
+ * Detects and masks sensitive data patterns in text
1172
+ */
1173
+ /**
1174
+ * PII patterns to detect and mask
1175
+ */
1176
+ const PII_PATTERNS = [
1177
+ {
1178
+ // Credit card numbers (with or without separators)
1179
+ name: 'credit_card',
1180
+ pattern: /\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}/g,
1181
+ mask: '[CARD XXXX]',
1182
+ },
1183
+ {
1184
+ // US Social Security Numbers
1185
+ name: 'ssn',
1186
+ pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
1187
+ mask: '[SSN XXX-XX-XXXX]',
1188
+ },
1189
+ {
1190
+ // US Phone numbers (various formats)
1191
+ name: 'phone_us',
1192
+ pattern: /(?:\+1[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}/g,
1193
+ mask: '[PHONE XXX-XXX-XXXX]',
1194
+ },
1195
+ {
1196
+ // Email addresses (supports multi-part TLDs like .co.uk)
1197
+ name: 'email',
1198
+ pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?:\.[A-Za-z]{2,})?/g,
1199
+ mask: '[EMAIL]',
1200
+ },
1201
+ ];
1202
+ /**
1203
+ * Mask PII in text
1204
+ */
1205
+ function maskPII(text) {
1206
+ if (!text) {
1207
+ return text;
1208
+ }
1209
+ let masked = text;
1210
+ for (const pii of PII_PATTERNS) {
1211
+ // Reset lastIndex for global patterns
1212
+ pii.pattern.lastIndex = 0;
1213
+ masked = masked.replace(pii.pattern, pii.mask);
1214
+ }
1215
+ return masked;
1216
+ }
1217
+
1218
+ /**
1219
+ * Autocapture module
1220
+ * Handles automatic capture of clicks and form submissions
1221
+ */
1222
+ let isClickTrackingActive = false;
1223
+ let isFormTrackingActive = false;
1224
+ let config$2 = null;
1225
+ /**
1226
+ * Set the configuration
1227
+ */
1228
+ function setAutocaptureConfig(cfg) {
1229
+ config$2 = cfg;
1230
+ }
1231
+ /**
1232
+ * Handle click events
1233
+ */
1234
+ function handleClick(event) {
1235
+ const target = event.target;
1236
+ if (!target || shouldIgnoreElement(target)) {
1237
+ return;
1238
+ }
1239
+ // Find the most relevant interactive element
1240
+ const element = getInteractiveParent(target) || target;
1241
+ if (shouldIgnoreElement(element)) {
1242
+ return;
1243
+ }
1244
+ const properties = {
1245
+ $element_tag: getElementTag(element),
1246
+ $element_text: maskPII(getElementText(element)),
1247
+ $element_classes: getElementClasses(element),
1248
+ $element_id: getElementId(element),
1249
+ $element_selector: generateSelector(element),
1250
+ $element_href: getElementHref(element),
1251
+ };
1252
+ captureEvent('$click', properties);
1253
+ }
1254
+ /**
1255
+ * Handle form submission events
1256
+ */
1257
+ function handleFormSubmit(event) {
1258
+ const form = event.target;
1259
+ if (!form || !(form instanceof HTMLFormElement)) {
1260
+ return;
1261
+ }
1262
+ if (shouldIgnoreElement(form)) {
1263
+ return;
1264
+ }
1265
+ const properties = {
1266
+ $form_id: form.id || null,
1267
+ $form_action: form.action || '',
1268
+ $form_method: (form.method || 'GET').toUpperCase(),
1269
+ $form_name: form.name || null,
1270
+ };
1271
+ captureEvent('$form_submit', properties);
1272
+ }
1273
+ /**
1274
+ * Initialize click tracking
1275
+ */
1276
+ function initClickTracking() {
1277
+ if (isClickTrackingActive) {
1278
+ return;
1279
+ }
1280
+ isClickTrackingActive = true;
1281
+ document.addEventListener('click', handleClick, { capture: true });
1282
+ }
1283
+ /**
1284
+ * Initialize form submission tracking
1285
+ */
1286
+ function initFormTracking() {
1287
+ if (isFormTrackingActive) {
1288
+ return;
1289
+ }
1290
+ isFormTrackingActive = true;
1291
+ document.addEventListener('submit', handleFormSubmit, { capture: true });
1292
+ }
1293
+ /**
1294
+ * Initialize all autocapture based on configuration
1295
+ */
1296
+ function initAutocapture(cfg) {
1297
+ config$2 = cfg;
1298
+ if (config$2.autocapture.clicks) {
1299
+ initClickTracking();
1300
+ }
1301
+ if (config$2.autocapture.formSubmit) {
1302
+ initFormTracking();
1303
+ }
1304
+ }
1305
+
1306
+ /**
1307
+ * Payload compression module
1308
+ * Uses CompressionStream API when available
1309
+ */
1310
+ /**
1311
+ * Check if compression is supported
1312
+ */
1313
+ function isCompressionSupported() {
1314
+ return (typeof CompressionStream !== 'undefined' &&
1315
+ typeof ReadableStream !== 'undefined');
1316
+ }
1317
+ /**
1318
+ * Compress a string using gzip
1319
+ * Returns the compressed data as a Blob, or null if compression is not supported
1320
+ */
1321
+ async function compressPayload(data) {
1322
+ if (!isCompressionSupported()) {
1323
+ return null;
1324
+ }
1325
+ try {
1326
+ const encoder = new TextEncoder();
1327
+ const inputBytes = encoder.encode(data);
1328
+ const stream = new ReadableStream({
1329
+ start(controller) {
1330
+ controller.enqueue(inputBytes);
1331
+ controller.close();
1332
+ },
1333
+ });
1334
+ const compressedStream = stream.pipeThrough(new CompressionStream('gzip'));
1335
+ const reader = compressedStream.getReader();
1336
+ const chunks = [];
1337
+ while (true) {
1338
+ const { done, value } = await reader.read();
1339
+ if (done)
1340
+ break;
1341
+ chunks.push(value);
1342
+ }
1343
+ // Combine chunks into a single Blob
1344
+ return new Blob(chunks, { type: 'application/gzip' });
1345
+ }
1346
+ catch {
1347
+ // Compression failed, return null to use uncompressed
1348
+ return null;
1349
+ }
1350
+ }
1351
+ /**
1352
+ * Get the size of data in bytes
1353
+ */
1354
+ function getByteSize(data) {
1355
+ return new Blob([data]).size;
1356
+ }
1357
+ /**
1358
+ * Check if payload should be compressed (based on size threshold)
1359
+ * Only compress if payload is larger than 1KB
1360
+ */
1361
+ function shouldCompress(data) {
1362
+ return isCompressionSupported() && getByteSize(data) > 1024;
1363
+ }
1364
+
1365
+ /**
1366
+ * HTTP transport module
1367
+ * Handles sending events to the ingest API
1368
+ */
1369
+ let config$1 = null;
1370
+ let consecutiveFailures = 0;
1371
+ /**
1372
+ * Set the configuration
1373
+ */
1374
+ function setTransportConfig(cfg) {
1375
+ config$1 = cfg;
1376
+ }
1377
+ /**
1378
+ * Sleep for a given number of milliseconds
1379
+ */
1380
+ function sleep(ms) {
1381
+ return new Promise((resolve) => setTimeout(resolve, ms));
1382
+ }
1383
+ /**
1384
+ * Send events to the ingest API
1385
+ * Handles compression and retry logic
1386
+ */
1387
+ async function sendEvents(payload) {
1388
+ if (!config$1) {
1389
+ console.warn('[SessionVision] SDK not initialized');
1390
+ return false;
1391
+ }
1392
+ const url = `${config$1.ingestHost}/api/v1/ingest/events`;
1393
+ const jsonPayload = JSON.stringify(payload);
1394
+ // Try to compress if payload is large enough
1395
+ const useCompression = shouldCompress(jsonPayload);
1396
+ let body = jsonPayload;
1397
+ const headers = {
1398
+ 'Content-Type': 'application/json',
1399
+ };
1400
+ if (useCompression) {
1401
+ const compressed = await compressPayload(jsonPayload);
1402
+ if (compressed) {
1403
+ body = compressed;
1404
+ headers['Content-Type'] = 'application/json';
1405
+ headers['Content-Encoding'] = 'gzip';
1406
+ }
1407
+ }
1408
+ // Attempt to send with retries
1409
+ for (let attempt = 0; attempt <= BUFFER_CONFIG.MAX_RETRIES; attempt++) {
1410
+ try {
1411
+ const response = await fetch(url, {
1412
+ method: 'POST',
1413
+ headers,
1414
+ body,
1415
+ keepalive: true, // Keep connection alive for background sends
1416
+ });
1417
+ if (response.ok || response.status === 202) {
1418
+ // Success
1419
+ consecutiveFailures = 0;
1420
+ if (config$1.debug) {
1421
+ console.log(`[SessionVision] Events sent successfully (${payload.events.length} events)`);
1422
+ }
1423
+ return true;
1424
+ }
1425
+ // Server error, might be worth retrying
1426
+ if (response.status >= 500) {
1427
+ throw new Error(`Server error: ${response.status}`);
1428
+ }
1429
+ // Client error (4xx), don't retry
1430
+ if (config$1.debug) {
1431
+ console.warn(`[SessionVision] Failed to send events: ${response.status}`);
1432
+ }
1433
+ return false;
1434
+ }
1435
+ catch (error) {
1436
+ // Network error or server error, retry if attempts remaining
1437
+ if (attempt < BUFFER_CONFIG.MAX_RETRIES) {
1438
+ const delay = BUFFER_CONFIG.RETRY_DELAYS_MS[attempt] || 4000;
1439
+ if (config$1.debug) {
1440
+ console.log(`[SessionVision] Retry ${attempt + 1}/${BUFFER_CONFIG.MAX_RETRIES} in ${delay}ms`);
1441
+ }
1442
+ await sleep(delay);
1443
+ }
1444
+ else {
1445
+ // All retries exhausted
1446
+ consecutiveFailures++;
1447
+ if (config$1.debug) {
1448
+ console.warn('[SessionVision] Failed to send events after retries:', error);
1449
+ }
1450
+ }
1451
+ }
1452
+ }
1453
+ return false;
1454
+ }
1455
+ /**
1456
+ * Check if we should stop retrying (3+ consecutive failures)
1457
+ */
1458
+ function shouldStopRetrying() {
1459
+ return consecutiveFailures >= 3;
1460
+ }
1461
+
1462
+ /**
1463
+ * Event buffer module
1464
+ * Buffers events and flushes them periodically or when buffer is full
1465
+ */
1466
+ let eventBuffer = [];
1467
+ let flushTimer = null;
1468
+ let config = null;
1469
+ let isFlushing = false;
1470
+ /**
1471
+ * Set the configuration
1472
+ */
1473
+ function setBufferConfig(cfg) {
1474
+ config = cfg;
1475
+ }
1476
+ /**
1477
+ * Add an event to the buffer
1478
+ */
1479
+ function addToBuffer(event) {
1480
+ // If we've had too many failures, drop events
1481
+ if (shouldStopRetrying()) {
1482
+ if (config?.debug) {
1483
+ console.warn('[SessionVision] Too many failures, dropping event');
1484
+ }
1485
+ return;
1486
+ }
1487
+ eventBuffer.push(event);
1488
+ // Flush if buffer is full
1489
+ if (eventBuffer.length >= BUFFER_CONFIG.MAX_EVENTS) {
1490
+ flush();
1491
+ }
1492
+ }
1493
+ /**
1494
+ * Flush the event buffer
1495
+ * Sends all buffered events to the server
1496
+ */
1497
+ async function flush() {
1498
+ if (isFlushing || eventBuffer.length === 0 || !config) {
1499
+ return;
1500
+ }
1501
+ isFlushing = true;
1502
+ // Take events from buffer (FIFO eviction on failure)
1503
+ const eventsToSend = [...eventBuffer];
1504
+ eventBuffer = [];
1505
+ const payload = {
1506
+ projectToken: config.projectToken,
1507
+ events: eventsToSend,
1508
+ };
1509
+ const success = await sendEvents(payload);
1510
+ if (!success) {
1511
+ // Re-add events to buffer if we haven't exceeded max retries
1512
+ if (!shouldStopRetrying()) {
1513
+ // Only keep most recent events up to max buffer size
1514
+ const combined = [...eventsToSend, ...eventBuffer];
1515
+ eventBuffer = combined.slice(-10);
1516
+ if (config.debug && combined.length > BUFFER_CONFIG.MAX_EVENTS) {
1517
+ console.warn(`[SessionVision] Buffer overflow, dropped ${combined.length - BUFFER_CONFIG.MAX_EVENTS} oldest events`);
1518
+ }
1519
+ }
1520
+ }
1521
+ isFlushing = false;
1522
+ }
1523
+ /**
1524
+ * Start the flush timer
1525
+ */
1526
+ function startFlushTimer() {
1527
+ if (flushTimer) {
1528
+ return;
1529
+ }
1530
+ flushTimer = setInterval(() => {
1531
+ flush();
1532
+ }, BUFFER_CONFIG.FLUSH_INTERVAL_MS);
1533
+ }
1534
+ /**
1535
+ * Initialize visibility change handler for flushing on tab hide
1536
+ */
1537
+ function initVisibilityHandler() {
1538
+ document.addEventListener('visibilitychange', () => {
1539
+ if (document.visibilityState === 'hidden') {
1540
+ // Best-effort flush when tab is hidden
1541
+ flush();
1542
+ }
1543
+ });
1544
+ }
1545
+
1546
+ /**
1547
+ * SDK initialization module
1548
+ * Orchestrates the initialization of all SDK components
1549
+ */
1550
+ let isInitialized = false;
1551
+ let resolvedConfig = null;
1552
+ /**
1553
+ * Initialize the SDK
1554
+ */
1555
+ async function init(projectToken, config) {
1556
+ if (isInitialized) {
1557
+ console.warn('[SessionVision] SDK already initialized');
1558
+ return;
1559
+ }
1560
+ // Validate project token
1561
+ if (!projectToken || typeof projectToken !== 'string') {
1562
+ console.error('[SessionVision] init() requires a valid project token');
1563
+ return;
1564
+ }
1565
+ // Resolve configuration
1566
+ resolvedConfig = resolveConfig(projectToken, config);
1567
+ if (resolvedConfig.debug) {
1568
+ console.log('[SessionVision] Initializing with config:', resolvedConfig);
1569
+ }
1570
+ // Set up components with config
1571
+ setConfig(resolvedConfig);
1572
+ setBufferConfig(resolvedConfig);
1573
+ setTransportConfig(resolvedConfig);
1574
+ setAutocaptureConfig(resolvedConfig);
1575
+ // Wire up event callback to buffer
1576
+ setEventCallback((event) => {
1577
+ addToBuffer(event);
1578
+ });
1579
+ // Wire up identify callback
1580
+ setIdentifyCallback((userId, traits) => {
1581
+ captureSystemEvent('$identify', {
1582
+ $user_id: userId,
1583
+ ...traits,
1584
+ });
1585
+ });
1586
+ // Initialize identity
1587
+ getAnonymousId();
1588
+ getSessionId();
1589
+ // Check if opted out
1590
+ if (resolvedConfig.optOut) {
1591
+ if (resolvedConfig.debug) {
1592
+ console.log('[SessionVision] Tracking disabled (optOut: true)');
1593
+ }
1594
+ isInitialized = true;
1595
+ return;
1596
+ }
1597
+ // Fetch remote config (async, don't block initialization)
1598
+ fetchRemoteConfig(resolvedConfig).then((remoteConfig) => {
1599
+ if (remoteConfig) {
1600
+ applyRemoteConfig(remoteConfig);
1601
+ }
1602
+ });
1603
+ // Start event buffer flush timer
1604
+ startFlushTimer();
1605
+ // Initialize visibility change handler
1606
+ initVisibilityHandler();
1607
+ // Initialize autocapture when DOM is ready
1608
+ onDocumentReady(() => {
1609
+ if (resolvedConfig && resolvedConfig.autocapture.pageview) {
1610
+ initPageviewTracking();
1611
+ }
1612
+ if (resolvedConfig) {
1613
+ initAutocapture(resolvedConfig);
1614
+ }
1615
+ });
1616
+ isInitialized = true;
1617
+ if (resolvedConfig.debug) {
1618
+ console.log('[SessionVision] SDK initialized successfully');
1619
+ }
1620
+ }
1621
+ /**
1622
+ * Capture a custom event
1623
+ */
1624
+ function capture(eventName, properties) {
1625
+ if (!isInitialized) {
1626
+ console.warn('[SessionVision] SDK not initialized. Call init() first.');
1627
+ return;
1628
+ }
1629
+ captureEvent(eventName, properties || {});
1630
+ }
1631
+ /**
1632
+ * Identify a user
1633
+ */
1634
+ function identify(userId, traits) {
1635
+ if (!isInitialized) {
1636
+ console.warn('[SessionVision] SDK not initialized. Call init() first.');
1637
+ return;
1638
+ }
1639
+ identify$1(userId, traits);
1640
+ }
1641
+ /**
1642
+ * Reset user identity (for logout)
1643
+ */
1644
+ function reset() {
1645
+ if (!isInitialized) {
1646
+ console.warn('[SessionVision] SDK not initialized. Call init() first.');
1647
+ return;
1648
+ }
1649
+ reset$1();
1650
+ }
1651
+ /**
1652
+ * Get the current distinct ID
1653
+ */
1654
+ function getDistinctIdValue() {
1655
+ return getDistinctId();
1656
+ }
1657
+ /**
1658
+ * Register properties to send with every event
1659
+ */
1660
+ function register(properties) {
1661
+ if (!isInitialized) {
1662
+ console.warn('[SessionVision] SDK not initialized. Call init() first.');
1663
+ return;
1664
+ }
1665
+ registerProperties(properties);
1666
+ }
1667
+ /**
1668
+ * Register properties only if they don't exist
1669
+ */
1670
+ function registerOnce(properties) {
1671
+ if (!isInitialized) {
1672
+ console.warn('[SessionVision] SDK not initialized. Call init() first.');
1673
+ return;
1674
+ }
1675
+ registerOnceProperties(properties);
1676
+ }
1677
+
1678
+ /**
1679
+ * Queue replay module
1680
+ * Handles replaying method calls that were queued before SDK loaded
1681
+ */
1682
+ /**
1683
+ * Replay queued method calls
1684
+ * The stub queues calls like ['capture', 'event_name', {...}] before SDK loads
1685
+ */
1686
+ function replayQueue(queue, api) {
1687
+ if (!queue || !Array.isArray(queue)) {
1688
+ return;
1689
+ }
1690
+ for (const call of queue) {
1691
+ if (!call || typeof call !== 'object') {
1692
+ continue;
1693
+ }
1694
+ const { method, args } = call;
1695
+ if (!method || typeof method !== 'string') {
1696
+ continue;
1697
+ }
1698
+ // Get the method from the API
1699
+ const fn = api[method];
1700
+ if (typeof fn === 'function') {
1701
+ try {
1702
+ // Call the method with the queued arguments
1703
+ fn.apply(api, args || []);
1704
+ }
1705
+ catch (error) {
1706
+ console.warn(`[SessionVision] Error replaying queued call ${method}:`, error);
1707
+ }
1708
+ }
1709
+ else {
1710
+ console.warn(`[SessionVision] Unknown method in queue: ${method}`);
1711
+ }
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Parse the legacy array-style queue format
1716
+ * PostHog-style: sessionvision._q = [['capture', 'event', {}], ...]
1717
+ */
1718
+ function parseLegacyQueue(legacyQueue) {
1719
+ if (!legacyQueue || !Array.isArray(legacyQueue)) {
1720
+ return [];
1721
+ }
1722
+ return legacyQueue
1723
+ .filter((item) => Array.isArray(item) && item.length > 0)
1724
+ .map((item) => ({
1725
+ method: String(item[0]),
1726
+ args: item.slice(1),
1727
+ }));
1728
+ }
1729
+ /**
1730
+ * Get init calls from _i array
1731
+ */
1732
+ function getInitCalls(initArray) {
1733
+ if (!initArray || !Array.isArray(initArray)) {
1734
+ return [];
1735
+ }
1736
+ return initArray
1737
+ .filter((item) => Array.isArray(item) && item.length > 0 && typeof item[0] === 'string')
1738
+ .map((item) => ({
1739
+ projectToken: item[0],
1740
+ config: item[1],
1741
+ }));
1742
+ }
1743
+
1744
+ /**
1745
+ * Session Vision JavaScript Snippet
1746
+ * Main SDK entry point
1747
+ *
1748
+ * @version "0.2.0"
1749
+ */
1750
+ /**
1751
+ * Session Vision SDK instance
1752
+ */
1753
+ const sessionvision = {
1754
+ /**
1755
+ * SDK version
1756
+ */
1757
+ version: "0.2.0" ,
1758
+ /**
1759
+ * Initialize the SDK with a project token and optional configuration
1760
+ *
1761
+ * @param projectToken - Your Session Vision project token
1762
+ * @param config - Optional configuration options
1763
+ *
1764
+ * @example
1765
+ * ```js
1766
+ * sessionvision.init('proj_abc123', {
1767
+ * apiHost: 'https://cdn.sessionvision.com',
1768
+ * debug: true,
1769
+ * autocapture: true
1770
+ * });
1771
+ * ```
1772
+ */
1773
+ init(projectToken, config) {
1774
+ init(projectToken, config);
1775
+ },
1776
+ /**
1777
+ * Capture a custom event
1778
+ *
1779
+ * @param eventName - Name of the event to capture
1780
+ * @param properties - Optional properties to attach to the event
1781
+ *
1782
+ * @example
1783
+ * ```js
1784
+ * sessionvision.capture('button_clicked', {
1785
+ * button_id: 'signup_cta',
1786
+ * page: '/pricing'
1787
+ * });
1788
+ * ```
1789
+ */
1790
+ capture(eventName, properties) {
1791
+ capture(eventName, properties);
1792
+ },
1793
+ /**
1794
+ * Identify a user with a unique ID and optional traits
1795
+ *
1796
+ * @param userId - Unique identifier for the user
1797
+ * @param traits - Optional user properties (email, name, plan, etc.)
1798
+ *
1799
+ * @example
1800
+ * ```js
1801
+ * sessionvision.identify('user_12345', {
1802
+ * email: 'user@example.com',
1803
+ * name: 'Jane Doe',
1804
+ * plan: 'pro'
1805
+ * });
1806
+ * ```
1807
+ */
1808
+ identify(userId, traits) {
1809
+ identify(userId, traits);
1810
+ },
1811
+ /**
1812
+ * Reset user identity (call on logout)
1813
+ * - Clears the current user ID
1814
+ * - Generates a new anonymous ID
1815
+ * - Starts a new session
1816
+ *
1817
+ * @example
1818
+ * ```js
1819
+ * // On user logout
1820
+ * sessionvision.reset();
1821
+ * ```
1822
+ */
1823
+ reset() {
1824
+ reset();
1825
+ },
1826
+ /**
1827
+ * Get the current distinct ID
1828
+ * Returns the user ID if identified, or the anonymous ID otherwise
1829
+ *
1830
+ * @returns The current user identifier
1831
+ *
1832
+ * @example
1833
+ * ```js
1834
+ * const distinctId = sessionvision.getDistinctId();
1835
+ * // Use for server-side correlation
1836
+ * ```
1837
+ */
1838
+ getDistinctId() {
1839
+ return getDistinctIdValue();
1840
+ },
1841
+ /**
1842
+ * Register properties to be sent with every event
1843
+ *
1844
+ * @param properties - Properties to attach to all future events
1845
+ *
1846
+ * @example
1847
+ * ```js
1848
+ * sessionvision.register({
1849
+ * app_version: '2.1.0',
1850
+ * environment: 'production'
1851
+ * });
1852
+ * ```
1853
+ */
1854
+ register(properties) {
1855
+ register(properties);
1856
+ },
1857
+ /**
1858
+ * Register properties only if they don't already exist
1859
+ *
1860
+ * @param properties - Properties to attach if not already set
1861
+ *
1862
+ * @example
1863
+ * ```js
1864
+ * sessionvision.registerOnce({
1865
+ * initial_referrer: document.referrer
1866
+ * });
1867
+ * ```
1868
+ */
1869
+ registerOnce(properties) {
1870
+ registerOnce(properties);
1871
+ },
1872
+ };
1873
+ /**
1874
+ * Bootstrap the SDK
1875
+ * - Check for existing global instance
1876
+ * - Replay queued method calls
1877
+ * - Process any pending init calls
1878
+ */
1879
+ function bootstrap() {
1880
+ // Check if there's an existing stub instance
1881
+ const existingInstance = window.sessionvision;
1882
+ if (existingInstance && existingInstance.__SV) {
1883
+ // SDK already loaded, don't reinitialize
1884
+ return;
1885
+ }
1886
+ // Replay queued method calls from stub
1887
+ if (existingInstance?._q) {
1888
+ const queue = parseLegacyQueue(existingInstance._q);
1889
+ replayQueue(queue, sessionvision);
1890
+ }
1891
+ // Process init calls
1892
+ if (existingInstance?._i) {
1893
+ const initCalls = getInitCalls(existingInstance._i);
1894
+ for (const { projectToken, config } of initCalls) {
1895
+ sessionvision.init(projectToken, config);
1896
+ }
1897
+ }
1898
+ // Mark SDK as loaded
1899
+ sessionvision.__SV = 1;
1900
+ // Set global instance
1901
+ window.sessionvision = sessionvision;
1902
+ }
1903
+ // Bootstrap on load
1904
+ bootstrap();
1905
+
1906
+ return sessionvision;
1907
+
1908
+ }));
1909
+ //# sourceMappingURL=sessionvision.js.map