@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.
@@ -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
+
@@ -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