@sessionlens/sdk 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,6 +4,106 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var react = require('react');
6
6
 
7
+ /** RFC 4122 v4 UUID: crypto.randomUUID() when available, else getRandomValues, else Math.random. */
8
+ function uuidv4() {
9
+ const c = typeof crypto !== 'undefined' ? crypto : undefined;
10
+ if (c && typeof c.randomUUID === 'function') {
11
+ return c.randomUUID();
12
+ }
13
+ const bytes = new Uint8Array(16);
14
+ if (c && typeof c.getRandomValues === 'function') {
15
+ c.getRandomValues(bytes);
16
+ }
17
+ else {
18
+ for (let i = 0; i < 16; i++)
19
+ bytes[i] = Math.floor(Math.random() * 256);
20
+ }
21
+ bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
22
+ bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10
23
+ const hex = [];
24
+ for (let i = 0; i < 16; i++)
25
+ hex.push((bytes[i] + 0x100).toString(16).substring(1));
26
+ return (hex.slice(0, 4).join('') + '-' + hex.slice(4, 6).join('') + '-' + hex.slice(6, 8).join('') + '-' +
27
+ hex.slice(8, 10).join('') + '-' + hex.slice(10, 16).join(''));
28
+ }
29
+
30
+ // Storage keys (localStorage). Deliberately not scoped per project: a device is one device.
31
+ const DEVICE_ID_KEY = '_sl_device_id';
32
+ const DISTINCT_ID_KEY = '_sl_distinct_id';
33
+ const USER_ID_KEY = '_sl_user_id';
34
+ const INITIAL_REFERRER_KEY = '_sl_initial_referrer';
35
+ // Used when localStorage is unavailable (SSR, private mode, blocked storage).
36
+ const memoryStore = {};
37
+ function storageGet(key) {
38
+ try {
39
+ if (typeof window !== 'undefined' && window.localStorage) {
40
+ return window.localStorage.getItem(key);
41
+ }
42
+ }
43
+ catch (_) {
44
+ // fall through to memory
45
+ }
46
+ return key in memoryStore ? memoryStore[key] : null;
47
+ }
48
+ function storageSet(key, value) {
49
+ memoryStore[key] = value;
50
+ try {
51
+ if (typeof window !== 'undefined' && window.localStorage) {
52
+ window.localStorage.setItem(key, value);
53
+ }
54
+ }
55
+ catch (_) {
56
+ // memory copy above keeps the identity stable for this page session
57
+ }
58
+ }
59
+ function storageRemove(key) {
60
+ delete memoryStore[key];
61
+ try {
62
+ if (typeof window !== 'undefined' && window.localStorage) {
63
+ window.localStorage.removeItem(key);
64
+ }
65
+ }
66
+ catch (_) {
67
+ // ignore
68
+ }
69
+ }
70
+ function getDeviceId() {
71
+ let deviceId = storageGet(DEVICE_ID_KEY);
72
+ if (!deviceId) {
73
+ deviceId = uuidv4();
74
+ storageSet(DEVICE_ID_KEY, deviceId);
75
+ }
76
+ return deviceId;
77
+ }
78
+ /** Current identity. Anonymous visitors have distinct_id === device_id and no user_id. */
79
+ function getIdentity() {
80
+ const device_id = getDeviceId();
81
+ const user_id = storageGet(USER_ID_KEY) || undefined;
82
+ const distinct_id = storageGet(DISTINCT_ID_KEY) || user_id || device_id;
83
+ return { device_id, distinct_id, user_id };
84
+ }
85
+ /** Persist a user identity. From now on events carry distinct_id = userId. */
86
+ function setUser(userId) {
87
+ storageSet(USER_ID_KEY, userId);
88
+ storageSet(DISTINCT_ID_KEY, userId);
89
+ }
90
+ /** Forget the user and start a fresh anonymous identity on a new device id. */
91
+ function resetIdentity() {
92
+ storageRemove(USER_ID_KEY);
93
+ storageRemove(DISTINCT_ID_KEY);
94
+ storageRemove(DEVICE_ID_KEY);
95
+ return getIdentity();
96
+ }
97
+ /** Referrer of the very first page load, persisted once ('$direct' when there was none). */
98
+ function getInitialReferrer(currentReferrer) {
99
+ let value = storageGet(INITIAL_REFERRER_KEY);
100
+ if (value === null) {
101
+ value = currentReferrer || '$direct';
102
+ storageSet(INITIAL_REFERRER_KEY, value);
103
+ }
104
+ return value;
105
+ }
106
+
7
107
  // Session timeout (30 minutes of inactivity)
