@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
|
@@ -26,7 +26,7 @@ var Loamly = (() => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
// src/config.ts
|
|
29
|
-
var VERSION = "1.
|
|
29
|
+
var VERSION = "1.8.0";
|
|
30
30
|
var DEFAULT_CONFIG = {
|
|
31
31
|
apiHost: "https://app.loamly.ai",
|
|
32
32
|
endpoints: {
|
|
@@ -191,6 +191,467 @@ var Loamly = (() => {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// src/detection/behavioral-classifier.ts
|
|
195
|
+
var NAIVE_BAYES_WEIGHTS = {
|
|
196
|
+
human: {
|
|
197
|
+
time_to_first_click_delayed: 0.85,
|
|
198
|
+
time_to_first_click_normal: 0.75,
|
|
199
|
+
time_to_first_click_fast: 0.5,
|
|
200
|
+
time_to_first_click_immediate: 0.25,
|
|
201
|
+
scroll_speed_variable: 0.8,
|
|
202
|
+
scroll_speed_erratic: 0.7,
|
|
203
|
+
scroll_speed_uniform: 0.35,
|
|
204
|
+
scroll_speed_none: 0.45,
|
|
205
|
+
nav_timing_click: 0.75,
|
|
206
|
+
nav_timing_unknown: 0.55,
|
|
207
|
+
nav_timing_paste: 0.35,
|
|
208
|
+
has_referrer: 0.7,
|
|
209
|
+
no_referrer: 0.45,
|
|
210
|
+
homepage_landing: 0.65,
|
|
211
|
+
deep_landing: 0.5,
|
|
212
|
+
mouse_movement_curved: 0.9,
|
|
213
|
+
mouse_movement_linear: 0.3,
|
|
214
|
+
mouse_movement_none: 0.4,
|
|
215
|
+
form_fill_normal: 0.85,
|
|
216
|
+
form_fill_fast: 0.6,
|
|
217
|
+
form_fill_instant: 0.2,
|
|
218
|
+
focus_blur_normal: 0.75,
|
|
219
|
+
focus_blur_rapid: 0.45
|
|
220
|
+
},
|
|
221
|
+
ai_influenced: {
|
|
222
|
+
time_to_first_click_immediate: 0.75,
|
|
223
|
+
time_to_first_click_fast: 0.55,
|
|
224
|
+
time_to_first_click_normal: 0.4,
|
|
225
|
+
time_to_first_click_delayed: 0.35,
|
|
226
|
+
scroll_speed_none: 0.55,
|
|
227
|
+
scroll_speed_uniform: 0.7,
|
|
228
|
+
scroll_speed_variable: 0.35,
|
|
229
|
+
scroll_speed_erratic: 0.4,
|
|
230
|
+
nav_timing_paste: 0.75,
|
|
231
|
+
nav_timing_unknown: 0.5,
|
|
232
|
+
nav_timing_click: 0.35,
|
|
233
|
+
no_referrer: 0.65,
|
|
234
|
+
has_referrer: 0.4,
|
|
235
|
+
deep_landing: 0.6,
|
|
236
|
+
homepage_landing: 0.45,
|
|
237
|
+
mouse_movement_none: 0.6,
|
|
238
|
+
mouse_movement_linear: 0.75,
|
|
239
|
+
mouse_movement_curved: 0.25,
|
|
240
|
+
form_fill_instant: 0.8,
|
|
241
|
+
form_fill_fast: 0.55,
|
|
242
|
+
form_fill_normal: 0.3,
|
|
243
|
+
focus_blur_rapid: 0.6,
|
|
244
|
+
focus_blur_normal: 0.4
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
var PRIORS = {
|
|
248
|
+
human: 0.85,
|
|
249
|
+
ai_influenced: 0.15
|
|
250
|
+
};
|
|
251
|
+
var DEFAULT_WEIGHT = 0.5;
|
|
252
|
+
var BehavioralClassifier = class {
|
|
253
|
+
/**
|
|
254
|
+
* Create a new classifier
|
|
255
|
+
* @param minSessionTimeMs Minimum session time before classification (default: 10s)
|
|
256
|
+
*/
|
|
257
|
+
constructor(minSessionTimeMs = 1e4) {
|
|
258
|
+
this.classified = false;
|
|
259
|
+
this.result = null;
|
|
260
|
+
this.onClassify = null;
|
|
261
|
+
this.minSessionTime = minSessionTimeMs;
|
|
262
|
+
this.data = {
|
|
263
|
+
firstClickTime: null,
|
|
264
|
+
scrollEvents: [],
|
|
265
|
+
mouseEvents: [],
|
|
266
|
+
formEvents: [],
|
|
267
|
+
focusBlurEvents: [],
|
|
268
|
+
startTime: Date.now()
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Set callback for when classification completes
|
|
273
|
+
*/
|
|
274
|
+
setOnClassify(callback) {
|
|
275
|
+
this.onClassify = callback;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Record a click event
|
|
279
|
+
*/
|
|
280
|
+
recordClick() {
|
|
281
|
+
if (this.data.firstClickTime === null) {
|
|
282
|
+
this.data.firstClickTime = Date.now();
|
|
283
|
+
}
|
|
284
|
+
this.checkAndClassify();
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Record a scroll event
|
|
288
|
+
*/
|
|
289
|
+
recordScroll(position) {
|
|
290
|
+
this.data.scrollEvents.push({ time: Date.now(), position });
|
|
291
|
+
if (this.data.scrollEvents.length > 50) {
|
|
292
|
+
this.data.scrollEvents = this.data.scrollEvents.slice(-50);
|
|
293
|
+
}
|
|
294
|
+
this.checkAndClassify();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Record mouse movement
|
|
298
|
+
*/
|
|
299
|
+
recordMouse(x, y) {
|
|
300
|
+
this.data.mouseEvents.push({ time: Date.now(), x, y });
|
|
301
|
+
if (this.data.mouseEvents.length > 100) {
|
|
302
|
+
this.data.mouseEvents = this.data.mouseEvents.slice(-100);
|
|
303
|
+
}
|
|
304
|
+
this.checkAndClassify();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Record form field interaction start
|
|
308
|
+
*/
|
|
309
|
+
recordFormStart(fieldId) {
|
|
310
|
+
const existing = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
|
|
311
|
+
if (!existing) {
|
|
312
|
+
this.data.formEvents.push({ fieldId, startTime: Date.now(), endTime: 0 });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Record form field interaction end
|
|
317
|
+
*/
|
|
318
|
+
recordFormEnd(fieldId) {
|
|
319
|
+
const event = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
|
|
320
|
+
if (event) {
|
|
321
|
+
event.endTime = Date.now();
|
|
322
|
+
}
|
|
323
|
+
this.checkAndClassify();
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Record focus/blur event
|
|
327
|
+
*/
|
|
328
|
+
recordFocusBlur(type) {
|
|
329
|
+
this.data.focusBlurEvents.push({ type, time: Date.now() });
|
|
330
|
+
if (this.data.focusBlurEvents.length > 20) {
|
|
331
|
+
this.data.focusBlurEvents = this.data.focusBlurEvents.slice(-20);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Check if we have enough data and classify
|
|
336
|
+
*/
|
|
337
|
+
checkAndClassify() {
|
|
338
|
+
if (this.classified) return;
|
|
339
|
+
const sessionDuration = Date.now() - this.data.startTime;
|
|
340
|
+
if (sessionDuration < this.minSessionTime) return;
|
|
341
|
+
const hasData = this.data.scrollEvents.length >= 2 || this.data.mouseEvents.length >= 5 || this.data.firstClickTime !== null;
|
|
342
|
+
if (!hasData) return;
|
|
343
|
+
this.classify();
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Force classification (for beforeunload)
|
|
347
|
+
*/
|
|
348
|
+
forceClassify() {
|
|
349
|
+
if (this.classified) return this.result;
|
|
350
|
+
return this.classify();
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Perform classification
|
|
354
|
+
*/
|
|
355
|
+
classify() {
|
|
356
|
+
const sessionDuration = Date.now() - this.data.startTime;
|
|
357
|
+
const signals = this.extractSignals();
|
|
358
|
+
let humanLogProb = Math.log(PRIORS.human);
|
|
359
|
+
let aiLogProb = Math.log(PRIORS.ai_influenced);
|
|
360
|
+
for (const signal of signals) {
|
|
361
|
+
const humanWeight = NAIVE_BAYES_WEIGHTS.human[signal] ?? DEFAULT_WEIGHT;
|
|
362
|
+
const aiWeight = NAIVE_BAYES_WEIGHTS.ai_influenced[signal] ?? DEFAULT_WEIGHT;
|
|
363
|
+
humanLogProb += Math.log(humanWeight);
|
|
364
|
+
aiLogProb += Math.log(aiWeight);
|
|
365
|
+
}
|
|
366
|
+
const maxLog = Math.max(humanLogProb, aiLogProb);
|
|
367
|
+
const humanExp = Math.exp(humanLogProb - maxLog);
|
|
368
|
+
const aiExp = Math.exp(aiLogProb - maxLog);
|
|
369
|
+
const total = humanExp + aiExp;
|
|
370
|
+
const humanProbability = humanExp / total;
|
|
371
|
+
const aiProbability = aiExp / total;
|
|
372
|
+
let classification;
|
|
373
|
+
let confidence;
|
|
374
|
+
if (humanProbability > 0.6) {
|
|
375
|
+
classification = "human";
|
|
376
|
+
confidence = humanProbability;
|
|
377
|
+
} else if (aiProbability > 0.6) {
|
|
378
|
+
classification = "ai_influenced";
|
|
379
|
+
confidence = aiProbability;
|
|
380
|
+
} else {
|
|
381
|
+
classification = "uncertain";
|
|
382
|
+
confidence = Math.max(humanProbability, aiProbability);
|
|
383
|
+
}
|
|
384
|
+
this.result = {
|
|
385
|
+
classification,
|
|
386
|
+
humanProbability,
|
|
387
|
+
aiProbability,
|
|
388
|
+
confidence,
|
|
389
|
+
signals,
|
|
390
|
+
timestamp: Date.now(),
|
|
391
|
+
sessionDurationMs: sessionDuration
|
|
392
|
+
};
|
|
393
|
+
this.classified = true;
|
|
394
|
+
if (this.onClassify) {
|
|
395
|
+
this.onClassify(this.result);
|
|
396
|
+
}
|
|
397
|
+
return this.result;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Extract behavioral signals from collected data
|
|
401
|
+
*/
|
|
402
|
+
extractSignals() {
|
|
403
|
+
const signals = [];
|
|
404
|
+
if (this.data.firstClickTime !== null) {
|
|
405
|
+
const timeToClick = this.data.firstClickTime - this.data.startTime;
|
|
406
|
+
if (timeToClick < 500) {
|
|
407
|
+
signals.push("time_to_first_click_immediate");
|
|
408
|
+
} else if (timeToClick < 2e3) {
|
|
409
|
+
signals.push("time_to_first_click_fast");
|
|
410
|
+
} else if (timeToClick < 1e4) {
|
|
411
|
+
signals.push("time_to_first_click_normal");
|
|
412
|
+
} else {
|
|
413
|
+
signals.push("time_to_first_click_delayed");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (this.data.scrollEvents.length === 0) {
|
|
417
|
+
signals.push("scroll_speed_none");
|
|
418
|
+
} else if (this.data.scrollEvents.length >= 3) {
|
|
419
|
+
const scrollDeltas = [];
|
|
420
|
+
for (let i = 1; i < this.data.scrollEvents.length; i++) {
|
|
421
|
+
const delta = this.data.scrollEvents[i].time - this.data.scrollEvents[i - 1].time;
|
|
422
|
+
scrollDeltas.push(delta);
|
|
423
|
+
}
|
|
424
|
+
const mean = scrollDeltas.reduce((a, b) => a + b, 0) / scrollDeltas.length;
|
|
425
|
+
const variance = scrollDeltas.reduce((sum, d) => sum + Math.pow(d - mean, 2), 0) / scrollDeltas.length;
|
|
426
|
+
const stdDev = Math.sqrt(variance);
|
|
427
|
+
const cv = mean > 0 ? stdDev / mean : 0;
|
|
428
|
+
if (cv < 0.2) {
|
|
429
|
+
signals.push("scroll_speed_uniform");
|
|
430
|
+
} else if (cv < 0.6) {
|
|
431
|
+
signals.push("scroll_speed_variable");
|
|
432
|
+
} else {
|
|
433
|
+
signals.push("scroll_speed_erratic");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
if (this.data.mouseEvents.length === 0) {
|
|
437
|
+
signals.push("mouse_movement_none");
|
|
438
|
+
} else if (this.data.mouseEvents.length >= 10) {
|
|
439
|
+
const n = Math.min(this.data.mouseEvents.length, 20);
|
|
440
|
+
const recentMouse = this.data.mouseEvents.slice(-n);
|
|
441
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
442
|
+
for (const event of recentMouse) {
|
|
443
|
+
sumX += event.x;
|
|
444
|
+
sumY += event.y;
|
|
445
|
+
sumXY += event.x * event.y;
|
|
446
|
+
sumX2 += event.x * event.x;
|
|
447
|
+
}
|
|
448
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
449
|
+
const slope = denominator !== 0 ? (n * sumXY - sumX * sumY) / denominator : 0;
|
|
450
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
451
|
+
let ssRes = 0, ssTot = 0;
|
|
452
|
+
const yMean = sumY / n;
|
|
453
|
+
for (const event of recentMouse) {
|
|
454
|
+
const yPred = slope * event.x + intercept;
|
|
455
|
+
ssRes += Math.pow(event.y - yPred, 2);
|
|
456
|
+
ssTot += Math.pow(event.y - yMean, 2);
|
|
457
|
+
}
|
|
458
|
+
const r2 = ssTot !== 0 ? 1 - ssRes / ssTot : 0;
|
|
459
|
+
if (r2 > 0.95) {
|
|
460
|
+
signals.push("mouse_movement_linear");
|
|
461
|
+
} else {
|
|
462
|
+
signals.push("mouse_movement_curved");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const completedForms = this.data.formEvents.filter((e) => e.endTime > 0);
|
|
466
|
+
if (completedForms.length > 0) {
|
|
467
|
+
const avgFillTime = completedForms.reduce((sum, e) => sum + (e.endTime - e.startTime), 0) / completedForms.length;
|
|
468
|
+
if (avgFillTime < 100) {
|
|
469
|
+
signals.push("form_fill_instant");
|
|
470
|
+
} else if (avgFillTime < 500) {
|
|
471
|
+
signals.push("form_fill_fast");
|
|
472
|
+
} else {
|
|
473
|
+
signals.push("form_fill_normal");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (this.data.focusBlurEvents.length >= 4) {
|
|
477
|
+
const recentFB = this.data.focusBlurEvents.slice(-10);
|
|
478
|
+
const intervals = [];
|
|
479
|
+
for (let i = 1; i < recentFB.length; i++) {
|
|
480
|
+
intervals.push(recentFB[i].time - recentFB[i - 1].time);
|
|
481
|
+
}
|
|
482
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
483
|
+
if (avgInterval < 1e3) {
|
|
484
|
+
signals.push("focus_blur_rapid");
|
|
485
|
+
} else {
|
|
486
|
+
signals.push("focus_blur_normal");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return signals;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Add context signals (set by tracker from external data)
|
|
493
|
+
*/
|
|
494
|
+
addContextSignal(_signal) {
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Get current result (null if not yet classified)
|
|
498
|
+
*/
|
|
499
|
+
getResult() {
|
|
500
|
+
return this.result;
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Check if classification has been performed
|
|
504
|
+
*/
|
|
505
|
+
hasClassified() {
|
|
506
|
+
return this.classified;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// src/detection/focus-blur.ts
|
|
511
|
+
var FocusBlurAnalyzer = class {
|
|
512
|
+
constructor() {
|
|
513
|
+
this.sequence = [];
|
|
514
|
+
this.firstInteractionTime = null;
|
|
515
|
+
this.analyzed = false;
|
|
516
|
+
this.result = null;
|
|
517
|
+
this.pageLoadTime = performance.now();
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Initialize event tracking
|
|
521
|
+
* Must be called after DOM is ready
|
|
522
|
+
*/
|
|
523
|
+
initTracking() {
|
|
524
|
+
document.addEventListener("focus", (e) => {
|
|
525
|
+
this.recordEvent("focus", e.target);
|
|
526
|
+
}, true);
|
|
527
|
+
document.addEventListener("blur", (e) => {
|
|
528
|
+
this.recordEvent("blur", e.target);
|
|
529
|
+
}, true);
|
|
530
|
+
window.addEventListener("focus", () => {
|
|
531
|
+
this.recordEvent("window_focus", null);
|
|
532
|
+
});
|
|
533
|
+
window.addEventListener("blur", () => {
|
|
534
|
+
this.recordEvent("window_blur", null);
|
|
535
|
+
});
|
|
536
|
+
const recordFirstInteraction = () => {
|
|
537
|
+
if (this.firstInteractionTime === null) {
|
|
538
|
+
this.firstInteractionTime = performance.now();
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
document.addEventListener("click", recordFirstInteraction, { once: true, passive: true });
|
|
542
|
+
document.addEventListener("keydown", recordFirstInteraction, { once: true, passive: true });
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Record a focus/blur event
|
|
546
|
+
*/
|
|
547
|
+
recordEvent(type, target) {
|
|
548
|
+
const event = {
|
|
549
|
+
type,
|
|
550
|
+
target: target?.tagName || "WINDOW",
|
|
551
|
+
timestamp: performance.now()
|
|
552
|
+
};
|
|
553
|
+
this.sequence.push(event);
|
|
554
|
+
if (this.sequence.length > 20) {
|
|
555
|
+
this.sequence = this.sequence.slice(-20);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Analyze the focus/blur sequence for paste patterns
|
|
560
|
+
*/
|
|
561
|
+
analyze() {
|
|
562
|
+
if (this.analyzed && this.result) {
|
|
563
|
+
return this.result;
|
|
564
|
+
}
|
|
565
|
+
const signals = [];
|
|
566
|
+
let confidence = 0;
|
|
567
|
+
const earlyEvents = this.sequence.filter((e) => e.timestamp < this.pageLoadTime + 500);
|
|
568
|
+
const hasEarlyWindowFocus = earlyEvents.some((e) => e.type === "window_focus");
|
|
569
|
+
if (hasEarlyWindowFocus) {
|
|
570
|
+
signals.push("early_window_focus");
|
|
571
|
+
confidence += 0.15;
|
|
572
|
+
}
|
|
573
|
+
const hasEarlyBodyFocus = earlyEvents.some(
|
|
574
|
+
(e) => e.type === "focus" && e.target === "BODY"
|
|
575
|
+
);
|
|
576
|
+
if (hasEarlyBodyFocus) {
|
|
577
|
+
signals.push("early_body_focus");
|
|
578
|
+
confidence += 0.15;
|
|
579
|
+
}
|
|
580
|
+
const hasLinkFocus = this.sequence.some(
|
|
581
|
+
(e) => e.type === "focus" && e.target === "A"
|
|
582
|
+
);
|
|
583
|
+
if (!hasLinkFocus) {
|
|
584
|
+
signals.push("no_link_focus");
|
|
585
|
+
confidence += 0.1;
|
|
586
|
+
}
|
|
587
|
+
const firstFocus = this.sequence.find((e) => e.type === "focus");
|
|
588
|
+
if (firstFocus && (firstFocus.target === "BODY" || firstFocus.target === "HTML")) {
|
|
589
|
+
signals.push("first_focus_body");
|
|
590
|
+
confidence += 0.1;
|
|
591
|
+
}
|
|
592
|
+
const windowEvents = this.sequence.filter(
|
|
593
|
+
(e) => e.type === "window_focus" || e.type === "window_blur"
|
|
594
|
+
);
|
|
595
|
+
if (windowEvents.length <= 2) {
|
|
596
|
+
signals.push("minimal_window_switches");
|
|
597
|
+
confidence += 0.05;
|
|
598
|
+
}
|
|
599
|
+
if (this.firstInteractionTime !== null) {
|
|
600
|
+
const timeToInteraction = this.firstInteractionTime - this.pageLoadTime;
|
|
601
|
+
if (timeToInteraction > 3e3) {
|
|
602
|
+
signals.push("delayed_first_interaction");
|
|
603
|
+
confidence += 0.1;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
confidence = Math.min(confidence, 0.65);
|
|
607
|
+
let navType;
|
|
608
|
+
if (confidence >= 0.35) {
|
|
609
|
+
navType = "likely_paste";
|
|
610
|
+
} else if (signals.length === 0) {
|
|
611
|
+
navType = "unknown";
|
|
612
|
+
} else {
|
|
613
|
+
navType = "likely_click";
|
|
614
|
+
}
|
|
615
|
+
this.result = {
|
|
616
|
+
nav_type: navType,
|
|
617
|
+
confidence,
|
|
618
|
+
signals,
|
|
619
|
+
sequence: this.sequence.slice(-10),
|
|
620
|
+
time_to_first_interaction_ms: this.firstInteractionTime ? Math.round(this.firstInteractionTime - this.pageLoadTime) : null
|
|
621
|
+
};
|
|
622
|
+
this.analyzed = true;
|
|
623
|
+
return this.result;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Get current result (analyze if not done)
|
|
627
|
+
*/
|
|
628
|
+
getResult() {
|
|
629
|
+
return this.analyze();
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Check if analysis has been performed
|
|
633
|
+
*/
|
|
634
|
+
hasAnalyzed() {
|
|
635
|
+
return this.analyzed;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Get the raw sequence for debugging
|
|
639
|
+
*/
|
|
640
|
+
getSequence() {
|
|
641
|
+
return [...this.sequence];
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Reset the analyzer
|
|
645
|
+
*/
|
|
646
|
+
reset() {
|
|
647
|
+
this.sequence = [];
|
|
648
|
+
this.pageLoadTime = performance.now();
|
|
649
|
+
this.firstInteractionTime = null;
|
|
650
|
+
this.analyzed = false;
|
|
651
|
+
this.result = null;
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
194
655
|
// src/utils.ts
|
|
195
656
|
function generateUUID() {
|
|
196
657
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
@@ -276,6 +737,10 @@ var Loamly = (() => {
|
|
|
276
737
|
var sessionStartTime = null;
|
|
277
738
|
var navigationTiming = null;
|
|
278
739
|
var aiDetection = null;
|
|
740
|
+
var behavioralClassifier = null;
|
|
741
|
+
var behavioralMLResult = null;
|
|
742
|
+
var focusBlurAnalyzer = null;
|
|
743
|
+
var focusBlurResult = null;
|
|
279
744
|
function log(...args) {
|
|
280
745
|
if (debugMode) {
|
|
281
746
|
console.log("[Loamly]", ...args);
|
|
@@ -315,6 +780,16 @@ var Loamly = (() => {
|
|
|
315
780
|
if (!userConfig.disableBehavioral) {
|
|
316
781
|
setupBehavioralTracking();
|
|
317
782
|
}
|
|
783
|
+
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
784
|
+
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
785
|
+
setupBehavioralMLTracking();
|
|
786
|
+
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
787
|
+
focusBlurAnalyzer.initTracking();
|
|
788
|
+
setTimeout(() => {
|
|
789
|
+
if (focusBlurAnalyzer) {
|
|
790
|
+
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
791
|
+
}
|
|
792
|
+
}, 5e3);
|
|
318
793
|
log("Initialization complete");
|
|
319
794
|
}
|
|
320
795
|
function pageview(customUrl) {
|
|
@@ -496,6 +971,116 @@ var Loamly = (() => {
|
|
|
496
971
|
body: JSON.stringify(payload)
|
|
497
972
|
});
|
|
498
973
|
}
|
|
974
|
+
function setupBehavioralMLTracking() {
|
|
975
|
+
if (!behavioralClassifier) return;
|
|
976
|
+
let mouseSampleCount = 0;
|
|
977
|
+
document.addEventListener("mousemove", (e) => {
|
|
978
|
+
mouseSampleCount++;
|
|
979
|
+
if (mouseSampleCount % 10 === 0 && behavioralClassifier) {
|
|
980
|
+
behavioralClassifier.recordMouse(e.clientX, e.clientY);
|
|
981
|
+
}
|
|
982
|
+
}, { passive: true });
|
|
983
|
+
document.addEventListener("click", () => {
|
|
984
|
+
if (behavioralClassifier) {
|
|
985
|
+
behavioralClassifier.recordClick();
|
|
986
|
+
}
|
|
987
|
+
}, { passive: true });
|
|
988
|
+
let lastScrollY = 0;
|
|
989
|
+
document.addEventListener("scroll", () => {
|
|
990
|
+
const currentY = window.scrollY;
|
|
991
|
+
if (Math.abs(currentY - lastScrollY) > 50 && behavioralClassifier) {
|
|
992
|
+
lastScrollY = currentY;
|
|
993
|
+
behavioralClassifier.recordScroll(currentY);
|
|
994
|
+
}
|
|
995
|
+
}, { passive: true });
|
|
996
|
+
document.addEventListener("focusin", (e) => {
|
|
997
|
+
if (behavioralClassifier) {
|
|
998
|
+
behavioralClassifier.recordFocusBlur("focus");
|
|
999
|
+
const target = e.target;
|
|
1000
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
1001
|
+
behavioralClassifier.recordFormStart(target.id || target.getAttribute("name") || "unknown");
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}, { passive: true });
|
|
1005
|
+
document.addEventListener("focusout", (e) => {
|
|
1006
|
+
if (behavioralClassifier) {
|
|
1007
|
+
behavioralClassifier.recordFocusBlur("blur");
|
|
1008
|
+
const target = e.target;
|
|
1009
|
+
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA") {
|
|
1010
|
+
behavioralClassifier.recordFormEnd(target.id || target.getAttribute("name") || "unknown");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}, { passive: true });
|
|
1014
|
+
window.addEventListener("beforeunload", () => {
|
|
1015
|
+
if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
|
|
1016
|
+
const result = behavioralClassifier.forceClassify();
|
|
1017
|
+
if (result) {
|
|
1018
|
+
handleBehavioralClassification(result);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
setTimeout(() => {
|
|
1023
|
+
if (behavioralClassifier && !behavioralClassifier.hasClassified()) {
|
|
1024
|
+
behavioralClassifier.forceClassify();
|
|
1025
|
+
}
|
|
1026
|
+
}, 3e4);
|
|
1027
|
+
}
|
|
1028
|
+
function handleBehavioralClassification(result) {
|
|
1029
|
+
log("Behavioral ML classification:", result);
|
|
1030
|
+
behavioralMLResult = {
|
|
1031
|
+
classification: result.classification,
|
|
1032
|
+
humanProbability: result.humanProbability,
|
|
1033
|
+
aiProbability: result.aiProbability,
|
|
1034
|
+
confidence: result.confidence,
|
|
1035
|
+
signals: result.signals,
|
|
1036
|
+
sessionDurationMs: result.sessionDurationMs
|
|
1037
|
+
};
|
|
1038
|
+
sendBehavioralEvent("ml_classification", {
|
|
1039
|
+
classification: result.classification,
|
|
1040
|
+
human_probability: result.humanProbability,
|
|
1041
|
+
ai_probability: result.aiProbability,
|
|
1042
|
+
confidence: result.confidence,
|
|
1043
|
+
signals: result.signals,
|
|
1044
|
+
session_duration_ms: result.sessionDurationMs,
|
|
1045
|
+
navigation_timing: navigationTiming,
|
|
1046
|
+
ai_detection: aiDetection,
|
|
1047
|
+
focus_blur: focusBlurResult
|
|
1048
|
+
});
|
|
1049
|
+
if (result.classification === "ai_influenced" && result.confidence >= 0.7) {
|
|
1050
|
+
aiDetection = {
|
|
1051
|
+
isAI: true,
|
|
1052
|
+
confidence: result.confidence,
|
|
1053
|
+
method: "behavioral"
|
|
1054
|
+
};
|
|
1055
|
+
log("AI detection updated from behavioral ML:", aiDetection);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function handleFocusBlurAnalysis(result) {
|
|
1059
|
+
log("Focus/blur analysis:", result);
|
|
1060
|
+
focusBlurResult = {
|
|
1061
|
+
navType: result.nav_type,
|
|
1062
|
+
confidence: result.confidence,
|
|
1063
|
+
signals: result.signals,
|
|
1064
|
+
timeToFirstInteractionMs: result.time_to_first_interaction_ms
|
|
1065
|
+
};
|
|
1066
|
+
sendBehavioralEvent("focus_blur_analysis", {
|
|
1067
|
+
nav_type: result.nav_type,
|
|
1068
|
+
confidence: result.confidence,
|
|
1069
|
+
signals: result.signals,
|
|
1070
|
+
time_to_first_interaction_ms: result.time_to_first_interaction_ms,
|
|
1071
|
+
sequence_length: result.sequence.length
|
|
1072
|
+
});
|
|
1073
|
+
if (result.nav_type === "likely_paste" && result.confidence >= 0.4) {
|
|
1074
|
+
if (!aiDetection || aiDetection.confidence < result.confidence) {
|
|
1075
|
+
aiDetection = {
|
|
1076
|
+
isAI: true,
|
|
1077
|
+
confidence: result.confidence,
|
|
1078
|
+
method: "behavioral"
|
|
1079
|
+
};
|
|
1080
|
+
log("AI detection updated from focus/blur analysis:", aiDetection);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
499
1084
|
function getCurrentSessionId() {
|
|
500
1085
|
return sessionId;
|
|
501
1086
|
}
|
|
@@ -508,6 +1093,12 @@ var Loamly = (() => {
|
|
|
508
1093
|
function getNavigationTimingResult() {
|
|
509
1094
|
return navigationTiming;
|
|
510
1095
|
}
|
|
1096
|
+
function getBehavioralMLResult() {
|
|
1097
|
+
return behavioralMLResult;
|
|
1098
|
+
}
|
|
1099
|
+
function getFocusBlurResult() {
|
|
1100
|
+
return focusBlurResult;
|
|
1101
|
+
}
|
|
511
1102
|
function isTrackerInitialized() {
|
|
512
1103
|
return initialized;
|
|
513
1104
|
}
|
|
@@ -519,6 +1110,10 @@ var Loamly = (() => {
|
|
|
519
1110
|
sessionStartTime = null;
|
|
520
1111
|
navigationTiming = null;
|
|
521
1112
|
aiDetection = null;
|
|
1113
|
+
behavioralClassifier = null;
|
|
1114
|
+
behavioralMLResult = null;
|
|
1115
|
+
focusBlurAnalyzer = null;
|
|
1116
|
+
focusBlurResult = null;
|
|
522
1117
|
try {
|
|
523
1118
|
sessionStorage.removeItem("loamly_session");
|
|
524
1119
|
sessionStorage.removeItem("loamly_start");
|
|
@@ -539,6 +1134,8 @@ var Loamly = (() => {
|
|
|
539
1134
|
getVisitorId: getCurrentVisitorId,
|
|
540
1135
|
getAIDetection: getAIDetectionResult,
|
|
541
1136
|
getNavigationTiming: getNavigationTimingResult,
|
|
1137
|
+
getBehavioralML: getBehavioralMLResult,
|
|
1138
|
+
getFocusBlur: getFocusBlurResult,
|
|
542
1139
|
isInitialized: isTrackerInitialized,
|
|
543
1140
|
reset,
|
|
544
1141
|
debug: setDebug
|
|
@@ -591,4 +1188,11 @@ var Loamly = (() => {
|
|
|
591
1188
|
var browser_default = loamly;
|
|
592
1189
|
return __toCommonJS(browser_exports);
|
|
593
1190
|
})();
|
|
1191
|
+
/**
|
|
1192
|
+
* Loamly Tracker Configuration
|
|
1193
|
+
*
|
|
1194
|
+
* @module @loamly/tracker
|
|
1195
|
+
* @license MIT
|
|
1196
|
+
* @see https://github.com/loamly/loamly
|
|
1197
|
+
*/
|
|
594
1198
|
//# sourceMappingURL=loamly.iife.global.js.map
|