@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
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Focus/Blur Event Sequence Analysis
|
|
3
|
+
*
|
|
4
|
+
* LOA-182: Detects paste vs click navigation patterns by analyzing
|
|
5
|
+
* the sequence of focus and blur events when a page loads.
|
|
6
|
+
*
|
|
7
|
+
* Research: 55-65% accuracy (improves when combined with other signals)
|
|
8
|
+
*
|
|
9
|
+
* @module @loamly/tracker/detection/focus-blur
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Focus/blur event record
|
|
14
|
+
*/
|
|
15
|
+
export interface FocusBlurEvent {
|
|
16
|
+
type: 'focus' | 'blur' | 'window_focus' | 'window_blur'
|
|
17
|
+
target: string
|
|
18
|
+
timestamp: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Focus/blur analysis result
|
|
23
|
+
*/
|
|
24
|
+
export interface FocusBlurResult {
|
|
25
|
+
/** Navigation pattern type */
|
|
26
|
+
nav_type: 'likely_paste' | 'likely_click' | 'unknown'
|
|
27
|
+
/** Confidence score (0-1) */
|
|
28
|
+
confidence: number
|
|
29
|
+
/** Detection signals */
|
|
30
|
+
signals: string[]
|
|
31
|
+
/** Event sequence (last 10 events) */
|
|
32
|
+
sequence: FocusBlurEvent[]
|
|
33
|
+
/** Time from page load to first interaction */
|
|
34
|
+
time_to_first_interaction_ms: number | null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Focus/Blur Sequence Analyzer
|
|
39
|
+
*
|
|
40
|
+
* Tracks focus and blur events to detect paste navigation patterns.
|
|
41
|
+
* Paste navigation typically shows:
|
|
42
|
+
* 1. Early window focus event
|
|
43
|
+
* 2. Body focus without prior link navigation
|
|
44
|
+
* 3. No referrer blur pattern
|
|
45
|
+
*/
|
|
46
|
+
export class FocusBlurAnalyzer {
|
|
47
|
+
private sequence: FocusBlurEvent[] = []
|
|
48
|
+
private pageLoadTime: number
|
|
49
|
+
private firstInteractionTime: number | null = null
|
|
50
|
+
private analyzed = false
|
|
51
|
+
private result: FocusBlurResult | null = null
|
|
52
|
+
|
|
53
|
+
constructor() {
|
|
54
|
+
this.pageLoadTime = performance.now()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initialize event tracking
|
|
59
|
+
* Must be called after DOM is ready
|
|
60
|
+
*/
|
|
61
|
+
initTracking(): void {
|
|
62
|
+
// Track focus events (use capturing phase to catch all events)
|
|
63
|
+
document.addEventListener('focus', (e) => {
|
|
64
|
+
this.recordEvent('focus', e.target as HTMLElement)
|
|
65
|
+
}, true)
|
|
66
|
+
|
|
67
|
+
document.addEventListener('blur', (e) => {
|
|
68
|
+
this.recordEvent('blur', e.target as HTMLElement)
|
|
69
|
+
}, true)
|
|
70
|
+
|
|
71
|
+
// Track window focus/blur
|
|
72
|
+
window.addEventListener('focus', () => {
|
|
73
|
+
this.recordEvent('window_focus', null)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
window.addEventListener('blur', () => {
|
|
77
|
+
this.recordEvent('window_blur', null)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Track first click/keypress as first interaction
|
|
81
|
+
const recordFirstInteraction = () => {
|
|
82
|
+
if (this.firstInteractionTime === null) {
|
|
83
|
+
this.firstInteractionTime = performance.now()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
document.addEventListener('click', recordFirstInteraction, { once: true, passive: true })
|
|
87
|
+
document.addEventListener('keydown', recordFirstInteraction, { once: true, passive: true })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Record a focus/blur event
|
|
92
|
+
*/
|
|
93
|
+
private recordEvent(type: FocusBlurEvent['type'], target: HTMLElement | null): void {
|
|
94
|
+
const event: FocusBlurEvent = {
|
|
95
|
+
type,
|
|
96
|
+
target: target?.tagName || 'WINDOW',
|
|
97
|
+
timestamp: performance.now()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.sequence.push(event)
|
|
101
|
+
|
|
102
|
+
// Keep only last 20 events to limit memory
|
|
103
|
+
if (this.sequence.length > 20) {
|
|
104
|
+
this.sequence = this.sequence.slice(-20)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Analyze the focus/blur sequence for paste patterns
|
|
110
|
+
*/
|
|
111
|
+
analyze(): FocusBlurResult {
|
|
112
|
+
if (this.analyzed && this.result) {
|
|
113
|
+
return this.result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const signals: string[] = []
|
|
117
|
+
let confidence = 0
|
|
118
|
+
|
|
119
|
+
// Get early events (first 500ms after page load)
|
|
120
|
+
const earlyEvents = this.sequence.filter(e => e.timestamp < this.pageLoadTime + 500)
|
|
121
|
+
|
|
122
|
+
// Pattern 1: Window focus as first event
|
|
123
|
+
const hasEarlyWindowFocus = earlyEvents.some(e => e.type === 'window_focus')
|
|
124
|
+
if (hasEarlyWindowFocus) {
|
|
125
|
+
signals.push('early_window_focus')
|
|
126
|
+
confidence += 0.15
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Pattern 2: Body focus early (paste causes immediate body focus)
|
|
130
|
+
const hasEarlyBodyFocus = earlyEvents.some(
|
|
131
|
+
e => e.type === 'focus' && e.target === 'BODY'
|
|
132
|
+
)
|
|
133
|
+
if (hasEarlyBodyFocus) {
|
|
134
|
+
signals.push('early_body_focus')
|
|
135
|
+
confidence += 0.15
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Pattern 3: No link/anchor focus (would indicate click navigation)
|
|
139
|
+
const hasLinkFocus = this.sequence.some(
|
|
140
|
+
e => e.type === 'focus' && e.target === 'A'
|
|
141
|
+
)
|
|
142
|
+
if (!hasLinkFocus) {
|
|
143
|
+
signals.push('no_link_focus')
|
|
144
|
+
confidence += 0.10
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Pattern 4: First focus is on document/body (not a specific element)
|
|
148
|
+
const firstFocus = this.sequence.find(e => e.type === 'focus')
|
|
149
|
+
if (firstFocus && (firstFocus.target === 'BODY' || firstFocus.target === 'HTML')) {
|
|
150
|
+
signals.push('first_focus_body')
|
|
151
|
+
confidence += 0.10
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Pattern 5: No rapid tab switching (common in human navigation)
|
|
155
|
+
const windowEvents = this.sequence.filter(
|
|
156
|
+
e => e.type === 'window_focus' || e.type === 'window_blur'
|
|
157
|
+
)
|
|
158
|
+
if (windowEvents.length <= 2) {
|
|
159
|
+
signals.push('minimal_window_switches')
|
|
160
|
+
confidence += 0.05
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Pattern 6: Time to first interaction
|
|
164
|
+
// Paste users often have longer time before first interaction
|
|
165
|
+
// (reading content they copied from AI)
|
|
166
|
+
if (this.firstInteractionTime !== null) {
|
|
167
|
+
const timeToInteraction = this.firstInteractionTime - this.pageLoadTime
|
|
168
|
+
if (timeToInteraction > 3000) {
|
|
169
|
+
signals.push('delayed_first_interaction')
|
|
170
|
+
confidence += 0.10
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Cap confidence based on research (55-65% accuracy)
|
|
175
|
+
confidence = Math.min(confidence, 0.65)
|
|
176
|
+
|
|
177
|
+
// Determine navigation type
|
|
178
|
+
let navType: FocusBlurResult['nav_type']
|
|
179
|
+
if (confidence >= 0.35) {
|
|
180
|
+
navType = 'likely_paste'
|
|
181
|
+
} else if (signals.length === 0) {
|
|
182
|
+
navType = 'unknown'
|
|
183
|
+
} else {
|
|
184
|
+
navType = 'likely_click'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.result = {
|
|
188
|
+
nav_type: navType,
|
|
189
|
+
confidence,
|
|
190
|
+
signals,
|
|
191
|
+
sequence: this.sequence.slice(-10),
|
|
192
|
+
time_to_first_interaction_ms: this.firstInteractionTime
|
|
193
|
+
? Math.round(this.firstInteractionTime - this.pageLoadTime)
|
|
194
|
+
: null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.analyzed = true
|
|
198
|
+
return this.result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get current result (analyze if not done)
|
|
203
|
+
*/
|
|
204
|
+
getResult(): FocusBlurResult {
|
|
205
|
+
return this.analyze()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Check if analysis has been performed
|
|
210
|
+
*/
|
|
211
|
+
hasAnalyzed(): boolean {
|
|
212
|
+
return this.analyzed
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get the raw sequence for debugging
|
|
217
|
+
*/
|
|
218
|
+
getSequence(): FocusBlurEvent[] {
|
|
219
|
+
return [...this.sequence]
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Reset the analyzer
|
|
224
|
+
*/
|
|
225
|
+
reset(): void {
|
|
226
|
+
this.sequence = []
|
|
227
|
+
this.pageLoadTime = performance.now()
|
|
228
|
+
this.firstInteractionTime = null
|
|
229
|
+
this.analyzed = false
|
|
230
|
+
this.result = null
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Create a new focus/blur analyzer
|
|
236
|
+
*/
|
|
237
|
+
export function createFocusBlurAnalyzer(): FocusBlurAnalyzer {
|
|
238
|
+
const analyzer = new FocusBlurAnalyzer()
|
|
239
|
+
|
|
240
|
+
// Initialize tracking when DOM is ready
|
|
241
|
+
if (typeof document !== 'undefined') {
|
|
242
|
+
if (document.readyState === 'loading') {
|
|
243
|
+
document.addEventListener('DOMContentLoaded', () => analyzer.initTracking())
|
|
244
|
+
} else {
|
|
245
|
+
analyzer.initTracking()
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return analyzer
|
|
250
|
+
}
|
|
251
|
+
|
package/src/detection/index.ts
CHANGED
|
@@ -5,9 +5,28 @@
|
|
|
5
5
|
* traditional analytics miss.
|
|
6
6
|
*
|
|
7
7
|
* @module @loamly/tracker/detection
|
|
8
|
+
* @license MIT
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
export { detectNavigationType } from './navigation-timing'
|
|
11
12
|
export { detectAIFromReferrer, detectAIFromUTM } from './referrer'
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export {
|
|
14
|
+
BehavioralClassifier,
|
|
15
|
+
createBehavioralClassifier,
|
|
16
|
+
type BehavioralSignal,
|
|
17
|
+
type BehavioralClassificationResult
|
|
18
|
+
} from './behavioral-classifier'
|
|
19
|
+
export {
|
|
20
|
+
FocusBlurAnalyzer,
|
|
21
|
+
createFocusBlurAnalyzer,
|
|
22
|
+
type FocusBlurEvent,
|
|
23
|
+
type FocusBlurResult
|
|
24
|
+
} from './focus-blur'
|
|
25
|
+
export {
|
|
26
|
+
AgenticBrowserAnalyzer,
|
|
27
|
+
CometDetector,
|
|
28
|
+
MouseAnalyzer,
|
|
29
|
+
CDPDetector,
|
|
30
|
+
createAgenticAnalyzer,
|
|
31
|
+
type AgenticDetectionResult
|
|
32
|
+
} from './agentic-browser'
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
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
9
|
* @license MIT
|
|
10
|
+
* @see https://github.com/loamly/loamly
|
|
9
11
|
* @see https://loamly.ai
|
|
10
12
|
*/
|
|
11
13
|
|
|
@@ -24,6 +26,11 @@ export type {
|
|
|
24
26
|
// Detection utilities (for advanced usage)
|
|
25
27
|
export { detectNavigationType } from './detection/navigation-timing'
|
|
26
28
|
export { detectAIFromReferrer, detectAIFromUTM } from './detection/referrer'
|
|
29
|
+
export {
|
|
30
|
+
AgenticBrowserAnalyzer,
|
|
31
|
+
createAgenticAnalyzer,
|
|
32
|
+
type AgenticDetectionResult
|
|
33
|
+
} from './detection/agentic-browser'
|
|
27
34
|
|
|
28
35
|
// Configuration
|
|
29
36
|
export { VERSION, AI_PLATFORMS, AI_BOT_PATTERNS } from './config'
|
package/src/types.ts
CHANGED
|
@@ -59,6 +59,38 @@ export interface AIDetectionResult {
|
|
|
59
59
|
method: 'referrer' | 'timing' | 'behavioral' | 'temporal' | 'unknown'
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Behavioral ML classification result
|
|
64
|
+
*/
|
|
65
|
+
export interface BehavioralMLResult {
|
|
66
|
+
/** Classification result */
|
|
67
|
+
classification: 'human' | 'ai_influenced' | 'uncertain'
|
|
68
|
+
/** Probability of human behavior (0-1) */
|
|
69
|
+
humanProbability: number
|
|
70
|
+
/** Probability of AI-influenced behavior (0-1) */
|
|
71
|
+
aiProbability: number
|
|
72
|
+
/** Confidence in classification */
|
|
73
|
+
confidence: number
|
|
74
|
+
/** Behavioral signals detected */
|
|
75
|
+
signals: string[]
|
|
76
|
+
/** Session duration when classified */
|
|
77
|
+
sessionDurationMs: number
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Focus/Blur analysis result
|
|
82
|
+
*/
|
|
83
|
+
export interface FocusBlurMLResult {
|
|
84
|
+
/** Navigation pattern type */
|
|
85
|
+
navType: 'likely_paste' | 'likely_click' | 'unknown'
|
|
86
|
+
/** Confidence score (0-1) */
|
|
87
|
+
confidence: number
|
|
88
|
+
/** Detection signals */
|
|
89
|
+
signals: string[]
|
|
90
|
+
/** Time to first interaction in ms */
|
|
91
|
+
timeToFirstInteractionMs: number | null
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
export interface LoamlyTracker {
|
|
63
95
|
/** Initialize the tracker with configuration */
|
|
64
96
|
init: (config: LoamlyConfig) => void
|
|
@@ -87,6 +119,12 @@ export interface LoamlyTracker {
|
|
|
87
119
|
/** Get navigation timing analysis */
|
|
88
120
|
getNavigationTiming: () => NavigationTiming | null
|
|
89
121
|
|
|
122
|
+
/** Get behavioral ML classification result */
|
|
123
|
+
getBehavioralML: () => BehavioralMLResult | null
|
|
124
|
+
|
|
125
|
+
/** Get focus/blur analysis result */
|
|
126
|
+
getFocusBlur: () => FocusBlurMLResult | null
|
|
127
|
+
|
|
90
128
|
/** Check if tracker is initialized */
|
|
91
129
|
isInitialized: () => boolean
|
|
92
130
|
|