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