8
108
  const SESSION_TIMEOUT = 30 * 60 * 1000;
9
109
  // Store the config and session info
@@ -61,7 +161,7 @@ const startNewSession = (config) => {
61
161
  client_id: getOrCreateClientId(config),
62
162
  org_id: config.org_id,
63
163
  project_id: project_id,
64
- user_id: config.user_id,
164
+ user_id: getIdentity().user_id,
65
165
  started_at: now,
66
166
  last_activity: now
67
167
  };
@@ -79,6 +179,12 @@ const isSessionExpired = () => {
79
179
  return true;
80
180
  return Date.now() - currentSession.last_activity > SESSION_TIMEOUT;
81
181
  };
182
+ // Begin a fresh session (used by reset())
183
+ function restartSession() {
184
+ if (currentConfig) {
185
+ startNewSession(currentConfig);
186
+ }
187
+ }
82
188
  // Update session activity timestamp
83
189
  const updateSessionActivity = () => {
84
190
  if (currentSession) {
@@ -106,14 +212,11 @@ const getEndpoint = (config, endpointType, defaultEndpoint) => {
106
212
  return ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a[endpointType]) || defaultEndpoint;
107
213
  };
108
214
  // `sessionLensInit()` sets the config and validates with backend
109
- async function sessionLensInit(config) {
215
+ async function sessionLensInit$1(config) {
110
216
  // Validate required fields
111
217
  if (!config.sdk_key) {
112
218
  throw new Error('[SessionLens] sdk_key is required');
113
219
  }
114
- if (!config.user_id) {
115
- throw new Error('[SessionLens] user_id is required');
116
- }
117
220
  if (!config.org_id) {
118
221
  throw new Error('[SessionLens] org_id is required');
119
222
  }
@@ -128,7 +231,9 @@ async function sessionLensInit(config) {
128
231
  // Use GET with query parameters for org_id and user_id validation
129
232
  const validationUrl = new URL(`${apiBaseUrl}${validateEndpoint}`);
130
233
  validationUrl.searchParams.set('org_id', config.org_id);
131
- validationUrl.searchParams.set('user_id', config.user_id);
234
+ if (config.user_id) {
235
+ validationUrl.searchParams.set('user_id', config.user_id);
236
+ }
132
237
  const response = await fetch(validationUrl.toString(), {
133
238
  method: 'GET',
134
239
  headers: {
@@ -196,154 +301,260 @@ function getSessionLensApiBaseUrl() {
196
301
  return getApiBaseUrl(currentConfig || undefined);
197
302
  }
198
303
 
199
- function sessionLensTrack(event) {
200
- const config = sessionLensGetConfig();
201
- const sessionInfo = getSessionInfo(); // Get current session with activity update
202
- // Add timestamp if not provided
203
- const eventWithTimestamp = {
204
- ...event,
205
- timestamp: event.timestamp || Date.now()
304
+ // Order matters: Edge/Opera/Samsung embed "Chrome", Chrome embeds "Safari".
305
+ const BROWSERS = [
306
+ ['Edge', /(?:Edg|EdgA|EdgiOS|Edge)\/([\d.]+)/],
307
+ ['Opera', /(?:OPR|Opera)\/([\d.]+)/],
308
+ ['Samsung Internet', /SamsungBrowser\/([\d.]+)/],
309
+ ['Firefox', /(?:Firefox|FxiOS)\/([\d.]+)/],
310
+ ['Chrome', /(?:Chrome|CriOS)\/([\d.]+)/],
311
+ ['Internet Explorer', /(?:MSIE |Trident\/.*rv:)([\d.]+)/],
312
+ ['Safari', /Version\/([\d.]+).*Safari/],
313
+ ];
314
+ const OSES = [
315
+ ['Windows', /Windows NT ([\d.]+)/],
316
+ ['iOS', /(?:iPhone|CPU) OS ([\d_]+)/],
317
+ ['Android', /Android ([\d.]+)/],
318
+ ['Mac OS X', /Mac OS X ([\d_.]+)/],
319
+ ['Chrome OS', /CrOS \S+ ([\d.]+)/],
320
+ ['Linux', /Linux()/],
321
+ ];
322
+ function parseUserAgent(ua) {
323
+ const result = { browser: '', browser_version: '', os: '', os_version: '', device_type: '' };
324
+ if (!ua)
325
+ return result;
326
+ for (const [name, re] of BROWSERS) {
327
+ const m = ua.match(re);
328
+ if (m) {
329
+ result.browser = name;
330
+ result.browser_version = m[1];
331
+ break;
332
+ }
333
+ }
334
+ for (const [name, re] of OSES) {
335
+ const m = ua.match(re);
336
+ if (m) {
337
+ result.os = name;
338
+ result.os_version = (m[1] || '').replace(/_/g, '.');
339
+ break;
340
+ }
341
+ }
342
+ if (/iPad|Tablet|PlayBook|Silk/i.test(ua) || (/Android/i.test(ua) && !/Mobile/i.test(ua))) {
343
+ result.device_type = 'Tablet';
344
+ }
345
+ else if (/Mobi|iPhone|iPod|Android|Windows Phone/i.test(ua)) {
346
+ result.device_type = 'Mobile';
347
+ }
348
+ else if (result.os) {
349
+ result.device_type = 'Desktop';
350
+ }
351
+ return result;
352
+ }
353
+
354
+ // Hand-maintained (bundling package.json would need an extra rollup plugin).
355
+ // Keep in sync with "version" in package.json when releasing.
356
+ const SDK_VERSION = '0.3.0';
357
+ const SDK_LIBRARY = 'sessionlens-js';
358
+
359
+ const UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'];
360
+ function hostOf(url) {
361
+ try {
362
+ return new URL(url).hostname;
363
+ }
364
+ catch (_) {
365
+ return '';
366
+ }
367
+ }
368
+ /**
369
+ * System properties attached to every event. All browser access is guarded so this
370
+ * is safe to call during SSR (only identity/library fields are returned there).
371
+ * The server re-derives browser/os/geo authoritatively; the client UA fields are a fallback.
372
+ */
373
+ function buildSystemProperties(clientTimestamp) {
374
+ const identity = getIdentity();
375
+ const props = {
376
+ $device_id: identity.device_id,
377
+ $distinct_id: identity.distinct_id,
378
+ $insert_id: uuidv4(),
379
+ $library: SDK_LIBRARY,
380
+ $library_version: SDK_VERSION,
381
+ $client_timestamp: clientTimestamp,
206
382
  };
207
- if (config.debug) {
208
- console.log('SessionLens Track:', eventWithTimestamp);
209
- console.log('Session Context:', {
210
- session_id: sessionInfo.session_id,
211
- client_id: sessionInfo.client_id,
212
- project_id: sessionInfo.project_id
213
- });
383
+ if (identity.user_id)
384
+ props.$user_id = identity.user_id;
385
+ if (typeof window === 'undefined')
386
+ return props;
387
+ try {
388
+ const loc = window.location;
389
+ props.$current_url = loc.href;
390
+ props.$pathname = loc.pathname;
391
+ const referrer = typeof document !== 'undefined' ? document.referrer || '' : '';
392
+ props.$referrer = referrer || '$direct';
393
+ props.$referring_domain = referrer ? hostOf(referrer) : '$direct';
394
+ props.$initial_referrer = getInitialReferrer(referrer);
395
+ const params = new URLSearchParams(loc.search);
396
+ for (const key of UTM_KEYS) {
397
+ const value = params.get(key);
398
+ if (value)
399
+ props['$' + key] = value;
400
+ }
401
+ if (window.screen) {
402
+ props.$screen_width = window.screen.width;
403
+ props.$screen_height = window.screen.height;
404
+ }
405
+ if (typeof navigator !== 'undefined' && navigator.userAgent) {
406
+ const ua = parseUserAgent(navigator.userAgent);
407
+ if (ua.browser)
408
+ props.$browser = ua.browser;
409
+ if (ua.browser_version)
410
+ props.$browser_version = ua.browser_version;
411
+ if (ua.os)
412
+ props.$os = ua.os;
413
+ if (ua.os_version)
414
+ props.$os_version = ua.os_version;
415
+ if (ua.device_type)
416
+ props.$device_type = ua.device_type;
417
+ }
214
418
  }
215
- // Send event to backend with session context
216
- sendEventToBackend(eventWithTimestamp, config, sessionInfo);
419
+ catch (_) {
420
+ // enrichment must never break tracking
421
+ }
422
+ return props;
217
423
  }
218
- function sessionLensIdentify(user) {
219
- const config = sessionLensGetConfig();
220
- const sessionInfo = getSessionInfo(); // Get current session with activity update
221
- if (config.debug) {
222
- console.log('SessionLens Identify:', user);
223
- console.log('Session Context:', {
224
- session_id: sessionInfo.session_id,
225
- client_id: sessionInfo.client_id,
226
- project_id: sessionInfo.project_id
227
- });
424
+
425
+ /**
426
+ * Initialize the SDK. If `config.user_id` is supplied (pre-0.3 integrations) it is treated
427
+ * as an implicit identify() - sent only when it differs from the persisted identity.
428
+ */
429
+ async function sessionLensInit(config) {
430
+ await sessionLensInit$1(config);
431
+ if (config.user_id && getIdentity().user_id !== config.user_id) {
432
+ identify(config.user_id);
228
433
  }
229
- // Send identify event to backend with session context
230
- sendIdentifyToBackend(user, config, sessionInfo);
231
434
  }
232
- function sessionLensReset() {
435
+ function sessionLensTrack(event) {
233
436
  const config = sessionLensGetConfig();
234
437
  const sessionInfo = getSessionInfo(); // Get current session with activity update
438
+ const timestamp = event.timestamp || Date.now();
439
+ // Caller-supplied properties override auto-collected ones (e.g. to pass a stable $insert_id for retries).
440
+ const properties = { ...buildSystemProperties(timestamp), ...(event.properties || {}) };
441
+ const eventWithContext = { ...event, timestamp, properties };
235
442
  if (config.debug) {
236
- console.log('SessionLens Reset');
443
+ console.log('SessionLens Track:', eventWithContext);
237
444
  console.log('Session Context:', {
238
445
  session_id: sessionInfo.session_id,
239
446
  client_id: sessionInfo.client_id,
240
447
  project_id: sessionInfo.project_id
241
448
  });
242
449
  }
243
- // Clear any stored user data and send reset event to backend
244
- sendResetToBackend(config, sessionInfo);
450
+ sendEventToBackend(eventWithContext, config, sessionInfo);
245
451
  }
246
- // Helper functions for backend communication with session context
247
- async function sendEventToBackend(event, config, sessionInfo) {
452
+ /** Link the current device to `userId` (last identify wins) and optionally set traits. */
453
+ function identify(userId, traits) {
248
454
  var _a;
249
- const apiBaseUrl = getSessionLensApiBaseUrl();
250
- const eventsEndpoint = ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.events) || '/events/ingest';
251
- try {
252
- const response = await fetch(`${apiBaseUrl}${eventsEndpoint}`, {
253
- method: 'POST',
254
- headers: {
255
- 'Content-Type': 'application/json',
256
- 'Authorization': `Bearer ${config.sdk_key}`
257
- },
258
- body: JSON.stringify({
259
- org_id: config.org_id,
260
- project_id: sessionInfo.project_id,
261
- user_id: config.user_id,
262
- session_id: sessionInfo.session_id,
263
- client_id: sessionInfo.client_id,
264
- session_started_at: sessionInfo.started_at,
265
- event: event
266
- })
267
- });
268
- if (!response.ok && config.debug) {
269
- console.warn('SessionLens: Failed to send event:', response.status);
270
- }
271
- else if (config.debug) {
272
- console.log('SessionLens: Event sent successfully with session context');
273
- }
455
+ const config = sessionLensGetConfig();
456
+ const sessionInfo = getSessionInfo();
457
+ if (!userId) {
458
+ if (config.debug)
459
+ console.warn('SessionLens: identify() requires a userId');
460
+ return;
274
461
  }
275
- catch (error) {
276
- if (config.debug) {
277
- console.warn('SessionLens: Network error sending event:', error);
278
- }
462
+ setUser(userId);
463
+ if (config.debug) {
464
+ console.log('SessionLens Identify:', { userId, traits });
279
465
  }
466
+ post(config, ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.identify) || '/identify', {
467
+ org_id: config.org_id,
468
+ project_id: sessionInfo.project_id,
469
+ app: config.app || 'unknown',
470
+ user_id: userId,
471
+ device_id: getIdentity().device_id,
472
+ session_id: sessionInfo.session_id,
473
+ client_id: sessionInfo.client_id,
474
+ session_started_at: sessionInfo.started_at,
475
+ traits: traits || {}
476
+ }, 'identify');
477
+ }
478
+ /** @deprecated Aliasing is subsumed by identify(); `oldId` is ignored (the device link is implicit). */
479
+ function alias(newId, _oldId) {
480
+ identify(newId);
481
+ }
482
+ /** Set traits on the current profile (`name` and `email` map to core profile fields). */
483
+ function setUserProperties(traits) {
484
+ var _a;
485
+ const config = sessionLensGetConfig();
486
+ const sessionInfo = getSessionInfo();
487
+ post(config, ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.userProperties) || '/user-properties', {
488
+ org_id: config.org_id,
489
+ project_id: sessionInfo.project_id,
490
+ distinct_id: getIdentity().distinct_id,
491
+ traits: traits || {}
492
+ }, 'user properties');
493
+ }
494
+ /** Legacy signature: sessionLensIdentify({ user_id, properties }). */
495
+ function sessionLensIdentify(user) {
496
+ identify(user.user_id, user.properties);
280
497
  }
281
- async function sendIdentifyToBackend(user, config, sessionInfo) {
498
+ /** Forget the user, start an anonymous identity on a new device id and a new session. */
499
+ function reset() {
282
500
  var _a;
283
- const apiBaseUrl = getSessionLensApiBaseUrl();
284
- const identifyEndpoint = ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.identify) || '/identify';
501
+ const config = sessionLensGetConfig();
502
+ const sessionInfo = getSessionInfo();
503
+ if (config.debug) {
504
+ console.log('SessionLens Reset');
505
+ }
506
+ post(config, ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.reset) || '/reset', {
507
+ org_id: config.org_id,
508
+ project_id: sessionInfo.project_id,
509
+ session_id: sessionInfo.session_id,
510
+ client_id: sessionInfo.client_id,
511
+ session_started_at: sessionInfo.started_at
512
+ }, 'reset');
513
+ resetIdentity();
514
+ restartSession();
515
+ }
516
+ const sessionLensReset = reset;
517
+ async function post(config, endpoint, body, label) {
285
518
  try {
286
- const response = await fetch(`${apiBaseUrl}${identifyEndpoint}`, {
519
+ const response = await fetch(`${getSessionLensApiBaseUrl()}${endpoint}`, {
287
520
  method: 'POST',
288
521
  headers: {
289
522
  'Content-Type': 'application/json',
290
523
  'Authorization': `Bearer ${config.sdk_key}`
291
524
  },
292
- body: JSON.stringify({
293
- org_id: config.org_id,
294
- project_id: sessionInfo.project_id,
295
- user_id: user.user_id,
296
- session_id: sessionInfo.session_id,
297
- client_id: sessionInfo.client_id,
298
- session_started_at: sessionInfo.started_at,
299
- properties: user.properties
300
- })
525
+ body: JSON.stringify(body)
301
526
  });
302
527
  if (!response.ok && config.debug) {
303
- console.warn('SessionLens: Failed to send identify:', response.status);
528
+ console.warn(`SessionLens: Failed to send ${label}:`, response.status);
304
529
  }
305
530
  else if (config.debug) {
306
- console.log('SessionLens: Identify sent successfully with session context');
531
+ console.log(`SessionLens: ${label} sent successfully`);
307
532
  }
308
533
  }
309
534
  catch (error) {
310
535
  if (config.debug) {
311
- console.warn('SessionLens: Network error sending identify:', error);
536
+ console.warn(`SessionLens: Network error sending ${label}:`, error);
312
537
  }
313
538
  }
314
539
  }
315
- async function sendResetToBackend(config, sessionInfo) {
540
+ function sendEventToBackend(event, config, sessionInfo) {
316
541
  var _a;
317
- const apiBaseUrl = getSessionLensApiBaseUrl();
318
- const resetEndpoint = ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.reset) || '/reset';
319
- try {
320
- const response = await fetch(`${apiBaseUrl}${resetEndpoint}`, {
321
- method: 'POST',
322
- headers: {
323
- 'Content-Type': 'application/json',
324
- 'Authorization': `Bearer ${config.sdk_key}`
325
- },
326
- body: JSON.stringify({
327
- org_id: config.org_id,
328
- project_id: sessionInfo.project_id,
329
- user_id: config.user_id,
330
- session_id: sessionInfo.session_id,
331
- client_id: sessionInfo.client_id,
332
- session_started_at: sessionInfo.started_at
333
- })
334
- });
335
- if (!response.ok && config.debug) {
336
- console.warn('SessionLens: Failed to send reset:', response.status);
337
- }
338
- else if (config.debug) {
339
- console.log('SessionLens: Reset sent successfully with session context');
340
- }
341
- }
342
- catch (error) {
343
- if (config.debug) {
344
- console.warn('SessionLens: Network error sending reset:', error);
345
- }
542
+ const identity = getIdentity();
543
+ const body = {
544
+ org_id: config.org_id,
545
+ project_id: sessionInfo.project_id,
546
+ app: event.app || config.app || 'unknown',
547
+ device_id: identity.device_id,
548
+ distinct_id: identity.distinct_id,
549
+ session_id: sessionInfo.session_id,
550
+ client_id: sessionInfo.client_id,
551
+ session_started_at: sessionInfo.started_at,
552
+ event: event
553
+ };
554
+ if (identity.user_id) {
555
+ body.user_id = identity.user_id;
346
556
  }
557
+ return post(config, ((_a = config.endpoints) === null || _a === void 0 ? void 0 : _a.events) || '/events/ingest', body, 'event');
347
558
  }
348
559
 
349
560
  const useSessionLens = (config) => {
@@ -358,10 +569,8 @@ const useSessionLens = (config) => {
358
569
  }
359
570
  };
360
571
  initializeSDK();
361
- // Cleanup on unmount
362
- return () => {
363
- sessionLensReset();
364
- };
572
+ // No cleanup reset: reset() now clears the persisted device/user identity, which must
573
+ // survive unmounts and re-renders. Call reset() explicitly on logout instead.
365
574
  }, [config]);
366
575
  };
367
576
  const useSessionLensTrack = () => {
@@ -398,10 +607,24 @@ class SessionLens {
398
607
  sessionLensReset() {
399
608
  sessionLensReset();
400
609
  }
610
+ identify(userId, traits) {
611
+ identify(userId, traits);
612
+ }
613
+ /** @deprecated Use identify(). */
614
+ alias(newId, oldId) {
615
+ alias(newId);
616
+ }
617
+ setUserProperties(traits) {
618
+ setUserProperties(traits);
619
+ }
620
+ reset() {
621
+ reset();
622
+ }
401
623
  }
402
624
  // Create and export singleton instance
403
625
  const sessionLens = new SessionLens();
404
626
 
627
+ exports.SDK_VERSION = SDK_VERSION;
405
628
  exports["default"] = sessionLens;
406
629
  exports.useSessionLens = useSessionLens;
407
630
  exports.useSessionLensIdentify = useSessionLensIdentify;