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