sitepong 0.2.0 → 0.2.2

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.
@@ -626,6 +626,60 @@ function clearSession() {
626
626
  memorySessionTs = null;
627
627
  }
628
628
 
629
+ // src/analytics/utm.ts
630
+ var STORAGE_KEY2 = "sp_utm";
631
+ var UTM_KEYS = [
632
+ ["source", "utm_source"],
633
+ ["medium", "utm_medium"],
634
+ ["campaign", "utm_campaign"],
635
+ ["term", "utm_term"],
636
+ ["content", "utm_content"]
637
+ ];
638
+ function parseFromLocation() {
639
+ if (typeof window === "undefined" || !window.location || !window.location.search) return null;
640
+ let params;
641
+ try {
642
+ params = new URLSearchParams(window.location.search);
643
+ } catch {
644
+ return null;
645
+ }
646
+ const result = {};
647
+ let found = false;
648
+ for (const [key, queryKey] of UTM_KEYS) {
649
+ const value = params.get(queryKey);
650
+ if (value) {
651
+ result[key] = value;
652
+ found = true;
653
+ }
654
+ }
655
+ return found ? result : null;
656
+ }
657
+ function readStored() {
658
+ if (typeof sessionStorage === "undefined") return null;
659
+ try {
660
+ const raw = sessionStorage.getItem(STORAGE_KEY2);
661
+ if (!raw) return null;
662
+ const parsed = JSON.parse(raw);
663
+ return parsed && typeof parsed === "object" ? parsed : null;
664
+ } catch {
665
+ return null;
666
+ }
667
+ }
668
+ function writeStored(utm) {
669
+ if (typeof sessionStorage === "undefined") return;
670
+ try {
671
+ sessionStorage.setItem(STORAGE_KEY2, JSON.stringify(utm));
672
+ } catch {
673
+ }
674
+ }
675
+ function getSessionUtm() {
676
+ const stored = readStored();
677
+ if (stored) return stored;
678
+ const fresh = parseFromLocation();
679
+ if (fresh) writeStored(fresh);
680
+ return fresh;
681
+ }
682
+
629
683
  // src/analytics/autocapture.ts
