@loamly/tracker 2.1.0 → 2.4.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 +4 -3
- package/dist/index.cjs +1580 -1286
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +23 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.mjs +1580 -1286
- package/dist/index.mjs.map +1 -1
- package/dist/loamly.iife.global.js +1578 -1270
- package/dist/loamly.iife.global.js.map +1 -1
- package/dist/loamly.iife.min.global.js +1 -1
- package/dist/loamly.iife.min.global.js.map +1 -1
- package/package.json +1 -1
- package/src/behavioral/form-tracker.ts +107 -1
- package/src/browser.ts +25 -6
- package/src/config.ts +1 -1
- package/src/core.ts +386 -132
- package/src/index.ts +1 -1
- package/src/infrastructure/event-queue.ts +58 -32
- package/src/infrastructure/ping.ts +19 -3
- package/src/types.ts +23 -0
|
@@ -25,633 +25,584 @@ var Loamly = (() => {
|
|
|
25
25
|
loamly: () => loamly
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
// src/
|
|
29
|
-
var VERSION = "2.1.0";
|
|
28
|
+
// src/behavioral/form-tracker.ts
|
|
30
29
|
var DEFAULT_CONFIG = {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
30
|
+
sensitiveFields: [
|
|
31
|
+
"password",
|
|
32
|
+
"pwd",
|
|
33
|
+
"pass",
|
|
34
|
+
"credit",
|
|
35
|
+
"card",
|
|
36
|
+
"cvv",
|
|
37
|
+
"cvc",
|
|
38
|
+
"ssn",
|
|
39
|
+
"social",
|
|
40
|
+
"secret",
|
|
41
|
+
"token",
|
|
42
|
+
"key",
|
|
43
|
+
"pin",
|
|
44
|
+
"security",
|
|
45
|
+
"answer"
|
|
46
|
+
],
|
|
47
|
+
trackableFields: [
|
|
48
|
+
"email",
|
|
49
|
+
"name",
|
|
50
|
+
"phone",
|
|
51
|
+
"company",
|
|
52
|
+
"first",
|
|
53
|
+
"last",
|
|
54
|
+
"city",
|
|
55
|
+
"country",
|
|
56
|
+
"domain",
|
|
57
|
+
"website",
|
|
58
|
+
"url",
|
|
59
|
+
"organization"
|
|
60
|
+
],
|
|
61
|
+
thankYouPatterns: [
|
|
62
|
+
/thank[-_]?you/i,
|
|
63
|
+
/success/i,
|
|
64
|
+
/confirmation/i,
|
|
65
|
+
/submitted/i,
|
|
66
|
+
/complete/i
|
|
67
|
+
],
|
|
68
|
+
// LOA-482: Enable field value capture by default
|
|
69
|
+
captureFieldValues: true,
|
|
70
|
+
maxFieldValueLength: 200
|
|
62
71
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
const dnsTime = nav.domainLookupEnd - nav.domainLookupStart;
|
|
83
|
-
if (dnsTime === 0) {
|
|
84
|
-
pasteScore += 0.15;
|
|
85
|
-
signals.push("no_dns_lookup");
|
|
86
|
-
}
|
|
87
|
-
const connectTime = nav.connectEnd - nav.connectStart;
|
|
88
|
-
if (connectTime === 0) {
|
|
89
|
-
pasteScore += 0.15;
|
|
90
|
-
signals.push("no_tcp_connect");
|
|
91
|
-
}
|
|
92
|
-
if (nav.redirectCount === 0) {
|
|
93
|
-
pasteScore += 0.1;
|
|
94
|
-
signals.push("no_redirects");
|
|
95
|
-
}
|
|
96
|
-
const timingVariance = calculateTimingVariance(nav);
|
|
97
|
-
if (timingVariance < 10) {
|
|
98
|
-
pasteScore += 0.15;
|
|
99
|
-
signals.push("uniform_timing");
|
|
100
|
-
}
|
|
101
|
-
if (!document.referrer || document.referrer === "") {
|
|
102
|
-
pasteScore += 0.1;
|
|
103
|
-
signals.push("no_referrer");
|
|
104
|
-
}
|
|
105
|
-
const confidence = Math.min(pasteScore, 1);
|
|
106
|
-
const nav_type = pasteScore >= 0.5 ? "likely_paste" : "likely_click";
|
|
107
|
-
return {
|
|
108
|
-
nav_type,
|
|
109
|
-
confidence: Math.round(confidence * 1e3) / 1e3,
|
|
110
|
-
signals
|
|
111
|
-
};
|
|
112
|
-
} catch {
|
|
113
|
-
return { nav_type: "unknown", confidence: 0, signals: ["detection_error"] };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
function calculateTimingVariance(nav) {
|
|
117
|
-
const timings = [
|
|
118
|
-
nav.fetchStart - nav.startTime,
|
|
119
|
-
nav.domainLookupEnd - nav.domainLookupStart,
|
|
120
|
-
nav.connectEnd - nav.connectStart,
|
|
121
|
-
nav.responseStart - nav.requestStart
|
|
122
|
-
].filter((t) => t >= 0);
|
|
123
|
-
if (timings.length === 0) return 100;
|
|
124
|
-
const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
125
|
-
const variance = timings.reduce((sum, t) => sum + Math.pow(t - mean, 2), 0) / timings.length;
|
|
126
|
-
return Math.sqrt(variance);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// src/detection/referrer.ts
|
|
130
|
-
function detectAIFromReferrer(referrer) {
|
|
131
|
-
if (!referrer) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
const url = new URL(referrer);
|
|
136
|
-
const hostname = url.hostname.toLowerCase();
|
|
137
|
-
for (const [pattern, platform] of Object.entries(AI_PLATFORMS)) {
|
|
138
|
-
if (hostname.includes(pattern) || referrer.includes(pattern)) {
|
|
139
|
-
return {
|
|
140
|
-
isAI: true,
|
|
141
|
-
platform,
|
|
142
|
-
confidence: 0.95,
|
|
143
|
-
// High confidence when referrer matches
|
|
144
|
-
method: "referrer"
|
|
145
|
-
};
|
|
72
|
+
var FormTracker = class {
|
|
73
|
+
constructor(config2 = {}) {
|
|
74
|
+
this.formStartTimes = /* @__PURE__ */ new Map();
|
|
75
|
+
this.interactedForms = /* @__PURE__ */ new Set();
|
|
76
|
+
this.mutationObserver = null;
|
|
77
|
+
this.handleFocusIn = (e) => {
|
|
78
|
+
const target = e.target;
|
|
79
|
+
if (!this.isFormField(target)) return;
|
|
80
|
+
const form = target.closest("form");
|
|
81
|
+
const formId = this.getFormId(form || target);
|
|
82
|
+
if (!this.formStartTimes.has(formId)) {
|
|
83
|
+
this.formStartTimes.set(formId, Date.now());
|
|
84
|
+
this.interactedForms.add(formId);
|
|
85
|
+
this.emitEvent({
|
|
86
|
+
event_type: "form_start",
|
|
87
|
+
form_id: formId,
|
|
88
|
+
form_type: this.detectFormType(form || target)
|
|
89
|
+
});
|
|
146
90
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
method: "referrer"
|
|
157
|
-
};
|
|
91
|
+
const fieldName = this.getFieldName(target);
|
|
92
|
+
if (fieldName && !this.isSensitiveField(fieldName)) {
|
|
93
|
+
this.emitEvent({
|
|
94
|
+
event_type: "form_field",
|
|
95
|
+
form_id: formId,
|
|
96
|
+
form_type: this.detectFormType(form || target),
|
|
97
|
+
field_name: this.sanitizeFieldName(fieldName),
|
|
98
|
+
field_type: target.type || target.tagName.toLowerCase()
|
|
99
|
+
});
|
|
158
100
|
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
101
|
+
};
|
|
102
|
+
this.handleSubmit = (e) => {
|
|
103
|
+
const form = e.target;
|
|
104
|
+
if (!form || form.tagName !== "FORM") return;
|
|
105
|
+
const formId = this.getFormId(form);
|
|
106
|
+
const startTime = this.formStartTimes.get(formId);
|
|
107
|
+
const { fields, emailSubmitted } = this.captureFormFields(form);
|
|
108
|
+
this.emitEvent({
|
|
109
|
+
event_type: "form_submit",
|
|
110
|
+
form_id: formId,
|
|
111
|
+
form_type: this.detectFormType(form),
|
|
112
|
+
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
113
|
+
is_conversion: true,
|
|
114
|
+
submit_source: "submit",
|
|
115
|
+
fields: fields.length > 0 ? fields : void 0,
|
|
116
|
+
email_submitted: emailSubmitted
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
this.handleClick = (e) => {
|
|
120
|
+
const target = e.target;
|
|
121
|
+
if (target.closest(".hs-button") || target.closest('[type="submit"]')) {
|
|
122
|
+
const form = target.closest("form");
|
|
123
|
+
if (form && form.classList.contains("hs-form")) {
|
|
124
|
+
const formId = this.getFormId(form);
|
|
125
|
+
const startTime = this.formStartTimes.get(formId);
|
|
126
|
+
const { fields, emailSubmitted } = this.captureFormFields(form);
|
|
127
|
+
this.emitEvent({
|
|
128
|
+
event_type: "form_submit",
|
|
129
|
+
form_id: formId,
|
|
130
|
+
form_type: "hubspot",
|
|
131
|
+
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
132
|
+
is_conversion: true,
|
|
133
|
+
submit_source: "click",
|
|
134
|
+
fields: fields.length > 0 ? fields : void 0,
|
|
135
|
+
email_submitted: emailSubmitted
|
|
136
|
+
});
|
|
177
137
|
}
|
|
178
138
|
}
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
139
|
+
if (target.closest('[data-qa="submit-button"]')) {
|
|
140
|
+
this.emitEvent({
|
|
141
|
+
event_type: "form_submit",
|
|
142
|
+
form_id: "typeform_embed",
|
|
143
|
+
form_type: "typeform",
|
|
144
|
+
is_conversion: true,
|
|
145
|
+
submit_source: "click"
|
|
146
|
+
});
|
|
186
147
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
var NAIVE_BAYES_WEIGHTS = {
|
|
196
|
-
human: {
|
|
197
|
-
time_to_first_click_delayed: 0.85,
|
|
198
|
-
time_to_first_click_normal: 0.75,
|
|
199
|
-
time_to_first_click_fast: 0.5,
|
|
200
|
-
time_to_first_click_immediate: 0.25,
|
|
201
|
-
scroll_speed_variable: 0.8,
|
|
202
|
-
scroll_speed_erratic: 0.7,
|
|
203
|
-
scroll_speed_uniform: 0.35,
|
|
204
|
-
scroll_speed_none: 0.45,
|
|
205
|
-
nav_timing_click: 0.75,
|
|
206
|
-
nav_timing_unknown: 0.55,
|
|
207
|
-
nav_timing_paste: 0.35,
|
|
208
|
-
has_referrer: 0.7,
|
|
209
|
-
no_referrer: 0.45,
|
|
210
|
-
homepage_landing: 0.65,
|
|
211
|
-
deep_landing: 0.5,
|
|
212
|
-
mouse_movement_curved: 0.9,
|
|
213
|
-
mouse_movement_linear: 0.3,
|
|
214
|
-
mouse_movement_none: 0.4,
|
|
215
|
-
form_fill_normal: 0.85,
|
|
216
|
-
form_fill_fast: 0.6,
|
|
217
|
-
form_fill_instant: 0.2,
|
|
218
|
-
focus_blur_normal: 0.75,
|
|
219
|
-
focus_blur_rapid: 0.45
|
|
220
|
-
},
|
|
221
|
-
ai_influenced: {
|
|
222
|
-
time_to_first_click_immediate: 0.75,
|
|
223
|
-
time_to_first_click_fast: 0.55,
|
|
224
|
-
time_to_first_click_normal: 0.4,
|
|
225
|
-
time_to_first_click_delayed: 0.35,
|
|
226
|
-
scroll_speed_none: 0.55,
|
|
227
|
-
scroll_speed_uniform: 0.7,
|
|
228
|
-
scroll_speed_variable: 0.35,
|
|
229
|
-
scroll_speed_erratic: 0.4,
|
|
230
|
-
nav_timing_paste: 0.75,
|
|
231
|
-
nav_timing_unknown: 0.5,
|
|
232
|
-
nav_timing_click: 0.35,
|
|
233
|
-
no_referrer: 0.65,
|
|
234
|
-
has_referrer: 0.4,
|
|
235
|
-
deep_landing: 0.6,
|
|
236
|
-
homepage_landing: 0.45,
|
|
237
|
-
mouse_movement_none: 0.6,
|
|
238
|
-
mouse_movement_linear: 0.75,
|
|
239
|
-
mouse_movement_curved: 0.25,
|
|
240
|
-
form_fill_instant: 0.8,
|
|
241
|
-
form_fill_fast: 0.55,
|
|
242
|
-
form_fill_normal: 0.3,
|
|
243
|
-
focus_blur_rapid: 0.6,
|
|
244
|
-
focus_blur_normal: 0.4
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
|
-
var PRIORS = {
|
|
248
|
-
human: 0.85,
|
|
249
|
-
ai_influenced: 0.15
|
|
250
|
-
};
|
|
251
|
-
var DEFAULT_WEIGHT = 0.5;
|
|
252
|
-
var BehavioralClassifier = class {
|
|
253
|
-
/**
|
|
254
|
-
* Create a new classifier
|
|
255
|
-
* @param minSessionTimeMs Minimum session time before classification (default: 10s)
|
|
256
|
-
*/
|
|
257
|
-
constructor(minSessionTimeMs = 1e4) {
|
|
258
|
-
this.classified = false;
|
|
259
|
-
this.result = null;
|
|
260
|
-
this.onClassify = null;
|
|
261
|
-
this.minSessionTime = minSessionTimeMs;
|
|
262
|
-
this.data = {
|
|
263
|
-
firstClickTime: null,
|
|
264
|
-
scrollEvents: [],
|
|
265
|
-
mouseEvents: [],
|
|
266
|
-
formEvents: [],
|
|
267
|
-
focusBlurEvents: [],
|
|
268
|
-
startTime: Date.now()
|
|
148
|
+
};
|
|
149
|
+
this.config = {
|
|
150
|
+
...DEFAULT_CONFIG,
|
|
151
|
+
...config2,
|
|
152
|
+
sensitiveFields: [
|
|
153
|
+
...DEFAULT_CONFIG.sensitiveFields,
|
|
154
|
+
...config2.sensitiveFields || []
|
|
155
|
+
]
|
|
269
156
|
};
|
|
270
157
|
}
|
|
271
158
|
/**
|
|
272
|
-
*
|
|
273
|
-
*/
|
|
274
|
-
setOnClassify(callback) {
|
|
275
|
-
this.onClassify = callback;
|
|
276
|
-
}
|
|
277
|
-
/**
|
|
278
|
-
* Record a click event
|
|
279
|
-
*/
|
|
280
|
-
recordClick() {
|
|
281
|
-
if (this.data.firstClickTime === null) {
|
|
282
|
-
this.data.firstClickTime = Date.now();
|
|
283
|
-
}
|
|
284
|
-
this.checkAndClassify();
|
|
285
|
-
}
|
|
286
|
-
/**
|
|
287
|
-
* Record a scroll event
|
|
159
|
+
* Start tracking forms
|
|
288
160
|
*/
|
|
289
|
-
|
|
290
|
-
this.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
this.
|
|
161
|
+
start() {
|
|
162
|
+
document.addEventListener("focusin", this.handleFocusIn, { passive: true });
|
|
163
|
+
document.addEventListener("submit", this.handleSubmit);
|
|
164
|
+
document.addEventListener("click", this.handleClick, { passive: true });
|
|
165
|
+
this.startMutationObserver();
|
|
166
|
+
this.checkThankYouPage();
|
|
167
|
+
this.scanForEmbeddedForms();
|
|
295
168
|
}
|
|
296
169
|
/**
|
|
297
|
-
*
|
|
170
|
+
* Stop tracking
|
|
298
171
|
*/
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
this.checkAndClassify();
|
|
172
|
+
stop() {
|
|
173
|
+
document.removeEventListener("focusin", this.handleFocusIn);
|
|
174
|
+
document.removeEventListener("submit", this.handleSubmit);
|
|
175
|
+
document.removeEventListener("click", this.handleClick);
|
|
176
|
+
this.mutationObserver?.disconnect();
|
|
305
177
|
}
|
|
306
178
|
/**
|
|
307
|
-
*
|
|
179
|
+
* Get forms that had interaction
|
|
308
180
|
*/
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (!existing) {
|
|
312
|
-
this.data.formEvents.push({ fieldId, startTime: Date.now(), endTime: 0 });
|
|
313
|
-
}
|
|
181
|
+
getInteractedForms() {
|
|
182
|
+
return Array.from(this.interactedForms);
|
|
314
183
|
}
|
|
315
184
|
/**
|
|
316
|
-
*
|
|
185
|
+
* LOA-482: Capture form field values with privacy-safe sanitization
|
|
186
|
+
* - Never captures sensitive fields (password, credit card, etc.)
|
|
187
|
+
* - Truncates values to maxFieldValueLength
|
|
188
|
+
* - Extracts email if found
|
|
317
189
|
*/
|
|
318
|
-
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
190
|
+
captureFormFields(form) {
|
|
191
|
+
const fields = [];
|
|
192
|
+
let emailSubmitted;
|
|
193
|
+
if (!this.config.captureFieldValues) {
|
|
194
|
+
return { fields, emailSubmitted };
|
|
322
195
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
196
|
+
try {
|
|
197
|
+
const formData = new FormData(form);
|
|
198
|
+
for (const [name, value] of formData.entries()) {
|
|
199
|
+
if (this.isSensitiveField(name)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const input = form.elements.namedItem(name);
|
|
203
|
+
const inputType = input?.type || "text";
|
|
204
|
+
if (inputType === "file" || value instanceof File) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const stringValue = String(value);
|
|
208
|
+
if (this.isEmailField(name, stringValue)) {
|
|
209
|
+
emailSubmitted = stringValue.substring(0, 254);
|
|
210
|
+
}
|
|
211
|
+
const truncatedValue = stringValue.length > this.config.maxFieldValueLength ? stringValue.substring(0, this.config.maxFieldValueLength) + "..." : stringValue;
|
|
212
|
+
fields.push({
|
|
213
|
+
name: this.sanitizeFieldName(name),
|
|
214
|
+
type: inputType,
|
|
215
|
+
value: truncatedValue
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
} catch (err) {
|
|
219
|
+
console.warn("[Loamly] Failed to capture form fields:", err);
|
|
332
220
|
}
|
|
221
|
+
return { fields, emailSubmitted };
|
|
333
222
|
}
|
|
334
223
|
/**
|
|
335
|
-
* Check if
|
|
224
|
+
* Check if a field contains an email
|
|
336
225
|
*/
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (!hasData) return;
|
|
343
|
-
this.classify();
|
|
226
|
+
isEmailField(fieldName, value) {
|
|
227
|
+
const lowerName = fieldName.toLowerCase();
|
|
228
|
+
const isEmailName = lowerName.includes("email") || lowerName === "e-mail";
|
|
229
|
+
const isEmailValue = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
230
|
+
return isEmailName && isEmailValue;
|
|
344
231
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
232
|
+
startMutationObserver() {
|
|
233
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
234
|
+
for (const mutation of mutations) {
|
|
235
|
+
for (const node of mutation.addedNodes) {
|
|
236
|
+
if (node instanceof HTMLElement) {
|
|
237
|
+
if (node.classList?.contains("hs-form") || node.querySelector?.(".hs-form")) {
|
|
238
|
+
this.trackEmbeddedForm(node, "hubspot");
|
|
239
|
+
}
|
|
240
|
+
if (node.classList?.contains("typeform-widget") || node.querySelector?.("[data-tf-widget]")) {
|
|
241
|
+
this.trackEmbeddedForm(node, "typeform");
|
|
242
|
+
}
|
|
243
|
+
if (node.classList?.contains("jotform-form") || node.querySelector?.(".jotform-form")) {
|
|
244
|
+
this.trackEmbeddedForm(node, "jotform");
|
|
245
|
+
}
|
|
246
|
+
if (node.classList?.contains("gform_wrapper") || node.querySelector?.(".gform_wrapper")) {
|
|
247
|
+
this.trackEmbeddedForm(node, "gravity");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
this.mutationObserver.observe(document.body, {
|
|
254
|
+
childList: true,
|
|
255
|
+
subtree: true
|
|
256
|
+
});
|
|
351
257
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
const maxLog = Math.max(humanLogProb, aiLogProb);
|
|
367
|
-
const humanExp = Math.exp(humanLogProb - maxLog);
|
|
368
|
-
const aiExp = Math.exp(aiLogProb - maxLog);
|
|
369
|
-
const total = humanExp + aiExp;
|
|
370
|
-
const humanProbability = humanExp / total;
|
|
371
|
-
const aiProbability = aiExp / total;
|
|
372
|
-
let classification;
|
|
373
|
-
let confidence;
|
|
374
|
-
if (humanProbability > 0.6) {
|
|
375
|
-
classification = "human";
|
|
376
|
-
confidence = humanProbability;
|
|
377
|
-
} else if (aiProbability > 0.6) {
|
|
378
|
-
classification = "ai_influenced";
|
|
379
|
-
confidence = aiProbability;
|
|
380
|
-
} else {
|
|
381
|
-
classification = "uncertain";
|
|
382
|
-
confidence = Math.max(humanProbability, aiProbability);
|
|
383
|
-
}
|
|
384
|
-
this.result = {
|
|
385
|
-
classification,
|
|
386
|
-
humanProbability,
|
|
387
|
-
aiProbability,
|
|
388
|
-
confidence,
|
|
389
|
-
signals,
|
|
390
|
-
timestamp: Date.now(),
|
|
391
|
-
sessionDurationMs: sessionDuration
|
|
392
|
-
};
|
|
393
|
-
this.classified = true;
|
|
394
|
-
if (this.onClassify) {
|
|
395
|
-
this.onClassify(this.result);
|
|
396
|
-
}
|
|
397
|
-
return this.result;
|
|
258
|
+
scanForEmbeddedForms() {
|
|
259
|
+
document.querySelectorAll(".hs-form").forEach((form) => {
|
|
260
|
+
this.trackEmbeddedForm(form, "hubspot");
|
|
261
|
+
});
|
|
262
|
+
document.querySelectorAll("[data-tf-widget], .typeform-widget").forEach((form) => {
|
|
263
|
+
this.trackEmbeddedForm(form, "typeform");
|
|
264
|
+
});
|
|
265
|
+
document.querySelectorAll(".jotform-form").forEach((form) => {
|
|
266
|
+
this.trackEmbeddedForm(form, "jotform");
|
|
267
|
+
});
|
|
268
|
+
document.querySelectorAll(".gform_wrapper").forEach((form) => {
|
|
269
|
+
this.trackEmbeddedForm(form, "gravity");
|
|
270
|
+
});
|
|
398
271
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
} else if (timeToClick < 1e4) {
|
|
411
|
-
signals.push("time_to_first_click_normal");
|
|
412
|
-
} else {
|
|
413
|
-
signals.push("time_to_first_click_delayed");
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if (this.data.scrollEvents.length === 0) {
|
|
417
|
-
signals.push("scroll_speed_none");
|
|
418
|
-
} else if (this.data.scrollEvents.length >= 3) {
|
|
419
|
-
const scrollDeltas = [];
|
|
420
|
-
for (let i = 1; i < this.data.scrollEvents.length; i++) {
|
|
421
|
-
const delta = this.data.scrollEvents[i].time - this.data.scrollEvents[i - 1].time;
|
|
422
|
-
scrollDeltas.push(delta);
|
|
423
|
-
}
|
|
424
|
-
const mean = scrollDeltas.reduce((a, b) => a + b, 0) / scrollDeltas.length;
|
|
425
|
-
const variance = scrollDeltas.reduce((sum, d) => sum + Math.pow(d - mean, 2), 0) / scrollDeltas.length;
|
|
426
|
-
const stdDev = Math.sqrt(variance);
|
|
427
|
-
const cv = mean > 0 ? stdDev / mean : 0;
|
|
428
|
-
if (cv < 0.2) {
|
|
429
|
-
signals.push("scroll_speed_uniform");
|
|
430
|
-
} else if (cv < 0.6) {
|
|
431
|
-
signals.push("scroll_speed_variable");
|
|
432
|
-
} else {
|
|
433
|
-
signals.push("scroll_speed_erratic");
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
if (this.data.mouseEvents.length === 0) {
|
|
437
|
-
signals.push("mouse_movement_none");
|
|
438
|
-
} else if (this.data.mouseEvents.length >= 10) {
|
|
439
|
-
const n = Math.min(this.data.mouseEvents.length, 20);
|
|
440
|
-
const recentMouse = this.data.mouseEvents.slice(-n);
|
|
441
|
-
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
442
|
-
for (const event of recentMouse) {
|
|
443
|
-
sumX += event.x;
|
|
444
|
-
sumY += event.y;
|
|
445
|
-
sumXY += event.x * event.y;
|
|
446
|
-
sumX2 += event.x * event.x;
|
|
447
|
-
}
|
|
448
|
-
const denominator = n * sumX2 - sumX * sumX;
|
|
449
|
-
const slope = denominator !== 0 ? (n * sumXY - sumX * sumY) / denominator : 0;
|
|
450
|
-
const intercept = (sumY - slope * sumX) / n;
|
|
451
|
-
let ssRes = 0, ssTot = 0;
|
|
452
|
-
const yMean = sumY / n;
|
|
453
|
-
for (const event of recentMouse) {
|
|
454
|
-
const yPred = slope * event.x + intercept;
|
|
455
|
-
ssRes += Math.pow(event.y - yPred, 2);
|
|
456
|
-
ssTot += Math.pow(event.y - yMean, 2);
|
|
457
|
-
}
|
|
458
|
-
const r2 = ssTot !== 0 ? 1 - ssRes / ssTot : 0;
|
|
459
|
-
if (r2 > 0.95) {
|
|
460
|
-
signals.push("mouse_movement_linear");
|
|
461
|
-
} else {
|
|
462
|
-
signals.push("mouse_movement_curved");
|
|
272
|
+
trackEmbeddedForm(element, type) {
|
|
273
|
+
const formId = `${type}_${this.getFormId(element)}`;
|
|
274
|
+
element.addEventListener("focusin", () => {
|
|
275
|
+
if (!this.formStartTimes.has(formId)) {
|
|
276
|
+
this.formStartTimes.set(formId, Date.now());
|
|
277
|
+
this.interactedForms.add(formId);
|
|
278
|
+
this.emitEvent({
|
|
279
|
+
event_type: "form_start",
|
|
280
|
+
form_id: formId,
|
|
281
|
+
form_type: type
|
|
282
|
+
});
|
|
463
283
|
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
284
|
+
}, { passive: true });
|
|
285
|
+
}
|
|
286
|
+
checkThankYouPage() {
|
|
287
|
+
const url = window.location.href.toLowerCase();
|
|
288
|
+
const title = document.title.toLowerCase();
|
|
289
|
+
for (const pattern of this.config.thankYouPatterns) {
|
|
290
|
+
if (pattern.test(url) || pattern.test(title)) {
|
|
291
|
+
this.emitEvent({
|
|
292
|
+
event_type: "form_success",
|
|
293
|
+
form_id: "page_conversion",
|
|
294
|
+
form_type: "unknown",
|
|
295
|
+
is_conversion: true,
|
|
296
|
+
submit_source: "thank_you"
|
|
297
|
+
});
|
|
298
|
+
break;
|
|
474
299
|
}
|
|
475
300
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
301
|
+
}
|
|
302
|
+
isFormField(element) {
|
|
303
|
+
const tagName = element.tagName;
|
|
304
|
+
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
305
|
+
}
|
|
306
|
+
getFormId(element) {
|
|
307
|
+
if (!element) return "unknown";
|
|
308
|
+
return element.id || element.getAttribute("name") || element.getAttribute("data-form-id") || "form_" + Math.random().toString(36).substring(2, 8);
|
|
309
|
+
}
|
|
310
|
+
getFieldName(input) {
|
|
311
|
+
return input.name || input.id || input.getAttribute("data-name") || "";
|
|
312
|
+
}
|
|
313
|
+
isSensitiveField(fieldName) {
|
|
314
|
+
const lowerName = fieldName.toLowerCase();
|
|
315
|
+
return this.config.sensitiveFields.some((sensitive) => lowerName.includes(sensitive));
|
|
316
|
+
}
|
|
317
|
+
sanitizeFieldName(fieldName) {
|
|
318
|
+
return fieldName.replace(/[0-9]+/g, "*").substring(0, 50);
|
|
319
|
+
}
|
|
320
|
+
detectFormType(element) {
|
|
321
|
+
if (element.classList.contains("hs-form") || element.closest(".hs-form")) {
|
|
322
|
+
return "hubspot";
|
|
488
323
|
}
|
|
489
|
-
|
|
324
|
+
if (element.classList.contains("typeform-widget") || element.closest("[data-tf-widget]")) {
|
|
325
|
+
return "typeform";
|
|
326
|
+
}
|
|
327
|
+
if (element.classList.contains("jotform-form") || element.closest(".jotform-form")) {
|
|
328
|
+
return "jotform";
|
|
329
|
+
}
|
|
330
|
+
if (element.classList.contains("gform_wrapper") || element.closest(".gform_wrapper")) {
|
|
331
|
+
return "gravity";
|
|
332
|
+
}
|
|
333
|
+
if (element.tagName === "FORM") {
|
|
334
|
+
return "native";
|
|
335
|
+
}
|
|
336
|
+
return "unknown";
|
|
337
|
+
}
|
|
338
|
+
emitEvent(event) {
|
|
339
|
+
this.config.onFormEvent?.(event);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// src/behavioral/scroll-tracker.ts
|
|
344
|
+
var DEFAULT_CHUNKS = [30, 60, 90, 100];
|
|
345
|
+
var ScrollTracker = class {
|
|
346
|
+
constructor(config2 = {}) {
|
|
347
|
+
this.maxDepth = 0;
|
|
348
|
+
this.reportedChunks = /* @__PURE__ */ new Set();
|
|
349
|
+
this.ticking = false;
|
|
350
|
+
this.isVisible = true;
|
|
351
|
+
this.handleScroll = () => {
|
|
352
|
+
if (!this.ticking && this.isVisible) {
|
|
353
|
+
requestAnimationFrame(() => {
|
|
354
|
+
this.checkScrollDepth();
|
|
355
|
+
this.ticking = false;
|
|
356
|
+
});
|
|
357
|
+
this.ticking = true;
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
this.handleVisibility = () => {
|
|
361
|
+
this.isVisible = document.visibilityState === "visible";
|
|
362
|
+
};
|
|
363
|
+
this.config = {
|
|
364
|
+
chunks: DEFAULT_CHUNKS,
|
|
365
|
+
...config2
|
|
366
|
+
};
|
|
367
|
+
this.startTime = Date.now();
|
|
490
368
|
}
|
|
491
369
|
/**
|
|
492
|
-
*
|
|
370
|
+
* Start tracking scroll depth
|
|
493
371
|
*/
|
|
494
|
-
|
|
372
|
+
start() {
|
|
373
|
+
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
374
|
+
document.addEventListener("visibilitychange", this.handleVisibility);
|
|
375
|
+
this.checkScrollDepth();
|
|
495
376
|
}
|
|
496
377
|
/**
|
|
497
|
-
*
|
|
378
|
+
* Stop tracking
|
|
498
379
|
*/
|
|
499
|
-
|
|
500
|
-
|
|
380
|
+
stop() {
|
|
381
|
+
window.removeEventListener("scroll", this.handleScroll);
|
|
382
|
+
document.removeEventListener("visibilitychange", this.handleVisibility);
|
|
501
383
|
}
|
|
502
384
|
/**
|
|
503
|
-
*
|
|
385
|
+
* Get current max scroll depth
|
|
504
386
|
*/
|
|
505
|
-
|
|
506
|
-
return this.
|
|
507
|
-
}
|
|
508
|
-
};
|
|
509
|
-
|
|
510
|
-
// src/detection/focus-blur.ts
|
|
511
|
-
var FocusBlurAnalyzer = class {
|
|
512
|
-
constructor() {
|
|
513
|
-
this.sequence = [];
|
|
514
|
-
this.firstInteractionTime = null;
|
|
515
|
-
this.analyzed = false;
|
|
516
|
-
this.result = null;
|
|
517
|
-
this.pageLoadTime = performance.now();
|
|
387
|
+
getMaxDepth() {
|
|
388
|
+
return this.maxDepth;
|
|
518
389
|
}
|
|
519
390
|
/**
|
|
520
|
-
*
|
|
521
|
-
* Must be called after DOM is ready
|
|
391
|
+
* Get reported chunks
|
|
522
392
|
*/
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
this.recordEvent("focus", e.target);
|
|
526
|
-
}, true);
|
|
527
|
-
document.addEventListener("blur", (e) => {
|
|
528
|
-
this.recordEvent("blur", e.target);
|
|
529
|
-
}, true);
|
|
530
|
-
window.addEventListener("focus", () => {
|
|
531
|
-
this.recordEvent("window_focus", null);
|
|
532
|
-
});
|
|
533
|
-
window.addEventListener("blur", () => {
|
|
534
|
-
this.recordEvent("window_blur", null);
|
|
535
|
-
});
|
|
536
|
-
const recordFirstInteraction = () => {
|
|
537
|
-
if (this.firstInteractionTime === null) {
|
|
538
|
-
this.firstInteractionTime = performance.now();
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
document.addEventListener("click", recordFirstInteraction, { once: true, passive: true });
|
|
542
|
-
document.addEventListener("keydown", recordFirstInteraction, { once: true, passive: true });
|
|
393
|
+
getReportedChunks() {
|
|
394
|
+
return Array.from(this.reportedChunks).sort((a, b) => a - b);
|
|
543
395
|
}
|
|
544
396
|
/**
|
|
545
|
-
*
|
|
397
|
+
* Get final scroll event (for unload)
|
|
546
398
|
*/
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
399
|
+
getFinalEvent() {
|
|
400
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
401
|
+
const viewportHeight = window.innerHeight;
|
|
402
|
+
return {
|
|
403
|
+
depth: this.maxDepth,
|
|
404
|
+
chunk: this.getChunkForDepth(this.maxDepth),
|
|
405
|
+
time_to_reach_ms: Date.now() - this.startTime,
|
|
406
|
+
total_height: docHeight,
|
|
407
|
+
viewport_height: viewportHeight
|
|
552
408
|
};
|
|
553
|
-
this.sequence.push(event);
|
|
554
|
-
if (this.sequence.length > 20) {
|
|
555
|
-
this.sequence = this.sequence.slice(-20);
|
|
556
|
-
}
|
|
557
409
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
if (
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const signals = [];
|
|
566
|
-
let confidence = 0;
|
|
567
|
-
const earlyEvents = this.sequence.filter((e) => e.timestamp < this.pageLoadTime + 500);
|
|
568
|
-
const hasEarlyWindowFocus = earlyEvents.some((e) => e.type === "window_focus");
|
|
569
|
-
if (hasEarlyWindowFocus) {
|
|
570
|
-
signals.push("early_window_focus");
|
|
571
|
-
confidence += 0.15;
|
|
572
|
-
}
|
|
573
|
-
const hasEarlyBodyFocus = earlyEvents.some(
|
|
574
|
-
(e) => e.type === "focus" && e.target === "BODY"
|
|
575
|
-
);
|
|
576
|
-
if (hasEarlyBodyFocus) {
|
|
577
|
-
signals.push("early_body_focus");
|
|
578
|
-
confidence += 0.15;
|
|
579
|
-
}
|
|
580
|
-
const hasLinkFocus = this.sequence.some(
|
|
581
|
-
(e) => e.type === "focus" && e.target === "A"
|
|
582
|
-
);
|
|
583
|
-
if (!hasLinkFocus) {
|
|
584
|
-
signals.push("no_link_focus");
|
|
585
|
-
confidence += 0.1;
|
|
410
|
+
checkScrollDepth() {
|
|
411
|
+
const scrollY = window.scrollY;
|
|
412
|
+
const viewportHeight = window.innerHeight;
|
|
413
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
414
|
+
if (docHeight <= viewportHeight) {
|
|
415
|
+
this.updateDepth(100);
|
|
416
|
+
return;
|
|
586
417
|
}
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
418
|
+
const scrollableHeight = docHeight - viewportHeight;
|
|
419
|
+
const currentDepth = Math.min(100, Math.round(scrollY / scrollableHeight * 100));
|
|
420
|
+
this.updateDepth(currentDepth);
|
|
421
|
+
}
|
|
422
|
+
updateDepth(depth) {
|
|
423
|
+
if (depth <= this.maxDepth) return;
|
|
424
|
+
this.maxDepth = depth;
|
|
425
|
+
this.config.onDepthChange?.(depth);
|
|
426
|
+
for (const chunk of this.config.chunks) {
|
|
427
|
+
if (depth >= chunk && !this.reportedChunks.has(chunk)) {
|
|
428
|
+
this.reportedChunks.add(chunk);
|
|
429
|
+
this.reportChunk(chunk);
|
|
430
|
+
}
|
|
591
431
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
432
|
+
}
|
|
433
|
+
reportChunk(chunk) {
|
|
434
|
+
const docHeight = document.documentElement.scrollHeight;
|
|
435
|
+
const viewportHeight = window.innerHeight;
|
|
436
|
+
const event = {
|
|
437
|
+
depth: this.maxDepth,
|
|
438
|
+
chunk,
|
|
439
|
+
time_to_reach_ms: Date.now() - this.startTime,
|
|
440
|
+
total_height: docHeight,
|
|
441
|
+
viewport_height: viewportHeight
|
|
442
|
+
};
|
|
443
|
+
this.config.onChunkReached?.(event);
|
|
444
|
+
}
|
|
445
|
+
getChunkForDepth(depth) {
|
|
446
|
+
const chunks = this.config.chunks.sort((a, b) => b - a);
|
|
447
|
+
for (const chunk of chunks) {
|
|
448
|
+
if (depth >= chunk) return chunk;
|
|
598
449
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
450
|
+
return 0;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// src/behavioral/time-tracker.ts
|
|
455
|
+
var DEFAULT_CONFIG2 = {
|
|
456
|
+
idleThresholdMs: 3e4,
|
|
457
|
+
// 30 seconds
|
|
458
|
+
updateIntervalMs: 5e3
|
|
459
|
+
// 5 seconds
|
|
460
|
+
};
|
|
461
|
+
var TimeTracker = class {
|
|
462
|
+
constructor(config2 = {}) {
|
|
463
|
+
this.activeTime = 0;
|
|
464
|
+
this.idleTime = 0;
|
|
465
|
+
this.isVisible = true;
|
|
466
|
+
this.isIdle = false;
|
|
467
|
+
this.updateInterval = null;
|
|
468
|
+
this.idleCheckInterval = null;
|
|
469
|
+
this.handleVisibility = () => {
|
|
470
|
+
const wasVisible = this.isVisible;
|
|
471
|
+
this.isVisible = document.visibilityState === "visible";
|
|
472
|
+
if (wasVisible && !this.isVisible) {
|
|
473
|
+
this.updateTimes();
|
|
474
|
+
} else if (!wasVisible && this.isVisible) {
|
|
475
|
+
this.lastUpdateTime = Date.now();
|
|
476
|
+
this.lastActivityTime = Date.now();
|
|
604
477
|
}
|
|
605
|
-
}
|
|
606
|
-
confidence = Math.min(confidence, 0.65);
|
|
607
|
-
let navType;
|
|
608
|
-
if (confidence >= 0.35) {
|
|
609
|
-
navType = "likely_paste";
|
|
610
|
-
} else if (signals.length === 0) {
|
|
611
|
-
navType = "unknown";
|
|
612
|
-
} else {
|
|
613
|
-
navType = "likely_click";
|
|
614
|
-
}
|
|
615
|
-
this.result = {
|
|
616
|
-
nav_type: navType,
|
|
617
|
-
confidence,
|
|
618
|
-
signals,
|
|
619
|
-
sequence: this.sequence.slice(-10),
|
|
620
|
-
time_to_first_interaction_ms: this.firstInteractionTime ? Math.round(this.firstInteractionTime - this.pageLoadTime) : null
|
|
621
478
|
};
|
|
622
|
-
this.
|
|
623
|
-
|
|
479
|
+
this.handleActivity = () => {
|
|
480
|
+
const now = Date.now();
|
|
481
|
+
if (this.isIdle) {
|
|
482
|
+
this.isIdle = false;
|
|
483
|
+
}
|
|
484
|
+
this.lastActivityTime = now;
|
|
485
|
+
};
|
|
486
|
+
this.config = { ...DEFAULT_CONFIG2, ...config2 };
|
|
487
|
+
this.startTime = Date.now();
|
|
488
|
+
this.lastActivityTime = this.startTime;
|
|
489
|
+
this.lastUpdateTime = this.startTime;
|
|
624
490
|
}
|
|
625
491
|
/**
|
|
626
|
-
*
|
|
492
|
+
* Start tracking time
|
|
627
493
|
*/
|
|
628
|
-
|
|
629
|
-
|
|
494
|
+
start() {
|
|
495
|
+
document.addEventListener("visibilitychange", this.handleVisibility);
|
|
496
|
+
const activityEvents = ["mousemove", "keydown", "scroll", "click", "touchstart"];
|
|
497
|
+
activityEvents.forEach((event) => {
|
|
498
|
+
document.addEventListener(event, this.handleActivity, { passive: true });
|
|
499
|
+
});
|
|
500
|
+
this.updateInterval = setInterval(() => {
|
|
501
|
+
this.update();
|
|
502
|
+
}, this.config.updateIntervalMs);
|
|
503
|
+
this.idleCheckInterval = setInterval(() => {
|
|
504
|
+
this.checkIdle();
|
|
505
|
+
}, 1e3);
|
|
630
506
|
}
|
|
631
507
|
/**
|
|
632
|
-
*
|
|
508
|
+
* Stop tracking
|
|
633
509
|
*/
|
|
634
|
-
|
|
635
|
-
|
|
510
|
+
stop() {
|
|
511
|
+
document.removeEventListener("visibilitychange", this.handleVisibility);
|
|
512
|
+
const activityEvents = ["mousemove", "keydown", "scroll", "click", "touchstart"];
|
|
513
|
+
activityEvents.forEach((event) => {
|
|
514
|
+
document.removeEventListener(event, this.handleActivity);
|
|
515
|
+
});
|
|
516
|
+
if (this.updateInterval) {
|
|
517
|
+
clearInterval(this.updateInterval);
|
|
518
|
+
this.updateInterval = null;
|
|
519
|
+
}
|
|
520
|
+
if (this.idleCheckInterval) {
|
|
521
|
+
clearInterval(this.idleCheckInterval);
|
|
522
|
+
this.idleCheckInterval = null;
|
|
523
|
+
}
|
|
636
524
|
}
|
|
637
525
|
/**
|
|
638
|
-
* Get
|
|
526
|
+
* Get current time metrics
|
|
639
527
|
*/
|
|
640
|
-
|
|
641
|
-
|
|
528
|
+
getMetrics() {
|
|
529
|
+
this.updateTimes();
|
|
530
|
+
return {
|
|
531
|
+
active_time_ms: this.activeTime,
|
|
532
|
+
total_time_ms: Date.now() - this.startTime,
|
|
533
|
+
idle_time_ms: this.idleTime,
|
|
534
|
+
is_engaged: !this.isIdle && this.isVisible
|
|
535
|
+
};
|
|
642
536
|
}
|
|
643
537
|
/**
|
|
644
|
-
*
|
|
538
|
+
* Get final metrics (for unload)
|
|
645
539
|
*/
|
|
646
|
-
|
|
647
|
-
this.
|
|
648
|
-
this.
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
540
|
+
getFinalMetrics() {
|
|
541
|
+
this.updateTimes();
|
|
542
|
+
return this.getMetrics();
|
|
543
|
+
}
|
|
544
|
+
checkIdle() {
|
|
545
|
+
const now = Date.now();
|
|
546
|
+
const timeSinceActivity = now - this.lastActivityTime;
|
|
547
|
+
if (!this.isIdle && timeSinceActivity >= this.config.idleThresholdMs) {
|
|
548
|
+
this.isIdle = true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
updateTimes() {
|
|
552
|
+
const now = Date.now();
|
|
553
|
+
const elapsed = now - this.lastUpdateTime;
|
|
554
|
+
if (this.isVisible) {
|
|
555
|
+
if (this.isIdle) {
|
|
556
|
+
this.idleTime += elapsed;
|
|
557
|
+
} else {
|
|
558
|
+
this.activeTime += elapsed;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
this.lastUpdateTime = now;
|
|
562
|
+
}
|
|
563
|
+
update() {
|
|
564
|
+
if (!this.isVisible) return;
|
|
565
|
+
this.updateTimes();
|
|
566
|
+
this.config.onUpdate?.(this.getMetrics());
|
|
652
567
|
}
|
|
653
568
|
};
|
|
654
569
|
|
|
570
|
+
// src/config.ts
|
|
571
|
+
var VERSION = "2.4.0";
|
|
572
|
+
var DEFAULT_CONFIG3 = {
|
|
573
|
+
apiHost: "https://app.loamly.ai",
|
|
574
|
+
endpoints: {
|
|
575
|
+
visit: "/api/ingest/visit",
|
|
576
|
+
behavioral: "/api/ingest/behavioral",
|
|
577
|
+
session: "/api/ingest/session",
|
|
578
|
+
resolve: "/api/tracker/resolve",
|
|
579
|
+
health: "/api/tracker/health",
|
|
580
|
+
ping: "/api/tracker/ping"
|
|
581
|
+
},
|
|
582
|
+
pingInterval: 3e4,
|
|
583
|
+
// 30 seconds
|
|
584
|
+
batchSize: 10,
|
|
585
|
+
batchTimeout: 5e3,
|
|
586
|
+
sessionTimeout: 18e5,
|
|
587
|
+
// 30 minutes
|
|
588
|
+
maxTextLength: 100,
|
|
589
|
+
timeSpentThresholdMs: 5e3
|
|
590
|
+
// Only send time_spent when delta >= 5 seconds
|
|
591
|
+
};
|
|
592
|
+
var AI_PLATFORMS = {
|
|
593
|
+
"chatgpt.com": "chatgpt",
|
|
594
|
+
"chat.openai.com": "chatgpt",
|
|
595
|
+
"claude.ai": "claude",
|
|
596
|
+
"perplexity.ai": "perplexity",
|
|
597
|
+
"bard.google.com": "bard",
|
|
598
|
+
"gemini.google.com": "gemini",
|
|
599
|
+
"copilot.microsoft.com": "copilot",
|
|
600
|
+
"github.com/copilot": "github-copilot",
|
|
601
|
+
"you.com": "you",
|
|
602
|
+
"phind.com": "phind",
|
|
603
|
+
"poe.com": "poe"
|
|
604
|
+
};
|
|
605
|
+
|
|
655
606
|
// src/detection/agentic-browser.ts
|
|
656
607
|
var CometDetector = class {
|
|
657
608
|
constructor() {
|
|
@@ -825,743 +776,894 @@ var Loamly = (() => {
|
|
|
825
776
|
signals.push(`teleporting_clicks:${mousePatterns.teleportingClicks}`);
|
|
826
777
|
probability = Math.max(probability, 0.78);
|
|
827
778
|
}
|
|
828
|
-
return {
|
|
829
|
-
cometDOMDetected: this.cometDetector.isDetected(),
|
|
830
|
-
cdpDetected: this.cdpDetector.isDetected(),
|
|
831
|
-
mousePatterns,
|
|
832
|
-
agenticProbability: probability,
|
|
833
|
-
signals
|
|
834
|
-
};
|
|
779
|
+
return {
|
|
780
|
+
cometDOMDetected: this.cometDetector.isDetected(),
|
|
781
|
+
cdpDetected: this.cdpDetector.isDetected(),
|
|
782
|
+
mousePatterns,
|
|
783
|
+
agenticProbability: probability,
|
|
784
|
+
signals
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Cleanup resources
|
|
789
|
+
*/
|
|
790
|
+
destroy() {
|
|
791
|
+
this.cometDetector.destroy();
|
|
792
|
+
this.mouseAnalyzer.destroy();
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
// src/detection/behavioral-classifier.ts
|
|
797
|
+
var NAIVE_BAYES_WEIGHTS = {
|
|
798
|
+
human: {
|
|
799
|
+
time_to_first_click_delayed: 0.85,
|
|
800
|
+
time_to_first_click_normal: 0.75,
|
|
801
|
+
time_to_first_click_fast: 0.5,
|
|
802
|
+
time_to_first_click_immediate: 0.25,
|
|
803
|
+
scroll_speed_variable: 0.8,
|
|
804
|
+
scroll_speed_erratic: 0.7,
|
|
805
|
+
scroll_speed_uniform: 0.35,
|
|
806
|
+
scroll_speed_none: 0.45,
|
|
807
|
+
nav_timing_click: 0.75,
|
|
808
|
+
nav_timing_unknown: 0.55,
|
|
809
|
+
nav_timing_paste: 0.35,
|
|
810
|
+
has_referrer: 0.7,
|
|
811
|
+
no_referrer: 0.45,
|
|
812
|
+
homepage_landing: 0.65,
|
|
813
|
+
deep_landing: 0.5,
|
|
814
|
+
mouse_movement_curved: 0.9,
|
|
815
|
+
mouse_movement_linear: 0.3,
|
|
816
|
+
mouse_movement_none: 0.4,
|
|
817
|
+
form_fill_normal: 0.85,
|
|
818
|
+
form_fill_fast: 0.6,
|
|
819
|
+
form_fill_instant: 0.2,
|
|
820
|
+
focus_blur_normal: 0.75,
|
|
821
|
+
focus_blur_rapid: 0.45
|
|
822
|
+
},
|
|
823
|
+
ai_influenced: {
|
|
824
|
+
time_to_first_click_immediate: 0.75,
|
|
825
|
+
time_to_first_click_fast: 0.55,
|
|
826
|
+
time_to_first_click_normal: 0.4,
|
|
827
|
+
time_to_first_click_delayed: 0.35,
|
|
828
|
+
scroll_speed_none: 0.55,
|
|
829
|
+
scroll_speed_uniform: 0.7,
|
|
830
|
+
scroll_speed_variable: 0.35,
|
|
831
|
+
scroll_speed_erratic: 0.4,
|
|
832
|
+
nav_timing_paste: 0.75,
|
|
833
|
+
nav_timing_unknown: 0.5,
|
|
834
|
+
nav_timing_click: 0.35,
|
|
835
|
+
no_referrer: 0.65,
|
|
836
|
+
has_referrer: 0.4,
|
|
837
|
+
deep_landing: 0.6,
|
|
838
|
+
homepage_landing: 0.45,
|
|
839
|
+
mouse_movement_none: 0.6,
|
|
840
|
+
mouse_movement_linear: 0.75,
|
|
841
|
+
mouse_movement_curved: 0.25,
|
|
842
|
+
form_fill_instant: 0.8,
|
|
843
|
+
form_fill_fast: 0.55,
|
|
844
|
+
form_fill_normal: 0.3,
|
|
845
|
+
focus_blur_rapid: 0.6,
|
|
846
|
+
focus_blur_normal: 0.4
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
var PRIORS = {
|
|
850
|
+
human: 0.85,
|
|
851
|
+
ai_influenced: 0.15
|
|
852
|
+
};
|
|
853
|
+
var DEFAULT_WEIGHT = 0.5;
|
|
854
|
+
var BehavioralClassifier = class {
|
|
855
|
+
/**
|
|
856
|
+
* Create a new classifier
|
|
857
|
+
* @param minSessionTimeMs Minimum session time before classification (default: 10s)
|
|
858
|
+
*/
|
|
859
|
+
constructor(minSessionTimeMs = 1e4) {
|
|
860
|
+
this.classified = false;
|
|
861
|
+
this.result = null;
|
|
862
|
+
this.onClassify = null;
|
|
863
|
+
this.minSessionTime = minSessionTimeMs;
|
|
864
|
+
this.data = {
|
|
865
|
+
firstClickTime: null,
|
|
866
|
+
scrollEvents: [],
|
|
867
|
+
mouseEvents: [],
|
|
868
|
+
formEvents: [],
|
|
869
|
+
focusBlurEvents: [],
|
|
870
|
+
startTime: Date.now()
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Set callback for when classification completes
|
|
875
|
+
*/
|
|
876
|
+
setOnClassify(callback) {
|
|
877
|
+
this.onClassify = callback;
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Record a click event
|
|
881
|
+
*/
|
|
882
|
+
recordClick() {
|
|
883
|
+
if (this.data.firstClickTime === null) {
|
|
884
|
+
this.data.firstClickTime = Date.now();
|
|
885
|
+
}
|
|
886
|
+
this.checkAndClassify();
|
|
835
887
|
}
|
|
836
888
|
/**
|
|
837
|
-
*
|
|
889
|
+
* Record a scroll event
|
|
838
890
|
*/
|
|
839
|
-
|
|
840
|
-
this.
|
|
841
|
-
this.
|
|
891
|
+
recordScroll(position) {
|
|
892
|
+
this.data.scrollEvents.push({ time: Date.now(), position });
|
|
893
|
+
if (this.data.scrollEvents.length > 50) {
|
|
894
|
+
this.data.scrollEvents = this.data.scrollEvents.slice(-50);
|
|
895
|
+
}
|
|
896
|
+
this.checkAndClassify();
|
|
842
897
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
};
|
|
853
|
-
var EventQueue = class {
|
|
854
|
-
constructor(endpoint2, config2 = {}) {
|
|
855
|
-
this.queue = [];
|
|
856
|
-
this.batchTimer = null;
|
|
857
|
-
this.isFlushing = false;
|
|
858
|
-
this.endpoint = endpoint2;
|
|
859
|
-
this.config = { ...DEFAULT_QUEUE_CONFIG, ...config2 };
|
|
860
|
-
this.loadFromStorage();
|
|
898
|
+
/**
|
|
899
|
+
* Record mouse movement
|
|
900
|
+
*/
|
|
901
|
+
recordMouse(x, y) {
|
|
902
|
+
this.data.mouseEvents.push({ time: Date.now(), x, y });
|
|
903
|
+
if (this.data.mouseEvents.length > 100) {
|
|
904
|
+
this.data.mouseEvents = this.data.mouseEvents.slice(-100);
|
|
905
|
+
}
|
|
906
|
+
this.checkAndClassify();
|
|
861
907
|
}
|
|
862
908
|
/**
|
|
863
|
-
*
|
|
909
|
+
* Record form field interaction start
|
|
864
910
|
*/
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
timestamp: Date.now(),
|
|
871
|
-
retries: 0
|
|
872
|
-
};
|
|
873
|
-
this.queue.push(event);
|
|
874
|
-
this.saveToStorage();
|
|
875
|
-
this.scheduleBatch();
|
|
911
|
+
recordFormStart(fieldId) {
|
|
912
|
+
const existing = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
|
|
913
|
+
if (!existing) {
|
|
914
|
+
this.data.formEvents.push({ fieldId, startTime: Date.now(), endTime: 0 });
|
|
915
|
+
}
|
|
876
916
|
}
|
|
877
917
|
/**
|
|
878
|
-
*
|
|
918
|
+
* Record form field interaction end
|
|
879
919
|
*/
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
try {
|
|
885
|
-
const events = [...this.queue];
|
|
886
|
-
this.queue = [];
|
|
887
|
-
await this.sendBatch(events);
|
|
888
|
-
} finally {
|
|
889
|
-
this.isFlushing = false;
|
|
890
|
-
this.saveToStorage();
|
|
920
|
+
recordFormEnd(fieldId) {
|
|
921
|
+
const event = this.data.formEvents.find((e) => e.fieldId === fieldId && e.endTime === 0);
|
|
922
|
+
if (event) {
|
|
923
|
+
event.endTime = Date.now();
|
|
891
924
|
}
|
|
925
|
+
this.checkAndClassify();
|
|
892
926
|
}
|
|
893
927
|
/**
|
|
894
|
-
*
|
|
928
|
+
* Record focus/blur event
|
|
895
929
|
*/
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
...e.payload,
|
|
901
|
-
_queue_id: e.id,
|
|
902
|
-
_queue_timestamp: e.timestamp
|
|
903
|
-
}));
|
|
904
|
-
const success = navigator.sendBeacon?.(
|
|
905
|
-
this.endpoint,
|
|
906
|
-
JSON.stringify({ events, beacon: true })
|
|
907
|
-
) ?? false;
|
|
908
|
-
if (success) {
|
|
909
|
-
this.queue = [];
|
|
910
|
-
this.clearStorage();
|
|
930
|
+
recordFocusBlur(type) {
|
|
931
|
+
this.data.focusBlurEvents.push({ type, time: Date.now() });
|
|
932
|
+
if (this.data.focusBlurEvents.length > 20) {
|
|
933
|
+
this.data.focusBlurEvents = this.data.focusBlurEvents.slice(-20);
|
|
911
934
|
}
|
|
912
|
-
return success;
|
|
913
935
|
}
|
|
914
936
|
/**
|
|
915
|
-
*
|
|
937
|
+
* Check if we have enough data and classify
|
|
916
938
|
*/
|
|
917
|
-
|
|
918
|
-
|
|
939
|
+
checkAndClassify() {
|
|
940
|
+
if (this.classified) return;
|
|
941
|
+
const sessionDuration = Date.now() - this.data.startTime;
|
|
942
|
+
if (sessionDuration < this.minSessionTime) return;
|
|
943
|
+
const hasData = this.data.scrollEvents.length >= 2 || this.data.mouseEvents.length >= 5 || this.data.firstClickTime !== null;
|
|
944
|
+
if (!hasData) return;
|
|
945
|
+
this.classify();
|
|
919
946
|
}
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
this.batchTimer = setTimeout(() => {
|
|
927
|
-
this.batchTimer = null;
|
|
928
|
-
this.flush();
|
|
929
|
-
}, this.config.batchTimeout);
|
|
947
|
+
/**
|
|
948
|
+
* Force classification (for beforeunload)
|
|
949
|
+
*/
|
|
950
|
+
forceClassify() {
|
|
951
|
+
if (this.classified) return this.result;
|
|
952
|
+
return this.classify();
|
|
930
953
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
954
|
+
/**
|
|
955
|
+
* Perform classification
|
|
956
|
+
*/
|
|
957
|
+
classify() {
|
|
958
|
+
const sessionDuration = Date.now() - this.data.startTime;
|
|
959
|
+
const signals = this.extractSignals();
|
|
960
|
+
let humanLogProb = Math.log(PRIORS.human);
|
|
961
|
+
let aiLogProb = Math.log(PRIORS.ai_influenced);
|
|
962
|
+
for (const signal of signals) {
|
|
963
|
+
const humanWeight = NAIVE_BAYES_WEIGHTS.human[signal] ?? DEFAULT_WEIGHT;
|
|
964
|
+
const aiWeight = NAIVE_BAYES_WEIGHTS.ai_influenced[signal] ?? DEFAULT_WEIGHT;
|
|
965
|
+
humanLogProb += Math.log(humanWeight);
|
|
966
|
+
aiLogProb += Math.log(aiWeight);
|
|
935
967
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
968
|
+
const maxLog = Math.max(humanLogProb, aiLogProb);
|
|
969
|
+
const humanExp = Math.exp(humanLogProb - maxLog);
|
|
970
|
+
const aiExp = Math.exp(aiLogProb - maxLog);
|
|
971
|
+
const total = humanExp + aiExp;
|
|
972
|
+
const humanProbability = humanExp / total;
|
|
973
|
+
const aiProbability = aiExp / total;
|
|
974
|
+
let classification;
|
|
975
|
+
let confidence;
|
|
976
|
+
if (humanProbability > 0.6) {
|
|
977
|
+
classification = "human";
|
|
978
|
+
confidence = humanProbability;
|
|
979
|
+
} else if (aiProbability > 0.6) {
|
|
980
|
+
classification = "ai_influenced";
|
|
981
|
+
confidence = aiProbability;
|
|
982
|
+
} else {
|
|
983
|
+
classification = "uncertain";
|
|
984
|
+
confidence = Math.max(humanProbability, aiProbability);
|
|
985
|
+
}
|
|
986
|
+
this.result = {
|
|
987
|
+
classification,
|
|
988
|
+
humanProbability,
|
|
989
|
+
aiProbability,
|
|
990
|
+
confidence,
|
|
991
|
+
signals,
|
|
992
|
+
timestamp: Date.now(),
|
|
993
|
+
sessionDurationMs: sessionDuration
|
|
947
994
|
};
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
995
|
+
this.classified = true;
|
|
996
|
+
if (this.onClassify) {
|
|
997
|
+
this.onClassify(this.result);
|
|
998
|
+
}
|
|
999
|
+
return this.result;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Extract behavioral signals from collected data
|
|
1003
|
+
*/
|
|
1004
|
+
extractSignals() {
|
|
1005
|
+
const signals = [];
|
|
1006
|
+
if (this.data.firstClickTime !== null) {
|
|
1007
|
+
const timeToClick = this.data.firstClickTime - this.data.startTime;
|
|
1008
|
+
if (timeToClick < 500) {
|
|
1009
|
+
signals.push("time_to_first_click_immediate");
|
|
1010
|
+
} else if (timeToClick < 2e3) {
|
|
1011
|
+
signals.push("time_to_first_click_fast");
|
|
1012
|
+
} else if (timeToClick < 1e4) {
|
|
1013
|
+
signals.push("time_to_first_click_normal");
|
|
1014
|
+
} else {
|
|
1015
|
+
signals.push("time_to_first_click_delayed");
|
|
956
1016
|
}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1017
|
+
}
|
|
1018
|
+
if (this.data.scrollEvents.length === 0) {
|
|
1019
|
+
signals.push("scroll_speed_none");
|
|
1020
|
+
} else if (this.data.scrollEvents.length >= 3) {
|
|
1021
|
+
const scrollDeltas = [];
|
|
1022
|
+
for (let i = 1; i < this.data.scrollEvents.length; i++) {
|
|
1023
|
+
const delta = this.data.scrollEvents[i].time - this.data.scrollEvents[i - 1].time;
|
|
1024
|
+
scrollDeltas.push(delta);
|
|
963
1025
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1026
|
+
const mean = scrollDeltas.reduce((a, b) => a + b, 0) / scrollDeltas.length;
|
|
1027
|
+
const variance = scrollDeltas.reduce((sum, d) => sum + Math.pow(d - mean, 2), 0) / scrollDeltas.length;
|
|
1028
|
+
const stdDev = Math.sqrt(variance);
|
|
1029
|
+
const cv = mean > 0 ? stdDev / mean : 0;
|
|
1030
|
+
if (cv < 0.2) {
|
|
1031
|
+
signals.push("scroll_speed_uniform");
|
|
1032
|
+
} else if (cv < 0.6) {
|
|
1033
|
+
signals.push("scroll_speed_variable");
|
|
1034
|
+
} else {
|
|
1035
|
+
signals.push("scroll_speed_erratic");
|
|
967
1036
|
}
|
|
968
1037
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1038
|
+
if (this.data.mouseEvents.length === 0) {
|
|
1039
|
+
signals.push("mouse_movement_none");
|
|
1040
|
+
} else if (this.data.mouseEvents.length >= 10) {
|
|
1041
|
+
const n = Math.min(this.data.mouseEvents.length, 20);
|
|
1042
|
+
const recentMouse = this.data.mouseEvents.slice(-n);
|
|
1043
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
1044
|
+
for (const event of recentMouse) {
|
|
1045
|
+
sumX += event.x;
|
|
1046
|
+
sumY += event.y;
|
|
1047
|
+
sumXY += event.x * event.y;
|
|
1048
|
+
sumX2 += event.x * event.x;
|
|
979
1049
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1050
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
1051
|
+
const slope = denominator !== 0 ? (n * sumXY - sumX * sumY) / denominator : 0;
|
|
1052
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
1053
|
+
let ssRes = 0, ssTot = 0;
|
|
1054
|
+
const yMean = sumY / n;
|
|
1055
|
+
for (const event of recentMouse) {
|
|
1056
|
+
const yPred = slope * event.x + intercept;
|
|
1057
|
+
ssRes += Math.pow(event.y - yPred, 2);
|
|
1058
|
+
ssTot += Math.pow(event.y - yMean, 2);
|
|
1059
|
+
}
|
|
1060
|
+
const r2 = ssTot !== 0 ? 1 - ssRes / ssTot : 0;
|
|
1061
|
+
if (r2 > 0.95) {
|
|
1062
|
+
signals.push("mouse_movement_linear");
|
|
987
1063
|
} else {
|
|
988
|
-
|
|
1064
|
+
signals.push("mouse_movement_curved");
|
|
989
1065
|
}
|
|
990
|
-
} catch {
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
clearStorage() {
|
|
994
|
-
try {
|
|
995
|
-
localStorage.removeItem(this.config.storageKey);
|
|
996
|
-
} catch {
|
|
997
1066
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
this.intervalId = null;
|
|
1008
|
-
this.isVisible = true;
|
|
1009
|
-
this.currentScrollDepth = 0;
|
|
1010
|
-
this.ping = async () => {
|
|
1011
|
-
const data = this.getData();
|
|
1012
|
-
this.config.onPing?.(data);
|
|
1013
|
-
if (this.config.endpoint) {
|
|
1014
|
-
try {
|
|
1015
|
-
await fetch(this.config.endpoint, {
|
|
1016
|
-
method: "POST",
|
|
1017
|
-
headers: { "Content-Type": "application/json" },
|
|
1018
|
-
body: JSON.stringify(data)
|
|
1019
|
-
});
|
|
1020
|
-
} catch {
|
|
1021
|
-
}
|
|
1067
|
+
const completedForms = this.data.formEvents.filter((e) => e.endTime > 0);
|
|
1068
|
+
if (completedForms.length > 0) {
|
|
1069
|
+
const avgFillTime = completedForms.reduce((sum, e) => sum + (e.endTime - e.startTime), 0) / completedForms.length;
|
|
1070
|
+
if (avgFillTime < 100) {
|
|
1071
|
+
signals.push("form_fill_instant");
|
|
1072
|
+
} else if (avgFillTime < 500) {
|
|
1073
|
+
signals.push("form_fill_fast");
|
|
1074
|
+
} else {
|
|
1075
|
+
signals.push("form_fill_normal");
|
|
1022
1076
|
}
|
|
1023
|
-
}
|
|
1024
|
-
this.
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
(window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
|
|
1030
|
-
);
|
|
1031
|
-
if (scrollPercent > this.currentScrollDepth) {
|
|
1032
|
-
this.currentScrollDepth = Math.min(scrollPercent, 100);
|
|
1077
|
+
}
|
|
1078
|
+
if (this.data.focusBlurEvents.length >= 4) {
|
|
1079
|
+
const recentFB = this.data.focusBlurEvents.slice(-10);
|
|
1080
|
+
const intervals = [];
|
|
1081
|
+
for (let i = 1; i < recentFB.length; i++) {
|
|
1082
|
+
intervals.push(recentFB[i].time - recentFB[i - 1].time);
|
|
1033
1083
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
this.config = {
|
|
1040
|
-
interval: DEFAULT_CONFIG.pingInterval,
|
|
1041
|
-
endpoint: "",
|
|
1042
|
-
...config2
|
|
1043
|
-
};
|
|
1044
|
-
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1045
|
-
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
1046
|
-
}
|
|
1047
|
-
/**
|
|
1048
|
-
* Start the ping service
|
|
1049
|
-
*/
|
|
1050
|
-
start() {
|
|
1051
|
-
if (this.intervalId) return;
|
|
1052
|
-
this.intervalId = setInterval(() => {
|
|
1053
|
-
if (this.isVisible) {
|
|
1054
|
-
this.ping();
|
|
1084
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
1085
|
+
if (avgInterval < 1e3) {
|
|
1086
|
+
signals.push("focus_blur_rapid");
|
|
1087
|
+
} else {
|
|
1088
|
+
signals.push("focus_blur_normal");
|
|
1055
1089
|
}
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1090
|
+
}
|
|
1091
|
+
return signals;
|
|
1058
1092
|
}
|
|
1059
1093
|
/**
|
|
1060
|
-
*
|
|
1094
|
+
* Add context signals (set by tracker from external data)
|
|
1061
1095
|
*/
|
|
1062
|
-
|
|
1063
|
-
if (this.intervalId) {
|
|
1064
|
-
clearInterval(this.intervalId);
|
|
1065
|
-
this.intervalId = null;
|
|
1066
|
-
}
|
|
1067
|
-
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1068
|
-
window.removeEventListener("scroll", this.handleScroll);
|
|
1096
|
+
addContextSignal(_signal) {
|
|
1069
1097
|
}
|
|
1070
1098
|
/**
|
|
1071
|
-
*
|
|
1099
|
+
* Get current result (null if not yet classified)
|
|
1072
1100
|
*/
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
this.currentScrollDepth = depth;
|
|
1076
|
-
}
|
|
1101
|
+
getResult() {
|
|
1102
|
+
return this.result;
|
|
1077
1103
|
}
|
|
1078
1104
|
/**
|
|
1079
|
-
*
|
|
1105
|
+
* Check if classification has been performed
|
|
1080
1106
|
*/
|
|
1081
|
-
|
|
1082
|
-
return
|
|
1083
|
-
session_id: this.sessionId,
|
|
1084
|
-
visitor_id: this.visitorId,
|
|
1085
|
-
url: window.location.href,
|
|
1086
|
-
time_on_page_ms: Date.now() - this.pageLoadTime,
|
|
1087
|
-
scroll_depth: this.currentScrollDepth,
|
|
1088
|
-
is_active: this.isVisible,
|
|
1089
|
-
tracker_version: this.version
|
|
1090
|
-
};
|
|
1107
|
+
hasClassified() {
|
|
1108
|
+
return this.classified;
|
|
1091
1109
|
}
|
|
1092
1110
|
};
|
|
1093
1111
|
|
|
1094
|
-
// src/
|
|
1095
|
-
var
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
this.
|
|
1099
|
-
this.
|
|
1100
|
-
this.
|
|
1101
|
-
this.
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1112
|
+
// src/detection/focus-blur.ts
|
|
1113
|
+
var FocusBlurAnalyzer = class {
|
|
1114
|
+
constructor() {
|
|
1115
|
+
this.sequence = [];
|
|
1116
|
+
this.firstInteractionTime = null;
|
|
1117
|
+
this.analyzed = false;
|
|
1118
|
+
this.result = null;
|
|
1119
|
+
this.pageLoadTime = performance.now();
|
|
1120
|
+
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Initialize event tracking
|
|
1123
|
+
* Must be called after DOM is ready
|
|
1124
|
+
*/
|
|
1125
|
+
initTracking() {
|
|
1126
|
+
document.addEventListener("focus", (e) => {
|
|
1127
|
+
this.recordEvent("focus", e.target);
|
|
1128
|
+
}, true);
|
|
1129
|
+
document.addEventListener("blur", (e) => {
|
|
1130
|
+
this.recordEvent("blur", e.target);
|
|
1131
|
+
}, true);
|
|
1132
|
+
window.addEventListener("focus", () => {
|
|
1133
|
+
this.recordEvent("window_focus", null);
|
|
1134
|
+
});
|
|
1135
|
+
window.addEventListener("blur", () => {
|
|
1136
|
+
this.recordEvent("window_blur", null);
|
|
1137
|
+
});
|
|
1138
|
+
const recordFirstInteraction = () => {
|
|
1139
|
+
if (this.firstInteractionTime === null) {
|
|
1140
|
+
this.firstInteractionTime = performance.now();
|
|
1109
1141
|
}
|
|
1110
1142
|
};
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1143
|
+
document.addEventListener("click", recordFirstInteraction, { once: true, passive: true });
|
|
1144
|
+
document.addEventListener("keydown", recordFirstInteraction, { once: true, passive: true });
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Record a focus/blur event
|
|
1148
|
+
*/
|
|
1149
|
+
recordEvent(type, target) {
|
|
1150
|
+
const event = {
|
|
1151
|
+
type,
|
|
1152
|
+
target: target?.tagName || "WINDOW",
|
|
1153
|
+
timestamp: performance.now()
|
|
1117
1154
|
};
|
|
1118
|
-
this.
|
|
1155
|
+
this.sequence.push(event);
|
|
1156
|
+
if (this.sequence.length > 20) {
|
|
1157
|
+
this.sequence = this.sequence.slice(-20);
|
|
1158
|
+
}
|
|
1119
1159
|
}
|
|
1120
1160
|
/**
|
|
1121
|
-
*
|
|
1161
|
+
* Analyze the focus/blur sequence for paste patterns
|
|
1122
1162
|
*/
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1163
|
+
analyze() {
|
|
1164
|
+
if (this.analyzed && this.result) {
|
|
1165
|
+
return this.result;
|
|
1166
|
+
}
|
|
1167
|
+
const signals = [];
|
|
1168
|
+
let confidence = 0;
|
|
1169
|
+
const earlyEvents = this.sequence.filter((e) => e.timestamp < this.pageLoadTime + 500);
|
|
1170
|
+
const hasEarlyWindowFocus = earlyEvents.some((e) => e.type === "window_focus");
|
|
1171
|
+
if (hasEarlyWindowFocus) {
|
|
1172
|
+
signals.push("early_window_focus");
|
|
1173
|
+
confidence += 0.15;
|
|
1174
|
+
}
|
|
1175
|
+
const hasEarlyBodyFocus = earlyEvents.some(
|
|
1176
|
+
(e) => e.type === "focus" && e.target === "BODY"
|
|
1177
|
+
);
|
|
1178
|
+
if (hasEarlyBodyFocus) {
|
|
1179
|
+
signals.push("early_body_focus");
|
|
1180
|
+
confidence += 0.15;
|
|
1181
|
+
}
|
|
1182
|
+
const hasLinkFocus = this.sequence.some(
|
|
1183
|
+
(e) => e.type === "focus" && e.target === "A"
|
|
1184
|
+
);
|
|
1185
|
+
if (!hasLinkFocus) {
|
|
1186
|
+
signals.push("no_link_focus");
|
|
1187
|
+
confidence += 0.1;
|
|
1188
|
+
}
|
|
1189
|
+
const firstFocus = this.sequence.find((e) => e.type === "focus");
|
|
1190
|
+
if (firstFocus && (firstFocus.target === "BODY" || firstFocus.target === "HTML")) {
|
|
1191
|
+
signals.push("first_focus_body");
|
|
1192
|
+
confidence += 0.1;
|
|
1193
|
+
}
|
|
1194
|
+
const windowEvents = this.sequence.filter(
|
|
1195
|
+
(e) => e.type === "window_focus" || e.type === "window_blur"
|
|
1196
|
+
);
|
|
1197
|
+
if (windowEvents.length <= 2) {
|
|
1198
|
+
signals.push("minimal_window_switches");
|
|
1199
|
+
confidence += 0.05;
|
|
1200
|
+
}
|
|
1201
|
+
if (this.firstInteractionTime !== null) {
|
|
1202
|
+
const timeToInteraction = this.firstInteractionTime - this.pageLoadTime;
|
|
1203
|
+
if (timeToInteraction > 3e3) {
|
|
1204
|
+
signals.push("delayed_first_interaction");
|
|
1205
|
+
confidence += 0.1;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
confidence = Math.min(confidence, 0.65);
|
|
1209
|
+
let navType;
|
|
1210
|
+
if (confidence >= 0.35) {
|
|
1211
|
+
navType = "likely_paste";
|
|
1212
|
+
} else if (signals.length === 0) {
|
|
1213
|
+
navType = "unknown";
|
|
1214
|
+
} else {
|
|
1215
|
+
navType = "likely_click";
|
|
1216
|
+
}
|
|
1217
|
+
this.result = {
|
|
1218
|
+
nav_type: navType,
|
|
1219
|
+
confidence,
|
|
1220
|
+
signals,
|
|
1221
|
+
sequence: this.sequence.slice(-10),
|
|
1222
|
+
time_to_first_interaction_ms: this.firstInteractionTime ? Math.round(this.firstInteractionTime - this.pageLoadTime) : null
|
|
1223
|
+
};
|
|
1224
|
+
this.analyzed = true;
|
|
1225
|
+
return this.result;
|
|
1127
1226
|
}
|
|
1128
1227
|
/**
|
|
1129
|
-
*
|
|
1228
|
+
* Get current result (analyze if not done)
|
|
1130
1229
|
*/
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
document.removeEventListener("visibilitychange", this.handleVisibility);
|
|
1230
|
+
getResult() {
|
|
1231
|
+
return this.analyze();
|
|
1134
1232
|
}
|
|
1135
1233
|
/**
|
|
1136
|
-
*
|
|
1234
|
+
* Check if analysis has been performed
|
|
1137
1235
|
*/
|
|
1138
|
-
|
|
1139
|
-
return this.
|
|
1236
|
+
hasAnalyzed() {
|
|
1237
|
+
return this.analyzed;
|
|
1140
1238
|
}
|
|
1141
1239
|
/**
|
|
1142
|
-
* Get
|
|
1240
|
+
* Get the raw sequence for debugging
|
|
1143
1241
|
*/
|
|
1144
|
-
|
|
1145
|
-
return
|
|
1242
|
+
getSequence() {
|
|
1243
|
+
return [...this.sequence];
|
|
1146
1244
|
}
|
|
1147
1245
|
/**
|
|
1148
|
-
*
|
|
1246
|
+
* Reset the analyzer
|
|
1149
1247
|
*/
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1248
|
+
reset() {
|
|
1249
|
+
this.sequence = [];
|
|
1250
|
+
this.pageLoadTime = performance.now();
|
|
1251
|
+
this.firstInteractionTime = null;
|
|
1252
|
+
this.analyzed = false;
|
|
1253
|
+
this.result = null;
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
// src/detection/navigation-timing.ts
|
|
1258
|
+
function detectNavigationType() {
|
|
1259
|
+
try {
|
|
1260
|
+
const entries = performance.getEntriesByType("navigation");
|
|
1261
|
+
if (!entries || entries.length === 0) {
|
|
1262
|
+
return { nav_type: "unknown", confidence: 0, signals: ["no_timing_data"] };
|
|
1263
|
+
}
|
|
1264
|
+
const nav = entries[0];
|
|
1265
|
+
const signals = [];
|
|
1266
|
+
let pasteScore = 0;
|
|
1267
|
+
const fetchStartDelta = nav.fetchStart - nav.startTime;
|
|
1268
|
+
if (fetchStartDelta < 5) {
|
|
1269
|
+
pasteScore += 0.25;
|
|
1270
|
+
signals.push("instant_fetch_start");
|
|
1271
|
+
} else if (fetchStartDelta < 20) {
|
|
1272
|
+
pasteScore += 0.15;
|
|
1273
|
+
signals.push("fast_fetch_start");
|
|
1274
|
+
}
|
|
1275
|
+
const dnsTime = nav.domainLookupEnd - nav.domainLookupStart;
|
|
1276
|
+
if (dnsTime === 0) {
|
|
1277
|
+
pasteScore += 0.15;
|
|
1278
|
+
signals.push("no_dns_lookup");
|
|
1279
|
+
}
|
|
1280
|
+
const connectTime = nav.connectEnd - nav.connectStart;
|
|
1281
|
+
if (connectTime === 0) {
|
|
1282
|
+
pasteScore += 0.15;
|
|
1283
|
+
signals.push("no_tcp_connect");
|
|
1284
|
+
}
|
|
1285
|
+
if (nav.redirectCount === 0) {
|
|
1286
|
+
pasteScore += 0.1;
|
|
1287
|
+
signals.push("no_redirects");
|
|
1288
|
+
}
|
|
1289
|
+
const timingVariance = calculateTimingVariance(nav);
|
|
1290
|
+
if (timingVariance < 10) {
|
|
1291
|
+
pasteScore += 0.15;
|
|
1292
|
+
signals.push("uniform_timing");
|
|
1293
|
+
}
|
|
1294
|
+
if (!document.referrer || document.referrer === "") {
|
|
1295
|
+
pasteScore += 0.1;
|
|
1296
|
+
signals.push("no_referrer");
|
|
1297
|
+
}
|
|
1298
|
+
const confidence = Math.min(pasteScore, 1);
|
|
1299
|
+
const nav_type = pasteScore >= 0.5 ? "likely_paste" : "likely_click";
|
|
1153
1300
|
return {
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
total_height: docHeight,
|
|
1158
|
-
viewport_height: viewportHeight
|
|
1301
|
+
nav_type,
|
|
1302
|
+
confidence: Math.round(confidence * 1e3) / 1e3,
|
|
1303
|
+
signals
|
|
1159
1304
|
};
|
|
1305
|
+
} catch {
|
|
1306
|
+
return { nav_type: "unknown", confidence: 0, signals: ["detection_error"] };
|
|
1160
1307
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1308
|
+
}
|
|
1309
|
+
function calculateTimingVariance(nav) {
|
|
1310
|
+
const timings = [
|
|
1311
|
+
nav.fetchStart - nav.startTime,
|
|
1312
|
+
nav.domainLookupEnd - nav.domainLookupStart,
|
|
1313
|
+
nav.connectEnd - nav.connectStart,
|
|
1314
|
+
nav.responseStart - nav.requestStart
|
|
1315
|
+
].filter((t) => t >= 0);
|
|
1316
|
+
if (timings.length === 0) return 100;
|
|
1317
|
+
const mean = timings.reduce((a, b) => a + b, 0) / timings.length;
|
|
1318
|
+
const variance = timings.reduce((sum, t) => sum + Math.pow(t - mean, 2), 0) / timings.length;
|
|
1319
|
+
return Math.sqrt(variance);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/detection/referrer.ts
|
|
1323
|
+
function detectAIFromReferrer(referrer) {
|
|
1324
|
+
if (!referrer) {
|
|
1325
|
+
return null;
|
|
1172
1326
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1327
|
+
try {
|
|
1328
|
+
const url = new URL(referrer);
|
|
1329
|
+
const hostname = url.hostname.toLowerCase();
|
|
1330
|
+
for (const [pattern, platform] of Object.entries(AI_PLATFORMS)) {
|
|
1331
|
+
if (hostname.includes(pattern) || referrer.includes(pattern)) {
|
|
1332
|
+
return {
|
|
1333
|
+
isAI: true,
|
|
1334
|
+
platform,
|
|
1335
|
+
confidence: 0.95,
|
|
1336
|
+
// High confidence when referrer matches
|
|
1337
|
+
method: "referrer"
|
|
1338
|
+
};
|
|
1181
1339
|
}
|
|
1182
1340
|
}
|
|
1341
|
+
return null;
|
|
1342
|
+
} catch {
|
|
1343
|
+
for (const [pattern, platform] of Object.entries(AI_PLATFORMS)) {
|
|
1344
|
+
if (referrer.toLowerCase().includes(pattern.toLowerCase())) {
|
|
1345
|
+
return {
|
|
1346
|
+
isAI: true,
|
|
1347
|
+
platform,
|
|
1348
|
+
confidence: 0.85,
|
|
1349
|
+
method: "referrer"
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
return null;
|
|
1183
1354
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1355
|
+
}
|
|
1356
|
+
function detectAIFromUTM(url) {
|
|
1357
|
+
try {
|
|
1358
|
+
const params = new URL(url).searchParams;
|
|
1359
|
+
const utmSource = params.get("utm_source")?.toLowerCase();
|
|
1360
|
+
if (utmSource) {
|
|
1361
|
+
for (const [pattern, platform] of Object.entries(AI_PLATFORMS)) {
|
|
1362
|
+
if (utmSource.includes(pattern.split(".")[0])) {
|
|
1363
|
+
return {
|
|
1364
|
+
isAI: true,
|
|
1365
|
+
platform,
|
|
1366
|
+
confidence: 0.99,
|
|
1367
|
+
// Very high confidence from explicit UTM
|
|
1368
|
+
method: "referrer"
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
if (utmSource.includes("ai") || utmSource.includes("llm") || utmSource.includes("chatbot")) {
|
|
1373
|
+
return {
|
|
1374
|
+
isAI: true,
|
|
1375
|
+
platform: utmSource,
|
|
1376
|
+
confidence: 0.9,
|
|
1377
|
+
method: "referrer"
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1200
1380
|
}
|
|
1201
|
-
return
|
|
1381
|
+
return null;
|
|
1382
|
+
} catch {
|
|
1383
|
+
return null;
|
|
1202
1384
|
}
|
|
1203
|
-
}
|
|
1385
|
+
}
|
|
1204
1386
|
|
|
1205
|
-
// src/
|
|
1206
|
-
var
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1387
|
+
// src/infrastructure/event-queue.ts
|
|
1388
|
+
var DEFAULT_QUEUE_CONFIG = {
|
|
1389
|
+
batchSize: DEFAULT_CONFIG3.batchSize,
|
|
1390
|
+
batchTimeout: DEFAULT_CONFIG3.batchTimeout,
|
|
1391
|
+
maxRetries: 3,
|
|
1392
|
+
retryDelayMs: 1e3,
|
|
1393
|
+
storageKey: "_loamly_queue"
|
|
1211
1394
|
};
|
|
1212
|
-
var
|
|
1213
|
-
constructor(config2 = {}) {
|
|
1214
|
-
this.
|
|
1215
|
-
this.
|
|
1216
|
-
this.
|
|
1217
|
-
this.
|
|
1218
|
-
this.
|
|
1219
|
-
this.
|
|
1220
|
-
this.handleVisibility = () => {
|
|
1221
|
-
const wasVisible = this.isVisible;
|
|
1222
|
-
this.isVisible = document.visibilityState === "visible";
|
|
1223
|
-
if (wasVisible && !this.isVisible) {
|
|
1224
|
-
this.updateTimes();
|
|
1225
|
-
} else if (!wasVisible && this.isVisible) {
|
|
1226
|
-
this.lastUpdateTime = Date.now();
|
|
1227
|
-
this.lastActivityTime = Date.now();
|
|
1228
|
-
}
|
|
1229
|
-
};
|
|
1230
|
-
this.handleActivity = () => {
|
|
1231
|
-
const now = Date.now();
|
|
1232
|
-
if (this.isIdle) {
|
|
1233
|
-
this.isIdle = false;
|
|
1234
|
-
}
|
|
1235
|
-
this.lastActivityTime = now;
|
|
1236
|
-
};
|
|
1237
|
-
this.config = { ...DEFAULT_CONFIG2, ...config2 };
|
|
1238
|
-
this.startTime = Date.now();
|
|
1239
|
-
this.lastActivityTime = this.startTime;
|
|
1240
|
-
this.lastUpdateTime = this.startTime;
|
|
1395
|
+
var EventQueue = class {
|
|
1396
|
+
constructor(endpoint2, config2 = {}) {
|
|
1397
|
+
this.queue = [];
|
|
1398
|
+
this.batchTimer = null;
|
|
1399
|
+
this.isFlushing = false;
|
|
1400
|
+
this.endpoint = endpoint2;
|
|
1401
|
+
this.config = { ...DEFAULT_QUEUE_CONFIG, ...config2 };
|
|
1402
|
+
this.loadFromStorage();
|
|
1241
1403
|
}
|
|
1242
1404
|
/**
|
|
1243
|
-
*
|
|
1405
|
+
* Add event to queue
|
|
1244
1406
|
*/
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
}
|
|
1254
|
-
this.
|
|
1255
|
-
|
|
1256
|
-
|
|
1407
|
+
push(type, payload, headers) {
|
|
1408
|
+
const event = {
|
|
1409
|
+
id: this.generateId(),
|
|
1410
|
+
type,
|
|
1411
|
+
payload,
|
|
1412
|
+
headers,
|
|
1413
|
+
timestamp: Date.now(),
|
|
1414
|
+
retries: 0
|
|
1415
|
+
};
|
|
1416
|
+
this.queue.push(event);
|
|
1417
|
+
this.saveToStorage();
|
|
1418
|
+
this.scheduleBatch();
|
|
1257
1419
|
}
|
|
1258
1420
|
/**
|
|
1259
|
-
*
|
|
1421
|
+
* Force flush all events immediately
|
|
1260
1422
|
*/
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
clearInterval(this.idleCheckInterval);
|
|
1273
|
-
this.idleCheckInterval = null;
|
|
1423
|
+
async flush() {
|
|
1424
|
+
if (this.isFlushing || this.queue.length === 0) return;
|
|
1425
|
+
this.isFlushing = true;
|
|
1426
|
+
this.clearBatchTimer();
|
|
1427
|
+
try {
|
|
1428
|
+
const events = [...this.queue];
|
|
1429
|
+
this.queue = [];
|
|
1430
|
+
await this.sendBatch(events);
|
|
1431
|
+
} finally {
|
|
1432
|
+
this.isFlushing = false;
|
|
1433
|
+
this.saveToStorage();
|
|
1274
1434
|
}
|
|
1275
1435
|
}
|
|
1276
1436
|
/**
|
|
1277
|
-
*
|
|
1437
|
+
* Flush using sendBeacon (for unload events)
|
|
1278
1438
|
*/
|
|
1279
|
-
|
|
1280
|
-
this.
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1439
|
+
flushBeacon() {
|
|
1440
|
+
if (this.queue.length === 0) return true;
|
|
1441
|
+
const baseUrl = this.config.apiKey ? `${this.endpoint}?api_key=${encodeURIComponent(this.config.apiKey)}` : this.endpoint;
|
|
1442
|
+
let allSent = true;
|
|
1443
|
+
for (const event of this.queue) {
|
|
1444
|
+
const payload = {
|
|
1445
|
+
...event.payload,
|
|
1446
|
+
_queue_id: event.id,
|
|
1447
|
+
_queue_timestamp: event.timestamp
|
|
1448
|
+
};
|
|
1449
|
+
const success = navigator.sendBeacon?.(baseUrl, JSON.stringify(payload)) ?? false;
|
|
1450
|
+
if (!success) {
|
|
1451
|
+
allSent = false;
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (allSent) {
|
|
1456
|
+
this.queue = [];
|
|
1457
|
+
this.clearStorage();
|
|
1458
|
+
}
|
|
1459
|
+
return allSent;
|
|
1287
1460
|
}
|
|
1288
1461
|
/**
|
|
1289
|
-
* Get
|
|
1462
|
+
* Get current queue length
|
|
1290
1463
|
*/
|
|
1291
|
-
|
|
1292
|
-
this.
|
|
1293
|
-
return this.getMetrics();
|
|
1464
|
+
get length() {
|
|
1465
|
+
return this.queue.length;
|
|
1294
1466
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1467
|
+
scheduleBatch() {
|
|
1468
|
+
if (this.batchTimer) return;
|
|
1469
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
1470
|
+
this.flush();
|
|
1471
|
+
return;
|
|
1300
1472
|
}
|
|
1473
|
+
this.batchTimer = setTimeout(() => {
|
|
1474
|
+
this.batchTimer = null;
|
|
1475
|
+
this.flush();
|
|
1476
|
+
}, this.config.batchTimeout);
|
|
1301
1477
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1478
|
+
clearBatchTimer() {
|
|
1479
|
+
if (this.batchTimer) {
|
|
1480
|
+
clearTimeout(this.batchTimer);
|
|
1481
|
+
this.batchTimer = null;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async sendBatch(events) {
|
|
1485
|
+
if (events.length === 0) return;
|
|
1486
|
+
try {
|
|
1487
|
+
const results = await Promise.allSettled(
|
|
1488
|
+
events.map(async (event) => {
|
|
1489
|
+
const response = await fetch(this.endpoint, {
|
|
1490
|
+
method: "POST",
|
|
1491
|
+
headers: {
|
|
1492
|
+
"Content-Type": "application/json",
|
|
1493
|
+
...event.headers || {}
|
|
1494
|
+
},
|
|
1495
|
+
body: JSON.stringify({
|
|
1496
|
+
...event.payload,
|
|
1497
|
+
_queue_id: event.id,
|
|
1498
|
+
_queue_timestamp: event.timestamp
|
|
1499
|
+
})
|
|
1500
|
+
});
|
|
1501
|
+
if (!response.ok) {
|
|
1502
|
+
throw new Error(`HTTP ${response.status}`);
|
|
1503
|
+
}
|
|
1504
|
+
})
|
|
1505
|
+
);
|
|
1506
|
+
const failedEvents = events.filter((_, index) => results[index]?.status === "rejected");
|
|
1507
|
+
if (failedEvents.length > 0) {
|
|
1508
|
+
for (const event of failedEvents) {
|
|
1509
|
+
if (event.retries < this.config.maxRetries) {
|
|
1510
|
+
event.retries++;
|
|
1511
|
+
this.queue.push(event);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const delay = this.config.retryDelayMs * Math.pow(2, failedEvents[0].retries - 1);
|
|
1515
|
+
setTimeout(() => this.flush(), delay);
|
|
1516
|
+
}
|
|
1517
|
+
return;
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
for (const event of events) {
|
|
1520
|
+
if (event.retries < this.config.maxRetries) {
|
|
1521
|
+
event.retries++;
|
|
1522
|
+
this.queue.push(event);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
if (this.queue.length > 0) {
|
|
1526
|
+
const delay = this.config.retryDelayMs * Math.pow(2, events[0].retries - 1);
|
|
1527
|
+
setTimeout(() => this.flush(), delay);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
loadFromStorage() {
|
|
1532
|
+
try {
|
|
1533
|
+
const stored = localStorage.getItem(this.config.storageKey);
|
|
1534
|
+
if (stored) {
|
|
1535
|
+
const parsed = JSON.parse(stored);
|
|
1536
|
+
if (Array.isArray(parsed)) {
|
|
1537
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1e3;
|
|
1538
|
+
this.queue = parsed.filter((e) => e.timestamp > cutoff);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
saveToStorage() {
|
|
1545
|
+
try {
|
|
1546
|
+
if (this.queue.length > 0) {
|
|
1547
|
+
localStorage.setItem(this.config.storageKey, JSON.stringify(this.queue));
|
|
1308
1548
|
} else {
|
|
1309
|
-
this.
|
|
1549
|
+
this.clearStorage();
|
|
1310
1550
|
}
|
|
1551
|
+
} catch {
|
|
1311
1552
|
}
|
|
1312
|
-
this.lastUpdateTime = now;
|
|
1313
1553
|
}
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1554
|
+
clearStorage() {
|
|
1555
|
+
try {
|
|
1556
|
+
localStorage.removeItem(this.config.storageKey);
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
generateId() {
|
|
1561
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1318
1562
|
}
|
|
1319
1563
|
};
|
|
1320
1564
|
|
|
1321
|
-
// src/
|
|
1322
|
-
var
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
"phone",
|
|
1341
|
-
"company",
|
|
1342
|
-
"first",
|
|
1343
|
-
"last",
|
|
1344
|
-
"city",
|
|
1345
|
-
"country"
|
|
1346
|
-
],
|
|
1347
|
-
thankYouPatterns: [
|
|
1348
|
-
/thank[-_]?you/i,
|
|
1349
|
-
/success/i,
|
|
1350
|
-
/confirmation/i,
|
|
1351
|
-
/submitted/i,
|
|
1352
|
-
/complete/i
|
|
1353
|
-
]
|
|
1354
|
-
};
|
|
1355
|
-
var FormTracker = class {
|
|
1356
|
-
constructor(config2 = {}) {
|
|
1357
|
-
this.formStartTimes = /* @__PURE__ */ new Map();
|
|
1358
|
-
this.interactedForms = /* @__PURE__ */ new Set();
|
|
1359
|
-
this.mutationObserver = null;
|
|
1360
|
-
this.handleFocusIn = (e) => {
|
|
1361
|
-
const target = e.target;
|
|
1362
|
-
if (!this.isFormField(target)) return;
|
|
1363
|
-
const form = target.closest("form");
|
|
1364
|
-
const formId = this.getFormId(form || target);
|
|
1365
|
-
if (!this.formStartTimes.has(formId)) {
|
|
1366
|
-
this.formStartTimes.set(formId, Date.now());
|
|
1367
|
-
this.interactedForms.add(formId);
|
|
1368
|
-
this.emitEvent({
|
|
1369
|
-
event_type: "form_start",
|
|
1370
|
-
form_id: formId,
|
|
1371
|
-
form_type: this.detectFormType(form || target)
|
|
1372
|
-
});
|
|
1373
|
-
}
|
|
1374
|
-
const fieldName = this.getFieldName(target);
|
|
1375
|
-
if (fieldName && !this.isSensitiveField(fieldName)) {
|
|
1376
|
-
this.emitEvent({
|
|
1377
|
-
event_type: "form_field",
|
|
1378
|
-
form_id: formId,
|
|
1379
|
-
form_type: this.detectFormType(form || target),
|
|
1380
|
-
field_name: this.sanitizeFieldName(fieldName),
|
|
1381
|
-
field_type: target.type || target.tagName.toLowerCase()
|
|
1382
|
-
});
|
|
1565
|
+
// src/infrastructure/ping.ts
|
|
1566
|
+
var PingService = class {
|
|
1567
|
+
constructor(sessionId2, visitorId2, version, config2 = {}) {
|
|
1568
|
+
this.intervalId = null;
|
|
1569
|
+
this.isVisible = true;
|
|
1570
|
+
this.isFocused = true;
|
|
1571
|
+
this.currentScrollDepth = 0;
|
|
1572
|
+
this.ping = async () => {
|
|
1573
|
+
const data = this.getData();
|
|
1574
|
+
this.config.onPing?.(data);
|
|
1575
|
+
if (this.config.endpoint) {
|
|
1576
|
+
try {
|
|
1577
|
+
await fetch(this.config.endpoint, {
|
|
1578
|
+
method: "POST",
|
|
1579
|
+
headers: { "Content-Type": "application/json" },
|
|
1580
|
+
body: JSON.stringify(data)
|
|
1581
|
+
});
|
|
1582
|
+
} catch {
|
|
1583
|
+
}
|
|
1383
1584
|
}
|
|
1384
1585
|
};
|
|
1385
|
-
this.
|
|
1386
|
-
|
|
1387
|
-
if (!form || form.tagName !== "FORM") return;
|
|
1388
|
-
const formId = this.getFormId(form);
|
|
1389
|
-
const startTime = this.formStartTimes.get(formId);
|
|
1390
|
-
this.emitEvent({
|
|
1391
|
-
event_type: "form_submit",
|
|
1392
|
-
form_id: formId,
|
|
1393
|
-
form_type: this.detectFormType(form),
|
|
1394
|
-
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1395
|
-
is_conversion: true
|
|
1396
|
-
});
|
|
1586
|
+
this.handleVisibilityChange = () => {
|
|
1587
|
+
this.isVisible = document.visibilityState === "visible";
|
|
1397
1588
|
};
|
|
1398
|
-
this.
|
|
1399
|
-
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
if (form && form.classList.contains("hs-form")) {
|
|
1403
|
-
const formId = this.getFormId(form);
|
|
1404
|
-
const startTime = this.formStartTimes.get(formId);
|
|
1405
|
-
this.emitEvent({
|
|
1406
|
-
event_type: "form_submit",
|
|
1407
|
-
form_id: formId,
|
|
1408
|
-
form_type: "hubspot",
|
|
1409
|
-
time_to_submit_ms: startTime ? Date.now() - startTime : void 0,
|
|
1410
|
-
is_conversion: true
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1589
|
+
this.handleFocusChange = () => {
|
|
1590
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1591
|
+
if (this.intervalId && this.isVisible && this.isFocused) {
|
|
1592
|
+
this.ping();
|
|
1413
1593
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1594
|
+
};
|
|
1595
|
+
this.handleScroll = () => {
|
|
1596
|
+
const scrollPercent = Math.round(
|
|
1597
|
+
(window.scrollY + window.innerHeight) / document.documentElement.scrollHeight * 100
|
|
1598
|
+
);
|
|
1599
|
+
if (scrollPercent > this.currentScrollDepth) {
|
|
1600
|
+
this.currentScrollDepth = Math.min(scrollPercent, 100);
|
|
1421
1601
|
}
|
|
1422
1602
|
};
|
|
1603
|
+
this.sessionId = sessionId2;
|
|
1604
|
+
this.visitorId = visitorId2;
|
|
1605
|
+
this.version = version;
|
|
1606
|
+
this.pageLoadTime = Date.now();
|
|
1607
|
+
this.isVisible = document.visibilityState === "visible";
|
|
1608
|
+
this.isFocused = typeof document.hasFocus === "function" ? document.hasFocus() : true;
|
|
1423
1609
|
this.config = {
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
...DEFAULT_CONFIG3.sensitiveFields,
|
|
1428
|
-
...config2.sensitiveFields || []
|
|
1429
|
-
]
|
|
1610
|
+
interval: DEFAULT_CONFIG3.pingInterval,
|
|
1611
|
+
endpoint: "",
|
|
1612
|
+
...config2
|
|
1430
1613
|
};
|
|
1614
|
+
document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1615
|
+
window.addEventListener("focus", this.handleFocusChange);
|
|
1616
|
+
window.addEventListener("blur", this.handleFocusChange);
|
|
1617
|
+
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
1431
1618
|
}
|
|
1432
1619
|
/**
|
|
1433
|
-
* Start
|
|
1620
|
+
* Start the ping service
|
|
1434
1621
|
*/
|
|
1435
1622
|
start() {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
this.
|
|
1623
|
+
if (this.intervalId) return;
|
|
1624
|
+
this.intervalId = setInterval(() => {
|
|
1625
|
+
if (this.isVisible && this.isFocused) {
|
|
1626
|
+
this.ping();
|
|
1627
|
+
}
|
|
1628
|
+
}, this.config.interval);
|
|
1629
|
+
if (this.isVisible && this.isFocused) {
|
|
1630
|
+
this.ping();
|
|
1631
|
+
}
|
|
1442
1632
|
}
|
|
1443
1633
|
/**
|
|
1444
|
-
* Stop
|
|
1634
|
+
* Stop the ping service
|
|
1445
1635
|
*/
|
|
1446
1636
|
stop() {
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1637
|
+
if (this.intervalId) {
|
|
1638
|
+
clearInterval(this.intervalId);
|
|
1639
|
+
this.intervalId = null;
|
|
1640
|
+
}
|
|
1641
|
+
document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
1642
|
+
window.removeEventListener("focus", this.handleFocusChange);
|
|
1643
|
+
window.removeEventListener("blur", this.handleFocusChange);
|
|
1644
|
+
window.removeEventListener("scroll", this.handleScroll);
|
|
1451
1645
|
}
|
|
1452
1646
|
/**
|
|
1453
|
-
*
|
|
1647
|
+
* Update scroll depth (called by external scroll tracker)
|
|
1454
1648
|
*/
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
startMutationObserver() {
|
|
1459
|
-
this.mutationObserver = new MutationObserver((mutations) => {
|
|
1460
|
-
for (const mutation of mutations) {
|
|
1461
|
-
for (const node of mutation.addedNodes) {
|
|
1462
|
-
if (node instanceof HTMLElement) {
|
|
1463
|
-
if (node.classList?.contains("hs-form") || node.querySelector?.(".hs-form")) {
|
|
1464
|
-
this.trackEmbeddedForm(node, "hubspot");
|
|
1465
|
-
}
|
|
1466
|
-
if (node.classList?.contains("typeform-widget") || node.querySelector?.("[data-tf-widget]")) {
|
|
1467
|
-
this.trackEmbeddedForm(node, "typeform");
|
|
1468
|
-
}
|
|
1469
|
-
if (node.classList?.contains("jotform-form") || node.querySelector?.(".jotform-form")) {
|
|
1470
|
-
this.trackEmbeddedForm(node, "jotform");
|
|
1471
|
-
}
|
|
1472
|
-
if (node.classList?.contains("gform_wrapper") || node.querySelector?.(".gform_wrapper")) {
|
|
1473
|
-
this.trackEmbeddedForm(node, "gravity");
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
});
|
|
1479
|
-
this.mutationObserver.observe(document.body, {
|
|
1480
|
-
childList: true,
|
|
1481
|
-
subtree: true
|
|
1482
|
-
});
|
|
1483
|
-
}
|
|
1484
|
-
scanForEmbeddedForms() {
|
|
1485
|
-
document.querySelectorAll(".hs-form").forEach((form) => {
|
|
1486
|
-
this.trackEmbeddedForm(form, "hubspot");
|
|
1487
|
-
});
|
|
1488
|
-
document.querySelectorAll("[data-tf-widget], .typeform-widget").forEach((form) => {
|
|
1489
|
-
this.trackEmbeddedForm(form, "typeform");
|
|
1490
|
-
});
|
|
1491
|
-
document.querySelectorAll(".jotform-form").forEach((form) => {
|
|
1492
|
-
this.trackEmbeddedForm(form, "jotform");
|
|
1493
|
-
});
|
|
1494
|
-
document.querySelectorAll(".gform_wrapper").forEach((form) => {
|
|
1495
|
-
this.trackEmbeddedForm(form, "gravity");
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1498
|
-
trackEmbeddedForm(element, type) {
|
|
1499
|
-
const formId = `${type}_${this.getFormId(element)}`;
|
|
1500
|
-
element.addEventListener("focusin", () => {
|
|
1501
|
-
if (!this.formStartTimes.has(formId)) {
|
|
1502
|
-
this.formStartTimes.set(formId, Date.now());
|
|
1503
|
-
this.interactedForms.add(formId);
|
|
1504
|
-
this.emitEvent({
|
|
1505
|
-
event_type: "form_start",
|
|
1506
|
-
form_id: formId,
|
|
1507
|
-
form_type: type
|
|
1508
|
-
});
|
|
1509
|
-
}
|
|
1510
|
-
}, { passive: true });
|
|
1511
|
-
}
|
|
1512
|
-
checkThankYouPage() {
|
|
1513
|
-
const url = window.location.href.toLowerCase();
|
|
1514
|
-
const title = document.title.toLowerCase();
|
|
1515
|
-
for (const pattern of this.config.thankYouPatterns) {
|
|
1516
|
-
if (pattern.test(url) || pattern.test(title)) {
|
|
1517
|
-
this.emitEvent({
|
|
1518
|
-
event_type: "form_success",
|
|
1519
|
-
form_id: "page_conversion",
|
|
1520
|
-
form_type: "unknown",
|
|
1521
|
-
is_conversion: true
|
|
1522
|
-
});
|
|
1523
|
-
break;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
isFormField(element) {
|
|
1528
|
-
const tagName = element.tagName;
|
|
1529
|
-
return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
|
|
1530
|
-
}
|
|
1531
|
-
getFormId(element) {
|
|
1532
|
-
if (!element) return "unknown";
|
|
1533
|
-
return element.id || element.getAttribute("name") || element.getAttribute("data-form-id") || "form_" + Math.random().toString(36).substring(2, 8);
|
|
1534
|
-
}
|
|
1535
|
-
getFieldName(input) {
|
|
1536
|
-
return input.name || input.id || input.getAttribute("data-name") || "";
|
|
1537
|
-
}
|
|
1538
|
-
isSensitiveField(fieldName) {
|
|
1539
|
-
const lowerName = fieldName.toLowerCase();
|
|
1540
|
-
return this.config.sensitiveFields.some((sensitive) => lowerName.includes(sensitive));
|
|
1541
|
-
}
|
|
1542
|
-
sanitizeFieldName(fieldName) {
|
|
1543
|
-
return fieldName.replace(/[0-9]+/g, "*").substring(0, 50);
|
|
1544
|
-
}
|
|
1545
|
-
detectFormType(element) {
|
|
1546
|
-
if (element.classList.contains("hs-form") || element.closest(".hs-form")) {
|
|
1547
|
-
return "hubspot";
|
|
1548
|
-
}
|
|
1549
|
-
if (element.classList.contains("typeform-widget") || element.closest("[data-tf-widget]")) {
|
|
1550
|
-
return "typeform";
|
|
1551
|
-
}
|
|
1552
|
-
if (element.classList.contains("jotform-form") || element.closest(".jotform-form")) {
|
|
1553
|
-
return "jotform";
|
|
1554
|
-
}
|
|
1555
|
-
if (element.classList.contains("gform_wrapper") || element.closest(".gform_wrapper")) {
|
|
1556
|
-
return "gravity";
|
|
1557
|
-
}
|
|
1558
|
-
if (element.tagName === "FORM") {
|
|
1559
|
-
return "native";
|
|
1649
|
+
updateScrollDepth(depth) {
|
|
1650
|
+
if (depth > this.currentScrollDepth) {
|
|
1651
|
+
this.currentScrollDepth = depth;
|
|
1560
1652
|
}
|
|
1561
|
-
return "unknown";
|
|
1562
1653
|
}
|
|
1563
|
-
|
|
1564
|
-
|
|
1654
|
+
/**
|
|
1655
|
+
* Get current ping data
|
|
1656
|
+
*/
|
|
1657
|
+
getData() {
|
|
1658
|
+
return {
|
|
1659
|
+
session_id: this.sessionId,
|
|
1660
|
+
visitor_id: this.visitorId,
|
|
1661
|
+
url: window.location.href,
|
|
1662
|
+
time_on_page_ms: Date.now() - this.pageLoadTime,
|
|
1663
|
+
scroll_depth: this.currentScrollDepth,
|
|
1664
|
+
is_active: this.isVisible && this.isFocused,
|
|
1665
|
+
tracker_version: this.version
|
|
1666
|
+
};
|
|
1565
1667
|
}
|
|
1566
1668
|
};
|
|
1567
1669
|
|
|
@@ -1737,13 +1839,15 @@ var Loamly = (() => {
|
|
|
1737
1839
|
}
|
|
1738
1840
|
|
|
1739
1841
|
// src/core.ts
|
|
1740
|
-
var config = { apiHost:
|
|
1842
|
+
var config = { apiHost: DEFAULT_CONFIG3.apiHost };
|
|
1741
1843
|
var initialized = false;
|
|
1742
1844
|
var debugMode = false;
|
|
1743
1845
|
var visitorId = null;
|
|
1744
1846
|
var sessionId = null;
|
|
1847
|
+
var workspaceId = null;
|
|
1745
1848
|
var navigationTiming = null;
|
|
1746
1849
|
var aiDetection = null;
|
|
1850
|
+
var pageStartTime = null;
|
|
1747
1851
|
var behavioralClassifier = null;
|
|
1748
1852
|
var behavioralMLResult = null;
|
|
1749
1853
|
var focusBlurAnalyzer = null;
|
|
@@ -1763,6 +1867,28 @@ var Loamly = (() => {
|
|
|
1763
1867
|
function endpoint(path) {
|
|
1764
1868
|
return `${config.apiHost}${path}`;
|
|
1765
1869
|
}
|
|
1870
|
+
function buildHeaders(idempotencyKey) {
|
|
1871
|
+
const headers = {
|
|
1872
|
+
"Content-Type": "application/json"
|
|
1873
|
+
};
|
|
1874
|
+
if (config.apiKey) {
|
|
1875
|
+
headers["X-Loamly-Api-Key"] = config.apiKey;
|
|
1876
|
+
}
|
|
1877
|
+
if (idempotencyKey) {
|
|
1878
|
+
headers["X-Idempotency-Key"] = idempotencyKey;
|
|
1879
|
+
}
|
|
1880
|
+
return headers;
|
|
1881
|
+
}
|
|
1882
|
+
function buildBeaconUrl(path) {
|
|
1883
|
+
if (!config.apiKey) return path;
|
|
1884
|
+
const url = new URL(path, config.apiHost);
|
|
1885
|
+
url.searchParams.set("api_key", config.apiKey);
|
|
1886
|
+
return url.toString();
|
|
1887
|
+
}
|
|
1888
|
+
function buildIdempotencyKey(prefix) {
|
|
1889
|
+
const base = sessionId || visitorId || "unknown";
|
|
1890
|
+
return `${prefix}:${base}:${Date.now()}`;
|
|
1891
|
+
}
|
|
1766
1892
|
function init(userConfig = {}) {
|
|
1767
1893
|
if (initialized) {
|
|
1768
1894
|
log("Already initialized");
|
|
@@ -1771,9 +1897,13 @@ var Loamly = (() => {
|
|
|
1771
1897
|
config = {
|
|
1772
1898
|
...config,
|
|
1773
1899
|
...userConfig,
|
|
1774
|
-
apiHost: userConfig.apiHost ||
|
|
1900
|
+
apiHost: userConfig.apiHost || DEFAULT_CONFIG3.apiHost
|
|
1775
1901
|
};
|
|
1902
|
+
workspaceId = userConfig.workspaceId ?? null;
|
|
1776
1903
|
debugMode = userConfig.debug ?? false;
|
|
1904
|
+
if (config.apiKey && !workspaceId) {
|
|
1905
|
+
log("Workspace ID missing. Behavioral events require workspaceId.");
|
|
1906
|
+
}
|
|
1777
1907
|
const features = {
|
|
1778
1908
|
scroll: true,
|
|
1779
1909
|
time: true,
|
|
@@ -1791,13 +1921,11 @@ var Loamly = (() => {
|
|
|
1791
1921
|
log("Features:", features);
|
|
1792
1922
|
visitorId = getVisitorId();
|
|
1793
1923
|
log("Visitor ID:", visitorId);
|
|
1794
|
-
const session = getSessionId();
|
|
1795
|
-
sessionId = session.sessionId;
|
|
1796
|
-
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
1797
1924
|
if (features.eventQueue) {
|
|
1798
|
-
eventQueue = new EventQueue(endpoint(
|
|
1799
|
-
batchSize:
|
|
1800
|
-
batchTimeout:
|
|
1925
|
+
eventQueue = new EventQueue(endpoint(DEFAULT_CONFIG3.endpoints.behavioral), {
|
|
1926
|
+
batchSize: DEFAULT_CONFIG3.batchSize,
|
|
1927
|
+
batchTimeout: DEFAULT_CONFIG3.batchTimeout,
|
|
1928
|
+
apiKey: config.apiKey
|
|
1801
1929
|
});
|
|
1802
1930
|
}
|
|
1803
1931
|
navigationTiming = detectNavigationType();
|
|
@@ -1807,37 +1935,40 @@ var Loamly = (() => {
|
|
|
1807
1935
|
log("AI detected:", aiDetection);
|
|
1808
1936
|
}
|
|
1809
1937
|
initialized = true;
|
|
1810
|
-
if (!userConfig.disableAutoPageview) {
|
|
1811
|
-
pageview();
|
|
1812
|
-
}
|
|
1813
|
-
if (!userConfig.disableBehavioral) {
|
|
1814
|
-
setupAdvancedBehavioralTracking(features);
|
|
1815
|
-
}
|
|
1816
|
-
if (features.behavioralML) {
|
|
1817
|
-
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1818
|
-
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1819
|
-
setupBehavioralMLTracking();
|
|
1820
|
-
}
|
|
1821
|
-
if (features.focusBlur) {
|
|
1822
|
-
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1823
|
-
focusBlurAnalyzer.initTracking();
|
|
1824
|
-
setTimeout(() => {
|
|
1825
|
-
if (focusBlurAnalyzer) {
|
|
1826
|
-
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1827
|
-
}
|
|
1828
|
-
}, 5e3);
|
|
1829
|
-
}
|
|
1830
1938
|
if (features.agentic) {
|
|
1831
1939
|
agenticAnalyzer = new AgenticBrowserAnalyzer();
|
|
1832
1940
|
agenticAnalyzer.init();
|
|
1833
1941
|
}
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1942
|
+
void initializeSession().finally(() => {
|
|
1943
|
+
void registerServiceWorker();
|
|
1944
|
+
if (!userConfig.disableAutoPageview) {
|
|
1945
|
+
pageview();
|
|
1946
|
+
}
|
|
1947
|
+
if (!userConfig.disableBehavioral) {
|
|
1948
|
+
setupAdvancedBehavioralTracking(features);
|
|
1949
|
+
}
|
|
1950
|
+
if (features.behavioralML) {
|
|
1951
|
+
behavioralClassifier = new BehavioralClassifier(1e4);
|
|
1952
|
+
behavioralClassifier.setOnClassify(handleBehavioralClassification);
|
|
1953
|
+
setupBehavioralMLTracking();
|
|
1954
|
+
}
|
|
1955
|
+
if (features.focusBlur) {
|
|
1956
|
+
focusBlurAnalyzer = new FocusBlurAnalyzer();
|
|
1957
|
+
focusBlurAnalyzer.initTracking();
|
|
1958
|
+
setTimeout(() => {
|
|
1959
|
+
if (focusBlurAnalyzer) {
|
|
1960
|
+
handleFocusBlurAnalysis(focusBlurAnalyzer.analyze());
|
|
1961
|
+
}
|
|
1962
|
+
}, 5e3);
|
|
1963
|
+
}
|
|
1964
|
+
if (features.ping && visitorId && sessionId) {
|
|
1965
|
+
pingService = new PingService(sessionId, visitorId, VERSION, {
|
|
1966
|
+
interval: DEFAULT_CONFIG3.pingInterval,
|
|
1967
|
+
endpoint: endpoint(DEFAULT_CONFIG3.endpoints.ping)
|
|
1968
|
+
});
|
|
1969
|
+
pingService.start();
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1841
1972
|
spaRouter = new SPARouter({
|
|
1842
1973
|
onNavigate: handleSPANavigation
|
|
1843
1974
|
});
|
|
@@ -1846,6 +1977,78 @@ var Loamly = (() => {
|
|
|
1846
1977
|
reportHealth("initialized");
|
|
1847
1978
|
log("Initialization complete");
|
|
1848
1979
|
}
|
|
1980
|
+
async function registerServiceWorker() {
|
|
1981
|
+
if (typeof navigator === "undefined" || !("serviceWorker" in navigator)) return;
|
|
1982
|
+
if (!config.apiKey || !workspaceId) return;
|
|
1983
|
+
try {
|
|
1984
|
+
const swUrl = new URL("/tracker/loamly-sw.js", window.location.origin);
|
|
1985
|
+
swUrl.searchParams.set("workspace_id", workspaceId);
|
|
1986
|
+
swUrl.searchParams.set("api_key", config.apiKey);
|
|
1987
|
+
const registration = await navigator.serviceWorker.register(swUrl.toString(), { scope: "/" });
|
|
1988
|
+
registration.addEventListener("updatefound", () => {
|
|
1989
|
+
const installing = registration.installing;
|
|
1990
|
+
installing?.addEventListener("statechange", () => {
|
|
1991
|
+
if (installing.state === "activated") {
|
|
1992
|
+
installing.postMessage({ type: "SKIP_WAITING" });
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
});
|
|
1996
|
+
setInterval(() => {
|
|
1997
|
+
registration.update().catch(() => {
|
|
1998
|
+
});
|
|
1999
|
+
}, 24 * 60 * 60 * 1e3);
|
|
2000
|
+
} catch {
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
async function initializeSession() {
|
|
2004
|
+
const now = Date.now();
|
|
2005
|
+
pageStartTime = now;
|
|
2006
|
+
try {
|
|
2007
|
+
const storedSession = sessionStorage.getItem("loamly_session");
|
|
2008
|
+
const storedStart = sessionStorage.getItem("loamly_start");
|
|
2009
|
+
const sessionTimeout = config.sessionTimeout ?? DEFAULT_CONFIG3.sessionTimeout;
|
|
2010
|
+
if (storedSession && storedStart) {
|
|
2011
|
+
const startTime = parseInt(storedStart, 10);
|
|
2012
|
+
const elapsed = now - startTime;
|
|
2013
|
+
if (elapsed > 0 && elapsed < sessionTimeout) {
|
|
2014
|
+
sessionId = storedSession;
|
|
2015
|
+
log("Session ID:", sessionId, "(existing)");
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
} catch {
|
|
2020
|
+
}
|
|
2021
|
+
if (config.apiKey && workspaceId && visitorId) {
|
|
2022
|
+
try {
|
|
2023
|
+
const response = await safeFetch(endpoint(DEFAULT_CONFIG3.endpoints.session), {
|
|
2024
|
+
method: "POST",
|
|
2025
|
+
headers: buildHeaders(),
|
|
2026
|
+
body: JSON.stringify({
|
|
2027
|
+
workspace_id: workspaceId,
|
|
2028
|
+
visitor_id: visitorId
|
|
2029
|
+
})
|
|
2030
|
+
});
|
|
2031
|
+
if (response?.ok) {
|
|
2032
|
+
const data = await response.json();
|
|
2033
|
+
sessionId = data.session_id || sessionId;
|
|
2034
|
+
const startTime = data.start_time || now;
|
|
2035
|
+
if (sessionId) {
|
|
2036
|
+
try {
|
|
2037
|
+
sessionStorage.setItem("loamly_session", sessionId);
|
|
2038
|
+
sessionStorage.setItem("loamly_start", String(startTime));
|
|
2039
|
+
} catch {
|
|
2040
|
+
}
|
|
2041
|
+
log("Session ID:", sessionId, "(server)");
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
} catch {
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const session = getSessionId();
|
|
2049
|
+
sessionId = session.sessionId;
|
|
2050
|
+
log("Session ID:", sessionId, session.isNew ? "(new)" : "(existing)");
|
|
2051
|
+
}
|
|
1849
2052
|
function setupAdvancedBehavioralTracking(features) {
|
|
1850
2053
|
if (features.scroll) {
|
|
1851
2054
|
scrollTracker = new ScrollTracker({
|
|
@@ -1853,8 +2056,8 @@ var Loamly = (() => {
|
|
|
1853
2056
|
onChunkReached: (event) => {
|
|
1854
2057
|
log("Scroll chunk:", event.chunk);
|
|
1855
2058
|
queueEvent("scroll_depth", {
|
|
1856
|
-
|
|
1857
|
-
|
|
2059
|
+
scroll_depth: Math.round(event.depth / 100 * 100) / 100,
|
|
2060
|
+
milestone: Math.round(event.chunk / 100 * 100) / 100,
|
|
1858
2061
|
time_to_reach_ms: event.time_to_reach_ms
|
|
1859
2062
|
});
|
|
1860
2063
|
}
|
|
@@ -1866,12 +2069,10 @@ var Loamly = (() => {
|
|
|
1866
2069
|
updateIntervalMs: 1e4,
|
|
1867
2070
|
// Report every 10 seconds
|
|
1868
2071
|
onUpdate: (event) => {
|
|
1869
|
-
if (event.active_time_ms >=
|
|
2072
|
+
if (event.active_time_ms >= DEFAULT_CONFIG3.timeSpentThresholdMs) {
|
|
1870
2073
|
queueEvent("time_spent", {
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
idle_time_ms: event.idle_time_ms,
|
|
1874
|
-
is_engaged: event.is_engaged
|
|
2074
|
+
visible_time_ms: event.total_time_ms,
|
|
2075
|
+
page_start_time: pageStartTime || Date.now()
|
|
1875
2076
|
});
|
|
1876
2077
|
}
|
|
1877
2078
|
}
|
|
@@ -1882,13 +2083,22 @@ var Loamly = (() => {
|
|
|
1882
2083
|
formTracker = new FormTracker({
|
|
1883
2084
|
onFormEvent: (event) => {
|
|
1884
2085
|
log("Form event:", event.event_type, event.form_id);
|
|
1885
|
-
|
|
2086
|
+
const isSubmitEvent = event.event_type === "form_submit";
|
|
2087
|
+
const isSuccessEvent = event.event_type === "form_success";
|
|
2088
|
+
const normalizedEventType = isSubmitEvent || isSuccessEvent ? "form_submit" : "form_focus";
|
|
2089
|
+
const submitSource = event.submit_source || (isSuccessEvent ? "thank_you" : isSubmitEvent ? "submit" : null);
|
|
2090
|
+
queueEvent(normalizedEventType, {
|
|
1886
2091
|
form_id: event.form_id,
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
2092
|
+
form_provider: event.form_type || "unknown",
|
|
2093
|
+
form_field_type: event.field_type || null,
|
|
2094
|
+
form_field_name: event.field_name || null,
|
|
2095
|
+
form_event_type: event.event_type,
|
|
2096
|
+
submit_source: submitSource,
|
|
2097
|
+
is_inferred: isSuccessEvent,
|
|
2098
|
+
time_to_submit_seconds: event.time_to_submit_ms ? Math.round(event.time_to_submit_ms / 1e3) : null,
|
|
2099
|
+
// LOA-482: Include captured form field values
|
|
2100
|
+
fields: event.fields || null,
|
|
2101
|
+
email_submitted: event.email_submitted || null
|
|
1892
2102
|
});
|
|
1893
2103
|
}
|
|
1894
2104
|
});
|
|
@@ -1909,7 +2119,7 @@ var Loamly = (() => {
|
|
|
1909
2119
|
if (link && link.href) {
|
|
1910
2120
|
const isExternal = link.hostname !== window.location.hostname;
|
|
1911
2121
|
queueEvent("click", {
|
|
1912
|
-
|
|
2122
|
+
element_type: "link",
|
|
1913
2123
|
href: truncateText(link.href, 200),
|
|
1914
2124
|
text: truncateText(link.textContent || "", 100),
|
|
1915
2125
|
is_external: isExternal
|
|
@@ -1919,15 +2129,32 @@ var Loamly = (() => {
|
|
|
1919
2129
|
}
|
|
1920
2130
|
function queueEvent(eventType, data) {
|
|
1921
2131
|
if (!eventQueue) return;
|
|
1922
|
-
|
|
2132
|
+
if (!config.apiKey) {
|
|
2133
|
+
log("Missing apiKey, behavioral event skipped:", eventType);
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (!workspaceId) {
|
|
2137
|
+
log("Missing workspaceId, behavioral event skipped:", eventType);
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
if (!sessionId) {
|
|
2141
|
+
log("Missing sessionId, behavioral event skipped:", eventType);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
const idempotencyKey = buildIdempotencyKey(eventType);
|
|
2145
|
+
const payload = {
|
|
1923
2146
|
visitor_id: visitorId,
|
|
1924
2147
|
session_id: sessionId,
|
|
1925
2148
|
event_type: eventType,
|
|
1926
|
-
|
|
1927
|
-
|
|
2149
|
+
event_data: data,
|
|
2150
|
+
page_url: window.location.href,
|
|
2151
|
+
page_path: window.location.pathname,
|
|
1928
2152
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1929
|
-
tracker_version: VERSION
|
|
1930
|
-
|
|
2153
|
+
tracker_version: VERSION,
|
|
2154
|
+
idempotency_key: idempotencyKey
|
|
2155
|
+
};
|
|
2156
|
+
payload.workspace_id = workspaceId;
|
|
2157
|
+
eventQueue.push(eventType, payload, buildHeaders(idempotencyKey));
|
|
1931
2158
|
}
|
|
1932
2159
|
function handleSPANavigation(event) {
|
|
1933
2160
|
log("SPA navigation:", event.navigation_type, event.to_url);
|
|
@@ -1955,34 +2182,42 @@ var Loamly = (() => {
|
|
|
1955
2182
|
}
|
|
1956
2183
|
function setupUnloadHandlers() {
|
|
1957
2184
|
const handleUnload = () => {
|
|
2185
|
+
if (!workspaceId || !config.apiKey || !sessionId) return;
|
|
1958
2186
|
const scrollEvent = scrollTracker?.getFinalEvent();
|
|
1959
2187
|
if (scrollEvent) {
|
|
1960
|
-
sendBeacon(endpoint(
|
|
2188
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG3.endpoints.behavioral)), {
|
|
2189
|
+
workspace_id: workspaceId,
|
|
1961
2190
|
visitor_id: visitorId,
|
|
1962
2191
|
session_id: sessionId,
|
|
1963
2192
|
event_type: "scroll_depth_final",
|
|
1964
|
-
|
|
1965
|
-
|
|
2193
|
+
event_data: {
|
|
2194
|
+
scroll_depth: Math.round(scrollEvent.depth / 100 * 100) / 100,
|
|
2195
|
+
milestone: Math.round(scrollEvent.chunk / 100 * 100) / 100,
|
|
2196
|
+
time_to_reach_ms: scrollEvent.time_to_reach_ms
|
|
2197
|
+
},
|
|
2198
|
+
page_url: window.location.href,
|
|
2199
|
+
page_path: window.location.pathname,
|
|
2200
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2201
|
+
tracker_version: VERSION,
|
|
2202
|
+
idempotency_key: buildIdempotencyKey("scroll_depth_final")
|
|
1966
2203
|
});
|
|
1967
2204
|
}
|
|
1968
2205
|
const timeEvent = timeTracker?.getFinalMetrics();
|
|
1969
2206
|
if (timeEvent) {
|
|
1970
|
-
sendBeacon(endpoint(
|
|
1971
|
-
|
|
1972
|
-
session_id: sessionId,
|
|
1973
|
-
event_type: "time_spent_final",
|
|
1974
|
-
data: timeEvent,
|
|
1975
|
-
url: window.location.href
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
1978
|
-
const agenticResult = agenticAnalyzer?.getResult();
|
|
1979
|
-
if (agenticResult && agenticResult.agenticProbability > 0) {
|
|
1980
|
-
sendBeacon(endpoint(DEFAULT_CONFIG.endpoints.behavioral), {
|
|
2207
|
+
sendBeacon(buildBeaconUrl(endpoint(DEFAULT_CONFIG3.endpoints.behavioral)), {
|
|
2208
|
+
workspace_id: workspaceId,
|
|
1981
2209
|
visitor_id: visitorId,
|
|
1982
2210
|
session_id: sessionId,
|
|
1983
|
-
event_type: "
|
|
1984
|
-
|
|
1985
|
-
|
|
2211
|
+
event_type: "time_spent",
|
|
2212
|
+
event_data: {
|
|
2213
|
+
visible_time_ms: timeEvent.total_time_ms,
|
|
2214
|
+
page_start_time: pageStartTime || Date.now()
|
|
2215
|
+
},
|
|
2216
|
+
page_url: window.location.href,
|
|
2217
|
+
page_path: window.location.pathname,
|
|
2218
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2219
|
+
tracker_version: VERSION,
|
|
2220
|
+
idempotency_key: buildIdempotencyKey("time_spent")
|
|
1986
2221
|
});
|
|
1987
2222
|
}
|
|
1988
2223
|
eventQueue?.flushBeacon();
|
|
@@ -2005,30 +2240,55 @@ var Loamly = (() => {
|
|
|
2005
2240
|
log("Not initialized, call init() first");
|
|
2006
2241
|
return;
|
|
2007
2242
|
}
|
|
2243
|
+
if (!config.apiKey) {
|
|
2244
|
+
log("Missing apiKey, pageview skipped");
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2008
2247
|
const url = customUrl || window.location.href;
|
|
2248
|
+
const utmParams = extractUTMParams(url);
|
|
2249
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2250
|
+
const idempotencyKey = buildIdempotencyKey("visit");
|
|
2251
|
+
const agenticResult = agenticAnalyzer?.getResult();
|
|
2252
|
+
const pagePath = (() => {
|
|
2253
|
+
try {
|
|
2254
|
+
return new URL(url).pathname;
|
|
2255
|
+
} catch {
|
|
2256
|
+
return window.location.pathname;
|
|
2257
|
+
}
|
|
2258
|
+
})();
|
|
2009
2259
|
const payload = {
|
|
2010
2260
|
visitor_id: visitorId,
|
|
2011
2261
|
session_id: sessionId,
|
|
2012
|
-
url,
|
|
2262
|
+
page_url: url,
|
|
2263
|
+
page_path: pagePath,
|
|
2013
2264
|
referrer: document.referrer || null,
|
|
2014
2265
|
title: document.title || null,
|
|
2015
|
-
utm_source:
|
|
2016
|
-
utm_medium:
|
|
2017
|
-
utm_campaign:
|
|
2266
|
+
utm_source: utmParams.utm_source || null,
|
|
2267
|
+
utm_medium: utmParams.utm_medium || null,
|
|
2268
|
+
utm_campaign: utmParams.utm_campaign || null,
|
|
2269
|
+
utm_term: utmParams.utm_term || null,
|
|
2270
|
+
utm_content: utmParams.utm_content || null,
|
|
2018
2271
|
user_agent: navigator.userAgent,
|
|
2019
2272
|
screen_width: window.screen?.width,
|
|
2020
2273
|
screen_height: window.screen?.height,
|
|
2021
2274
|
language: navigator.language,
|
|
2022
2275
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
2023
2276
|
tracker_version: VERSION,
|
|
2277
|
+
event_type: "pageview",
|
|
2278
|
+
event_data: null,
|
|
2279
|
+
timestamp,
|
|
2024
2280
|
navigation_timing: navigationTiming,
|
|
2025
2281
|
ai_platform: aiDetection?.platform || null,
|
|
2026
|
-
is_ai_referrer: aiDetection?.isAI || false
|
|
2282
|
+
is_ai_referrer: aiDetection?.isAI || false,
|
|
2283
|
+
agentic_detection: agenticResult || null
|
|
2027
2284
|
};
|
|
2285
|
+
if (workspaceId) {
|
|
2286
|
+
payload.workspace_id = workspaceId;
|
|
2287
|
+
}
|
|
2028
2288
|
log("Pageview:", payload);
|
|
2029
|
-
safeFetch(endpoint(
|
|
2289
|
+
safeFetch(endpoint(DEFAULT_CONFIG3.endpoints.visit), {
|
|
2030
2290
|
method: "POST",
|
|
2031
|
-
headers:
|
|
2291
|
+
headers: buildHeaders(idempotencyKey),
|
|
2032
2292
|
body: JSON.stringify(payload)
|
|
2033
2293
|
});
|
|
2034
2294
|
}
|
|
@@ -2037,6 +2297,11 @@ var Loamly = (() => {
|
|
|
2037
2297
|
log("Not initialized, call init() first");
|
|
2038
2298
|
return;
|
|
2039
2299
|
}
|
|
2300
|
+
if (!config.apiKey) {
|
|
2301
|
+
log("Missing apiKey, event skipped:", eventName);
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
const idempotencyKey = buildIdempotencyKey(`event:${eventName}`);
|
|
2040
2305
|
const payload = {
|
|
2041
2306
|
visitor_id: visitorId,
|
|
2042
2307
|
session_id: sessionId,
|
|
@@ -2045,14 +2310,19 @@ var Loamly = (() => {
|
|
|
2045
2310
|
properties: options.properties || {},
|
|
2046
2311
|
revenue: options.revenue,
|
|
2047
2312
|
currency: options.currency || "USD",
|
|
2048
|
-
|
|
2313
|
+
page_url: window.location.href,
|
|
2314
|
+
referrer: document.referrer || null,
|
|
2049
2315
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2050
|
-
tracker_version: VERSION
|
|
2316
|
+
tracker_version: VERSION,
|
|
2317
|
+
idempotency_key: idempotencyKey
|
|
2051
2318
|
};
|
|
2319
|
+
if (workspaceId) {
|
|
2320
|
+
payload.workspace_id = workspaceId;
|
|
2321
|
+
}
|
|
2052
2322
|
log("Event:", eventName, payload);
|
|
2053
2323
|
safeFetch(endpoint("/api/ingest/event"), {
|
|
2054
2324
|
method: "POST",
|
|
2055
|
-
headers:
|
|
2325
|
+
headers: buildHeaders(idempotencyKey),
|
|
2056
2326
|
body: JSON.stringify(payload)
|
|
2057
2327
|
});
|
|
2058
2328
|
}
|
|
@@ -2064,17 +2334,26 @@ var Loamly = (() => {
|
|
|
2064
2334
|
log("Not initialized, call init() first");
|
|
2065
2335
|
return;
|
|
2066
2336
|
}
|
|
2337
|
+
if (!config.apiKey) {
|
|
2338
|
+
log("Missing apiKey, identify skipped");
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2067
2341
|
log("Identify:", userId, traits);
|
|
2342
|
+
const idempotencyKey = buildIdempotencyKey("identify");
|
|
2068
2343
|
const payload = {
|
|
2069
2344
|
visitor_id: visitorId,
|
|
2070
2345
|
session_id: sessionId,
|
|
2071
2346
|
user_id: userId,
|
|
2072
2347
|
traits,
|
|
2073
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2348
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2349
|
+
idempotency_key: idempotencyKey
|
|
2074
2350
|
};
|
|
2351
|
+
if (workspaceId) {
|
|
2352
|
+
payload.workspace_id = workspaceId;
|
|
2353
|
+
}
|
|
2075
2354
|
safeFetch(endpoint("/api/ingest/identify"), {
|
|
2076
2355
|
method: "POST",
|
|
2077
|
-
headers:
|
|
2356
|
+
headers: buildHeaders(idempotencyKey),
|
|
2078
2357
|
body: JSON.stringify(payload)
|
|
2079
2358
|
});
|
|
2080
2359
|
}
|
|
@@ -2205,14 +2484,15 @@ var Loamly = (() => {
|
|
|
2205
2484
|
return initialized;
|
|
2206
2485
|
}
|
|
2207
2486
|
function reportHealth(status, errorMessage) {
|
|
2208
|
-
if (!config.apiKey) return;
|
|
2209
2487
|
try {
|
|
2210
2488
|
const healthData = {
|
|
2211
|
-
workspace_id:
|
|
2489
|
+
workspace_id: workspaceId,
|
|
2490
|
+
visitor_id: visitorId,
|
|
2491
|
+
session_id: sessionId,
|
|
2212
2492
|
status,
|
|
2213
2493
|
error_message: errorMessage || null,
|
|
2214
|
-
|
|
2215
|
-
|
|
2494
|
+
tracker_version: VERSION,
|
|
2495
|
+
page_url: typeof window !== "undefined" ? window.location.href : null,
|
|
2216
2496
|
user_agent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
|
2217
2497
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2218
2498
|
features: {
|
|
@@ -2227,7 +2507,7 @@ var Loamly = (() => {
|
|
|
2227
2507
|
event_queue: !!eventQueue
|
|
2228
2508
|
}
|
|
2229
2509
|
};
|
|
2230
|
-
safeFetch(endpoint(
|
|
2510
|
+
safeFetch(endpoint(DEFAULT_CONFIG3.endpoints.health), {
|
|
2231
2511
|
method: "POST",
|
|
2232
2512
|
headers: { "Content-Type": "application/json" },
|
|
2233
2513
|
body: JSON.stringify(healthData)
|
|
@@ -2271,14 +2551,28 @@ var Loamly = (() => {
|
|
|
2271
2551
|
debugMode = enabled;
|
|
2272
2552
|
log("Debug mode:", enabled ? "enabled" : "disabled");
|
|
2273
2553
|
}
|
|
2554
|
+
function trackBehavioral(eventType, eventData) {
|
|
2555
|
+
if (!initialized) {
|
|
2556
|
+
log("Not initialized, trackBehavioral skipped:", eventType);
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
queueEvent(eventType, eventData);
|
|
2560
|
+
}
|
|
2561
|
+
function getCurrentWorkspaceId() {
|
|
2562
|
+
return workspaceId;
|
|
2563
|
+
}
|
|
2274
2564
|
var loamly = {
|
|
2275
2565
|
init,
|
|
2276
2566
|
pageview,
|
|
2277
2567
|
track,
|
|
2568
|
+
trackBehavioral,
|
|
2569
|
+
// NEW: For secondary modules/plugins
|
|
2278
2570
|
conversion,
|
|
2279
2571
|
identify,
|
|
2280
2572
|
getSessionId: getCurrentSessionId,
|
|
2281
2573
|
getVisitorId: getCurrentVisitorId,
|
|
2574
|
+
getWorkspaceId: getCurrentWorkspaceId,
|
|
2575
|
+
// NEW: For debugging/introspection
|
|
2282
2576
|
getAIDetection: getAIDetectionResult,
|
|
2283
2577
|
getNavigationTiming: getNavigationTimingResult,
|
|
2284
2578
|
getBehavioralML: getBehavioralMLResult,
|
|
@@ -2308,16 +2602,17 @@ var Loamly = (() => {
|
|
|
2308
2602
|
}
|
|
2309
2603
|
async function resolveWorkspaceConfig(domain) {
|
|
2310
2604
|
try {
|
|
2311
|
-
const response = await fetch(`${
|
|
2605
|
+
const response = await fetch(`${DEFAULT_CONFIG3.apiHost}${DEFAULT_CONFIG3.endpoints.resolve}?d=${encodeURIComponent(domain)}`);
|
|
2312
2606
|
if (!response.ok) {
|
|
2313
2607
|
console.warn("[Loamly] Failed to resolve workspace for domain:", domain);
|
|
2314
2608
|
return null;
|
|
2315
2609
|
}
|
|
2316
2610
|
const data = await response.json();
|
|
2317
|
-
if (data.workspace_id) {
|
|
2611
|
+
if (data.workspace_id && data.public_key) {
|
|
2318
2612
|
return {
|
|
2319
|
-
apiKey: data.
|
|
2320
|
-
|
|
2613
|
+
apiKey: data.public_key,
|
|
2614
|
+
workspaceId: data.workspace_id,
|
|
2615
|
+
apiHost: DEFAULT_CONFIG3.apiHost
|
|
2321
2616
|
};
|
|
2322
2617
|
}
|
|
2323
2618
|
return null;
|
|
@@ -2334,6 +2629,9 @@ var Loamly = (() => {
|
|
|
2334
2629
|
if (script.dataset.apiKey) {
|
|
2335
2630
|
config2.apiKey = script.dataset.apiKey;
|
|
2336
2631
|
}
|
|
2632
|
+
if (script.dataset.workspaceId) {
|
|
2633
|
+
config2.workspaceId = script.dataset.workspaceId;
|
|
2634
|
+
}
|
|
2337
2635
|
if (script.dataset.apiHost) {
|
|
2338
2636
|
config2.apiHost = script.dataset.apiHost;
|
|
2339
2637
|
}
|
|
@@ -2346,7 +2644,7 @@ var Loamly = (() => {
|
|
|
2346
2644
|
if (script.dataset.disableBehavioral === "true") {
|
|
2347
2645
|
config2.disableBehavioral = true;
|
|
2348
2646
|
}
|
|
2349
|
-
if (config2.apiKey) {
|
|
2647
|
+
if (config2.apiKey || config2.workspaceId) {
|
|
2350
2648
|
return config2;
|
|
2351
2649
|
}
|
|
2352
2650
|
}
|
|
@@ -2367,6 +2665,16 @@ var Loamly = (() => {
|
|
|
2367
2665
|
loamly.init(dataConfig);
|
|
2368
2666
|
return;
|
|
2369
2667
|
}
|
|
2668
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2669
|
+
const apiKeyParam = urlParams.get("api_key");
|
|
2670
|
+
const workspaceIdParam = urlParams.get("workspace_id");
|
|
2671
|
+
if (apiKeyParam || workspaceIdParam) {
|
|
2672
|
+
loamly.init({
|
|
2673
|
+
apiKey: apiKeyParam || void 0,
|
|
2674
|
+
workspaceId: workspaceIdParam || void 0
|
|
2675
|
+
});
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2370
2678
|
const currentDomain = window.location.hostname;
|
|
2371
2679
|
if (currentDomain && currentDomain !== "localhost") {
|
|
2372
2680
|
const resolvedConfig = await resolveWorkspaceConfig(currentDomain);
|