@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 +82 -0
- package/dist/index.cjs +584 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +146 -0
- package/dist/index.d.ts +146 -0
- package/dist/index.mjs +551 -0
- package/dist/index.mjs.map +1 -0
- package/dist/loamly.iife.global.js +594 -0
- package/dist/loamly.iife.global.js.map +1 -0
- package/dist/loamly.iife.min.global.js +2 -0
- package/dist/loamly.iife.min.global.js.map +1 -0
- package/package.json +68 -0
- package/src/browser.ts +81 -0
- package/src/config.ts +59 -0
- package/src/core.ts +428 -0
- package/src/detection/index.ts +13 -0
- package/src/detection/navigation-timing.ts +117 -0
- package/src/detection/referrer.ts +98 -0
- package/src/index.ts +31 -0
- package/src/types.ts +100 -0
- package/src/utils.ts +130 -0
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
|
+
[](https://www.npmjs.com/package/@loamly/tracker)
|
|
8
|
+
[](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
|