@loamly/tracker 1.6.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/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # @loamly/tracker
2
+
3
+ **Open-source AI traffic detection for websites.**
4
+
5
+ > See what AI tells your customers — and track when they click.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@loamly/tracker.svg)](https://www.npmjs.com/package/@loamly/tracker)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ Part of the [Loamly](https://github.com/loamly/loamly) open-source project.
11
+
12
+ ---
13
+
14
+ ## The Problem
15
+
16
+ When users copy URLs from ChatGPT, Claude, or Perplexity:
17
+
18
+ - ❌ No referrer header
19
+ - ❌ No UTM parameters
20
+ - ❌ Analytics shows "Direct Traffic"
21
+
22
+ ## The Solution
23
+
24
+ Loamly detects AI-referred traffic with **75-85% accuracy** using:
25
+
26
+ - 🔍 Referrer detection
27
+ - ⏱️ Navigation Timing API (paste vs click)
28
+ - 🧠 Behavioral signals
29
+ - 📋 Zero-party surveys
30
+
31
+ ## Quick Start
32
+
33
+ ### Script Tag
34
+
35
+ ```html
36
+ <script
37
+ src="https://unpkg.com/@loamly/tracker"
38
+ data-api-key="your-api-key"
39
+ ></script>
40
+ ```
41
+
42
+ ### NPM
43
+
44
+ ```bash
45
+ npm install @loamly/tracker
46
+ ```
47
+
48
+ ```typescript
49
+ import loamly from '@loamly/tracker'
50
+
51
+ loamly.init({ apiKey: 'your-api-key' })
52
+ loamly.track('signup_started')
53
+ loamly.conversion('purchase', 99.99)
54
+ ```
55
+
56
+ ## API
57
+
58
+ | Method | Description |
59
+ |--------|-------------|
60
+ | `init(config)` | Initialize the tracker |
61
+ | `pageview(url?)` | Track page view |
62
+ | `track(event, options?)` | Track custom event |
63
+ | `conversion(event, revenue, currency?)` | Track conversion |
64
+ | `identify(userId, traits?)` | Identify user |
65
+ | `getAIDetection()` | Get AI detection result |
66
+ | `getNavigationTiming()` | Get paste vs click analysis |
67
+
68
+ ## Privacy
69
+
70
+ - 🍪 Cookie-free
71
+ - 📍 No IP tracking
72
+ - 🔒 GDPR compliant
73
+
74
+ ## Documentation
75
+
76
+ See [loamly.ai/docs](https://loamly.ai/docs) for full documentation.
77
+
78
+ ## License
79
+
80
+ MIT © [Loamly](https://loamly.ai)
81
+
82
+
package/dist/index.cjs ADDED
@@ -0,0 +1,584 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AI_BOT_PATTERNS: () => AI_BOT_PATTERNS,
24
+ AI_PLATFORMS: () => AI_PLATFORMS,
25
+ VERSION: () => VERSION,
26
+ default: () => loamly,
27
+ detectAIFromReferrer: () => detectAIFromReferrer,
28
+ detectAIFromUTM: () => detectAIFromUTM,
29
+ detectNavigationType: () => detectNavigationType,
30
+ loamly: () => loamly
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/config.ts
35
+ var VERSION = "1.6.0";
36
+ var DEFAULT_CONFIG = {
37
+ apiHost: "https://app.loamly.ai",
38
+ endpoints: {
39
+ visit: "/api/ingest/visit",
40
+ behavioral: "/api/ingest/behavioral",
41
+ session: "/api/ingest/session",
42
+ resolve: "/api/tracker/resolve",
43
+ health: "/api/tracker/health",
44
+ ping: "/api/tracker/ping"
45
+ },
46
+ pingInterval: 3e4,
47
+ // 30 seconds
48
+ batchSize: 10,
49
+ batchTimeout: 5e3,
50
+ sessionTimeout: 18e5,
51
+ // 30 minutes
52
+ maxTextLength: 100,
53
+ timeSpentThresholdMs: 5e3
54
+ // Only send time_spent when delta >= 5 seconds
55
+ };
56
+ var AI_PLATFORMS = {
57
+ "chatgpt.com": "chatgpt",
58
+ "chat.openai.com": "chatgpt",
59
+ "claude.ai": "claude",
60
+ "perplexity.ai": "perplexity",
61
+ "bard.google.com": "bard",
62
+ "gemini.google.com": "gemini",
63
+ "copilot.microsoft.com": "copilot",
64
+ "github.com/copilot": "github-copilot",
65
+ "you.com": "you",
66
+ "phind.com": "phind",
67
+ "poe.com": "poe"
68
+ };
69
+ var AI_BOT_PATTERNS = [
70
+ "GPTBot",
71
+ "ChatGPT-User",
72
+ "ClaudeBot",
73
+ "Claude-Web",
74
+ "PerplexityBot",
75
+ "Amazonbot",
76
+ "Google-Extended",
77
+ "CCBot",
78
+ "anthropic-ai",
79
+ "cohere-ai"
80
+ ];
81
+
82
+ // src/detection/navigation-timing.ts
83
+ function detectNavigationType() {
84
+ try {
85
+ const entries = performance.getEntriesByType("navigation");
86
+ if (!entries || entries.length === 0) {
87
+ return { nav_type: "unknown", confidence: 0, signals: ["no_timing_data"] };
88
+ }
89
+ const nav = entries[0];
90
+ const signals = [];
91
+ let pasteScore = 0;
92
+ const fetchStartDelta = nav.fetchStart - nav.startTime;
93
+ if (fetchStartDelta < 5) {
94
+ pasteScore += 0.25;
95
+ signals.push("instant_fetch_start");
96
+ } else if (fetchStartDelta < 20) {
97
+ pasteScore += 0.15;
98
+ signals.push("fast_fetch_start");
99
+ }
100
+ const dnsTime = nav.domainLookupEnd - nav.domainLookupStart;
101
+ if (dnsTime === 0) {
102
+ pasteScore += 0.15;
103
+ signals.push("no_dns_lookup");
104
+ }
105
+ const connectTime = nav.connectEnd - nav.connectStart;
106
+ if (connectTime === 0) {
107
+ pasteScore += 0.15;
108
+ signals.push("no_tcp_connect");
109
+ }
110
+ if (nav.redirectCount === 0) {
111
+ pasteScore += 0.1;
112
+ signals.push("no_redirects");
113
+ }
114
+ const timingVariance = calculateTimingVariance(nav);
115
+ if (timingVariance < 10) {
116
+ pasteScore += 0.15;
117
+ signals.push("uniform_timing");
118
+ }
119
+ if (!document.referrer || document.referrer === "") {
120
+ pasteScore += 0.1;
121
+ signals.push("no_referrer");
122
+ }
123
+ const confidence = Math.min(pasteScore, 1);
124
+ const nav_type = pasteScore >= 0.5 ? "likely_paste" : "likely_click";
125
+ return {
126
+ nav_type,
127
+ confidence: Math.round(confidence * 1e3) / 1e3,
128
+ signals
129
+ };
130
+ } catch {
131
+ return { nav_type: "unknown", confidence: 0, signals: ["detection_error"] };
132
+ }
133
+ }
134
+ function calculateTimingVariance(nav) {
135
+ const timings = [
136
+ nav.fetchStart - nav.startTime,
137
+ nav.domainLookupEnd - nav.domainLookupStart,
138
+ nav.connectEnd - nav.connectStart,
139
+ nav.responseStart - nav.requestStart
140
+ ].filter((t) => t >= 0);
141
+ if (timings.length === 0) return 100;
142
+ const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
143
+ const variance = timings.reduce((sum, t) => sum + Math.pow(t - mean, 2), 0) / timings.length;
144
+ return Math.sqrt(variance);
145
+ }
146
+
147
+ // src/detection/referrer.ts
148
+ function detectAIFromReferrer(referrer) {
149
+ if (!referrer) {
150
+ return null;
151
+ }
152
+ try {
153
+ const url = new URL(referrer);
154
+ const hostname = url.hostname.toLowerCase();
155
+ for (const [pattern, platform] of Object.entries(AI_PLATFORMS)) {
156
+ if (hostname.includes(pattern) || referrer.includes(pattern)) {
157
+ return {
158
+ isAI: true,
159
+ platform,
160
+ confidence: 0.95,
161
+ // High confidence when referrer matches
162
+ method: "referrer"
163
+ };
164
+ }
165
+ }
166
+ return null;
167
+ } catch {
168
+ for (const [pattern, platform] of Object.entries(AI_PLATFORMS)) {
169
+ if (referrer.toLowerCase().includes(pattern.toLowerCase())) {
170
+ return {
171
+ isAI: true,
172
+ platform,
173
+ confidence: 0.85,
174
+ method: "referrer"
175
+ };
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+ }
181
+ function detectAIFromUTM(url) {
182
+ try {
183
+ const params = new URL(url).searchParams;
184
+ const utmSource = params.get("utm_source")?.toLowerCase();
185
+ if (utmSource) {
186
+ for (const [pattern, platform] of Object.entries(AI_PLATFORMS)) {
187
+ if (utmSource.includes(pattern.split(".")[0])) {
188
+ return {
189
+ isAI: true,
190
+ platform,
191
+ confidence: 0.99,
192
+ // Very high confidence from explicit UTM
193
+ method: "referrer"
194
+ };
195
+ }
196
+ }
197
+ if (utmSource.includes("ai") || utmSource.includes("llm") || utmSource.includes("chatbot")) {
198
+ return {
199
+ isAI: true,
200
+ platform: utmSource,
201
+ confidence: 0.9,
202
+ method: "referrer"
203
+ };
204
+ }
205
+ }
206
+ return null;
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+
212
+ // src/utils.ts
213
+ function generateUUID() {
214
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
215
+ return crypto.randomUUID();
216
+ }
217
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
218
+ const r = Math.random() * 16 | 0;
219
+ const v = c === "x" ? r : r & 3 | 8;
220
+ return v.toString(16);
221
+ });
222
+ }
223
+ function getVisitorId() {
224
+ try {
225
+ const stored = localStorage.getItem("_loamly_vid");
226
+ if (stored) return stored;
227
+ const newId = generateUUID();
228
+ localStorage.setItem("_loamly_vid", newId);
229
+ return newId;
230
+ } catch {
231
+ return generateUUID();
232
+ }
233
+ }
234
+ function getSessionId() {
235
+ try {
236
+ const storedSession = sessionStorage.getItem("loamly_session");
237
+ const storedStart = sessionStorage.getItem("loamly_start");
238
+ if (storedSession && storedStart) {
239
+ return { sessionId: storedSession, isNew: false };
240
+ }
241
+ const newSession = generateUUID();
242
+ const startTime = Date.now().toString();
243
+ sessionStorage.setItem("loamly_session", newSession);
244
+ sessionStorage.setItem("loamly_start", startTime);
245
+ return { sessionId: newSession, isNew: true };
246
+ } catch {
247
+ return { sessionId: generateUUID(), isNew: true };
248
+ }
249
+ }
250
+ function extractUTMParams(url) {
251
+ const params = {};
252
+ try {
253
+ const searchParams = new URL(url).searchParams;
254
+ const utmKeys = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
255
+ for (const key of utmKeys) {
256
+ const value = searchParams.get(key);
257
+ if (value) params[key] = value;
258
+ }
259
+ } catch {
260
+ }
261
+ return params;
262
+ }
263
+ function truncateText(text, maxLength) {
264
+ if (text.length <= maxLength) return text;
265
+ return text.substring(0, maxLength - 3) + "...";
266
+ }
267
+ async function safeFetch(url, options, timeout = 1e4) {
268
+ try {
269
+ const controller = new AbortController();
270
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
271
+ const response = await fetch(url, {
272
+ ...options,
273
+ signal: controller.signal
274
+ });
275
+ clearTimeout(timeoutId);
276
+ return response;
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+ function sendBeacon(url, data) {
282
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
283
+ return navigator.sendBeacon(url, JSON.stringify(data));
284
+ }
285
+ return false;
286
+ }
287
+
288
+ // src/core.ts
289
+ var config = { apiHost: DEFAULT_CONFIG.apiHost };
290
+ var initialized = false;
291
+ var debugMode = false;
292
+ var visitorId = null;
293
+ var sessionId = null;
294
+ var sessionStartTime = null;
295
+ var navigationTiming = null;
296
+ var aiDetection = null;
297
+ function log(...args) {
298
+ if (debugMode) {
299
+ console.log("[Loamly]", ...args);
300
+ }
301
+ }
302
+ function endpoint(path) {
303
+ return `${config.apiHost}${path}`;
304
+ }
305
+ function init(userConfig = {}) {
306
+ if (initialized) {
307
+ log("Already initialized");
308
+ return;
309
+ }
310
+ config = {
311
+ ...config,
312
+ ...userConfig,
313
+ apiHost: userConfig.apiHost || DEFAULT_CONFIG.apiHost
314
+ };
315
+ debugMode = userConfig.debug ?? false;
316
+ log("Initializing Loamly Tracker v" + VERSION);
317
+ visitorId = getVisitorId();
318
+ log("Visitor ID:", visitorId);
319
+ const session = getSessionId();
320
+ sessionId = session.sessionId;
321
+ sessionStartTime = Date.now();
322
+ log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
323
+ navigationTiming = detectNavigationType();
324
+ log("Navigation timing:", navigationTiming);
325
+ aiDetection = detectAIFromReferrer(document.referrer) || detectAIFromUTM(window.location.href);
326
+ if (aiDetection) {
327
+ log("AI detected:", aiDetection);
328
+ }
329
+ initialized = true;
330
+ if (!userConfig.disableAutoPageview) {
331
+ pageview();
332
+ }
333
+ if (!userConfig.disableBehavioral) {
334
+ setupBehavioralTracking();
335
+ }
336
+ log("Initialization complete");
337
+ }
338
+ function pageview(customUrl) {
339
+ if (!initialized) {
340
+ log("Not initialized, call init() first");
341
+ return;
342
+ }
343
+ const url = customUrl || window.location.href;
344
+ const payload = {
345
+ visitor_id: visitorId,
346
+ session_id: sessionId,
347
+ url,
348
+ referrer: document.referrer || null,
349
+ title: document.title || null,
350
+ utm_source: extractUTMParams(url).utm_source || null,
351
+ utm_medium: extractUTMParams(url).utm_medium || null,
352
+ utm_campaign: extractUTMParams(url).utm_campaign || null,
353
+ user_agent: navigator.userAgent,
354
+ screen_width: window.screen?.width,
355
+ screen_height: window.screen?.height,
356
+ language: navigator.language,
357
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
358
+ tracker_version: VERSION,
359
+ navigation_timing: navigationTiming,
360
+ ai_platform: aiDetection?.platform || null,
361
+ is_ai_referrer: aiDetection?.isAI || false
362
+ };
363
+ log("Pageview:", payload);
364
+ safeFetch(endpoint(DEFAULT_CONFIG.endpoints.visit), {
365
+ method: "POST",
366
+ headers: { "Content-Type": "application/json" },
367
+ body: JSON.stringify(payload)
368
+ });
369
+ }
370
+ function track(eventName, options = {}) {
371
+ if (!initialized) {
372
+ log("Not initialized, call init() first");
373
+ return;
374
+ }
375
+ const payload = {
376
+ visitor_id: visitorId,
377
+ session_id: sessionId,
378
+ event_name: eventName,
379
+ event_type: "custom",
380
+ properties: options.properties || {},
381
+ revenue: options.revenue,
382
+ currency: options.currency || "USD",
383
+ url: window.location.href,
384
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
385
+ tracker_version: VERSION
386
+ };
387
+ log("Event:", eventName, payload);
388
+ safeFetch(endpoint("/api/ingest/event"), {
389
+ method: "POST",
390
+ headers: { "Content-Type": "application/json" },
391
+ body: JSON.stringify(payload)
392
+ });
393
+ }
394
+ function conversion(eventName, revenue, currency = "USD") {
395
+ track(eventName, { revenue, currency, properties: { type: "conversion" } });
396
+ }
397
+ function identify(userId, traits = {}) {
398
+ if (!initialized) {
399
+ log("Not initialized, call init() first");
400
+ return;
401
+ }
402
+ log("Identify:", userId, traits);
403
+ const payload = {
404
+ visitor_id: visitorId,
405
+ session_id: sessionId,
406
+ user_id: userId,
407
+ traits,
408
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
409
+ };
410
+ safeFetch(endpoint("/api/ingest/identify"), {
411
+ method: "POST",
412
+ headers: { "Content-Type": "application/json" },
413
+ body: JSON.stringify(payload)
414
+ });
415
+ }
416
+ function setupBehavioralTracking() {
417
+ let maxScrollDepth = 0;
418
+ let lastScrollUpdate = 0;
419
+ let lastTimeUpdate = Date.now();
420
+ let scrollTicking = false;
421
+ window.addEventListener("scroll", () => {
422
+ if (!scrollTicking) {
423
+ requestAnimationFrame(() => {
424
+ const scrollPercent = Math.round(
425
+ (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
426
+ );
427
+ if (scrollPercent > maxScrollDepth) {
428
+ maxScrollDepth = scrollPercent;
429
+ const milestones = [25, 50, 75, 100];
430
+ for (const milestone of milestones) {
431
+ if (scrollPercent >= milestone && lastScrollUpdate < milestone) {
432
+ lastScrollUpdate = milestone;
433
+ sendBehavioralEvent("scroll_depth", { depth: milestone });
434
+ }
435
+ }
436
+ }
437
+ scrollTicking = false;
438
+ });
439
+ scrollTicking = true;
440
+ }
441
+ });
442
+ const trackTimeSpent = () => {
443
+ const now = Date.now();
444
+ const delta = now - lastTimeUpdate;
445
+ if (delta >= DEFAULT_CONFIG.timeSpentThresholdMs) {
446
+ lastTimeUpdate = now;
447
+ sendBehavioralEvent("time_spent", {
448
+ seconds: Math.round(delta / 1e3),
449
+ total_seconds: Math.round((now - (sessionStartTime || now)) / 1e3)
450
+ });
451
+ }
452
+ };
453
+ document.addEventListener("visibilitychange", () => {
454
+ if (document.visibilityState === "hidden") {
455
+ trackTimeSpent();
456
+ }
457
+ });
458
+ window.addEventListener("beforeunload", () => {
459
+ trackTimeSpent();
460
+ if (maxScrollDepth > 0) {
461
+ sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
462
+ visitor_id: visitorId,
463
+ session_id: sessionId,
464
+ event_type: "scroll_depth_final",
465
+ data: { depth: maxScrollDepth },
466
+ url: window.location.href
467
+ });
468
+ }
469
+ });
470
+ document.addEventListener("focusin", (e) => {
471
+ const target = e.target;
472
+ if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT") {
473
+ sendBehavioralEvent("form_focus", {
474
+ field_type: target.tagName.toLowerCase(),
475
+ field_name: target.name || target.id || "unknown"
476
+ });
477
+ }
478
+ });
479
+ document.addEventListener("submit", (e) => {
480
+ const form = e.target;
481
+ sendBehavioralEvent("form_submit", {
482
+ form_id: form.id || form.name || "unknown",
483
+ form_action: form.action ? new URL(form.action).pathname : "unknown"
484
+ });
485
+ });
486
+ document.addEventListener("click", (e) => {
487
+ const target = e.target;
488
+ const link = target.closest("a");
489
+ if (link && link.href) {
490
+ const isExternal = link.hostname !== window.location.hostname;
491
+ sendBehavioralEvent("click", {
492
+ element: "link",
493
+ href: truncateText(link.href, 200),
494
+ text: truncateText(link.textContent || "", 100),
495
+ is_external: isExternal
496
+ });
497
+ }
498
+ });
499
+ }
500
+ function sendBehavioralEvent(eventType, data) {
501
+ const payload = {
502
+ visitor_id: visitorId,
503
+ session_id: sessionId,
504
+ event_type: eventType,
505
+ data,
506
+ url: window.location.href,
507
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
508
+ tracker_version: VERSION
509
+ };
510
+ log("Behavioral:", eventType, data);
511
+ safeFetch(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
512
+ method: "POST",
513
+ headers: { "Content-Type": "application/json" },
514
+ body: JSON.stringify(payload)
515
+ });
516
+ }
517
+ function getCurrentSessionId() {
518
+ return sessionId;
519
+ }
520
+ function getCurrentVisitorId() {
521
+ return visitorId;
522
+ }
523
+ function getAIDetectionResult() {
524
+ return aiDetection;
525
+ }
526
+ function getNavigationTimingResult() {
527
+ return navigationTiming;
528
+ }
529
+ function isTrackerInitialized() {
530
+ return initialized;
531
+ }
532
+ function reset() {
533
+ log("Resetting tracker");
534
+ initialized = false;
535
+ visitorId = null;
536
+ sessionId = null;
537
+ sessionStartTime = null;
538
+ navigationTiming = null;
539
+ aiDetection = null;
540
+ try {
541
+ sessionStorage.removeItem("loamly_session");
542
+ sessionStorage.removeItem("loamly_start");
543
+ } catch {
544
+ }
545
+ }
546
+ function setDebug(enabled) {
547
+ debugMode = enabled;
548
+ log("Debug mode:", enabled ? "enabled" : "disabled");
549
+ }
550
+ var loamly = {
551
+ init,
552
+ pageview,
553
+ track,
554
+ conversion,
555
+ identify,
556
+ getSessionId: getCurrentSessionId,
557
+ getVisitorId: getCurrentVisitorId,
558
+ getAIDetection: getAIDetectionResult,
559
+ getNavigationTiming: getNavigationTimingResult,
560
+ isInitialized: isTrackerInitialized,
561
+ reset,
562
+ debug: setDebug
563
+ };
564
+ // Annotate the CommonJS export names for ESM import in node:
565
+ 0 && (module.exports = {
566
+ AI_BOT_PATTERNS,
567
+ AI_PLATFORMS,
568
+ VERSION,
569
+ detectAIFromReferrer,
570
+ detectAIFromUTM,
571
+ detectNavigationType,
572
+ loamly
573
+ });
574
+ /**
575
+ * Loamly Tracker
576
+ *
577
+ * Open-source AI traffic detection for websites.
578
+ * See what AI tells your customers — and track when they click.
579
+ *
580
+ * @module @loamly/tracker
581
+ * @license MIT
582
+ * @see https://loamly.ai
583
+ */
584
+ //# sourceMappingURL=index.cjs.map