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/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 DEFAULT_ENDPOINT = "https://ingest.sitepong.com";
7
- var DEFAULT_BATCH_SIZE = 10;
8
- var DEFAULT_FLUSH_INTERVAL = 5e3;
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: DEFAULT_ENDPOINT,
4039
+ endpoint: DEFAULT_ENDPOINT6,
18
4040
  environment: "production",
19
4041
  release: "",
20
4042
  autoCapture: true,
21
- maxBatchSize: DEFAULT_BATCH_SIZE,
22
- flushInterval: DEFAULT_FLUSH_INTERVAL,
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 : void 0;
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