@loamly/tracker 2.0.1 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loamly/tracker",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "See every AI bot that visits your website. ChatGPT, Claude, Perplexity, Gemini — know when they crawl or refer traffic.",
5
5
  "author": "Loamly <hello@loamly.ai>",
6
6
  "license": "MIT",
@@ -46,6 +46,13 @@
46
46
  "engines": {
47
47
  "node": ">=18"
48
48
  },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "dev": "tsup --watch",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "clean": "rm -rf dist"
55
+ },
49
56
  "devDependencies": {
50
57
  "@types/node": "^20.10.0",
51
58
  "tsup": "^8.0.1",
@@ -55,12 +62,7 @@
55
62
  "publishConfig": {
56
63
  "access": "public",
57
64
  "registry": "https://registry.npmjs.org/"
58
- },
59
- "scripts": {
60
- "build": "tsup",
61
- "dev": "tsup --watch",
62
- "typecheck": "tsc --noEmit",
63
- "test": "vitest run",
64
- "clean": "rm -rf dist"
65
65
  }
66
- }
66
+ }
67
+
68
+
package/src/config.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * @see https://github.com/loamly/loamly
7
7
  */
8
8
 
9
- export const VERSION = '2.0.1'
9
+ export const VERSION = '2.1.0'
10
10
 
11
11
  export const DEFAULT_CONFIG = {
12
12
  apiHost: 'https://app.loamly.ai',
package/src/core.ts CHANGED
@@ -117,7 +117,22 @@ function init(userConfig: LoamlyConfig = {}): void {
117
117
 
118
118
  debugMode = userConfig.debug ?? false
119
119
 
120
+ // Feature flags with defaults (all enabled except ping)
121
+ const features = {
122
+ scroll: true,
123
+ time: true,
124
+ forms: true,
125
+ spa: true,
126
+ behavioralML: true,
127
+ focusBlur: true,
128
+ agentic: true,
129
+ eventQueue: true,
130
+ ping: false, // Opt-in only
131
+ ...userConfig.features,
132
+ }
133
+
120
134
  log('Initializing Loamly Tracker v' + VERSION)
135
+ log('Features:', features)
121
136
 
122
137
  // Get/create visitor ID
123
138
  visitorId = getVisitorId()
@@ -128,17 +143,19 @@ function init(userConfig: LoamlyConfig = {}): void {
128
143
  sessionId = session.sessionId
129
144
  log('Session ID:', sessionId, session.isNew ? '(new)' : '(existing)')
130
145
 
131
- // Initialize event queue with batching
132
- eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
133
- batchSize: DEFAULT_CONFIG.batchSize,
134
- batchTimeout: DEFAULT_CONFIG.batchTimeout,
135
- })
146
+ // Initialize event queue with batching (if enabled)
147
+ if (features.eventQueue) {
148
+ eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
149
+ batchSize: DEFAULT_CONFIG.batchSize,
150
+ batchTimeout: DEFAULT_CONFIG.batchTimeout,
151
+ })
152
+ }
136
153
 
137
- // Detect navigation timing (paste vs click)
154
+ // Detect navigation timing (paste vs click) - always lightweight
138
155
  navigationTiming = detectNavigationType()
139
156
  log('Navigation timing:', navigationTiming)
140
157
 
141
- // Detect AI from referrer/UTM
158
+ // Detect AI from referrer/UTM - always lightweight
142
159
  aiDetection = detectAIFromReferrer(document.referrer) || detectAIFromUTM(window.location.href)
143
160
  if (aiDetection) {
144
161
  log('AI detected:', aiDetection)
@@ -151,33 +168,39 @@ function init(userConfig: LoamlyConfig = {}): void {
151
168
  pageview()
152
169
  }
153
170
 
154
- // Set up behavioral tracking unless disabled
171
+ // Set up behavioral tracking (scroll, time, forms) unless disabled
155
172
  if (!userConfig.disableBehavioral) {
156
- setupAdvancedBehavioralTracking()
173
+ setupAdvancedBehavioralTracking(features)
157
174
  }
158
175
 
159
- // Initialize behavioral ML classifier (LOA-180)
160
- behavioralClassifier = new BehavioralClassifier(10000) // 10s min session
161
- behavioralClassifier.setOnClassify(handleBehavioralClassification)
162
- setupBehavioralMLTracking()
163
-
164
- // Initialize focus/blur analyzer (LOA-182)
165
- focusBlurAnalyzer = new FocusBlurAnalyzer()
166
- focusBlurAnalyzer.initTracking()
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
+ }
167
182
 
168
- // Analyze focus/blur after 5 seconds
169
- setTimeout(() => {
170
- if (focusBlurAnalyzer) {
171
- handleFocusBlurAnalysis(focusBlurAnalyzer.analyze())
172
- }
173
- }, 5000)
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
+ }
174
195
 
