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