@loamly/tracker 1.6.0 → 1.8.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.cjs +827 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +101 -2
- package/dist/index.d.ts +101 -2
- package/dist/index.mjs +825 -1
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +605 -1
- package/dist/loamly.iife.global.js.map +1 -1
- package/dist/loamly.iife.min.global.js +8 -1
- package/dist/loamly.iife.min.global.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +4 -1
- package/src/core.ts +201 -1
- package/src/detection/agentic-browser.ts +328 -0
- package/src/detection/behavioral-classifier.ts +489 -0
- package/src/detection/focus-blur.ts +251 -0
- package/src/detection/index.ts +21 -2
- package/src/index.ts +7 -0
- package/src/types.ts +38 -0
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/config.ts
|
|
2
|
-
var VERSION = "1.
|
|
2
|
+
var VERSION = "1.8.0";
|
|
3
3
|
var DEFAULT_CONFIG = {
|
|
4
4
|
apiHost: "https://app.loamly.ai",
|
|
5
5
|
endpoints: {
|
|
@@ -176,6 +176,467 @@ function detectAIFromUTM(url) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
// src/detection/behavioral-classifier.ts
|
|
180
|
+
var NAIVE_BAYES_WEIGHTS = {
|
|
181
|
+
human: {
|
|
182
|
+
time_to_first_click_delayed: 0.85,
|
|
183
|
+
time_to_first_click_normal: 0.75,
|
|
184
|
+
time_to_first_click_fast: 0.5,
|
|
185
|
+
time_to_first_click_immediate: 0.25,
|
|
186
|
+
scroll_speed_variable: 0.8,
|
|
187
|
+
scroll_speed_erratic: 0.7,
|
|
188
|
+
scroll_speed_uniform: 0.35,
|
|
189
|
+
scroll_speed_none: 0.45,
|
|
190
|
+
nav_timing_click: 0.75,
|
|
191
|
+
nav_timing_unknown: 0.55,
|
|
192
|
+
nav_timing_paste: 0.35,
|
|
193
|
+
has_referrer: 0.7,
|
|
194
|
+
no_referrer: 0.45,
|
|
195
|
+
homepage_landing: 0.65,
|
|
196
|
+
deep_landing: 0.5,
|
|
197
|
+
mouse_movement_curved: 0.9,
|
|
198
|
+
mouse_movement_linear: 0.3,
|
|
199
|
+
mouse_movement_none: 0.4,
|
|
200
|
+
form_fill_normal: 0.85,
|
|
201
|
+
form_fill_fast: 0.6,
|
|
202
|
+
form_fill_instant: 0.2,
|
|
203
|
+
focus_blur_normal: 0.75,
|
|
204
|
+
focus_blur_rapid: 0.45
|
|
205
|
+
},
|
|
206
|
+
ai_influenced: {
|
|
207
|
+
time_to_first_click_immediate: 0.75,
|
|
208
|
+
time_to_first_click_fast: 0.55,
|
|
209
|
+
time_to_first_click_normal: 0.4,
|
|
210
|
+
time_to_first_click_delayed: 0.35,
|
|
211
|
+
scroll_speed_none: 0.55,
|
|
212
|
+
scroll_speed_uniform: 0.7,
|
|
213
|
+
scroll_speed_variable: 0.35,
|
|
214
|
+
scroll_speed_erratic: 0.4,
|
|
215
|
+
nav_timing_paste: 0.75,
|
|
216
|
+
nav_timing_unknown: 0.5,
|
|
217
|
+
nav_timing_click: 0.35,
|
|
218
|
+
no_referrer: 0.65,
|
|
219
|
+
has_referrer: 0.4,
|
|
220
|
+
deep_landing: 0.6,
|
|
221
|
+
homepage_landing: 0.45,
|
|
222
|
+
mouse_movement_none: 0.6,
|
|
223
|
+
mouse_movement_linear: 0.75,
|
|
224
|
+
mouse_movement_curved: 0.25,
|
|
225
|
+
form_fill_instant: 0.8,
|
|
226
|
+
form_fill_fast: 0.55,
|
|
227
|
+
form_fill_normal: 0.3,
|
|
228
|
+
focus_blur_rapid: 0.6,
|
|
229
|
+
focus_blur_normal: 0.4
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
var PRIORS = {
|
|
233
|
+
human: 0.85,
|
|
234
|
+
ai_influenced: 0.15
|
|
235
|
+
};
|
|
236
|
+
var DEFAULT_WEIGHT = 0.5;
|
|
237
|
+
var BehavioralClassifier = class {
|
|
238
|
+
/**
|
|
239
|
+
* Create a new classifier
|
|
240
|
+
* @param minSessionTimeMs Minimum session time before classification (default: 10s)
|
|
241
|
+
*/
|
|
242
|
+
constructor(minSessionTimeMs = 1e4) {
|
|
243
|
+
this.classified = false;
|
|
244
|
+
this.result = null;
|
|
245
|
+
this.onClassify = null;
|
|
246
|
+
this.minSessionTime = minSessionTimeMs;
|
|
247
|
+
this.data = {
|
|
248
|
+
firstClickTime: null,
|
|
249
|
+
scrollEvents: [],
|
|
250
|
+
mouseEvents: [],
|
|
251
|
+
formEvents: [],
|
|
252
|
+
focusBlurEvents: [],
|
|
253
|
+
startTime: Date.now()
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Set callback for when classification completes
|
|
258
|
+
*/
|
|
259
|
+
setOnClassify(callback) {
|
|
260
|
+
this.onClassify = callback;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Record a click event
|
|
264
|
+
*/
|
|
265
|
+
recordClick() {
|
|
266
|
+
if (this.data.firstClickTime === null) {
|
|
267
|
+
this.data.firstClickTime = Date.now();
|
|
268
|
+
}
|
|
269
|
+
this.checkAndClassify();
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Record a scroll event
|
|
273
|
+
*/
|
|
274
|
+
recordScroll(position) {
|
|
275
|
+
this.data.scrollEvents.push({ time: Date.now(), position });
|
|
276
|
+
if (this.data.scrollEvents.length > 50) {
|
|
277
|
+
this.data.scrollEvents = this.data.scrollEvents.slice(-50);
|
|
278
|
+
}
|
|
279
|
+
this.checkAndClassify();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Record mouse movement
|
|
283
|
+
*/
|
|
284
|
+
recordMouse(x, y) {
|
|
285
|
+
this.data.mouseEvents.push({ time: Date.now(), x, y });
|
|
286
|
+
if (this.data.mouseEvents.length > 100) {
|
|
287
|
+
this.data.mouseEvents = this.data.mouseEvents.slice(-100);
|
|
288
|
+
}
|
|
289
|
+
this.checkAndClassify();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Record form field interaction start
|
|
293
|
+
*/
|
|
294
|
+
recordFormStart(fieldId) {
|
|
295
|
+
const existing = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
|
|
296
|
+
if (!existing) {
|
|
297
|
+
this.data.formEvents.push({ fieldId, startTime: Date.now(), endTime: 0 });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Record form field interaction end
|
|
302
|
+
*/
|
|
303
|
+
recordFormEnd(fieldId) {
|
|
304
|
+
const event = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
|
|
305
|
+
if (event) {
|
|
306
|
+
event.endTime = Date.now();
|
|
307
|
+
}
|
|
308
|
+
this.checkAndClassify();
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Record focus/blur event
|
|
312
|
+
*/
|
|
313
|
+
recordFocusBlur(type) {
|
|
314
|
+
this.data.focusBlurEvents.push({ type, time: Date.now() });
|
|
315
|
+
if (this.data.focusBlurEvents.length > 20) {
|
|
316
|
+
this.data.focusBlurEvents = this.data.focusBlurEvents.slice(-20);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Check if we have enough data and classify
|
|
321
|
+
*/
|
|
322
|
+
checkAndClassify() {
|
|
323
|
+
if (this.classified) return;
|
|
324
|
+
const sessionDuration = Date.now() - this.data.startTime;
|
|
325
|
+
if (sessionDuration < this.minSessionTime) return;
|
|
326
|
+
const hasData = this.data.scrollEvents.length >= 2 || this.data.mouseEvents.length >= 5 || this.data.firstClickTime !== null;
|
|
327
|
+
if (!hasData) return;
|
|
328
|
+
this.classify();
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Force classification (for beforeunload)
|
|
332
|
+
*/
|
|
333
|
+
forceClassify() {
|
|
334
|
+
if (this.classified) return this.result;
|
|
335
|
+
return this.classify();
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Perform classification
|
|
339
|
+
*/
|
|
340
|
+
classify() {
|
|
341
|
+
const sessionDuration = Date.now() - this.data.startTime;
|
|
342
|
+
const signals = this.extractSignals();
|
|
343
|
+
let humanLogProb = Math.log(PRIORS.human);
|
|
344
|
+
let aiLogProb = Math.log(PRIORS.ai_influenced);
|
|
345
|
+
for (const signal of signals) {
|
|
346
|
+
const humanWeight = NAIVE_BAYES_WEIGHTS.human[signal] ?? DEFAULT_WEIGHT;
|
|
347
|
+
const aiWeight = NAIVE_BAYES_WEIGHTS.ai_influenced[signal] ?? DEFAULT_WEIGHT;
|
|
348
|
+
humanLogProb += Math.log(humanWeight);
|
|
349
|
+
aiLogProb += Math.log(aiWeight);
|
|
350
|
+
}
|
|
351
|
+
const maxLog = Math.max(humanLogProb, aiLogProb);
|
|
352
|
+
const humanExp = Math.exp(humanLogProb - maxLog);
|
|
353
|
+
const aiExp = Math.exp(aiLogProb - maxLog);
|
|
354
|
+
const total = humanExp + aiExp;
|
|
355
|
+
const humanProbability = humanExp / total;
|
|
356
|
+
const aiProbability = aiExp / total;
|
|
357
|
+
let classification;
|
|
358
|
+
let confidence;
|
|
359
|
+
if (humanProbability > 0.6) {
|
|
360
|
+
classification = "human";
|
|
361
|
+
confidence = humanProbability;
|
|
362
|
+
} else if (aiProbability > 0.6) {
|
|
363
|
+
classification = "ai_influenced";
|
|
364
|
+
confidence = aiProbability;
|
|
365
|
+
} else {
|
|
366
|
+
classification = "uncertain";
|
|
367
|
+
confidence = Math.max(humanProbability, aiProbability);
|
|
368
|
+
}
|
|
369
|
+
this.result = {
|
|
370
|
+
classification,
|
|
371
|
+
humanProbability,
|
|
372
|
+
aiProbability,
|
|
373
|
+
confidence,
|
|
374
|
+
signals,
|
|
375
|
+
timestamp: Date.now(),
|
|
376
|
+
sessionDurationMs: sessionDuration
|
|
377
|
+
};
|
|
378
|
+
this.classified = true;
|
|
379
|
+
if (this.onClassify) {
|
|
380
|
+
this.onClassify(this.result);
|
|
381
|
+
}
|
|
382
|
+
return this.result;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Extract behavioral signals from collected data
|
|
386
|
+
*/
|
|
387
|
+
extractSignals() {
|
|
388
|
+
const signals = [];
|
|
389
|
+
if (this.data.firstClickTime !== null) {
|
|
390
|
+
const timeToClick = this.data.firstClickTime - this.data.startTime;
|
|
391
|
+
if (timeToClick < 500) {
|
|
392
|
+
signals.push("time_to_first_click_immediate");
|
|
393
|
+
} else if (timeToClick < 2e3) {
|
|
394
|
+
signals.push("time_to_first_click_fast");
|
|
395
|
+
} else if (timeToClick < 1e4) {
|
|
396
|
+
signals.push("time_to_first_click_normal");
|
|
397
|
+
} else {
|
|
398
|
+
signals.push("time_to_first_click_delayed");
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (this.data.scrollEvents.length === 0) {
|
|
402
|
+
signals.push("scroll_speed_none");
|
|
403
|
+
} else if (this.data.scrollEvents.length >= 3) {
|
|
404
|
+
const scrollDeltas = [];
|
|
405
|
+
for (let i = 1; i < this.data.scrollEvents.length; i++) {
|
|
406
|
+
const delta = this.data.scrollEvents[i].time - this.data.scrollEvents[i - 1].time;
|
|
407
|
+
scrollDeltas.push(delta);
|
|
408
|
+
}
|
|
409
|
+
const mean = scrollDeltas.reduce((a, b) => a + b, 0) / scrollDeltas.length;
|
|
410
|
+
const variance = scrollDeltas.reduce((sum, d) => sum + Math.pow(d - mean, 2), 0) / scrollDeltas.length;
|
|
411
|
+
const stdDev = Math.sqrt(variance);
|
|
412
|
+
const cv = mean > 0 ? stdDev / mean : 0;
|
|
413
|
+
if (cv < 0.2) {
|
|
414
|
+
signals.push("scroll_speed_uniform");
|
|
415
|
+
} else if (cv < 0.6) {
|
|
416
|
+
signals.push("scroll_speed_variable");
|
|
417
|
+
} else {
|
|
418
|
+
signals.push("scroll_speed_erratic");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (this.data.mouseEvents.length === 0) {
|
|
422
|
+
signals.push("mouse_movement_none");
|
|
423
|
+
} else if (this.data.mouseEvents.length >= 10) {
|
|
424
|
+
const n = Math.min(this.data.mouseEvents.length, 20);
|
|
425
|
+
const recentMouse = this.data.mouseEvents.slice(-n);
|
|
426
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
427
|
+
for (const event of recentMouse) {
|
|
428
|
+
sumX += event.x;
|
|
429
|
+
sumY += event.y;
|
|
430
|
+
sumXY += event.x * event.y;
|
|
431
|
+
sumX2 += event.x * event.x;
|
|
432
|
+
}
|
|
433
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
434
|
+
const slope = denominator !== 0 ? (n * sumXY - sumX * sumY) / denominator : 0;
|
|
435
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
436
|
+
let ssRes = 0, ssTot = 0;
|
|
437
|
+
const yMean = sumY / n;
|
|
438
|
+
for (const event of recentMouse) {
|
|
439
|
+
const yPred = slope * event.x + intercept;
|
|
440
|
+
ssRes += Math.pow(event.y - yPred, 2);
|
|
441
|
+
ssTot += Math.pow(event.y - yMean, 2);
|
|
442
|
+
}
|
|
443
|
+
const r2 = ssTot !== 0 ? 1 - ssRes / ssTot : 0;
|
|
444
|
+
if (r2 > 0.95) {
|
|
445
|
+
signals.push("mouse_movement_linear");
|
|
446
|
+
} else {
|
|
447
|
+
signals.push("mouse_movement_curved");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const completedForms = this.data.formEvents.filter((e) => e.endTime > 0);
|
|
451
|
+
if (completedForms.length > 0) {
|
|
452
|
+
const avgFillTime = completedForms.reduce((sum, e) => sum + (e.endTime - e.startTime), 0) / completedForms.length;
|
|
453
|
+
if (avgFillTime < 100) {
|
|
454
|
+
signals.push("form_fill_instant");
|
|
455
|
+
} else if (avgFillTime < 500) {
|
|
456
|
+
signals.push("form_fill_fast");
|
|
457
|
+
} else {
|
|
458
|
+
signals.push("form_fill_normal");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (this.data.focusBlurEvents.length >= 4) {
|
|
462
|
+
const recentFB = this.data.focusBlurEvents.slice(-10);
|
|
463
|
+
const intervals = [];
|
|
464
|
+
for (let i = 1; i < recentFB.length; i++) {
|
|
465
|
+
intervals.push(recentFB[i].time - recentFB[i - 1].time);
|
|
466
|
+
}
|
|
467
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
468
|
+
if (avgInterval < 1e3) {
|
|
469
|
+
signals.push("focus_blur_rapid");
|
|
470
|
+
} else {
|
|
471
|
+
signals.push("focus_blur_normal");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return signals;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Add context signals (set by tracker from external data)
|
|
478
|
+
*/
|
|
479
|
+
addContextSignal(_signal) {
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Get current result (null if not yet classified)
|
|
483
|
+
*/
|
|
484
|
+
getResult() {
|
|
485
|
+
return this.result;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Check if classification has been performed
|
|
489
|
+
*/
|
|
490
|
+
hasClassified() {
|
|
491
|
+
return this.classified;
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
// src/detection/focus-blur.ts
|
|
496
|
+
var FocusBlurAnalyzer = class {
|
|
497
|
+
constructor() {
|
|
498
|
+
this.sequence = [];
|
|
499
|
+
this.firstInteractionTime = null;
|
|
500
|
+
this.analyzed = false;
|
|
501
|
+
this.result = null;
|
|
502
|
+
this.pageLoadTime = performance.now();
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Initialize event tracking
|
|
506
|
+
* Must be called after DOM is ready
|
|
507
|
+
*/
|
|
508
|
+
initTracking() {
|
|
509
|
+
document.addEventListener("focus", (e) => {
|
|
510
|
+
this.recordEvent("focus", e.target);
|
|
511
|
+
}, true);
|
|
512
|
+
document.addEventListener("blur", (e) => {
|
|
513
|
+
this.recordEvent("blur", e.target);
|
|
514
|
+
}, true);
|
|
515
|
+
window.addEventListener("focus", () => {
|
|
516
|
+
this.recordEvent("window_focus", null);
|
|
517
|
+
});
|
|
518
|
+
window.addEventListener("blur", () => {
|
|
519
|
+
this.recordEvent("window_blur", null);
|
|
520
|
+
});
|
|
521
|
+
const recordFirstInteraction = () => {
|
|
522
|
+
if (this.firstInteractionTime === null) {
|
|
523
|
+
this.firstInteractionTime = performance.now();
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
document.addEventListener("click", recordFirstInteraction, { once: true, passive: true });
|
|
527
|
+
document.addEventListener("keydown", recordFirstInteraction, { once: true, passive: true });
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Record a focus/blur event
|
|
531
|
+
*/
|
|
532
|
+
recordEvent(type, target) {
|
|
533
|
+
const event = {
|
|
534
|
+
type,
|
|
535
|
+
target: target?.tagName || "WINDOW",
|
|
536
|
+
timestamp: performance.now()
|
|
537
|
+
};
|
|
538
|
+
this.sequence.push(event);
|
|
539
|
+
if (this.sequence.length > 20) {
|
|
540
|
+
this.sequence = this.sequence.slice(-20);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Analyze the focus/blur sequence for paste patterns
|
|
545
|
+
*/
|
|
546
|
+
analyze() {
|
|
547
|
+
if (this.analyzed && this.result) {
|
|
548
|
+
return this.result;
|
|
549
|
+
}
|
|
550
|
+
const signals = [];
|
|
551
|
+
let confidence = 0;
|
|
552
|
+
const earlyEvents = this.sequence.filter((e) => e.timestamp < this.pageLoadTime + 500);
|
|
553
|
+
const hasEarlyWindowFocus = earlyEvents.some((e) => e.type === "window_focus");
|
|
554
|
+
if (hasEarlyWindowFocus) {
|
|
555
|
+
signals.push("early_window_focus");
|
|
556
|
+
confidence += 0.15;
|
|
557
|
+
}
|
|
558
|
+
const hasEarlyBodyFocus = earlyEvents.some(
|
|
559
|
+
(e) => e.type === "focus" && e.target === "BODY"
|
|
560
|
+
);
|
|
561
|
+
if (hasEarlyBodyFocus) {
|
|
562
|
+
signals.push("early_body_focus");
|
|
563
|
+
confidence += 0.15;
|
|
564
|
+
}
|
|
565
|
+
const hasLinkFocus = this.sequence.some(
|
|
566
|
+
(e) => e.type === "focus" && e.target === "A"
|
|
567
|
+
);
|
|
568
|
+
if (!hasLinkFocus) {
|
|
569
|
+
signals.push("no_link_focus");
|
|
570
|
+
confidence += 0.1;
|
|
571
|
+
}
|
|
572
|
+
const firstFocus = this.sequence.find((e) => e.type === "focus");
|
|
573
|
+
if (firstFocus && (firstFocus.target === "BODY" || firstFocus.target === "HTML")) {
|
|
574
|
+
signals.push("first_focus_body");
|
|
575
|
+
confidence += 0.1;
|
|
576
|
+
}
|
|
577
|
+
const windowEvents = this.sequence.filter(
|
|
578
|
+
(e) => e.type === "window_focus" || e.type === "window_blur"
|
|
579
|
+
);
|
|
580
|
+
if (windowEvents.length <= 2) {
|
|
581
|
+
signals.push("minimal_window_switches");
|
|
582
|
+
confidence += 0.05;
|
|
583
|
+
}
|
|
584
|
+
if (this.firstInteractionTime !== null) {
|
|
585
|
+
const timeToInteraction = this.firstInteractionTime - this.pageLoadTime;
|
|
586
|
+
if (timeToInteraction > 3e3) {
|
|
587
|
+
signals.push("delayed_first_interaction");
|
|
588
|
+
confidence += 0.1;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
confidence = Math.min(confidence, 0.65);
|
|
592
|
+
let navType;
|
|
593
|
+
if (confidence >= 0.35) {
|
|
594
|
+
navType = "likely_paste";
|
|
595
|
+
} else if (signals.length === 0) {
|
|
596
|
+
navType = "unknown";
|
|
597
|
+
} else {
|
|
598
|
+
navType = "likely_click";
|
|
599
|
+
}
|
|
600
|
+
this.result = {
|
|
601
|
+
nav_type: navType,
|
|
602
|
+
confidence,
|
|
603
|
+
signals,
|
|
604
|
+
sequence: this.sequence.slice(-10),
|
|
605
|
+
time_to_first_interaction_ms: this.firstInteractionTime ? Math.round(this.firstInteractionTime - this.pageLoadTime) : null
|
|
606
|
+
};
|
|
607
|
+
this.analyzed = true;
|
|
608
|
+
return this.result;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Get current result (analyze if not done)
|
|
612
|
+
*/
|
|
613
|
+
getResult() {
|
|
614
|
+
return this.analyze();
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Check if analysis has been performed
|
|
618
|
+
*/
|
|
619
|
+
hasAnalyzed() {
|
|
620
|
+
return this.analyzed;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Get the raw sequence for debugging
|
|
624
|
+
*/
|
|
625
|
+
getSequence() {
|
|
626
|
+
return [...this.sequence];
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Reset the analyzer
|
|
630
|
+
*/
|
|
631
|
+
reset() {
|
|
632
|
+
this.sequence = [];
|
|
633
|
+
this.pageLoadTime = performance.now();
|
|
634
|
+
this.firstInteractionTime = null;
|
|
635
|
+
this.analyzed = false;
|
|
636
|
+
this.result = null;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
|
|
179
640
|
// src/utils.ts
|
|
180
641
|
function generateUUID() {
|
|
181
642
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
@@ -261,6 +722,10 @@ var sessionId = null;
|
|
|
261
722
|
var sessionStartTime = null;
|
|
262
723
|
var navigationTiming = null;
|
|
263
724
|
var aiDetection = null;
|
|
725
|
+
var behavioralClassifier = null;
|
|
726
|
+
var behavioralMLResult = null;
|
|
727
|
+
var focusBlurAnalyzer = null;
|
|
728
|
+
var focusBlurResult = null;
|
|
264
729
|
function log(...args) {
|
|
265
730
|
if (debugMode) {
|
|
266
731
|
console.log("[Loamly]", ...args);
|
|
@@ -300,6 +765,16 @@ function init(userConfig = {}) {
|
|
|
300
765
|
if (!userConfig.disableBehavioral) {
|
|
301
766
|
setupBehavioralTracking();
|
|
302
767
|
}
|
|
768
|
+
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
769
|
+
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
770
|
+
setupBehavioralMLTracking();
|
|
771
|
+
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
772
|
+
focusBlurAnalyzer.initTracking();
|
|
773
|
+
setTimeout(() => {
|
|
774
|
+
if (focusBlurAnalyzer) {
|
|
775
|
+
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
776
|
+
}
|
|
777
|
+
}, 5e3);
|
|
303
778
|
log("Initialization complete");
|
|
304
779
|
}
|
|
305
780
|
function pageview(customUrl) {
|
|
@@ -481,6 +956,116 @@ function sendBehavioralEvent(eventType, data) {
|
|
|
481
956
|
body: JSON.stringify(payload)
|
|
482
957
|
});
|
|
483
958
|
}
|
|
959
|
+
function setupBehavioralMLTracking() {
|
|
960
|
+
if (!behavioralClassifier) return;
|
|
961
|
+
let mouseSampleCount = 0;
|
|
962
|
+
document.addEventListener("mousemove", (e) => {
|
|
963
|
+
mouseSampleCount++;
|
|
964
|
+
if (mouseSampleCount % 10 === 0 && behavioralClassifier) {
|
|
965
|
+
behavioralClassifier.recordMouse(e.clientX, e.clientY);
|
|
966
|
+
}
|
|
967
|
+
}, { passive: true });
|
|
968
|
+
document.addEventListener("click", () => {
|
|
969
|
+
if (behavioralClassifier) {
|
|
970
|
+
behavioralClassifier.recordClick();
|
|
971
|
+
}
|
|
972
|
+
}, { passive: true });
|
|
973
|
+
let lastScrollY = 0;
|
|
974
|
+
document.addEventListener("scroll", () => {
|
|
975
|
+
const currentY = window.scrollY;
|
|
976
|
+
if (Math.abs(currentY - lastScrollY) > 50 && behavioralClassifier) {
|
|
977
|
+
lastScrollY = currentY;
|
|
978
|
+
behavioralClassifier.recordScroll(currentY);
|
|
979
|
+
}
|
|
980
|
+
}, { passive: true });
|
|
981
|
+
document.addEventListener("focusin", (e) => {
|
|
982
|
+
if (behavioralClassifier) {
|
|
983
|
+
behavioralClassifier.recordFocusBlur("focus");
|
|
984
|
+
const target = e.target;
|
|
985
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
986
|
+
behavioralClassifier.recordFormStart(target.id || target.getAttribute("name") || "unknown");
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}, { passive: true });
|
|
990
|
+
document.addEventListener("focusout", (e) => {
|
|
991
|
+
if (behavioralClassifier) {
|
|
992
|
+
behavioralClassifier.recordFocusBlur("blur");
|
|
993
|
+
const target = e.target;
|
|
994
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
995
|
+
behavioralClassifier.recordFormEnd(target.id || target.getAttribute("name") || "unknown");
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}, { passive: true });
|
|
999
|
+
window.addEventListener("beforeunload", () => {
|
|
1000
|
+
if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
|
|
1001
|
+
const result = behavioralClassifier.forceClassify();
|
|
1002
|
+
if (result) {
|
|
1003
|
+
handleBehavioralClassification(result);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
setTimeout(() => {
|
|
1008
|
+
if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
|
|
1009
|
+
behavioralClassifier.forceClassify();
|
|
1010
|
+
}
|
|
1011
|
+
}, 3e4);
|
|
1012
|
+
}
|
|
1013
|
+
function handleBehavioralClassification(result) {
|
|
1014
|
+
log("Behavioral ML classification:", result);
|
|
1015
|
+
behavioralMLResult = {
|
|
1016
|
+
classification: result.classification,
|
|
1017
|
+
humanProbability: result.humanProbability,
|
|
1018
|
+
aiProbability: result.aiProbability,
|
|
1019
|
+
confidence: result.confidence,
|
|
1020
|
+
signals: result.signals,
|
|
1021
|
+
sessionDurationMs: result.sessionDurationMs
|
|
1022
|
+
};
|
|
1023
|
+
sendBehavioralEvent("ml_classification", {
|
|
1024
|
+
classification: result.classification,
|
|
1025
|
+
human_probability: result.humanProbability,
|
|
1026
|
+
ai_probability: result.aiProbability,
|
|
1027
|
+
confidence: result.confidence,
|
|
1028
|
+
signals: result.signals,
|
|
1029
|
+
session_duration_ms: result.sessionDurationMs,
|
|
1030
|
+
navigation_timing: navigationTiming,
|
|
1031
|
+
ai_detection: aiDetection,
|
|
1032
|
+
focus_blur: focusBlurResult
|
|
1033
|
+
});
|
|
1034
|
+
if (result.classification === "ai_influenced" && result.confidence >= 0.7) {
|
|
1035
|
+
aiDetection = {
|
|
1036
|
+
isAI: true,
|
|
1037
|
+
confidence: result.confidence,
|
|
1038
|
+
method: "behavioral"
|
|
1039
|
+
};
|
|
1040
|
+
log("AI detection updated from behavioral ML:", aiDetection);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function handleFocusBlurAnalysis(result) {
|
|
1044
|
+
log("Focus/blur analysis:", result);
|
|
1045
|
+
focusBlurResult = {
|
|
1046
|
+
navType: result.nav_type,
|
|
1047
|
+
confidence: result.confidence,
|
|
1048
|
+
signals: result.signals,
|
|
1049
|
+
timeToFirstInteractionMs: result.time_to_first_interaction_ms
|
|
1050
|
+
};
|
|
1051
|
+
sendBehavioralEvent("focus_blur_analysis", {
|
|
1052
|
+
nav_type: result.nav_type,
|
|
1053
|
+
confidence: result.confidence,
|
|
1054
|
+
signals: result.signals,
|
|
1055
|
+
time_to_first_interaction_ms: result.time_to_first_interaction_ms,
|
|
1056
|
+
sequence_length: result.sequence.length
|
|
1057
|
+
});
|
|
1058
|
+
if (result.nav_type === "likely_paste" && result.confidence >= 0.4) {
|
|
1059
|
+
if (!aiDetection || aiDetection.confidence < result.confidence) {
|
|
1060
|
+
aiDetection = {
|
|
1061
|
+
isAI: true,
|
|
1062
|
+
confidence: result.confidence,
|
|
1063
|
+
method: "behavioral"
|
|
1064
|
+
};
|
|
1065
|
+
log("AI detection updated from focus/blur analysis:", aiDetection);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
484
1069
|
function getCurrentSessionId() {
|
|
485
1070
|
return sessionId;
|
|
486
1071
|
}
|
|
@@ -493,6 +1078,12 @@ function getAIDetectionResult() {
|
|
|
493
1078
|
function getNavigationTimingResult() {
|
|
494
1079
|
return navigationTiming;
|
|
495
1080
|
}
|
|
1081
|
+
function getBehavioralMLResult() {
|
|
1082
|
+
return behavioralMLResult;
|
|
1083
|
+
}
|
|
1084
|
+
function getFocusBlurResult() {
|
|
1085
|
+
return focusBlurResult;
|
|
1086
|
+
}
|
|
496
1087
|
function isTrackerInitialized() {
|
|
497
1088
|
return initialized;
|
|
498
1089
|
}
|
|
@@ -504,6 +1095,10 @@ function reset() {
|
|
|
504
1095
|
sessionStartTime = null;
|
|
505
1096
|
navigationTiming = null;
|
|
506
1097
|
aiDetection = null;
|
|
1098
|
+
behavioralClassifier = null;
|
|
1099
|
+
behavioralMLResult = null;
|
|
1100
|
+
focusBlurAnalyzer = null;
|
|
1101
|
+
focusBlurResult = null;
|
|
507
1102
|
try {
|
|
508
1103
|
sessionStorage.removeItem("loamly_session");
|
|
509
1104
|
sessionStorage.removeItem("loamly_start");
|
|
@@ -524,20 +1119,247 @@ var loamly = {
|
|
|
524
1119
|
getVisitorId: getCurrentVisitorId,
|
|
525
1120
|
getAIDetection: getAIDetectionResult,
|
|
526
1121
|
getNavigationTiming: getNavigationTimingResult,
|
|
1122
|
+
getBehavioralML: getBehavioralMLResult,
|
|
1123
|
+
getFocusBlur: getFocusBlurResult,
|
|
527
1124
|
isInitialized: isTrackerInitialized,
|
|
528
1125
|
reset,
|
|
529
1126
|
debug: setDebug
|
|
530
1127
|
};
|
|
1128
|
+
|
|
1129
|
+
// src/detection/agentic-browser.ts
|
|
1130
|
+
var CometDetector = class {
|
|
1131
|
+
constructor() {
|
|
1132
|
+
this.detected = false;
|
|
1133
|
+
this.checkComplete = false;
|
|
1134
|
+
this.observer = null;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Initialize detection
|
|
1138
|
+
* @param timeout - Max time to observe for Comet DOM (default: 5s)
|
|
1139
|
+
*/
|
|
1140
|
+
init(timeout = 5e3) {
|
|
1141
|
+
if (typeof document === "undefined") return;
|
|
1142
|
+
this.check();
|
|
1143
|
+
if (!this.detected && document.body) {
|
|
1144
|
+
this.observer = new MutationObserver(() => this.check());
|
|
1145
|
+
this.observer.observe(document.body, { childList: true, subtree: true });
|
|
1146
|
+
setTimeout(() => {
|
|
1147
|
+
if (this.observer && !this.detected) {
|
|
1148
|
+
this.observer.disconnect();
|
|
1149
|
+
this.observer = null;
|
|
1150
|
+
this.checkComplete = true;
|
|
1151
|
+
}
|
|
1152
|
+
}, timeout);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
check() {
|
|
1156
|
+
if (document.querySelector(".pplx-agent-overlay-stop-button")) {
|
|
1157
|
+
this.detected = true;
|
|
1158
|
+
this.checkComplete = true;
|
|
1159
|
+
if (this.observer) {
|
|
1160
|
+
this.observer.disconnect();
|
|
1161
|
+
this.observer = null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
isDetected() {
|
|
1166
|
+
return this.detected;
|
|
1167
|
+
}
|
|
1168
|
+
isCheckComplete() {
|
|
1169
|
+
return this.checkComplete;
|
|
1170
|
+
}
|
|
1171
|
+
destroy() {
|
|
1172
|
+
if (this.observer) {
|
|
1173
|
+
this.observer.disconnect();
|
|
1174
|
+
this.observer = null;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
};
|
|
1178
|
+
var MouseAnalyzer = class {
|
|
1179
|
+
/**
|
|
1180
|
+
* @param teleportThreshold - Distance in pixels to consider a teleport (default: 500)
|
|
1181
|
+
*/
|
|
1182
|
+
constructor(teleportThreshold = 500) {
|
|
1183
|
+
this.lastX = -1;
|
|
1184
|
+
this.lastY = -1;
|
|
1185
|
+
this.teleportingClicks = 0;
|
|
1186
|
+
this.totalMovements = 0;
|
|
1187
|
+
this.handleMove = (e) => {
|
|
1188
|
+
this.totalMovements++;
|
|
1189
|
+
this.lastX = e.clientX;
|
|
1190
|
+
this.lastY = e.clientY;
|
|
1191
|
+
};
|
|
1192
|
+
this.handleClick = (e) => {
|
|
1193
|
+
if (this.lastX !== -1 && this.lastY !== -1) {
|
|
1194
|
+
const dx = Math.abs(e.clientX - this.lastX);
|
|
1195
|
+
const dy = Math.abs(e.clientY - this.lastY);
|
|
1196
|
+
if (dx > this.teleportThreshold || dy > this.teleportThreshold) {
|
|
1197
|
+
this.teleportingClicks++;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
this.lastX = e.clientX;
|
|
1201
|
+
this.lastY = e.clientY;
|
|
1202
|
+
};
|
|
1203
|
+
this.teleportThreshold = teleportThreshold;
|
|
1204
|
+
}
|
|
1205
|
+
/**
|
|
1206
|
+
* Initialize mouse tracking
|
|
1207
|
+
*/
|
|
1208
|
+
init() {
|
|
1209
|
+
if (typeof document === "undefined") return;
|
|
1210
|
+
document.addEventListener("mousemove", this.handleMove, { passive: true });
|
|
1211
|
+
document.addEventListener("mousedown", this.handleClick, { passive: true });
|
|
1212
|
+
}
|
|
1213
|
+
getPatterns() {
|
|
1214
|
+
return {
|
|
1215
|
+
teleportingClicks: this.teleportingClicks,
|
|
1216
|
+
totalMovements: this.totalMovements
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
destroy() {
|
|
1220
|
+
if (typeof document === "undefined") return;
|
|
1221
|
+
document.removeEventListener("mousemove", this.handleMove);
|
|
1222
|
+
document.removeEventListener("mousedown", this.handleClick);
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
var CDPDetector = class {
|
|
1226
|
+
constructor() {
|
|
1227
|
+
this.detected = false;
|
|
1228
|
+
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Run detection checks
|
|
1231
|
+
*/
|
|
1232
|
+
detect() {
|
|
1233
|
+
if (typeof navigator === "undefined") return false;
|
|
1234
|
+
if (navigator.webdriver) {
|
|
1235
|
+
this.detected = true;
|
|
1236
|
+
return true;
|
|
1237
|
+
}
|
|
1238
|
+
if (typeof window !== "undefined") {
|
|
1239
|
+
const win = window;
|
|
1240
|
+
const automationProps = [
|
|
1241
|
+
"__webdriver_evaluate",
|
|
1242
|
+
"__selenium_evaluate",
|
|
1243
|
+
"__webdriver_script_function",
|
|
1244
|
+
"__webdriver_script_func",
|
|
1245
|
+
"__webdriver_script_fn",
|
|
1246
|
+
"__fxdriver_evaluate",
|
|
1247
|
+
"__driver_unwrapped",
|
|
1248
|
+
"__webdriver_unwrapped",
|
|
1249
|
+
"__driver_evaluate",
|
|
1250
|
+
"__selenium_unwrapped",
|
|
1251
|
+
"__fxdriver_unwrapped"
|
|
1252
|
+
];
|
|
1253
|
+
for (const prop of automationProps) {
|
|
1254
|
+
if (prop in win) {
|
|
1255
|
+
this.detected = true;
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
isDetected() {
|
|
1263
|
+
return this.detected;
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
var AgenticBrowserAnalyzer = class {
|
|
1267
|
+
constructor() {
|
|
1268
|
+
this.initialized = false;
|
|
1269
|
+
this.cometDetector = new CometDetector();
|
|
1270
|
+
this.mouseAnalyzer = new MouseAnalyzer();
|
|
1271
|
+
this.cdpDetector = new CDPDetector();
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Initialize all detectors
|
|
1275
|
+
*/
|
|
1276
|
+
init() {
|
|
1277
|
+
if (this.initialized) return;
|
|
1278
|
+
this.initialized = true;
|
|
1279
|
+
this.cometDetector.init();
|
|
1280
|
+
this.mouseAnalyzer.init();
|
|
1281
|
+
this.cdpDetector.detect();
|
|
1282
|
+
}
|
|
1283
|
+
/**
|
|
1284
|
+
* Get current detection result
|
|
1285
|
+
*/
|
|
1286
|
+
getResult() {
|
|
1287
|
+
const signals = [];
|
|
1288
|
+
let probability = 0;
|
|
1289
|
+
if (this.cometDetector.isDetected()) {
|
|
1290
|
+
signals.push("comet_dom_detected");
|
|
1291
|
+
probability = Math.max(probability, 0.85);
|
|
1292
|
+
}
|
|
1293
|
+
if (this.cdpDetector.isDetected()) {
|
|
1294
|
+
signals.push("cdp_detected");
|
|
1295
|
+
probability = Math.max(probability, 0.92);
|
|
1296
|
+
}
|
|
1297
|
+
const mousePatterns = this.mouseAnalyzer.getPatterns();
|
|
1298
|
+
if (mousePatterns.teleportingClicks > 0) {
|
|
1299
|
+
signals.push(`teleporting_clicks:${mousePatterns.teleportingClicks}`);
|
|
1300
|
+
probability = Math.max(probability, 0.78);
|
|
1301
|
+
}
|
|
1302
|
+
return {
|
|
1303
|
+
cometDOMDetected: this.cometDetector.isDetected(),
|
|
1304
|
+
cdpDetected: this.cdpDetector.isDetected(),
|
|
1305
|
+
mousePatterns,
|
|
1306
|
+
agenticProbability: probability,
|
|
1307
|
+
signals
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
/**
|
|
1311
|
+
* Cleanup resources
|
|
1312
|
+
*/
|
|
1313
|
+
destroy() {
|
|
1314
|
+
this.cometDetector.destroy();
|
|
1315
|
+
this.mouseAnalyzer.destroy();
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
function createAgenticAnalyzer() {
|
|
1319
|
+
const analyzer = new AgenticBrowserAnalyzer();
|
|
1320
|
+
if (typeof document !== "undefined") {
|
|
1321
|
+
if (document.readyState === "loading") {
|
|
1322
|
+
document.addEventListener("DOMContentLoaded", () => analyzer.init());
|
|
1323
|
+
} else {
|
|
1324
|
+
analyzer.init();
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
return analyzer;
|
|
1328
|
+
}
|
|
531
1329
|
export {
|
|
532
1330
|
AI_BOT_PATTERNS,
|
|
533
1331
|
AI_PLATFORMS,
|
|
1332
|
+
AgenticBrowserAnalyzer,
|
|
534
1333
|
VERSION,
|
|
1334
|
+
createAgenticAnalyzer,
|
|
535
1335
|
loamly as default,
|
|
536
1336
|
detectAIFromReferrer,
|
|
537
1337
|
detectAIFromUTM,
|
|
538
1338
|
detectNavigationType,
|
|
539
1339
|
loamly
|
|
540
1340
|
};
|
|
1341
|
+
/**
|
|
1342
|
+
* Loamly Tracker Configuration
|
|
1343
|
+
*
|
|
1344
|
+
* @module @loamly/tracker
|
|
1345
|
+
* @license MIT
|
|
1346
|
+
* @see https://github.com/loamly/loamly
|
|
1347
|
+
*/
|
|
1348
|
+
/**
|
|
1349
|
+
* Agentic Browser Detection
|
|
1350
|
+
*
|
|
1351
|
+
* LOA-187: Detects AI agentic browsers like Perplexity Comet, ChatGPT Atlas,
|
|
1352
|
+
* and other automated browsing agents.
|
|
1353
|
+
*
|
|
1354
|
+
* Detection methods:
|
|
1355
|
+
* - DOM fingerprinting (Perplexity Comet overlay)
|
|
1356
|
+
* - Mouse movement patterns (teleporting clicks)
|
|
1357
|
+
* - CDP (Chrome DevTools Protocol) automation fingerprint
|
|
1358
|
+
* - navigator.webdriver detection
|
|
1359
|
+
*
|
|
1360
|
+
* @module @loamly/tracker/detection/agentic-browser
|
|
1361
|
+
* @license MIT
|
|
1362
|
+
*/
|
|
541
1363
|
/**
|
|
542
1364
|
* Loamly Tracker
|
|
543
1365
|
*
|
|
@@ -545,7 +1367,9 @@ export {
|
|
|
545
1367
|
* See what AI tells your customers — and track when they click.
|
|
546
1368
|
*
|
|
547
1369
|
* @module @loamly/tracker
|
|
1370
|
+
* @version 1.8.0
|
|
548
1371
|
* @license MIT
|
|
1372
|
+
* @see https://github.com/loamly/loamly
|
|
549
1373
|
* @see https://loamly.ai
|
|
550
1374
|
*/
|
|
551
1375
|
//# sourceMappingURL=index.mjs.map
|