630
684
  var DEFAULT_BLOCK_SELECTORS = [
631
685
  "[data-sp-no-capture]",
@@ -634,6 +688,7 @@ var DEFAULT_BLOCK_SELECTORS = [
634
688
  var MAX_TEXT_LENGTH = 255;
635
689
  var AutocaptureModule = class {
636
690
  constructor(config, callback) {
691
+ this.listeners = [];
637
692
  this.clickHandler = null;
638
693
  this.submitHandler = null;
639
694
  this.active = false;
@@ -648,6 +703,27 @@ var AutocaptureModule = class {
648
703
  };
649
704
  this.callback = callback;
650
705
  }
706
+ /**
707
+ * Register a secondary listener that receives the same events as the
708
+ * primary callback (e.g. Watchtower tap capture reusing the single click
709
+ * listener + selector resolution). Returns an unsubscribe function.
710
+ */
711
+ addListener(cb) {
712
+ this.listeners.push(cb);
713
+ return () => {
714
+ const idx = this.listeners.indexOf(cb);
715
+ if (idx >= 0) this.listeners.splice(idx, 1);
716
+ };
717
+ }
718
+ emit(event) {
719
+ this.callback(event);
720
+ for (const listener of this.listeners) {
721
+ try {
722
+ listener(event);
723
+ } catch {
724
+ }
725
+ }
726
+ }
651
727
  start() {
652
728
  if (this.active || typeof document === "undefined" || typeof document.addEventListener !== "function") return;
653
729
  this.active = true;
@@ -680,7 +756,7 @@ var AutocaptureModule = class {
680
756
  properties.$event_type = "click";
681
757
  properties.$x = e.clientX;
682
758
  properties.$y = e.clientY;
683
- this.callback({
759
+ this.emit({
684
760
  type: "click",
685
761
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
686
762
  properties
@@ -702,7 +778,7 @@ var AutocaptureModule = class {
702
778
  };
703
779
  const elemProps = this.getElementProperties(form);
704
780
  Object.assign(properties, elemProps);
705
- this.callback({
781
+ this.emit({
706
782
  type: "form_submit",
707
783
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
708
784
  properties
@@ -919,6 +995,14 @@ var AnalyticsManager = class {
919
995
  );
920
996
  this.autocaptureModule.start();
921
997
  }
998
+ /**
999
+ * The live AutocaptureModule (if click/form autocapture is enabled) —
1000
+ * Watchtower tap capture attaches to it so there is only ONE document-level
1001
+ * click listener and one selector/element resolution path.
1002
+ */
1003
+ getAutocaptureModule() {
1004
+ return this.autocaptureModule;
1005
+ }
922
1006
  track(eventName, properties) {
923
1007
  if (!this.config.enabled) return;
924
1008
  const event = this.createEvent("track", { name: eventName, properties });
@@ -1032,7 +1116,9 @@ var AnalyticsManager = class {
1032
1116
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1033
1117
  url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
1034
1118
  referrer: typeof document !== "undefined" && typeof document.referrer === "string" ? document.referrer : void 0,
1035
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
1119
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1120
+ utm: getSessionUtm() || void 0,
1121
+ appVersion: this.config.appVersion || void 0
1036
1122
  };
1037
1123
  }
1038
1124
  enqueue(event) {
@@ -1730,7 +1816,7 @@ var DEFAULT_REMOTE_CONFIG = {
1730
1816
  };
1731
1817
 
1732
1818
  // src/remote-config/manager.ts
1733
- var STORAGE_KEY2 = "sitepong_remote_config";
1819
+ var STORAGE_KEY3 = "sitepong_remote_config";
1734
1820
  var STORAGE_TS_KEY = "sitepong_remote_config_ts";
1735
1821
  var RemoteConfigManager = class {
1736
1822
  constructor(options) {
@@ -1773,7 +1859,7 @@ var RemoteConfigManager = class {
1773
1859
  const storage = this.options.storage;
1774
1860
  if (!storage) return;
1775
1861
  try {
1776
- const cached = await storage.getItem(STORAGE_KEY2);
1862
+ const cached = await storage.getItem(STORAGE_KEY3);
1777
1863
  const cachedTs = await storage.getItem(STORAGE_TS_KEY);
1778
1864
  if (cached && cachedTs) {
1779
1865
  const age = Date.now() - parseInt(cachedTs, 10);
@@ -1834,7 +1920,7 @@ var RemoteConfigManager = class {
1834
1920
  const storage = this.options.storage;
1835
1921
  if (!storage) return;
1836
1922
  try {
1837
- await storage.setItem(STORAGE_KEY2, JSON.stringify(this.config));
1923
+ await storage.setItem(STORAGE_KEY3, JSON.stringify(this.config));
1838
1924
  await storage.setItem(STORAGE_TS_KEY, String(Date.now()));
1839
1925
  } catch {
1840
1926
  this.log("Failed to cache remote config");
@@ -1868,781 +1954,282 @@ var RemoteConfigManager = class {
1868
1954
  }
1869
1955
  }
1870
1956
  };
1871
- var DEFAULT_ENDPOINT3 = "https://ingest.sitepong.com";
1872
- var DEFAULT_FLUSH_INTERVAL2 = 1e4;
1873
- var MAX_FLUSH_FAILURES2 = 3;
1874
- var MAX_RESOURCES_PER_FLUSH = 200;
1875
- var PAGE_TIMING_POLL_INTERVAL = 200;
1876
- var PAGE_TIMING_TIMEOUT = 3e4;
1877
- function getPaintBlocks(resources) {
1878
- const paintBlocks = [];
1879
- const elements = document.getElementsByTagName("*");
1880
- const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i;
1881
- for (let i = 0; i < elements.length; i++) {
1882
- const element = elements[i];
1883
- let src = "";
1884
- if (element.tagName === "IMG") {
1885
- src = element.currentSrc || element.src;
1886
- }
1887
- if (!src) {
1888
- const backgroundImage = getComputedStyle(element).getPropertyValue("background-image");
1889
- if (backgroundImage) {
1890
- const matches = styleURL.exec(backgroundImage);
1891
- if (matches !== null) {
1892
- src = matches[1];
1893
- if (src.startsWith('"') || src.startsWith("'")) {
1894
- src = src.substr(1, src.length - 2);
1895
- }
1896
- }
1897
- }
1957
+
1958
+ // src/performance/tracing.ts
1959
+ function randomHex(byteLength) {
1960
+ try {
1961
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
1962
+ const bytes = new Uint8Array(byteLength);
1963
+ crypto.getRandomValues(bytes);
1964
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
1898
1965
  }
1899
- if (!src) continue;
1900
- const time = src.substr(0, 10) === "data:image" ? 0 : resources[src];
1901
- if (time === void 0) continue;
1902
- const rect = element.getBoundingClientRect();
1903
- const top = Math.max(rect.top, 0);
1904
- const left = Math.max(rect.left, 0);
1905
- const bottom = Math.min(
1906
- rect.bottom,
1907
- window.innerHeight || document.documentElement && document.documentElement.clientHeight || 0
1908
- );
1909
- const right = Math.min(
1910
- rect.right,
1911
- window.innerWidth || document.documentElement && document.documentElement.clientWidth || 0
1912
- );
1913
- if (bottom <= top || right <= left) continue;
1914
- const area = (bottom - top) * (right - left);
1915
- paintBlocks.push({ time, area });
1966
+ } catch {
1916
1967
  }
1917
- return paintBlocks;
1918
- }
1919
- function calculateSpeedIndex(firstContentfulPaint, paintBlocks) {
1920
- let a = Math.max(
1921
- document.documentElement && document.documentElement.clientWidth || 0,
1922
- window.innerWidth || 0
1923
- ) * Math.max(
1924
- document.documentElement && document.documentElement.clientHeight || 0,
1925
- window.innerHeight || 0
1926
- ) / 10;
1927
- let s = a * firstContentfulPaint;
1928
- for (let i = 0; i < paintBlocks.length; i++) {
1929
- const { time, area } = paintBlocks[i];
1930
- a += area;
1931
- s += area * (time > firstContentfulPaint ? time : firstContentfulPaint);
1968
+ let hex = "";
1969
+ for (let i = 0; i < byteLength; i++) {
1970
+ hex += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
1932
1971
  }
1933
- return a === 0 ? 0 : s / a;
1972
+ return hex;
1934
1973
  }
1935
- var PerformanceManager = class {
1936
- constructor(config) {
1937
- this.metrics = [];
1938
- this.flushTimer = null;
1939
- this.flushFailures = 0;
1940
- this.disabled = false;
1941
- this.vitals = {};
1942
- this.activeTransactions = /* @__PURE__ */ new Map();
1943
- // Resource timing state for Speed Index
1944
- this.resourceObserver = null;
1945
- this.resourceTimeMap = {};
1946
- this.resourceCount = 0;
1947
- // Page timing polling timers
1948
- this.pageLoadTimer = null;
1949
- this.pageRenderTimer = null;
1950
- this.config = {
1951
- endpoint: DEFAULT_ENDPOINT3,
1952
- enabled: true,
1953
- webVitals: true,
1954
- navigationTiming: true,
1955
- resourceTiming: true,
1956
- capturePageLoadTimings: true,
1957
- capturePageRenderTimings: true,
1958
- excludedResourceUrls: [],
1959
- flushInterval: DEFAULT_FLUSH_INTERVAL2,
1960
- sampleRate: 1,
1961
- debug: false,
1962
- ...config
1963
- };
1964
- this.sampled = Math.random() <= this.config.sampleRate;
1974
+ function generateTraceId() {
1975
+ return randomHex(16);
1976
+ }
1977
+ function generateSpanId() {
1978
+ return randomHex(8);
1979
+ }
1980
+ function createTraceContext() {
1981
+ return {
1982
+ traceId: generateTraceId(),
1983
+ spanId: generateSpanId(),
1984
+ parentSpanId: null,
1985
+ sampled: true
1986
+ };
1987
+ }
1988
+ function propagateTrace(context) {
1989
+ const sampledFlag = context.sampled ? "01" : "00";
1990
+ return {
1991
+ traceparent: `00-${context.traceId}-${context.spanId}-${sampledFlag}`,
1992
+ tracestate: `sitepong=${context.spanId}`
1993
+ };
1994
+ }
1995
+ function extractTrace(headers) {
1996
+ const traceparent = headers["traceparent"] || headers["Traceparent"];
1997
+ if (!traceparent) {
1998
+ return null;
1965
1999
  }
1966
- init() {
1967
- if (!this.config.enabled || !this.sampled) return;
1968
- if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
1969
- if (this.config.webVitals !== false) {
1970
- this.initWebVitals();
1971
- }
1972
- if (this.config.resourceTiming !== false) {
1973
- this.initResourceTiming();
1974
- }
1975
- if (this.config.capturePageLoadTimings !== false) {
1976
- this.initPageLoadTiming();
1977
- }
1978
- if (this.config.capturePageRenderTimings !== false) {
1979
- this.initPageRenderTiming();
1980
- }
1981
- this.startFlushTimer();
1982
- this.log("Performance monitoring initialized");
2000
+ const parts = traceparent.split("-");
2001
+ if (parts.length !== 4) {
2002
+ return null;
1983
2003
  }
1984
- // ---- Web Vitals (via web-vitals library) ----
1985
- initWebVitals() {
1986
- webVitals.onCLS((m) => {
1987
- this.vitals.cls = m.value;
1988
- this.reportVital("CLS", m.value, "score");
1989
- });
1990
- webVitals.onINP((m) => {
1991
- this.vitals.inp = m.value;
1992
- this.reportVital("INP", m.value);
1993
- });
1994
- webVitals.onLCP((m) => {
1995
- this.vitals.lcp = m.value;
1996
- this.reportVital("LCP", m.value);
1997
- });
1998
- webVitals.onTTFB((m) => {
1999
- this.vitals.ttfb = m.value;
2000
- this.reportVital("TTFB", m.value);
2001
- });
2002
- webVitals.onFCP((m) => {
2003
- this.vitals.fcp = m.value;
2004
- this.reportVital("FCP", m.value);
2005
- });
2004
+ const [version, traceId, parentSpanId, flags] = parts;
2005
+ if (version !== "00") {
2006
+ return null;
2006
2007
  }
2007
- // ---- Resource Timing (adapted from OpenReplay timing.ts) ----
2008
- initResourceTiming() {
2009
- if (typeof PerformanceObserver === "undefined") return;
2010
- try {
2011
- this.resourceObserver = new PerformanceObserver((list) => {
2012
- for (const entry of list.getEntries()) {
2013
- this.processResourceEntry(entry);
2014
- }
2015
- });
2016
- this.resourceObserver.observe({ type: "resource", buffered: true });
2017
- } catch {
2018
- this.log("PerformanceObserver for resource timing not supported");
2019
- }
2008
+ if (traceId.length !== 32 || !/^[0-9a-f]{32}$/.test(traceId)) {
2009
+ return null;
2020
2010
  }
2021
- isServiceURL(url) {
2022
- const endpoint = this.config.performanceEndpoint || this.config.endpoint;
2023
- return url.startsWith(endpoint);
2011
+ if (parentSpanId.length !== 16 || !/^[0-9a-f]{16}$/.test(parentSpanId)) {
2012
+ return null;
2024
2013
  }
2025
- processResourceEntry(entry) {
2026
- if (entry.duration < 0 || !entry.name.startsWith("http")) return;
2027
- if (this.isServiceURL(entry.name)) return;
2028
- if (this.resourceTimeMap !== null) {
2029
- this.resourceTimeMap[entry.name] = entry.startTime + entry.duration;
2030
- }
2031
- for (const excluded of this.config.excludedResourceUrls) {
2032
- if (entry.name.startsWith(excluded)) return;
2033
- }
2034
- if (this.resourceCount >= MAX_RESOURCES_PER_FLUSH) return;
2035
- this.resourceCount++;
2036
- let stalled = 0;
2037
- if (entry.connectEnd && entry.connectEnd > entry.domainLookupEnd) {
2038
- stalled = Math.max(0, entry.requestStart - entry.connectEnd);
2039
- } else {
2040
- stalled = Math.max(0, entry.requestStart - entry.domainLookupEnd);
2014
+ if (flags.length !== 2 || !/^[0-9a-f]{2}$/.test(flags)) {
2015
+ return null;
2016
+ }
2017
+ const sampled = (parseInt(flags, 16) & 1) === 1;
2018
+ return {
2019
+ traceId,
2020
+ spanId: generateSpanId(),
2021
+ parentSpanId,
2022
+ sampled
2023
+ };
2024
+ }
2025
+ var TracePropagator = class {
2026
+ constructor(targets) {
2027
+ this.originalFetch = null;
2028
+ this.targets = targets;
2029
+ }
2030
+ shouldPropagate(url) {
2031
+ return this.targets.some((pattern) => {
2032
+ if (pattern.includes("*")) {
2033
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
2034
+ const regex = new RegExp(`^${escaped}$`);
2035
+ return regex.test(url);
2036
+ }
2037
+ return url.startsWith(pattern);
2038
+ });
2039
+ }
2040
+ injectHeaders(url, headers, context) {
2041
+ if (!this.shouldPropagate(url)) {
2042
+ return headers;
2041
2043
  }
2042
- const cached = entry.responseStatus && entry.responseStatus === 304 || entry.deliveryType && entry.deliveryType === "cache" || entry.transferSize === 0 && entry.decodedBodySize > 0;
2043
- const responseStatus = entry.responseStatus || 0;
2044
- const failed = responseStatus >= 400;
2045
- const breakdown = {
2046
- queueing: entry.requestStart - entry.fetchStart,
2047
- dnsLookup: entry.domainLookupEnd - entry.domainLookupStart,
2048
- initialConnection: entry.connectEnd - entry.connectStart,
2049
- ssl: entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0,
2050
- ttfb: entry.responseStart - entry.requestStart,
2051
- contentDownload: entry.responseEnd - entry.responseStart,
2052
- total: entry.duration ?? entry.responseEnd - entry.startTime,
2053
- stalled,
2054
- cached,
2055
- failed,
2056
- responseStatus,
2057
- headerSize: entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
2058
- encodedBodySize: entry.encodedBodySize || 0,
2059
- decodedBodySize: failed ? -111 : entry.decodedBodySize || 0,
2060
- transferSize: entry.transferSize,
2061
- initiatorType: entry.initiatorType
2044
+ const traceHeaders = propagateTrace(context);
2045
+ return {
2046
+ ...headers,
2047
+ ...traceHeaders
2062
2048
  };
2063
- const entryName = this.config.resourceNameSanitizer ? this.config.resourceNameSanitizer(entry.name) : entry.name;
2064
- this.addMetric({
2065
- type: "resource",
2066
- name: entryName,
2067
- value: entry.duration,
2068
- unit: "ms",
2069
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2070
- url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
2071
- data: breakdown
2072
- });
2073
2049
  }
2074
- // ---- Page Load Timing (adapted from OpenReplay timing.ts) ----
2075
- initPageLoadTiming() {
2076
- if (typeof window === "undefined" || !window.performance) return;
2077
- let firstPaint = 0;
2078
- let firstContentfulPaint = 0;
2079
- let sent = false;
2080
- const startTime = performance.now();
2081
- this.pageLoadTimer = setInterval(() => {
2082
- if (sent) {
2083
- if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
2084
- return;
2085
- }
2086
- if (firstPaint === 0 || firstContentfulPaint === 0) {
2087
- for (const entry of performance.getEntriesByType("paint")) {
2088
- if (entry.name === "first-paint") firstPaint = entry.startTime;
2089
- if (entry.name === "first-contentful-paint") firstContentfulPaint = entry.startTime;
2090
- }
2091
- }
2092
- const nav = performance.getEntriesByType("navigation")[0];
2093
- const timedOut = performance.now() - startTime > PAGE_TIMING_TIMEOUT;
2094
- if (nav && nav.loadEventEnd > 0 || timedOut) {
2095
- sent = true;
2096
- if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
2097
- if (nav) {
2098
- const navigationStart = nav.startTime;
2099
- this.addMetric({
2100
- type: "navigation",
2101
- name: "page_load",
2102
- value: nav.loadEventEnd - navigationStart,
2103
- unit: "ms",
2104
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2105
- url: window.location.href,
2106
- data: {
2107
- requestStart: nav.requestStart - navigationStart,
2108
- responseStart: nav.responseStart - navigationStart,
2109
- responseEnd: nav.responseEnd - navigationStart,
2110
- domContentLoadedEventStart: nav.domContentLoadedEventEnd - navigationStart,
2111
- domContentLoadedEventEnd: nav.domContentLoadedEventEnd - navigationStart,
2112
- loadEventStart: nav.loadEventStart - navigationStart,
2113
- loadEventEnd: nav.loadEventEnd - navigationStart,
2114
- firstPaint,
2115
- firstContentfulPaint,
2116
- dns: nav.domainLookupEnd - nav.domainLookupStart,
2117
- tcp: nav.connectEnd - nav.connectStart,
2118
- ttfb: nav.responseStart - nav.requestStart,
2119
- domComplete: nav.domComplete - navigationStart,
2120
- transferSize: nav.transferSize,
2121
- encodedBodySize: nav.encodedBodySize,
2122
- decodedBodySize: nav.decodedBodySize
2050
+ start() {
2051
+ if (typeof window === "undefined" || !window.fetch) {
2052
+ return;
2053
+ }
2054
+ if (this.originalFetch) {
2055
+ return;
2056
+ }
2057
+ this.originalFetch = window.fetch;
2058
+ const self = this;
2059
+ window.fetch = function(input, init2) {
2060
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2061
+ if (self.shouldPropagate(url)) {
2062
+ const context = createTraceContext();
2063
+ const traceHeaders = propagateTrace(context);
2064
+ const existingHeaders = {};
2065
+ if (init2?.headers) {
2066
+ if (init2.headers instanceof Headers) {
2067
+ init2.headers.forEach((value, key) => {
2068
+ existingHeaders[key] = value;
2069
+ });
2070
+ } else if (Array.isArray(init2.headers)) {
2071
+ for (const [key, value] of init2.headers) {
2072
+ existingHeaders[key] = value;
2123
2073
  }
2124
- });
2125
- }
2126
- }
2127
- }, PAGE_TIMING_POLL_INTERVAL);
2128
- }
2129
- // ---- Page Render Timing: Speed Index, Visually Complete, TTI (adapted from OpenReplay) ----
2130
- initPageRenderTiming() {
2131
- if (typeof window === "undefined" || !window.performance) return;
2132
- let firstContentfulPaint = 0;
2133
- let visuallyComplete = 0;
2134
- let interactiveWindowStartTime = 0;
2135
- let interactiveWindowTickTime = 0;
2136
- let paintBlocks = null;
2137
- let sent = false;
2138
- const startTime = performance.now();
2139
- this.pageRenderTimer = setInterval(() => {
2140
- if (sent) {
2141
- if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
2142
- return;
2143
- }
2144
- const time = performance.now();
2145
- if (firstContentfulPaint === 0) {
2146
- for (const entry of performance.getEntriesByType("paint")) {
2147
- if (entry.name === "first-contentful-paint") {
2148
- firstContentfulPaint = entry.startTime;
2074
+ } else {
2075
+ Object.assign(existingHeaders, init2.headers);
2149
2076
  }
2150
2077
  }
2078
+ const mergedHeaders = {
2079
+ ...existingHeaders,
2080
+ ...traceHeaders
2081
+ };
2082
+ const patchedInit = {
2083
+ ...init2,
2084
+ headers: mergedHeaders
2085
+ };
2086
+ return self.originalFetch.call(window, input, patchedInit);
2151
2087
  }
2152
- if (this.resourceTimeMap !== null) {
2153
- const times = Object.values(this.resourceTimeMap);
2154
- if (times.length > 0) {
2155
- visuallyComplete = Math.max(...times);
2156
- }
2157
- if (time - visuallyComplete > 1e3) {
2158
- paintBlocks = getPaintBlocks(this.resourceTimeMap);
2159
- this.resourceTimeMap = null;
2160
- }
2161
- }
2162
- if (interactiveWindowTickTime !== null) {
2163
- if (time - interactiveWindowTickTime > 50) {
2164
- interactiveWindowStartTime = time;
2165
- }
2166
- interactiveWindowTickTime = time - interactiveWindowStartTime > 5e3 ? null : time;
2167
- }
2168
- const timedOut = time - startTime > PAGE_TIMING_TIMEOUT;
2169
- if (paintBlocks !== null && interactiveWindowTickTime === null || timedOut) {
2170
- sent = true;
2171
- if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
2172
- this.resourceTimeMap = null;
2173
- const speedIndex = paintBlocks === null ? 0 : calculateSpeedIndex(firstContentfulPaint || 0, paintBlocks);
2174
- const nav = performance.getEntriesByType("navigation")[0];
2175
- const domContentLoadedEnd = nav ? nav.domContentLoadedEventEnd - nav.startTime : 0;
2176
- const timeToInteractive = interactiveWindowTickTime === null ? Math.max(interactiveWindowStartTime, firstContentfulPaint, domContentLoadedEnd) : 0;
2177
- const url = window.location.href;
2178
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2179
- if (speedIndex > 0) {
2180
- this.addMetric({
2181
- type: "page_render",
2182
- name: "speed_index",
2183
- value: Math.round(speedIndex),
2184
- unit: "ms",
2185
- timestamp,
2186
- url
2187
- });
2188
- }
2189
- if (visuallyComplete > 0) {
2190
- this.addMetric({
2191
- type: "page_render",
2192
- name: "visually_complete",
2193
- value: Math.round(visuallyComplete),
2194
- unit: "ms",
2195
- timestamp,
2196
- url
2197
- });
2198
- }
2199
- if (timeToInteractive > 0) {
2200
- this.addMetric({
2201
- type: "page_render",
2202
- name: "tti",
2203
- value: Math.round(timeToInteractive),
2204
- unit: "ms",
2205
- timestamp,
2206
- url
2207
- });
2208
- }
2209
- }
2210
- }, PAGE_TIMING_POLL_INTERVAL);
2211
- }
2212
- // ---- Transactions / Spans (unchanged) ----
2213
- startTransaction(name, data) {
2214
- const id = `txn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
2215
- const transaction = {
2216
- id,
2217
- name,
2218
- startTime: performance.now(),
2219
- status: "running",
2220
- spans: [],
2221
- data
2088
+ return self.originalFetch.call(window, input, init2);
2222
2089
  };
2223
- this.activeTransactions.set(id, transaction);
2224
- this.log("Transaction started:", name, id);
2225
- return id;
2226
2090
  }
2227
- endTransaction(id, status = "ok") {
2228
- const transaction = this.activeTransactions.get(id);
2229
- if (!transaction) {
2230
- this.log("Transaction not found:", id);
2091
+ stop() {
2092
+ if (typeof window === "undefined") {
2231
2093
  return;
2232
2094
  }
2233
- transaction.endTime = performance.now();
2234
- transaction.duration = transaction.endTime - transaction.startTime;
2235
- transaction.status = status;
2236
- for (const span of transaction.spans) {
2237
- if (span.status === "running") {
2238
- span.endTime = transaction.endTime;
2239
- span.duration = span.endTime - span.startTime;
2240
- span.status = status;
2241
- }
2095
+ if (this.originalFetch) {
2096
+ window.fetch = this.originalFetch;
2097
+ this.originalFetch = null;
2242
2098
  }
2243
- this.activeTransactions.delete(id);
2244
- this.addMetric({
2245
- type: "transaction",
2246
- name: transaction.name,
2247
- value: transaction.duration,
2248
- unit: "ms",
2249
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2250
- url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
2251
- transaction
2252
- });
2253
- this.log("Transaction ended:", transaction.name, `${transaction.duration.toFixed(1)}ms`);
2254
2099
  }
2255
- startSpan(transactionId, name, data) {
2256
- const transaction = this.activeTransactions.get(transactionId);
2257
- if (!transaction) {
2258
- this.log("Transaction not found for span:", transactionId);
2259
- return "";
2260
- }
2261
- const span = {
2262
- id: `span_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
2263
- name,
2264
- startTime: performance.now(),
2265
- status: "running",
2266
- data
2100
+ };
2101
+
2102
+ // src/superlink/client.ts
2103
+ var DEFAULT_SUPERLINK_ENDPOINT = "https://pongl.ink";
2104
+ var SuperLinkClient = class {
2105
+ constructor(config = {}) {
2106
+ this.config = {
2107
+ endpoint: stripTrailingSlash(config.endpoint || DEFAULT_SUPERLINK_ENDPOINT),
2108
+ appId: config.appId,
2109
+ installId: config.installId,
2110
+ debug: config.debug ?? false
2267
2111
  };
2268
- transaction.spans.push(span);
2269
- return span.id;
2270
- }
2271
- endSpan(transactionId, spanId, status = "ok") {
2272
- const transaction = this.activeTransactions.get(transactionId);
2273
- if (!transaction) return;
2274
- const span = transaction.spans.find((s) => s.id === spanId);
2275
- if (!span) return;
2276
- span.endTime = performance.now();
2277
- span.duration = span.endTime - span.startTime;
2278
- span.status = status;
2279
- }
2280
- // ---- Public API ----
2281
- getVitals() {
2282
- return { ...this.vitals };
2283
- }
2284
- destroy() {
2285
- if (this.flushTimer) {
2286
- clearInterval(this.flushTimer);
2287
- this.flushTimer = null;
2288
- }
2289
- if (this.pageLoadTimer) {
2290
- clearInterval(this.pageLoadTimer);
2291
- this.pageLoadTimer = null;
2292
- }
2293
- if (this.pageRenderTimer) {
2294
- clearInterval(this.pageRenderTimer);
2295
- this.pageRenderTimer = null;
2296
- }
2297
- if (this.resourceObserver) {
2298
- this.resourceObserver.disconnect();
2299
- this.resourceObserver = null;
2300
- }
2301
- this.flush();
2302
2112
  }
2303
- // ---- Internal ----
2304
- reportVital(name, value, unit = "ms") {
2305
- this.addMetric({
2306
- type: "web_vital",
2307
- name,
2308
- value,
2309
- unit,
2310
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2311
- url: typeof window !== "undefined" && window.location ? window.location.href : void 0
2312
- });
2113
+ /** Replace config in place (used by init() so module-level helpers see updates). */
2114
+ configure(config) {
2115
+ if (config.endpoint) this.config.endpoint = stripTrailingSlash(config.endpoint);
2116
+ if (config.appId !== void 0) this.config.appId = config.appId;
2117
+ if (config.installId !== void 0) this.config.installId = config.installId;
2118
+ if (config.debug !== void 0) this.config.debug = config.debug;
2313
2119
  }
2314
- addMetric(metric) {
2315
- this.metrics.push(metric);
2120
+ log(...args) {
2121
+ if (this.config.debug) console.warn("[SuperLink]", ...args);
2316
2122
  }
2317
- startFlushTimer() {
2318
- this.flushTimer = setInterval(() => {
2319
- this.flush();
2320
- }, this.config.flushInterval);
2321
- if (typeof document !== "undefined") {
2322
- document.addEventListener("visibilitychange", () => {
2323
- if (document.visibilityState === "hidden") {
2324
- this.flush();
2325
- }
2123
+ /**
2124
+ * POST /match ask the redirect engine to resolve a deferred deep link from
2125
+ * a click token (Android referrer / iOS clipboard) and/or a device
2126
+ * fingerprint. Returns `{ matched: false }` on any error.
2127
+ */
2128
+ async match(body) {
2129
+ const payload = {
2130
+ ...body,
2131
+ install_id: body.install_id ?? this.config.installId
2132
+ };
2133
+ try {
2134
+ const res = await fetch(`${this.config.endpoint}/match`, {
2135
+ method: "POST",
2136
+ headers: { "Content-Type": "application/json" },
2137
+ body: JSON.stringify(payload)
2326
2138
  });
2139
+ if (!res.ok) {
2140
+ this.log("match failed", res.status);
2141
+ return { matched: false };
2142
+ }
2143
+ const data = await res.json();
2144
+ return data ?? { matched: false };
2145
+ } catch (err) {
2146
+ this.log("match error", err);
2147
+ return { matched: false };
2327
2148
  }
2328
2149
  }
2329
- async flush() {
2330
- if (this.metrics.length === 0) return;
2331
- if (this.disabled) {
2332
- this.metrics = [];
2333
- return;
2334
- }
2335
- const metrics = [...this.metrics];
2336
- this.metrics = [];
2337
- this.resourceCount = 0;
2338
- const endpoint = this.config.performanceEndpoint || `${this.config.endpoint}/api/performance`;
2150
+ /**
2151
+ * POST /events — report a lifecycle event (opened / converted) back to the
2152
+ * redirect engine, which fans out to analytics + webhooks. Best-effort.
2153
+ */
2154
+ async reportEvent(input) {
2339
2155
  try {
2340
- const response = await fetch(endpoint, {
2156
+ const res = await fetch(`${this.config.endpoint}/events`, {
2341
2157
  method: "POST",
2342
- headers: {
2343
- "Content-Type": "application/json",
2344
- "X-API-Key": this.config.apiKey
2345
- },
2346
- body: JSON.stringify({ metrics })
2158
+ headers: { "Content-Type": "application/json" },
2159
+ body: JSON.stringify({
2160
+ app_id: this.config.appId,
2161
+ install_id: this.config.installId,
2162
+ ...input
2163
+ })
2347
2164
  });
2348
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
2349
- this.flushFailures = 0;
2350
- this.log(`Flushed ${metrics.length} performance metrics`);
2165
+ if (!res.ok) this.log("event failed", input.type, res.status);
2166
+ return res.ok;
2351
2167
  } catch (err) {
2352
- this.flushFailures++;
2353
- if (this.flushFailures >= MAX_FLUSH_FAILURES2) {
2354
- this.disabled = true;
2355
- console.warn(
2356
- `[SitePong Performance] Disabled after ${MAX_FLUSH_FAILURES2} consecutive failures. Check that ${endpoint} is accessible and has proper CORS headers.`
2357
- );
2358
- return;
2359
- }
2360
- this.metrics.unshift(...metrics);
2361
- this.log(`Failed to flush metrics (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES2}):`, err);
2362
- }
2363
- }
2364
- log(...args) {
2365
- if (this.config.debug) {
2366
- console.log("[SitePong Performance]", ...args);
2168
+ this.log("event error", err);
2169
+ return false;
2367
2170
  }
2368
2171
  }
2369
2172
  };
2173
+ function stripTrailingSlash(url) {
2174
+ return url.endsWith("/") ? url.slice(0, -1) : url;
2175
+ }
2176
+ var superlinkClient = new SuperLinkClient();
2370
2177
 
2371
- // src/performance/tracing.ts
2372
- function randomHex(byteLength) {
2178
+ // src/superlink/parse.ts
2179
+ var UTM_KEYS2 = [
2180
+ ["source", "utm_source"],
2181
+ ["medium", "utm_medium"],
2182
+ ["campaign", "utm_campaign"],
2183
+ ["term", "utm_term"],
2184
+ ["content", "utm_content"]
2185
+ ];
2186
+ function parseUniversalLink(url) {
2187
+ let parsed;
2373
2188
  try {
2374
- if (typeof crypto !== "undefined" && crypto.getRandomValues) {
2375
- const bytes = new Uint8Array(byteLength);
2376
- crypto.getRandomValues(bytes);
2377
- return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2378
- }
2189
+ parsed = new URL(url);
2379
2190
  } catch {
2191
+ return null;
2380
2192
  }
2381
- let hex = "";
2382
- for (let i = 0; i < byteLength; i++) {
2383
- hex += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
2193
+ const params = parsed.searchParams;
2194
+ const utm = {};
2195
+ for (const [key, queryKey] of UTM_KEYS2) {
2196
+ const value = params.get(queryKey);
2197
+ if (value) utm[key] = value;
2384
2198
  }
2385
- return hex;
2386
- }
2387
- function generateTraceId() {
2388
- return randomHex(16);
2389
- }
2390
- function generateSpanId() {
2391
- return randomHex(8);
2392
- }
2393
- function createTraceContext() {
2199
+ const referral = {};
2200
+ const deep_link_data = {};
2201
+ params.forEach((value, key) => {
2202
+ if (key.startsWith("r_")) {
2203
+ referral[key.slice(2)] = value;
2204
+ } else if (key.startsWith("~")) {
2205
+ deep_link_data[key.slice(1)] = value;
2206
+ }
2207
+ });
2208
+ const explicitPath = params.get("$deep_link_path") ?? params.get("dlp");
2209
+ const deep_link_path = explicitPath ?? (parsed.pathname && parsed.pathname !== "/" ? parsed.pathname : null);
2394
2210
  return {
2395
- traceId: generateTraceId(),
2396
- spanId: generateSpanId(),
2397
- parentSpanId: null,
2398
- sampled: true
2211
+ deep_link_path,
2212
+ deep_link_data,
2213
+ utm,
2214
+ referral,
2215
+ click_id: params.get("click_id"),
2216
+ match_type: "none",
2217
+ confidence: 1
2399
2218
  };
2400
2219
  }
2401
- function propagateTrace(context) {
2402
- const sampledFlag = context.sampled ? "01" : "00";
2220
+
2221
+ // src/superlink/deferred.ts
2222
+ var handlers = /* @__PURE__ */ new Set();
2223
+ var lastMatched = null;
2224
+ function toDeepLink(res) {
2403
2225
  return {
2404
- traceparent: `00-${context.traceId}-${context.spanId}-${sampledFlag}`,
2405
- tracestate: `sitepong=${context.spanId}`
2406
- };
2407
- }
2408
- function extractTrace(headers) {
2409
- const traceparent = headers["traceparent"] || headers["Traceparent"];
2410
- if (!traceparent) {
2411
- return null;
2412
- }
2413
- const parts = traceparent.split("-");
2414
- if (parts.length !== 4) {
2415
- return null;
2416
- }
2417
- const [version, traceId, parentSpanId, flags] = parts;
2418
- if (version !== "00") {
2419
- return null;
2420
- }
2421
- if (traceId.length !== 32 || !/^[0-9a-f]{32}$/.test(traceId)) {
2422
- return null;
2423
- }
2424
- if (parentSpanId.length !== 16 || !/^[0-9a-f]{16}$/.test(parentSpanId)) {
2425
- return null;
2426
- }
2427
- if (flags.length !== 2 || !/^[0-9a-f]{2}$/.test(flags)) {
2428
- return null;
2429
- }
2430
- const sampled = (parseInt(flags, 16) & 1) === 1;
2431
- return {
2432
- traceId,
2433
- spanId: generateSpanId(),
2434
- parentSpanId,
2435
- sampled
2436
- };
2437
- }
2438
- var TracePropagator = class {
2439
- constructor(targets) {
2440
- this.originalFetch = null;
2441
- this.targets = targets;
2442
- }
2443
- shouldPropagate(url) {
2444
- return this.targets.some((pattern) => {
2445
- if (pattern.includes("*")) {
2446
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
2447
- const regex = new RegExp(`^${escaped}$`);
2448
- return regex.test(url);
2449
- }
2450
- return url.startsWith(pattern);
2451
- });
2452
- }
2453
- injectHeaders(url, headers, context) {
2454
- if (!this.shouldPropagate(url)) {
2455
- return headers;
2456
- }
2457
- const traceHeaders = propagateTrace(context);
2458
- return {
2459
- ...headers,
2460
- ...traceHeaders
2461
- };
2462
- }
2463
- start() {
2464
- if (typeof window === "undefined" || !window.fetch) {
2465
- return;
2466
- }
2467
- if (this.originalFetch) {
2468
- return;
2469
- }
2470
- this.originalFetch = window.fetch;
2471
- const self = this;
2472
- window.fetch = function(input, init2) {
2473
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2474
- if (self.shouldPropagate(url)) {
2475
- const context = createTraceContext();
2476
- const traceHeaders = propagateTrace(context);
2477
- const existingHeaders = {};
2478
- if (init2?.headers) {
2479
- if (init2.headers instanceof Headers) {
2480
- init2.headers.forEach((value, key) => {
2481
- existingHeaders[key] = value;
2482
- });
2483
- } else if (Array.isArray(init2.headers)) {
2484
- for (const [key, value] of init2.headers) {
2485
- existingHeaders[key] = value;
2486
- }
2487
- } else {
2488
- Object.assign(existingHeaders, init2.headers);
2489
- }
2490
- }
2491
- const mergedHeaders = {
2492
- ...existingHeaders,
2493
- ...traceHeaders
2494
- };
2495
- const patchedInit = {
2496
- ...init2,
2497
- headers: mergedHeaders
2498
- };
2499
- return self.originalFetch.call(window, input, patchedInit);
2500
- }
2501
- return self.originalFetch.call(window, input, init2);
2502
- };
2503
- }
2504
- stop() {
2505
- if (typeof window === "undefined") {
2506
- return;
2507
- }
2508
- if (this.originalFetch) {
2509
- window.fetch = this.originalFetch;
2510
- this.originalFetch = null;
2511
- }
2512
- }
2513
- };
2514
-
2515
- // src/superlink/client.ts
2516
- var DEFAULT_SUPERLINK_ENDPOINT = "https://pongl.ink";
2517
- var SuperLinkClient = class {
2518
- constructor(config = {}) {
2519
- this.config = {
2520
- endpoint: stripTrailingSlash(config.endpoint || DEFAULT_SUPERLINK_ENDPOINT),
2521
- appId: config.appId,
2522
- installId: config.installId,
2523
- debug: config.debug ?? false
2524
- };
2525
- }
2526
- /** Replace config in place (used by init() so module-level helpers see updates). */
2527
- configure(config) {
2528
- if (config.endpoint) this.config.endpoint = stripTrailingSlash(config.endpoint);
2529
- if (config.appId !== void 0) this.config.appId = config.appId;
2530
- if (config.installId !== void 0) this.config.installId = config.installId;
2531
- if (config.debug !== void 0) this.config.debug = config.debug;
2532
- }
2533
- log(...args) {
2534
- if (this.config.debug) console.warn("[SuperLink]", ...args);
2535
- }
2536
- /**
2537
- * POST /match — ask the redirect engine to resolve a deferred deep link from
2538
- * a click token (Android referrer / iOS clipboard) and/or a device
2539
- * fingerprint. Returns `{ matched: false }` on any error.
2540
- */
2541
- async match(body) {
2542
- const payload = {
2543
- ...body,
2544
- install_id: body.install_id ?? this.config.installId
2545
- };
2546
- try {
2547
- const res = await fetch(`${this.config.endpoint}/match`, {
2548
- method: "POST",
2549
- headers: { "Content-Type": "application/json" },
2550
- body: JSON.stringify(payload)
2551
- });
2552
- if (!res.ok) {
2553
- this.log("match failed", res.status);
2554
- return { matched: false };
2555
- }
2556
- const data = await res.json();
2557
- return data ?? { matched: false };
2558
- } catch (err) {
2559
- this.log("match error", err);
2560
- return { matched: false };
2561
- }
2562
- }
2563
- /**
2564
- * POST /events — report a lifecycle event (opened / converted) back to the
2565
- * redirect engine, which fans out to analytics + webhooks. Best-effort.
2566
- */
2567
- async reportEvent(input) {
2568
- try {
2569
- const res = await fetch(`${this.config.endpoint}/events`, {
2570
- method: "POST",
2571
- headers: { "Content-Type": "application/json" },
2572
- body: JSON.stringify({
2573
- app_id: this.config.appId,
2574
- install_id: this.config.installId,
2575
- ...input
2576
- })
2577
- });
2578
- if (!res.ok) this.log("event failed", input.type, res.status);
2579
- return res.ok;
2580
- } catch (err) {
2581
- this.log("event error", err);
2582
- return false;
2583
- }
2584
- }
2585
- };
2586
- function stripTrailingSlash(url) {
2587
- return url.endsWith("/") ? url.slice(0, -1) : url;
2588
- }
2589
- var superlinkClient = new SuperLinkClient();
2590
-
2591
- // src/superlink/parse.ts
2592
- var UTM_KEYS = [
2593
- ["source", "utm_source"],
2594
- ["medium", "utm_medium"],
2595
- ["campaign", "utm_campaign"],
2596
- ["term", "utm_term"],
2597
- ["content", "utm_content"]
2598
- ];
2599
- function parseUniversalLink(url) {
2600
- let parsed;
2601
- try {
2602
- parsed = new URL(url);
2603
- } catch {
2604
- return null;
2605
- }
2606
- const params = parsed.searchParams;
2607
- const utm = {};
2608
- for (const [key, queryKey] of UTM_KEYS) {
2609
- const value = params.get(queryKey);
2610
- if (value) utm[key] = value;
2611
- }
2612
- const referral = {};
2613
- const deep_link_data = {};
2614
- params.forEach((value, key) => {
2615
- if (key.startsWith("r_")) {
2616
- referral[key.slice(2)] = value;
2617
- } else if (key.startsWith("~")) {
2618
- deep_link_data[key.slice(1)] = value;
2619
- }
2620
- });
2621
- const explicitPath = params.get("$deep_link_path") ?? params.get("dlp");
2622
- const deep_link_path = explicitPath ?? (parsed.pathname && parsed.pathname !== "/" ? parsed.pathname : null);
2623
- return {
2624
- deep_link_path,
2625
- deep_link_data,
2626
- utm,
2627
- referral,
2628
- click_id: params.get("click_id"),
2629
- match_type: "none",
2630
- confidence: 1
2631
- };
2632
- }
2633
-
2634
- // src/superlink/deferred.ts
2635
- var handlers = /* @__PURE__ */ new Set();
2636
- var lastMatched = null;
2637
- function toDeepLink(res) {
2638
- return {
2639
- deep_link_path: res.deep_link_path ?? null,
2640
- deep_link_data: res.deep_link_data ?? {},
2641
- utm: res.utm ?? {},
2642
- referral: res.referral ?? {},
2643
- click_id: res.click_id ?? null,
2644
- match_type: res.match_type ?? "fingerprint",
2645
- confidence: res.confidence ?? (res.match_type && res.match_type !== "none" ? 1 : 0)
2226
+ deep_link_path: res.deep_link_path ?? null,
2227
+ deep_link_data: res.deep_link_data ?? {},
2228
+ utm: res.utm ?? {},
2229
+ referral: res.referral ?? {},
2230
+ click_id: res.click_id ?? null,
2231
+ match_type: res.match_type ?? "fingerprint",
2232
+ confidence: res.confidence ?? (res.match_type && res.match_type !== "none" ? 1 : 0)
2646
2233
  };
2647
2234
  }
2648
2235
  function emitDeferredDeepLink(link) {
@@ -2672,13 +2259,13 @@ function getMatchedDeepLink() {
2672
2259
  }
2673
2260
 
2674
2261
  // src/superlink/web.ts
2675
- var STORAGE_KEY3 = "sp_superlink";
2262
+ var STORAGE_KEY4 = "sp_superlink";
2676
2263
  var captured = null;
2677
2264
  var didCapture = false;
2678
- function readStored() {
2265
+ function readStored2() {
2679
2266
  if (typeof sessionStorage === "undefined") return null;
2680
2267
  try {
2681
- const raw = sessionStorage.getItem(STORAGE_KEY3);
2268
+ const raw = sessionStorage.getItem(STORAGE_KEY4);
2682
2269
  if (!raw) return null;
2683
2270
  const parsed = JSON.parse(raw);
2684
2271
  return parsed && typeof parsed === "object" ? parsed : null;
@@ -2686,10 +2273,10 @@ function readStored() {
2686
2273
  return null;
2687
2274
  }
2688
2275
  }
2689
- function writeStored(link) {
2276
+ function writeStored2(link) {
2690
2277
  if (typeof sessionStorage === "undefined") return;
2691
2278
  try {
2692
- sessionStorage.setItem(STORAGE_KEY3, JSON.stringify(link));
2279
+ sessionStorage.setItem(STORAGE_KEY4, JSON.stringify(link));
2693
2280
  } catch {
2694
2281
  }
2695
2282
  }
@@ -2737,7 +2324,7 @@ function extractIdentityMetadata(url) {
2737
2324
  function captureWebDeepLink() {
2738
2325
  if (didCapture) return captured;
2739
2326
  didCapture = true;
2740
- const stored = readStored();
2327
+ const stored = readStored2();
2741
2328
  if (stored) {
2742
2329
  captured = stored;
2743
2330
  return captured;
@@ -2746,7 +2333,7 @@ function captureWebDeepLink() {
2746
2333
  const link = parseUniversalLink(window.location.href);
2747
2334
  if (link && hasPayload(link)) {
2748
2335
  captured = link;
2749
- writeStored(link);
2336
+ writeStored2(link);
2750
2337
  void superlinkClient.reportEvent({
2751
2338
  type: "opened",
2752
2339
  click_id: link.click_id,
@@ -2905,10 +2492,10 @@ var webManagerFactories = {};
2905
2492
  function registerWebManagerFactories(factories) {
2906
2493
  webManagerFactories = { ...webManagerFactories, ...factories };
2907
2494
  }
2908
- var DEFAULT_ENDPOINT4 = "https://ingest.sitepong.com";
2495
+ var DEFAULT_ENDPOINT3 = "https://ingest.sitepong.com";
2909
2496
  var DEFAULT_BATCH_SIZE2 = 10;
2910
- var DEFAULT_FLUSH_INTERVAL3 = 5e3;
2911
- var MAX_FLUSH_FAILURES3 = 3;
2497
+ var DEFAULT_FLUSH_INTERVAL2 = 5e3;
2498
+ var MAX_FLUSH_FAILURES2 = 3;
2912
2499
  var SitePongClient = class {
2913
2500
  constructor() {
2914
2501
  this.errorQueue = [];
@@ -2927,16 +2514,18 @@ var SitePongClient = class {
2927
2514
  this.databaseTracker = null;
2928
2515
  this.profiler = null;
2929
2516
  this.remoteConfigManager = null;
2517
+ this.watchtowerCapture = null;
2518
+ this.watchtowerOptions = void 0;
2930
2519
  this.envUnsubs = [];
2931
2520
  this.identifyHooks = [];
2932
2521
  this.config = {
2933
2522
  apiKey: "",
2934
- endpoint: DEFAULT_ENDPOINT4,
2523
+ endpoint: DEFAULT_ENDPOINT3,
2935
2524
  environment: "production",
2936
2525
  release: "",
2937
2526
  autoCapture: true,
2938
2527
  maxBatchSize: DEFAULT_BATCH_SIZE2,
2939
- flushInterval: DEFAULT_FLUSH_INTERVAL3,
2528
+ flushInterval: DEFAULT_FLUSH_INTERVAL2,
2940
2529
  debug: false
2941
2530
  };
2942
2531
  }
@@ -2958,7 +2547,7 @@ var SitePongClient = class {
2958
2547
  if (config.enableFlags) {
2959
2548
  this.flagManager = new FlagManager({
2960
2549
  apiKey: config.apiKey,
2961
- endpoint: config.flagsEndpoint || config.endpoint || DEFAULT_ENDPOINT4,
2550
+ endpoint: config.flagsEndpoint || config.endpoint || DEFAULT_ENDPOINT3,
2962
2551
  debug: config.debug
2963
2552
  });
2964
2553
  this.flagManager.init().catch((err) => {
@@ -2971,7 +2560,7 @@ var SitePongClient = class {
2971
2560
  }
2972
2561
  this.analyticsManager = new AnalyticsManager({
2973
2562
  apiKey: config.apiKey,
2974
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2563
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
2975
2564
  enabled: true,
2976
2565
  autocapturePageviews: config.analytics.autocapturePageviews,
2977
2566
  autocaptureClicks: config.analytics.autocaptureClicks,
@@ -2987,7 +2576,7 @@ var SitePongClient = class {
2987
2576
  if (config.fingerprint?.enabled && webManagerFactories.createFingerprint) {
2988
2577
  this.fingerprintManager = webManagerFactories.createFingerprint({
2989
2578
  apiKey: config.apiKey,
2990
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2579
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
2991
2580
  enabled: true,
2992
2581
  extendedSignals: config.fingerprint.extendedSignals,
2993
2582
  visitorEndpoint: config.fingerprint.visitorEndpoint,
@@ -3002,7 +2591,7 @@ var SitePongClient = class {
3002
2591
  }
3003
2592
  this.performanceManager = webManagerFactories.createPerformance({
3004
2593
  apiKey: config.apiKey,
3005
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2594
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3006
2595
  enabled: true,
3007
2596
  webVitals: config.performance.webVitals,
3008
2597
  navigationTiming: config.performance.navigationTiming,
@@ -3026,7 +2615,7 @@ var SitePongClient = class {
3026
2615
  }
3027
2616
  this.replayManager = webManagerFactories.createReplay({
3028
2617
  apiKey: config.apiKey,
3029
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2618
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3030
2619
  enabled: true,
3031
2620
  maskInputs: config.replay.maskInputs,
3032
2621
  blockSelector: config.replay.blockSelector,
@@ -3046,17 +2635,23 @@ var SitePongClient = class {
3046
2635
  } else if (config.replay?.enabled) {
3047
2636
  this.log("Replay requested but not available on this platform (web only).");
3048
2637
  }
2638
+ this.watchtowerOptions = config.watchtower;
2639
+ if (config.watchtower?.enabled && webManagerFactories.createWatchtowerCapture) {
2640
+ this.startWatchtowerCapture();
2641
+ } else if (config.watchtower?.enabled) {
2642
+ this.log("Watchtower capture requested but not available on this platform (web only).");
2643
+ }
3049
2644
  if (config.crons?.enabled) {
3050
2645
  this.cronManager = new CronMonitorManager({
3051
2646
  apiKey: config.apiKey,
3052
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2647
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3053
2648
  debug: config.debug
3054
2649
  });
3055
2650
  }
3056
2651
  if (config.database?.enabled) {
3057
2652
  this.databaseTracker = new DatabaseTracker({
3058
2653
  apiKey: config.apiKey,
3059
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2654
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3060
2655
  debug: config.debug,
3061
2656
  slowQueryThreshold: config.database.slowQueryThreshold,
3062
2657
  redactParams: config.database.redactParams
@@ -3065,7 +2660,7 @@ var SitePongClient = class {
3065
2660
  if (config.metrics?.enabled) {
3066
2661
  this.metricsManager = new MetricsManager({
3067
2662
  apiKey: config.apiKey,
3068
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2663
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3069
2664
  debug: config.debug,
3070
2665
  flushInterval: config.metrics.flushInterval,
3071
2666
  maxBatchSize: config.metrics.maxBatchSize
@@ -3074,7 +2669,7 @@ var SitePongClient = class {
3074
2669
  if (config.profiling?.enabled) {
3075
2670
  this.profiler = new Profiler({
3076
2671
  apiKey: config.apiKey,
3077
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2672
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3078
2673
  debug: config.debug,
3079
2674
  sampleRate: config.profiling.sampleRate,
3080
2675
  maxDuration: config.profiling.maxDuration,
@@ -3085,7 +2680,7 @@ var SitePongClient = class {
3085
2680
  const storage = config._storageAdapter || getStorageAdapter();
3086
2681
  this.remoteConfigManager = new RemoteConfigManager({
3087
2682
  apiKey: config.apiKey,
3088
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2683
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3089
2684
  storage,
3090
2685
  debug: config.debug
3091
2686
  });
@@ -3119,7 +2714,7 @@ var SitePongClient = class {
3119
2714
  if (config.enableFlags) {
3120
2715
  this.flagManager = new FlagManager({
3121
2716
  apiKey: config.apiKey,
3122
- endpoint: config.flagsEndpoint || config.endpoint || DEFAULT_ENDPOINT4,
2717
+ endpoint: config.flagsEndpoint || config.endpoint || DEFAULT_ENDPOINT3,
3123
2718
  debug: config.debug
3124
2719
  });
3125
2720
  this.flagManager.init().catch((err) => {
@@ -3132,7 +2727,7 @@ var SitePongClient = class {
3132
2727
  }
3133
2728
  this.analyticsManager = new AnalyticsManager({
3134
2729
  apiKey: config.apiKey,
3135
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2730
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3136
2731
  enabled: true,
3137
2732
  maxBatchSize: config.analytics.maxBatchSize,
3138
2733
  flushInterval: config.analytics.flushInterval,
@@ -3144,14 +2739,14 @@ var SitePongClient = class {
3144
2739
  if (config.crons?.enabled) {
3145
2740
  this.cronManager = new CronMonitorManager({
3146
2741
  apiKey: config.apiKey,
3147
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2742
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3148
2743
  debug: config.debug
3149
2744
  });
3150
2745
  }
3151
2746
  if (config.metrics?.enabled) {
3152
2747
  this.metricsManager = new MetricsManager({
3153
2748
  apiKey: config.apiKey,
3154
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2749
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3155
2750
  debug: config.debug,
3156
2751
  flushInterval: config.metrics.flushInterval,
3157
2752
  maxBatchSize: config.metrics.maxBatchSize
@@ -3160,7 +2755,7 @@ var SitePongClient = class {
3160
2755
  if (config.database?.enabled) {
3161
2756
  this.databaseTracker = new DatabaseTracker({
3162
2757
  apiKey: config.apiKey,
3163
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2758
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3164
2759
  debug: config.debug,
3165
2760
  slowQueryThreshold: config.database.slowQueryThreshold,
3166
2761
  redactParams: config.database.redactParams
@@ -3169,7 +2764,7 @@ var SitePongClient = class {
3169
2764
  if (config.profiling?.enabled) {
3170
2765
  this.profiler = new Profiler({
3171
2766
  apiKey: config.apiKey,
3172
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2767
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3173
2768
  debug: config.debug,
3174
2769
  sampleRate: config.profiling.sampleRate,
3175
2770
  maxDuration: config.profiling.maxDuration,
@@ -3180,7 +2775,7 @@ var SitePongClient = class {
3180
2775
  const storage = config._storageAdapter || getStorageAdapter();
3181
2776
  this.remoteConfigManager = new RemoteConfigManager({
3182
2777
  apiKey: config.apiKey,
3183
- endpoint: config.endpoint || DEFAULT_ENDPOINT4,
2778
+ endpoint: config.endpoint || DEFAULT_ENDPOINT3,
3184
2779
  storage,
3185
2780
  debug: config.debug
3186
2781
  });
@@ -3285,6 +2880,7 @@ var SitePongClient = class {
3285
2880
  this.log("identify hook failed:", err);
3286
2881
  }
3287
2882
  }
2883
+ this.watchtowerCapture?.setDistinctId(userId);
3288
2884
  if (!this.analyticsManager) {
3289
2885
  this.log("Analytics not enabled. Set analytics.enabled: true in init()");
3290
2886
  return;
@@ -3352,11 +2948,54 @@ var SitePongClient = class {
3352
2948
  getReplaySessionId() {
3353
2949
  return this.replayManager?.getSessionId() ?? null;
3354
2950
  }
3355
- // --- Performance (Tier 7) ---
3356
- startTransaction(name, data) {
3357
- if (!this.performanceManager) {
3358
- this.log("Performance not enabled. Set performance.enabled: true in init()");
3359
- return "";
2951
+ // --- Watchtower tap capture (web) ---
2952
+ /**
2953
+ * Start Watchtower web tap capture. Options merge over `watchtower` from
2954
+ * init(); `projectId` is required (either here or in init config). Reuses
2955
+ * the analytics AutocaptureModule's click listener when autocapture is on.
2956
+ */
2957
+ startWatchtowerCapture(options) {
2958
+ if (!webManagerFactories.createWatchtowerCapture) {
2959
+ this.log("Watchtower capture not available on this platform (web only).");
2960
+ return false;
2961
+ }
2962
+ if (!this.initialized || !this.config.apiKey) {
2963
+ this.log("Watchtower capture requires init() to be called first.");
2964
+ return false;
2965
+ }
2966
+ const opts = { ...this.watchtowerOptions, ...options };
2967
+ if (!opts.projectId) {
2968
+ this.log("Watchtower capture requires watchtower.projectId.");
2969
+ return false;
2970
+ }
2971
+ if (this.watchtowerCapture) {
2972
+ this.watchtowerCapture.stop();
2973
+ this.watchtowerCapture = null;
2974
+ }
2975
+ this.watchtowerCapture = webManagerFactories.createWatchtowerCapture({
2976
+ apiKey: this.config.apiKey,
2977
+ projectId: opts.projectId,
2978
+ endpoint: opts.endpoint || this.config.endpoint || DEFAULT_ENDPOINT3,
2979
+ appVersion: this.config.release || void 0,
2980
+ maxBatchSize: opts.maxBatchSize,
2981
+ flushInterval: opts.flushInterval,
2982
+ blockSelector: opts.blockSelector,
2983
+ maskSelector: opts.maskSelector,
2984
+ debug: this.config.debug
2985
+ });
2986
+ this.watchtowerCapture.start(this.analyticsManager?.getAutocaptureModule() ?? null);
2987
+ return true;
2988
+ }
2989
+ stopWatchtowerCapture() {
2990
+ if (!this.watchtowerCapture) return;
2991
+ this.watchtowerCapture.stop();
2992
+ this.watchtowerCapture = null;
2993
+ }
2994
+ // --- Performance (Tier 7) ---
2995
+ startTransaction(name, data) {
2996
+ if (!this.performanceManager) {
2997
+ this.log("Performance not enabled. Set performance.enabled: true in init()");
2998
+ return "";
3360
2999
  }
3361
3000
  return this.performanceManager.startTransaction(name, data);
3362
3001
  }
@@ -3503,6 +3142,7 @@ var SitePongClient = class {
3503
3142
  if (this.replayManager) {
3504
3143
  this.replayManager.setUser(null);
3505
3144
  }
3145
+ this.watchtowerCapture?.setDistinctId(null);
3506
3146
  }
3507
3147
  addBreadcrumb(breadcrumb) {
3508
3148
  this.log("Breadcrumb added:", breadcrumb);
@@ -3572,15 +3212,15 @@ var SitePongClient = class {
3572
3212
  this.log(`Flushed ${errors.length} errors`);
3573
3213
  } catch (err) {
3574
3214
  this.flushFailures++;
3575
- if (this.flushFailures >= MAX_FLUSH_FAILURES3) {
3215
+ if (this.flushFailures >= MAX_FLUSH_FAILURES2) {
3576
3216
  this.disabled = true;
3577
3217
  console.warn(
3578
- `[SitePong] SDK disabled after ${MAX_FLUSH_FAILURES3} consecutive failures. Events will no longer be sent. This may be due to CORS issues or network problems. Check that ${this.config.endpoint} is accessible and has proper CORS headers.`
3218
+ `[SitePong] SDK disabled after ${MAX_FLUSH_FAILURES2} consecutive failures. Events will no longer be sent. This may be due to CORS issues or network problems. Check that ${this.config.endpoint} is accessible and has proper CORS headers.`
3579
3219
  );
3580
3220
  return;
3581
3221
  }
3582
3222
  this.errorQueue.unshift(...errors);
3583
- this.log(`Failed to flush errors (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES3}):`, err);
3223
+ this.log(`Failed to flush errors (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES2}):`, err);
3584
3224
  }
3585
3225
  }
3586
3226
  formatError(error, additionalContext) {
@@ -3727,6 +3367,8 @@ var getDeviceSignals = sitepong.getDeviceSignals.bind(sitepong);
3727
3367
  var getFraudCheck = sitepong.getFraudCheck.bind(sitepong);
3728
3368
  var startReplay = sitepong.startReplay.bind(sitepong);
3729
3369
  var stopReplay = sitepong.stopReplay.bind(sitepong);
3370
+ var startWatchtowerCapture = sitepong.startWatchtowerCapture.bind(sitepong);
3371
+ var stopWatchtowerCapture = sitepong.stopWatchtowerCapture.bind(sitepong);
3730
3372
  var isReplayRecording = sitepong.isReplayRecording.bind(sitepong);
3731
3373
  var getReplaySessionId = sitepong.getReplaySessionId.bind(sitepong);
3732
3374
  var startTransaction = sitepong.startTransaction.bind(sitepong);
@@ -4443,10 +4085,10 @@ var ConsoleRecorder = class {
4443
4085
  };
4444
4086
 
4445
4087
  // src/replay/manager.ts
4446
- var DEFAULT_ENDPOINT5 = "https://ingest.sitepong.com";
4088
+ var DEFAULT_ENDPOINT4 = "https://ingest.sitepong.com";
4447
4089
  var DEFAULT_BUFFER_DURATION = 2e4;
4448
4090
  var DEFAULT_MAX_SESSION_DURATION = 60 * 60 * 1e3;
4449
- var MAX_FLUSH_FAILURES4 = 3;
4091
+ var MAX_FLUSH_FAILURES3 = 3;
4450
4092
  var TRIM_INTERVAL = 5e3;
4451
4093
  var ReplayManager = class {
4452
4094
  constructor(config) {
@@ -4473,7 +4115,7 @@ var ReplayManager = class {
4473
4115
  /** Most recent full snapshot, kept so we can prepend it to error replays */
4474
4116
  this.latestSnapshot = null;
4475
4117
  this.config = {
4476
- endpoint: DEFAULT_ENDPOINT5,
4118
+ endpoint: DEFAULT_ENDPOINT4,
4477
4119
  enabled: true,
4478
4120
  maskInputs: true,
4479
4121
  maxSessionDuration: DEFAULT_MAX_SESSION_DURATION,
@@ -4609,14 +4251,14 @@ var ReplayManager = class {
4609
4251
  this.log(`Flushed ${eventsToSend.length} replay events (error-triggered)`);
4610
4252
  } catch (err) {
4611
4253
  this.flushFailures++;
4612
- if (this.flushFailures >= MAX_FLUSH_FAILURES4) {
4254
+ if (this.flushFailures >= MAX_FLUSH_FAILURES3) {
4613
4255
  this.disabled = true;
4614
4256
  console.warn(
4615
- `[SitePong Replay] Disabled after ${MAX_FLUSH_FAILURES4} consecutive failures. Check that ${endpoint} is accessible and has proper CORS headers.`
4257
+ `[SitePong Replay] Disabled after ${MAX_FLUSH_FAILURES3} consecutive failures. Check that ${endpoint} is accessible and has proper CORS headers.`
4616
4258
  );
4617
4259
  return;
4618
4260
  }
4619
- this.log(`Failed to flush replay events (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES4}):`, err);
4261
+ this.log(`Failed to flush replay events (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES3}):`, err);
4620
4262
  }
4621
4263
  }
4622
4264
  /**
@@ -5655,11 +5297,11 @@ function getNotificationsPermission() {
5655
5297
  return Notification.permission === "granted";
5656
5298
  }
5657
5299
  function getDeviceAge() {
5658
- const STORAGE_KEY4 = "sitepong_device_age";
5300
+ const STORAGE_KEY5 = "sitepong_device_age";
5659
5301
  const result = { visitCount: 1 };
5660
5302
  if (typeof localStorage === "undefined") return result;
5661
5303
  try {
5662
- const stored = localStorage.getItem(STORAGE_KEY4);
5304
+ const stored = localStorage.getItem(STORAGE_KEY5);
5663
5305
  if (stored) {
5664
5306
  const parsed = JSON.parse(stored);
5665
5307
  result.firstSeen = parsed.firstSeen;
@@ -5667,7 +5309,7 @@ function getDeviceAge() {
5667
5309
  } else {
5668
5310
  result.firstSeen = (/* @__PURE__ */ new Date()).toISOString();
5669
5311
  }
5670
- localStorage.setItem(STORAGE_KEY4, JSON.stringify({
5312
+ localStorage.setItem(STORAGE_KEY5, JSON.stringify({
5671
5313
  firstSeen: result.firstSeen,
5672
5314
  visitCount: result.visitCount
5673
5315
  }));
@@ -5911,542 +5553,1757 @@ var BehaviorAnalyzer = class {
5911
5553
  const score = this.calculateHumanScore(mouse, keyboard, scroll, clicks);
5912
5554
  return { mouse, keyboard, scroll, clicks, score };
5913
5555
  }
5914
- reset() {
5915
- this.mousePoints = [];
5916
- this.keyDownTimes.clear();
5917
- this.keyIntervals = [];
5918
- this.keyHoldTimes = [];
5919
- this.lastKeyTime = 0;
5920
- this.scrollEvents = [];
5921
- this.clickTimes = [];
5556
+ reset() {
5557
+ this.mousePoints = [];
5558
+ this.keyDownTimes.clear();
5559
+ this.keyIntervals = [];
5560
+ this.keyHoldTimes = [];
5561
+ this.lastKeyTime = 0;
5562
+ this.scrollEvents = [];
5563
+ this.clickTimes = [];
5564
+ }
5565
+ analyzeMouseMovement() {
5566
+ if (this.mousePoints.length < 3) {
5567
+ return {
5568
+ totalMovements: this.mousePoints.length,
5569
+ averageSpeed: 0,
5570
+ maxSpeed: 0,
5571
+ averageAcceleration: 0,
5572
+ straightLineRatio: 0,
5573
+ jitter: 0,
5574
+ hasMovement: false
5575
+ };
5576
+ }
5577
+ const speeds = [];
5578
+ const accelerations = [];
5579
+ let straightSegments = 0;
5580
+ let totalSegments = 0;
5581
+ let jitterSum = 0;
5582
+ for (let i = 1; i < this.mousePoints.length; i++) {
5583
+ const prev = this.mousePoints[i - 1];
5584
+ const curr = this.mousePoints[i];
5585
+ const dt = curr.t - prev.t || 1;
5586
+ const dx = curr.x - prev.x;
5587
+ const dy = curr.y - prev.y;
5588
+ const distance = Math.sqrt(dx * dx + dy * dy);
5589
+ const speed = distance / dt;
5590
+ speeds.push(speed);
5591
+ if (distance < 3 && distance > 0) {
5592
+ jitterSum++;
5593
+ }
5594
+ if (i >= 2) {
5595
+ const pp = this.mousePoints[i - 2];
5596
+ const expectedX = pp.x + (curr.x - pp.x) * ((prev.t - pp.t) / (curr.t - pp.t));
5597
+ const expectedY = pp.y + (curr.y - pp.y) * ((prev.t - pp.t) / (curr.t - pp.t));
5598
+ const deviation = Math.sqrt((prev.x - expectedX) ** 2 + (prev.y - expectedY) ** 2);
5599
+ totalSegments++;
5600
+ if (deviation < 2) straightSegments++;
5601
+ }
5602
+ if (i >= 2) {
5603
+ const prevSpeed = speeds[speeds.length - 2] || 0;
5604
+ accelerations.push(Math.abs(speed - prevSpeed) / dt);
5605
+ }
5606
+ }
5607
+ const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
5608
+ const maxSpeed = Math.max(...speeds);
5609
+ const avgAccel = accelerations.length > 0 ? accelerations.reduce((a, b) => a + b, 0) / accelerations.length : 0;
5610
+ return {
5611
+ totalMovements: this.mousePoints.length,
5612
+ averageSpeed: Math.round(avgSpeed * 1e3) / 1e3,
5613
+ maxSpeed: Math.round(maxSpeed * 1e3) / 1e3,
5614
+ averageAcceleration: Math.round(avgAccel * 1e3) / 1e3,
5615
+ straightLineRatio: totalSegments > 0 ? straightSegments / totalSegments : 0,
5616
+ jitter: this.mousePoints.length > 0 ? jitterSum / this.mousePoints.length : 0,
5617
+ hasMovement: true
5618
+ };
5619
+ }
5620
+ analyzeKeyboard() {
5621
+ if (this.keyIntervals.length < 2) {
5622
+ return {
5623
+ totalKeystrokes: this.keyIntervals.length,
5624
+ averageInterval: 0,
5625
+ intervalVariance: 0,
5626
+ holdTimeAverage: 0,
5627
+ holdTimeVariance: 0,
5628
+ hasKeystrokes: false
5629
+ };
5630
+ }
5631
+ const avgInterval = this.keyIntervals.reduce((a, b) => a + b, 0) / this.keyIntervals.length;
5632
+ const intervalVar = this.variance(this.keyIntervals);
5633
+ const avgHold = this.keyHoldTimes.length > 0 ? this.keyHoldTimes.reduce((a, b) => a + b, 0) / this.keyHoldTimes.length : 0;
5634
+ const holdVar = this.keyHoldTimes.length > 0 ? this.variance(this.keyHoldTimes) : 0;
5635
+ return {
5636
+ totalKeystrokes: this.keyIntervals.length + 1,
5637
+ averageInterval: Math.round(avgInterval),
5638
+ intervalVariance: Math.round(intervalVar),
5639
+ holdTimeAverage: Math.round(avgHold),
5640
+ holdTimeVariance: Math.round(holdVar),
5641
+ hasKeystrokes: true
5642
+ };
5643
+ }
5644
+ analyzeScroll() {
5645
+ if (this.scrollEvents.length < 2) {
5646
+ return { totalScrolls: this.scrollEvents.length, averageSpeed: 0, smoothness: 0, hasScroll: false };
5647
+ }
5648
+ const speeds = [];
5649
+ for (let i = 1; i < this.scrollEvents.length; i++) {
5650
+ const dt = this.scrollEvents[i].t - this.scrollEvents[i - 1].t || 1;
5651
+ const dy = Math.abs(this.scrollEvents[i].deltaY - this.scrollEvents[i - 1].deltaY);
5652
+ speeds.push(dy / dt);
5653
+ }
5654
+ const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
5655
+ const speedVar = this.variance(speeds);
5656
+ const smoothness = avgSpeed > 0 ? Math.min(1, avgSpeed / (speedVar + avgSpeed)) : 0;
5657
+ return {
5658
+ totalScrolls: this.scrollEvents.length,
5659
+ averageSpeed: Math.round(avgSpeed * 100) / 100,
5660
+ smoothness: Math.round(smoothness * 100) / 100,
5661
+ hasScroll: true
5662
+ };
5663
+ }
5664
+ analyzeClicks() {
5665
+ if (this.clickTimes.length < 2) {
5666
+ return {
5667
+ totalClicks: this.clickTimes.length,
5668
+ averageInterval: 0,
5669
+ intervalVariance: 0,
5670
+ doubleClickCount: 0,
5671
+ hasClicks: this.clickTimes.length > 0
5672
+ };
5673
+ }
5674
+ const intervals = [];
5675
+ let doubleClicks = 0;
5676
+ for (let i = 1; i < this.clickTimes.length; i++) {
5677
+ const interval = this.clickTimes[i] - this.clickTimes[i - 1];
5678
+ intervals.push(interval);
5679
+ if (interval < 500) doubleClicks++;
5680
+ }
5681
+ const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
5682
+ const intervalVar = this.variance(intervals);
5683
+ return {
5684
+ totalClicks: this.clickTimes.length,
5685
+ averageInterval: Math.round(avgInterval),
5686
+ intervalVariance: Math.round(intervalVar),
5687
+ doubleClickCount: doubleClicks,
5688
+ hasClicks: true
5689
+ };
5690
+ }
5691
+ calculateHumanScore(mouse, keyboard, scroll, clicks) {
5692
+ let score = 0.5;
5693
+ let factors = 0;
5694
+ if (mouse.hasMovement) {
5695
+ factors++;
5696
+ let mouseScore = 0.5;
5697
+ if (mouse.jitter > 0.05) mouseScore += 0.15;
5698
+ if (mouse.straightLineRatio < 0.5) mouseScore += 0.15;
5699
+ else if (mouse.straightLineRatio > 0.9) mouseScore -= 0.2;
5700
+ if (mouse.averageSpeed > 0 && mouse.maxSpeed / mouse.averageSpeed > 3) {
5701
+ mouseScore += 0.1;
5702
+ }
5703
+ if (mouse.averageAcceleration > 0) mouseScore += 0.1;
5704
+ score += Math.max(0, Math.min(1, mouseScore)) - 0.5;
5705
+ }
5706
+ if (keyboard.hasKeystrokes) {
5707
+ factors++;
5708
+ let keyScore = 0.5;
5709
+ if (keyboard.intervalVariance > 1e3) keyScore += 0.2;
5710
+ else if (keyboard.intervalVariance < 100) keyScore -= 0.2;
5711
+ const cv = keyboard.averageInterval > 0 ? Math.sqrt(keyboard.intervalVariance) / keyboard.averageInterval : 0;
5712
+ if (cv > 0.3) keyScore += 0.15;
5713
+ if (keyboard.holdTimeVariance > 500) keyScore += 0.1;
5714
+ score += Math.max(0, Math.min(1, keyScore)) - 0.5;
5715
+ }
5716
+ if (scroll.hasScroll) {
5717
+ factors++;
5718
+ let scrollScore = 0.5;
5719
+ if (scroll.smoothness > 0.3 && scroll.smoothness < 0.9) {
5720
+ scrollScore += 0.2;
5721
+ }
5722
+ score += Math.max(0, Math.min(1, scrollScore)) - 0.5;
5723
+ }
5724
+ if (clicks.hasClicks) {
5725
+ factors++;
5726
+ let clickScore = 0.5;
5727
+ if (clicks.intervalVariance > 5e4) clickScore += 0.15;
5728
+ else if (clicks.intervalVariance < 100) clickScore -= 0.2;
5729
+ score += Math.max(0, Math.min(1, clickScore)) - 0.5;
5730
+ }
5731
+ if (factors === 0) return 0.3;
5732
+ return Math.max(0, Math.min(1, score));
5733
+ }
5734
+ variance(values) {
5735
+ if (values.length < 2) return 0;
5736
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
5737
+ return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1);
5738
+ }
5739
+ };
5740
+
5741
+ // src/fingerprint/vpn-detection.ts
5742
+ async function detectVPN() {
5743
+ const signals = {
5744
+ webrtcLeak: null,
5745
+ timezoneIPMismatch: false,
5746
+ dnsLeak: null,
5747
+ connectionType: null,
5748
+ multipleIPs: false,
5749
+ torBrowser: false,
5750
+ knownVPNExtension: false
5751
+ };
5752
+ let confidence = 0;
5753
+ let vpnDetected = false;
5754
+ let proxyDetected = false;
5755
+ let torDetected = false;
5756
+ signals.torBrowser = detectTorBrowser();
5757
+ if (signals.torBrowser) {
5758
+ torDetected = true;
5759
+ confidence += 0.9;
5760
+ }
5761
+ try {
5762
+ signals.webrtcLeak = await detectWebRTCLeak();
5763
+ if (signals.webrtcLeak) {
5764
+ vpnDetected = true;
5765
+ confidence += 0.3;
5766
+ }
5767
+ } catch {
5768
+ signals.webrtcLeak = null;
5769
+ }
5770
+ signals.connectionType = getConnectionType2();
5771
+ if (signals.connectionType === "unknown" || signals.connectionType === null) {
5772
+ confidence += 0.05;
5773
+ }
5774
+ signals.knownVPNExtension = detectVPNExtensions();
5775
+ if (signals.knownVPNExtension) {
5776
+ vpnDetected = true;
5777
+ confidence += 0.4;
5778
+ }
5779
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
5780
+ const lang = navigator.language;
5781
+ if (tz && lang) {
5782
+ const tzCountry = getTimezoneCountry(tz);
5783
+ const langCountry = lang.split("-")[1]?.toUpperCase();
5784
+ if (tzCountry && langCountry && tzCountry !== langCountry) {
5785
+ signals.timezoneIPMismatch = true;
5786
+ confidence += 0.1;
5787
+ }
5788
+ }
5789
+ confidence = Math.min(confidence, 1);
5790
+ if (confidence < 0.3) {
5791
+ vpnDetected = false;
5792
+ proxyDetected = false;
5793
+ }
5794
+ return {
5795
+ vpnDetected,
5796
+ proxyDetected,
5797
+ torDetected,
5798
+ confidence,
5799
+ signals
5800
+ };
5801
+ }
5802
+ function detectTorBrowser() {
5803
+ if (typeof window === "undefined") return false;
5804
+ const checks = [];
5805
+ checks.push(window.screen.width === 1e3 && window.screen.height === 1e3);
5806
+ checks.push(!window.MediaDevices);
5807
+ checks.push(navigator.plugins.length === 0);
5808
+ checks.push((/* @__PURE__ */ new Date()).getTimezoneOffset() === 0);
5809
+ const ua = navigator.userAgent;
5810
+ checks.push(ua.includes("Firefox") && !ua.includes("Chrome"));
5811
+ const score = checks.filter(Boolean).length;
5812
+ return score >= 3;
5813
+ }
5814
+ async function detectWebRTCLeak() {
5815
+ if (typeof window === "undefined" || !window.RTCPeerConnection) {
5816
+ return false;
5817
+ }
5818
+ return new Promise((resolve) => {
5819
+ const timeout = setTimeout(() => resolve(false), 3e3);
5820
+ const localIPs = /* @__PURE__ */ new Set();
5821
+ try {
5822
+ const pc = new RTCPeerConnection({
5823
+ iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
5824
+ });
5825
+ pc.createDataChannel("");
5826
+ pc.onicecandidate = (event) => {
5827
+ if (!event.candidate) {
5828
+ clearTimeout(timeout);
5829
+ pc.close();
5830
+ resolve(localIPs.size > 1);
5831
+ return;
5832
+ }
5833
+ const candidate = event.candidate.candidate;
5834
+ const ipMatch = candidate.match(/(\d{1,3}\.){3}\d{1,3}/);
5835
+ if (ipMatch) {
5836
+ localIPs.add(ipMatch[0]);
5837
+ }
5838
+ };
5839
+ pc.createOffer().then((offer) => pc.setLocalDescription(offer)).catch(() => {
5840
+ clearTimeout(timeout);
5841
+ resolve(false);
5842
+ });
5843
+ } catch {
5844
+ clearTimeout(timeout);
5845
+ resolve(false);
5846
+ }
5847
+ });
5848
+ }
5849
+ function getConnectionType2() {
5850
+ if (typeof navigator === "undefined") return null;
5851
+ const nav = navigator;
5852
+ if (nav.connection) {
5853
+ return nav.connection.type || nav.connection.effectiveType || null;
5854
+ }
5855
+ return null;
5856
+ }
5857
+ function detectVPNExtensions() {
5858
+ if (typeof document === "undefined") return false;
5859
+ const body = document.body;
5860
+ if (!body) return false;
5861
+ const vpnIndicators = [
5862
+ "[data-windscribe]",
5863
+ "[data-nord]",
5864
+ ".surfshark-extension",
5865
+ "#vpn-indicator",
5866
+ "[data-expressvpn]"
5867
+ ];
5868
+ for (const selector of vpnIndicators) {
5869
+ try {
5870
+ if (document.querySelector(selector)) return true;
5871
+ } catch {
5872
+ }
5873
+ }
5874
+ return false;
5875
+ }
5876
+ function getTimezoneCountry(tz) {
5877
+ const tzMap = {
5878
+ "America/New_York": "US",
5879
+ "America/Chicago": "US",
5880
+ "America/Denver": "US",
5881
+ "America/Los_Angeles": "US",
5882
+ "Europe/London": "GB",
5883
+ "Europe/Paris": "FR",
5884
+ "Europe/Berlin": "DE",
5885
+ "Europe/Madrid": "ES",
5886
+ "Europe/Rome": "IT",
5887
+ "Asia/Tokyo": "JP",
5888
+ "Asia/Shanghai": "CN",
5889
+ "Asia/Kolkata": "IN",
5890
+ "Asia/Seoul": "KR",
5891
+ "Australia/Sydney": "AU",
5892
+ "America/Toronto": "CA",
5893
+ "America/Sao_Paulo": "BR",
5894
+ "America/Mexico_City": "MX"
5895
+ };
5896
+ return tzMap[tz] || null;
5897
+ }
5898
+
5899
+ // src/fingerprint/manager.ts
5900
+ var DEFAULT_ENDPOINT5 = "https://ingest.sitepong.com";
5901
+ var CACHE_KEY = "sitepong_visitor";
5902
+ var CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
5903
+ var COOKIE_NAME = "__sp_vid";
5904
+ var COOKIE_MAX_AGE = 63072e3;
5905
+ var FingerprintManager = class {
5906
+ constructor(config) {
5907
+ this.cachedVisitor = null;
5908
+ this.pendingRequest = null;
5909
+ this.config = {
5910
+ endpoint: DEFAULT_ENDPOINT5,
5911
+ enabled: true,
5912
+ debug: false,
5913
+ ...config
5914
+ };
5915
+ this.behaviorAnalyzer = new BehaviorAnalyzer();
5916
+ if (this.config.enabled && typeof window !== "undefined" && typeof window.addEventListener === "function") {
5917
+ this.behaviorAnalyzer.start();
5918
+ }
5919
+ this.loadFromStorage();
5920
+ }
5921
+ async getVisitorId() {
5922
+ if (!this.config.enabled) {
5923
+ throw new Error("Fingerprint is not enabled");
5924
+ }
5925
+ isReactNative() ? getNativeDeviceId() : null;
5926
+ if (this.cachedVisitor && !this.isCacheExpired()) {
5927
+ this.log("Returning cached visitor ID:", this.cachedVisitor.result.visitorId);
5928
+ return this.cachedVisitor.result;
5929
+ }
5930
+ if (this.pendingRequest) {
5931
+ return this.pendingRequest;
5932
+ }
5933
+ this.pendingRequest = this.fetchVisitorId();
5934
+ try {
5935
+ const result = await this.pendingRequest;
5936
+ return result;
5937
+ } finally {
5938
+ this.pendingRequest = null;
5939
+ }
5940
+ }
5941
+ async getDeviceSignals() {
5942
+ return collectDeviceSignals(this.config.extendedSignals ?? false);
5943
+ }
5944
+ async getFraudCheck() {
5945
+ const [visitorResult, signals, vpnResult] = await Promise.all([
5946
+ this.getVisitorId(),
5947
+ this.getDeviceSignals(),
5948
+ detectVPN().catch(() => null)
5949
+ ]);
5950
+ const behaviorMetrics = this.behaviorAnalyzer.getMetrics();
5951
+ signals.behavior = {
5952
+ score: behaviorMetrics.score,
5953
+ mouseMovements: behaviorMetrics.mouse.totalMovements,
5954
+ keystrokes: behaviorMetrics.keyboard.totalKeystrokes,
5955
+ scrolls: behaviorMetrics.scroll.totalScrolls,
5956
+ clicks: behaviorMetrics.clicks.totalClicks,
5957
+ isHuman: behaviorMetrics.score > 0.6
5958
+ };
5959
+ const vpnDetected = vpnResult?.vpnDetected || vpnResult?.torDetected || false;
5960
+ return {
5961
+ visitorId: visitorResult.visitorId,
5962
+ riskScore: this.calculateRiskScore(signals, vpnDetected),
5963
+ signals,
5964
+ flags: {
5965
+ incognito: signals.smart?.incognito || signals.incognito,
5966
+ bot: signals.smart?.bot || signals.bot || !signals.behavior.isHuman,
5967
+ vpn: vpnDetected || signals.smart?.timezoneMismatch,
5968
+ tampered: signals.smart?.privacyBrowser
5969
+ }
5970
+ };
5971
+ }
5972
+ stopBehaviorTracking() {
5973
+ this.behaviorAnalyzer.stop();
5974
+ }
5975
+ async fetchVisitorId() {
5976
+ const signals = await this.getDeviceSignals();
5977
+ const stableFingerprint = await getStableFingerprint(signals);
5978
+ const endpoint = this.config.visitorEndpoint || `${this.config.endpoint}/api/visitors`;
5979
+ const previousVisitorId = this.getCookieVisitorId() || this.cachedVisitor?.result.visitorId || void 0;
5980
+ try {
5981
+ const response = await fetch(endpoint, {
5982
+ method: "POST",
5983
+ headers: {
5984
+ "Content-Type": "application/json",
5985
+ "X-API-Key": this.config.apiKey
5986
+ },
5987
+ credentials: "include",
5988
+ body: JSON.stringify({
5989
+ signals,
5990
+ stableFingerprint,
5991
+ previousVisitorId
5992
+ })
5993
+ });
5994
+ if (!response.ok) {
5995
+ throw new Error(`HTTP ${response.status}`);
5996
+ }
5997
+ const result = await response.json();
5998
+ this.cachedVisitor = {
5999
+ result,
6000
+ signals,
6001
+ cachedAt: Date.now()
6002
+ };
6003
+ this.saveToStorage();
6004
+ this.setCookieVisitorId(result.visitorId);
6005
+ this.log("Visitor ID fetched:", result.visitorId);
6006
+ return result;
6007
+ } catch (err) {
6008
+ this.log("Failed to fetch visitor ID:", err);
6009
+ throw err;
6010
+ }
6011
+ }
6012
+ calculateRiskScore(signals, vpnDetected = false) {
6013
+ let score = 0;
6014
+ const smart = signals.smart;
6015
+ if (smart?.bot || signals.bot) score += 0.8;
6016
+ if (signals.behavior) {
6017
+ if (signals.behavior.score < 0.3) score += 0.4;
6018
+ else if (signals.behavior.score < 0.5) score += 0.2;
6019
+ }
6020
+ if (vpnDetected) score += 0.25;
6021
+ if (smart?.incognito || signals.incognito) score += 0.2;
6022
+ if (smart?.devToolsOpen) score += 0.1;
6023
+ if (smart?.privacyBrowser) score += 0.1;
6024
+ if (signals.hardwareConcurrency === 0) score += 0.1;
6025
+ if (signals.maxTouchPoints === 0 && /mobile|android|iphone/i.test(signals.userAgent)) {
6026
+ score += 0.15;
6027
+ }
6028
+ if (!signals.cookieEnabled) score += 0.1;
6029
+ if (smart?.doNotTrack) score += 0.05;
6030
+ if (smart?.deviceVisitCount === 1) score += 0.05;
6031
+ return Math.round(Math.min(score, 1) * 100) / 100;
6032
+ }
6033
+ isCacheExpired() {
6034
+ if (!this.cachedVisitor) return true;
6035
+ return Date.now() - this.cachedVisitor.cachedAt > CACHE_TTL_MS;
6036
+ }
6037
+ loadFromStorage() {
6038
+ if (typeof window === "undefined" || typeof localStorage === "undefined") return;
6039
+ try {
6040
+ const stored = localStorage.getItem(CACHE_KEY);
6041
+ if (!stored) return;
6042
+ const parsed = JSON.parse(stored);
6043
+ if (!this.isCacheExpiredAt(parsed.cachedAt)) {
6044
+ this.cachedVisitor = parsed;
6045
+ this.log("Loaded cached visitor from storage");
6046
+ } else {
6047
+ localStorage.removeItem(CACHE_KEY);
6048
+ }
6049
+ } catch {
6050
+ }
6051
+ }
6052
+ saveToStorage() {
6053
+ if (typeof window === "undefined" || typeof localStorage === "undefined") return;
6054
+ if (!this.cachedVisitor) return;
6055
+ try {
6056
+ localStorage.setItem(CACHE_KEY, JSON.stringify(this.cachedVisitor));
6057
+ } catch {
6058
+ }
6059
+ }
6060
+ isCacheExpiredAt(cachedAt) {
6061
+ return Date.now() - cachedAt > CACHE_TTL_MS;
6062
+ }
6063
+ getCookieVisitorId() {
6064
+ if (typeof document === "undefined") return void 0;
6065
+ try {
6066
+ const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));
6067
+ return match?.[1] || void 0;
6068
+ } catch {
6069
+ return void 0;
6070
+ }
6071
+ }
6072
+ setCookieVisitorId(visitorId) {
6073
+ if (typeof document === "undefined") return;
6074
+ try {
6075
+ document.cookie = `${COOKIE_NAME}=${visitorId}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
6076
+ } catch {
6077
+ }
6078
+ }
6079
+ log(...args) {
6080
+ if (this.config.debug) {
6081
+ console.log("[SitePong Fingerprint]", ...args);
6082
+ }
6083
+ }
6084
+ };
6085
+ var DEFAULT_ENDPOINT6 = "https://ingest.sitepong.com";
6086
+ var DEFAULT_FLUSH_INTERVAL3 = 1e4;
6087
+ var MAX_FLUSH_FAILURES4 = 3;
6088
+ var MAX_RESOURCES_PER_FLUSH = 200;
6089
+ var PAGE_TIMING_POLL_INTERVAL = 200;
6090
+ var PAGE_TIMING_TIMEOUT = 3e4;
6091
+ function getPaintBlocks(resources) {
6092
+ const paintBlocks = [];
6093
+ const elements = document.getElementsByTagName("*");
6094
+ const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i;
6095
+ for (let i = 0; i < elements.length; i++) {
6096
+ const element = elements[i];
6097
+ let src = "";
6098
+ if (element.tagName === "IMG") {
6099
+ src = element.currentSrc || element.src;
6100
+ }
6101
+ if (!src) {
6102
+ const backgroundImage = getComputedStyle(element).getPropertyValue("background-image");
6103
+ if (backgroundImage) {
6104
+ const matches = styleURL.exec(backgroundImage);
6105
+ if (matches !== null) {
6106
+ src = matches[1];
6107
+ if (src.startsWith('"') || src.startsWith("'")) {
6108
+ src = src.substr(1, src.length - 2);
6109
+ }
6110
+ }
6111
+ }
6112
+ }
6113
+ if (!src) continue;
6114
+ const time = src.substr(0, 10) === "data:image" ? 0 : resources[src];
6115
+ if (time === void 0) continue;
6116
+ const rect = element.getBoundingClientRect();
6117
+ const top = Math.max(rect.top, 0);
6118
+ const left = Math.max(rect.left, 0);
6119
+ const bottom = Math.min(
6120
+ rect.bottom,
6121
+ window.innerHeight || document.documentElement && document.documentElement.clientHeight || 0
6122
+ );
6123
+ const right = Math.min(
6124
+ rect.right,
6125
+ window.innerWidth || document.documentElement && document.documentElement.clientWidth || 0
6126
+ );
6127
+ if (bottom <= top || right <= left) continue;
6128
+ const area = (bottom - top) * (right - left);
6129
+ paintBlocks.push({ time, area });
6130
+ }
6131
+ return paintBlocks;
6132
+ }
6133
+ function calculateSpeedIndex(firstContentfulPaint, paintBlocks) {
6134
+ let a = Math.max(
6135
+ document.documentElement && document.documentElement.clientWidth || 0,
6136
+ window.innerWidth || 0
6137
+ ) * Math.max(
6138
+ document.documentElement && document.documentElement.clientHeight || 0,
6139
+ window.innerHeight || 0
6140
+ ) / 10;
6141
+ let s = a * firstContentfulPaint;
6142
+ for (let i = 0; i < paintBlocks.length; i++) {
6143
+ const { time, area } = paintBlocks[i];
6144
+ a += area;
6145
+ s += area * (time > firstContentfulPaint ? time : firstContentfulPaint);
6146
+ }
6147
+ return a === 0 ? 0 : s / a;
6148
+ }
6149
+ var PerformanceManager = class {
6150
+ constructor(config) {
6151
+ this.metrics = [];
6152
+ this.flushTimer = null;
6153
+ this.flushFailures = 0;
6154
+ this.disabled = false;
6155
+ this.vitals = {};
6156
+ this.activeTransactions = /* @__PURE__ */ new Map();
6157
+ // Resource timing state for Speed Index
6158
+ this.resourceObserver = null;
6159
+ this.resourceTimeMap = {};
6160
+ this.resourceCount = 0;
6161
+ // Page timing polling timers
6162
+ this.pageLoadTimer = null;
6163
+ this.pageRenderTimer = null;
6164
+ this.config = {
6165
+ endpoint: DEFAULT_ENDPOINT6,
6166
+ enabled: true,
6167
+ webVitals: true,
6168
+ navigationTiming: true,
6169
+ resourceTiming: true,
6170
+ capturePageLoadTimings: true,
6171
+ capturePageRenderTimings: true,
6172
+ excludedResourceUrls: [],
6173
+ flushInterval: DEFAULT_FLUSH_INTERVAL3,
6174
+ sampleRate: 1,
6175
+ debug: false,
6176
+ ...config
6177
+ };
6178
+ this.sampled = Math.random() <= this.config.sampleRate;
6179
+ }
6180
+ init() {
6181
+ if (!this.config.enabled || !this.sampled) return;
6182
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
6183
+ if (this.config.webVitals !== false) {
6184
+ this.initWebVitals();
6185
+ }
6186
+ if (this.config.resourceTiming !== false) {
6187
+ this.initResourceTiming();
6188
+ }
6189
+ if (this.config.capturePageLoadTimings !== false) {
6190
+ this.initPageLoadTiming();
6191
+ }
6192
+ if (this.config.capturePageRenderTimings !== false) {
6193
+ this.initPageRenderTiming();
6194
+ }
6195
+ this.startFlushTimer();
6196
+ this.log("Performance monitoring initialized");
6197
+ }
6198
+ // ---- Web Vitals (via web-vitals library) ----
6199
+ initWebVitals() {
6200
+ webVitals.onCLS((m) => {
6201
+ this.vitals.cls = m.value;
6202
+ this.reportVital("CLS", m.value, "score");
6203
+ });
6204
+ webVitals.onINP((m) => {
6205
+ this.vitals.inp = m.value;
6206
+ this.reportVital("INP", m.value);
6207
+ });
6208
+ webVitals.onLCP((m) => {
6209
+ this.vitals.lcp = m.value;
6210
+ this.reportVital("LCP", m.value);
6211
+ });
6212
+ webVitals.onTTFB((m) => {
6213
+ this.vitals.ttfb = m.value;
6214
+ this.reportVital("TTFB", m.value);
6215
+ });
6216
+ webVitals.onFCP((m) => {
6217
+ this.vitals.fcp = m.value;
6218
+ this.reportVital("FCP", m.value);
6219
+ });
6220
+ }
6221
+ // ---- Resource Timing (adapted from OpenReplay timing.ts) ----
6222
+ initResourceTiming() {
6223
+ if (typeof PerformanceObserver === "undefined") return;
6224
+ try {
6225
+ this.resourceObserver = new PerformanceObserver((list) => {
6226
+ for (const entry of list.getEntries()) {
6227
+ this.processResourceEntry(entry);
6228
+ }
6229
+ });
6230
+ this.resourceObserver.observe({ type: "resource", buffered: true });
6231
+ } catch {
6232
+ this.log("PerformanceObserver for resource timing not supported");
6233
+ }
6234
+ }
6235
+ isServiceURL(url) {
6236
+ const endpoint = this.config.performanceEndpoint || this.config.endpoint;
6237
+ return url.startsWith(endpoint);
6238
+ }
6239
+ processResourceEntry(entry) {
6240
+ if (entry.duration < 0 || !entry.name.startsWith("http")) return;
6241
+ if (this.isServiceURL(entry.name)) return;
6242
+ if (this.resourceTimeMap !== null) {
6243
+ this.resourceTimeMap[entry.name] = entry.startTime + entry.duration;
6244
+ }
6245
+ for (const excluded of this.config.excludedResourceUrls) {
6246
+ if (entry.name.startsWith(excluded)) return;
6247
+ }
6248
+ if (this.resourceCount >= MAX_RESOURCES_PER_FLUSH) return;
6249
+ this.resourceCount++;
6250
+ let stalled = 0;
6251
+ if (entry.connectEnd && entry.connectEnd > entry.domainLookupEnd) {
6252
+ stalled = Math.max(0, entry.requestStart - entry.connectEnd);
6253
+ } else {
6254
+ stalled = Math.max(0, entry.requestStart - entry.domainLookupEnd);
6255
+ }
6256
+ const cached = entry.responseStatus && entry.responseStatus === 304 || entry.deliveryType && entry.deliveryType === "cache" || entry.transferSize === 0 && entry.decodedBodySize > 0;
6257
+ const responseStatus = entry.responseStatus || 0;
6258
+ const failed = responseStatus >= 400;
6259
+ const breakdown = {
6260
+ queueing: entry.requestStart - entry.fetchStart,
6261
+ dnsLookup: entry.domainLookupEnd - entry.domainLookupStart,
6262
+ initialConnection: entry.connectEnd - entry.connectStart,
6263
+ ssl: entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0,
6264
+ ttfb: entry.responseStart - entry.requestStart,
6265
+ contentDownload: entry.responseEnd - entry.responseStart,
6266
+ total: entry.duration ?? entry.responseEnd - entry.startTime,
6267
+ stalled,
6268
+ cached,
6269
+ failed,
6270
+ responseStatus,
6271
+ headerSize: entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
6272
+ encodedBodySize: entry.encodedBodySize || 0,
6273
+ decodedBodySize: failed ? -111 : entry.decodedBodySize || 0,
6274
+ transferSize: entry.transferSize,
6275
+ initiatorType: entry.initiatorType
6276
+ };
6277
+ const entryName = this.config.resourceNameSanitizer ? this.config.resourceNameSanitizer(entry.name) : entry.name;
6278
+ this.addMetric({
6279
+ type: "resource",
6280
+ name: entryName,
6281
+ value: entry.duration,
6282
+ unit: "ms",
6283
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6284
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
6285
+ data: breakdown
6286
+ });
6287
+ }
6288
+ // ---- Page Load Timing (adapted from OpenReplay timing.ts) ----
6289
+ initPageLoadTiming() {
6290
+ if (typeof window === "undefined" || !window.performance) return;
6291
+ let firstPaint = 0;
6292
+ let firstContentfulPaint = 0;
6293
+ let sent = false;
6294
+ const startTime = performance.now();
6295
+ this.pageLoadTimer = setInterval(() => {
6296
+ if (sent) {
6297
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
6298
+ return;
6299
+ }
6300
+ if (firstPaint === 0 || firstContentfulPaint === 0) {
6301
+ for (const entry of performance.getEntriesByType("paint")) {
6302
+ if (entry.name === "first-paint") firstPaint = entry.startTime;
6303
+ if (entry.name === "first-contentful-paint") firstContentfulPaint = entry.startTime;
6304
+ }
6305
+ }
6306
+ const nav = performance.getEntriesByType("navigation")[0];
6307
+ const timedOut = performance.now() - startTime > PAGE_TIMING_TIMEOUT;
6308
+ if (nav && nav.loadEventEnd > 0 || timedOut) {
6309
+ sent = true;
6310
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
6311
+ if (nav) {
6312
+ const navigationStart = nav.startTime;
6313
+ this.addMetric({
6314
+ type: "navigation",
6315
+ name: "page_load",
6316
+ value: nav.loadEventEnd - navigationStart,
6317
+ unit: "ms",
6318
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6319
+ url: window.location.href,
6320
+ data: {
6321
+ requestStart: nav.requestStart - navigationStart,
6322
+ responseStart: nav.responseStart - navigationStart,
6323
+ responseEnd: nav.responseEnd - navigationStart,
6324
+ domContentLoadedEventStart: nav.domContentLoadedEventEnd - navigationStart,
6325
+ domContentLoadedEventEnd: nav.domContentLoadedEventEnd - navigationStart,
6326
+ loadEventStart: nav.loadEventStart - navigationStart,
6327
+ loadEventEnd: nav.loadEventEnd - navigationStart,
6328
+ firstPaint,
6329
+ firstContentfulPaint,
6330
+ dns: nav.domainLookupEnd - nav.domainLookupStart,
6331
+ tcp: nav.connectEnd - nav.connectStart,
6332
+ ttfb: nav.responseStart - nav.requestStart,
6333
+ domComplete: nav.domComplete - navigationStart,
6334
+ transferSize: nav.transferSize,
6335
+ encodedBodySize: nav.encodedBodySize,
6336
+ decodedBodySize: nav.decodedBodySize
6337
+ }
6338
+ });
6339
+ }
6340
+ }
6341
+ }, PAGE_TIMING_POLL_INTERVAL);
6342
+ }
6343
+ // ---- Page Render Timing: Speed Index, Visually Complete, TTI (adapted from OpenReplay) ----
6344
+ initPageRenderTiming() {
6345
+ if (typeof window === "undefined" || !window.performance) return;
6346
+ let firstContentfulPaint = 0;
6347
+ let visuallyComplete = 0;
6348
+ let interactiveWindowStartTime = 0;
6349
+ let interactiveWindowTickTime = 0;
6350
+ let paintBlocks = null;
6351
+ let sent = false;
6352
+ const startTime = performance.now();
6353
+ this.pageRenderTimer = setInterval(() => {
6354
+ if (sent) {
6355
+ if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
6356
+ return;
6357
+ }
6358
+ const time = performance.now();
6359
+ if (firstContentfulPaint === 0) {
6360
+ for (const entry of performance.getEntriesByType("paint")) {
6361
+ if (entry.name === "first-contentful-paint") {
6362
+ firstContentfulPaint = entry.startTime;
6363
+ }
6364
+ }
6365
+ }
6366
+ if (this.resourceTimeMap !== null) {
6367
+ const times = Object.values(this.resourceTimeMap);
6368
+ if (times.length > 0) {
6369
+ visuallyComplete = Math.max(...times);
6370
+ }
6371
+ if (time - visuallyComplete > 1e3) {
6372
+ paintBlocks = getPaintBlocks(this.resourceTimeMap);
6373
+ this.resourceTimeMap = null;
6374
+ }
6375
+ }
6376
+ if (interactiveWindowTickTime !== null) {
6377
+ if (time - interactiveWindowTickTime > 50) {
6378
+ interactiveWindowStartTime = time;
6379
+ }
6380
+ interactiveWindowTickTime = time - interactiveWindowStartTime > 5e3 ? null : time;
6381
+ }
6382
+ const timedOut = time - startTime > PAGE_TIMING_TIMEOUT;
6383
+ if (paintBlocks !== null && interactiveWindowTickTime === null || timedOut) {
6384
+ sent = true;
6385
+ if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
6386
+ this.resourceTimeMap = null;
6387
+ const speedIndex = paintBlocks === null ? 0 : calculateSpeedIndex(firstContentfulPaint || 0, paintBlocks);
6388
+ const nav = performance.getEntriesByType("navigation")[0];
6389
+ const domContentLoadedEnd = nav ? nav.domContentLoadedEventEnd - nav.startTime : 0;
6390
+ const timeToInteractive = interactiveWindowTickTime === null ? Math.max(interactiveWindowStartTime, firstContentfulPaint, domContentLoadedEnd) : 0;
6391
+ const url = window.location.href;
6392
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
6393
+ if (speedIndex > 0) {
6394
+ this.addMetric({
6395
+ type: "page_render",
6396
+ name: "speed_index",
6397
+ value: Math.round(speedIndex),
6398
+ unit: "ms",
6399
+ timestamp,
6400
+ url
6401
+ });
6402
+ }
6403
+ if (visuallyComplete > 0) {
6404
+ this.addMetric({
6405
+ type: "page_render",
6406
+ name: "visually_complete",
6407
+ value: Math.round(visuallyComplete),
6408
+ unit: "ms",
6409
+ timestamp,
6410
+ url
6411
+ });
6412
+ }
6413
+ if (timeToInteractive > 0) {
6414
+ this.addMetric({
6415
+ type: "page_render",
6416
+ name: "tti",
6417
+ value: Math.round(timeToInteractive),
6418
+ unit: "ms",
6419
+ timestamp,
6420
+ url
6421
+ });
6422
+ }
6423
+ }
6424
+ }, PAGE_TIMING_POLL_INTERVAL);
6425
+ }
6426
+ // ---- Transactions / Spans (unchanged) ----
6427
+ startTransaction(name, data) {
6428
+ const id = `txn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
6429
+ const transaction = {
6430
+ id,
6431
+ name,
6432
+ startTime: performance.now(),
6433
+ status: "running",
6434
+ spans: [],
6435
+ data
6436
+ };
6437
+ this.activeTransactions.set(id, transaction);
6438
+ this.log("Transaction started:", name, id);
6439
+ return id;
5922
6440
  }
5923
- analyzeMouseMovement() {
5924
- if (this.mousePoints.length < 3) {
5925
- return {
5926
- totalMovements: this.mousePoints.length,
5927
- averageSpeed: 0,
5928
- maxSpeed: 0,
5929
- averageAcceleration: 0,
5930
- straightLineRatio: 0,
5931
- jitter: 0,
5932
- hasMovement: false
5933
- };
6441
+ endTransaction(id, status = "ok") {
6442
+ const transaction = this.activeTransactions.get(id);
6443
+ if (!transaction) {
6444
+ this.log("Transaction not found:", id);
6445
+ return;
5934
6446
  }
5935
- const speeds = [];
5936
- const accelerations = [];
5937
- let straightSegments = 0;
5938
- let totalSegments = 0;
5939
- let jitterSum = 0;
5940
- for (let i = 1; i < this.mousePoints.length; i++) {
5941
- const prev = this.mousePoints[i - 1];
5942
- const curr = this.mousePoints[i];
5943
- const dt = curr.t - prev.t || 1;
5944
- const dx = curr.x - prev.x;
5945
- const dy = curr.y - prev.y;
5946
- const distance = Math.sqrt(dx * dx + dy * dy);
5947
- const speed = distance / dt;
5948
- speeds.push(speed);
5949
- if (distance < 3 && distance > 0) {
5950
- jitterSum++;
5951
- }
5952
- if (i >= 2) {
5953
- const pp = this.mousePoints[i - 2];
5954
- const expectedX = pp.x + (curr.x - pp.x) * ((prev.t - pp.t) / (curr.t - pp.t));
5955
- const expectedY = pp.y + (curr.y - pp.y) * ((prev.t - pp.t) / (curr.t - pp.t));
5956
- const deviation = Math.sqrt((prev.x - expectedX) ** 2 + (prev.y - expectedY) ** 2);
5957
- totalSegments++;
5958
- if (deviation < 2) straightSegments++;
5959
- }
5960
- if (i >= 2) {
5961
- const prevSpeed = speeds[speeds.length - 2] || 0;
5962
- accelerations.push(Math.abs(speed - prevSpeed) / dt);
6447
+ transaction.endTime = performance.now();
6448
+ transaction.duration = transaction.endTime - transaction.startTime;
6449
+ transaction.status = status;
6450
+ for (const span of transaction.spans) {
6451
+ if (span.status === "running") {
6452
+ span.endTime = transaction.endTime;
6453
+ span.duration = span.endTime - span.startTime;
6454
+ span.status = status;
5963
6455
  }
5964
6456
  }
5965
- const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
5966
- const maxSpeed = Math.max(...speeds);
5967
- const avgAccel = accelerations.length > 0 ? accelerations.reduce((a, b) => a + b, 0) / accelerations.length : 0;
5968
- return {
5969
- totalMovements: this.mousePoints.length,
5970
- averageSpeed: Math.round(avgSpeed * 1e3) / 1e3,
5971
- maxSpeed: Math.round(maxSpeed * 1e3) / 1e3,
5972
- averageAcceleration: Math.round(avgAccel * 1e3) / 1e3,
5973
- straightLineRatio: totalSegments > 0 ? straightSegments / totalSegments : 0,
5974
- jitter: this.mousePoints.length > 0 ? jitterSum / this.mousePoints.length : 0,
5975
- hasMovement: true
5976
- };
6457
+ this.activeTransactions.delete(id);
6458
+ this.addMetric({
6459
+ type: "transaction",
6460
+ name: transaction.name,
6461
+ value: transaction.duration,
6462
+ unit: "ms",
6463
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6464
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
6465
+ transaction
6466
+ });
6467
+ this.log("Transaction ended:", transaction.name, `${transaction.duration.toFixed(1)}ms`);
5977
6468
  }
5978
- analyzeKeyboard() {
5979
- if (this.keyIntervals.length < 2) {
5980
- return {
5981
- totalKeystrokes: this.keyIntervals.length,
5982
- averageInterval: 0,
5983
- intervalVariance: 0,
5984
- holdTimeAverage: 0,
5985
- holdTimeVariance: 0,
5986
- hasKeystrokes: false
5987
- };
6469
+ startSpan(transactionId, name, data) {
6470
+ const transaction = this.activeTransactions.get(transactionId);
6471
+ if (!transaction) {
6472
+ this.log("Transaction not found for span:", transactionId);
6473
+ return "";
5988
6474
  }
5989
- const avgInterval = this.keyIntervals.reduce((a, b) => a + b, 0) / this.keyIntervals.length;
5990
- const intervalVar = this.variance(this.keyIntervals);
5991
- const avgHold = this.keyHoldTimes.length > 0 ? this.keyHoldTimes.reduce((a, b) => a + b, 0) / this.keyHoldTimes.length : 0;
5992
- const holdVar = this.keyHoldTimes.length > 0 ? this.variance(this.keyHoldTimes) : 0;
5993
- return {
5994
- totalKeystrokes: this.keyIntervals.length + 1,
5995
- averageInterval: Math.round(avgInterval),
5996
- intervalVariance: Math.round(intervalVar),
5997
- holdTimeAverage: Math.round(avgHold),
5998
- holdTimeVariance: Math.round(holdVar),
5999
- hasKeystrokes: true
6475
+ const span = {
6476
+ id: `span_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
6477
+ name,
6478
+ startTime: performance.now(),
6479
+ status: "running",
6480
+ data
6000
6481
  };
6482
+ transaction.spans.push(span);
6483
+ return span.id;
6001
6484
  }
6002
- analyzeScroll() {
6003
- if (this.scrollEvents.length < 2) {
6004
- return { totalScrolls: this.scrollEvents.length, averageSpeed: 0, smoothness: 0, hasScroll: false };
6485
+ endSpan(transactionId, spanId, status = "ok") {
6486
+ const transaction = this.activeTransactions.get(transactionId);
6487
+ if (!transaction) return;
6488
+ const span = transaction.spans.find((s) => s.id === spanId);
6489
+ if (!span) return;
6490
+ span.endTime = performance.now();
6491
+ span.duration = span.endTime - span.startTime;
6492
+ span.status = status;
6493
+ }
6494
+ // ---- Public API ----
6495
+ getVitals() {
6496
+ return { ...this.vitals };
6497
+ }
6498
+ destroy() {
6499
+ if (this.flushTimer) {
6500
+ clearInterval(this.flushTimer);
6501
+ this.flushTimer = null;
6005
6502
  }
6006
- const speeds = [];
6007
- for (let i = 1; i < this.scrollEvents.length; i++) {
6008
- const dt = this.scrollEvents[i].t - this.scrollEvents[i - 1].t || 1;
6009
- const dy = Math.abs(this.scrollEvents[i].deltaY - this.scrollEvents[i - 1].deltaY);
6010
- speeds.push(dy / dt);
6503
+ if (this.pageLoadTimer) {
6504
+ clearInterval(this.pageLoadTimer);
6505
+ this.pageLoadTimer = null;
6011
6506
  }
6012
- const avgSpeed = speeds.reduce((a, b) => a + b, 0) / speeds.length;
6013
- const speedVar = this.variance(speeds);
6014
- const smoothness = avgSpeed > 0 ? Math.min(1, avgSpeed / (speedVar + avgSpeed)) : 0;
6015
- return {
6016
- totalScrolls: this.scrollEvents.length,
6017
- averageSpeed: Math.round(avgSpeed * 100) / 100,
6018
- smoothness: Math.round(smoothness * 100) / 100,
6019
- hasScroll: true
6020
- };
6021
- }
6022
- analyzeClicks() {
6023
- if (this.clickTimes.length < 2) {
6024
- return {
6025
- totalClicks: this.clickTimes.length,
6026
- averageInterval: 0,
6027
- intervalVariance: 0,
6028
- doubleClickCount: 0,
6029
- hasClicks: this.clickTimes.length > 0
6030
- };
6507
+ if (this.pageRenderTimer) {
6508
+ clearInterval(this.pageRenderTimer);
6509
+ this.pageRenderTimer = null;
6031
6510
  }
6032
- const intervals = [];
6033
- let doubleClicks = 0;
6034
- for (let i = 1; i < this.clickTimes.length; i++) {
6035
- const interval = this.clickTimes[i] - this.clickTimes[i - 1];
6036
- intervals.push(interval);
6037
- if (interval < 500) doubleClicks++;
6511
+ if (this.resourceObserver) {
6512
+ this.resourceObserver.disconnect();
6513
+ this.resourceObserver = null;
6038
6514
  }
6039
- const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
6040
- const intervalVar = this.variance(intervals);
6041
- return {
6042
- totalClicks: this.clickTimes.length,
6043
- averageInterval: Math.round(avgInterval),
6044
- intervalVariance: Math.round(intervalVar),
6045
- doubleClickCount: doubleClicks,
6046
- hasClicks: true
6047
- };
6515
+ this.flush();
6048
6516
  }
6049
- calculateHumanScore(mouse, keyboard, scroll, clicks) {
6050
- let score = 0.5;
6051
- let factors = 0;
6052
- if (mouse.hasMovement) {
6053
- factors++;
6054
- let mouseScore = 0.5;
6055
- if (mouse.jitter > 0.05) mouseScore += 0.15;
6056
- if (mouse.straightLineRatio < 0.5) mouseScore += 0.15;
6057
- else if (mouse.straightLineRatio > 0.9) mouseScore -= 0.2;
6058
- if (mouse.averageSpeed > 0 && mouse.maxSpeed / mouse.averageSpeed > 3) {
6059
- mouseScore += 0.1;
6060
- }
6061
- if (mouse.averageAcceleration > 0) mouseScore += 0.1;
6062
- score += Math.max(0, Math.min(1, mouseScore)) - 0.5;
6517
+ // ---- Internal ----
6518
+ reportVital(name, value, unit = "ms") {
6519
+ this.addMetric({
6520
+ type: "web_vital",
6521
+ name,
6522
+ value,
6523
+ unit,
6524
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6525
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0
6526
+ });
6527
+ }
6528
+ addMetric(metric) {
6529
+ this.metrics.push(metric);
6530
+ }
6531
+ startFlushTimer() {
6532
+ this.flushTimer = setInterval(() => {
6533
+ this.flush();
6534
+ }, this.config.flushInterval);
6535
+ if (typeof document !== "undefined") {
6536
+ document.addEventListener("visibilitychange", () => {
6537
+ if (document.visibilityState === "hidden") {
6538
+ this.flush();
6539
+ }
6540
+ });
6063
6541
  }
6064
- if (keyboard.hasKeystrokes) {
6065
- factors++;
6066
- let keyScore = 0.5;
6067
- if (keyboard.intervalVariance > 1e3) keyScore += 0.2;
6068
- else if (keyboard.intervalVariance < 100) keyScore -= 0.2;
6069
- const cv = keyboard.averageInterval > 0 ? Math.sqrt(keyboard.intervalVariance) / keyboard.averageInterval : 0;
6070
- if (cv > 0.3) keyScore += 0.15;
6071
- if (keyboard.holdTimeVariance > 500) keyScore += 0.1;
6072
- score += Math.max(0, Math.min(1, keyScore)) - 0.5;
6542
+ }
6543
+ async flush() {
6544
+ if (this.metrics.length === 0) return;
6545
+ if (this.disabled) {
6546
+ this.metrics = [];
6547
+ return;
6073
6548
  }
6074
- if (scroll.hasScroll) {
6075
- factors++;
6076
- let scrollScore = 0.5;
6077
- if (scroll.smoothness > 0.3 && scroll.smoothness < 0.9) {
6078
- scrollScore += 0.2;
6549
+ const metrics = [...this.metrics];
6550
+ this.metrics = [];
6551
+ this.resourceCount = 0;
6552
+ const endpoint = this.config.performanceEndpoint || `${this.config.endpoint}/api/performance`;
6553
+ try {
6554
+ const response = await fetch(endpoint, {
6555
+ method: "POST",
6556
+ headers: {
6557
+ "Content-Type": "application/json",
6558
+ "X-API-Key": this.config.apiKey
6559
+ },
6560
+ body: JSON.stringify({ metrics })
6561
+ });
6562
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
6563
+ this.flushFailures = 0;
6564
+ this.log(`Flushed ${metrics.length} performance metrics`);
6565
+ } catch (err) {
6566
+ this.flushFailures++;
6567
+ if (this.flushFailures >= MAX_FLUSH_FAILURES4) {
6568
+ this.disabled = true;
6569
+ console.warn(
6570
+ `[SitePong Performance] Disabled after ${MAX_FLUSH_FAILURES4} consecutive failures. Check that ${endpoint} is accessible and has proper CORS headers.`
6571
+ );
6572
+ return;
6079
6573
  }
6080
- score += Math.max(0, Math.min(1, scrollScore)) - 0.5;
6081
- }
6082
- if (clicks.hasClicks) {
6083
- factors++;
6084
- let clickScore = 0.5;
6085
- if (clicks.intervalVariance > 5e4) clickScore += 0.15;
6086
- else if (clicks.intervalVariance < 100) clickScore -= 0.2;
6087
- score += Math.max(0, Math.min(1, clickScore)) - 0.5;
6574
+ this.metrics.unshift(...metrics);
6575
+ this.log(`Failed to flush metrics (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES4}):`, err);
6088
6576
  }
6089
- if (factors === 0) return 0.3;
6090
- return Math.max(0, Math.min(1, score));
6091
6577
  }
6092
- variance(values) {
6093
- if (values.length < 2) return 0;
6094
- const mean = values.reduce((a, b) => a + b, 0) / values.length;
6095
- return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1);
6578
+ log(...args) {
6579
+ if (this.config.debug) {
6580
+ console.log("[SitePong Performance]", ...args);
6581
+ }
6096
6582
  }
6097
6583
  };
6098
6584
 
6099
- // src/fingerprint/vpn-detection.ts
6100
- async function detectVPN() {
6101
- const signals = {
6102
- webrtcLeak: null,
6103
- timezoneIPMismatch: false,
6104
- dnsLeak: null,
6105
- connectionType: null,
6106
- multipleIPs: false,
6107
- torBrowser: false,
6108
- knownVPNExtension: false
6109
- };
6110
- let confidence = 0;
6111
- let vpnDetected = false;
6112
- let proxyDetected = false;
6113
- let torDetected = false;
6114
- signals.torBrowser = detectTorBrowser();
6115
- if (signals.torBrowser) {
6116
- torDetected = true;
6117
- confidence += 0.9;
6585
+ // ../capture-core/dist/index.mjs
6586
+ var DHASH_WIDTH = 9;
6587
+ var DHASH_HEIGHT = 8;
6588
+ function toGray(r, g, b) {
6589
+ return Math.round(0.299 * r + 0.587 * g + 0.114 * b);
6590
+ }
6591
+ function rgbaToGray(rgba, width, height) {
6592
+ const out = new Float64Array(width * height);
6593
+ for (let i = 0; i < width * height; i++) {
6594
+ const o = i * 4;
6595
+ out[i] = toGray(rgba[o], rgba[o + 1], rgba[o + 2]);
6118
6596
  }
6119
- try {
6120
- signals.webrtcLeak = await detectWebRTCLeak();
6121
- if (signals.webrtcLeak) {
6122
- vpnDetected = true;
6123
- confidence += 0.3;
6597
+ return out;
6598
+ }
6599
+ function bilinearResizeGray(src, srcW, srcH, dstW, dstH) {
6600
+ const dst = new Float64Array(dstW * dstH);
6601
+ const scaleX = srcW / dstW;
6602
+ const scaleY = srcH / dstH;
6603
+ for (let dy = 0; dy < dstH; dy++) {
6604
+ let sy = (dy + 0.5) * scaleY - 0.5;
6605
+ if (sy < 0) sy = 0;
6606
+ if (sy > srcH - 1) sy = srcH - 1;
6607
+ const y0 = Math.floor(sy);
6608
+ const y1 = Math.min(y0 + 1, srcH - 1);
6609
+ const wy = sy - y0;
6610
+ for (let dx = 0; dx < dstW; dx++) {
6611
+ let sx = (dx + 0.5) * scaleX - 0.5;
6612
+ if (sx < 0) sx = 0;
6613
+ if (sx > srcW - 1) sx = srcW - 1;
6614
+ const x0 = Math.floor(sx);
6615
+ const x1 = Math.min(x0 + 1, srcW - 1);
6616
+ const wx = sx - x0;
6617
+ const p00 = src[y0 * srcW + x0];
6618
+ const p01 = src[y0 * srcW + x1];
6619
+ const p10 = src[y1 * srcW + x0];
6620
+ const p11 = src[y1 * srcW + x1];
6621
+ const top = p00 + (p01 - p00) * wx;
6622
+ const bottom = p10 + (p11 - p10) * wx;
6623
+ dst[dy * dstW + dx] = top + (bottom - top) * wy;
6624
+ }
6625
+ }
6626
+ return dst;
6627
+ }
6628
+ function dHash(rgba, width, height) {
6629
+ if (width <= 0 || height <= 0) {
6630
+ throw new Error(`dHash: invalid dimensions ${width}x${height}`);
6631
+ }
6632
+ if (rgba.length < width * height * 4) {
6633
+ throw new Error(
6634
+ `dHash: rgba length ${rgba.length} too short for ${width}x${height} (need ${width * height * 4})`
6635
+ );
6636
+ }
6637
+ const gray = rgbaToGray(rgba, width, height);
6638
+ const small = bilinearResizeGray(gray, width, height, DHASH_WIDTH, DHASH_HEIGHT);
6639
+ const bytes = new Uint8Array(8);
6640
+ for (let row = 0; row < DHASH_HEIGHT; row++) {
6641
+ let byte = 0;
6642
+ for (let c = 0; c < 8; c++) {
6643
+ const left = small[row * DHASH_WIDTH + c];
6644
+ const right = small[row * DHASH_WIDTH + c + 1];
6645
+ const bit = left < right ? 1 : 0;
6646
+ byte |= bit << 7 - c;
6124
6647
  }
6125
- } catch {
6126
- signals.webrtcLeak = null;
6648
+ bytes[row] = byte;
6127
6649
  }
6128
- signals.connectionType = getConnectionType2();
6129
- if (signals.connectionType === "unknown" || signals.connectionType === null) {
6130
- confidence += 0.05;
6650
+ let hex = "";
6651
+ for (let i = 0; i < 8; i++) {
6652
+ hex += bytes[i].toString(16).padStart(2, "0");
6131
6653
  }
6132
- signals.knownVPNExtension = detectVPNExtensions();
6133
- if (signals.knownVPNExtension) {
6134
- vpnDetected = true;
6135
- confidence += 0.4;
6654
+ return hex;
6655
+ }
6656
+ function popcount8(n) {
6657
+ let count = 0;
6658
+ while (n) {
6659
+ n &= n - 1;
6660
+ count++;
6136
6661
  }
6137
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
6138
- const lang = navigator.language;
6139
- if (tz && lang) {
6140
- const tzCountry = getTimezoneCountry(tz);
6141
- const langCountry = lang.split("-")[1]?.toUpperCase();
6142
- if (tzCountry && langCountry && tzCountry !== langCountry) {
6143
- signals.timezoneIPMismatch = true;
6144
- confidence += 0.1;
6662
+ return count;
6663
+ }
6664
+ function hamming(a, b) {
6665
+ if (a.length !== b.length) {
6666
+ throw new Error(`hamming: length mismatch (${a.length} vs ${b.length})`);
6667
+ }
6668
+ if (a.length % 2 !== 0) {
6669
+ throw new Error(`hamming: hex string length must be even, got ${a.length}`);
6670
+ }
6671
+ let dist = 0;
6672
+ for (let i = 0; i < a.length; i += 2) {
6673
+ const byteA = parseInt(a.slice(i, i + 2), 16);
6674
+ const byteB = parseInt(b.slice(i, i + 2), 16);
6675
+ if (Number.isNaN(byteA) || Number.isNaN(byteB)) {
6676
+ throw new Error(`hamming: invalid hex at offset ${i}`);
6145
6677
  }
6678
+ dist += popcount8(byteA ^ byteB);
6146
6679
  }
6147
- confidence = Math.min(confidence, 1);
6148
- if (confidence < 0.3) {
6149
- vpnDetected = false;
6150
- proxyDetected = false;
6680
+ return dist;
6681
+ }
6682
+ var HAMMING_THRESHOLD = 10;
6683
+ function screenFingerprint(screenName, pHashHex) {
6684
+ return `${screenName}:${pHashHex}`;
6685
+ }
6686
+ function parseFingerprint(fp) {
6687
+ const idx = fp.lastIndexOf(":");
6688
+ if (idx < 0) {
6689
+ throw new Error(`parseFingerprint: missing ':' in "${fp}"`);
6151
6690
  }
6152
6691
  return {
6153
- vpnDetected,
6154
- proxyDetected,
6155
- torDetected,
6156
- confidence,
6157
- signals
6692
+ screenName: fp.slice(0, idx),
6693
+ pHashHex: fp.slice(idx + 1)
6158
6694
  };
6159
6695
  }
6160
- function detectTorBrowser() {
6161
- if (typeof window === "undefined") return false;
6162
- const checks = [];
6163
- checks.push(window.screen.width === 1e3 && window.screen.height === 1e3);
6164
- checks.push(!window.MediaDevices);
6165
- checks.push(navigator.plugins.length === 0);
6166
- checks.push((/* @__PURE__ */ new Date()).getTimezoneOffset() === 0);
6167
- const ua = navigator.userAgent;
6168
- checks.push(ua.includes("Firefox") && !ua.includes("Chrome"));
6169
- const score = checks.filter(Boolean).length;
6170
- return score >= 3;
6696
+
6697
+ // src/analytics/tap-capture.ts
6698
+ var ZERO_HASH = "0000000000000000";
6699
+ var DEFAULT_MAX_BATCH = 50;
6700
+ var DEFAULT_FLUSH_INTERVAL4 = 1e4;
6701
+ var DEFAULT_TEMPLATE_THROTTLE = 1e3;
6702
+ var MAX_FLUSH_FAILURES5 = 3;
6703
+ var RASTER_MAX_WIDTH = 480;
6704
+ var DEFAULT_BLOCK_SELECTOR = "img, picture, video, canvas, iframe, [data-sp-mask]";
6705
+ var SS_SESSION_ID = "sitepong_wt_session_id";
6706
+ var SS_SEQUENCE = "sitepong_wt_seq";
6707
+ var SS_STARTED_AT = "sitepong_wt_started_at";
6708
+ var SS_START_SENT = "sitepong_wt_start_sent";
6709
+ function generateUuid() {
6710
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
6711
+ return crypto.randomUUID();
6712
+ }
6713
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
6714
+ const r = Math.random() * 16 | 0;
6715
+ const v = c === "x" ? r : r & 3 | 8;
6716
+ return v.toString(16);
6717
+ });
6171
6718
  }
6172
- async function detectWebRTCLeak() {
6173
- if (typeof window === "undefined" || !window.RTCPeerConnection) {
6174
- return false;
6719
+ var SessionState = class {
6720
+ constructor() {
6721
+ this.memory = {};
6175
6722
  }
6176
- return new Promise((resolve) => {
6177
- const timeout = setTimeout(() => resolve(false), 3e3);
6178
- const localIPs = /* @__PURE__ */ new Set();
6723
+ get(key) {
6179
6724
  try {
6180
- const pc = new RTCPeerConnection({
6181
- iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
6182
- });
6183
- pc.createDataChannel("");
6184
- pc.onicecandidate = (event) => {
6185
- if (!event.candidate) {
6186
- clearTimeout(timeout);
6187
- pc.close();
6188
- resolve(localIPs.size > 1);
6189
- return;
6190
- }
6191
- const candidate = event.candidate.candidate;
6192
- const ipMatch = candidate.match(/(\d{1,3}\.){3}\d{1,3}/);
6193
- if (ipMatch) {
6194
- localIPs.add(ipMatch[0]);
6195
- }
6196
- };
6197
- pc.createOffer().then((offer) => pc.setLocalDescription(offer)).catch(() => {
6198
- clearTimeout(timeout);
6199
- resolve(false);
6200
- });
6725
+ if (typeof sessionStorage !== "undefined") {
6726
+ return sessionStorage.getItem(key);
6727
+ }
6201
6728
  } catch {
6202
- clearTimeout(timeout);
6203
- resolve(false);
6204
6729
  }
6205
- });
6206
- }
6207
- function getConnectionType2() {
6208
- if (typeof navigator === "undefined") return null;
6209
- const nav = navigator;
6210
- if (nav.connection) {
6211
- return nav.connection.type || nav.connection.effectiveType || null;
6730
+ return this.memory[key] ?? null;
6212
6731
  }
6213
- return null;
6214
- }
6215
- function detectVPNExtensions() {
6216
- if (typeof document === "undefined") return false;
6217
- const body = document.body;
6218
- if (!body) return false;
6219
- const vpnIndicators = [
6220
- "[data-windscribe]",
6221
- "[data-nord]",
6222
- ".surfshark-extension",
6223
- "#vpn-indicator",
6224
- "[data-expressvpn]"
6225
- ];
6226
- for (const selector of vpnIndicators) {
6732
+ set(key, value) {
6227
6733
  try {
6228
- if (document.querySelector(selector)) return true;
6734
+ if (typeof sessionStorage !== "undefined") {
6735
+ sessionStorage.setItem(key, value);
6736
+ return;
6737
+ }
6229
6738
  } catch {
6230
6739
  }
6740
+ this.memory[key] = value;
6231
6741
  }
6232
- return false;
6233
- }
6234
- function getTimezoneCountry(tz) {
6235
- const tzMap = {
6236
- "America/New_York": "US",
6237
- "America/Chicago": "US",
6238
- "America/Denver": "US",
6239
- "America/Los_Angeles": "US",
6240
- "Europe/London": "GB",
6241
- "Europe/Paris": "FR",
6242
- "Europe/Berlin": "DE",
6243
- "Europe/Madrid": "ES",
6244
- "Europe/Rome": "IT",
6245
- "Asia/Tokyo": "JP",
6246
- "Asia/Shanghai": "CN",
6247
- "Asia/Kolkata": "IN",
6248
- "Asia/Seoul": "KR",
6249
- "Australia/Sydney": "AU",
6250
- "America/Toronto": "CA",
6251
- "America/Sao_Paulo": "BR",
6252
- "America/Mexico_City": "MX"
6253
- };
6254
- return tzMap[tz] || null;
6255
- }
6256
-
6257
- // src/fingerprint/manager.ts
6258
- var DEFAULT_ENDPOINT6 = "https://ingest.sitepong.com";
6259
- var CACHE_KEY = "sitepong_visitor";
6260
- var CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
6261
- var COOKIE_NAME = "__sp_vid";
6262
- var COOKIE_MAX_AGE = 63072e3;
6263
- var FingerprintManager = class {
6264
- constructor(config) {
6265
- this.cachedVisitor = null;
6266
- this.pendingRequest = null;
6267
- this.config = {
6268
- endpoint: DEFAULT_ENDPOINT6,
6269
- enabled: true,
6270
- debug: false,
6271
- ...config
6272
- };
6273
- this.behaviorAnalyzer = new BehaviorAnalyzer();
6274
- if (this.config.enabled && typeof window !== "undefined" && typeof window.addEventListener === "function") {
6275
- this.behaviorAnalyzer.start();
6742
+ get sessionId() {
6743
+ let id = this.get(SS_SESSION_ID);
6744
+ if (!id) {
6745
+ id = generateUuid();
6746
+ this.set(SS_SESSION_ID, id);
6747
+ this.set(SS_STARTED_AT, String(Date.now()));
6748
+ this.set(SS_SEQUENCE, "0");
6749
+ this.set(SS_START_SENT, "0");
6750
+ }
6751
+ return id;
6752
+ }
6753
+ get startedAt() {
6754
+ const raw = this.get(SS_STARTED_AT);
6755
+ const parsed = raw ? parseInt(raw, 10) : NaN;
6756
+ return Number.isFinite(parsed) ? parsed : Date.now();
6757
+ }
6758
+ get startSent() {
6759
+ return this.get(SS_START_SENT) === "1";
6760
+ }
6761
+ markStartSent() {
6762
+ this.set(SS_START_SENT, "1");
6763
+ }
6764
+ nextSequence() {
6765
+ const raw = this.get(SS_SEQUENCE);
6766
+ const prev = raw ? parseInt(raw, 10) : 0;
6767
+ const next = (Number.isFinite(prev) ? prev : 0) + 1;
6768
+ this.set(SS_SEQUENCE, String(next));
6769
+ return next;
6770
+ }
6771
+ };
6772
+ var fetchTransport = async (url, body, opts) => {
6773
+ const res = await fetch(url, {
6774
+ method: "POST",
6775
+ headers: opts.headers,
6776
+ body,
6777
+ keepalive: opts.keepalive
6778
+ });
6779
+ return { ok: res.ok, status: res.status };
6780
+ };
6781
+ var WatchtowerWebCapture = class {
6782
+ constructor(config) {
6783
+ this.session = new SessionState();
6784
+ this.queue = [];
6785
+ this.flushTimer = null;
6786
+ this.flushFailures = 0;
6787
+ this.disabled = false;
6788
+ this.active = false;
6789
+ this.distinctId = null;
6790
+ this.currentScreen = null;
6791
+ // Autocapture wiring
6792
+ this.detachAutocapture = null;
6793
+ this.ownAutocapture = null;
6794
+ // History patching
6795
+ this.originalPushState = null;
6796
+ this.originalReplaceState = null;
6797
+ this.popstateHandler = null;
6798
+ this.pagehideHandler = null;
6799
+ // Fingerprint state (per-session, in memory)
6800
+ this.fpCache = /* @__PURE__ */ new Map();
6801
+ // screen_name -> screen_fp
6802
+ this.uploadedFps = /* @__PURE__ */ new Set();
6803
+ // spec §1.3 uploaded-fingerprint set
6804
+ this.lastRasterAt = /* @__PURE__ */ new Map();
6805
+ this.rasterInFlight = false;
6806
+ this.config = { ...config };
6807
+ this.transport = config.transport || fetchTransport;
6808
+ this.rasterizer = config.rasterizer || null;
6809
+ }
6810
+ /**
6811
+ * Start capturing. Pass the existing AutocaptureModule (via
6812
+ * AnalyticsManager.getAutocaptureModule()) to reuse its click listener +
6813
+ * selector resolution; when absent, a private clicks-only module is created.
6814
+ */
6815
+ start(source) {
6816
+ if (this.active || typeof window === "undefined") return;
6817
+ this.active = true;
6818
+ if (!this.rasterizer) {
6819
+ this.rasterizer = new DomRasterizer({
6820
+ blockSelector: this.combinedBlockSelector(),
6821
+ maskSelector: this.config.maskSelector,
6822
+ maxWidth: RASTER_MAX_WIDTH
6823
+ });
6824
+ }
6825
+ const sessionId = this.session.sessionId;
6826
+ if (!this.session.startSent) {
6827
+ this.enqueue(this.baseEvent("session_start"));
6828
+ this.session.markStartSent();
6829
+ }
6830
+ this.log("Started (session", sessionId + ")");
6831
+ this.currentScreen = this.pathname();
6832
+ if (this.currentScreen !== null) {
6833
+ const ev = this.baseEvent("screen_view");
6834
+ ev.screen_name = this.currentScreen;
6835
+ this.enqueue(ev);
6836
+ this.scheduleFingerprint(this.currentScreen);
6837
+ }
6838
+ this.setupNavigationTracking();
6839
+ this.setupPagehide();
6840
+ this.startFlushTimer();
6841
+ if (source) {
6842
+ this.detachAutocapture = source.addListener((event) => this.onAutocaptureEvent(event));
6843
+ } else {
6844
+ this.ownAutocapture = new AutocaptureModule(
6845
+ { clicks: true, forms: false, pageviews: false, debug: this.config.debug },
6846
+ (event) => this.onAutocaptureEvent(event)
6847
+ );
6848
+ this.ownAutocapture.start();
6276
6849
  }
6277
- this.loadFromStorage();
6278
6850
  }
6279
- async getVisitorId() {
6280
- if (!this.config.enabled) {
6281
- throw new Error("Fingerprint is not enabled");
6851
+ stop() {
6852
+ if (!this.active) return;
6853
+ this.active = false;
6854
+ if (this.detachAutocapture) {
6855
+ this.detachAutocapture();
6856
+ this.detachAutocapture = null;
6282
6857
  }
6283
- isReactNative() ? getNativeDeviceId() : null;
6284
- if (this.cachedVisitor && !this.isCacheExpired()) {
6285
- this.log("Returning cached visitor ID:", this.cachedVisitor.result.visitorId);
6286
- return this.cachedVisitor.result;
6858
+ if (this.ownAutocapture) {
6859
+ this.ownAutocapture.stop();
6860
+ this.ownAutocapture = null;
6287
6861
  }
6288
- if (this.pendingRequest) {
6289
- return this.pendingRequest;
6862
+ if (this.flushTimer) {
6863
+ clearInterval(this.flushTimer);
6864
+ this.flushTimer = null;
6290
6865
  }
6291
- this.pendingRequest = this.fetchVisitorId();
6292
- try {
6293
- const result = await this.pendingRequest;
6294
- return result;
6295
- } finally {
6296
- this.pendingRequest = null;
6866
+ if (typeof window !== "undefined") {
6867
+ if (this.popstateHandler) {
6868
+ window.removeEventListener("popstate", this.popstateHandler);
6869
+ this.popstateHandler = null;
6870
+ }
6871
+ if (this.pagehideHandler) {
6872
+ window.removeEventListener("pagehide", this.pagehideHandler);
6873
+ this.pagehideHandler = null;
6874
+ }
6875
+ if (this.originalPushState) {
6876
+ window.history.pushState = this.originalPushState;
6877
+ this.originalPushState = null;
6878
+ }
6879
+ if (this.originalReplaceState) {
6880
+ window.history.replaceState = this.originalReplaceState;
6881
+ this.originalReplaceState = null;
6882
+ }
6297
6883
  }
6884
+ void this.flush();
6885
+ this.log("Stopped");
6298
6886
  }
6299
- async getDeviceSignals() {
6300
- return collectDeviceSignals(this.config.extendedSignals ?? false);
6887
+ isActive() {
6888
+ return this.active;
6301
6889
  }
6302
- async getFraudCheck() {
6303
- const [visitorResult, signals, vpnResult] = await Promise.all([
6304
- this.getVisitorId(),
6305
- this.getDeviceSignals(),
6306
- detectVPN().catch(() => null)
6307
- ]);
6308
- const behaviorMetrics = this.behaviorAnalyzer.getMetrics();
6309
- signals.behavior = {
6310
- score: behaviorMetrics.score,
6311
- mouseMovements: behaviorMetrics.mouse.totalMovements,
6312
- keystrokes: behaviorMetrics.keyboard.totalKeystrokes,
6313
- scrolls: behaviorMetrics.scroll.totalScrolls,
6314
- clicks: behaviorMetrics.clicks.totalClicks,
6315
- isHuman: behaviorMetrics.score > 0.6
6890
+ getSessionId() {
6891
+ return this.session.sessionId;
6892
+ }
6893
+ /** Stamped as distinct_id on every subsequent event (identify() feeds this). */
6894
+ setDistinctId(distinctId) {
6895
+ this.distinctId = distinctId;
6896
+ }
6897
+ // -------------------------------------------------------------------------
6898
+ // Tap path (hot — record + enqueue only)
6899
+ // -------------------------------------------------------------------------
6900
+ onAutocaptureEvent(event) {
6901
+ if (!this.active || event.type !== "click") return;
6902
+ const p = event.properties;
6903
+ const clientX = typeof p.$x === "number" ? p.$x : null;
6904
+ const clientY = typeof p.$y === "number" ? p.$y : null;
6905
+ if (clientX === null || clientY === null) return;
6906
+ this.recordTap({
6907
+ selector: typeof p.$selector === "string" ? p.$selector : "",
6908
+ label: typeof p.$el_text === "string" && p.$el_text || typeof p.$aria_label === "string" && p.$aria_label || void 0,
6909
+ role: typeof p.$el_role === "string" && p.$el_role || roleForTag(typeof p.$tag_name === "string" ? p.$tag_name : void 0),
6910
+ clientX,
6911
+ clientY
6912
+ });
6913
+ }
6914
+ /** Record a tap. Coordinates are viewport CSS px; normalization happens here. */
6915
+ recordTap(input) {
6916
+ if (!this.active || typeof window === "undefined") return;
6917
+ const vw = window.innerWidth || 1;
6918
+ const vh = window.innerHeight || 1;
6919
+ const screenName = this.pathname() ?? "/";
6920
+ const ev = this.baseEvent("tap");
6921
+ ev.screen_name = screenName;
6922
+ ev.screen_fp = this.fpCache.get(screenName) ?? screenFingerprint(screenName, ZERO_HASH);
6923
+ ev.element_id = input.selector || "unknown";
6924
+ if (input.label) ev.element_label = input.label;
6925
+ if (input.role) ev.element_role = input.role;
6926
+ ev.x = clamp01(input.clientX / vw);
6927
+ ev.y = clamp01(input.clientY / vh);
6928
+ ev.viewport_w = vw;
6929
+ ev.viewport_h = vh;
6930
+ this.enqueue(ev);
6931
+ this.scheduleFingerprint(screenName);
6932
+ }
6933
+ // -------------------------------------------------------------------------
6934
+ // Navigation → screen_view
6935
+ // -------------------------------------------------------------------------
6936
+ setupNavigationTracking() {
6937
+ if (typeof window === "undefined" || !window.history || typeof window.addEventListener !== "function") {
6938
+ return;
6939
+ }
6940
+ const onNavigate = () => this.handleRouteChange();
6941
+ this.popstateHandler = onNavigate;
6942
+ window.addEventListener("popstate", this.popstateHandler);
6943
+ const originalPushState = window.history.pushState.bind(window.history);
6944
+ const originalReplaceState = window.history.replaceState.bind(window.history);
6945
+ this.originalPushState = window.history.pushState;
6946
+ this.originalReplaceState = window.history.replaceState;
6947
+ window.history.pushState = (...args) => {
6948
+ originalPushState(...args);
6949
+ onNavigate();
6316
6950
  };
6317
- const vpnDetected = vpnResult?.vpnDetected || vpnResult?.torDetected || false;
6951
+ window.history.replaceState = (...args) => {
6952
+ originalReplaceState(...args);
6953
+ onNavigate();
6954
+ };
6955
+ }
6956
+ handleRouteChange() {
6957
+ if (!this.active) return;
6958
+ const next = this.pathname();
6959
+ if (next === null || next === this.currentScreen) return;
6960
+ const prev = this.currentScreen;
6961
+ this.currentScreen = next;
6962
+ const ev = this.baseEvent("screen_view");
6963
+ ev.screen_name = next;
6964
+ if (prev) ev.prev_screen_name = prev;
6965
+ this.enqueue(ev);
6966
+ this.scheduleFingerprint(next);
6967
+ }
6968
+ // -------------------------------------------------------------------------
6969
+ // Session end / pagehide
6970
+ // -------------------------------------------------------------------------
6971
+ setupPagehide() {
6972
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
6973
+ this.pagehideHandler = () => {
6974
+ if (!this.active) return;
6975
+ const ev = this.baseEvent("session_end");
6976
+ ev.reason = "background";
6977
+ ev.duration_ms = Math.max(0, Date.now() - this.session.startedAt);
6978
+ this.enqueue(ev);
6979
+ void this.flush({ keepalive: true });
6980
+ };
6981
+ window.addEventListener("pagehide", this.pagehideHandler);
6982
+ }
6983
+ // -------------------------------------------------------------------------
6984
+ // Batching (spec §1.3 — 50 events / 10 s / pagehide)
6985
+ // -------------------------------------------------------------------------
6986
+ baseEvent(type) {
6318
6987
  return {
6319
- visitorId: visitorResult.visitorId,
6320
- riskScore: this.calculateRiskScore(signals, vpnDetected),
6321
- signals,
6322
- flags: {
6323
- incognito: signals.smart?.incognito || signals.incognito,
6324
- bot: signals.smart?.bot || signals.bot || !signals.behavior.isHuman,
6325
- vpn: vpnDetected || signals.smart?.timezoneMismatch,
6326
- tampered: signals.smart?.privacyBrowser
6988
+ type,
6989
+ session_id: this.session.sessionId,
6990
+ sequence: this.session.nextSequence(),
6991
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6992
+ platform: "web",
6993
+ app_version: this.config.appVersion || void 0,
6994
+ distinct_id: this.distinctId || void 0,
6995
+ os_version: typeof navigator !== "undefined" && navigator.userAgent ? navigator.userAgent : void 0
6996
+ };
6997
+ }
6998
+ enqueue(event) {
6999
+ if (this.disabled) return;
7000
+ this.queue.push(event);
7001
+ if (this.queue.length >= (this.config.maxBatchSize ?? DEFAULT_MAX_BATCH)) {
7002
+ void this.flush();
7003
+ }
7004
+ }
7005
+ startFlushTimer() {
7006
+ if (this.flushTimer) clearInterval(this.flushTimer);
7007
+ this.flushTimer = setInterval(() => {
7008
+ void this.flush();
7009
+ }, this.config.flushInterval ?? DEFAULT_FLUSH_INTERVAL4);
7010
+ }
7011
+ async flush(opts) {
7012
+ if (this.queue.length === 0) return;
7013
+ if (this.disabled) {
7014
+ this.queue = [];
7015
+ return;
7016
+ }
7017
+ const events = this.queue;
7018
+ this.queue = [];
7019
+ try {
7020
+ const res = await this.transport(
7021
+ `${this.config.endpoint}/api/taps`,
7022
+ JSON.stringify({ events }),
7023
+ { headers: this.headers(), keepalive: opts?.keepalive }
7024
+ );
7025
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
7026
+ this.flushFailures = 0;
7027
+ this.log(`Flushed ${events.length} events`);
7028
+ } catch (err) {
7029
+ this.flushFailures++;
7030
+ if (this.flushFailures >= MAX_FLUSH_FAILURES5) {
7031
+ this.disabled = true;
7032
+ this.queue = [];
7033
+ this.log(`Disabled after ${MAX_FLUSH_FAILURES5} consecutive flush failures`);
7034
+ return;
6327
7035
  }
7036
+ this.queue.unshift(...events);
7037
+ this.log(`Flush failed (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES5}):`, err);
7038
+ }
7039
+ }
7040
+ headers() {
7041
+ return {
7042
+ "Content-Type": "application/json",
7043
+ "X-Project-ID": this.config.projectId,
7044
+ "X-API-Key": this.config.apiKey
6328
7045
  };
6329
7046
  }
6330
- stopBehaviorTracking() {
6331
- this.behaviorAnalyzer.stop();
7047
+ // -------------------------------------------------------------------------
7048
+ // Fingerprint pipeline (idle-gated, throttled, upload-on-miss)
7049
+ // -------------------------------------------------------------------------
7050
+ scheduleFingerprint(screenName) {
7051
+ if (!this.active || this.disabled || !this.rasterizer) return;
7052
+ const now = Date.now();
7053
+ const last = this.lastRasterAt.get(screenName) ?? 0;
7054
+ const throttle = this.config.templateThrottleMs ?? DEFAULT_TEMPLATE_THROTTLE;
7055
+ if (now - last < throttle || this.rasterInFlight) return;
7056
+ this.lastRasterAt.set(screenName, now);
7057
+ idle(() => {
7058
+ void this.captureFingerprint(screenName);
7059
+ });
6332
7060
  }
6333
- async fetchVisitorId() {
6334
- const signals = await this.getDeviceSignals();
6335
- const stableFingerprint = await getStableFingerprint(signals);
6336
- const endpoint = this.config.visitorEndpoint || `${this.config.endpoint}/api/visitors`;
6337
- const previousVisitorId = this.getCookieVisitorId() || this.cachedVisitor?.result.visitorId || void 0;
7061
+ async captureFingerprint(screenName) {
7062
+ if (!this.active || this.rasterInFlight || !this.rasterizer) return;
7063
+ this.rasterInFlight = true;
6338
7064
  try {
6339
- const response = await fetch(endpoint, {
6340
- method: "POST",
6341
- headers: {
6342
- "Content-Type": "application/json",
6343
- "X-API-Key": this.config.apiKey
6344
- },
6345
- credentials: "include",
6346
- body: JSON.stringify({
6347
- signals,
6348
- stableFingerprint,
6349
- previousVisitorId
6350
- })
6351
- });
6352
- if (!response.ok) {
6353
- throw new Error(`HTTP ${response.status}`);
7065
+ const raster = await this.rasterizer.rasterize();
7066
+ if (!raster) {
7067
+ if (!this.fpCache.has(screenName)) {
7068
+ this.fpCache.set(screenName, screenFingerprint(screenName, ZERO_HASH));
7069
+ }
7070
+ return;
6354
7071
  }
6355
- const result = await response.json();
6356
- this.cachedVisitor = {
6357
- result,
6358
- signals,
6359
- cachedAt: Date.now()
6360
- };
6361
- this.saveToStorage();
6362
- this.setCookieVisitorId(result.visitorId);
6363
- this.log("Visitor ID fetched:", result.visitorId);
6364
- return result;
7072
+ const hash = dHash(raster.rgba, raster.width, raster.height);
7073
+ const fp = screenFingerprint(screenName, hash);
7074
+ this.fpCache.set(screenName, fp);
7075
+ await this.maybeUploadTemplate(screenName, hash, fp, raster);
6365
7076
  } catch (err) {
6366
- this.log("Failed to fetch visitor ID:", err);
6367
- throw err;
7077
+ this.log("Fingerprint capture failed:", err);
7078
+ if (!this.fpCache.has(screenName)) {
7079
+ this.fpCache.set(screenName, screenFingerprint(screenName, ZERO_HASH));
7080
+ }
7081
+ } finally {
7082
+ this.rasterInFlight = false;
6368
7083
  }
6369
7084
  }
6370
- calculateRiskScore(signals, vpnDetected = false) {
6371
- let score = 0;
6372
- const smart = signals.smart;
6373
- if (smart?.bot || signals.bot) score += 0.8;
6374
- if (signals.behavior) {
6375
- if (signals.behavior.score < 0.3) score += 0.4;
6376
- else if (signals.behavior.score < 0.5) score += 0.2;
7085
+ async maybeUploadTemplate(screenName, hash, fp, raster) {
7086
+ for (const uploaded of this.uploadedFps) {
7087
+ const parsed = parseFingerprint(uploaded);
7088
+ if (parsed.screenName === screenName && hamming(parsed.pHashHex, hash) <= HAMMING_THRESHOLD) {
7089
+ return;
7090
+ }
6377
7091
  }
6378
- if (vpnDetected) score += 0.25;
6379
- if (smart?.incognito || signals.incognito) score += 0.2;
6380
- if (smart?.devToolsOpen) score += 0.1;
6381
- if (smart?.privacyBrowser) score += 0.1;
6382
- if (signals.hardwareConcurrency === 0) score += 0.1;
6383
- if (signals.maxTouchPoints === 0 && /mobile|android|iphone/i.test(signals.userAgent)) {
6384
- score += 0.15;
7092
+ if (!raster.pngBase64) return;
7093
+ try {
7094
+ const res = await this.transport(
7095
+ `${this.config.endpoint}/api/taps/template`,
7096
+ JSON.stringify({
7097
+ route_name: screenName,
7098
+ p_hash: hash,
7099
+ width: raster.width,
7100
+ height: raster.height,
7101
+ image_base64: raster.pngBase64
7102
+ }),
7103
+ { headers: this.headers() }
7104
+ );
7105
+ if (res.ok) {
7106
+ this.uploadedFps.add(fp);
7107
+ this.log("Uploaded template for", fp);
7108
+ }
7109
+ } catch (err) {
7110
+ this.log("Template upload failed:", err);
6385
7111
  }
6386
- if (!signals.cookieEnabled) score += 0.1;
6387
- if (smart?.doNotTrack) score += 0.05;
6388
- if (smart?.deviceVisitCount === 1) score += 0.05;
6389
- return Math.round(Math.min(score, 1) * 100) / 100;
6390
7112
  }
6391
- isCacheExpired() {
6392
- if (!this.cachedVisitor) return true;
6393
- return Date.now() - this.cachedVisitor.cachedAt > CACHE_TTL_MS;
7113
+ // -------------------------------------------------------------------------
7114
+ // Helpers
7115
+ // -------------------------------------------------------------------------
7116
+ combinedBlockSelector() {
7117
+ return this.config.blockSelector ? `${DEFAULT_BLOCK_SELECTOR}, ${this.config.blockSelector}` : DEFAULT_BLOCK_SELECTOR;
6394
7118
  }
6395
- loadFromStorage() {
6396
- if (typeof window === "undefined" || typeof localStorage === "undefined") return;
6397
- try {
6398
- const stored = localStorage.getItem(CACHE_KEY);
6399
- if (!stored) return;
6400
- const parsed = JSON.parse(stored);
6401
- if (!this.isCacheExpiredAt(parsed.cachedAt)) {
6402
- this.cachedVisitor = parsed;
6403
- this.log("Loaded cached visitor from storage");
6404
- } else {
6405
- localStorage.removeItem(CACHE_KEY);
6406
- }
6407
- } catch {
7119
+ pathname() {
7120
+ if (typeof window === "undefined" || !window.location) return null;
7121
+ return window.location.pathname || "/";
7122
+ }
7123
+ log(...args) {
7124
+ if (this.config.debug) {
7125
+ console.log("[SitePong Watchtower]", ...args);
6408
7126
  }
6409
7127
  }
6410
- saveToStorage() {
6411
- if (typeof window === "undefined" || typeof localStorage === "undefined") return;
6412
- if (!this.cachedVisitor) return;
6413
- try {
6414
- localStorage.setItem(CACHE_KEY, JSON.stringify(this.cachedVisitor));
6415
- } catch {
7128
+ };
7129
+ function clamp01(n) {
7130
+ if (!Number.isFinite(n)) return 0;
7131
+ return Math.min(1, Math.max(0, n));
7132
+ }
7133
+ function roleForTag(tag) {
7134
+ switch (tag) {
7135
+ case "a":
7136
+ return "link";
7137
+ case "button":
7138
+ return "button";
7139
+ case "input":
7140
+ return "input";
7141
+ case "select":
7142
+ return "select";
7143
+ case "textarea":
7144
+ return "textarea";
7145
+ default:
7146
+ return tag || void 0;
7147
+ }
7148
+ }
7149
+ function idle(cb) {
7150
+ const g = globalThis;
7151
+ if (typeof g.requestIdleCallback === "function") {
7152
+ g.requestIdleCallback(cb, { timeout: 2e3 });
7153
+ } else {
7154
+ setTimeout(cb, 250);
7155
+ }
7156
+ }
7157
+ var VOID_TAGS = /* @__PURE__ */ new Set([
7158
+ "area",
7159
+ "base",
7160
+ "br",
7161
+ "col",
7162
+ "embed",
7163
+ "hr",
7164
+ "img",
7165
+ "input",
7166
+ "link",
7167
+ "meta",
7168
+ "param",
7169
+ "source",
7170
+ "track",
7171
+ "wbr"
7172
+ ]);
7173
+ function escapeXml(text) {
7174
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
7175
+ }
7176
+ function stripInvalidXml(text) {
7177
+ return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g, "");
7178
+ }
7179
+ function serializedToXhtml(node, isRoot = false) {
7180
+ if (node.type === "text") {
7181
+ return escapeXml(stripInvalidXml(node.textContent ?? ""));
7182
+ }
7183
+ if (node.type === "comment") {
7184
+ return "";
7185
+ }
7186
+ const tagName = (node.tagName || "div").toLowerCase();
7187
+ const attrs = { ...node.attributes || {} };
7188
+ const cssText = attrs["_cssText"];
7189
+ if (tagName === "style" || tagName === "link") {
7190
+ if (typeof cssText === "string" && cssText) {
7191
+ return `<style>${escapeXml(stripInvalidXml(cssText))}</style>`;
6416
7192
  }
7193
+ if (tagName === "link") return "";
6417
7194
  }
6418
- isCacheExpiredAt(cachedAt) {
6419
- return Date.now() - cachedAt > CACHE_TTL_MS;
7195
+ const computedStyle = attrs["_computedStyle"];
7196
+ if (typeof computedStyle === "string" && computedStyle) {
7197
+ attrs["style"] = attrs["style"] ? `${attrs["style"]};${computedStyle}` : computedStyle;
6420
7198
  }
6421
- getCookieVisitorId() {
6422
- if (typeof document === "undefined") return void 0;
6423
- try {
6424
- const match = document.cookie.match(new RegExp(`(?:^|; )${COOKIE_NAME}=([^;]*)`));
6425
- return match?.[1] || void 0;
6426
- } catch {
6427
- return void 0;
7199
+ const value = attrs["_value"];
7200
+ if (typeof value === "string" && value) {
7201
+ attrs["value"] = value;
7202
+ }
7203
+ if (attrs["_checked"] === "true") {
7204
+ attrs["checked"] = "checked";
7205
+ }
7206
+ let attrString = "";
7207
+ for (const [name, val] of Object.entries(attrs)) {
7208
+ if (name.startsWith("_")) continue;
7209
+ if (name.startsWith("on")) continue;
7210
+ if (!/^[a-zA-Z_][\w:.-]*$/.test(name)) continue;
7211
+ attrString += ` ${name}="${escapeXml(stripInvalidXml(String(val)))}"`;
7212
+ }
7213
+ if (isRoot) {
7214
+ attrString = ` xmlns="http://www.w3.org/1999/xhtml"${attrString}`;
7215
+ }
7216
+ if (VOID_TAGS.has(tagName)) {
7217
+ return `<${tagName}${attrString}/>`;
7218
+ }
7219
+ let childHtml = "";
7220
+ if (tagName === "style" && typeof cssText !== "string") {
7221
+ for (const child of node.children || []) {
7222
+ childHtml += serializedToXhtml(child);
7223
+ }
7224
+ } else {
7225
+ for (const child of node.children || []) {
7226
+ childHtml += serializedToXhtml(child);
6428
7227
  }
6429
7228
  }
6430
- setCookieVisitorId(visitorId) {
6431
- if (typeof document === "undefined") return;
7229
+ return `<${tagName}${attrString}>${childHtml}</${tagName}>`;
7230
+ }
7231
+ var DomRasterizer = class {
7232
+ constructor(options) {
7233
+ this.options = options;
7234
+ }
7235
+ async rasterize() {
6432
7236
  try {
6433
- document.cookie = `${COOKIE_NAME}=${visitorId}; path=/; max-age=${COOKIE_MAX_AGE}; SameSite=Lax`;
7237
+ if (typeof document === "undefined" || typeof window === "undefined") return null;
7238
+ resetNodeIds();
7239
+ const tree = serializeNode(document.documentElement, {
7240
+ maskInputs: true,
7241
+ blockSelector: this.options.blockSelector,
7242
+ maskSelector: this.options.maskSelector
7243
+ });
7244
+ if (!tree) return null;
7245
+ const vw = window.innerWidth || 1;
7246
+ const vh = window.innerHeight || 1;
7247
+ const xhtml = serializedToXhtml(tree, true);
7248
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${vw}" height="${vh}"><foreignObject width="100%" height="100%">${xhtml}</foreignObject></svg>`;
7249
+ const img = await loadSvgImage(svg);
7250
+ const scale = Math.min(1, (this.options.maxWidth ?? RASTER_MAX_WIDTH) / vw);
7251
+ const w = Math.max(1, Math.round(vw * scale));
7252
+ const h = Math.max(1, Math.round(vh * scale));
7253
+ const canvas = document.createElement("canvas");
7254
+ canvas.width = w;
7255
+ canvas.height = h;
7256
+ const ctx = canvas.getContext("2d");
7257
+ if (!ctx) return null;
7258
+ ctx.fillStyle = "#ffffff";
7259
+ ctx.fillRect(0, 0, w, h);
7260
+ ctx.drawImage(img, 0, 0, w, h);
7261
+ const imageData = ctx.getImageData(0, 0, w, h);
7262
+ const rgba = new Uint8Array(imageData.data.buffer, imageData.data.byteOffset, imageData.data.byteLength);
7263
+ let pngBase64 = null;
7264
+ try {
7265
+ const dataUrl = canvas.toDataURL("image/png");
7266
+ const prefix = "data:image/png;base64,";
7267
+ pngBase64 = dataUrl.startsWith(prefix) ? dataUrl.slice(prefix.length) : null;
7268
+ } catch {
7269
+ pngBase64 = null;
7270
+ }
7271
+ return { rgba, width: w, height: h, pngBase64 };
6434
7272
  } catch {
6435
- }
6436
- }
6437
- log(...args) {
6438
- if (this.config.debug) {
6439
- console.log("[SitePong Fingerprint]", ...args);
7273
+ return null;
6440
7274
  }
6441
7275
  }
6442
7276
  };
7277
+ function loadSvgImage(svg) {
7278
+ return new Promise((resolve, reject) => {
7279
+ const blob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
7280
+ const url = URL.createObjectURL(blob);
7281
+ const img = new Image();
7282
+ const timer = setTimeout(() => {
7283
+ URL.revokeObjectURL(url);
7284
+ reject(new Error("SVG rasterization timed out"));
7285
+ }, 3e3);
7286
+ img.onload = () => {
7287
+ clearTimeout(timer);
7288
+ URL.revokeObjectURL(url);
7289
+ resolve(img);
7290
+ };
7291
+ img.onerror = () => {
7292
+ clearTimeout(timer);
7293
+ URL.revokeObjectURL(url);
7294
+ reject(new Error("SVG rasterization failed"));
7295
+ };
7296
+ img.src = url;
7297
+ });
7298
+ }
6443
7299
 
6444
7300
  // src/entries/web.ts
6445
7301
  setEnvironment(createWebEnvironment());
6446
7302
  registerWebManagerFactories({
6447
7303
  createReplay: (cfg) => new ReplayManager(cfg),
6448
7304
  createFingerprint: (cfg) => new FingerprintManager(cfg),
6449
- createPerformance: (cfg) => new PerformanceManager(cfg)
7305
+ createPerformance: (cfg) => new PerformanceManager(cfg),
7306
+ createWatchtowerCapture: (cfg) => new WatchtowerWebCapture(cfg)
6450
7307
  });
6451
7308
 
6452
7309
  exports.DEFAULT_SUPERLINK_ENDPOINT = DEFAULT_SUPERLINK_ENDPOINT;
@@ -6529,7 +7386,9 @@ exports.startProfileSpan = startProfileSpan;
6529
7386
  exports.startReplay = startReplay;
6530
7387
  exports.startSpan = startSpan;
6531
7388
  exports.startTransaction = startTransaction;
7389
+ exports.startWatchtowerCapture = startWatchtowerCapture;
6532
7390
  exports.stopReplay = stopReplay;
7391
+ exports.stopWatchtowerCapture = stopWatchtowerCapture;
6533
7392
  exports.superlinkClient = superlinkClient;
6534
7393
  exports.superlinkIdentify = identify;
6535
7394
  exports.track = track;