@loamly/tracker 2.1.0 → 2.1.1

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/src/core.ts CHANGED
@@ -62,8 +62,10 @@ let initialized = false
62
62
  let debugMode = false
63
63
  let visitorId: string | null = null
64
64
  let sessionId: string | null = null
65
+ let workspaceId: string | null = null
65
66
  let navigationTiming: NavigationTiming | null = null
66
67
  let aiDetection: AIDetectionResult | null = null
68
+ let pageStartTime: number | null = null
67
69
 
68
70
  // Detection modules
69
71
  let behavioralClassifier: BehavioralClassifier | null = null
@@ -100,6 +102,34 @@ function endpoint(path: string): string {
100
102
  return `${config.apiHost}${path}`
101
103
  }
102
104
 
105
+ function buildHeaders(idempotencyKey?: string): Record<string, string> {
106
+ const headers: Record<string, string> = {
107
+ 'Content-Type': 'application/json',
108
+ }
109
+
110
+ if (config.apiKey) {
111
+ headers['X-Loamly-Api-Key'] = config.apiKey
112
+ }
113
+
114
+ if (idempotencyKey) {
115
+ headers['X-Idempotency-Key'] = idempotencyKey
116
+ }
117
+
118
+ return headers
119
+ }
120
+
121
+ function buildBeaconUrl(path: string): string {
122
+ if (!config.apiKey) return path
123
+ const url = new URL(path, config.apiHost)
124
+ url.searchParams.set('api_key', config.apiKey)
125
+ return url.toString()
126
+ }
127
+
128
+ function buildIdempotencyKey(prefix: string): string {
129
+ const base = sessionId || visitorId || 'unknown'
130
+ return `${prefix}:${base}:${Date.now()}`
131
+ }
132
+
103
133
  /**
104
134
  * Initialize the tracker
105
135
  */
@@ -114,8 +144,13 @@ function init(userConfig: LoamlyConfig = {}): void {
114
144
  ...userConfig,
115
145
  apiHost: userConfig.apiHost || DEFAULT_CONFIG.apiHost,
116
146
  }
147
+ workspaceId = userConfig.workspaceId ?? null
117
148
 
118
149
  debugMode = userConfig.debug ?? false
150
+
151
+ if (config.apiKey && !workspaceId) {
152
+ log('Workspace ID missing. Behavioral events require workspaceId.')
153
+ }
119
154
 
120
155
  // Feature flags with defaults (all enabled except ping)
121
156
  const features = {
@@ -137,17 +172,13 @@ function init(userConfig: LoamlyConfig = {}): void {
137
172
  // Get/create visitor ID
138
173
  visitorId = getVisitorId()
139
174
  log('Visitor ID:', visitorId)
140
-
141
- // Get/create session
142
- const session = getSessionId()
143
- sessionId = session.sessionId
144
- log('Session ID:', sessionId, session.isNew ? '(new)' : '(existing)')
145
-
146
- // Initialize event queue with batching (if enabled)
175
+
176
+ // Initialize event queue (if enabled)
147
177
  if (features.eventQueue) {
148
178
  eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
149
179
  batchSize: DEFAULT_CONFIG.batchSize,
150
180
  batchTimeout: DEFAULT_CONFIG.batchTimeout,
181
+ apiKey: config.apiKey,
151
182
  })
152
183
  }
153
184
 
@@ -162,51 +193,57 @@ function init(userConfig: LoamlyConfig = {}): void {
162
193
  }
163
194
 
164
195
  initialized = true