175
- // Initialize agentic browser detection (LOA-187)
176
- agenticAnalyzer = new AgenticBrowserAnalyzer()
177
- agenticAnalyzer.init()
196
+ // Initialize agentic browser detection (LOA-187) - if enabled
197
+ if (features.agentic) {
198
+ agenticAnalyzer = new AgenticBrowserAnalyzer()
199
+ agenticAnalyzer.init()
200
+ }
178
201
 
179
- // Set up ping service
180
- if (visitorId && sessionId) {
202
+ // Set up ping service - if enabled (opt-in)
203
+ if (features.ping && visitorId && sessionId) {
181
204
  pingService = new PingService(sessionId, visitorId, VERSION, {
182
205
  interval: DEFAULT_CONFIG.pingInterval,
183
206
  endpoint: endpoint(DEFAULT_CONFIG.endpoints.ping),
@@ -194,60 +217,92 @@ function init(userConfig: LoamlyConfig = {}): void {
194
217
  // Set up unload handlers
195
218
  setupUnloadHandlers()
196
219
 
220
+ // Report health status
221
+ reportHealth('initialized')
222
+
197
223
  log('Initialization complete')
198
224
  }
199
225
 
200
226
  /**
201
227
  * Set up advanced behavioral tracking with new modules
202
228
  */
203
- function setupAdvancedBehavioralTracking(): void {
204
- // Scroll tracker with 30% chunks
205
- scrollTracker = new ScrollTracker({
206
- chunks: [30, 60, 90, 100],
207
- onChunkReached: (event: ScrollEvent) => {
208
- log('Scroll chunk:', event.chunk)
209
- queueEvent('scroll_depth', {
210
- depth: event.depth,
211
- chunk: event.chunk,
212
- time_to_reach_ms: event.time_to_reach_ms,
213
- })
214
- },
215
- })
216
- scrollTracker.start()
229
+ interface FeatureFlags {
230
+ scroll?: boolean
231
+ time?: boolean
232
+ forms?: boolean
233
+ spa?: boolean
234
+ behavioralML?: boolean
235
+ focusBlur?: boolean
236
+ agentic?: boolean
237
+ eventQueue?: boolean
238
+ ping?: boolean
239
+ }
240
+
241
+ function setupAdvancedBehavioralTracking(features: FeatureFlags): void {
242
+ // Scroll tracker with 30% chunks (if enabled)
243
+ if (features.scroll) {
244
+ scrollTracker = new ScrollTracker({
245
+ chunks: [30, 60, 90, 100],
246
+ onChunkReached: (event: ScrollEvent) => {
247
+ log('Scroll chunk:', event.chunk)
248
+ queueEvent('scroll_depth', {
249
+ depth: event.depth,
250
+ chunk: event.chunk,
251
+ time_to_reach_ms: event.time_to_reach_ms,
252
+ })
253
+ },
254
+ })
255
+ scrollTracker.start()
256
+ }
217
257
 
218
- // Time tracker
219
- timeTracker = new TimeTracker({
220
- updateIntervalMs: 10000, // Report every 10 seconds
221
- onUpdate: (event: TimeEvent) => {
222
- if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
223
- queueEvent('time_spent', {
224
- active_time_ms: event.active_time_ms,
225
- total_time_ms: event.total_time_ms,
226
- idle_time_ms: event.idle_time_ms,
227
- is_engaged: event.is_engaged,
258
+ // Time tracker (if enabled)
259
+ if (features.time) {
260
+ timeTracker = new TimeTracker({
261
+ updateIntervalMs: 10000, // Report every 10 seconds
262
+ onUpdate: (event: TimeEvent) => {
263
+ if (event.active_time_ms >= DEFAULT_CONFIG.timeSpentThresholdMs) {
264
+ queueEvent('time_spent', {
265
+ active_time_ms: event.active_time_ms,
266
+ total_time_ms: event.total_time_ms,
267
+ idle_time_ms: event.idle_time_ms,
268
+ is_engaged: event.is_engaged,
269
+ })
270
+ }
271
+ },
272
+ })
273
+ timeTracker.start()
274
+ }
275
+
276
+ // Form tracker with universal support (if enabled)
277
+ if (features.forms) {
278
+ formTracker = new FormTracker({
279
+ onFormEvent: (event: FormEvent) => {
280
+ log('Form event:', event.event_type, event.form_id)
281
+ queueEvent(event.event_type, {
282
+ form_id: event.form_id,
283
+ form_type: event.form_type,
284
+ field_name: event.field_name,
285
+ field_type: event.field_type,
286
+ time_to_submit_ms: event.time_to_submit_ms,
287
+ is_conversion: event.is_conversion,
228
288
  })
229
- }
230
- },
231
- })
232
- timeTracker.start()
233
-
234
- // Form tracker with universal support
235
- formTracker = new FormTracker({
236
- onFormEvent: (event: FormEvent) => {
237
- log('Form event:', event.event_type, event.form_id)
238
- queueEvent(event.event_type, {
239
- form_id: event.form_id,
240
- form_type: event.form_type,
241
- field_name: event.field_name,
242
- field_type: event.field_type,
243
- time_to_submit_ms: event.time_to_submit_ms,
244
- is_conversion: event.is_conversion,
245
- })
246
- },
247
- })
248
- formTracker.start()
289
+ },
290
+ })
291
+ formTracker.start()
292
+ }
293
+
294
+ // SPA router (if enabled)
295
+ if (features.spa) {
296
+ spaRouter = new SPARouter({
297
+ onNavigate: (event: NavigationEvent) => {
298
+ log('SPA navigation:', event.navigation_type)
299
+ pageview(event.to_url)
300
+ },
301
+ })
302
+ spaRouter.start()
303
+ }
249
304
 
250
- // Click tracking for links (basic)
305
+ // Click tracking for links (always enabled, lightweight)
251
306
  document.addEventListener('click', (e) => {
252
307
  const target = e.target as HTMLElement
253
308
  const link = target.closest('a')
@@ -678,6 +733,50 @@ function isTrackerInitialized(): boolean {
678
733
  return initialized
679
734
  }
680
735
 
736
+ /**
737
+ * Report tracker health status
738
+ * Used for monitoring and debugging
739
+ */
740
+ function reportHealth(status: 'initialized' | 'error' | 'ready', errorMessage?: string): void {
741
+ if (!config.apiKey) return
742
+
743
+ try {
744
+ const healthData = {
745
+ workspace_id: config.apiKey,
746
+ status,
747
+ error_message: errorMessage || null,
748
+ version: VERSION,
749
+ url: typeof window !== 'undefined' ? window.location.href : null,
750
+ user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : null,
751
+ timestamp: new Date().toISOString(),
752
+ features: {
753
+ scroll_tracker: !!scrollTracker,
754
+ time_tracker: !!timeTracker,
755
+ form_tracker: !!formTracker,
756
+ spa_router: !!spaRouter,
757
+ behavioral_ml: !!behavioralClassifier,
758
+ focus_blur: !!focusBlurAnalyzer,
759
+ agentic: !!agenticAnalyzer,
760
+ ping_service: !!pingService,
761
+ event_queue: !!eventQueue,
762
+ },
763
+ }
764
+
765
+ // Fire and forget
766
+ safeFetch(endpoint(DEFAULT_CONFIG.endpoints.health), {
767
+ method: 'POST',
768
+ headers: { 'Content-Type': 'application/json' },
769
+ body: JSON.stringify(healthData),
770
+ }).catch(() => {
771
+ // Ignore health reporting errors
772
+ })
773
+
774
+ log('Health reported:', status)
775
+ } catch {
776
+ // Ignore
777
+ }
778
+ }
779
+
681
780
  /**
682
781
  * Reset the tracker
683
782
  */
@@ -729,7 +828,10 @@ function setDebug(enabled: boolean): void {
729
828
  /**
730
829
  * The Loamly Tracker instance
731
830
  */
732
- export const loamly: LoamlyTracker & { getAgentic: () => AgenticDetectionResult | null } = {
831
+ export const loamly: LoamlyTracker & {
832
+ getAgentic: () => AgenticDetectionResult | null
833
+ reportHealth: (status: 'initialized' | 'error' | 'ready', errorMessage?: string) => void
834
+ } = {
733
835
  init,
734
836
  pageview,
735
837
  track,
@@ -745,6 +847,7 @@ export const loamly: LoamlyTracker & { getAgentic: () => AgenticDetectionResult
745
847
  isInitialized: isTrackerInitialized,
746
848
  reset,
747
849
  debug: setDebug,
850
+ reportHealth,
748
851
  }
749
852
 
750
853
  export default loamly
package/src/types.ts CHANGED
@@ -21,6 +21,31 @@ export interface LoamlyConfig {
21
21
 
22
22
  /** Custom session timeout in milliseconds (default: 30 minutes) */
23
23
  sessionTimeout?: number
24
+
25
+ /**
26
+ * Feature flags for lightweight mode
27
+ * Set to false to reduce initialization overhead
28
+ */
29
+ features?: {
30
+ /** Scroll depth tracking (default: true) */
31
+ scroll?: boolean
32
+ /** Time on page tracking (default: true) */
33
+ time?: boolean
34
+ /** Form interaction tracking (default: true) */
35
+ forms?: boolean
36
+ /** SPA navigation support (default: true) */
37
+ spa?: boolean
38
+ /** Behavioral ML classification (default: true) */
39
+ behavioralML?: boolean
40
+ /** Focus/blur paste detection (default: true) */
41
+ focusBlur?: boolean
42
+ /** Agentic browser detection (default: true) */
43
+ agentic?: boolean
44
+ /** Event queue with retry (default: true) */
45
+ eventQueue?: boolean
46
+ /** Heartbeat ping service (default: false - opt-in) */
47
+ ping?: boolean
48
+ }
24
49
  }
25
50
 
26
51
  export interface TrackEventOptions {
package/LICENSE DELETED
@@ -1,23 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025-present Loamly
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
22
-
23
-