@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/README.md +4 -3
- package/dist/index.cjs +332 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.mjs +332 -119
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +349 -122
- package/dist/loamly.iife.global.js.map +1 -1
- package/dist/loamly.iife.min.global.js +1 -1
- package/dist/loamly.iife.min.global.js.map +1 -1
- package/package.json +1 -1
- package/src/behavioral/form-tracker.ts +5 -0
- package/src/browser.ts +25 -6
- package/src/config.ts +1 -1
- package/src/core.ts +313 -101
- package/src/index.ts +1 -1
- package/src/infrastructure/event-queue.ts +58 -32
- package/src/infrastructure/ping.ts +19 -3
- package/src/types.ts +3 -0
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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: '
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
|
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:
|
|
455
|
-
utm_medium:
|
|
456
|
-
utm_campaign:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
955
|
+
workspace_id: workspaceId,
|
|
956
|
+
visitor_id: visitorId,
|
|
957
|
+
session_id: sessionId,
|
|
746
958
|
status,
|
|
747
959
|
error_message: errorMessage || null,
|
|
748
|
-
|
|
749
|
-
|
|
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: {
|