165
-
166
- // Auto pageview unless disabled
167
- if (!userConfig.disableAutoPageview) {
168
- pageview()
169
- }
170
-
171
- // Set up behavioral tracking (scroll, time, forms) unless disabled
172
- if (!userConfig.disableBehavioral) {
173
- setupAdvancedBehavioralTracking(features)
174
- }
175
-
176
- // Initialize behavioral ML classifier (LOA-180) - if enabled
177
- if (features.behavioralML) {
178
- behavioralClassifier = new BehavioralClassifier(10000) // 10s min session
179
- behavioralClassifier.setOnClassify(handleBehavioralClassification)
180
- setupBehavioralMLTracking()
181
- }
182
-
183
- // Initialize focus/blur analyzer (LOA-182) - if enabled
184
- if (features.focusBlur) {
185
- focusBlurAnalyzer = new FocusBlurAnalyzer()
186
- focusBlurAnalyzer.initTracking()
187
-
188
- // Analyze focus/blur after 5 seconds
189
- setTimeout(() => {
190
- if (focusBlurAnalyzer) {
191
- handleFocusBlurAnalysis(focusBlurAnalyzer.analyze())
192
- }
193
- }, 5000)
194
- }
195
-
196
- // Initialize agentic browser detection (LOA-187) - if enabled
196
+
197
+ // Initialize agentic browser detection early for pageview payloads
197
198
  if (features.agentic) {
198
199
  agenticAnalyzer = new AgenticBrowserAnalyzer()
199
200
  agenticAnalyzer.init()
200
201
  }
201
-
202
- // Set up ping service - if enabled (opt-in)
203
- if (features.ping && visitorId && sessionId) {
204
- pingService = new PingService(sessionId, visitorId, VERSION, {
205
- interval: DEFAULT_CONFIG.pingInterval,
206
- endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping),
207
- })
208
- pingService.start()
209
- }
202
+
203
+ // Initialize session (async), then start tracking
204
+ void initializeSession().finally(() => {
205
+ // Register service worker for RFC 9421 verification (optional)
206
+ void registerServiceWorker()
207
+
208
+ // Auto pageview unless disabled
209
+ if (!userConfig.disableAutoPageview) {
210
+ pageview()
211
+ }
212
+
213
+ // Set up behavioral tracking (scroll, time, forms) unless disabled
214
+ if (!userConfig.disableBehavioral) {
215
+ setupAdvancedBehavioralTracking(features)
216
+ }
217
+
218
+ // Initialize behavioral ML classifier (LOA-180) - if enabled
219
+ if (features.behavioralML) {
220
+ behavioralClassifier = new BehavioralClassifier(10000) // 10s min session
221
+ behavioralClassifier.setOnClassify(handleBehavioralClassification)
222
+ setupBehavioralMLTracking()
223
+ }
224
+
225
+ // Initialize focus/blur analyzer (LOA-182) - if enabled
226
+ if (features.focusBlur) {
227
+ focusBlurAnalyzer = new FocusBlurAnalyzer()
228
+ focusBlurAnalyzer.initTracking()
229
+
230
+ // Analyze focus/blur after 5 seconds
231
+ setTimeout(() => {
232
+ if (focusBlurAnalyzer) {
233
+ handleFocusBlurAnalysis(focusBlurAnalyzer.analyze())
234
+ }
235
+ }, 5000)
236
+ }
237
+
238
+ // Set up ping service - if enabled (opt-in)
239
+ if (features.ping && visitorId && sessionId) {
240
+ pingService = new PingService(sessionId, visitorId, VERSION, {
241
+ interval: DEFAULT_CONFIG.pingInterval,
242
+ endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping),
243
+ })
244
+ pingService.start()
245
+ }
246
+ })
210
247
 
211
248
  // Set up SPA navigation tracking
