sitepong 0.0.1 → 0.0.5
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 +613 -61
- package/dist/index.d.mts +596 -3
- package/dist/index.d.ts +596 -3
- package/dist/index.js +4507 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4455 -10
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +409 -0
- package/dist/react/index.d.ts +409 -0
- package/dist/react/index.js +5922 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/index.mjs +5839 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +66 -54
package/dist/index.js
CHANGED
|
@@ -2,24 +2,4046 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
// src/flags/anonymous-id.ts
|
|
6
|
+
var STORAGE_KEY = "sitepong_anonymous_id";
|
|
7
|
+
function generateUUID() {
|
|
8
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
9
|
+
return crypto.randomUUID();
|
|
10
|
+
}
|
|
11
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
12
|
+
const r = Math.random() * 16 | 0;
|
|
13
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
14
|
+
return v.toString(16);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
var memoryId = null;
|
|
18
|
+
function getAnonymousId() {
|
|
19
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
20
|
+
try {
|
|
21
|
+
let id = localStorage.getItem(STORAGE_KEY);
|
|
22
|
+
if (!id) {
|
|
23
|
+
id = generateUUID();
|
|
24
|
+
localStorage.setItem(STORAGE_KEY, id);
|
|
25
|
+
}
|
|
26
|
+
return id;
|
|
27
|
+
} catch {
|
|
28
|
+
if (!memoryId) {
|
|
29
|
+
memoryId = generateUUID();
|
|
30
|
+
}
|
|
31
|
+
return memoryId;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!memoryId) {
|
|
35
|
+
memoryId = generateUUID();
|
|
36
|
+
}
|
|
37
|
+
return memoryId;
|
|
38
|
+
}
|
|
39
|
+
function clearAnonymousId() {
|
|
40
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
41
|
+
try {
|
|
42
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
memoryId = null;
|
|
47
|
+
}
|
|
48
|
+
function setAnonymousId(id) {
|
|
49
|
+
if (typeof window !== "undefined" && typeof localStorage !== "undefined") {
|
|
50
|
+
try {
|
|
51
|
+
localStorage.setItem(STORAGE_KEY, id);
|
|
52
|
+
} catch {
|
|
53
|
+
memoryId = id;
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
memoryId = id;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/flags/context.ts
|
|
61
|
+
function detectDeviceType(userAgent) {
|
|
62
|
+
const ua = userAgent.toLowerCase();
|
|
63
|
+
if (/ipad/.test(ua) || /android/.test(ua) && !/mobile/.test(ua) || /tablet/.test(ua)) {
|
|
64
|
+
return "tablet";
|
|
65
|
+
}
|
|
66
|
+
if (/mobile/.test(ua) || /iphone/.test(ua) || /ipod/.test(ua) || /android/.test(ua) || /blackberry/.test(ua) || /windows phone/.test(ua)) {
|
|
67
|
+
return "mobile";
|
|
68
|
+
}
|
|
69
|
+
return "desktop";
|
|
70
|
+
}
|
|
71
|
+
function detectBrowser(userAgent) {
|
|
72
|
+
const ua = userAgent.toLowerCase();
|
|
73
|
+
if (/edg/.test(ua)) return "edge";
|
|
74
|
+
if (/opr|opera/.test(ua)) return "opera";
|
|
75
|
+
if (/samsungbrowser/.test(ua)) return "samsung";
|
|
76
|
+
if (/chrome|chromium|crios/.test(ua)) return "chrome";
|
|
77
|
+
if (/safari/.test(ua) && !/chrome/.test(ua)) return "safari";
|
|
78
|
+
if (/firefox|fxios/.test(ua)) return "firefox";
|
|
79
|
+
if (/msie|trident/.test(ua)) return "ie";
|
|
80
|
+
return "other";
|
|
81
|
+
}
|
|
82
|
+
function detectOS(userAgent) {
|
|
83
|
+
const ua = userAgent.toLowerCase();
|
|
84
|
+
if (/iphone|ipad|ipod/.test(ua)) return "ios";
|
|
85
|
+
if (/android/.test(ua)) return "android";
|
|
86
|
+
if (/mac os|macos|macintosh/.test(ua)) return "macos";
|
|
87
|
+
if (/windows/.test(ua)) return "windows";
|
|
88
|
+
if (/linux/.test(ua)) return "linux";
|
|
89
|
+
return "other";
|
|
90
|
+
}
|
|
91
|
+
function getEvaluationContext() {
|
|
92
|
+
const userAgent = typeof navigator !== "undefined" ? navigator.userAgent : "";
|
|
93
|
+
return {
|
|
94
|
+
anonymous_id: getAnonymousId(),
|
|
95
|
+
device_type: userAgent ? detectDeviceType(userAgent) : void 0,
|
|
96
|
+
browser: userAgent ? detectBrowser(userAgent) : void 0,
|
|
97
|
+
os: userAgent ? detectOS(userAgent) : void 0,
|
|
98
|
+
user_agent: userAgent || void 0,
|
|
99
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/flags/evaluator.ts
|
|
104
|
+
function djb2Hash(str) {
|
|
105
|
+
let hash = 5381;
|
|
106
|
+
for (let i = 0; i < str.length; i++) {
|
|
107
|
+
hash = (hash << 5) + hash ^ str.charCodeAt(i);
|
|
108
|
+
}
|
|
109
|
+
return hash >>> 0;
|
|
110
|
+
}
|
|
111
|
+
function evaluatePercentageRollout(config) {
|
|
112
|
+
return Math.random() * 100 < config.percentage;
|
|
113
|
+
}
|
|
114
|
+
function evaluateUserbasePercentage(config, context, flagKey) {
|
|
115
|
+
const hashInput = `${context.anonymous_id}:${flagKey}`;
|
|
116
|
+
const hash = djb2Hash(hashInput);
|
|
117
|
+
const bucket = hash % 100;
|
|
118
|
+
return bucket < config.percentage;
|
|
119
|
+
}
|
|
120
|
+
function parseTime(timeStr) {
|
|
121
|
+
const [hours, minutes] = timeStr.split(":").map(Number);
|
|
122
|
+
return hours * 60 + minutes;
|
|
123
|
+
}
|
|
124
|
+
function evaluateTimeBased(config, context) {
|
|
125
|
+
const now = context.timestamp || /* @__PURE__ */ new Date();
|
|
126
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
127
|
+
timeZone: config.timezone,
|
|
128
|
+
weekday: "short",
|
|
129
|
+
hour: "2-digit",
|
|
130
|
+
minute: "2-digit",
|
|
131
|
+
hour12: false
|
|
132
|
+
});
|
|
133
|
+
const parts = formatter.formatToParts(now);
|
|
134
|
+
const dayOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(
|
|
135
|
+
parts.find((p) => p.type === "weekday")?.value || ""
|
|
136
|
+
);
|
|
137
|
+
const hour = parseInt(parts.find((p) => p.type === "hour")?.value || "0", 10);
|
|
138
|
+
const minute = parseInt(parts.find((p) => p.type === "minute")?.value || "0", 10);
|
|
139
|
+
const currentMinutes = hour * 60 + minute;
|
|
140
|
+
if (config.days_of_week && config.days_of_week.length > 0) {
|
|
141
|
+
if (!config.days_of_week.includes(dayOfWeek)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (config.start_time && config.end_time) {
|
|
146
|
+
const startMinutes = parseTime(config.start_time);
|
|
147
|
+
const endMinutes = parseTime(config.end_time);
|
|
148
|
+
if (startMinutes > endMinutes) {
|
|
149
|
+
if (currentMinutes < startMinutes && currentMinutes >= endMinutes) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
if (currentMinutes < startMinutes || currentMinutes >= endMinutes) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else if (config.start_time) {
|
|
158
|
+
const startMinutes = parseTime(config.start_time);
|
|
159
|
+
if (currentMinutes < startMinutes) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
} else if (config.end_time) {
|
|
163
|
+
const endMinutes = parseTime(config.end_time);
|
|
164
|
+
if (currentMinutes >= endMinutes) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
function evaluateDeviceType(config, context) {
|
|
171
|
+
if (!context.device_type) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
return config.types.includes(context.device_type);
|
|
175
|
+
}
|
|
176
|
+
function evaluateBrowser(config, context) {
|
|
177
|
+
if (!context.browser) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return config.browsers.includes(context.browser);
|
|
181
|
+
}
|
|
182
|
+
function evaluateOS(config, context) {
|
|
183
|
+
if (!context.os) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return config.operating_systems.includes(context.os);
|
|
187
|
+
}
|
|
188
|
+
function evaluateRule(ruleType, config, context, flagKey) {
|
|
189
|
+
switch (ruleType) {
|
|
190
|
+
case "percentage_rollout":
|
|
191
|
+
return evaluatePercentageRollout(config);
|
|
192
|
+
case "userbase_percentage":
|
|
193
|
+
return evaluateUserbasePercentage(
|
|
194
|
+
config,
|
|
195
|
+
context,
|
|
196
|
+
flagKey
|
|
197
|
+
);
|
|
198
|
+
case "time_based":
|
|
199
|
+
return evaluateTimeBased(config, context);
|
|
200
|
+
case "device_type":
|
|
201
|
+
return evaluateDeviceType(config, context);
|
|
202
|
+
case "browser":
|
|
203
|
+
return evaluateBrowser(config, context);
|
|
204
|
+
case "os":
|
|
205
|
+
return evaluateOS(config, context);
|
|
206
|
+
default:
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function evaluateFlag(flag, context) {
|
|
211
|
+
if (flag.override === "force_on") {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
if (flag.override === "force_off") {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (!flag.enabled) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
if (!flag.rules || flag.rules.length === 0) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
for (const rule of flag.rules) {
|
|
224
|
+
const result = evaluateRule(rule.rule_type, rule.config, context, flag.key);
|
|
225
|
+
if (!result) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
function evaluateVariant(flag, context) {
|
|
232
|
+
if (!evaluateFlag(flag, context)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
if (flag.flag_type !== "multivariate" || !flag.variants || flag.variants.length === 0) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const hashInput = `${context.anonymous_id}:${flag.key}:variant`;
|
|
239
|
+
const hash = djb2Hash(hashInput);
|
|
240
|
+
const totalWeight = flag.variants.reduce((sum, v) => sum + v.weight, 0);
|
|
241
|
+
const bucket = hash % totalWeight;
|
|
242
|
+
let cumulative = 0;
|
|
243
|
+
for (const variant of flag.variants) {
|
|
244
|
+
cumulative += variant.weight;
|
|
245
|
+
if (bucket < cumulative) {
|
|
246
|
+
return variant.key;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return flag.variants[0].key;
|
|
250
|
+
}
|
|
251
|
+
function getVariantPayload(flag, variantKey) {
|
|
252
|
+
if (flag.variant_payloads && flag.variant_payloads[variantKey] !== void 0) {
|
|
253
|
+
return flag.variant_payloads[variantKey];
|
|
254
|
+
}
|
|
255
|
+
const variant = flag.variants?.find((v) => v.key === variantKey);
|
|
256
|
+
return variant?.payload || null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/flags/manager.ts
|
|
260
|
+
var DEFAULT_ENDPOINT = "https://api.sitepong.com";
|
|
261
|
+
var FlagManager = class {
|
|
262
|
+
constructor(config) {
|
|
263
|
+
this.flags = /* @__PURE__ */ new Map();
|
|
264
|
+
this.evaluatedFlags = /* @__PURE__ */ new Map();
|
|
265
|
+
this.evaluatedVariants = /* @__PURE__ */ new Map();
|
|
266
|
+
this.context = null;
|
|
267
|
+
this.initialized = false;
|
|
268
|
+
this.initPromise = null;
|
|
269
|
+
this.config = {
|
|
270
|
+
endpoint: DEFAULT_ENDPOINT,
|
|
271
|
+
debug: false,
|
|
272
|
+
...config
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Initialize the flag manager by fetching flags
|
|
277
|
+
* Flags are evaluated once on init and cached
|
|
278
|
+
*/
|
|
279
|
+
async init() {
|
|
280
|
+
if (this.initialized) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (this.initPromise) {
|
|
284
|
+
return this.initPromise;
|
|
285
|
+
}
|
|
286
|
+
this.initPromise = this.fetchAndEvaluateFlags();
|
|
287
|
+
try {
|
|
288
|
+
await this.initPromise;
|
|
289
|
+
this.initialized = true;
|
|
290
|
+
} catch (err) {
|
|
291
|
+
this.log("Failed to initialize flags:", err);
|
|
292
|
+
this.initialized = true;
|
|
293
|
+
} finally {
|
|
294
|
+
this.initPromise = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Fetch flags from the API and evaluate them
|
|
299
|
+
*/
|
|
300
|
+
async fetchAndEvaluateFlags() {
|
|
301
|
+
try {
|
|
302
|
+
const response = await fetch(`${this.config.endpoint}/api/sdk/flags`, {
|
|
303
|
+
method: "GET",
|
|
304
|
+
headers: {
|
|
305
|
+
"Content-Type": "application/json",
|
|
306
|
+
"X-API-Key": this.config.apiKey
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (!response.ok) {
|
|
310
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
311
|
+
}
|
|
312
|
+
const data = await response.json();
|
|
313
|
+
this.log("Fetched flags:", data.flags.length);
|
|
314
|
+
this.context = getEvaluationContext();
|
|
315
|
+
this.log("Evaluation context:", this.context);
|
|
316
|
+
this.flags.clear();
|
|
317
|
+
this.evaluatedFlags.clear();
|
|
318
|
+
this.evaluatedVariants.clear();
|
|
319
|
+
for (const flag of data.flags) {
|
|
320
|
+
this.flags.set(flag.key, flag);
|
|
321
|
+
const result = evaluateFlag(flag, this.context);
|
|
322
|
+
this.evaluatedFlags.set(flag.key, result);
|
|
323
|
+
if (flag.flag_type === "multivariate" && flag.variants) {
|
|
324
|
+
const variant = evaluateVariant(flag, this.context);
|
|
325
|
+
this.evaluatedVariants.set(flag.key, variant);
|
|
326
|
+
this.log(`Flag "${flag.key}": ${result}, variant: ${variant}`);
|
|
327
|
+
} else {
|
|
328
|
+
this.log(`Flag "${flag.key}": ${result}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
} catch (err) {
|
|
332
|
+
this.log("Error fetching flags:", err);
|
|
333
|
+
throw err;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get a flag value
|
|
338
|
+
* Returns the cached evaluated result or default value
|
|
339
|
+
*/
|
|
340
|
+
getFlag(key, defaultValue = false) {
|
|
341
|
+
if (!this.initialized) {
|
|
342
|
+
this.log(`Flag "${key}" requested before init, returning default:`, defaultValue);
|
|
343
|
+
return defaultValue;
|
|
344
|
+
}
|
|
345
|
+
const value = this.evaluatedFlags.get(key);
|
|
346
|
+
if (value === void 0) {
|
|
347
|
+
this.log(`Flag "${key}" not found, returning default:`, defaultValue);
|
|
348
|
+
return defaultValue;
|
|
349
|
+
}
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Get a multivariate flag variant
|
|
354
|
+
* Returns the assigned variant key or the default value
|
|
355
|
+
*/
|
|
356
|
+
getVariant(key, defaultValue = null) {
|
|
357
|
+
if (!this.initialized) {
|
|
358
|
+
this.log(`Variant "${key}" requested before init, returning default:`, defaultValue);
|
|
359
|
+
return defaultValue;
|
|
360
|
+
}
|
|
361
|
+
const variant = this.evaluatedVariants.get(key);
|
|
362
|
+
if (variant === void 0) {
|
|
363
|
+
this.log(`Variant "${key}" not found, returning default:`, defaultValue);
|
|
364
|
+
return defaultValue;
|
|
365
|
+
}
|
|
366
|
+
return variant;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Get the payload for a multivariate flag's assigned variant
|
|
370
|
+
*/
|
|
371
|
+
getVariantPayload(key, defaultValue = null) {
|
|
372
|
+
const variant = this.getVariant(key);
|
|
373
|
+
if (!variant) return defaultValue;
|
|
374
|
+
const flag = this.flags.get(key);
|
|
375
|
+
if (!flag) return defaultValue;
|
|
376
|
+
const payload = getVariantPayload(flag, variant);
|
|
377
|
+
return payload ?? defaultValue;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get all flag values
|
|
381
|
+
*/
|
|
382
|
+
getAllFlags() {
|
|
383
|
+
const result = {};
|
|
384
|
+
for (const [key, value] of this.evaluatedFlags) {
|
|
385
|
+
result[key] = value;
|
|
386
|
+
}
|
|
387
|
+
return result;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Check if a flag exists
|
|
391
|
+
*/
|
|
392
|
+
hasFlag(key) {
|
|
393
|
+
return this.flags.has(key);
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Check if flags are initialized
|
|
397
|
+
*/
|
|
398
|
+
isInitialized() {
|
|
399
|
+
return this.initialized;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Wait for initialization to complete
|
|
403
|
+
*/
|
|
404
|
+
async waitForInit() {
|
|
405
|
+
if (this.initialized) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
await this.init();
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Force re-fetch and re-evaluate flags
|
|
412
|
+
* Call this if you need to refresh flags (e.g., on explicit user action)
|
|
413
|
+
*/
|
|
414
|
+
async refresh() {
|
|
415
|
+
this.initialized = false;
|
|
416
|
+
await this.init();
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get the current evaluation context
|
|
420
|
+
*/
|
|
421
|
+
getContext() {
|
|
422
|
+
return this.context;
|
|
423
|
+
}
|
|
424
|
+
log(...args) {
|
|
425
|
+
if (this.config.debug) {
|
|
426
|
+
console.log("[SitePong Flags]", ...args);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// src/analytics/session.ts
|
|
432
|
+
var SESSION_KEY = "sitepong_session_id";
|
|
433
|
+
var SESSION_TIMESTAMP_KEY = "sitepong_session_ts";
|
|
434
|
+
var SESSION_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
435
|
+
var memorySessionId = null;
|
|
436
|
+
var memorySessionTs = null;
|
|
437
|
+
function generateSessionId() {
|
|
438
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
439
|
+
return crypto.randomUUID();
|
|
440
|
+
}
|
|
441
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
442
|
+
const r = Math.random() * 16 | 0;
|
|
443
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
444
|
+
return v.toString(16);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function isSessionExpired(lastActivity) {
|
|
448
|
+
return Date.now() - lastActivity > SESSION_TIMEOUT_MS;
|
|
449
|
+
}
|
|
450
|
+
function getSessionId() {
|
|
451
|
+
if (typeof window !== "undefined" && typeof sessionStorage !== "undefined") {
|
|
452
|
+
try {
|
|
453
|
+
const existingId = sessionStorage.getItem(SESSION_KEY);
|
|
454
|
+
const lastTs = sessionStorage.getItem(SESSION_TIMESTAMP_KEY);
|
|
455
|
+
if (existingId && lastTs && !isSessionExpired(parseInt(lastTs, 10))) {
|
|
456
|
+
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, String(Date.now()));
|
|
457
|
+
return existingId;
|
|
458
|
+
}
|
|
459
|
+
const newId = generateSessionId();
|
|
460
|
+
sessionStorage.setItem(SESSION_KEY, newId);
|
|
461
|
+
sessionStorage.setItem(SESSION_TIMESTAMP_KEY, String(Date.now()));
|
|
462
|
+
return newId;
|
|
463
|
+
} catch {
|
|
464
|
+
return getMemorySession();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return getMemorySession();
|
|
468
|
+
}
|
|
469
|
+
function getMemorySession() {
|
|
470
|
+
if (memorySessionId && memorySessionTs && !isSessionExpired(memorySessionTs)) {
|
|
471
|
+
memorySessionTs = Date.now();
|
|
472
|
+
return memorySessionId;
|
|
473
|
+
}
|
|
474
|
+
memorySessionId = generateSessionId();
|
|
475
|
+
memorySessionTs = Date.now();
|
|
476
|
+
return memorySessionId;
|
|
477
|
+
}
|
|
478
|
+
function clearSession() {
|
|
479
|
+
if (typeof window !== "undefined" && typeof sessionStorage !== "undefined") {
|
|
480
|
+
try {
|
|
481
|
+
sessionStorage.removeItem(SESSION_KEY);
|
|
482
|
+
sessionStorage.removeItem(SESSION_TIMESTAMP_KEY);
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
memorySessionId = null;
|
|
487
|
+
memorySessionTs = null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/analytics/autocapture.ts
|
|
491
|
+
var DEFAULT_BLOCK_SELECTORS = [
|
|
492
|
+
"[data-sp-no-capture]",
|
|
493
|
+
".sp-no-capture"
|
|
494
|
+
];
|
|
495
|
+
var MAX_TEXT_LENGTH = 255;
|
|
496
|
+
var AutocaptureModule = class {
|
|
497
|
+
constructor(config, callback) {
|
|
498
|
+
this.clickHandler = null;
|
|
499
|
+
this.submitHandler = null;
|
|
500
|
+
this.active = false;
|
|
501
|
+
this.config = {
|
|
502
|
+
clicks: true,
|
|
503
|
+
forms: true,
|
|
504
|
+
pageviews: true,
|
|
505
|
+
blockSelectors: DEFAULT_BLOCK_SELECTORS,
|
|
506
|
+
maxTextLength: MAX_TEXT_LENGTH,
|
|
507
|
+
debug: false,
|
|
508
|
+
...config
|
|
509
|
+
};
|
|
510
|
+
this.callback = callback;
|
|
511
|
+
}
|
|
512
|
+
start() {
|
|
513
|
+
if (this.active || typeof window === "undefined") return;
|
|
514
|
+
this.active = true;
|
|
515
|
+
if (this.config.clicks) {
|
|
516
|
+
this.setupClickCapture();
|
|
517
|
+
}
|
|
518
|
+
if (this.config.forms) {
|
|
519
|
+
this.setupFormCapture();
|
|
520
|
+
}
|
|
521
|
+
this.log("Autocapture started");
|
|
522
|
+
}
|
|
523
|
+
stop() {
|
|
524
|
+
if (!this.active) return;
|
|
525
|
+
this.active = false;
|
|
526
|
+
if (this.clickHandler) {
|
|
527
|
+
document.removeEventListener("click", this.clickHandler, true);
|
|
528
|
+
this.clickHandler = null;
|
|
529
|
+
}
|
|
530
|
+
if (this.submitHandler) {
|
|
531
|
+
document.removeEventListener("submit", this.submitHandler, true);
|
|
532
|
+
this.submitHandler = null;
|
|
533
|
+
}
|
|
534
|
+
this.log("Autocapture stopped");
|
|
535
|
+
}
|
|
536
|
+
setupClickCapture() {
|
|
537
|
+
this.clickHandler = (e) => {
|
|
538
|
+
const target = e.target;
|
|
539
|
+
if (!target || !this.shouldCapture(target)) return;
|
|
540
|
+
const properties = this.getElementProperties(target);
|
|
541
|
+
properties.$event_type = "click";
|
|
542
|
+
properties.$x = e.clientX;
|
|
543
|
+
properties.$y = e.clientY;
|
|
544
|
+
this.callback({
|
|
545
|
+
type: "click",
|
|
546
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
547
|
+
properties
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
document.addEventListener("click", this.clickHandler, true);
|
|
551
|
+
}
|
|
552
|
+
setupFormCapture() {
|
|
553
|
+
this.submitHandler = (e) => {
|
|
554
|
+
const form = e.target;
|
|
555
|
+
if (!form || !this.shouldCapture(form)) return;
|
|
556
|
+
const properties = {
|
|
557
|
+
$event_type: "form_submit",
|
|
558
|
+
$form_action: form.action || void 0,
|
|
559
|
+
$form_method: form.method || "get",
|
|
560
|
+
$form_name: form.name || form.id || void 0,
|
|
561
|
+
$field_names: this.getFormFieldNames(form),
|
|
562
|
+
$field_count: form.elements.length
|
|
563
|
+
};
|
|
564
|
+
const elemProps = this.getElementProperties(form);
|
|
565
|
+
Object.assign(properties, elemProps);
|
|
566
|
+
this.callback({
|
|
567
|
+
type: "form_submit",
|
|
568
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
569
|
+
properties
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
document.addEventListener("submit", this.submitHandler, true);
|
|
573
|
+
}
|
|
574
|
+
shouldCapture(element) {
|
|
575
|
+
const blockSelectors = this.config.blockSelectors || DEFAULT_BLOCK_SELECTORS;
|
|
576
|
+
for (const selector of blockSelectors) {
|
|
577
|
+
if (element.matches(selector) || element.closest(selector)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (this.config.allowSelectors && this.config.allowSelectors.length > 0) {
|
|
582
|
+
const matches = this.config.allowSelectors.some(
|
|
583
|
+
(sel) => element.matches(sel) || element.closest(sel) !== null
|
|
584
|
+
);
|
|
585
|
+
if (!matches) return false;
|
|
586
|
+
}
|
|
587
|
+
if (element.offsetParent === null && element.tagName !== "BODY") {
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
getElementProperties(element) {
|
|
593
|
+
const properties = {};
|
|
594
|
+
properties.$tag_name = element.tagName.toLowerCase();
|
|
595
|
+
properties.$el_id = element.id || void 0;
|
|
596
|
+
if (element.classList.length > 0) {
|
|
597
|
+
properties.$el_classes = Array.from(element.classList).slice(0, 5);
|
|
598
|
+
}
|
|
599
|
+
const text = this.getElementText(element);
|
|
600
|
+
if (text) {
|
|
601
|
+
properties.$el_text = text;
|
|
602
|
+
}
|
|
603
|
+
if (element instanceof HTMLAnchorElement) {
|
|
604
|
+
properties.$href = element.href || void 0;
|
|
605
|
+
properties.$target = element.target || void 0;
|
|
606
|
+
}
|
|
607
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLButtonElement) {
|
|
608
|
+
properties.$el_type = element.type || void 0;
|
|
609
|
+
properties.$el_name = element.name || void 0;
|
|
610
|
+
}
|
|
611
|
+
const ariaLabel = element.getAttribute("aria-label");
|
|
612
|
+
if (ariaLabel) {
|
|
613
|
+
properties.$aria_label = this.truncate(ariaLabel);
|
|
614
|
+
}
|
|
615
|
+
const role = element.getAttribute("role");
|
|
616
|
+
if (role) {
|
|
617
|
+
properties.$el_role = role;
|
|
618
|
+
}
|
|
619
|
+
properties.$selector = this.getSelector(element);
|
|
620
|
+
properties.$current_url = window.location.href;
|
|
621
|
+
properties.$pathname = window.location.pathname;
|
|
622
|
+
return properties;
|
|
623
|
+
}
|
|
624
|
+
getElementText(element) {
|
|
625
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
|
|
626
|
+
return void 0;
|
|
627
|
+
}
|
|
628
|
+
let text = "";
|
|
629
|
+
if (element instanceof HTMLButtonElement || element instanceof HTMLAnchorElement) {
|
|
630
|
+
text = element.innerText || element.textContent || "";
|
|
631
|
+
} else {
|
|
632
|
+
for (const node of Array.from(element.childNodes)) {
|
|
633
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
634
|
+
text += node.textContent || "";
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
text = text.trim().replace(/\s+/g, " ");
|
|
639
|
+
return text ? this.truncate(text) : void 0;
|
|
640
|
+
}
|
|
641
|
+
getFormFieldNames(form) {
|
|
642
|
+
const names = [];
|
|
643
|
+
for (const element of Array.from(form.elements)) {
|
|
644
|
+
const input = element;
|
|
645
|
+
if (input.name && input.type !== "password" && input.type !== "hidden") {
|
|
646
|
+
names.push(input.name);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return names.slice(0, 20);
|
|
650
|
+
}
|
|
651
|
+
getSelector(element) {
|
|
652
|
+
const parts = [];
|
|
653
|
+
let current = element;
|
|
654
|
+
let depth = 0;
|
|
655
|
+
while (current && current !== document.body && depth < 5) {
|
|
656
|
+
let selector = current.tagName.toLowerCase();
|
|
657
|
+
if (current.id) {
|
|
658
|
+
selector = `#${current.id}`;
|
|
659
|
+
parts.unshift(selector);
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
if (current.classList.length > 0) {
|
|
663
|
+
selector += `.${Array.from(current.classList).slice(0, 2).join(".")}`;
|
|
664
|
+
}
|
|
665
|
+
const parent = current.parentElement;
|
|
666
|
+
if (parent) {
|
|
667
|
+
const siblings = Array.from(parent.children).filter(
|
|
668
|
+
(c) => c.tagName === current.tagName
|
|
669
|
+
);
|
|
670
|
+
if (siblings.length > 1) {
|
|
671
|
+
const index = siblings.indexOf(current) + 1;
|
|
672
|
+
selector += `:nth-of-type(${index})`;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
parts.unshift(selector);
|
|
676
|
+
current = current.parentElement;
|
|
677
|
+
depth++;
|
|
678
|
+
}
|
|
679
|
+
return parts.join(" > ");
|
|
680
|
+
}
|
|
681
|
+
truncate(text) {
|
|
682
|
+
const max = this.config.maxTextLength || MAX_TEXT_LENGTH;
|
|
683
|
+
return text.length > max ? text.slice(0, max) + "..." : text;
|
|
684
|
+
}
|
|
685
|
+
log(...args) {
|
|
686
|
+
if (this.config.debug) {
|
|
687
|
+
console.log("[SitePong Autocapture]", ...args);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/analytics/manager.ts
|
|
693
|
+
var DEFAULT_ENDPOINT2 = "https://ingest.sitepong.com";
|
|
694
|
+
var DEFAULT_BATCH_SIZE = 20;
|
|
695
|
+
var DEFAULT_FLUSH_INTERVAL = 1e4;
|
|
696
|
+
var AnalyticsManager = class {
|
|
697
|
+
constructor(config) {
|
|
698
|
+
this.eventQueue = [];
|
|
699
|
+
this.flushTimer = null;
|
|
700
|
+
this.userId = null;
|
|
701
|
+
this.groupId = null;
|
|
702
|
+
this.userTraits = null;
|
|
703
|
+
this.groupTraits = null;
|
|
704
|
+
this.lastUrl = null;
|
|
705
|
+
this.popstateHandler = null;
|
|
706
|
+
this.autocaptureModule = null;
|
|
707
|
+
this.config = {
|
|
708
|
+
endpoint: DEFAULT_ENDPOINT2,
|
|
709
|
+
enabled: true,
|
|
710
|
+
autocapturePageviews: false,
|
|
711
|
+
maxBatchSize: DEFAULT_BATCH_SIZE,
|
|
712
|
+
flushInterval: DEFAULT_FLUSH_INTERVAL,
|
|
713
|
+
debug: false,
|
|
714
|
+
...config
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
init() {
|
|
718
|
+
if (!this.config.enabled) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
this.startFlushTimer();
|
|
722
|
+
this.setupPageHideListener();
|
|
723
|
+
if (this.config.autocapturePageviews) {
|
|
724
|
+
this.setupSPATracking();
|
|
725
|
+
}
|
|
726
|
+
this.setupAutocapture();
|
|
727
|
+
this.log("Analytics initialized");
|
|
728
|
+
}
|
|
729
|
+
setupAutocapture() {
|
|
730
|
+
const ac = this.config.autocapture;
|
|
731
|
+
const clicksEnabled = this.config.autocaptureClicks;
|
|
732
|
+
const formsEnabled = this.config.autocaptureForms;
|
|
733
|
+
let captureClicks = clicksEnabled || false;
|
|
734
|
+
let captureForms = formsEnabled || false;
|
|
735
|
+
let blockSelectors;
|
|
736
|
+
let allowSelectors;
|
|
737
|
+
if (ac === true) {
|
|
738
|
+
captureClicks = true;
|
|
739
|
+
captureForms = true;
|
|
740
|
+
} else if (ac && typeof ac === "object") {
|
|
741
|
+
captureClicks = ac.clicks !== false;
|
|
742
|
+
captureForms = ac.forms !== false;
|
|
743
|
+
blockSelectors = ac.blockSelectors;
|
|
744
|
+
allowSelectors = ac.allowSelectors;
|
|
745
|
+
if (ac.pageviews !== false && !this.config.autocapturePageviews) {
|
|
746
|
+
this.setupSPATracking();
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
if (!captureClicks && !captureForms) return;
|
|
750
|
+
this.autocaptureModule = new AutocaptureModule(
|
|
751
|
+
{
|
|
752
|
+
clicks: captureClicks,
|
|
753
|
+
forms: captureForms,
|
|
754
|
+
pageviews: false,
|
|
755
|
+
// Handled by SPA tracking above
|
|
756
|
+
blockSelectors,
|
|
757
|
+
allowSelectors,
|
|
758
|
+
debug: this.config.debug
|
|
759
|
+
},
|
|
760
|
+
(event) => {
|
|
761
|
+
const name = event.type === "click" ? "$autocapture_click" : event.type === "form_submit" ? "$autocapture_form_submit" : "$autocapture";
|
|
762
|
+
this.track(name, event.properties);
|
|
763
|
+
}
|
|
764
|
+
);
|
|
765
|
+
this.autocaptureModule.start();
|
|
766
|
+
}
|
|
767
|
+
track(eventName, properties) {
|
|
768
|
+
if (!this.config.enabled) return;
|
|
769
|
+
const event = this.createEvent("track", { name: eventName, properties });
|
|
770
|
+
this.enqueue(event);
|
|
771
|
+
this.log("Track:", eventName, properties);
|
|
772
|
+
}
|
|
773
|
+
trackPageView(url, properties) {
|
|
774
|
+
if (!this.config.enabled) return;
|
|
775
|
+
const pageUrl = url || (typeof window !== "undefined" ? window.location.href : void 0);
|
|
776
|
+
const event = this.createEvent("page", {
|
|
777
|
+
properties: {
|
|
778
|
+
url: pageUrl,
|
|
779
|
+
referrer: typeof document !== "undefined" ? document.referrer : void 0,
|
|
780
|
+
title: typeof document !== "undefined" ? document.title : void 0,
|
|
781
|
+
...properties
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
this.enqueue(event);
|
|
785
|
+
this.log("Page view:", pageUrl);
|
|
786
|
+
}
|
|
787
|
+
identify(userId, traits) {
|
|
788
|
+
if (!this.config.enabled) return;
|
|
789
|
+
this.userId = userId;
|
|
790
|
+
if (traits) {
|
|
791
|
+
this.userTraits = { ...this.userTraits, ...traits };
|
|
792
|
+
}
|
|
793
|
+
const event = this.createEvent("identify", { traits: this.userTraits || void 0 });
|
|
794
|
+
this.enqueue(event);
|
|
795
|
+
this.log("Identify:", userId, traits);
|
|
796
|
+
}
|
|
797
|
+
group(groupId, traits) {
|
|
798
|
+
if (!this.config.enabled) return;
|
|
799
|
+
this.groupId = groupId;
|
|
800
|
+
if (traits) {
|
|
801
|
+
this.groupTraits = { ...this.groupTraits, ...traits };
|
|
802
|
+
}
|
|
803
|
+
const event = this.createEvent("group", { traits: this.groupTraits || void 0 });
|
|
804
|
+
this.enqueue(event);
|
|
805
|
+
this.log("Group:", groupId, traits);
|
|
806
|
+
}
|
|
807
|
+
reset() {
|
|
808
|
+
this.userId = null;
|
|
809
|
+
this.groupId = null;
|
|
810
|
+
this.userTraits = null;
|
|
811
|
+
this.groupTraits = null;
|
|
812
|
+
clearSession();
|
|
813
|
+
this.log("Analytics state reset");
|
|
814
|
+
}
|
|
815
|
+
async flush() {
|
|
816
|
+
if (this.eventQueue.length === 0) return;
|
|
817
|
+
const events = [...this.eventQueue];
|
|
818
|
+
this.eventQueue = [];
|
|
819
|
+
const endpoint = this.config.eventsEndpoint || `${this.config.endpoint}/api/events`;
|
|
820
|
+
try {
|
|
821
|
+
const response = await fetch(endpoint, {
|
|
822
|
+
method: "POST",
|
|
823
|
+
headers: {
|
|
824
|
+
"Content-Type": "application/json",
|
|
825
|
+
"X-API-Key": this.config.apiKey
|
|
826
|
+
},
|
|
827
|
+
body: JSON.stringify({ events })
|
|
828
|
+
});
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
throw new Error(`HTTP ${response.status}`);
|
|
831
|
+
}
|
|
832
|
+
this.log(`Flushed ${events.length} events`);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
this.eventQueue.unshift(...events);
|
|
835
|
+
this.log("Failed to flush events:", err);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
destroy() {
|
|
839
|
+
if (this.flushTimer) {
|
|
840
|
+
clearInterval(this.flushTimer);
|
|
841
|
+
this.flushTimer = null;
|
|
842
|
+
}
|
|
843
|
+
if (this.popstateHandler && typeof window !== "undefined") {
|
|
844
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
845
|
+
this.popstateHandler = null;
|
|
846
|
+
}
|
|
847
|
+
if (this.autocaptureModule) {
|
|
848
|
+
this.autocaptureModule.stop();
|
|
849
|
+
this.autocaptureModule = null;
|
|
850
|
+
}
|
|
851
|
+
this.flushWithBeacon();
|
|
852
|
+
}
|
|
853
|
+
createEvent(type, options) {
|
|
854
|
+
return {
|
|
855
|
+
type,
|
|
856
|
+
name: options.name,
|
|
857
|
+
properties: options.properties,
|
|
858
|
+
userId: this.userId || void 0,
|
|
859
|
+
anonymousId: getAnonymousId(),
|
|
860
|
+
groupId: this.groupId || void 0,
|
|
861
|
+
traits: options.traits,
|
|
862
|
+
sessionId: getSessionId(),
|
|
863
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
864
|
+
url: typeof window !== "undefined" ? window.location.href : void 0,
|
|
865
|
+
referrer: typeof document !== "undefined" ? document.referrer : void 0,
|
|
866
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
enqueue(event) {
|
|
870
|
+
this.eventQueue.push(event);
|
|
871
|
+
if (this.eventQueue.length >= this.config.maxBatchSize) {
|
|
872
|
+
this.flush();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
startFlushTimer() {
|
|
876
|
+
if (this.flushTimer) {
|
|
877
|
+
clearInterval(this.flushTimer);
|
|
878
|
+
}
|
|
879
|
+
this.flushTimer = setInterval(() => {
|
|
880
|
+
this.flush();
|
|
881
|
+
}, this.config.flushInterval);
|
|
882
|
+
}
|
|
883
|
+
setupPageHideListener() {
|
|
884
|
+
if (typeof window === "undefined") return;
|
|
885
|
+
window.addEventListener("visibilitychange", () => {
|
|
886
|
+
if (document.visibilityState === "hidden") {
|
|
887
|
+
this.flushWithBeacon();
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
flushWithBeacon() {
|
|
892
|
+
if (this.eventQueue.length === 0 || typeof navigator?.sendBeacon !== "function") {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const events = [...this.eventQueue];
|
|
896
|
+
this.eventQueue = [];
|
|
897
|
+
const endpoint = this.config.eventsEndpoint || `${this.config.endpoint}/api/events`;
|
|
898
|
+
const blob = new Blob(
|
|
899
|
+
[JSON.stringify({ events, apiKey: this.config.apiKey })],
|
|
900
|
+
{ type: "application/json" }
|
|
901
|
+
);
|
|
902
|
+
navigator.sendBeacon(endpoint, blob);
|
|
903
|
+
this.log(`Flushed ${events.length} events via beacon`);
|
|
904
|
+
}
|
|
905
|
+
setupSPATracking() {
|
|
906
|
+
if (typeof window === "undefined") return;
|
|
907
|
+
this.lastUrl = window.location.href;
|
|
908
|
+
this.trackPageView();
|
|
909
|
+
this.popstateHandler = () => {
|
|
910
|
+
const currentUrl = window.location.href;
|
|
911
|
+
if (currentUrl !== this.lastUrl) {
|
|
912
|
+
this.lastUrl = currentUrl;
|
|
913
|
+
this.trackPageView(currentUrl);
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
917
|
+
const originalPushState = history.pushState.bind(history);
|
|
918
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
919
|
+
history.pushState = (...args) => {
|
|
920
|
+
originalPushState(...args);
|
|
921
|
+
this.handleUrlChange();
|
|
922
|
+
};
|
|
923
|
+
history.replaceState = (...args) => {
|
|
924
|
+
originalReplaceState(...args);
|
|
925
|
+
this.handleUrlChange();
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
handleUrlChange() {
|
|
929
|
+
if (typeof window === "undefined") return;
|
|
930
|
+
const currentUrl = window.location.href;
|
|
931
|
+
if (currentUrl !== this.lastUrl) {
|
|
932
|
+
this.lastUrl = currentUrl;
|
|
933
|
+
this.trackPageView(currentUrl);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
log(...args) {
|
|
937
|
+
if (this.config.debug) {
|
|
938
|
+
console.log("[SitePong Analytics]", ...args);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
// src/fingerprint/tampering.ts
|
|
944
|
+
function detectTampering() {
|
|
945
|
+
const signals = {
|
|
946
|
+
uaSpoofed: false,
|
|
947
|
+
antiDetectBrowser: false,
|
|
948
|
+
canvasNoise: false,
|
|
949
|
+
canvasBlocked: false,
|
|
950
|
+
webglSpoofed: false,
|
|
951
|
+
webglBlocked: false,
|
|
952
|
+
vmIndicators: [],
|
|
953
|
+
platformMismatch: false,
|
|
954
|
+
pluginAnomaly: false,
|
|
955
|
+
featureAnomaly: false,
|
|
956
|
+
geolocationSpoofed: false,
|
|
957
|
+
mitmIndicators: []
|
|
958
|
+
};
|
|
959
|
+
let confidence = 0;
|
|
960
|
+
const browserResult = detectBrowserTampering(signals);
|
|
961
|
+
confidence += browserResult;
|
|
962
|
+
const canvasResult = detectCanvasTampering(signals);
|
|
963
|
+
confidence += canvasResult;
|
|
964
|
+
const webglResult = detectWebGLTampering(signals);
|
|
965
|
+
confidence += webglResult;
|
|
966
|
+
const vmResult = detectVM(signals);
|
|
967
|
+
confidence += vmResult;
|
|
968
|
+
const geoResult = detectGeolocationSpoofing(signals);
|
|
969
|
+
confidence += geoResult;
|
|
970
|
+
const mitmResult = detectMitM(signals);
|
|
971
|
+
confidence += mitmResult;
|
|
972
|
+
confidence = Math.min(confidence, 1);
|
|
973
|
+
return {
|
|
974
|
+
browserTampered: signals.uaSpoofed || signals.antiDetectBrowser || signals.platformMismatch,
|
|
975
|
+
canvasTampered: signals.canvasNoise || signals.canvasBlocked,
|
|
976
|
+
webglTampered: signals.webglSpoofed || signals.webglBlocked,
|
|
977
|
+
vmDetected: signals.vmIndicators.length >= 2,
|
|
978
|
+
geolocationSpoofed: signals.geolocationSpoofed,
|
|
979
|
+
mitmDetected: signals.mitmIndicators.length >= 1,
|
|
980
|
+
confidence,
|
|
981
|
+
signals
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
function detectBrowserTampering(signals) {
|
|
985
|
+
if (typeof navigator === "undefined" || typeof window === "undefined") return 0;
|
|
986
|
+
let score = 0;
|
|
987
|
+
const ua = navigator.userAgent;
|
|
988
|
+
const platform = navigator.platform || "";
|
|
989
|
+
const isWindowsUA = /Windows/.test(ua);
|
|
990
|
+
const isMacUA = /Mac/.test(ua);
|
|
991
|
+
const isLinuxUA = /Linux/.test(ua) && !/Android/.test(ua);
|
|
992
|
+
const isWindowsPlatform = /Win/.test(platform);
|
|
993
|
+
const isMacPlatform = /Mac/.test(platform);
|
|
994
|
+
const isLinuxPlatform = /Linux/.test(platform);
|
|
995
|
+
if (isWindowsUA && !isWindowsPlatform || isMacUA && !isMacPlatform || isLinuxUA && !isLinuxPlatform) {
|
|
996
|
+
signals.platformMismatch = true;
|
|
997
|
+
signals.uaSpoofed = true;
|
|
998
|
+
score += 0.3;
|
|
999
|
+
}
|
|
1000
|
+
const claimsChrome = /Chrome\/\d+/.test(ua) && !/Edge|OPR|Brave/.test(ua);
|
|
1001
|
+
if (claimsChrome) {
|
|
1002
|
+
const hasChromiumFeatures = "chrome" in window || "CSS" in window && "paintWorklet" in window.CSS;
|
|
1003
|
+
if (!hasChromiumFeatures && !("brave" in navigator)) {
|
|
1004
|
+
signals.uaSpoofed = true;
|
|
1005
|
+
score += 0.2;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const claimsFirefox = /Firefox\/\d+/.test(ua) && !/Seamonkey/.test(ua);
|
|
1009
|
+
if (claimsFirefox) {
|
|
1010
|
+
const hasFirefoxFeatures = "InstallTrigger" in window || CSS.supports("-moz-appearance", "none");
|
|
1011
|
+
if (!hasFirefoxFeatures) {
|
|
1012
|
+
signals.uaSpoofed = true;
|
|
1013
|
+
score += 0.2;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const nav = navigator;
|
|
1017
|
+
if (nav.userAgentData?.brands) {
|
|
1018
|
+
const brands = nav.userAgentData.brands.map((b) => b.brand);
|
|
1019
|
+
if (brands.length === 0 || brands.every((b) => b === "" || b === "Not A;Brand")) {
|
|
1020
|
+
signals.antiDetectBrowser = true;
|
|
1021
|
+
score += 0.25;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (!/mobile|android|iphone|ipad/i.test(ua)) {
|
|
1025
|
+
const pluginCount = navigator.plugins?.length || 0;
|
|
1026
|
+
if (pluginCount === 0 && claimsChrome) {
|
|
1027
|
+
signals.pluginAnomaly = true;
|
|
1028
|
+
score += 0.1;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const cores = navigator.hardwareConcurrency || 0;
|
|
1032
|
+
const memory = navigator.deviceMemory || 0;
|
|
1033
|
+
if (cores > 0 && memory > 0) {
|
|
1034
|
+
if (cores >= 16 && memory <= 1) {
|
|
1035
|
+
signals.featureAnomaly = true;
|
|
1036
|
+
score += 0.15;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
return Math.min(score, 0.5);
|
|
1040
|
+
}
|
|
1041
|
+
function detectCanvasTampering(signals) {
|
|
1042
|
+
if (typeof document === "undefined") return 0;
|
|
1043
|
+
let score = 0;
|
|
1044
|
+
try {
|
|
1045
|
+
const canvas = document.createElement("canvas");
|
|
1046
|
+
canvas.width = 100;
|
|
1047
|
+
canvas.height = 20;
|
|
1048
|
+
const ctx = canvas.getContext("2d");
|
|
1049
|
+
if (!ctx) {
|
|
1050
|
+
signals.canvasBlocked = true;
|
|
1051
|
+
return 0.1;
|
|
1052
|
+
}
|
|
1053
|
+
ctx.fillStyle = "#ff0000";
|
|
1054
|
+
ctx.fillRect(0, 0, 100, 20);
|
|
1055
|
+
ctx.fillStyle = "#00ff00";
|
|
1056
|
+
ctx.font = "12px Arial";
|
|
1057
|
+
ctx.fillText("SitePong", 5, 14);
|
|
1058
|
+
const draw1 = canvas.toDataURL();
|
|
1059
|
+
ctx.clearRect(0, 0, 100, 20);
|
|
1060
|
+
ctx.fillStyle = "#ff0000";
|
|
1061
|
+
ctx.fillRect(0, 0, 100, 20);
|
|
1062
|
+
ctx.fillStyle = "#00ff00";
|
|
1063
|
+
ctx.font = "12px Arial";
|
|
1064
|
+
ctx.fillText("SitePong", 5, 14);
|
|
1065
|
+
const draw2 = canvas.toDataURL();
|
|
1066
|
+
if (draw1 !== draw2) {
|
|
1067
|
+
signals.canvasNoise = true;
|
|
1068
|
+
score += 0.35;
|
|
1069
|
+
}
|
|
1070
|
+
const imageData = ctx.getImageData(0, 0, 100, 20);
|
|
1071
|
+
const pixels = imageData.data;
|
|
1072
|
+
let allZero = true;
|
|
1073
|
+
let allSame = true;
|
|
1074
|
+
const firstPixel = pixels[0];
|
|
1075
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
1076
|
+
if (pixels[i] !== 0 || pixels[i + 1] !== 0 || pixels[i + 2] !== 0) {
|
|
1077
|
+
allZero = false;
|
|
1078
|
+
}
|
|
1079
|
+
if (pixels[i] !== firstPixel) {
|
|
1080
|
+
allSame = false;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (allZero || allSame) {
|
|
1084
|
+
signals.canvasBlocked = true;
|
|
1085
|
+
score += 0.2;
|
|
1086
|
+
}
|
|
1087
|
+
const toDataURLDesc = Object.getOwnPropertyDescriptor(
|
|
1088
|
+
HTMLCanvasElement.prototype,
|
|
1089
|
+
"toDataURL"
|
|
1090
|
+
);
|
|
1091
|
+
if (toDataURLDesc && toDataURLDesc.value !== HTMLCanvasElement.prototype.toDataURL) {
|
|
1092
|
+
signals.canvasNoise = true;
|
|
1093
|
+
score += 0.3;
|
|
1094
|
+
}
|
|
1095
|
+
} catch {
|
|
1096
|
+
signals.canvasBlocked = true;
|
|
1097
|
+
score += 0.1;
|
|
1098
|
+
}
|
|
1099
|
+
return Math.min(score, 0.4);
|
|
1100
|
+
}
|
|
1101
|
+
function detectWebGLTampering(signals) {
|
|
1102
|
+
if (typeof document === "undefined") return 0;
|
|
1103
|
+
let score = 0;
|
|
1104
|
+
try {
|
|
1105
|
+
const canvas = document.createElement("canvas");
|
|
1106
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1107
|
+
if (!gl || !(gl instanceof WebGLRenderingContext)) {
|
|
1108
|
+
signals.webglBlocked = true;
|
|
1109
|
+
return 0.1;
|
|
1110
|
+
}
|
|
1111
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1112
|
+
if (!debugInfo) {
|
|
1113
|
+
signals.webglBlocked = true;
|
|
1114
|
+
return 0.1;
|
|
1115
|
+
}
|
|
1116
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
1117
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
1118
|
+
const spoofedVendors = ["Brian Paul", "Mesa", "VMware"];
|
|
1119
|
+
const spoofedRenderers = ["llvmpipe", "softpipe", "Mesa", "SwiftShader"];
|
|
1120
|
+
if (spoofedRenderers.some((s) => renderer.includes(s)) && !spoofedVendors.some((v) => vendor.includes(v))) {
|
|
1121
|
+
signals.webglSpoofed = true;
|
|
1122
|
+
score += 0.25;
|
|
1123
|
+
}
|
|
1124
|
+
const ua = navigator.userAgent;
|
|
1125
|
+
const isMobile = /mobile|android|iphone|ipad/i.test(ua);
|
|
1126
|
+
const isDesktopGPU = /GeForce|Radeon|Intel.*HD|GTX|RTX|Vega/i.test(renderer);
|
|
1127
|
+
if (isMobile && isDesktopGPU) {
|
|
1128
|
+
signals.webglSpoofed = true;
|
|
1129
|
+
score += 0.3;
|
|
1130
|
+
}
|
|
1131
|
+
const getParamDesc = Object.getOwnPropertyDescriptor(
|
|
1132
|
+
WebGLRenderingContext.prototype,
|
|
1133
|
+
"getParameter"
|
|
1134
|
+
);
|
|
1135
|
+
if (getParamDesc && typeof getParamDesc.value === "function") {
|
|
1136
|
+
const fnStr = getParamDesc.value.toString();
|
|
1137
|
+
if (!fnStr.includes("[native code]")) {
|
|
1138
|
+
signals.webglSpoofed = true;
|
|
1139
|
+
score += 0.35;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const extensions = gl.getSupportedExtensions() || [];
|
|
1143
|
+
if (extensions.length < 5 && isDesktopGPU) {
|
|
1144
|
+
signals.webglSpoofed = true;
|
|
1145
|
+
score += 0.1;
|
|
1146
|
+
}
|
|
1147
|
+
} catch {
|
|
1148
|
+
signals.webglBlocked = true;
|
|
1149
|
+
score += 0.1;
|
|
1150
|
+
}
|
|
1151
|
+
return Math.min(score, 0.4);
|
|
1152
|
+
}
|
|
1153
|
+
function detectVM(signals) {
|
|
1154
|
+
if (typeof navigator === "undefined" || typeof window === "undefined") return 0;
|
|
1155
|
+
const indicators = [];
|
|
1156
|
+
const ua = navigator.userAgent;
|
|
1157
|
+
try {
|
|
1158
|
+
const canvas = document.createElement("canvas");
|
|
1159
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1160
|
+
if (gl && gl instanceof WebGLRenderingContext) {
|
|
1161
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1162
|
+
if (debugInfo) {
|
|
1163
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL).toLowerCase();
|
|
1164
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL).toLowerCase();
|
|
1165
|
+
const vmRenderers = ["vmware", "virtualbox", "hyper-v", "parallels", "qemu", "llvmpipe", "mesa"];
|
|
1166
|
+
const vmVendors = ["vmware", "innotek", "microsoft basic", "parallels"];
|
|
1167
|
+
for (const vm of vmRenderers) {
|
|
1168
|
+
if (renderer.includes(vm)) {
|
|
1169
|
+
indicators.push(`webgl_renderer:${vm}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
for (const vm of vmVendors) {
|
|
1173
|
+
if (vendor.includes(vm)) {
|
|
1174
|
+
indicators.push(`webgl_vendor:${vm}`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
if (typeof screen !== "undefined") {
|
|
1182
|
+
const w = screen.width;
|
|
1183
|
+
const h = screen.height;
|
|
1184
|
+
const vmResolutions = [
|
|
1185
|
+
[800, 600],
|
|
1186
|
+
[1024, 768],
|
|
1187
|
+
[1280, 800]
|
|
1188
|
+
];
|
|
1189
|
+
if (vmResolutions.some(([vw, vh]) => w === vw && h === vh) && screen.colorDepth === 24 && navigator.hardwareConcurrency <= 2) {
|
|
1190
|
+
indicators.push("vm_resolution");
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const cores = navigator.hardwareConcurrency || 0;
|
|
1194
|
+
const memory = navigator.deviceMemory || 0;
|
|
1195
|
+
if (cores > 0 && cores <= 2 && memory > 0 && memory <= 2) {
|
|
1196
|
+
if (!/mobile|android|iphone/i.test(ua)) {
|
|
1197
|
+
indicators.push("low_hw_specs");
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (typeof performance !== "undefined") {
|
|
1201
|
+
const times = [];
|
|
1202
|
+
for (let i = 0; i < 10; i++) {
|
|
1203
|
+
times.push(performance.now());
|
|
1204
|
+
}
|
|
1205
|
+
const uniqueTimes = new Set(times.map((t) => Math.floor(t * 1e3)));
|
|
1206
|
+
if (uniqueTimes.size <= 3 && times.length === 10) {
|
|
1207
|
+
indicators.push("coarse_timing");
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
if (typeof navigator !== "undefined") {
|
|
1211
|
+
const connection = navigator.connection;
|
|
1212
|
+
if (connection) {
|
|
1213
|
+
if (connection.downlink && connection.downlink >= 10 && connection.rtt && connection.rtt <= 5) {
|
|
1214
|
+
indicators.push("cloud_vm_network");
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
signals.vmIndicators = indicators;
|
|
1219
|
+
const vmScore = Math.min(indicators.length * 0.15, 0.5);
|
|
1220
|
+
return vmScore;
|
|
1221
|
+
}
|
|
1222
|
+
function detectGeolocationSpoofing(signals) {
|
|
1223
|
+
if (typeof navigator === "undefined") return 0;
|
|
1224
|
+
let score = 0;
|
|
1225
|
+
if ("geolocation" in navigator) {
|
|
1226
|
+
const geoDesc = Object.getOwnPropertyDescriptor(navigator, "geolocation");
|
|
1227
|
+
if (geoDesc && geoDesc.configurable === false && geoDesc.get) {
|
|
1228
|
+
const fnStr = geoDesc.get.toString();
|
|
1229
|
+
if (!fnStr.includes("[native code]")) {
|
|
1230
|
+
signals.geolocationSpoofed = true;
|
|
1231
|
+
score += 0.3;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
try {
|
|
1235
|
+
const getCurrentPositionDesc = Object.getOwnPropertyDescriptor(
|
|
1236
|
+
Geolocation.prototype,
|
|
1237
|
+
"getCurrentPosition"
|
|
1238
|
+
);
|
|
1239
|
+
if (getCurrentPositionDesc?.value) {
|
|
1240
|
+
const fnStr = getCurrentPositionDesc.value.toString();
|
|
1241
|
+
if (!fnStr.includes("[native code]")) {
|
|
1242
|
+
signals.geolocationSpoofed = true;
|
|
1243
|
+
score += 0.3;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (typeof window !== "undefined") {
|
|
1250
|
+
try {
|
|
1251
|
+
const navProto = Object.getPrototypeOf(navigator);
|
|
1252
|
+
const geoGetter = Object.getOwnPropertyDescriptor(navProto, "geolocation");
|
|
1253
|
+
if (geoGetter && geoGetter.get && !geoGetter.get.toString().includes("[native code]")) {
|
|
1254
|
+
signals.geolocationSpoofed = true;
|
|
1255
|
+
score += 0.2;
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return Math.min(score, 0.4);
|
|
1261
|
+
}
|
|
1262
|
+
function detectMitM(signals) {
|
|
1263
|
+
if (typeof window === "undefined" || typeof document === "undefined") return 0;
|
|
1264
|
+
const indicators = [];
|
|
1265
|
+
if (window.location?.protocol === "https:") {
|
|
1266
|
+
const scripts = document.querySelectorAll("script[src]");
|
|
1267
|
+
for (let i = 0; i < scripts.length; i++) {
|
|
1268
|
+
const src = scripts[i].getAttribute("src") || "";
|
|
1269
|
+
if (src.startsWith("http://") && !src.includes("localhost")) {
|
|
1270
|
+
indicators.push("mixed_content_script");
|
|
1271
|
+
break;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
const iframes = document.querySelectorAll("iframe");
|
|
1276
|
+
for (let i = 0; i < iframes.length; i++) {
|
|
1277
|
+
const src = iframes[i].getAttribute("src") || "";
|
|
1278
|
+
if (src && !src.startsWith(window.location?.origin || "") && !src.startsWith("about:") && !src.startsWith("javascript:")) {
|
|
1279
|
+
const style = window.getComputedStyle(iframes[i]);
|
|
1280
|
+
if (style.display === "none" || style.visibility === "hidden" || parseInt(style.width) <= 1 || parseInt(style.height) <= 1) {
|
|
1281
|
+
indicators.push("hidden_iframe");
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if ("serviceWorker" in navigator) {
|
|
1286
|
+
navigator.serviceWorker.getRegistrations().then((registrations) => {
|
|
1287
|
+
for (const reg of registrations) {
|
|
1288
|
+
const swUrl = reg.active?.scriptURL || "";
|
|
1289
|
+
if (swUrl && !swUrl.startsWith(window.location?.origin || "")) {
|
|
1290
|
+
indicators.push("foreign_service_worker");
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}).catch(() => {
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
if (typeof performance !== "undefined" && performance.getEntriesByType) {
|
|
1297
|
+
try {
|
|
1298
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
1299
|
+
if (nav) {
|
|
1300
|
+
const connectTime = nav.connectEnd - nav.connectStart;
|
|
1301
|
+
const tlsTime = nav.requestStart - nav.secureConnectionStart;
|
|
1302
|
+
if (connectTime > 1e3 && tlsTime > 500) {
|
|
1303
|
+
indicators.push("slow_tls_handshake");
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
} catch {
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (window.location?.protocol === "https:" && typeof performance !== "undefined") {
|
|
1310
|
+
try {
|
|
1311
|
+
const resources = performance.getEntriesByType("resource");
|
|
1312
|
+
for (const resource of resources.slice(0, 20)) {
|
|
1313
|
+
if (resource.transferSize === 0 && resource.encodedBodySize > 0 && !resource.name.includes("cache")) {
|
|
1314
|
+
indicators.push("intercepted_resource");
|
|
1315
|
+
break;
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
} catch {
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
signals.mitmIndicators = indicators;
|
|
1322
|
+
return Math.min(indicators.length * 0.2, 0.5);
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/fingerprint/signals.ts
|
|
1326
|
+
function collectCoreSignals() {
|
|
1327
|
+
return {
|
|
1328
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1329
|
+
language: typeof navigator !== "undefined" ? navigator.language : "",
|
|
1330
|
+
languages: typeof navigator !== "undefined" ? Array.from(navigator.languages || []) : [],
|
|
1331
|
+
platform: typeof navigator !== "undefined" ? navigator.platform || "" : "",
|
|
1332
|
+
screenWidth: typeof screen !== "undefined" ? screen.width : 0,
|
|
1333
|
+
screenHeight: typeof screen !== "undefined" ? screen.height : 0,
|
|
1334
|
+
screenColorDepth: typeof screen !== "undefined" ? screen.colorDepth : 0,
|
|
1335
|
+
devicePixelRatio: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
|
|
1336
|
+
timezoneOffset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
|
|
1337
|
+
timezone: Intl?.DateTimeFormat?.()?.resolvedOptions?.()?.timeZone || "",
|
|
1338
|
+
sessionStorage: hasSessionStorage(),
|
|
1339
|
+
localStorage: hasLocalStorage(),
|
|
1340
|
+
indexedDB: typeof indexedDB !== "undefined",
|
|
1341
|
+
cookieEnabled: typeof navigator !== "undefined" ? navigator.cookieEnabled : false,
|
|
1342
|
+
hardwareConcurrency: typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 0 : 0,
|
|
1343
|
+
maxTouchPoints: typeof navigator !== "undefined" ? navigator.maxTouchPoints || 0 : 0,
|
|
1344
|
+
deviceMemory: typeof navigator !== "undefined" ? navigator.deviceMemory : void 0
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
function collectExtendedSignals() {
|
|
1348
|
+
const signals = {};
|
|
1349
|
+
signals.canvasFingerprint = getCanvasFingerprint();
|
|
1350
|
+
const webgl = getWebGLInfo();
|
|
1351
|
+
signals.webglVendor = webgl.vendor;
|
|
1352
|
+
signals.webglRenderer = webgl.renderer;
|
|
1353
|
+
signals.audioFingerprint = getAudioFingerprint();
|
|
1354
|
+
return signals;
|
|
1355
|
+
}
|
|
1356
|
+
function collectDeviceSignals(includeExtended) {
|
|
1357
|
+
const core = collectCoreSignals();
|
|
1358
|
+
const signals = { ...core };
|
|
1359
|
+
if (includeExtended) {
|
|
1360
|
+
signals.extended = collectExtendedSignals();
|
|
1361
|
+
}
|
|
1362
|
+
const smart = collectSmartSignals();
|
|
1363
|
+
signals.smart = smart;
|
|
1364
|
+
signals.incognito = smart.incognito;
|
|
1365
|
+
signals.bot = smart.bot;
|
|
1366
|
+
return signals;
|
|
1367
|
+
}
|
|
1368
|
+
function collectSmartSignals() {
|
|
1369
|
+
const signals = {};
|
|
1370
|
+
signals.incognito = detectIncognito();
|
|
1371
|
+
signals.bot = detectBot();
|
|
1372
|
+
signals.privacyBrowser = detectPrivacyBrowser();
|
|
1373
|
+
signals.devToolsOpen = detectDevTools();
|
|
1374
|
+
signals.connectionType = getConnectionType();
|
|
1375
|
+
signals.doNotTrack = getDoNotTrack();
|
|
1376
|
+
signals.notificationsEnabled = getNotificationsPermission();
|
|
1377
|
+
const deviceAge = getDeviceAge();
|
|
1378
|
+
signals.deviceFirstSeen = deviceAge.firstSeen;
|
|
1379
|
+
signals.deviceVisitCount = deviceAge.visitCount;
|
|
1380
|
+
const tampering = detectTampering();
|
|
1381
|
+
signals.browserTampered = tampering.browserTampered;
|
|
1382
|
+
signals.canvasTampered = tampering.canvasTampered;
|
|
1383
|
+
signals.webglTampered = tampering.webglTampered;
|
|
1384
|
+
signals.vmDetected = tampering.vmDetected;
|
|
1385
|
+
signals.vmIndicators = tampering.signals.vmIndicators;
|
|
1386
|
+
return signals;
|
|
1387
|
+
}
|
|
1388
|
+
function hasSessionStorage() {
|
|
1389
|
+
try {
|
|
1390
|
+
const key = "__sp_test__";
|
|
1391
|
+
sessionStorage.setItem(key, "1");
|
|
1392
|
+
sessionStorage.removeItem(key);
|
|
1393
|
+
return true;
|
|
1394
|
+
} catch {
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
function hasLocalStorage() {
|
|
1399
|
+
try {
|
|
1400
|
+
const key = "__sp_test__";
|
|
1401
|
+
localStorage.setItem(key, "1");
|
|
1402
|
+
localStorage.removeItem(key);
|
|
1403
|
+
return true;
|
|
1404
|
+
} catch {
|
|
1405
|
+
return false;
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function getCanvasFingerprint() {
|
|
1409
|
+
if (typeof document === "undefined") return void 0;
|
|
1410
|
+
try {
|
|
1411
|
+
const canvas = document.createElement("canvas");
|
|
1412
|
+
const ctx = canvas.getContext("2d");
|
|
1413
|
+
if (!ctx) return void 0;
|
|
1414
|
+
canvas.width = 200;
|
|
1415
|
+
canvas.height = 50;
|
|
1416
|
+
ctx.textBaseline = "top";
|
|
1417
|
+
ctx.font = "14px Arial";
|
|
1418
|
+
ctx.fillStyle = "#f60";
|
|
1419
|
+
ctx.fillRect(0, 0, 200, 50);
|
|
1420
|
+
ctx.fillStyle = "#069";
|
|
1421
|
+
ctx.fillText("SitePong fingerprint", 2, 15);
|
|
1422
|
+
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
1423
|
+
ctx.fillText("SitePong fingerprint", 4, 17);
|
|
1424
|
+
return hashString(canvas.toDataURL());
|
|
1425
|
+
} catch {
|
|
1426
|
+
return void 0;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function getWebGLInfo() {
|
|
1430
|
+
if (typeof document === "undefined") return {};
|
|
1431
|
+
try {
|
|
1432
|
+
const canvas = document.createElement("canvas");
|
|
1433
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
|
|
1434
|
+
if (!gl || !(gl instanceof WebGLRenderingContext)) return {};
|
|
1435
|
+
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
|
1436
|
+
if (!debugInfo) return {};
|
|
1437
|
+
return {
|
|
1438
|
+
vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
|
|
1439
|
+
renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
|
|
1440
|
+
};
|
|
1441
|
+
} catch {
|
|
1442
|
+
return {};
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
function getAudioFingerprint() {
|
|
1446
|
+
if (typeof window === "undefined" || typeof OfflineAudioContext === "undefined") {
|
|
1447
|
+
return void 0;
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
const context = new OfflineAudioContext(1, 44100, 44100);
|
|
1451
|
+
const oscillator = context.createOscillator();
|
|
1452
|
+
oscillator.type = "triangle";
|
|
1453
|
+
oscillator.frequency.setValueAtTime(1e4, context.currentTime);
|
|
1454
|
+
const compressor = context.createDynamicsCompressor();
|
|
1455
|
+
compressor.threshold.setValueAtTime(-50, context.currentTime);
|
|
1456
|
+
compressor.knee.setValueAtTime(40, context.currentTime);
|
|
1457
|
+
compressor.ratio.setValueAtTime(12, context.currentTime);
|
|
1458
|
+
compressor.attack.setValueAtTime(0, context.currentTime);
|
|
1459
|
+
compressor.release.setValueAtTime(0.25, context.currentTime);
|
|
1460
|
+
oscillator.connect(compressor);
|
|
1461
|
+
compressor.connect(context.destination);
|
|
1462
|
+
oscillator.start(0);
|
|
1463
|
+
return hashString(`audio:${context.sampleRate}:${context.length}`);
|
|
1464
|
+
} catch {
|
|
1465
|
+
return void 0;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
function detectIncognito() {
|
|
1469
|
+
if (typeof window === "undefined") return false;
|
|
1470
|
+
try {
|
|
1471
|
+
const testKey = "__sp_incognito_test__";
|
|
1472
|
+
try {
|
|
1473
|
+
localStorage.setItem(testKey, new Array(100).join("x"));
|
|
1474
|
+
localStorage.removeItem(testKey);
|
|
1475
|
+
} catch {
|
|
1476
|
+
return true;
|
|
1477
|
+
}
|
|
1478
|
+
if (typeof indexedDB !== "undefined") {
|
|
1479
|
+
try {
|
|
1480
|
+
const db = indexedDB.open("__sp_test__");
|
|
1481
|
+
db.onerror = () => {
|
|
1482
|
+
};
|
|
1483
|
+
} catch {
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
if ("webkitRequestFileSystem" in window) {
|
|
1488
|
+
let detected = false;
|
|
1489
|
+
try {
|
|
1490
|
+
window.webkitRequestFileSystem(
|
|
1491
|
+
0,
|
|
1492
|
+
1,
|
|
1493
|
+
() => {
|
|
1494
|
+
detected = false;
|
|
1495
|
+
},
|
|
1496
|
+
() => {
|
|
1497
|
+
detected = true;
|
|
1498
|
+
}
|
|
1499
|
+
);
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
if (detected) return true;
|
|
1503
|
+
}
|
|
1504
|
+
const perf = performance;
|
|
1505
|
+
if (perf.memory) {
|
|
1506
|
+
const heapLimit = perf.memory.jsHeapSizeLimit || 0;
|
|
1507
|
+
if (heapLimit > 0 && heapLimit < 1073741824) {
|
|
1508
|
+
return true;
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
return false;
|
|
1512
|
+
} catch {
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
function detectBot() {
|
|
1517
|
+
if (typeof navigator === "undefined") return false;
|
|
1518
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
1519
|
+
const botPatterns = [
|
|
1520
|
+
"bot",
|
|
1521
|
+
"crawler",
|
|
1522
|
+
"spider",
|
|
1523
|
+
"headless",
|
|
1524
|
+
"phantom",
|
|
1525
|
+
"puppeteer",
|
|
1526
|
+
"selenium",
|
|
1527
|
+
"webdriver",
|
|
1528
|
+
"playwright"
|
|
1529
|
+
];
|
|
1530
|
+
if (botPatterns.some((pattern) => ua.includes(pattern))) {
|
|
1531
|
+
return true;
|
|
1532
|
+
}
|
|
1533
|
+
if ("webdriver" in navigator && navigator.webdriver) {
|
|
1534
|
+
return true;
|
|
1535
|
+
}
|
|
1536
|
+
if (navigator.plugins && navigator.plugins.length === 0 && !/mobile|android|iphone/i.test(ua)) {
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
function detectPrivacyBrowser() {
|
|
1542
|
+
if (typeof navigator === "undefined") return false;
|
|
1543
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
1544
|
+
if ("brave" in navigator) return true;
|
|
1545
|
+
if (ua.includes("focus") && ua.includes("firefox")) return true;
|
|
1546
|
+
if (ua.includes("duckduckgo")) return true;
|
|
1547
|
+
if (typeof window !== "undefined") {
|
|
1548
|
+
if (screen.width === 1e3 && screen.height === 1e3) return true;
|
|
1549
|
+
if ((/* @__PURE__ */ new Date()).getTimezoneOffset() === 0 && navigator.language !== "en") return true;
|
|
1550
|
+
}
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
function detectDevTools() {
|
|
1554
|
+
if (typeof window === "undefined") return false;
|
|
1555
|
+
try {
|
|
1556
|
+
const widthThreshold = window.outerWidth - window.innerWidth > 160;
|
|
1557
|
+
const heightThreshold = window.outerHeight - window.innerHeight > 160;
|
|
1558
|
+
if (widthThreshold || heightThreshold) return true;
|
|
1559
|
+
} catch {
|
|
1560
|
+
}
|
|
1561
|
+
return false;
|
|
1562
|
+
}
|
|
1563
|
+
function getConnectionType() {
|
|
1564
|
+
if (typeof navigator === "undefined") return void 0;
|
|
1565
|
+
const connection = navigator.connection;
|
|
1566
|
+
if (!connection) return void 0;
|
|
1567
|
+
return connection.effectiveType || connection.type || void 0;
|
|
1568
|
+
}
|
|
1569
|
+
function getDoNotTrack() {
|
|
1570
|
+
if (typeof navigator === "undefined") return false;
|
|
1571
|
+
const dnt = navigator.doNotTrack;
|
|
1572
|
+
return dnt === "1" || dnt === "yes";
|
|
1573
|
+
}
|
|
1574
|
+
function getNotificationsPermission() {
|
|
1575
|
+
if (typeof Notification === "undefined") return false;
|
|
1576
|
+
return Notification.permission === "granted";
|
|
1577
|
+
}
|
|
1578
|
+
function getDeviceAge() {
|
|
1579
|
+
const STORAGE_KEY2 = "sitepong_device_age";
|
|
1580
|
+
const result = { visitCount: 1 };
|
|
1581
|
+
if (typeof localStorage === "undefined") return result;
|
|
1582
|
+
try {
|
|
1583
|
+
const stored = localStorage.getItem(STORAGE_KEY2);
|
|
1584
|
+
if (stored) {
|
|
1585
|
+
const parsed = JSON.parse(stored);
|
|
1586
|
+
result.firstSeen = parsed.firstSeen;
|
|
1587
|
+
result.visitCount = (parsed.visitCount || 0) + 1;
|
|
1588
|
+
} else {
|
|
1589
|
+
result.firstSeen = (/* @__PURE__ */ new Date()).toISOString();
|
|
1590
|
+
}
|
|
1591
|
+
localStorage.setItem(STORAGE_KEY2, JSON.stringify({
|
|
1592
|
+
firstSeen: result.firstSeen,
|
|
1593
|
+
visitCount: result.visitCount
|
|
1594
|
+
}));
|
|
1595
|
+
} catch {
|
|
1596
|
+
}
|
|
1597
|
+
return result;
|
|
1598
|
+
}
|
|
1599
|
+
function hashString(str) {
|
|
1600
|
+
let hash = 0;
|
|
1601
|
+
for (let i = 0; i < str.length; i++) {
|
|
1602
|
+
const char = str.charCodeAt(i);
|
|
1603
|
+
hash = (hash << 5) - hash + char;
|
|
1604
|
+
hash = hash & hash;
|
|
1605
|
+
}
|
|
1606
|
+
return Math.abs(hash).toString(16);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/fingerprint/behavior.ts
|
|
1610
|
+
var BehaviorAnalyzer = class {
|
|
1611
|
+
constructor() {
|
|
1612
|
+
this.mousePoints = [];
|
|
1613
|
+
this.keyDownTimes = /* @__PURE__ */ new Map();
|
|
1614
|
+
this.keyIntervals = [];
|
|
1615
|
+
this.keyHoldTimes = [];
|
|
1616
|
+
this.lastKeyTime = 0;
|
|
1617
|
+
this.scrollEvents = [];
|
|
1618
|
+
this.clickTimes = [];
|
|
1619
|
+
this.running = false;
|
|
1620
|
+
this.maxSamples = 500;
|
|
1621
|
+
this.handleMouseMove = (e) => {
|
|
1622
|
+
if (this.mousePoints.length >= this.maxSamples) return;
|
|
1623
|
+
this.mousePoints.push({ x: e.clientX, y: e.clientY, t: Date.now() });
|
|
1624
|
+
};
|
|
1625
|
+
this.handleKeyDown = (e) => {
|
|
1626
|
+
if (this.keyIntervals.length >= this.maxSamples) return;
|
|
1627
|
+
const now = Date.now();
|
|
1628
|
+
this.keyDownTimes.set(e.key, now);
|
|
1629
|
+
if (this.lastKeyTime > 0) {
|
|
1630
|
+
this.keyIntervals.push(now - this.lastKeyTime);
|
|
1631
|
+
}
|
|
1632
|
+
this.lastKeyTime = now;
|
|
1633
|
+
};
|
|
1634
|
+
this.handleKeyUp = (e) => {
|
|
1635
|
+
const downTime = this.keyDownTimes.get(e.key);
|
|
1636
|
+
if (downTime) {
|
|
1637
|
+
this.keyHoldTimes.push(Date.now() - downTime);
|
|
1638
|
+
this.keyDownTimes.delete(e.key);
|
|
1639
|
+
}
|
|
1640
|
+
};
|
|
1641
|
+
this.handleScroll = () => {
|
|
1642
|
+
if (this.scrollEvents.length >= this.maxSamples) return;
|
|
1643
|
+
this.scrollEvents.push({ deltaY: window.scrollY, t: Date.now() });
|
|
1644
|
+
};
|
|
1645
|
+
this.handleClick = () => {
|
|
1646
|
+
if (this.clickTimes.length >= this.maxSamples) return;
|
|
1647
|
+
this.clickTimes.push(Date.now());
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
start() {
|
|
1651
|
+
if (this.running || typeof window === "undefined") return;
|
|
1652
|
+
this.running = true;
|
|
1653
|
+
window.addEventListener("mousemove", this.handleMouseMove, { passive: true });
|
|
1654
|
+
window.addEventListener("keydown", this.handleKeyDown, { passive: true });
|
|
1655
|
+
window.addEventListener("keyup", this.handleKeyUp, { passive: true });
|
|
1656
|
+
window.addEventListener("scroll", this.handleScroll, { passive: true });
|
|
1657
|
+
window.addEventListener("click", this.handleClick, { passive: true });
|
|
1658
|
+
}
|
|
1659
|
+
stop() {
|
|
1660
|
+
if (!this.running || typeof window === "undefined") return;
|
|
1661
|
+
this.running = false;
|
|
1662
|
+
window.removeEventListener("mousemove", this.handleMouseMove);
|
|
1663
|
+
window.removeEventListener("keydown", this.handleKeyDown);
|
|
1664
|
+
window.removeEventListener("keyup", this.handleKeyUp);
|
|
1665
|
+
window.removeEventListener("scroll", this.handleScroll);
|
|
1666
|
+
window.removeEventListener("click", this.handleClick);
|
|
1667
|
+
}
|
|
1668
|
+
getMetrics() {
|
|
1669
|
+
const mouse = this.analyzeMouseMovement();
|
|
1670
|
+
const keyboard = this.analyzeKeyboard();
|
|
1671
|
+
const scroll = this.analyzeScroll();
|
|
1672
|
+
const clicks = this.analyzeClicks();
|
|
1673
|
+
const score = this.calculateHumanScore(mouse, keyboard, scroll, clicks);
|
|
1674
|
+
return { mouse, keyboard, scroll, clicks, score };
|
|
1675
|
+
}
|
|
1676
|
+
reset() {
|
|
1677
|
+
this.mousePoints = [];
|
|
1678
|
+
this.keyDownTimes.clear();
|
|
1679
|
+
this.keyIntervals = [];
|
|
1680
|
+
this.keyHoldTimes = [];
|
|
1681
|
+
this.lastKeyTime = 0;
|
|
1682
|
+
this.scrollEvents = [];
|
|
1683
|
+
this.clickTimes = [];
|
|
1684
|
+
}
|
|
1685
|
+
analyzeMouseMovement() {
|
|
1686
|
+
if (this.mousePoints.length < 3) {
|
|
1687
|
+
return {
|
|
1688
|
+
totalMovements: this.mousePoints.length,
|
|
1689
|
+
averageSpeed: 0,
|
|
1690
|
+
maxSpeed: 0,
|
|
1691
|
+
averageAcceleration: 0,
|
|
1692
|
+
straightLineRatio: 0,
|
|
1693
|
+
jitter: 0,
|
|
1694
|
+
hasMovement: false
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
const speeds = [];
|
|
1698
|
+
const accelerations = [];
|
|
1699
|
+
let straightSegments = 0;
|
|
1700
|
+
let totalSegments = 0;
|
|
1701
|
+
let jitterSum = 0;
|
|
1702
|
+
for (let i = 1; i < this.mousePoints.length; i++) {
|
|
1703
|
+
const prev = this.mousePoints[i - 1];
|
|
1704
|
+
const curr = this.mousePoints[i];
|
|
1705
|
+
const dt = curr.t - prev.t || 1;
|
|
1706
|
+
const dx = curr.x - prev.x;
|
|
1707
|
+
const dy = curr.y - prev.y;
|
|
1708
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
1709
|
+
const speed = distance / dt;
|
|
1710
|
+
speeds.push(speed);
|
|
1711
|
+
if (distance < 3 && distance > 0) {
|
|
1712
|
+
jitterSum++;
|
|
1713
|
+
}
|
|
1714
|
+
if (i >= 2) {
|
|
1715
|
+
const pp = this.mousePoints[i - 2];
|
|
1716
|
+
const expectedX = pp.x + (curr.x - pp.x) * ((prev.t - pp.t) / (curr.t - pp.t));
|
|
1717
|
+
const expectedY = pp.y + (curr.y - pp.y) * ((prev.t - pp.t) / (curr.t - pp.t));
|
|
1718
|
+
const deviation = Math.sqrt((prev.x - expectedX) ** 2 + (prev.y - expectedY) ** 2);
|
|
1719
|
+
totalSegments++;
|
|
1720
|
+
if (deviation < 2) straightSegments++;
|
|
1721
|
+
}
|
|
1722
|
+
if (i >= 2) {
|
|
1723
|
+
const prevSpeed = speeds[speeds.length - 2] || 0;
|
|
1724
|
+
accelerations.push(Math.abs(speed - prevSpeed) / dt);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
|
|
1728
|
+
const maxSpeed = Math.max(...speeds);
|
|
1729
|
+
const avgAccel = accelerations.length > 0 ? accelerations.reduce((a, b) => a + b, 0) / accelerations.length : 0;
|
|
1730
|
+
return {
|
|
1731
|
+
totalMovements: this.mousePoints.length,
|
|
1732
|
+
averageSpeed: Math.round(avgSpeed * 1e3) / 1e3,
|
|
1733
|
+
maxSpeed: Math.round(maxSpeed * 1e3) / 1e3,
|
|
1734
|
+
averageAcceleration: Math.round(avgAccel * 1e3) / 1e3,
|
|
1735
|
+
straightLineRatio: totalSegments > 0 ? straightSegments / totalSegments : 0,
|
|
1736
|
+
jitter: this.mousePoints.length > 0 ? jitterSum / this.mousePoints.length : 0,
|
|
1737
|
+
hasMovement: true
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
analyzeKeyboard() {
|
|
1741
|
+
if (this.keyIntervals.length < 2) {
|
|
1742
|
+
return {
|
|
1743
|
+
totalKeystrokes: this.keyIntervals.length,
|
|
1744
|
+
averageInterval: 0,
|
|
1745
|
+
intervalVariance: 0,
|
|
1746
|
+
holdTimeAverage: 0,
|
|
1747
|
+
holdTimeVariance: 0,
|
|
1748
|
+
hasKeystrokes: false
|
|
1749
|
+
};
|
|
1750
|
+
}
|
|
1751
|
+
const avgInterval = this.keyIntervals.reduce((a, b) => a + b, 0) / this.keyIntervals.length;
|
|
1752
|
+
const intervalVar = this.variance(this.keyIntervals);
|
|
1753
|
+
const avgHold = this.keyHoldTimes.length > 0 ? this.keyHoldTimes.reduce((a, b) => a + b, 0) / this.keyHoldTimes.length : 0;
|
|
1754
|
+
const holdVar = this.keyHoldTimes.length > 0 ? this.variance(this.keyHoldTimes) : 0;
|
|
1755
|
+
return {
|
|
1756
|
+
totalKeystrokes: this.keyIntervals.length + 1,
|
|
1757
|
+
averageInterval: Math.round(avgInterval),
|
|
1758
|
+
intervalVariance: Math.round(intervalVar),
|
|
1759
|
+
holdTimeAverage: Math.round(avgHold),
|
|
1760
|
+
holdTimeVariance: Math.round(holdVar),
|
|
1761
|
+
hasKeystrokes: true
|
|
1762
|
+
};
|
|
1763
|
+
}
|
|
1764
|
+
analyzeScroll() {
|
|
1765
|
+
if (this.scrollEvents.length < 2) {
|
|
1766
|
+
return { totalScrolls: this.scrollEvents.length, averageSpeed: 0, smoothness: 0, hasScroll: false };
|
|
1767
|
+
}
|
|
1768
|
+
const speeds = [];
|
|
1769
|
+
for (let i = 1; i < this.scrollEvents.length; i++) {
|
|
1770
|
+
const dt = this.scrollEvents[i].t - this.scrollEvents[i - 1].t || 1;
|
|
1771
|
+
const dy = Math.abs(this.scrollEvents[i].deltaY - this.scrollEvents[i - 1].deltaY);
|
|
1772
|
+
speeds.push(dy / dt);
|
|
1773
|
+
}
|
|
1774
|
+
const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
|
|
1775
|
+
const speedVar = this.variance(speeds);
|
|
1776
|
+
const smoothness = avgSpeed > 0 ? Math.min(1, avgSpeed / (speedVar + avgSpeed)) : 0;
|
|
1777
|
+
return {
|
|
1778
|
+
totalScrolls: this.scrollEvents.length,
|
|
1779
|
+
averageSpeed: Math.round(avgSpeed * 100) / 100,
|
|
1780
|
+
smoothness: Math.round(smoothness * 100) / 100,
|
|
1781
|
+
hasScroll: true
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
analyzeClicks() {
|
|
1785
|
+
if (this.clickTimes.length < 2) {
|
|
1786
|
+
return {
|
|
1787
|
+
totalClicks: this.clickTimes.length,
|
|
1788
|
+
averageInterval: 0,
|
|
1789
|
+
intervalVariance: 0,
|
|
1790
|
+
doubleClickCount: 0,
|
|
1791
|
+
hasClicks: this.clickTimes.length > 0
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
const intervals = [];
|
|
1795
|
+
let doubleClicks = 0;
|
|
1796
|
+
for (let i = 1; i < this.clickTimes.length; i++) {
|
|
1797
|
+
const interval = this.clickTimes[i] - this.clickTimes[i - 1];
|
|
1798
|
+
intervals.push(interval);
|
|
1799
|
+
if (interval < 500) doubleClicks++;
|
|
1800
|
+
}
|
|
1801
|
+
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
|
1802
|
+
const intervalVar = this.variance(intervals);
|
|
1803
|
+
return {
|
|
1804
|
+
totalClicks: this.clickTimes.length,
|
|
1805
|
+
averageInterval: Math.round(avgInterval),
|
|
1806
|
+
intervalVariance: Math.round(intervalVar),
|
|
1807
|
+
doubleClickCount: doubleClicks,
|
|
1808
|
+
hasClicks: true
|
|
1809
|
+
};
|
|
1810
|
+
}
|
|
1811
|
+
calculateHumanScore(mouse, keyboard, scroll, clicks) {
|
|
1812
|
+
let score = 0.5;
|
|
1813
|
+
let factors = 0;
|
|
1814
|
+
if (mouse.hasMovement) {
|
|
1815
|
+
factors++;
|
|
1816
|
+
let mouseScore = 0.5;
|
|
1817
|
+
if (mouse.jitter > 0.05) mouseScore += 0.15;
|
|
1818
|
+
if (mouse.straightLineRatio < 0.5) mouseScore += 0.15;
|
|
1819
|
+
else if (mouse.straightLineRatio > 0.9) mouseScore -= 0.2;
|
|
1820
|
+
if (mouse.averageSpeed > 0 && mouse.maxSpeed / mouse.averageSpeed > 3) {
|
|
1821
|
+
mouseScore += 0.1;
|
|
1822
|
+
}
|
|
1823
|
+
if (mouse.averageAcceleration > 0) mouseScore += 0.1;
|
|
1824
|
+
score += Math.max(0, Math.min(1, mouseScore)) - 0.5;
|
|
1825
|
+
}
|
|
1826
|
+
if (keyboard.hasKeystrokes) {
|
|
1827
|
+
factors++;
|
|
1828
|
+
let keyScore = 0.5;
|
|
1829
|
+
if (keyboard.intervalVariance > 1e3) keyScore += 0.2;
|
|
1830
|
+
else if (keyboard.intervalVariance < 100) keyScore -= 0.2;
|
|
1831
|
+
const cv = keyboard.averageInterval > 0 ? Math.sqrt(keyboard.intervalVariance) / keyboard.averageInterval : 0;
|
|
1832
|
+
if (cv > 0.3) keyScore += 0.15;
|
|
1833
|
+
if (keyboard.holdTimeVariance > 500) keyScore += 0.1;
|
|
1834
|
+
score += Math.max(0, Math.min(1, keyScore)) - 0.5;
|
|
1835
|
+
}
|
|
1836
|
+
if (scroll.hasScroll) {
|
|
1837
|
+
factors++;
|
|
1838
|
+
let scrollScore = 0.5;
|
|
1839
|
+
if (scroll.smoothness > 0.3 && scroll.smoothness < 0.9) {
|
|
1840
|
+
scrollScore += 0.2;
|
|
1841
|
+
}
|
|
1842
|
+
score += Math.max(0, Math.min(1, scrollScore)) - 0.5;
|
|
1843
|
+
}
|
|
1844
|
+
if (clicks.hasClicks) {
|
|
1845
|
+
factors++;
|
|
1846
|
+
let clickScore = 0.5;
|
|
1847
|
+
if (clicks.intervalVariance > 5e4) clickScore += 0.15;
|
|
1848
|
+
else if (clicks.intervalVariance < 100) clickScore -= 0.2;
|
|
1849
|
+
score += Math.max(0, Math.min(1, clickScore)) - 0.5;
|
|
1850
|
+
}
|
|
1851
|
+
if (factors === 0) return 0.3;
|
|
1852
|
+
return Math.max(0, Math.min(1, score));
|
|
1853
|
+
}
|
|
1854
|
+
variance(values) {
|
|
1855
|
+
if (values.length < 2) return 0;
|
|
1856
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
1857
|
+
return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1);
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
// src/fingerprint/vpn-detection.ts
|
|
1862
|
+
async function detectVPN() {
|
|
1863
|
+
const signals = {
|
|
1864
|
+
webrtcLeak: null,
|
|
1865
|
+
timezoneIPMismatch: false,
|
|
1866
|
+
dnsLeak: null,
|
|
1867
|
+
connectionType: null,
|
|
1868
|
+
multipleIPs: false,
|
|
1869
|
+
torBrowser: false,
|
|
1870
|
+
knownVPNExtension: false
|
|
1871
|
+
};
|
|
1872
|
+
let confidence = 0;
|
|
1873
|
+
let vpnDetected = false;
|
|
1874
|
+
let proxyDetected = false;
|
|
1875
|
+
let torDetected = false;
|
|
1876
|
+
signals.torBrowser = detectTorBrowser();
|
|
1877
|
+
if (signals.torBrowser) {
|
|
1878
|
+
torDetected = true;
|
|
1879
|
+
confidence += 0.9;
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
signals.webrtcLeak = await detectWebRTCLeak();
|
|
1883
|
+
if (signals.webrtcLeak) {
|
|
1884
|
+
vpnDetected = true;
|
|
1885
|
+
confidence += 0.3;
|
|
1886
|
+
}
|
|
1887
|
+
} catch {
|
|
1888
|
+
signals.webrtcLeak = null;
|
|
1889
|
+
}
|
|
1890
|
+
signals.connectionType = getConnectionType2();
|
|
1891
|
+
if (signals.connectionType === "unknown" || signals.connectionType === null) {
|
|
1892
|
+
confidence += 0.05;
|
|
1893
|
+
}
|
|
1894
|
+
signals.knownVPNExtension = detectVPNExtensions();
|
|
1895
|
+
if (signals.knownVPNExtension) {
|
|
1896
|
+
vpnDetected = true;
|
|
1897
|
+
confidence += 0.4;
|
|
1898
|
+
}
|
|
1899
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1900
|
+
const lang = navigator.language;
|
|
1901
|
+
if (tz && lang) {
|
|
1902
|
+
const tzCountry = getTimezoneCountry(tz);
|
|
1903
|
+
const langCountry = lang.split("-")[1]?.toUpperCase();
|
|
1904
|
+
if (tzCountry && langCountry && tzCountry !== langCountry) {
|
|
1905
|
+
signals.timezoneIPMismatch = true;
|
|
1906
|
+
confidence += 0.1;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
confidence = Math.min(confidence, 1);
|
|
1910
|
+
if (confidence < 0.3) {
|
|
1911
|
+
vpnDetected = false;
|
|
1912
|
+
proxyDetected = false;
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
vpnDetected,
|
|
1916
|
+
proxyDetected,
|
|
1917
|
+
torDetected,
|
|
1918
|
+
confidence,
|
|
1919
|
+
signals
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
function detectTorBrowser() {
|
|
1923
|
+
if (typeof window === "undefined") return false;
|
|
1924
|
+
const checks = [];
|
|
1925
|
+
checks.push(window.screen.width === 1e3 && window.screen.height === 1e3);
|
|
1926
|
+
checks.push(!window.MediaDevices);
|
|
1927
|
+
checks.push(navigator.plugins.length === 0);
|
|
1928
|
+
checks.push((/* @__PURE__ */ new Date()).getTimezoneOffset() === 0);
|
|
1929
|
+
const ua = navigator.userAgent;
|
|
1930
|
+
checks.push(ua.includes("Firefox") && !ua.includes("Chrome"));
|
|
1931
|
+
const score = checks.filter(Boolean).length;
|
|
1932
|
+
return score >= 3;
|
|
1933
|
+
}
|
|
1934
|
+
async function detectWebRTCLeak() {
|
|
1935
|
+
if (typeof window === "undefined" || !window.RTCPeerConnection) {
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1938
|
+
return new Promise((resolve) => {
|
|
1939
|
+
const timeout = setTimeout(() => resolve(false), 3e3);
|
|
1940
|
+
const localIPs = /* @__PURE__ */ new Set();
|
|
1941
|
+
try {
|
|
1942
|
+
const pc = new RTCPeerConnection({
|
|
1943
|
+
iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
|
|
1944
|
+
});
|
|
1945
|
+
pc.createDataChannel("");
|
|
1946
|
+
pc.onicecandidate = (event) => {
|
|
1947
|
+
if (!event.candidate) {
|
|
1948
|
+
clearTimeout(timeout);
|
|
1949
|
+
pc.close();
|
|
1950
|
+
resolve(localIPs.size > 1);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const candidate = event.candidate.candidate;
|
|
1954
|
+
const ipMatch = candidate.match(/(\d{1,3}\.){3}\d{1,3}/);
|
|
1955
|
+
if (ipMatch) {
|
|
1956
|
+
localIPs.add(ipMatch[0]);
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
pc.createOffer().then((offer) => pc.setLocalDescription(offer)).catch(() => {
|
|
1960
|
+
clearTimeout(timeout);
|
|
1961
|
+
resolve(false);
|
|
1962
|
+
});
|
|
1963
|
+
} catch {
|
|
1964
|
+
clearTimeout(timeout);
|
|
1965
|
+
resolve(false);
|
|
1966
|
+
}
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
function getConnectionType2() {
|
|
1970
|
+
if (typeof navigator === "undefined") return null;
|
|
1971
|
+
const nav = navigator;
|
|
1972
|
+
if (nav.connection) {
|
|
1973
|
+
return nav.connection.type || nav.connection.effectiveType || null;
|
|
1974
|
+
}
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
function detectVPNExtensions() {
|
|
1978
|
+
if (typeof document === "undefined") return false;
|
|
1979
|
+
const body = document.body;
|
|
1980
|
+
if (!body) return false;
|
|
1981
|
+
const vpnIndicators = [
|
|
1982
|
+
"[data-windscribe]",
|
|
1983
|
+
"[data-nord]",
|
|
1984
|
+
".surfshark-extension",
|
|
1985
|
+
"#vpn-indicator",
|
|
1986
|
+
"[data-expressvpn]"
|
|
1987
|
+
];
|
|
1988
|
+
for (const selector of vpnIndicators) {
|
|
1989
|
+
try {
|
|
1990
|
+
if (document.querySelector(selector)) return true;
|
|
1991
|
+
} catch {
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
function getTimezoneCountry(tz) {
|
|
1997
|
+
const tzMap = {
|
|
1998
|
+
"America/New_York": "US",
|
|
1999
|
+
"America/Chicago": "US",
|
|
2000
|
+
"America/Denver": "US",
|
|
2001
|
+
"America/Los_Angeles": "US",
|
|
2002
|
+
"Europe/London": "GB",
|
|
2003
|
+
"Europe/Paris": "FR",
|
|
2004
|
+
"Europe/Berlin": "DE",
|
|
2005
|
+
"Europe/Madrid": "ES",
|
|
2006
|
+
"Europe/Rome": "IT",
|
|
2007
|
+
"Asia/Tokyo": "JP",
|
|
2008
|
+
"Asia/Shanghai": "CN",
|
|
2009
|
+
"Asia/Kolkata": "IN",
|
|
2010
|
+
"Asia/Seoul": "KR",
|
|
2011
|
+
"Australia/Sydney": "AU",
|
|
2012
|
+
"America/Toronto": "CA",
|
|
2013
|
+
"America/Sao_Paulo": "BR",
|
|
2014
|
+
"America/Mexico_City": "MX"
|
|
2015
|
+
};
|
|
2016
|
+
return tzMap[tz] || null;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/fingerprint/manager.ts
|
|
2020
|
+
var DEFAULT_ENDPOINT3 = "https://ingest.sitepong.com";
|
|
2021
|
+
var CACHE_KEY = "sitepong_visitor";
|
|
2022
|
+
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
2023
|
+
var FingerprintManager = class {
|
|
2024
|
+
constructor(config) {
|
|
2025
|
+
this.cachedVisitor = null;
|
|
2026
|
+
this.pendingRequest = null;
|
|
2027
|
+
this.config = {
|
|
2028
|
+
endpoint: DEFAULT_ENDPOINT3,
|
|
2029
|
+
enabled: true,
|
|
2030
|
+
debug: false,
|
|
2031
|
+
...config
|
|
2032
|
+
};
|
|
2033
|
+
this.behaviorAnalyzer = new BehaviorAnalyzer();
|
|
2034
|
+
if (this.config.enabled && typeof window !== "undefined") {
|
|
2035
|
+
this.behaviorAnalyzer.start();
|
|
2036
|
+
}
|
|
2037
|
+
this.loadFromStorage();
|
|
2038
|
+
}
|
|
2039
|
+
async getVisitorId() {
|
|
2040
|
+
if (!this.config.enabled) {
|
|
2041
|
+
throw new Error("Fingerprint is not enabled");
|
|
2042
|
+
}
|
|
2043
|
+
if (this.cachedVisitor && !this.isCacheExpired()) {
|
|
2044
|
+
this.log("Returning cached visitor ID:", this.cachedVisitor.result.visitorId);
|
|
2045
|
+
return this.cachedVisitor.result;
|
|
2046
|
+
}
|
|
2047
|
+
if (this.pendingRequest) {
|
|
2048
|
+
return this.pendingRequest;
|
|
2049
|
+
}
|
|
2050
|
+
this.pendingRequest = this.fetchVisitorId();
|
|
2051
|
+
try {
|
|
2052
|
+
const result = await this.pendingRequest;
|
|
2053
|
+
return result;
|
|
2054
|
+
} finally {
|
|
2055
|
+
this.pendingRequest = null;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
async getDeviceSignals() {
|
|
2059
|
+
return collectDeviceSignals(this.config.extendedSignals ?? false);
|
|
2060
|
+
}
|
|
2061
|
+
async getFraudCheck() {
|
|
2062
|
+
const [visitorResult, signals, vpnResult] = await Promise.all([
|
|
2063
|
+
this.getVisitorId(),
|
|
2064
|
+
this.getDeviceSignals(),
|
|
2065
|
+
detectVPN().catch(() => null)
|
|
2066
|
+
]);
|
|
2067
|
+
const behaviorMetrics = this.behaviorAnalyzer.getMetrics();
|
|
2068
|
+
signals.behavior = {
|
|
2069
|
+
score: behaviorMetrics.score,
|
|
2070
|
+
mouseMovements: behaviorMetrics.mouse.totalMovements,
|
|
2071
|
+
keystrokes: behaviorMetrics.keyboard.totalKeystrokes,
|
|
2072
|
+
scrolls: behaviorMetrics.scroll.totalScrolls,
|
|
2073
|
+
clicks: behaviorMetrics.clicks.totalClicks,
|
|
2074
|
+
isHuman: behaviorMetrics.score > 0.6
|
|
2075
|
+
};
|
|
2076
|
+
const vpnDetected = vpnResult?.vpnDetected || vpnResult?.torDetected || false;
|
|
2077
|
+
return {
|
|
2078
|
+
visitorId: visitorResult.visitorId,
|
|
2079
|
+
riskScore: this.calculateRiskScore(signals, vpnDetected),
|
|
2080
|
+
signals,
|
|
2081
|
+
flags: {
|
|
2082
|
+
incognito: signals.smart?.incognito || signals.incognito,
|
|
2083
|
+
bot: signals.smart?.bot || signals.bot || !signals.behavior.isHuman,
|
|
2084
|
+
vpn: vpnDetected || signals.smart?.timezoneMismatch,
|
|
2085
|
+
tampered: signals.smart?.privacyBrowser
|
|
2086
|
+
}
|
|
2087
|
+
};
|
|
2088
|
+
}
|
|
2089
|
+
stopBehaviorTracking() {
|
|
2090
|
+
this.behaviorAnalyzer.stop();
|
|
2091
|
+
}
|
|
2092
|
+
async fetchVisitorId() {
|
|
2093
|
+
const signals = await this.getDeviceSignals();
|
|
2094
|
+
const endpoint = this.config.visitorEndpoint || `${this.config.endpoint}/api/visitors`;
|
|
2095
|
+
try {
|
|
2096
|
+
const response = await fetch(endpoint, {
|
|
2097
|
+
method: "POST",
|
|
2098
|
+
headers: {
|
|
2099
|
+
"Content-Type": "application/json",
|
|
2100
|
+
"X-API-Key": this.config.apiKey
|
|
2101
|
+
},
|
|
2102
|
+
body: JSON.stringify({ signals })
|
|
2103
|
+
});
|
|
2104
|
+
if (!response.ok) {
|
|
2105
|
+
throw new Error(`HTTP ${response.status}`);
|
|
2106
|
+
}
|
|
2107
|
+
const result = await response.json();
|
|
2108
|
+
this.cachedVisitor = {
|
|
2109
|
+
result,
|
|
2110
|
+
signals,
|
|
2111
|
+
cachedAt: Date.now()
|
|
2112
|
+
};
|
|
2113
|
+
this.saveToStorage();
|
|
2114
|
+
this.log("Visitor ID fetched:", result.visitorId);
|
|
2115
|
+
return result;
|
|
2116
|
+
} catch (err) {
|
|
2117
|
+
this.log("Failed to fetch visitor ID:", err);
|
|
2118
|
+
throw err;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
calculateRiskScore(signals, vpnDetected = false) {
|
|
2122
|
+
let score = 0;
|
|
2123
|
+
const smart = signals.smart;
|
|
2124
|
+
if (smart?.bot || signals.bot) score += 0.8;
|
|
2125
|
+
if (signals.behavior) {
|
|
2126
|
+
if (signals.behavior.score < 0.3) score += 0.4;
|
|
2127
|
+
else if (signals.behavior.score < 0.5) score += 0.2;
|
|
2128
|
+
}
|
|
2129
|
+
if (vpnDetected) score += 0.25;
|
|
2130
|
+
if (smart?.incognito || signals.incognito) score += 0.2;
|
|
2131
|
+
if (smart?.devToolsOpen) score += 0.1;
|
|
2132
|
+
if (smart?.privacyBrowser) score += 0.1;
|
|
2133
|
+
if (signals.hardwareConcurrency === 0) score += 0.1;
|
|
2134
|
+
if (signals.maxTouchPoints === 0 && /mobile|android|iphone/i.test(signals.userAgent)) {
|
|
2135
|
+
score += 0.15;
|
|
2136
|
+
}
|
|
2137
|
+
if (!signals.cookieEnabled) score += 0.1;
|
|
2138
|
+
if (smart?.doNotTrack) score += 0.05;
|
|
2139
|
+
if (smart?.deviceVisitCount === 1) score += 0.05;
|
|
2140
|
+
return Math.round(Math.min(score, 1) * 100) / 100;
|
|
2141
|
+
}
|
|
2142
|
+
isCacheExpired() {
|
|
2143
|
+
if (!this.cachedVisitor) return true;
|
|
2144
|
+
return Date.now() - this.cachedVisitor.cachedAt > CACHE_TTL_MS;
|
|
2145
|
+
}
|
|
2146
|
+
loadFromStorage() {
|
|
2147
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") return;
|
|
2148
|
+
try {
|
|
2149
|
+
const stored = localStorage.getItem(CACHE_KEY);
|
|
2150
|
+
if (!stored) return;
|
|
2151
|
+
const parsed = JSON.parse(stored);
|
|
2152
|
+
if (!this.isCacheExpiredAt(parsed.cachedAt)) {
|
|
2153
|
+
this.cachedVisitor = parsed;
|
|
2154
|
+
this.log("Loaded cached visitor from storage");
|
|
2155
|
+
} else {
|
|
2156
|
+
localStorage.removeItem(CACHE_KEY);
|
|
2157
|
+
}
|
|
2158
|
+
} catch {
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
saveToStorage() {
|
|
2162
|
+
if (typeof window === "undefined" || typeof localStorage === "undefined") return;
|
|
2163
|
+
if (!this.cachedVisitor) return;
|
|
2164
|
+
try {
|
|
2165
|
+
localStorage.setItem(CACHE_KEY, JSON.stringify(this.cachedVisitor));
|
|
2166
|
+
} catch {
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
isCacheExpiredAt(cachedAt) {
|
|
2170
|
+
return Date.now() - cachedAt > CACHE_TTL_MS;
|
|
2171
|
+
}
|
|
2172
|
+
log(...args) {
|
|
2173
|
+
if (this.config.debug) {
|
|
2174
|
+
console.log("[SitePong Fingerprint]", ...args);
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
|
|
2179
|
+
// src/replay/serializer.ts
|
|
2180
|
+
var nodeIdCounter = 0;
|
|
2181
|
+
var nodeIdMap = /* @__PURE__ */ new WeakMap();
|
|
2182
|
+
function resetNodeIds() {
|
|
2183
|
+
nodeIdCounter = 0;
|
|
2184
|
+
}
|
|
2185
|
+
function getNodeId(node) {
|
|
2186
|
+
let id = nodeIdMap.get(node);
|
|
2187
|
+
if (id === void 0) {
|
|
2188
|
+
id = ++nodeIdCounter;
|
|
2189
|
+
nodeIdMap.set(node, id);
|
|
2190
|
+
}
|
|
2191
|
+
return id;
|
|
2192
|
+
}
|
|
2193
|
+
function serializeNode(node, options) {
|
|
2194
|
+
const id = getNodeId(node);
|
|
2195
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
2196
|
+
const parent = node.parentElement;
|
|
2197
|
+
let textContent = node.textContent || "";
|
|
2198
|
+
if (parent && options.maskSelector && parent.matches(options.maskSelector)) {
|
|
2199
|
+
textContent = textContent.replace(/\S/g, "*");
|
|
2200
|
+
}
|
|
2201
|
+
return {
|
|
2202
|
+
id,
|
|
2203
|
+
type: "text",
|
|
2204
|
+
textContent
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
2208
|
+
return { id, type: "comment", textContent: "" };
|
|
2209
|
+
}
|
|
2210
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
2211
|
+
const el = node;
|
|
2212
|
+
if (options.blockSelector && el.matches(options.blockSelector)) {
|
|
2213
|
+
return {
|
|
2214
|
+
id,
|
|
2215
|
+
type: "element",
|
|
2216
|
+
tagName: "div",
|
|
2217
|
+
attributes: {
|
|
2218
|
+
"data-blocked": "true",
|
|
2219
|
+
style: `width:${el.offsetWidth}px;height:${el.offsetHeight}px;background:#f0f0f0;`
|
|
2220
|
+
}
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
const tagName = el.tagName.toLowerCase();
|
|
2224
|
+
const attributes = {};
|
|
2225
|
+
if (tagName === "script" || tagName === "noscript") {
|
|
2226
|
+
return { id, type: "element", tagName, attributes: {} };
|
|
2227
|
+
}
|
|
2228
|
+
for (const attr of Array.from(el.attributes)) {
|
|
2229
|
+
if (attr.name.startsWith("on")) continue;
|
|
2230
|
+
if (attr.name === "value" && options.maskInputs && isInputElement(el)) {
|
|
2231
|
+
attributes[attr.name] = "***";
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
attributes[attr.name] = attr.value;
|
|
2235
|
+
}
|
|
2236
|
+
if (options.maskInputs && isInputElement(el)) {
|
|
2237
|
+
const input = el;
|
|
2238
|
+
if (input.type !== "checkbox" && input.type !== "radio" && input.type !== "submit" && input.type !== "button") {
|
|
2239
|
+
attributes["value"] = "***";
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
const children = [];
|
|
2243
|
+
if (tagName !== "style") {
|
|
2244
|
+
for (const child of Array.from(el.childNodes)) {
|
|
2245
|
+
const serialized = serializeNode(child, options);
|
|
2246
|
+
if (serialized) {
|
|
2247
|
+
children.push(serialized);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
} else {
|
|
2251
|
+
const textContent = el.textContent || "";
|
|
2252
|
+
if (textContent) {
|
|
2253
|
+
children.push({
|
|
2254
|
+
id: getNodeId(el.firstChild || el),
|
|
2255
|
+
type: "text",
|
|
2256
|
+
textContent
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return {
|
|
2261
|
+
id,
|
|
2262
|
+
type: "element",
|
|
2263
|
+
tagName,
|
|
2264
|
+
attributes,
|
|
2265
|
+
children,
|
|
2266
|
+
isSVG: el instanceof SVGElement
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
return null;
|
|
2270
|
+
}
|
|
2271
|
+
function takeSnapshot(options) {
|
|
2272
|
+
if (typeof document === "undefined") return null;
|
|
2273
|
+
resetNodeIds();
|
|
2274
|
+
const html = serializeNode(document.documentElement, options);
|
|
2275
|
+
if (!html) return null;
|
|
2276
|
+
return {
|
|
2277
|
+
doctype: document.doctype ? `<!DOCTYPE ${document.doctype.name}>` : void 0,
|
|
2278
|
+
html,
|
|
2279
|
+
width: window.innerWidth,
|
|
2280
|
+
height: window.innerHeight
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
function isInputElement(el) {
|
|
2284
|
+
const tag = el.tagName.toLowerCase();
|
|
2285
|
+
return tag === "input" || tag === "textarea" || tag === "select";
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// src/replay/network.ts
|
|
2289
|
+
var REDACTED_HEADERS = [
|
|
2290
|
+
"authorization",
|
|
2291
|
+
"cookie",
|
|
2292
|
+
"set-cookie",
|
|
2293
|
+
"x-api-key",
|
|
2294
|
+
"x-auth-token"
|
|
2295
|
+
];
|
|
2296
|
+
var NetworkRecorder = class {
|
|
2297
|
+
constructor(callback, options) {
|
|
2298
|
+
this.originalFetch = null;
|
|
2299
|
+
this.originalXhrOpen = null;
|
|
2300
|
+
this.originalXhrSend = null;
|
|
2301
|
+
this.active = false;
|
|
2302
|
+
this.callback = callback;
|
|
2303
|
+
this.captureHeaders = options?.captureHeaders ?? false;
|
|
2304
|
+
this.urlFilter = options?.urlFilter;
|
|
2305
|
+
}
|
|
2306
|
+
start() {
|
|
2307
|
+
if (this.active || typeof window === "undefined") return;
|
|
2308
|
+
this.active = true;
|
|
2309
|
+
this.interceptFetch();
|
|
2310
|
+
this.interceptXHR();
|
|
2311
|
+
}
|
|
2312
|
+
stop() {
|
|
2313
|
+
if (!this.active) return;
|
|
2314
|
+
this.active = false;
|
|
2315
|
+
if (this.originalFetch) {
|
|
2316
|
+
window.fetch = this.originalFetch;
|
|
2317
|
+
this.originalFetch = null;
|
|
2318
|
+
}
|
|
2319
|
+
if (this.originalXhrOpen) {
|
|
2320
|
+
XMLHttpRequest.prototype.open = this.originalXhrOpen;
|
|
2321
|
+
this.originalXhrOpen = null;
|
|
2322
|
+
}
|
|
2323
|
+
if (this.originalXhrSend) {
|
|
2324
|
+
XMLHttpRequest.prototype.send = this.originalXhrSend;
|
|
2325
|
+
this.originalXhrSend = null;
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
interceptFetch() {
|
|
2329
|
+
if (typeof window.fetch !== "function") return;
|
|
2330
|
+
this.originalFetch = window.fetch;
|
|
2331
|
+
const self = this;
|
|
2332
|
+
window.fetch = async function(input, init2) {
|
|
2333
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2334
|
+
if (self.urlFilter && !self.urlFilter(url)) {
|
|
2335
|
+
return self.originalFetch.call(window, input, init2);
|
|
2336
|
+
}
|
|
2337
|
+
const startTime = Date.now();
|
|
2338
|
+
const method = init2?.method || (input instanceof Request ? input.method : "GET");
|
|
2339
|
+
let requestSize;
|
|
2340
|
+
if (init2?.body) {
|
|
2341
|
+
if (typeof init2.body === "string") {
|
|
2342
|
+
requestSize = init2.body.length;
|
|
2343
|
+
} else if (init2.body instanceof Blob) {
|
|
2344
|
+
requestSize = init2.body.size;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
try {
|
|
2348
|
+
const response = await self.originalFetch.call(window, input, init2);
|
|
2349
|
+
const duration = Date.now() - startTime;
|
|
2350
|
+
const data = {
|
|
2351
|
+
method: method.toUpperCase(),
|
|
2352
|
+
url: self.truncateUrl(url),
|
|
2353
|
+
status: response.status,
|
|
2354
|
+
statusText: response.statusText,
|
|
2355
|
+
duration,
|
|
2356
|
+
requestSize,
|
|
2357
|
+
initiator: "fetch"
|
|
2358
|
+
};
|
|
2359
|
+
if (self.captureHeaders) {
|
|
2360
|
+
data.requestHeaders = self.safeHeaders(init2?.headers);
|
|
2361
|
+
data.responseHeaders = self.headersToObject(response.headers);
|
|
2362
|
+
}
|
|
2363
|
+
self.emit(data);
|
|
2364
|
+
return response;
|
|
2365
|
+
} catch (err) {
|
|
2366
|
+
const duration = Date.now() - startTime;
|
|
2367
|
+
self.emit({
|
|
2368
|
+
method: method.toUpperCase(),
|
|
2369
|
+
url: self.truncateUrl(url),
|
|
2370
|
+
duration,
|
|
2371
|
+
requestSize,
|
|
2372
|
+
error: err instanceof Error ? err.message : "Network error",
|
|
2373
|
+
initiator: "fetch"
|
|
2374
|
+
});
|
|
2375
|
+
throw err;
|
|
2376
|
+
}
|
|
2377
|
+
};
|
|
2378
|
+
}
|
|
2379
|
+
interceptXHR() {
|
|
2380
|
+
if (typeof XMLHttpRequest === "undefined") return;
|
|
2381
|
+
this.originalXhrOpen = XMLHttpRequest.prototype.open;
|
|
2382
|
+
this.originalXhrSend = XMLHttpRequest.prototype.send;
|
|
2383
|
+
const self = this;
|
|
2384
|
+
XMLHttpRequest.prototype.open = function(method, url, ...args) {
|
|
2385
|
+
this._spMethod = method;
|
|
2386
|
+
this._spUrl = typeof url === "string" ? url : url.toString();
|
|
2387
|
+
return self.originalXhrOpen.apply(this, [method, url, ...args]);
|
|
2388
|
+
};
|
|
2389
|
+
XMLHttpRequest.prototype.send = function(body) {
|
|
2390
|
+
const xhr = this;
|
|
2391
|
+
const url = xhr._spUrl;
|
|
2392
|
+
if (self.urlFilter && !self.urlFilter(url)) {
|
|
2393
|
+
return self.originalXhrSend.call(this, body);
|
|
2394
|
+
}
|
|
2395
|
+
const startTime = Date.now();
|
|
2396
|
+
let requestSize;
|
|
2397
|
+
if (typeof body === "string") {
|
|
2398
|
+
requestSize = body.length;
|
|
2399
|
+
}
|
|
2400
|
+
const onLoadEnd = () => {
|
|
2401
|
+
const duration = Date.now() - startTime;
|
|
2402
|
+
const data = {
|
|
2403
|
+
method: (xhr._spMethod || "GET").toUpperCase(),
|
|
2404
|
+
url: self.truncateUrl(url),
|
|
2405
|
+
status: xhr.status,
|
|
2406
|
+
statusText: xhr.statusText,
|
|
2407
|
+
duration,
|
|
2408
|
+
requestSize,
|
|
2409
|
+
responseSize: xhr.responseText?.length,
|
|
2410
|
+
initiator: "xhr"
|
|
2411
|
+
};
|
|
2412
|
+
if (xhr.status === 0) {
|
|
2413
|
+
data.error = "Network error";
|
|
2414
|
+
}
|
|
2415
|
+
self.emit(data);
|
|
2416
|
+
xhr.removeEventListener("loadend", onLoadEnd);
|
|
2417
|
+
};
|
|
2418
|
+
xhr.addEventListener("loadend", onLoadEnd);
|
|
2419
|
+
return self.originalXhrSend.call(this, body);
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
emit(data) {
|
|
2423
|
+
if (!this.active) return;
|
|
2424
|
+
this.callback({
|
|
2425
|
+
type: "network",
|
|
2426
|
+
timestamp: Date.now(),
|
|
2427
|
+
data
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
truncateUrl(url) {
|
|
2431
|
+
try {
|
|
2432
|
+
const parsed = new URL(url, window.location.origin);
|
|
2433
|
+
const path = parsed.pathname + parsed.search;
|
|
2434
|
+
return path.length > 500 ? path.slice(0, 500) + "..." : parsed.origin + path;
|
|
2435
|
+
} catch {
|
|
2436
|
+
return url.length > 500 ? url.slice(0, 500) + "..." : url;
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
safeHeaders(headers) {
|
|
2440
|
+
if (!headers) return void 0;
|
|
2441
|
+
const result = {};
|
|
2442
|
+
if (headers instanceof Headers) {
|
|
2443
|
+
headers.forEach((value, key) => {
|
|
2444
|
+
result[key] = REDACTED_HEADERS.includes(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
2445
|
+
});
|
|
2446
|
+
} else if (Array.isArray(headers)) {
|
|
2447
|
+
for (const [key, value] of headers) {
|
|
2448
|
+
result[key] = REDACTED_HEADERS.includes(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
2449
|
+
}
|
|
2450
|
+
} else {
|
|
2451
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
2452
|
+
result[key] = REDACTED_HEADERS.includes(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
return result;
|
|
2456
|
+
}
|
|
2457
|
+
headersToObject(headers) {
|
|
2458
|
+
const result = {};
|
|
2459
|
+
headers.forEach((value, key) => {
|
|
2460
|
+
result[key] = REDACTED_HEADERS.includes(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
2461
|
+
});
|
|
2462
|
+
return result;
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
|
|
2466
|
+
// src/replay/console.ts
|
|
2467
|
+
var MAX_ARG_LENGTH = 1e3;
|
|
2468
|
+
var MAX_ARGS = 10;
|
|
2469
|
+
var MAX_TRACE_LENGTH = 2e3;
|
|
2470
|
+
var ConsoleRecorder = class {
|
|
2471
|
+
constructor(callback, options) {
|
|
2472
|
+
this.originals = {};
|
|
2473
|
+
this.active = false;
|
|
2474
|
+
this.callback = callback;
|
|
2475
|
+
this.levels = options?.levels || ["log", "info", "warn", "error", "debug"];
|
|
2476
|
+
}
|
|
2477
|
+
start() {
|
|
2478
|
+
if (this.active || typeof console === "undefined") return;
|
|
2479
|
+
this.active = true;
|
|
2480
|
+
for (const level of this.levels) {
|
|
2481
|
+
this.originals[level] = console[level].bind(console);
|
|
2482
|
+
console[level] = (...args) => {
|
|
2483
|
+
this.originals[level](...args);
|
|
2484
|
+
this.record(level, args);
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
stop() {
|
|
2489
|
+
if (!this.active) return;
|
|
2490
|
+
this.active = false;
|
|
2491
|
+
for (const level of this.levels) {
|
|
2492
|
+
if (this.originals[level]) {
|
|
2493
|
+
console[level] = this.originals[level];
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
this.originals = {};
|
|
2497
|
+
}
|
|
2498
|
+
record(level, args) {
|
|
2499
|
+
if (!this.active) return;
|
|
2500
|
+
if (args.length > 0 && typeof args[0] === "string" && args[0].startsWith("[SitePong")) {
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const serializedArgs = args.slice(0, MAX_ARGS).map((arg) => this.serialize(arg));
|
|
2504
|
+
const data = {
|
|
2505
|
+
level,
|
|
2506
|
+
args: serializedArgs
|
|
2507
|
+
};
|
|
2508
|
+
if (level === "error") {
|
|
2509
|
+
const trace = this.getStackTrace();
|
|
2510
|
+
if (trace) {
|
|
2511
|
+
data.trace = trace.slice(0, MAX_TRACE_LENGTH);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
this.callback({
|
|
2515
|
+
type: "console",
|
|
2516
|
+
timestamp: Date.now(),
|
|
2517
|
+
data
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
serialize(value) {
|
|
2521
|
+
if (value === null) return "null";
|
|
2522
|
+
if (value === void 0) return "undefined";
|
|
2523
|
+
if (value instanceof Error) {
|
|
2524
|
+
return `Error: ${value.message}${value.stack ? "\n" + value.stack : ""}`.slice(0, MAX_ARG_LENGTH);
|
|
2525
|
+
}
|
|
2526
|
+
if (typeof value === "string") {
|
|
2527
|
+
return value.slice(0, MAX_ARG_LENGTH);
|
|
2528
|
+
}
|
|
2529
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
2530
|
+
return String(value);
|
|
2531
|
+
}
|
|
2532
|
+
if (typeof value === "function") {
|
|
2533
|
+
return `[Function: ${value.name || "anonymous"}]`;
|
|
2534
|
+
}
|
|
2535
|
+
if (typeof value === "symbol") {
|
|
2536
|
+
return value.toString();
|
|
2537
|
+
}
|
|
2538
|
+
if (value instanceof HTMLElement) {
|
|
2539
|
+
return `<${value.tagName.toLowerCase()}${value.id ? "#" + value.id : ""}${value.className ? "." + value.className.split(" ").join(".") : ""}>`;
|
|
2540
|
+
}
|
|
2541
|
+
try {
|
|
2542
|
+
const str = JSON.stringify(value, this.getReplacer(), 0);
|
|
2543
|
+
return str ? str.slice(0, MAX_ARG_LENGTH) : String(value);
|
|
2544
|
+
} catch {
|
|
2545
|
+
return String(value).slice(0, MAX_ARG_LENGTH);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
getReplacer() {
|
|
2549
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2550
|
+
return (_key, value) => {
|
|
2551
|
+
if (typeof value === "object" && value !== null) {
|
|
2552
|
+
if (seen.has(value)) return "[Circular]";
|
|
2553
|
+
seen.add(value);
|
|
2554
|
+
}
|
|
2555
|
+
if (typeof value === "function") return "[Function]";
|
|
2556
|
+
if (value instanceof RegExp) return value.toString();
|
|
2557
|
+
return value;
|
|
2558
|
+
};
|
|
2559
|
+
}
|
|
2560
|
+
getStackTrace() {
|
|
2561
|
+
try {
|
|
2562
|
+
const err = new Error();
|
|
2563
|
+
if (!err.stack) return void 0;
|
|
2564
|
+
const lines = err.stack.split("\n");
|
|
2565
|
+
return lines.slice(4).join("\n") || void 0;
|
|
2566
|
+
} catch {
|
|
2567
|
+
return void 0;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
|
|
2572
|
+
// src/replay/manager.ts
|
|
2573
|
+
var DEFAULT_ENDPOINT4 = "https://ingest.sitepong.com";
|
|
2574
|
+
var DEFAULT_FLUSH_INTERVAL2 = 5e3;
|
|
2575
|
+
var DEFAULT_MAX_BATCH_SIZE = 100;
|
|
2576
|
+
var DEFAULT_MAX_SESSION_DURATION = 60 * 60 * 1e3;
|
|
2577
|
+
var ReplayManager = class {
|
|
2578
|
+
constructor(config) {
|
|
2579
|
+
this.recording = false;
|
|
2580
|
+
this.sessionId = null;
|
|
2581
|
+
this.events = [];
|
|
2582
|
+
this.flushTimer = null;
|
|
2583
|
+
this.sessionStartTime = 0;
|
|
2584
|
+
this.observer = null;
|
|
2585
|
+
this.mouseMoveBuffer = [];
|
|
2586
|
+
this.mouseMoveFlushTimer = null;
|
|
2587
|
+
this.cleanupFns = [];
|
|
2588
|
+
this.networkRecorder = null;
|
|
2589
|
+
this.consoleRecorder = null;
|
|
2590
|
+
this.lastSnapshotTime = 0;
|
|
2591
|
+
this.snapshotCooldown = 500;
|
|
2592
|
+
// min ms between interaction snapshots
|
|
2593
|
+
this.userContext = null;
|
|
2594
|
+
this.config = {
|
|
2595
|
+
endpoint: DEFAULT_ENDPOINT4,
|
|
2596
|
+
enabled: true,
|
|
2597
|
+
maskInputs: true,
|
|
2598
|
+
maxSessionDuration: DEFAULT_MAX_SESSION_DURATION,
|
|
2599
|
+
flushInterval: DEFAULT_FLUSH_INTERVAL2,
|
|
2600
|
+
maxBatchSize: DEFAULT_MAX_BATCH_SIZE,
|
|
2601
|
+
sampleRate: 1,
|
|
2602
|
+
debug: false,
|
|
2603
|
+
...config
|
|
2604
|
+
};
|
|
2605
|
+
}
|
|
2606
|
+
start() {
|
|
2607
|
+
if (typeof window === "undefined") return false;
|
|
2608
|
+
if (this.recording) return true;
|
|
2609
|
+
if (!this.config.enabled) return false;
|
|
2610
|
+
if (Math.random() > this.config.sampleRate) {
|
|
2611
|
+
this.log("Session not sampled");
|
|
2612
|
+
return false;
|
|
2613
|
+
}
|
|
2614
|
+
this.sessionId = this.getOrCreateSessionId();
|
|
2615
|
+
this.sessionStartTime = this.getOrCreateStartTime();
|
|
2616
|
+
this.recording = true;
|
|
2617
|
+
this.events = [];
|
|
2618
|
+
const snapshot = takeSnapshot({
|
|
2619
|
+
maskInputs: this.config.maskInputs,
|
|
2620
|
+
blockSelector: this.config.blockSelector,
|
|
2621
|
+
maskSelector: this.config.maskSelector
|
|
2622
|
+
});
|
|
2623
|
+
if (snapshot) {
|
|
2624
|
+
this.addEvent("snapshot", snapshot);
|
|
2625
|
+
}
|
|
2626
|
+
this.setupMutationObserver();
|
|
2627
|
+
this.setupEventListeners();
|
|
2628
|
+
this.setupNetworkRecording();
|
|
2629
|
+
this.setupConsoleRecording();
|
|
2630
|
+
this.startFlushTimer();
|
|
2631
|
+
this.log("Recording started:", this.sessionId);
|
|
2632
|
+
return true;
|
|
2633
|
+
}
|
|
2634
|
+
stop() {
|
|
2635
|
+
if (!this.recording) return;
|
|
2636
|
+
this.recording = false;
|
|
2637
|
+
this.flush();
|
|
2638
|
+
if (this.observer) {
|
|
2639
|
+
this.observer.disconnect();
|
|
2640
|
+
this.observer = null;
|
|
2641
|
+
}
|
|
2642
|
+
if (this.flushTimer) {
|
|
2643
|
+
clearInterval(this.flushTimer);
|
|
2644
|
+
this.flushTimer = null;
|
|
2645
|
+
}
|
|
2646
|
+
if (this.mouseMoveFlushTimer) {
|
|
2647
|
+
clearTimeout(this.mouseMoveFlushTimer);
|
|
2648
|
+
this.mouseMoveFlushTimer = null;
|
|
2649
|
+
}
|
|
2650
|
+
if (this.networkRecorder) {
|
|
2651
|
+
this.networkRecorder.stop();
|
|
2652
|
+
this.networkRecorder = null;
|
|
2653
|
+
}
|
|
2654
|
+
if (this.consoleRecorder) {
|
|
2655
|
+
this.consoleRecorder.stop();
|
|
2656
|
+
this.consoleRecorder = null;
|
|
2657
|
+
}
|
|
2658
|
+
for (const cleanup of this.cleanupFns) {
|
|
2659
|
+
cleanup();
|
|
2660
|
+
}
|
|
2661
|
+
this.cleanupFns = [];
|
|
2662
|
+
this.log("Recording stopped:", this.sessionId);
|
|
2663
|
+
this.sessionId = null;
|
|
2664
|
+
}
|
|
2665
|
+
isRecording() {
|
|
2666
|
+
return this.recording;
|
|
2667
|
+
}
|
|
2668
|
+
getSessionId() {
|
|
2669
|
+
return this.sessionId;
|
|
2670
|
+
}
|
|
2671
|
+
setUser(user) {
|
|
2672
|
+
this.userContext = user;
|
|
2673
|
+
}
|
|
2674
|
+
addEvent(type, data) {
|
|
2675
|
+
if (!this.recording) return;
|
|
2676
|
+
if (Date.now() - this.sessionStartTime > this.config.maxSessionDuration) {
|
|
2677
|
+
this.stop();
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
this.events.push({
|
|
2681
|
+
type,
|
|
2682
|
+
timestamp: Date.now() - this.sessionStartTime,
|
|
2683
|
+
data
|
|
2684
|
+
});
|
|
2685
|
+
if (this.events.length >= this.config.maxBatchSize) {
|
|
2686
|
+
this.flush();
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
setupMutationObserver() {
|
|
2690
|
+
if (typeof MutationObserver === "undefined") return;
|
|
2691
|
+
this.observer = new MutationObserver((mutations) => {
|
|
2692
|
+
const mutationData = {
|
|
2693
|
+
adds: [],
|
|
2694
|
+
removes: [],
|
|
2695
|
+
attributes: [],
|
|
2696
|
+
texts: []
|
|
2697
|
+
};
|
|
2698
|
+
for (const mutation of mutations) {
|
|
2699
|
+
switch (mutation.type) {
|
|
2700
|
+
case "childList": {
|
|
2701
|
+
for (const node of Array.from(mutation.addedNodes)) {
|
|
2702
|
+
const serialized = serializeNode(node, {
|
|
2703
|
+
maskInputs: this.config.maskInputs,
|
|
2704
|
+
blockSelector: this.config.blockSelector,
|
|
2705
|
+
maskSelector: this.config.maskSelector
|
|
2706
|
+
});
|
|
2707
|
+
if (serialized) {
|
|
2708
|
+
mutationData.adds.push({
|
|
2709
|
+
parentId: getNodeId(mutation.target),
|
|
2710
|
+
nextId: mutation.nextSibling ? getNodeId(mutation.nextSibling) : null,
|
|
2711
|
+
node: serialized
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
for (const node of Array.from(mutation.removedNodes)) {
|
|
2716
|
+
mutationData.removes.push({
|
|
2717
|
+
parentId: getNodeId(mutation.target),
|
|
2718
|
+
id: getNodeId(node)
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
break;
|
|
2722
|
+
}
|
|
2723
|
+
case "attributes": {
|
|
2724
|
+
const el = mutation.target;
|
|
2725
|
+
const value = el.getAttribute(mutation.attributeName || "");
|
|
2726
|
+
mutationData.attributes.push({
|
|
2727
|
+
id: getNodeId(mutation.target),
|
|
2728
|
+
attributes: { [mutation.attributeName || ""]: value }
|
|
2729
|
+
});
|
|
2730
|
+
break;
|
|
2731
|
+
}
|
|
2732
|
+
case "characterData": {
|
|
2733
|
+
let textValue = mutation.target.textContent || "";
|
|
2734
|
+
const parent = mutation.target.parentElement;
|
|
2735
|
+
if (parent && this.config.maskSelector && parent.matches(this.config.maskSelector)) {
|
|
2736
|
+
textValue = textValue.replace(/\S/g, "*");
|
|
2737
|
+
}
|
|
2738
|
+
mutationData.texts.push({
|
|
2739
|
+
id: getNodeId(mutation.target),
|
|
2740
|
+
value: textValue
|
|
2741
|
+
});
|
|
2742
|
+
break;
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
if (mutationData.adds.length || mutationData.removes.length || mutationData.attributes.length || mutationData.texts.length) {
|
|
2747
|
+
this.addEvent("mutation", mutationData);
|
|
2748
|
+
}
|
|
2749
|
+
});
|
|
2750
|
+
this.observer.observe(document.documentElement, {
|
|
2751
|
+
childList: true,
|
|
2752
|
+
subtree: true,
|
|
2753
|
+
attributes: true,
|
|
2754
|
+
characterData: true,
|
|
2755
|
+
attributeOldValue: false
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
setupEventListeners() {
|
|
2759
|
+
const onMouseMove = (e) => {
|
|
2760
|
+
this.mouseMoveBuffer.push({
|
|
2761
|
+
x: e.clientX,
|
|
2762
|
+
y: e.clientY,
|
|
2763
|
+
t: Date.now() - this.sessionStartTime
|
|
2764
|
+
});
|
|
2765
|
+
if (!this.mouseMoveFlushTimer) {
|
|
2766
|
+
this.mouseMoveFlushTimer = setTimeout(() => {
|
|
2767
|
+
if (this.mouseMoveBuffer.length > 0) {
|
|
2768
|
+
const data = { positions: [...this.mouseMoveBuffer] };
|
|
2769
|
+
this.addEvent("mouse_move", data);
|
|
2770
|
+
this.mouseMoveBuffer = [];
|
|
2771
|
+
}
|
|
2772
|
+
this.mouseMoveFlushTimer = null;
|
|
2773
|
+
}, 50);
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
const onClick = (e) => {
|
|
2777
|
+
const target = e.target;
|
|
2778
|
+
this.addEvent("mouse_click", {
|
|
2779
|
+
x: e.clientX,
|
|
2780
|
+
y: e.clientY,
|
|
2781
|
+
targetId: getNodeId(target),
|
|
2782
|
+
button: e.button
|
|
2783
|
+
});
|
|
2784
|
+
this.takeInteractionSnapshot();
|
|
2785
|
+
};
|
|
2786
|
+
const onContextMenu = (e) => {
|
|
2787
|
+
const target = e.target;
|
|
2788
|
+
this.addEvent("mouse_click", {
|
|
2789
|
+
x: e.clientX,
|
|
2790
|
+
y: e.clientY,
|
|
2791
|
+
targetId: getNodeId(target),
|
|
2792
|
+
button: 2
|
|
2793
|
+
});
|
|
2794
|
+
this.takeInteractionSnapshot();
|
|
2795
|
+
};
|
|
2796
|
+
const onMouseLeave = () => {
|
|
2797
|
+
this.addEvent("mouse_leave", {});
|
|
2798
|
+
};
|
|
2799
|
+
const onTouchStart = (e) => {
|
|
2800
|
+
const touch = e.touches[0];
|
|
2801
|
+
if (!touch) return;
|
|
2802
|
+
const target = e.target;
|
|
2803
|
+
this.addEvent("touch", {
|
|
2804
|
+
x: touch.clientX,
|
|
2805
|
+
y: touch.clientY,
|
|
2806
|
+
targetId: getNodeId(target)
|
|
2807
|
+
});
|
|
2808
|
+
this.takeInteractionSnapshot();
|
|
2809
|
+
};
|
|
2810
|
+
let scrollSnapshotTimer = null;
|
|
2811
|
+
const onScroll = () => {
|
|
2812
|
+
const data = {
|
|
2813
|
+
id: 0,
|
|
2814
|
+
// document scroll
|
|
2815
|
+
x: window.scrollX,
|
|
2816
|
+
y: window.scrollY
|
|
2817
|
+
};
|
|
2818
|
+
this.addEvent("scroll", data);
|
|
2819
|
+
if (scrollSnapshotTimer) clearTimeout(scrollSnapshotTimer);
|
|
2820
|
+
scrollSnapshotTimer = setTimeout(() => {
|
|
2821
|
+
this.takeInteractionSnapshot();
|
|
2822
|
+
}, 250);
|
|
2823
|
+
};
|
|
2824
|
+
const onInput = (e) => {
|
|
2825
|
+
const target = e.target;
|
|
2826
|
+
if (!target) return;
|
|
2827
|
+
const data = {
|
|
2828
|
+
id: getNodeId(target),
|
|
2829
|
+
value: this.config.maskInputs ? "***" : target.value
|
|
2830
|
+
};
|
|
2831
|
+
if ("checked" in target) {
|
|
2832
|
+
data.isChecked = target.checked;
|
|
2833
|
+
}
|
|
2834
|
+
this.addEvent("input", data);
|
|
2835
|
+
};
|
|
2836
|
+
const onResize = () => {
|
|
2837
|
+
this.addEvent("resize", {
|
|
2838
|
+
width: window.innerWidth,
|
|
2839
|
+
height: window.innerHeight
|
|
2840
|
+
});
|
|
2841
|
+
};
|
|
2842
|
+
const onFocus = (e) => {
|
|
2843
|
+
if (e.target && e.target !== document) {
|
|
2844
|
+
this.addEvent("focus", { id: getNodeId(e.target) });
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
const onBlur = (e) => {
|
|
2848
|
+
if (e.target && e.target !== document) {
|
|
2849
|
+
this.addEvent("blur", { id: getNodeId(e.target) });
|
|
2850
|
+
}
|
|
2851
|
+
};
|
|
2852
|
+
const onVisibilityChange = () => {
|
|
2853
|
+
if (document.visibilityState === "hidden") {
|
|
2854
|
+
this.flush();
|
|
2855
|
+
}
|
|
2856
|
+
};
|
|
2857
|
+
document.addEventListener("mousemove", onMouseMove, { passive: true });
|
|
2858
|
+
document.addEventListener("click", onClick, { passive: true });
|
|
2859
|
+
document.addEventListener("contextmenu", onContextMenu, { passive: true });
|
|
2860
|
+
document.documentElement.addEventListener("mouseleave", onMouseLeave, { passive: true });
|
|
2861
|
+
document.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
2862
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
2863
|
+
document.addEventListener("input", onInput, { passive: true });
|
|
2864
|
+
window.addEventListener("resize", onResize, { passive: true });
|
|
2865
|
+
document.addEventListener("focus", onFocus, { passive: true, capture: true });
|
|
2866
|
+
document.addEventListener("blur", onBlur, { passive: true, capture: true });
|
|
2867
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2868
|
+
this.cleanupFns.push(
|
|
2869
|
+
() => document.removeEventListener("mousemove", onMouseMove),
|
|
2870
|
+
() => document.removeEventListener("click", onClick),
|
|
2871
|
+
() => document.removeEventListener("contextmenu", onContextMenu),
|
|
2872
|
+
() => document.documentElement.removeEventListener("mouseleave", onMouseLeave),
|
|
2873
|
+
() => document.removeEventListener("touchstart", onTouchStart),
|
|
2874
|
+
() => window.removeEventListener("scroll", onScroll),
|
|
2875
|
+
() => document.removeEventListener("input", onInput),
|
|
2876
|
+
() => window.removeEventListener("resize", onResize),
|
|
2877
|
+
() => document.removeEventListener("focus", onFocus, { capture: true }),
|
|
2878
|
+
() => document.removeEventListener("blur", onBlur, { capture: true }),
|
|
2879
|
+
() => document.removeEventListener("visibilitychange", onVisibilityChange)
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
setupNetworkRecording() {
|
|
2883
|
+
if (!this.config.recordNetwork) return;
|
|
2884
|
+
const sdkEndpoint = this.config.replayEndpoint || this.config.endpoint;
|
|
2885
|
+
this.networkRecorder = new NetworkRecorder(
|
|
2886
|
+
(event) => {
|
|
2887
|
+
this.addEvent("network", event.data);
|
|
2888
|
+
},
|
|
2889
|
+
{
|
|
2890
|
+
captureHeaders: this.config.captureNetworkHeaders,
|
|
2891
|
+
// Skip recording our own SDK API calls
|
|
2892
|
+
urlFilter: (url) => !url.startsWith(sdkEndpoint)
|
|
2893
|
+
}
|
|
2894
|
+
);
|
|
2895
|
+
this.networkRecorder.start();
|
|
2896
|
+
}
|
|
2897
|
+
setupConsoleRecording() {
|
|
2898
|
+
if (!this.config.recordConsole) return;
|
|
2899
|
+
this.consoleRecorder = new ConsoleRecorder(
|
|
2900
|
+
(event) => {
|
|
2901
|
+
this.addEvent("console", event.data);
|
|
2902
|
+
},
|
|
2903
|
+
{
|
|
2904
|
+
levels: this.config.consoleLevels
|
|
2905
|
+
}
|
|
2906
|
+
);
|
|
2907
|
+
this.consoleRecorder.start();
|
|
2908
|
+
}
|
|
2909
|
+
startFlushTimer() {
|
|
2910
|
+
this.flushTimer = setInterval(() => {
|
|
2911
|
+
this.flush();
|
|
2912
|
+
}, this.config.flushInterval);
|
|
2913
|
+
}
|
|
2914
|
+
async flush() {
|
|
2915
|
+
if (this.events.length === 0 || !this.sessionId) return;
|
|
2916
|
+
const events = [...this.events];
|
|
2917
|
+
this.events = [];
|
|
2918
|
+
const endpoint = this.config.replayEndpoint || `${this.config.endpoint}/api/replays`;
|
|
2919
|
+
try {
|
|
2920
|
+
const response = await fetch(endpoint, {
|
|
2921
|
+
method: "POST",
|
|
2922
|
+
headers: {
|
|
2923
|
+
"Content-Type": "application/json",
|
|
2924
|
+
"X-API-Key": this.config.apiKey
|
|
2925
|
+
},
|
|
2926
|
+
body: JSON.stringify({
|
|
2927
|
+
sessionId: this.sessionId,
|
|
2928
|
+
events,
|
|
2929
|
+
startedAt: new Date(this.sessionStartTime).toISOString(),
|
|
2930
|
+
pageUrl: typeof location !== "undefined" ? location.href : void 0,
|
|
2931
|
+
pageTitle: typeof document !== "undefined" ? document.title : void 0,
|
|
2932
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
2933
|
+
userId: this.userContext?.id
|
|
2934
|
+
})
|
|
2935
|
+
});
|
|
2936
|
+
if (!response.ok) {
|
|
2937
|
+
throw new Error(`HTTP ${response.status}`);
|
|
2938
|
+
}
|
|
2939
|
+
this.log(`Flushed ${events.length} replay events`);
|
|
2940
|
+
} catch (err) {
|
|
2941
|
+
this.events.unshift(...events);
|
|
2942
|
+
this.log("Failed to flush replay events:", err);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
takeInteractionSnapshot() {
|
|
2946
|
+
const now = Date.now();
|
|
2947
|
+
if (now - this.lastSnapshotTime < this.snapshotCooldown) return;
|
|
2948
|
+
this.lastSnapshotTime = now;
|
|
2949
|
+
const snapshot = takeSnapshot({
|
|
2950
|
+
maskInputs: this.config.maskInputs,
|
|
2951
|
+
blockSelector: this.config.blockSelector,
|
|
2952
|
+
maskSelector: this.config.maskSelector
|
|
2953
|
+
});
|
|
2954
|
+
if (snapshot) {
|
|
2955
|
+
this.addEvent("snapshot", snapshot);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
getOrCreateSessionId() {
|
|
2959
|
+
const storageKey = "__sp_replay_session";
|
|
2960
|
+
try {
|
|
2961
|
+
const existing = sessionStorage.getItem(storageKey);
|
|
2962
|
+
if (existing) return existing;
|
|
2963
|
+
} catch {
|
|
2964
|
+
}
|
|
2965
|
+
const id = `rep_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
2966
|
+
try {
|
|
2967
|
+
sessionStorage.setItem(storageKey, id);
|
|
2968
|
+
} catch {
|
|
2969
|
+
}
|
|
2970
|
+
return id;
|
|
2971
|
+
}
|
|
2972
|
+
getOrCreateStartTime() {
|
|
2973
|
+
const storageKey = "__sp_replay_start";
|
|
2974
|
+
try {
|
|
2975
|
+
const existing = sessionStorage.getItem(storageKey);
|
|
2976
|
+
if (existing) return parseInt(existing, 10);
|
|
2977
|
+
} catch {
|
|
2978
|
+
}
|
|
2979
|
+
const now = Date.now();
|
|
2980
|
+
try {
|
|
2981
|
+
sessionStorage.setItem(storageKey, String(now));
|
|
2982
|
+
} catch {
|
|
2983
|
+
}
|
|
2984
|
+
return now;
|
|
2985
|
+
}
|
|
2986
|
+
log(...args) {
|
|
2987
|
+
if (this.config.debug) {
|
|
2988
|
+
console.log("[SitePong Replay]", ...args);
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
};
|
|
2992
|
+
|
|
2993
|
+
// src/performance/manager.ts
|
|
2994
|
+
var DEFAULT_ENDPOINT5 = "https://ingest.sitepong.com";
|
|
2995
|
+
var DEFAULT_FLUSH_INTERVAL3 = 1e4;
|
|
2996
|
+
var PerformanceManager = class {
|
|
2997
|
+
constructor(config) {
|
|
2998
|
+
this.metrics = [];
|
|
2999
|
+
this.flushTimer = null;
|
|
3000
|
+
this.vitals = {};
|
|
3001
|
+
this.activeTransactions = /* @__PURE__ */ new Map();
|
|
3002
|
+
this.config = {
|
|
3003
|
+
endpoint: DEFAULT_ENDPOINT5,
|
|
3004
|
+
enabled: true,
|
|
3005
|
+
webVitals: true,
|
|
3006
|
+
navigationTiming: true,
|
|
3007
|
+
resourceTiming: false,
|
|
3008
|
+
flushInterval: DEFAULT_FLUSH_INTERVAL3,
|
|
3009
|
+
sampleRate: 1,
|
|
3010
|
+
debug: false,
|
|
3011
|
+
...config
|
|
3012
|
+
};
|
|
3013
|
+
this.sampled = Math.random() <= this.config.sampleRate;
|
|
3014
|
+
}
|
|
3015
|
+
init() {
|
|
3016
|
+
if (!this.config.enabled || !this.sampled) return;
|
|
3017
|
+
if (typeof window === "undefined") return;
|
|
3018
|
+
if (this.config.webVitals) {
|
|
3019
|
+
this.observeWebVitals();
|
|
3020
|
+
}
|
|
3021
|
+
if (this.config.navigationTiming) {
|
|
3022
|
+
this.collectNavigationTiming();
|
|
3023
|
+
}
|
|
3024
|
+
this.startFlushTimer();
|
|
3025
|
+
this.log("Performance monitoring initialized");
|
|
3026
|
+
}
|
|
3027
|
+
/**
|
|
3028
|
+
* Start a custom transaction for measuring operations
|
|
3029
|
+
*/
|
|
3030
|
+
startTransaction(name, data) {
|
|
3031
|
+
const id = `txn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
3032
|
+
const transaction = {
|
|
3033
|
+
id,
|
|
3034
|
+
name,
|
|
3035
|
+
startTime: performance.now(),
|
|
3036
|
+
status: "running",
|
|
3037
|
+
spans: [],
|
|
3038
|
+
data
|
|
3039
|
+
};
|
|
3040
|
+
this.activeTransactions.set(id, transaction);
|
|
3041
|
+
this.log("Transaction started:", name, id);
|
|
3042
|
+
return id;
|
|
3043
|
+
}
|
|
3044
|
+
/**
|
|
3045
|
+
* End a transaction and report it
|
|
3046
|
+
*/
|
|
3047
|
+
endTransaction(id, status = "ok") {
|
|
3048
|
+
const transaction = this.activeTransactions.get(id);
|
|
3049
|
+
if (!transaction) {
|
|
3050
|
+
this.log("Transaction not found:", id);
|
|
3051
|
+
return;
|
|
3052
|
+
}
|
|
3053
|
+
transaction.endTime = performance.now();
|
|
3054
|
+
transaction.duration = transaction.endTime - transaction.startTime;
|
|
3055
|
+
transaction.status = status;
|
|
3056
|
+
for (const span of transaction.spans) {
|
|
3057
|
+
if (span.status === "running") {
|
|
3058
|
+
span.endTime = transaction.endTime;
|
|
3059
|
+
span.duration = span.endTime - span.startTime;
|
|
3060
|
+
span.status = status;
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
this.activeTransactions.delete(id);
|
|
3064
|
+
this.addMetric({
|
|
3065
|
+
type: "transaction",
|
|
3066
|
+
name: transaction.name,
|
|
3067
|
+
value: transaction.duration,
|
|
3068
|
+
unit: "ms",
|
|
3069
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3070
|
+
url: typeof window !== "undefined" ? window.location.href : void 0,
|
|
3071
|
+
transaction
|
|
3072
|
+
});
|
|
3073
|
+
this.log("Transaction ended:", transaction.name, `${transaction.duration.toFixed(1)}ms`);
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Start a span within a transaction
|
|
3077
|
+
*/
|
|
3078
|
+
startSpan(transactionId, name, data) {
|
|
3079
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
3080
|
+
if (!transaction) {
|
|
3081
|
+
this.log("Transaction not found for span:", transactionId);
|
|
3082
|
+
return "";
|
|
3083
|
+
}
|
|
3084
|
+
const span = {
|
|
3085
|
+
id: `span_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
3086
|
+
name,
|
|
3087
|
+
startTime: performance.now(),
|
|
3088
|
+
status: "running",
|
|
3089
|
+
data
|
|
3090
|
+
};
|
|
3091
|
+
transaction.spans.push(span);
|
|
3092
|
+
return span.id;
|
|
3093
|
+
}
|
|
3094
|
+
/**
|
|
3095
|
+
* End a span
|
|
3096
|
+
*/
|
|
3097
|
+
endSpan(transactionId, spanId, status = "ok") {
|
|
3098
|
+
const transaction = this.activeTransactions.get(transactionId);
|
|
3099
|
+
if (!transaction) return;
|
|
3100
|
+
const span = transaction.spans.find((s) => s.id === spanId);
|
|
3101
|
+
if (!span) return;
|
|
3102
|
+
span.endTime = performance.now();
|
|
3103
|
+
span.duration = span.endTime - span.startTime;
|
|
3104
|
+
span.status = status;
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Get current Web Vitals
|
|
3108
|
+
*/
|
|
3109
|
+
getVitals() {
|
|
3110
|
+
return { ...this.vitals };
|
|
3111
|
+
}
|
|
3112
|
+
destroy() {
|
|
3113
|
+
if (this.flushTimer) {
|
|
3114
|
+
clearInterval(this.flushTimer);
|
|
3115
|
+
this.flushTimer = null;
|
|
3116
|
+
}
|
|
3117
|
+
this.flush();
|
|
3118
|
+
}
|
|
3119
|
+
observeWebVitals() {
|
|
3120
|
+
if (typeof PerformanceObserver === "undefined") return;
|
|
3121
|
+
try {
|
|
3122
|
+
const lcpObserver = new PerformanceObserver((list) => {
|
|
3123
|
+
const entries = list.getEntries();
|
|
3124
|
+
const lastEntry = entries[entries.length - 1];
|
|
3125
|
+
if (lastEntry) {
|
|
3126
|
+
this.vitals.lcp = lastEntry.startTime;
|
|
3127
|
+
this.reportVital("LCP", lastEntry.startTime);
|
|
3128
|
+
}
|
|
3129
|
+
});
|
|
3130
|
+
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
|
|
3131
|
+
} catch (e) {
|
|
3132
|
+
}
|
|
3133
|
+
try {
|
|
3134
|
+
const fidObserver = new PerformanceObserver((list) => {
|
|
3135
|
+
const entry = list.getEntries()[0];
|
|
3136
|
+
if (entry) {
|
|
3137
|
+
const fid = entry.processingStart - entry.startTime;
|
|
3138
|
+
this.vitals.fid = fid;
|
|
3139
|
+
this.reportVital("FID", fid);
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
3142
|
+
fidObserver.observe({ type: "first-input", buffered: true });
|
|
3143
|
+
} catch (e) {
|
|
3144
|
+
}
|
|
3145
|
+
try {
|
|
3146
|
+
let clsValue = 0;
|
|
3147
|
+
const clsObserver = new PerformanceObserver((list) => {
|
|
3148
|
+
for (const entry of list.getEntries()) {
|
|
3149
|
+
const layoutShift = entry;
|
|
3150
|
+
if (!layoutShift.hadRecentInput) {
|
|
3151
|
+
clsValue += layoutShift.value;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
this.vitals.cls = clsValue;
|
|
3155
|
+
});
|
|
3156
|
+
clsObserver.observe({ type: "layout-shift", buffered: true });
|
|
3157
|
+
if (typeof document !== "undefined") {
|
|
3158
|
+
document.addEventListener("visibilitychange", () => {
|
|
3159
|
+
if (document.visibilityState === "hidden" && clsValue > 0) {
|
|
3160
|
+
this.reportVital("CLS", clsValue, "score");
|
|
3161
|
+
}
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
} catch (e) {
|
|
3165
|
+
}
|
|
3166
|
+
try {
|
|
3167
|
+
const fcpObserver = new PerformanceObserver((list) => {
|
|
3168
|
+
const entry = list.getEntries().find(
|
|
3169
|
+
(e) => e.name === "first-contentful-paint"
|
|
3170
|
+
);
|
|
3171
|
+
if (entry) {
|
|
3172
|
+
this.vitals.fcp = entry.startTime;
|
|
3173
|
+
this.reportVital("FCP", entry.startTime);
|
|
3174
|
+
}
|
|
3175
|
+
});
|
|
3176
|
+
fcpObserver.observe({ type: "paint", buffered: true });
|
|
3177
|
+
} catch (e) {
|
|
3178
|
+
}
|
|
3179
|
+
try {
|
|
3180
|
+
let maxINP = 0;
|
|
3181
|
+
const inpObserver = new PerformanceObserver((list) => {
|
|
3182
|
+
for (const entry of list.getEntries()) {
|
|
3183
|
+
const duration = entry.duration;
|
|
3184
|
+
if (duration > maxINP) {
|
|
3185
|
+
maxINP = duration;
|
|
3186
|
+
this.vitals.inp = maxINP;
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
3190
|
+
inpObserver.observe({ type: "event", buffered: true });
|
|
3191
|
+
} catch (e) {
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
collectNavigationTiming() {
|
|
3195
|
+
if (typeof window === "undefined" || !window.performance) return;
|
|
3196
|
+
const collect = () => {
|
|
3197
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
3198
|
+
if (!nav) return;
|
|
3199
|
+
const ttfb = nav.responseStart - nav.requestStart;
|
|
3200
|
+
this.vitals.ttfb = ttfb;
|
|
3201
|
+
this.reportVital("TTFB", ttfb);
|
|
3202
|
+
this.addMetric({
|
|
3203
|
+
type: "navigation",
|
|
3204
|
+
name: "page_load",
|
|
3205
|
+
value: nav.loadEventEnd - nav.startTime,
|
|
3206
|
+
unit: "ms",
|
|
3207
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3208
|
+
url: window.location.href,
|
|
3209
|
+
data: {
|
|
3210
|
+
dns: nav.domainLookupEnd - nav.domainLookupStart,
|
|
3211
|
+
tcp: nav.connectEnd - nav.connectStart,
|
|
3212
|
+
ttfb,
|
|
3213
|
+
domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
|
|
3214
|
+
domComplete: nav.domComplete - nav.startTime,
|
|
3215
|
+
transferSize: nav.transferSize,
|
|
3216
|
+
encodedBodySize: nav.encodedBodySize,
|
|
3217
|
+
decodedBodySize: nav.decodedBodySize
|
|
3218
|
+
}
|
|
3219
|
+
});
|
|
3220
|
+
};
|
|
3221
|
+
if (document.readyState === "complete") {
|
|
3222
|
+
setTimeout(collect, 0);
|
|
3223
|
+
} else {
|
|
3224
|
+
window.addEventListener("load", () => setTimeout(collect, 0));
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
reportVital(name, value, unit = "ms") {
|
|
3228
|
+
this.addMetric({
|
|
3229
|
+
type: "web_vital",
|
|
3230
|
+
name,
|
|
3231
|
+
value,
|
|
3232
|
+
unit,
|
|
3233
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3234
|
+
url: typeof window !== "undefined" ? window.location.href : void 0
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
addMetric(metric) {
|
|
3238
|
+
this.metrics.push(metric);
|
|
3239
|
+
}
|
|
3240
|
+
startFlushTimer() {
|
|
3241
|
+
this.flushTimer = setInterval(() => {
|
|
3242
|
+
this.flush();
|
|
3243
|
+
}, this.config.flushInterval);
|
|
3244
|
+
if (typeof document !== "undefined") {
|
|
3245
|
+
document.addEventListener("visibilitychange", () => {
|
|
3246
|
+
if (document.visibilityState === "hidden") {
|
|
3247
|
+
this.flush();
|
|
3248
|
+
}
|
|
3249
|
+
});
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
async flush() {
|
|
3253
|
+
if (this.metrics.length === 0) return;
|
|
3254
|
+
const metrics = [...this.metrics];
|
|
3255
|
+
this.metrics = [];
|
|
3256
|
+
const endpoint = this.config.performanceEndpoint || `${this.config.endpoint}/api/performance`;
|
|
3257
|
+
try {
|
|
3258
|
+
const response = await fetch(endpoint, {
|
|
3259
|
+
method: "POST",
|
|
3260
|
+
headers: {
|
|
3261
|
+
"Content-Type": "application/json",
|
|
3262
|
+
"X-API-Key": this.config.apiKey
|
|
3263
|
+
},
|
|
3264
|
+
body: JSON.stringify({ metrics })
|
|
3265
|
+
});
|
|
3266
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
3267
|
+
this.log(`Flushed ${metrics.length} performance metrics`);
|
|
3268
|
+
} catch (err) {
|
|
3269
|
+
this.metrics.unshift(...metrics);
|
|
3270
|
+
this.log("Failed to flush metrics:", err);
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
log(...args) {
|
|
3274
|
+
if (this.config.debug) {
|
|
3275
|
+
console.log("[SitePong Performance]", ...args);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
|
|
3280
|
+
// src/performance/tracing.ts
|
|
3281
|
+
function randomHex(byteLength) {
|
|
3282
|
+
try {
|
|
3283
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
3284
|
+
const bytes = new Uint8Array(byteLength);
|
|
3285
|
+
crypto.getRandomValues(bytes);
|
|
3286
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3287
|
+
}
|
|
3288
|
+
} catch {
|
|
3289
|
+
}
|
|
3290
|
+
let hex = "";
|
|
3291
|
+
for (let i = 0; i < byteLength; i++) {
|
|
3292
|
+
hex += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
|
|
3293
|
+
}
|
|
3294
|
+
return hex;
|
|
3295
|
+
}
|
|
3296
|
+
function generateTraceId() {
|
|
3297
|
+
return randomHex(16);
|
|
3298
|
+
}
|
|
3299
|
+
function generateSpanId() {
|
|
3300
|
+
return randomHex(8);
|
|
3301
|
+
}
|
|
3302
|
+
function createTraceContext() {
|
|
3303
|
+
return {
|
|
3304
|
+
traceId: generateTraceId(),
|
|
3305
|
+
spanId: generateSpanId(),
|
|
3306
|
+
parentSpanId: null,
|
|
3307
|
+
sampled: true
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
function propagateTrace(context) {
|
|
3311
|
+
const sampledFlag = context.sampled ? "01" : "00";
|
|
3312
|
+
return {
|
|
3313
|
+
traceparent: `00-${context.traceId}-${context.spanId}-${sampledFlag}`,
|
|
3314
|
+
tracestate: `sitepong=${context.spanId}`
|
|
3315
|
+
};
|
|
3316
|
+
}
|
|
3317
|
+
function extractTrace(headers) {
|
|
3318
|
+
const traceparent = headers["traceparent"] || headers["Traceparent"];
|
|
3319
|
+
if (!traceparent) {
|
|
3320
|
+
return null;
|
|
3321
|
+
}
|
|
3322
|
+
const parts = traceparent.split("-");
|
|
3323
|
+
if (parts.length !== 4) {
|
|
3324
|
+
return null;
|
|
3325
|
+
}
|
|
3326
|
+
const [version, traceId, parentSpanId, flags] = parts;
|
|
3327
|
+
if (version !== "00") {
|
|
3328
|
+
return null;
|
|
3329
|
+
}
|
|
3330
|
+
if (traceId.length !== 32 || !/^[0-9a-f]{32}$/.test(traceId)) {
|
|
3331
|
+
return null;
|
|
3332
|
+
}
|
|
3333
|
+
if (parentSpanId.length !== 16 || !/^[0-9a-f]{16}$/.test(parentSpanId)) {
|
|
3334
|
+
return null;
|
|
3335
|
+
}
|
|
3336
|
+
if (flags.length !== 2 || !/^[0-9a-f]{2}$/.test(flags)) {
|
|
3337
|
+
return null;
|
|
3338
|
+
}
|
|
3339
|
+
const sampled = (parseInt(flags, 16) & 1) === 1;
|
|
3340
|
+
return {
|
|
3341
|
+
traceId,
|
|
3342
|
+
spanId: generateSpanId(),
|
|
3343
|
+
parentSpanId,
|
|
3344
|
+
sampled
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
var TracePropagator = class {
|
|
3348
|
+
constructor(targets) {
|
|
3349
|
+
this.originalFetch = null;
|
|
3350
|
+
this.targets = targets;
|
|
3351
|
+
}
|
|
3352
|
+
shouldPropagate(url) {
|
|
3353
|
+
return this.targets.some((pattern) => {
|
|
3354
|
+
if (pattern.includes("*")) {
|
|
3355
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3356
|
+
const regex = new RegExp(`^${escaped}$`);
|
|
3357
|
+
return regex.test(url);
|
|
3358
|
+
}
|
|
3359
|
+
return url.startsWith(pattern);
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
3362
|
+
injectHeaders(url, headers, context) {
|
|
3363
|
+
if (!this.shouldPropagate(url)) {
|
|
3364
|
+
return headers;
|
|
3365
|
+
}
|
|
3366
|
+
const traceHeaders = propagateTrace(context);
|
|
3367
|
+
return {
|
|
3368
|
+
...headers,
|
|
3369
|
+
...traceHeaders
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
start() {
|
|
3373
|
+
if (typeof window === "undefined" || !window.fetch) {
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
if (this.originalFetch) {
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
this.originalFetch = window.fetch;
|
|
3380
|
+
const self = this;
|
|
3381
|
+
window.fetch = function(input, init2) {
|
|
3382
|
+
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
3383
|
+
if (self.shouldPropagate(url)) {
|
|
3384
|
+
const context = createTraceContext();
|
|
3385
|
+
const traceHeaders = propagateTrace(context);
|
|
3386
|
+
const existingHeaders = {};
|
|
3387
|
+
if (init2?.headers) {
|
|
3388
|
+
if (init2.headers instanceof Headers) {
|
|
3389
|
+
init2.headers.forEach((value, key) => {
|
|
3390
|
+
existingHeaders[key] = value;
|
|
3391
|
+
});
|
|
3392
|
+
} else if (Array.isArray(init2.headers)) {
|
|
3393
|
+
for (const [key, value] of init2.headers) {
|
|
3394
|
+
existingHeaders[key] = value;
|
|
3395
|
+
}
|
|
3396
|
+
} else {
|
|
3397
|
+
Object.assign(existingHeaders, init2.headers);
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
const mergedHeaders = {
|
|
3401
|
+
...existingHeaders,
|
|
3402
|
+
...traceHeaders
|
|
3403
|
+
};
|
|
3404
|
+
const patchedInit = {
|
|
3405
|
+
...init2,
|
|
3406
|
+
headers: mergedHeaders
|
|
3407
|
+
};
|
|
3408
|
+
return self.originalFetch.call(window, input, patchedInit);
|
|
3409
|
+
}
|
|
3410
|
+
return self.originalFetch.call(window, input, init2);
|
|
3411
|
+
};
|
|
3412
|
+
}
|
|
3413
|
+
stop() {
|
|
3414
|
+
if (typeof window === "undefined") {
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
if (this.originalFetch) {
|
|
3418
|
+
window.fetch = this.originalFetch;
|
|
3419
|
+
this.originalFetch = null;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
};
|
|
3423
|
+
|
|
3424
|
+
// src/crons.ts
|
|
3425
|
+
var CronMonitorManager = class {
|
|
3426
|
+
constructor(config) {
|
|
3427
|
+
this.apiKey = config.apiKey;
|
|
3428
|
+
this.endpoint = config.endpoint;
|
|
3429
|
+
this.debug = config.debug || false;
|
|
3430
|
+
}
|
|
3431
|
+
/**
|
|
3432
|
+
* Send a check-in for a cron monitor
|
|
3433
|
+
*/
|
|
3434
|
+
async checkin(slug, options = {}) {
|
|
3435
|
+
try {
|
|
3436
|
+
const response = await fetch(`${this.endpoint}/api/sdk/crons`, {
|
|
3437
|
+
method: "POST",
|
|
3438
|
+
headers: {
|
|
3439
|
+
"Content-Type": "application/json",
|
|
3440
|
+
"X-API-Key": this.apiKey
|
|
3441
|
+
},
|
|
3442
|
+
body: JSON.stringify({
|
|
3443
|
+
slug,
|
|
3444
|
+
status: options.status || "ok",
|
|
3445
|
+
duration_ms: options.duration_ms,
|
|
3446
|
+
message: options.message,
|
|
3447
|
+
environment: options.environment
|
|
3448
|
+
})
|
|
3449
|
+
});
|
|
3450
|
+
if (!response.ok) {
|
|
3451
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3452
|
+
}
|
|
3453
|
+
this.log(`Check-in sent for "${slug}" (${options.status || "ok"})`);
|
|
3454
|
+
} catch (err) {
|
|
3455
|
+
this.log("Failed to send check-in:", err);
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
/**
|
|
3459
|
+
* Start tracking a cron job execution.
|
|
3460
|
+
* Returns a handle with ok() and error() methods to complete the check-in.
|
|
3461
|
+
*/
|
|
3462
|
+
start(slug, environment) {
|
|
3463
|
+
const startTime = Date.now();
|
|
3464
|
+
this.checkin(slug, { status: "in_progress", environment });
|
|
3465
|
+
return {
|
|
3466
|
+
ok: async (message) => {
|
|
3467
|
+
const duration_ms = Date.now() - startTime;
|
|
3468
|
+
await this.checkin(slug, { status: "ok", duration_ms, message, environment });
|
|
3469
|
+
},
|
|
3470
|
+
error: async (message) => {
|
|
3471
|
+
const duration_ms = Date.now() - startTime;
|
|
3472
|
+
await this.checkin(slug, { status: "error", duration_ms, message, environment });
|
|
3473
|
+
}
|
|
3474
|
+
};
|
|
3475
|
+
}
|
|
3476
|
+
/**
|
|
3477
|
+
* Wrap an async function with automatic cron monitoring.
|
|
3478
|
+
* Sends 'in_progress' on start, 'ok' on success, 'error' on failure.
|
|
3479
|
+
*/
|
|
3480
|
+
wrap(slug, fn, environment) {
|
|
3481
|
+
const handle = this.start(slug, environment);
|
|
3482
|
+
return fn().then(
|
|
3483
|
+
async (result) => {
|
|
3484
|
+
await handle.ok();
|
|
3485
|
+
return result;
|
|
3486
|
+
},
|
|
3487
|
+
async (err) => {
|
|
3488
|
+
await handle.error(err?.message || String(err));
|
|
3489
|
+
throw err;
|
|
3490
|
+
}
|
|
3491
|
+
);
|
|
3492
|
+
}
|
|
3493
|
+
log(...args) {
|
|
3494
|
+
if (this.debug) {
|
|
3495
|
+
console.log("[SitePong:Cron]", ...args);
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
};
|
|
3499
|
+
|
|
3500
|
+
// src/metrics.ts
|
|
3501
|
+
var MetricsManager = class {
|
|
3502
|
+
constructor(config) {
|
|
3503
|
+
this.queue = [];
|
|
3504
|
+
this.flushTimer = null;
|
|
3505
|
+
this.apiKey = config.apiKey;
|
|
3506
|
+
this.endpoint = config.endpoint;
|
|
3507
|
+
this.debug = config.debug || false;
|
|
3508
|
+
this.flushInterval = config.flushInterval || 1e4;
|
|
3509
|
+
this.maxBatchSize = config.maxBatchSize || 100;
|
|
3510
|
+
this.startFlushTimer();
|
|
3511
|
+
}
|
|
3512
|
+
/**
|
|
3513
|
+
* Increment a counter metric
|
|
3514
|
+
*/
|
|
3515
|
+
increment(name, value = 1, options) {
|
|
3516
|
+
this.enqueue(name, value, "counter", options);
|
|
3517
|
+
}
|
|
3518
|
+
/**
|
|
3519
|
+
* Decrement a counter metric
|
|
3520
|
+
*/
|
|
3521
|
+
decrement(name, value = 1, options) {
|
|
3522
|
+
this.enqueue(name, -value, "counter", options);
|
|
3523
|
+
}
|
|
3524
|
+
/**
|
|
3525
|
+
* Set a gauge metric (instantaneous value)
|
|
3526
|
+
*/
|
|
3527
|
+
gauge(name, value, options) {
|
|
3528
|
+
this.enqueue(name, value, "gauge", options);
|
|
3529
|
+
}
|
|
3530
|
+
/**
|
|
3531
|
+
* Record a histogram value (for aggregation server-side)
|
|
3532
|
+
*/
|
|
3533
|
+
histogram(name, value, options) {
|
|
3534
|
+
this.enqueue(name, value, "histogram", options);
|
|
3535
|
+
}
|
|
3536
|
+
/**
|
|
3537
|
+
* Record a distribution value (for percentile calculations)
|
|
3538
|
+
*/
|
|
3539
|
+
distribution(name, value, options) {
|
|
3540
|
+
this.enqueue(name, value, "distribution", options);
|
|
3541
|
+
}
|
|
3542
|
+
/**
|
|
3543
|
+
* Time a function execution and record as distribution
|
|
3544
|
+
*/
|
|
3545
|
+
async time(name, fn, options) {
|
|
3546
|
+
const start = Date.now();
|
|
3547
|
+
try {
|
|
3548
|
+
const result = await fn();
|
|
3549
|
+
this.distribution(name, Date.now() - start, { ...options, unit: options?.unit || "ms" });
|
|
3550
|
+
return result;
|
|
3551
|
+
} catch (err) {
|
|
3552
|
+
this.distribution(name, Date.now() - start, {
|
|
3553
|
+
...options,
|
|
3554
|
+
unit: options?.unit || "ms",
|
|
3555
|
+
tags: { ...options?.tags, error: "true" }
|
|
3556
|
+
});
|
|
3557
|
+
throw err;
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
/**
|
|
3561
|
+
* Create a timer that records on stop
|
|
3562
|
+
*/
|
|
3563
|
+
startTimer(name, options) {
|
|
3564
|
+
const start = Date.now();
|
|
3565
|
+
return {
|
|
3566
|
+
stop: () => {
|
|
3567
|
+
this.distribution(name, Date.now() - start, { ...options, unit: options?.unit || "ms" });
|
|
3568
|
+
}
|
|
3569
|
+
};
|
|
3570
|
+
}
|
|
3571
|
+
/**
|
|
3572
|
+
* Force flush all queued metrics
|
|
3573
|
+
*/
|
|
3574
|
+
async flush() {
|
|
3575
|
+
if (this.queue.length === 0) return;
|
|
3576
|
+
const metrics = [...this.queue];
|
|
3577
|
+
this.queue = [];
|
|
3578
|
+
try {
|
|
3579
|
+
const response = await fetch(`${this.endpoint}/api/sdk/metrics`, {
|
|
3580
|
+
method: "POST",
|
|
3581
|
+
headers: {
|
|
3582
|
+
"Content-Type": "application/json",
|
|
3583
|
+
"X-API-Key": this.apiKey
|
|
3584
|
+
},
|
|
3585
|
+
body: JSON.stringify({ metrics })
|
|
3586
|
+
});
|
|
3587
|
+
if (!response.ok) {
|
|
3588
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3589
|
+
}
|
|
3590
|
+
this.log(`Flushed ${metrics.length} metrics`);
|
|
3591
|
+
} catch (err) {
|
|
3592
|
+
this.queue.unshift(...metrics);
|
|
3593
|
+
this.log("Failed to flush metrics:", err);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* Stop the flush timer and flush remaining metrics
|
|
3598
|
+
*/
|
|
3599
|
+
async destroy() {
|
|
3600
|
+
if (this.flushTimer) {
|
|
3601
|
+
clearInterval(this.flushTimer);
|
|
3602
|
+
this.flushTimer = null;
|
|
3603
|
+
}
|
|
3604
|
+
await this.flush();
|
|
3605
|
+
}
|
|
3606
|
+
enqueue(name, value, type, options) {
|
|
3607
|
+
this.queue.push({
|
|
3608
|
+
name,
|
|
3609
|
+
value,
|
|
3610
|
+
type,
|
|
3611
|
+
unit: options?.unit,
|
|
3612
|
+
tags: options?.tags,
|
|
3613
|
+
timestamp: options?.timestamp || (/* @__PURE__ */ new Date()).toISOString()
|
|
3614
|
+
});
|
|
3615
|
+
if (this.queue.length >= this.maxBatchSize) {
|
|
3616
|
+
this.flush();
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
startFlushTimer() {
|
|
3620
|
+
this.flushTimer = setInterval(() => {
|
|
3621
|
+
this.flush();
|
|
3622
|
+
}, this.flushInterval);
|
|
3623
|
+
if (typeof window !== "undefined") {
|
|
3624
|
+
window.addEventListener("visibilitychange", () => {
|
|
3625
|
+
if (document.visibilityState === "hidden") {
|
|
3626
|
+
this.flushWithBeacon();
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3631
|
+
flushWithBeacon() {
|
|
3632
|
+
if (this.queue.length === 0 || typeof navigator?.sendBeacon !== "function") return;
|
|
3633
|
+
const metrics = [...this.queue];
|
|
3634
|
+
this.queue = [];
|
|
3635
|
+
const blob = new Blob(
|
|
3636
|
+
[JSON.stringify({ metrics, apiKey: this.apiKey })],
|
|
3637
|
+
{ type: "application/json" }
|
|
3638
|
+
);
|
|
3639
|
+
navigator.sendBeacon(`${this.endpoint}/api/sdk/metrics`, blob);
|
|
3640
|
+
this.log(`Flushed ${metrics.length} metrics via beacon`);
|
|
3641
|
+
}
|
|
3642
|
+
log(...args) {
|
|
3643
|
+
if (this.debug) {
|
|
3644
|
+
console.log("[SitePong:Metrics]", ...args);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
};
|
|
3648
|
+
|
|
3649
|
+
// src/database.ts
|
|
3650
|
+
var DatabaseTracker = class {
|
|
3651
|
+
constructor(config) {
|
|
3652
|
+
this.queue = [];
|
|
3653
|
+
this.flushTimer = null;
|
|
3654
|
+
this.queryCounter = 0;
|
|
3655
|
+
this.nPlusOneDetector = /* @__PURE__ */ new Map();
|
|
3656
|
+
this.config = {
|
|
3657
|
+
slowQueryThreshold: 1e3,
|
|
3658
|
+
maxBatchSize: 50,
|
|
3659
|
+
flushInterval: 1e4,
|
|
3660
|
+
redactParams: true,
|
|
3661
|
+
...config
|
|
3662
|
+
};
|
|
3663
|
+
this.startFlushTimer();
|
|
3664
|
+
}
|
|
3665
|
+
/**
|
|
3666
|
+
* Track a database query execution
|
|
3667
|
+
*/
|
|
3668
|
+
async track(query, fn, source) {
|
|
3669
|
+
const start = Date.now();
|
|
3670
|
+
this.queryCounter++;
|
|
3671
|
+
try {
|
|
3672
|
+
const result = await fn();
|
|
3673
|
+
const duration_ms = Date.now() - start;
|
|
3674
|
+
this.recordQuery({
|
|
3675
|
+
query: this.config.redactParams ? this.redactQuery(query) : query,
|
|
3676
|
+
duration_ms,
|
|
3677
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3678
|
+
source,
|
|
3679
|
+
is_slow: duration_ms >= (this.config.slowQueryThreshold || 1e3)
|
|
3680
|
+
});
|
|
3681
|
+
this.detectNPlusOne(query);
|
|
3682
|
+
return result;
|
|
3683
|
+
} catch (err) {
|
|
3684
|
+
const duration_ms = Date.now() - start;
|
|
3685
|
+
this.recordQuery({
|
|
3686
|
+
query: this.config.redactParams ? this.redactQuery(query) : query,
|
|
3687
|
+
duration_ms,
|
|
3688
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3689
|
+
source,
|
|
3690
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3691
|
+
});
|
|
3692
|
+
throw err;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
/**
|
|
3696
|
+
* Track a synchronous database query
|
|
3697
|
+
*/
|
|
3698
|
+
trackSync(query, fn, source) {
|
|
3699
|
+
const start = Date.now();
|
|
3700
|
+
this.queryCounter++;
|
|
3701
|
+
try {
|
|
3702
|
+
const result = fn();
|
|
3703
|
+
const duration_ms = Date.now() - start;
|
|
3704
|
+
this.recordQuery({
|
|
3705
|
+
query: this.config.redactParams ? this.redactQuery(query) : query,
|
|
3706
|
+
duration_ms,
|
|
3707
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3708
|
+
source,
|
|
3709
|
+
is_slow: duration_ms >= (this.config.slowQueryThreshold || 1e3)
|
|
3710
|
+
});
|
|
3711
|
+
return result;
|
|
3712
|
+
} catch (err) {
|
|
3713
|
+
const duration_ms = Date.now() - start;
|
|
3714
|
+
this.recordQuery({
|
|
3715
|
+
query: this.config.redactParams ? this.redactQuery(query) : query,
|
|
3716
|
+
duration_ms,
|
|
3717
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3718
|
+
source,
|
|
3719
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3720
|
+
});
|
|
3721
|
+
throw err;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
/**
|
|
3725
|
+
* Get current query count (useful for N+1 detection in tests)
|
|
3726
|
+
*/
|
|
3727
|
+
getQueryCount() {
|
|
3728
|
+
return this.queryCounter;
|
|
3729
|
+
}
|
|
3730
|
+
/**
|
|
3731
|
+
* Reset the query counter
|
|
3732
|
+
*/
|
|
3733
|
+
resetQueryCount() {
|
|
3734
|
+
this.queryCounter = 0;
|
|
3735
|
+
this.nPlusOneDetector.clear();
|
|
3736
|
+
}
|
|
3737
|
+
/**
|
|
3738
|
+
* Get detected N+1 patterns
|
|
3739
|
+
*/
|
|
3740
|
+
getNPlusOnePatterns() {
|
|
3741
|
+
const patterns = [];
|
|
3742
|
+
for (const [query, { count }] of this.nPlusOneDetector.entries()) {
|
|
3743
|
+
if (count >= 5) {
|
|
3744
|
+
patterns.push({ query, count });
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
return patterns;
|
|
3748
|
+
}
|
|
3749
|
+
/**
|
|
3750
|
+
* Force flush queued query events
|
|
3751
|
+
*/
|
|
3752
|
+
async flush() {
|
|
3753
|
+
if (this.queue.length === 0) return;
|
|
3754
|
+
const queries = [...this.queue];
|
|
3755
|
+
this.queue = [];
|
|
3756
|
+
try {
|
|
3757
|
+
const response = await fetch(`${this.config.endpoint}/api/sdk/metrics`, {
|
|
3758
|
+
method: "POST",
|
|
3759
|
+
headers: {
|
|
3760
|
+
"Content-Type": "application/json",
|
|
3761
|
+
"X-API-Key": this.config.apiKey
|
|
3762
|
+
},
|
|
3763
|
+
body: JSON.stringify({
|
|
3764
|
+
metrics: queries.map((q) => ({
|
|
3765
|
+
name: "db.query",
|
|
3766
|
+
value: q.duration_ms,
|
|
3767
|
+
type: "distribution",
|
|
3768
|
+
unit: "ms",
|
|
3769
|
+
tags: {
|
|
3770
|
+
query: q.query.substring(0, 200),
|
|
3771
|
+
source: q.source || "unknown",
|
|
3772
|
+
is_slow: String(q.is_slow || false),
|
|
3773
|
+
...q.error ? { error: "true" } : {}
|
|
3774
|
+
},
|
|
3775
|
+
timestamp: q.timestamp
|
|
3776
|
+
}))
|
|
3777
|
+
})
|
|
3778
|
+
});
|
|
3779
|
+
if (!response.ok) {
|
|
3780
|
+
throw new Error(`HTTP ${response.status}`);
|
|
3781
|
+
}
|
|
3782
|
+
this.log(`Flushed ${queries.length} query events`);
|
|
3783
|
+
} catch (err) {
|
|
3784
|
+
this.queue.unshift(...queries);
|
|
3785
|
+
this.log("Failed to flush query events:", err);
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
/**
|
|
3789
|
+
* Destroy the tracker and flush remaining events
|
|
3790
|
+
*/
|
|
3791
|
+
async destroy() {
|
|
3792
|
+
if (this.flushTimer) {
|
|
3793
|
+
clearInterval(this.flushTimer);
|
|
3794
|
+
this.flushTimer = null;
|
|
3795
|
+
}
|
|
3796
|
+
await this.flush();
|
|
3797
|
+
}
|
|
3798
|
+
recordQuery(event) {
|
|
3799
|
+
this.queue.push(event);
|
|
3800
|
+
if (this.queue.length >= (this.config.maxBatchSize || 50)) {
|
|
3801
|
+
this.flush();
|
|
3802
|
+
}
|
|
3803
|
+
if (event.is_slow) {
|
|
3804
|
+
this.log(`Slow query (${event.duration_ms}ms):`, event.query.substring(0, 100));
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
detectNPlusOne(query) {
|
|
3808
|
+
const pattern = this.normalizeQuery(query);
|
|
3809
|
+
const now = Date.now();
|
|
3810
|
+
const existing = this.nPlusOneDetector.get(pattern);
|
|
3811
|
+
if (existing) {
|
|
3812
|
+
if (now - existing.since < 1e3) {
|
|
3813
|
+
existing.count++;
|
|
3814
|
+
if (existing.count === 5) {
|
|
3815
|
+
this.log(`N+1 query detected! Pattern repeated ${existing.count} times:`, pattern.substring(0, 100));
|
|
3816
|
+
}
|
|
3817
|
+
} else {
|
|
3818
|
+
this.nPlusOneDetector.set(pattern, { count: 1, since: now });
|
|
3819
|
+
}
|
|
3820
|
+
} else {
|
|
3821
|
+
this.nPlusOneDetector.set(pattern, { count: 1, since: now });
|
|
3822
|
+
}
|
|
3823
|
+
if (this.nPlusOneDetector.size > 100) {
|
|
3824
|
+
const entries = [...this.nPlusOneDetector.entries()];
|
|
3825
|
+
entries.sort((a, b) => a[1].since - b[1].since);
|
|
3826
|
+
for (let i = 0; i < 50; i++) {
|
|
3827
|
+
this.nPlusOneDetector.delete(entries[i][0]);
|
|
3828
|
+
}
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
normalizeQuery(query) {
|
|
3832
|
+
return query.replace(/\s+/g, " ").replace(/'[^']*'/g, "?").replace(/\b\d+\b/g, "?").trim().substring(0, 300);
|
|
3833
|
+
}
|
|
3834
|
+
redactQuery(query) {
|
|
3835
|
+
return query.replace(/'[^']*'/g, "'***'").replace(/\b\d{4,}\b/g, "***");
|
|
3836
|
+
}
|
|
3837
|
+
startFlushTimer() {
|
|
3838
|
+
this.flushTimer = setInterval(() => {
|
|
3839
|
+
this.flush();
|
|
3840
|
+
}, this.config.flushInterval || 1e4);
|
|
3841
|
+
}
|
|
3842
|
+
log(...args) {
|
|
3843
|
+
if (this.config.debug) {
|
|
3844
|
+
console.log("[SitePong:DB]", ...args);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
};
|
|
3848
|
+
|
|
3849
|
+
// src/profiling.ts
|
|
3850
|
+
var Profiler = class {
|
|
3851
|
+
constructor(config) {
|
|
3852
|
+
this.profiles = [];
|
|
3853
|
+
this.activeProfile = null;
|
|
3854
|
+
this.frameStack = [];
|
|
3855
|
+
this.flushTimer = null;
|
|
3856
|
+
this.config = {
|
|
3857
|
+
sampleRate: 0.1,
|
|
3858
|
+
maxDuration: 3e4,
|
|
3859
|
+
flushInterval: 3e4,
|
|
3860
|
+
...config
|
|
3861
|
+
};
|
|
3862
|
+
this.startFlushTimer();
|
|
3863
|
+
}
|
|
3864
|
+
/**
|
|
3865
|
+
* Profile an async function execution
|
|
3866
|
+
*/
|
|
3867
|
+
async profile(name, fn, metadata) {
|
|
3868
|
+
if (Math.random() > (this.config.sampleRate || 0.1)) {
|
|
3869
|
+
return fn();
|
|
3870
|
+
}
|
|
3871
|
+
const profileId = this.generateId();
|
|
3872
|
+
const startTime = performance.now();
|
|
3873
|
+
this.activeProfile = {
|
|
3874
|
+
id: profileId,
|
|
3875
|
+
name,
|
|
3876
|
+
startTime,
|
|
3877
|
+
duration_ms: 0,
|
|
3878
|
+
frames: [],
|
|
3879
|
+
metadata
|
|
3880
|
+
};
|
|
3881
|
+
this.frameStack = [];
|
|
3882
|
+
const rootFrame = {
|
|
3883
|
+
name,
|
|
3884
|
+
start_ms: 0,
|
|
3885
|
+
duration_ms: 0,
|
|
3886
|
+
children: [],
|
|
3887
|
+
metadata
|
|
3888
|
+
};
|
|
3889
|
+
this.frameStack.push(rootFrame);
|
|
3890
|
+
try {
|
|
3891
|
+
const result = await fn();
|
|
3892
|
+
rootFrame.duration_ms = performance.now() - startTime;
|
|
3893
|
+
this.activeProfile.duration_ms = rootFrame.duration_ms;
|
|
3894
|
+
this.activeProfile.frames = [rootFrame];
|
|
3895
|
+
if (rootFrame.duration_ms <= (this.config.maxDuration || 3e4)) {
|
|
3896
|
+
this.profiles.push(this.activeProfile);
|
|
3897
|
+
}
|
|
3898
|
+
this.activeProfile = null;
|
|
3899
|
+
this.frameStack = [];
|
|
3900
|
+
return result;
|
|
3901
|
+
} catch (err) {
|
|
3902
|
+
rootFrame.duration_ms = performance.now() - startTime;
|
|
3903
|
+
rootFrame.metadata = { ...rootFrame.metadata, error: true };
|
|
3904
|
+
if (this.activeProfile) {
|
|
3905
|
+
this.activeProfile.duration_ms = rootFrame.duration_ms;
|
|
3906
|
+
this.activeProfile.frames = [rootFrame];
|
|
3907
|
+
this.profiles.push(this.activeProfile);
|
|
3908
|
+
}
|
|
3909
|
+
this.activeProfile = null;
|
|
3910
|
+
this.frameStack = [];
|
|
3911
|
+
throw err;
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
/**
|
|
3915
|
+
* Start a profiling span within an active profile
|
|
3916
|
+
*/
|
|
3917
|
+
startSpan(name, metadata) {
|
|
3918
|
+
if (!this.activeProfile) return () => {
|
|
3919
|
+
};
|
|
3920
|
+
const startMs = performance.now() - this.activeProfile.startTime;
|
|
3921
|
+
const frame = {
|
|
3922
|
+
name,
|
|
3923
|
+
start_ms: startMs,
|
|
3924
|
+
duration_ms: 0,
|
|
3925
|
+
children: [],
|
|
3926
|
+
metadata
|
|
3927
|
+
};
|
|
3928
|
+
const parent = this.frameStack[this.frameStack.length - 1];
|
|
3929
|
+
if (parent) {
|
|
3930
|
+
parent.children.push(frame);
|
|
3931
|
+
}
|
|
3932
|
+
this.frameStack.push(frame);
|
|
3933
|
+
return () => {
|
|
3934
|
+
frame.duration_ms = performance.now() - this.activeProfile.startTime - frame.start_ms;
|
|
3935
|
+
this.frameStack.pop();
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Get all collected profiles
|
|
3940
|
+
*/
|
|
3941
|
+
getProfiles() {
|
|
3942
|
+
return [...this.profiles];
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Get the latest profile
|
|
3946
|
+
*/
|
|
3947
|
+
getLatestProfile() {
|
|
3948
|
+
return this.profiles.length > 0 ? this.profiles[this.profiles.length - 1] : null;
|
|
3949
|
+
}
|
|
3950
|
+
/**
|
|
3951
|
+
* Flush profiles to the server
|
|
3952
|
+
*/
|
|
3953
|
+
async flush() {
|
|
3954
|
+
if (this.profiles.length === 0) return;
|
|
3955
|
+
const profiles = [...this.profiles];
|
|
3956
|
+
this.profiles = [];
|
|
3957
|
+
try {
|
|
3958
|
+
const response = await fetch(`${this.config.endpoint}/api/sdk/metrics`, {
|
|
3959
|
+
method: "POST",
|
|
3960
|
+
headers: {
|
|
3961
|
+
"Content-Type": "application/json",
|
|
3962
|
+
"X-API-Key": this.config.apiKey
|
|
3963
|
+
},
|
|
3964
|
+
body: JSON.stringify({
|
|
3965
|
+
metrics: profiles.map((p) => ({
|
|
3966
|
+
name: `profile.${p.name}`,
|
|
3967
|
+
value: p.duration_ms,
|
|
3968
|
+
type: "distribution",
|
|
3969
|
+
unit: "ms",
|
|
3970
|
+
tags: {
|
|
3971
|
+
profile_id: p.id,
|
|
3972
|
+
frame_count: String(countFrames(p.frames)),
|
|
3973
|
+
...p.metadata || {}
|
|
3974
|
+
},
|
|
3975
|
+
timestamp: new Date(Date.now() - p.duration_ms).toISOString()
|
|
3976
|
+
}))
|
|
3977
|
+
})
|
|
3978
|
+
});
|
|
3979
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
3980
|
+
this.log(`Flushed ${profiles.length} profiles`);
|
|
3981
|
+
} catch (err) {
|
|
3982
|
+
this.profiles.unshift(...profiles);
|
|
3983
|
+
this.log("Failed to flush profiles:", err);
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
/**
|
|
3987
|
+
* Destroy the profiler
|
|
3988
|
+
*/
|
|
3989
|
+
async destroy() {
|
|
3990
|
+
if (this.flushTimer) {
|
|
3991
|
+
clearInterval(this.flushTimer);
|
|
3992
|
+
this.flushTimer = null;
|
|
3993
|
+
}
|
|
3994
|
+
await this.flush();
|
|
3995
|
+
}
|
|
3996
|
+
startFlushTimer() {
|
|
3997
|
+
this.flushTimer = setInterval(() => {
|
|
3998
|
+
this.flush();
|
|
3999
|
+
}, this.config.flushInterval || 3e4);
|
|
4000
|
+
}
|
|
4001
|
+
generateId() {
|
|
4002
|
+
return `prof_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
4003
|
+
}
|
|
4004
|
+
log(...args) {
|
|
4005
|
+
if (this.config.debug) {
|
|
4006
|
+
console.log("[SitePong:Profiler]", ...args);
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
function countFrames(frames) {
|
|
4011
|
+
let count = frames.length;
|
|
4012
|
+
for (const frame of frames) {
|
|
4013
|
+
count += countFrames(frame.children);
|
|
4014
|
+
}
|
|
4015
|
+
return count;
|
|
4016
|
+
}
|
|
4017
|
+
|
|
5
4018
|
// src/index.ts
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var
|
|
4019
|
+
var DEFAULT_ENDPOINT6 = "https://ingest.sitepong.com";
|
|
4020
|
+
var DEFAULT_BATCH_SIZE2 = 10;
|
|
4021
|
+
var DEFAULT_FLUSH_INTERVAL4 = 5e3;
|
|
9
4022
|
var SitePongClient = class {
|
|
10
4023
|
constructor() {
|
|
11
4024
|
this.errorQueue = [];
|
|
12
4025
|
this.flushTimer = null;
|
|
13
4026
|
this.context = {};
|
|
14
4027
|
this.initialized = false;
|
|
4028
|
+
this.flagManager = null;
|
|
4029
|
+
this.analyticsManager = null;
|
|
4030
|
+
this.fingerprintManager = null;
|
|
4031
|
+
this.replayManager = null;
|
|
4032
|
+
this.performanceManager = null;
|
|
4033
|
+
this.cronManager = null;
|
|
4034
|
+
this.metricsManager = null;
|
|
4035
|
+
this.databaseTracker = null;
|
|
4036
|
+
this.profiler = null;
|
|
15
4037
|
this.config = {
|
|
16
4038
|
apiKey: "",
|
|
17
|
-
endpoint:
|
|
4039
|
+
endpoint: DEFAULT_ENDPOINT6,
|
|
18
4040
|
environment: "production",
|
|
19
4041
|
release: "",
|
|
20
4042
|
autoCapture: true,
|
|
21
|
-
maxBatchSize:
|
|
22
|
-
flushInterval:
|
|
4043
|
+
maxBatchSize: DEFAULT_BATCH_SIZE2,
|
|
4044
|
+
flushInterval: DEFAULT_FLUSH_INTERVAL4,
|
|
23
4045
|
debug: false
|
|
24
4046
|
};
|
|
25
4047
|
}
|
|
@@ -38,12 +4060,389 @@ var SitePongClient = class {
|
|
|
38
4060
|
}
|
|
39
4061
|
this.startFlushTimer();
|
|
40
4062
|
this.log("Initialized with endpoint:", this.config.endpoint);
|
|
4063
|
+
if (config.enableFlags) {
|
|
4064
|
+
this.flagManager = new FlagManager({
|
|
4065
|
+
apiKey: config.apiKey,
|
|
4066
|
+
endpoint: config.flagsEndpoint || config.endpoint || DEFAULT_ENDPOINT6,
|
|
4067
|
+
debug: config.debug
|
|
4068
|
+
});
|
|
4069
|
+
this.flagManager.init().catch((err) => {
|
|
4070
|
+
this.log("Failed to initialize flags:", err);
|
|
4071
|
+
});
|
|
4072
|
+
}
|
|
4073
|
+
if (config.analytics?.enabled) {
|
|
4074
|
+
if (this.analyticsManager) {
|
|
4075
|
+
this.analyticsManager.destroy();
|
|
4076
|
+
}
|
|
4077
|
+
this.analyticsManager = new AnalyticsManager({
|
|
4078
|
+
apiKey: config.apiKey,
|
|
4079
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4080
|
+
enabled: true,
|
|
4081
|
+
autocapturePageviews: config.analytics.autocapturePageviews,
|
|
4082
|
+
autocaptureClicks: config.analytics.autocaptureClicks,
|
|
4083
|
+
autocaptureForms: config.analytics.autocaptureForms,
|
|
4084
|
+
autocapture: config.analytics.autocapture,
|
|
4085
|
+
maxBatchSize: config.analytics.maxBatchSize,
|
|
4086
|
+
flushInterval: config.analytics.flushInterval,
|
|
4087
|
+
eventsEndpoint: config.analytics.eventsEndpoint,
|
|
4088
|
+
debug: config.debug
|
|
4089
|
+
});
|
|
4090
|
+
this.analyticsManager.init();
|
|
4091
|
+
}
|
|
4092
|
+
if (config.fingerprint?.enabled) {
|
|
4093
|
+
this.fingerprintManager = new FingerprintManager({
|
|
4094
|
+
apiKey: config.apiKey,
|
|
4095
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4096
|
+
enabled: true,
|
|
4097
|
+
extendedSignals: config.fingerprint.extendedSignals,
|
|
4098
|
+
visitorEndpoint: config.fingerprint.visitorEndpoint,
|
|
4099
|
+
debug: config.debug
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
if (config.performance?.enabled) {
|
|
4103
|
+
if (this.performanceManager) {
|
|
4104
|
+
this.performanceManager.destroy();
|
|
4105
|
+
}
|
|
4106
|
+
this.performanceManager = new PerformanceManager({
|
|
4107
|
+
apiKey: config.apiKey,
|
|
4108
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4109
|
+
enabled: true,
|
|
4110
|
+
webVitals: config.performance.webVitals,
|
|
4111
|
+
navigationTiming: config.performance.navigationTiming,
|
|
4112
|
+
resourceTiming: config.performance.resourceTiming,
|
|
4113
|
+
sampleRate: config.performance.sampleRate,
|
|
4114
|
+
flushInterval: config.performance.flushInterval,
|
|
4115
|
+
performanceEndpoint: config.performance.performanceEndpoint,
|
|
4116
|
+
debug: config.debug
|
|
4117
|
+
});
|
|
4118
|
+
this.performanceManager.init();
|
|
4119
|
+
}
|
|
4120
|
+
if (config.replay?.enabled) {
|
|
4121
|
+
if (this.replayManager) {
|
|
4122
|
+
this.replayManager.stop();
|
|
4123
|
+
}
|
|
4124
|
+
this.replayManager = new ReplayManager({
|
|
4125
|
+
apiKey: config.apiKey,
|
|
4126
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4127
|
+
enabled: true,
|
|
4128
|
+
maskInputs: config.replay.maskInputs,
|
|
4129
|
+
blockSelector: config.replay.blockSelector,
|
|
4130
|
+
maskSelector: config.replay.maskSelector,
|
|
4131
|
+
sampleRate: config.replay.sampleRate,
|
|
4132
|
+
maxSessionDuration: config.replay.maxSessionDuration,
|
|
4133
|
+
flushInterval: config.replay.flushInterval,
|
|
4134
|
+
replayEndpoint: config.replay.replayEndpoint,
|
|
4135
|
+
recordNetwork: config.replay.recordNetwork,
|
|
4136
|
+
captureNetworkHeaders: config.replay.captureNetworkHeaders,
|
|
4137
|
+
recordConsole: config.replay.recordConsole,
|
|
4138
|
+
consoleLevels: config.replay.consoleLevels,
|
|
4139
|
+
debug: config.debug
|
|
4140
|
+
});
|
|
4141
|
+
this.replayManager.start();
|
|
4142
|
+
}
|
|
4143
|
+
if (config.crons?.enabled) {
|
|
4144
|
+
this.cronManager = new CronMonitorManager({
|
|
4145
|
+
apiKey: config.apiKey,
|
|
4146
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4147
|
+
debug: config.debug
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
if (config.database?.enabled) {
|
|
4151
|
+
this.databaseTracker = new DatabaseTracker({
|
|
4152
|
+
apiKey: config.apiKey,
|
|
4153
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4154
|
+
debug: config.debug,
|
|
4155
|
+
slowQueryThreshold: config.database.slowQueryThreshold,
|
|
4156
|
+
redactParams: config.database.redactParams
|
|
4157
|
+
});
|
|
4158
|
+
}
|
|
4159
|
+
if (config.metrics?.enabled) {
|
|
4160
|
+
this.metricsManager = new MetricsManager({
|
|
4161
|
+
apiKey: config.apiKey,
|
|
4162
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4163
|
+
debug: config.debug,
|
|
4164
|
+
flushInterval: config.metrics.flushInterval,
|
|
4165
|
+
maxBatchSize: config.metrics.maxBatchSize
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
if (config.profiling?.enabled) {
|
|
4169
|
+
this.profiler = new Profiler({
|
|
4170
|
+
apiKey: config.apiKey,
|
|
4171
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT6,
|
|
4172
|
+
debug: config.debug,
|
|
4173
|
+
sampleRate: config.profiling.sampleRate,
|
|
4174
|
+
maxDuration: config.profiling.maxDuration,
|
|
4175
|
+
flushInterval: config.profiling.flushInterval
|
|
4176
|
+
});
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
/**
|
|
4180
|
+
* Get a feature flag value
|
|
4181
|
+
* Returns the cached evaluated result or default value
|
|
4182
|
+
*/
|
|
4183
|
+
getFlag(key, defaultValue = false) {
|
|
4184
|
+
if (!this.flagManager) {
|
|
4185
|
+
this.log("Flags not enabled. Set enableFlags: true in init()");
|
|
4186
|
+
return defaultValue;
|
|
4187
|
+
}
|
|
4188
|
+
return this.flagManager.getFlag(key, defaultValue);
|
|
4189
|
+
}
|
|
4190
|
+
/**
|
|
4191
|
+
* Get all feature flag values
|
|
4192
|
+
*/
|
|
4193
|
+
getAllFlags() {
|
|
4194
|
+
if (!this.flagManager) {
|
|
4195
|
+
return {};
|
|
4196
|
+
}
|
|
4197
|
+
return this.flagManager.getAllFlags();
|
|
4198
|
+
}
|
|
4199
|
+
/**
|
|
4200
|
+
* Wait for flags to be initialized
|
|
4201
|
+
*/
|
|
4202
|
+
async waitForFlags() {
|
|
4203
|
+
if (!this.flagManager) {
|
|
4204
|
+
return;
|
|
4205
|
+
}
|
|
4206
|
+
await this.flagManager.waitForInit();
|
|
4207
|
+
}
|
|
4208
|
+
/**
|
|
4209
|
+
* Check if flags are ready
|
|
4210
|
+
*/
|
|
4211
|
+
areFlagsReady() {
|
|
4212
|
+
return this.flagManager?.isInitialized() ?? false;
|
|
4213
|
+
}
|
|
4214
|
+
/**
|
|
4215
|
+
* Get a multivariate flag variant
|
|
4216
|
+
* Returns the assigned variant key or the default value
|
|
4217
|
+
*/
|
|
4218
|
+
getVariant(key, defaultValue = null) {
|
|
4219
|
+
if (!this.flagManager) {
|
|
4220
|
+
this.log("Flags not enabled. Set enableFlags: true in init()");
|
|
4221
|
+
return defaultValue;
|
|
4222
|
+
}
|
|
4223
|
+
return this.flagManager.getVariant(key, defaultValue);
|
|
4224
|
+
}
|
|
4225
|
+
/**
|
|
4226
|
+
* Get the payload for a multivariate flag's assigned variant
|
|
4227
|
+
*/
|
|
4228
|
+
getVariantPayload(key, defaultValue = null) {
|
|
4229
|
+
if (!this.flagManager) {
|
|
4230
|
+
return defaultValue;
|
|
4231
|
+
}
|
|
4232
|
+
return this.flagManager.getVariantPayload(key, defaultValue);
|
|
4233
|
+
}
|
|
4234
|
+
/**
|
|
4235
|
+
* Force refresh flags from the server
|
|
4236
|
+
*/
|
|
4237
|
+
async refreshFlags() {
|
|
4238
|
+
if (!this.flagManager) {
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
await this.flagManager.refresh();
|
|
4242
|
+
}
|
|
4243
|
+
// --- Analytics (Tier 4) ---
|
|
4244
|
+
track(eventName, properties) {
|
|
4245
|
+
if (!this.analyticsManager) {
|
|
4246
|
+
this.log("Analytics not enabled. Set analytics.enabled: true in init()");
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
this.analyticsManager.track(eventName, properties);
|
|
4250
|
+
}
|
|
4251
|
+
trackPageView(url, properties) {
|
|
4252
|
+
if (!this.analyticsManager) {
|
|
4253
|
+
this.log("Analytics not enabled. Set analytics.enabled: true in init()");
|
|
4254
|
+
return;
|
|
4255
|
+
}
|
|
4256
|
+
this.analyticsManager.trackPageView(url, properties);
|
|
4257
|
+
}
|
|
4258
|
+
identify(userId, traits) {
|
|
4259
|
+
if (this.replayManager) {
|
|
4260
|
+
this.replayManager.setUser({ id: userId });
|
|
4261
|
+
}
|
|
4262
|
+
if (!this.analyticsManager) {
|
|
4263
|
+
this.log("Analytics not enabled. Set analytics.enabled: true in init()");
|
|
4264
|
+
return;
|
|
4265
|
+
}
|
|
4266
|
+
this.analyticsManager.identify(userId, traits);
|
|
4267
|
+
}
|
|
4268
|
+
group(groupId, traits) {
|
|
4269
|
+
if (!this.analyticsManager) {
|
|
4270
|
+
this.log("Analytics not enabled. Set analytics.enabled: true in init()");
|
|
4271
|
+
return;
|
|
4272
|
+
}
|
|
4273
|
+
this.analyticsManager.group(groupId, traits);
|
|
4274
|
+
}
|
|
4275
|
+
resetAnalytics() {
|
|
4276
|
+
if (!this.analyticsManager) {
|
|
4277
|
+
return;
|
|
4278
|
+
}
|
|
4279
|
+
this.analyticsManager.reset();
|
|
4280
|
+
}
|
|
4281
|
+
// --- Fingerprint (Tier 5) ---
|
|
4282
|
+
async getVisitorId() {
|
|
4283
|
+
if (!this.fingerprintManager) {
|
|
4284
|
+
throw new Error("Fingerprint not enabled. Set fingerprint.enabled: true in init()");
|
|
4285
|
+
}
|
|
4286
|
+
return this.fingerprintManager.getVisitorId();
|
|
4287
|
+
}
|
|
4288
|
+
async getDeviceSignals() {
|
|
4289
|
+
if (!this.fingerprintManager) {
|
|
4290
|
+
throw new Error("Fingerprint not enabled. Set fingerprint.enabled: true in init()");
|
|
4291
|
+
}
|
|
4292
|
+
return this.fingerprintManager.getDeviceSignals();
|
|
4293
|
+
}
|
|
4294
|
+
async getFraudCheck() {
|
|
4295
|
+
if (!this.fingerprintManager) {
|
|
4296
|
+
throw new Error("Fingerprint not enabled. Set fingerprint.enabled: true in init()");
|
|
4297
|
+
}
|
|
4298
|
+
return this.fingerprintManager.getFraudCheck();
|
|
4299
|
+
}
|
|
4300
|
+
// --- Session Replay (Tier 6) ---
|
|
4301
|
+
startReplay() {
|
|
4302
|
+
if (!this.replayManager) {
|
|
4303
|
+
this.log("Replay not enabled. Set replay.enabled: true in init()");
|
|
4304
|
+
return false;
|
|
4305
|
+
}
|
|
4306
|
+
return this.replayManager.start();
|
|
4307
|
+
}
|
|
4308
|
+
stopReplay() {
|
|
4309
|
+
if (!this.replayManager) return;
|
|
4310
|
+
this.replayManager.stop();
|
|
4311
|
+
}
|
|
4312
|
+
isReplayRecording() {
|
|
4313
|
+
return this.replayManager?.isRecording() ?? false;
|
|
4314
|
+
}
|
|
4315
|
+
getReplaySessionId() {
|
|
4316
|
+
return this.replayManager?.getSessionId() ?? null;
|
|
4317
|
+
}
|
|
4318
|
+
// --- Performance (Tier 7) ---
|
|
4319
|
+
startTransaction(name, data) {
|
|
4320
|
+
if (!this.performanceManager) {
|
|
4321
|
+
this.log("Performance not enabled. Set performance.enabled: true in init()");
|
|
4322
|
+
return "";
|
|
4323
|
+
}
|
|
4324
|
+
return this.performanceManager.startTransaction(name, data);
|
|
4325
|
+
}
|
|
4326
|
+
endTransaction(id, status = "ok") {
|
|
4327
|
+
if (!this.performanceManager) return;
|
|
4328
|
+
this.performanceManager.endTransaction(id, status);
|
|
4329
|
+
}
|
|
4330
|
+
startSpan(transactionId, name, data) {
|
|
4331
|
+
if (!this.performanceManager) return "";
|
|
4332
|
+
return this.performanceManager.startSpan(transactionId, name, data);
|
|
4333
|
+
}
|
|
4334
|
+
endSpan(transactionId, spanId, status = "ok") {
|
|
4335
|
+
if (!this.performanceManager) return;
|
|
4336
|
+
this.performanceManager.endSpan(transactionId, spanId, status);
|
|
4337
|
+
}
|
|
4338
|
+
getWebVitals() {
|
|
4339
|
+
if (!this.performanceManager) return {};
|
|
4340
|
+
return this.performanceManager.getVitals();
|
|
4341
|
+
}
|
|
4342
|
+
// --- Cron Monitor methods ---
|
|
4343
|
+
cronCheckin(slug, options) {
|
|
4344
|
+
if (!this.cronManager) {
|
|
4345
|
+
this.log("Cron monitoring not enabled. Set crons.enabled: true in init()");
|
|
4346
|
+
return Promise.resolve();
|
|
4347
|
+
}
|
|
4348
|
+
return this.cronManager.checkin(slug, options);
|
|
4349
|
+
}
|
|
4350
|
+
cronStart(slug, environment) {
|
|
4351
|
+
if (!this.cronManager) {
|
|
4352
|
+
this.log("Cron monitoring not enabled. Set crons.enabled: true in init()");
|
|
4353
|
+
return { ok: () => Promise.resolve(), error: () => Promise.resolve() };
|
|
4354
|
+
}
|
|
4355
|
+
return this.cronManager.start(slug, environment);
|
|
4356
|
+
}
|
|
4357
|
+
cronWrap(slug, fn, environment) {
|
|
4358
|
+
if (!this.cronManager) {
|
|
4359
|
+
return fn();
|
|
4360
|
+
}
|
|
4361
|
+
return this.cronManager.wrap(slug, fn, environment);
|
|
4362
|
+
}
|
|
4363
|
+
// --- Custom Metrics methods ---
|
|
4364
|
+
metricIncrement(name, value, options) {
|
|
4365
|
+
if (!this.metricsManager) {
|
|
4366
|
+
this.log("Metrics not enabled. Set metrics.enabled: true in init()");
|
|
4367
|
+
return;
|
|
4368
|
+
}
|
|
4369
|
+
this.metricsManager.increment(name, value, options);
|
|
4370
|
+
}
|
|
4371
|
+
metricGauge(name, value, options) {
|
|
4372
|
+
if (!this.metricsManager) return;
|
|
4373
|
+
this.metricsManager.gauge(name, value, options);
|
|
4374
|
+
}
|
|
4375
|
+
metricHistogram(name, value, options) {
|
|
4376
|
+
if (!this.metricsManager) return;
|
|
4377
|
+
this.metricsManager.histogram(name, value, options);
|
|
4378
|
+
}
|
|
4379
|
+
metricDistribution(name, value, options) {
|
|
4380
|
+
if (!this.metricsManager) return;
|
|
4381
|
+
this.metricsManager.distribution(name, value, options);
|
|
4382
|
+
}
|
|
4383
|
+
async metricTime(name, fn, options) {
|
|
4384
|
+
if (!this.metricsManager) return fn();
|
|
4385
|
+
return this.metricsManager.time(name, fn, options);
|
|
4386
|
+
}
|
|
4387
|
+
metricStartTimer(name, options) {
|
|
4388
|
+
if (!this.metricsManager) return { stop: () => {
|
|
4389
|
+
} };
|
|
4390
|
+
return this.metricsManager.startTimer(name, options);
|
|
4391
|
+
}
|
|
4392
|
+
async flushMetrics() {
|
|
4393
|
+
if (!this.metricsManager) return;
|
|
4394
|
+
return this.metricsManager.flush();
|
|
4395
|
+
}
|
|
4396
|
+
// --- Database Query Tracking methods ---
|
|
4397
|
+
async dbTrack(query, fn, source) {
|
|
4398
|
+
if (!this.databaseTracker) return fn();
|
|
4399
|
+
return this.databaseTracker.track(query, fn, source);
|
|
4400
|
+
}
|
|
4401
|
+
dbTrackSync(query, fn, source) {
|
|
4402
|
+
if (!this.databaseTracker) return fn();
|
|
4403
|
+
return this.databaseTracker.trackSync(query, fn, source);
|
|
4404
|
+
}
|
|
4405
|
+
getDbQueryCount() {
|
|
4406
|
+
if (!this.databaseTracker) return 0;
|
|
4407
|
+
return this.databaseTracker.getQueryCount();
|
|
4408
|
+
}
|
|
4409
|
+
resetDbQueryCount() {
|
|
4410
|
+
if (this.databaseTracker) this.databaseTracker.resetQueryCount();
|
|
4411
|
+
}
|
|
4412
|
+
getDbNPlusOnePatterns() {
|
|
4413
|
+
if (!this.databaseTracker) return [];
|
|
4414
|
+
return this.databaseTracker.getNPlusOnePatterns();
|
|
4415
|
+
}
|
|
4416
|
+
// --- Profiling methods ---
|
|
4417
|
+
async profile(name, fn, metadata) {
|
|
4418
|
+
if (!this.profiler) return fn();
|
|
4419
|
+
return this.profiler.profile(name, fn, metadata);
|
|
4420
|
+
}
|
|
4421
|
+
startProfileSpan(name, metadata) {
|
|
4422
|
+
if (!this.profiler) return () => {
|
|
4423
|
+
};
|
|
4424
|
+
return this.profiler.startSpan(name, metadata);
|
|
4425
|
+
}
|
|
4426
|
+
getProfiles() {
|
|
4427
|
+
if (!this.profiler) return [];
|
|
4428
|
+
return this.profiler.getProfiles();
|
|
4429
|
+
}
|
|
4430
|
+
getLatestProfile() {
|
|
4431
|
+
if (!this.profiler) return null;
|
|
4432
|
+
return this.profiler.getLatestProfile();
|
|
4433
|
+
}
|
|
4434
|
+
async flushProfiles() {
|
|
4435
|
+
if (!this.profiler) return;
|
|
4436
|
+
return this.profiler.flush();
|
|
41
4437
|
}
|
|
42
4438
|
setContext(context) {
|
|
43
4439
|
this.context = { ...this.context, ...context };
|
|
44
4440
|
}
|
|
45
4441
|
setUser(user) {
|
|
46
4442
|
this.context.user = user;
|
|
4443
|
+
if (this.replayManager) {
|
|
4444
|
+
this.replayManager.setUser(user ? { id: user.id } : null);
|
|
4445
|
+
}
|
|
47
4446
|
}
|
|
48
4447
|
setTags(tags) {
|
|
49
4448
|
this.context.tags = { ...this.context.tags, ...tags };
|
|
@@ -73,7 +4472,8 @@ var SitePongClient = class {
|
|
|
73
4472
|
userAgent: this.getUserAgent(),
|
|
74
4473
|
environment: this.config.environment,
|
|
75
4474
|
release: this.config.release,
|
|
76
|
-
context: { ...this.context, ...additionalContext }
|
|
4475
|
+
context: { ...this.context, ...additionalContext },
|
|
4476
|
+
replaySessionId: this.replayManager?.isRecording() ? this.replayManager.getSessionId() || void 0 : void 0
|
|
77
4477
|
};
|
|
78
4478
|
this.errorQueue.push(capturedError);
|
|
79
4479
|
this.log("Captured message:", message);
|
|
@@ -108,7 +4508,7 @@ var SitePongClient = class {
|
|
|
108
4508
|
formatError(error, additionalContext) {
|
|
109
4509
|
const isError = error instanceof Error;
|
|
110
4510
|
const message = isError ? error.message : String(error);
|
|
111
|
-
const stack = isError ? error.stack :
|
|
4511
|
+
const stack = isError ? error.stack : new Error(message).stack;
|
|
112
4512
|
const type = isError ? error.name : "Error";
|
|
113
4513
|
return {
|
|
114
4514
|
message,
|
|
@@ -120,7 +4520,8 @@ var SitePongClient = class {
|
|
|
120
4520
|
environment: this.config.environment,
|
|
121
4521
|
release: this.config.release,
|
|
122
4522
|
context: { ...this.context, ...additionalContext },
|
|
123
|
-
fingerprint: this.generateFingerprint(message, stack)
|
|
4523
|
+
fingerprint: this.generateFingerprint(message, stack),
|
|
4524
|
+
replaySessionId: this.replayManager?.isRecording() ? this.replayManager.getSessionId() || void 0 : void 0
|
|
124
4525
|
};
|
|
125
4526
|
}
|
|
126
4527
|
generateFingerprint(message, stack) {
|
|
@@ -211,15 +4612,112 @@ var setContext = sitepong.setContext.bind(sitepong);
|
|
|
211
4612
|
var setUser = sitepong.setUser.bind(sitepong);
|
|
212
4613
|
var setTags = sitepong.setTags.bind(sitepong);
|
|
213
4614
|
var flush = sitepong.flush.bind(sitepong);
|
|
4615
|
+
var getFlag = sitepong.getFlag.bind(sitepong);
|
|
4616
|
+
var getAllFlags = sitepong.getAllFlags.bind(sitepong);
|
|
4617
|
+
var getVariant = sitepong.getVariant.bind(sitepong);
|
|
4618
|
+
var getVariantPayload2 = sitepong.getVariantPayload.bind(sitepong);
|
|
4619
|
+
var waitForFlags = sitepong.waitForFlags.bind(sitepong);
|
|
4620
|
+
var areFlagsReady = sitepong.areFlagsReady.bind(sitepong);
|
|
4621
|
+
var refreshFlags = sitepong.refreshFlags.bind(sitepong);
|
|
4622
|
+
var track = sitepong.track.bind(sitepong);
|
|
4623
|
+
var trackPageView = sitepong.trackPageView.bind(sitepong);
|
|
4624
|
+
var identify = sitepong.identify.bind(sitepong);
|
|
4625
|
+
var group = sitepong.group.bind(sitepong);
|
|
4626
|
+
var resetAnalytics = sitepong.resetAnalytics.bind(sitepong);
|
|
4627
|
+
var getVisitorId = sitepong.getVisitorId.bind(sitepong);
|
|
4628
|
+
var getDeviceSignals = sitepong.getDeviceSignals.bind(sitepong);
|
|
4629
|
+
var getFraudCheck = sitepong.getFraudCheck.bind(sitepong);
|
|
4630
|
+
var startReplay = sitepong.startReplay.bind(sitepong);
|
|
4631
|
+
var stopReplay = sitepong.stopReplay.bind(sitepong);
|
|
4632
|
+
var isReplayRecording = sitepong.isReplayRecording.bind(sitepong);
|
|
4633
|
+
var getReplaySessionId = sitepong.getReplaySessionId.bind(sitepong);
|
|
4634
|
+
var startTransaction = sitepong.startTransaction.bind(sitepong);
|
|
4635
|
+
var endTransaction = sitepong.endTransaction.bind(sitepong);
|
|
4636
|
+
var startSpan = sitepong.startSpan.bind(sitepong);
|
|
4637
|
+
var endSpan = sitepong.endSpan.bind(sitepong);
|
|
4638
|
+
var getWebVitals = sitepong.getWebVitals.bind(sitepong);
|
|
4639
|
+
var dbTrack = sitepong.dbTrack.bind(sitepong);
|
|
4640
|
+
var dbTrackSync = sitepong.dbTrackSync.bind(sitepong);
|
|
4641
|
+
var getDbQueryCount = sitepong.getDbQueryCount.bind(sitepong);
|
|
4642
|
+
var resetDbQueryCount = sitepong.resetDbQueryCount.bind(sitepong);
|
|
4643
|
+
var getDbNPlusOnePatterns = sitepong.getDbNPlusOnePatterns.bind(sitepong);
|
|
4644
|
+
var cronCheckin = sitepong.cronCheckin.bind(sitepong);
|
|
4645
|
+
var cronStart = sitepong.cronStart.bind(sitepong);
|
|
4646
|
+
var cronWrap = sitepong.cronWrap.bind(sitepong);
|
|
4647
|
+
var metricIncrement = sitepong.metricIncrement.bind(sitepong);
|
|
4648
|
+
var metricGauge = sitepong.metricGauge.bind(sitepong);
|
|
4649
|
+
var metricHistogram = sitepong.metricHistogram.bind(sitepong);
|
|
4650
|
+
var metricDistribution = sitepong.metricDistribution.bind(sitepong);
|
|
4651
|
+
var metricTime = sitepong.metricTime.bind(sitepong);
|
|
4652
|
+
var metricStartTimer = sitepong.metricStartTimer.bind(sitepong);
|
|
4653
|
+
var flushMetrics = sitepong.flushMetrics.bind(sitepong);
|
|
4654
|
+
var profile = sitepong.profile.bind(sitepong);
|
|
4655
|
+
var startProfileSpan = sitepong.startProfileSpan.bind(sitepong);
|
|
4656
|
+
var getProfiles = sitepong.getProfiles.bind(sitepong);
|
|
4657
|
+
var getLatestProfile = sitepong.getLatestProfile.bind(sitepong);
|
|
4658
|
+
var flushProfiles = sitepong.flushProfiles.bind(sitepong);
|
|
214
4659
|
var index_default = sitepong;
|
|
215
4660
|
|
|
4661
|
+
exports.TracePropagator = TracePropagator;
|
|
4662
|
+
exports.areFlagsReady = areFlagsReady;
|
|
216
4663
|
exports.captureError = captureError;
|
|
217
4664
|
exports.captureMessage = captureMessage;
|
|
4665
|
+
exports.clearAnonymousId = clearAnonymousId;
|
|
4666
|
+
exports.createTraceContext = createTraceContext;
|
|
4667
|
+
exports.cronCheckin = cronCheckin;
|
|
4668
|
+
exports.cronStart = cronStart;
|
|
4669
|
+
exports.cronWrap = cronWrap;
|
|
4670
|
+
exports.dbTrack = dbTrack;
|
|
4671
|
+
exports.dbTrackSync = dbTrackSync;
|
|
218
4672
|
exports.default = index_default;
|
|
4673
|
+
exports.endSpan = endSpan;
|
|
4674
|
+
exports.endTransaction = endTransaction;
|
|
4675
|
+
exports.extractTrace = extractTrace;
|
|
219
4676
|
exports.flush = flush;
|
|
4677
|
+
exports.flushMetrics = flushMetrics;
|
|
4678
|
+
exports.flushProfiles = flushProfiles;
|
|
4679
|
+
exports.generateSpanId = generateSpanId;
|
|
4680
|
+
exports.generateTraceId = generateTraceId;
|
|
4681
|
+
exports.getAllFlags = getAllFlags;
|
|
4682
|
+
exports.getAnonymousId = getAnonymousId;
|
|
4683
|
+
exports.getDbNPlusOnePatterns = getDbNPlusOnePatterns;
|
|
4684
|
+
exports.getDbQueryCount = getDbQueryCount;
|
|
4685
|
+
exports.getDeviceSignals = getDeviceSignals;
|
|
4686
|
+
exports.getFlag = getFlag;
|
|
4687
|
+
exports.getFraudCheck = getFraudCheck;
|
|
4688
|
+
exports.getLatestProfile = getLatestProfile;
|
|
4689
|
+
exports.getProfiles = getProfiles;
|
|
4690
|
+
exports.getReplaySessionId = getReplaySessionId;
|
|
4691
|
+
exports.getVariant = getVariant;
|
|
4692
|
+
exports.getVariantPayload = getVariantPayload2;
|
|
4693
|
+
exports.getVisitorId = getVisitorId;
|
|
4694
|
+
exports.getWebVitals = getWebVitals;
|
|
4695
|
+
exports.group = group;
|
|
4696
|
+
exports.identify = identify;
|
|
220
4697
|
exports.init = init;
|
|
4698
|
+
exports.isReplayRecording = isReplayRecording;
|
|
4699
|
+
exports.metricDistribution = metricDistribution;
|
|
4700
|
+
exports.metricGauge = metricGauge;
|
|
4701
|
+
exports.metricHistogram = metricHistogram;
|
|
4702
|
+
exports.metricIncrement = metricIncrement;
|
|
4703
|
+
exports.metricStartTimer = metricStartTimer;
|
|
4704
|
+
exports.metricTime = metricTime;
|
|
4705
|
+
exports.profile = profile;
|
|
4706
|
+
exports.propagateTrace = propagateTrace;
|
|
4707
|
+
exports.refreshFlags = refreshFlags;
|
|
4708
|
+
exports.resetAnalytics = resetAnalytics;
|
|
4709
|
+
exports.resetDbQueryCount = resetDbQueryCount;
|
|
4710
|
+
exports.setAnonymousId = setAnonymousId;
|
|
221
4711
|
exports.setContext = setContext;
|
|
222
4712
|
exports.setTags = setTags;
|
|
223
4713
|
exports.setUser = setUser;
|
|
4714
|
+
exports.startProfileSpan = startProfileSpan;
|
|
4715
|
+
exports.startReplay = startReplay;
|
|
4716
|
+
exports.startSpan = startSpan;
|
|
4717
|
+
exports.startTransaction = startTransaction;
|
|
4718
|
+
exports.stopReplay = stopReplay;
|
|
4719
|
+
exports.track = track;
|
|
4720
|
+
exports.trackPageView = trackPageView;
|
|
4721
|
+
exports.waitForFlags = waitForFlags;
|
|
224
4722
|
//# sourceMappingURL=index.js.map
|
|
225
4723
|
//# sourceMappingURL=index.js.map
|