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