212
249
  spaRouter = new SPARouter({
@@ -223,6 +260,101 @@ function init(userConfig: LoamlyConfig = {}): void {
223
260
  log('Initialization complete')
224
261
  }
225
262
 
263
+ async function registerServiceWorker(): Promise<void> {
264
+ if (typeof navigator === 'undefined' || !('serviceWorker' in navigator)) return
265
+ if (!config.apiKey || !workspaceId) return
266
+
267
+ try {
268
+ const swUrl = new URL('/tracker/loamly-sw.js', window.location.origin)
269
+ swUrl.searchParams.set('workspace_id', workspaceId)
270
+ swUrl.searchParams.set('api_key', config.apiKey)
271
+
272
+ const registration = await navigator.serviceWorker.register(swUrl.toString(), { scope: '/' })
273
+
274
+ registration.addEventListener('updatefound', () => {
275
+ const installing = registration.installing
276
+ installing?.addEventListener('statechange', () => {
277
+ if (installing.state === 'activated') {
278
+ installing.postMessage({ type: 'SKIP_WAITING' })
279
+ }
280
+ })
281
+ })
282
+
283
+ setInterval(() => {
284
+ registration.update().catch(() => {
285
+ // Ignore update failures
286
+ })
287
+ }, 24 * 60 * 60 * 1000)
288
+ } catch {
289
+ // Ignore service worker errors
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Initialize session with server-side tracker_sessions when possible
295
+ */
296
+ async function initializeSession(): Promise<void> {
297
+ const now = Date.now()
298
+ pageStartTime = now
299
+
300
+ // Try sessionStorage first (fast path)
301
+ try {
302
+ const storedSession = sessionStorage.getItem('loamly_session')
303
+ const storedStart = sessionStorage.getItem('loamly_start')
304
+ const sessionTimeout = config.sessionTimeout ?? DEFAULT_CONFIG.sessionTimeout
305
+
306
+ if (storedSession && storedStart) {
307
+ const startTime = parseInt(storedStart, 10)
308
+ const elapsed = now - startTime
309
+ if (elapsed > 0 && elapsed < sessionTimeout) {
310
+ sessionId = storedSession
311
+ log('Session ID:', sessionId, '(existing)')
312
+ return
313
+ }
314
+ }
315
+ } catch {
316
+ // sessionStorage not available
317
+ }
318
+
319
+ // Try server-side session creation if we have required context
320
+ if (config.apiKey && workspaceId && visitorId) {
321
+ try {
322
+ const response = await safeFetch(endpoint(DEFAULT_CONFIG.endpoints.session), {
323
+ method: 'POST',
324
+ headers: buildHeaders(),
325
+ body: JSON.stringify({
326
+ workspace_id: workspaceId,
327
+ visitor_id: visitorId,
328
+ }),
329
+ })
330
+
331
+ if (response?.ok) {
332
+ const data = await response.json()
333
+ sessionId = data.session_id || sessionId
334
+ const startTime = data.start_time || now
335
+
336
+ if (sessionId) {
337
+ try {
338
+ sessionStorage.setItem('loamly_session', sessionId)
339
+ sessionStorage.setItem('loamly_start', String(startTime))
340
+ } catch {
341
+ // Ignore storage failures
342
+ }
343
+ log('Session ID:', sessionId, '(server)')
344
+ return
345
+ }
346
+ }
347
+ } catch {
348
+ // Fall through to local session
349
+ }
350
+ }
351
+
352
+ // Fallback: local session
353
+ const session = getSessionId()
354
+ sessionId = session.sessionId
355
+ log('Session ID:', sessionId, session.isNew ? '(new)' : '(existing)')
356
+ }
357
+
226
358
  /**
227
359
  * Set up advanced behavioral tracking with new modules
228
360
  */
@@ -246,8 +378,8 @@ function setupAdvancedBehavioralTracking(features: FeatureFlags): void {
246
378
  onChunkReached: (event: ScrollEvent) => {
247
379
  log('Scroll chunk:', event.chunk)
248
380
  queueEvent('scroll_depth', {
249
- depth: event.depth,
250
- chunk: event.chunk,
381
+ scroll_depth: Math.round((event.depth / 100) * 100) / 100,
382
+ milestone: Math.round((event.chunk / 100) * 100) / 100,
251
383
  time_to_reach_ms: event.time_to_reach_ms,
252
384
  })
253
385
  },
@@ -262,10 +394,8 @@ function setupAdvancedBehavioralTracking(features: FeatureFlags): void {
262
394
  onUpdate: (event: TimeEvent) => {
263
395
  if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
264
396
  queueEvent('time_spent', {
265
- active_time_ms: event.active_time_ms,
266
- total_time_ms: event.total_time_ms,
267
- idle_time_ms: event.idle_time_ms,
268
- is_engaged: event.is_engaged,
397
+ visible_time_ms: event.total_time_ms,
398
+ page_start_time: pageStartTime || Date.now(),
269
399
  })
270
400
  }
271
401
  },
@@ -278,13 +408,21 @@ function setupAdvancedBehavioralTracking(features: FeatureFlags): void {
278
408
  formTracker = new FormTracker({
279
409
  onFormEvent: (event: FormEvent) => {
280
410
  log('Form event:', event.event_type, event.form_id)
281
- queueEvent(event.event_type, {
411
+ const isSubmitEvent = event.event_type === 'form_submit'
412
+ const isSuccessEvent = event.event_type === 'form_success'
413
+ const normalizedEventType = isSubmitEvent || isSuccessEvent
414
+ ? 'form_submit'
415
+ : 'form_focus'
416
+ const submitSource = event.submit_source || (isSuccessEvent ? 'thank_you' : isSubmitEvent ? 'submit' : null)
417
+ queueEvent(normalizedEventType, {
282
418
  form_id: event.form_id,
283
- form_type: event.form_type,
284
- field_name: event.field_name,
285
- field_type: event.field_type,
286
- time_to_submit_ms: event.time_to_submit_ms,
287
- is_conversion: event.is_conversion,
419
+ form_provider: event.form_type || 'unknown',
420
+ form_field_type: event.field_type || null,
421
+ form_field_name: event.field_name || null,
422
+ form_event_type: event.event_type,
423
+ submit_source: submitSource,
424
+ is_inferred: isSuccessEvent,
425
+ time_to_submit_seconds: event.time_to_submit_ms ? Math.round(event.time_to_submit_ms / 1000) : null,
288
426
  })
289
427
  },
290
428
  })
@@ -310,7 +448,7 @@ function setupAdvancedBehavioralTracking(features: FeatureFlags): void {
310
448
  if (link && link.href) {
311
449
  const isExternal = link.hostname !== window.location.hostname
312
450
  queueEvent('click', {
313
- element: 'link',
451
+ element_type: 'link',
314
452
  href: truncateText(link.href, 200),
315
453
  text: truncateText(link.textContent || '', 100),
316
454
  is_external: isExternal,
@@ -324,16 +462,35 @@ function setupAdvancedBehavioralTracking(features: FeatureFlags): void {
324
462
  */
325
463
  function queueEvent(eventType: string, data: Record<string, unknown>): void {
326
464
  if (!eventQueue) return
327
-
328
- eventQueue.push(eventType, {
465
+ if (!config.apiKey) {
466
+ log('Missing apiKey, behavioral event skipped:', eventType)
467
+ return
468
+ }
469
+ if (!workspaceId) {
470
+ log('Missing workspaceId, behavioral event skipped:', eventType)
471
+ return
472
+ }
473
+ if (!sessionId) {
474
+ log('Missing sessionId, behavioral event skipped:', eventType)
475
+ return
476
+ }
477
+
478
+ const idempotencyKey = buildIdempotencyKey(eventType)
479
+ const payload: Record<string, unknown> = {
329
480
  visitor_id: visitorId,
330
481
  session_id: sessionId,
331
482
  event_type: eventType,
332
- ...data,
333
- url: window.location.href,
483
+ event_data: data,
484
+ page_url: window.location.href,
485
+ page_path: window.location.pathname,
334
486
  timestamp: new Date().toISOString(),
335
487
  tracker_version: VERSION,
336
- })
488
+ idempotency_key: idempotencyKey,
489
+ }
490
+
491
+ payload.workspace_id = workspaceId
492
+
493
+ eventQueue.push(eventType, payload, buildHeaders(idempotencyKey))
337
494
  }
338
495
 
339
496
  /**
@@ -379,39 +536,46 @@ function handleSPANavigation(event: NavigationEvent): void {
379
536
  */
380
537
  function setupUnloadHandlers(): void {
381
538
  const handleUnload = (): void => {
539
+ if (!workspaceId || !config.apiKey || !sessionId) return
540
+
382
541
  // Get final scroll depth
383
542
  const scrollEvent = scrollTracker?.getFinalEvent()
384
543
  if (scrollEvent) {
385
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
544
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
545
+ workspace_id: workspaceId,
386
546
  visitor_id: visitorId,
387
547
  session_id: sessionId,
388
548
  event_type: 'scroll_depth_final',
389
- data: scrollEvent,
390
- url: window.location.href,
549
+ event_data: {
550
+ scroll_depth: Math.round((scrollEvent.depth / 100) * 100) / 100,
551
+ milestone: Math.round((scrollEvent.chunk / 100) * 100) / 100,
552
+ time_to_reach_ms: scrollEvent.time_to_reach_ms,
553
+ },
554
+ page_url: window.location.href,
555
+ page_path: window.location.pathname,
556
+ timestamp: new Date().toISOString(),
557
+ tracker_version: VERSION,
558
+ idempotency_key: buildIdempotencyKey('scroll_depth_final'),
391
559
  })
392
560
  }
393
561
 
394
562
  // Get final time metrics
395
563
  const timeEvent = timeTracker?.getFinalMetrics()
396
564
  if (timeEvent) {
397
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
565
+ sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG.endpoints.behavioral)), {
566
+ workspace_id: workspaceId,
398
567
  visitor_id: visitorId,
399
568
  session_id: sessionId,
400
- event_type: 'time_spent_final',
401
- data: timeEvent,
402
- url: window.location.href,
403
- })
404
- }
405
-
406
- // Get agentic detection result
407
- const agenticResult = agenticAnalyzer?.getResult()
408
- if (agenticResult && agenticResult.agenticProbability > 0) {
409
- sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
410
- visitor_id: visitorId,
411
- session_id: sessionId,
412
- event_type: 'agentic_detection',
413
- data: agenticResult,
414
- url: window.location.href,
569
+ event_type: 'time_spent',
570
+ event_data: {
571
+ visible_time_ms: timeEvent.total_time_ms,
572
+ page_start_time: pageStartTime || Date.now(),
573
+ },
574
+ page_url: window.location.href,
575
+ page_path: window.location.pathname,
576
+ timestamp: new Date().toISOString(),
577
+ tracker_version: VERSION,
578
+ idempotency_key: buildIdempotencyKey('time_spent'),
415
579
  })
416
580
  }
417
581
 
@@ -443,33 +607,60 @@ function pageview(customUrl?: string): void {
443
607
  log('Not initialized, call init() first')
444
608
  return
445
609
  }
610
+ if (!config.apiKey) {
611
+ log('Missing apiKey, pageview skipped')
612
+ return
613
+ }
446
614
 
447
615
  const url = customUrl || window.location.href
448
- const payload = {
616
+ const utmParams = extractUTMParams(url)
617
+ const timestamp = new Date().toISOString()
618
+ const idempotencyKey = buildIdempotencyKey('visit')
619
+ const agenticResult = agenticAnalyzer?.getResult()
620
+ const pagePath = (() => {
621
+ try {
622
+ return new URL(url).pathname
623
+ } catch {
624
+ return window.location.pathname
625
+ }
626
+ })()
627
+
628
+ const payload: Record<string, unknown> = {
449
629
  visitor_id: visitorId,
450
630
  session_id: sessionId,
451
- url,
631
+ page_url: url,
632
+ page_path: pagePath,
452
633
  referrer: document.referrer || null,
453
634
  title: document.title || null,
454
- utm_source: extractUTMParams(url).utm_source || null,
455
- utm_medium: extractUTMParams(url).utm_medium || null,
456
- utm_campaign: extractUTMParams(url).utm_campaign || null,
635
+ utm_source: utmParams.utm_source || null,
636
+ utm_medium: utmParams.utm_medium || null,
637
+ utm_campaign: utmParams.utm_campaign || null,
638
+ utm_term: utmParams.utm_term || null,
639
+ utm_content: utmParams.utm_content || null,
457
640
  user_agent: navigator.userAgent,
458
641
  screen_width: window.screen?.width,
459
642
  screen_height: window.screen?.height,
460
643
  language: navigator.language,
461
644
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
462
645
  tracker_version: VERSION,
646
+ event_type: 'pageview',
647
+ event_data: null,
648
+ timestamp,
463
649
  navigation_timing: navigationTiming,
464
650
  ai_platform: aiDetection?.platform || null,
465
651
  is_ai_referrer: aiDetection?.isAI || false,
652
+ agentic_detection: agenticResult || null,
653
+ }
654
+
655
+ if (workspaceId) {
656
+ payload.workspace_id = workspaceId
466
657
  }
467
658
 
468
659
  log('Pageview:', payload)
469
660
 
470
661
  safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
471
662
  method: 'POST',
472
- headers: { 'Content-Type': 'application/json' },
663
+ headers: buildHeaders(idempotencyKey),
473
664
  body: JSON.stringify(payload),
474
665
  })
475
666
  }
@@ -482,8 +673,13 @@ function track(eventName: string, options: TrackEventOptions = {}): void {
482
673
  log('Not initialized, call init() first')
483
674
  return
484
675
  }
676
+ if (!config.apiKey) {
677
+ log('Missing apiKey, event skipped:', eventName)
678
+ return
679
+ }
485
680
 
486
- const payload = {
681
+ const idempotencyKey = buildIdempotencyKey(`event:${eventName}`)
682
+ const payload: Record<string, unknown> = {
487
683
  visitor_id: visitorId,
488
684
  session_id: sessionId,
489
685
  event_name: eventName,
@@ -491,16 +687,22 @@ function track(eventName: string, options: TrackEventOptions = {}): void {
491
687
  properties: options.properties || {},
492
688
  revenue: options.revenue,
493
689
  currency: options.currency || 'USD',
494
- url: window.location.href,
690
+ page_url: window.location.href,
691
+ referrer: document.referrer || null,
495
692
  timestamp: new Date().toISOString(),
496
693
  tracker_version: VERSION,
694
+ idempotency_key: idempotencyKey,
695
+ }
696
+
697
+ if (workspaceId) {
698
+ payload.workspace_id = workspaceId
497
699
  }
498
700
 
499
701
  log('Event:', eventName, payload)
500
702
 
501
703
  safeFetch(endpoint('/api/ingest/event'), {
502
704
  method: 'POST',
503
- headers: { 'Content-Type': 'application/json' },
705
+ headers: buildHeaders(idempotencyKey),
504
706
  body: JSON.stringify(payload),
505
707
  })
506
708
  }
@@ -520,20 +722,30 @@ function identify(userId: string, traits: Record<string, unknown> = {}): void {
520
722
  log('Not initialized, call init() first')
521
723
  return
522
724
  }
725
+ if (!config.apiKey) {
726
+ log('Missing apiKey, identify skipped')
727
+ return
728
+ }
523
729
 
524
730
  log('Identify:', userId, traits)
525
731
 
526
- const payload = {
732
+ const idempotencyKey = buildIdempotencyKey('identify')
733
+ const payload: Record<string, unknown> = {
527
734
  visitor_id: visitorId,
528
735
  session_id: sessionId,
529
736
  user_id: userId,
530
737
  traits,
531
738
  timestamp: new Date().toISOString(),
739
+ idempotency_key: idempotencyKey,
740
+ }
741
+
742
+ if (workspaceId) {
743
+ payload.workspace_id = workspaceId
532
744
  }
533
745
 
534
746
  safeFetch(endpoint('/api/ingest/identify'), {
535
747
  method: 'POST',
536
- headers: { 'Content-Type': 'application/json' },
748
+ headers: buildHeaders(idempotencyKey),
537
749
  body: JSON.stringify(payload),
538
750
  })
539
751
  }
@@ -738,15 +950,15 @@ function isTrackerInitialized(): boolean {
738
950
  * Used for monitoring and debugging
739
951
  */
740
952
  function reportHealth(status: 'initialized' | 'error' | 'ready', errorMessage?: string): void {
741
- if (!config.apiKey) return
742
-
743
953
  try {
744
954
  const healthData = {
745
- workspace_id: config.apiKey,
955
+ workspace_id: workspaceId,
956
+ visitor_id: visitorId,
957
+ session_id: sessionId,
746
958
  status,
747
959
  error_message: errorMessage || null,
748
- version: VERSION,
749
- url: typeof window !== 'undefined' ? window.location.href : null,
960
+ tracker_version: VERSION,
961
+ page_url: typeof window !== 'undefined' ? window.location.href : null,
750
962
  user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
751
963
  timestamp: new Date().toISOString(),
752
964
  features: {
package/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * See what AI tells your customers — and track when they click.
6
6
  *
7
7
  * @module @loamly/tracker
8
- * @version 1.8.0
8
+ * @version 2.1.0
9
9
  * @license MIT
10
10
  * @see https://github.com/loamly/loamly
11
11
  * @see https://loamly.ai