sitepong 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn/sitepong.min.js +5 -5
- package/dist/cdn/sitepong.min.js.map +1 -1
- package/dist/entries/rn.d.ts +995 -0
- package/dist/entries/rn.js +178 -23
- package/dist/entries/rn.js.map +1 -1
- package/dist/entries/web.d.mts +1 -1
- package/dist/entries/web.d.ts +1 -1
- package/dist/entries/web.js +1916 -1113
- package/dist/entries/web.js.map +1 -1
- package/dist/entries/web.mjs +1915 -1114
- package/dist/entries/web.mjs.map +1 -1
- package/dist/index.d.mts +160 -1
- package/dist/index.d.ts +160 -1
- package/dist/index.js +89 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +88 -5
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +89 -3
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +88 -4
- package/dist/react/index.mjs.map +1 -1
- package/package.json +12 -3
package/dist/entries/web.js
CHANGED
|
@@ -688,6 +688,7 @@ var DEFAULT_BLOCK_SELECTORS = [
|
|
|
688
688
|
var MAX_TEXT_LENGTH = 255;
|
|
689
689
|
var AutocaptureModule = class {
|
|
690
690
|
constructor(config, callback) {
|
|
691
|
+
this.listeners = [];
|
|
691
692
|
this.clickHandler = null;
|
|
692
693
|
this.submitHandler = null;
|
|
693
694
|
this.active = false;
|
|
@@ -702,6 +703,27 @@ var AutocaptureModule = class {
|
|
|
702
703
|
};
|
|
703
704
|
this.callback = callback;
|
|
704
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
|
+
}
|
|
705
727
|
start() {
|
|
706
728
|
if (this.active || typeof document === "undefined" || typeof document.addEventListener !== "function") return;
|
|
707
729
|
this.active = true;
|
|
@@ -734,7 +756,7 @@ var AutocaptureModule = class {
|
|
|
734
756
|
properties.$event_type = "click";
|
|
735
757
|
properties.$x = e.clientX;
|
|
736
758
|
properties.$y = e.clientY;
|
|
737
|
-
this.
|
|
759
|
+
this.emit({
|
|
738
760
|
type: "click",
|
|
739
761
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
740
762
|
properties
|
|
@@ -756,7 +778,7 @@ var AutocaptureModule = class {
|
|
|
756
778
|
};
|
|
757
779
|
const elemProps = this.getElementProperties(form);
|
|
758
780
|
Object.assign(properties, elemProps);
|
|
759
|
-
this.
|
|
781
|
+
this.emit({
|
|
760
782
|
type: "form_submit",
|
|
761
783
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
762
784
|
properties
|
|
@@ -973,6 +995,14 @@ var AnalyticsManager = class {
|
|
|
973
995
|
);
|
|
974
996
|
this.autocaptureModule.start();
|
|
975
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
|
+
}
|
|
976
1006
|
track(eventName, properties) {
|
|
977
1007
|
if (!this.config.enabled) return;
|
|
978
1008
|
const event = this.createEvent("track", { name: eventName, properties });
|
|
@@ -1924,646 +1954,147 @@ var RemoteConfigManager = class {
|
|
|
1924
1954
|
}
|
|
1925
1955
|
}
|
|
1926
1956
|
};
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
const elements = document.getElementsByTagName("*");
|
|
1936
|
-
const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i;
|
|
1937
|
-
for (let i = 0; i < elements.length; i++) {
|
|
1938
|
-
const element = elements[i];
|
|
1939
|
-
let src = "";
|
|
1940
|
-
if (element.tagName === "IMG") {
|
|
1941
|
-
src = element.currentSrc || element.src;
|
|
1942
|
-
}
|
|
1943
|
-
if (!src) {
|
|
1944
|
-
const backgroundImage = getComputedStyle(element).getPropertyValue("background-image");
|
|
1945
|
-
if (backgroundImage) {
|
|
1946
|
-
const matches = styleURL.exec(backgroundImage);
|
|
1947
|
-
if (matches !== null) {
|
|
1948
|
-
src = matches[1];
|
|
1949
|
-
if (src.startsWith('"') || src.startsWith("'")) {
|
|
1950
|
-
src = src.substr(1, src.length - 2);
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
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("");
|
|
1954
1965
|
}
|
|
1955
|
-
|
|
1956
|
-
const time = src.substr(0, 10) === "data:image" ? 0 : resources[src];
|
|
1957
|
-
if (time === void 0) continue;
|
|
1958
|
-
const rect = element.getBoundingClientRect();
|
|
1959
|
-
const top = Math.max(rect.top, 0);
|
|
1960
|
-
const left = Math.max(rect.left, 0);
|
|
1961
|
-
const bottom = Math.min(
|
|
1962
|
-
rect.bottom,
|
|
1963
|
-
window.innerHeight || document.documentElement && document.documentElement.clientHeight || 0
|
|
1964
|
-
);
|
|
1965
|
-
const right = Math.min(
|
|
1966
|
-
rect.right,
|
|
1967
|
-
window.innerWidth || document.documentElement && document.documentElement.clientWidth || 0
|
|
1968
|
-
);
|
|
1969
|
-
if (bottom <= top || right <= left) continue;
|
|
1970
|
-
const area = (bottom - top) * (right - left);
|
|
1971
|
-
paintBlocks.push({ time, area });
|
|
1966
|
+
} catch {
|
|
1972
1967
|
}
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
let a = Math.max(
|
|
1977
|
-
document.documentElement && document.documentElement.clientWidth || 0,
|
|
1978
|
-
window.innerWidth || 0
|
|
1979
|
-
) * Math.max(
|
|
1980
|
-
document.documentElement && document.documentElement.clientHeight || 0,
|
|
1981
|
-
window.innerHeight || 0
|
|
1982
|
-
) / 10;
|
|
1983
|
-
let s = a * firstContentfulPaint;
|
|
1984
|
-
for (let i = 0; i < paintBlocks.length; i++) {
|
|
1985
|
-
const { time, area } = paintBlocks[i];
|
|
1986
|
-
a += area;
|
|
1987
|
-
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");
|
|
1988
1971
|
}
|
|
1989
|
-
return
|
|
1972
|
+
return hex;
|
|
1990
1973
|
}
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
sampleRate: 1,
|
|
2017
|
-
debug: false,
|
|
2018
|
-
...config
|
|
2019
|
-
};
|
|
2020
|
-
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;
|
|
2021
1999
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
if (this.config.webVitals !== false) {
|
|
2026
|
-
this.initWebVitals();
|
|
2027
|
-
}
|
|
2028
|
-
if (this.config.resourceTiming !== false) {
|
|
2029
|
-
this.initResourceTiming();
|
|
2030
|
-
}
|
|
2031
|
-
if (this.config.capturePageLoadTimings !== false) {
|
|
2032
|
-
this.initPageLoadTiming();
|
|
2033
|
-
}
|
|
2034
|
-
if (this.config.capturePageRenderTimings !== false) {
|
|
2035
|
-
this.initPageRenderTiming();
|
|
2036
|
-
}
|
|
2037
|
-
this.startFlushTimer();
|
|
2038
|
-
this.log("Performance monitoring initialized");
|
|
2000
|
+
const parts = traceparent.split("-");
|
|
2001
|
+
if (parts.length !== 4) {
|
|
2002
|
+
return null;
|
|
2039
2003
|
}
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2004
|
+
const [version, traceId, parentSpanId, flags] = parts;
|
|
2005
|
+
if (version !== "00") {
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
if (traceId.length !== 32 || !/^[0-9a-f]{32}$/.test(traceId)) {
|
|
2009
|
+
return null;
|
|
2010
|
+
}
|
|
2011
|
+
if (parentSpanId.length !== 16 || !/^[0-9a-f]{16}$/.test(parentSpanId)) {
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
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);
|
|
2061
2038
|
});
|
|
2062
2039
|
}
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
try {
|
|
2067
|
-
this.resourceObserver = new PerformanceObserver((list) => {
|
|
2068
|
-
for (const entry of list.getEntries()) {
|
|
2069
|
-
this.processResourceEntry(entry);
|
|
2070
|
-
}
|
|
2071
|
-
});
|
|
2072
|
-
this.resourceObserver.observe({ type: "resource", buffered: true });
|
|
2073
|
-
} catch {
|
|
2074
|
-
this.log("PerformanceObserver for resource timing not supported");
|
|
2040
|
+
injectHeaders(url, headers, context) {
|
|
2041
|
+
if (!this.shouldPropagate(url)) {
|
|
2042
|
+
return headers;
|
|
2075
2043
|
}
|
|
2044
|
+
const traceHeaders = propagateTrace(context);
|
|
2045
|
+
return {
|
|
2046
|
+
...headers,
|
|
2047
|
+
...traceHeaders
|
|
2048
|
+
};
|
|
2076
2049
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
}
|
|
2081
|
-
processResourceEntry(entry) {
|
|
2082
|
-
if (entry.duration < 0 || !entry.name.startsWith("http")) return;
|
|
2083
|
-
if (this.isServiceURL(entry.name)) return;
|
|
2084
|
-
if (this.resourceTimeMap !== null) {
|
|
2085
|
-
this.resourceTimeMap[entry.name] = entry.startTime + entry.duration;
|
|
2050
|
+
start() {
|
|
2051
|
+
if (typeof window === "undefined" || !window.fetch) {
|
|
2052
|
+
return;
|
|
2086
2053
|
}
|
|
2087
|
-
|
|
2088
|
-
|
|
2054
|
+
if (this.originalFetch) {
|
|
2055
|
+
return;
|
|
2089
2056
|
}
|
|
2090
|
-
|
|
2091
|
-
this
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
ttfb: entry.responseStart - entry.requestStart,
|
|
2107
|
-
contentDownload: entry.responseEnd - entry.responseStart,
|
|
2108
|
-
total: entry.duration ?? entry.responseEnd - entry.startTime,
|
|
2109
|
-
stalled,
|
|
2110
|
-
cached,
|
|
2111
|
-
failed,
|
|
2112
|
-
responseStatus,
|
|
2113
|
-
headerSize: entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
|
|
2114
|
-
encodedBodySize: entry.encodedBodySize || 0,
|
|
2115
|
-
decodedBodySize: failed ? -111 : entry.decodedBodySize || 0,
|
|
2116
|
-
transferSize: entry.transferSize,
|
|
2117
|
-
initiatorType: entry.initiatorType
|
|
2118
|
-
};
|
|
2119
|
-
const entryName = this.config.resourceNameSanitizer ? this.config.resourceNameSanitizer(entry.name) : entry.name;
|
|
2120
|
-
this.addMetric({
|
|
2121
|
-
type: "resource",
|
|
2122
|
-
name: entryName,
|
|
2123
|
-
value: entry.duration,
|
|
2124
|
-
unit: "ms",
|
|
2125
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2126
|
-
url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
|
|
2127
|
-
data: breakdown
|
|
2128
|
-
});
|
|
2129
|
-
}
|
|
2130
|
-
// ---- Page Load Timing (adapted from OpenReplay timing.ts) ----
|
|
2131
|
-
initPageLoadTiming() {
|
|
2132
|
-
if (typeof window === "undefined" || !window.performance) return;
|
|
2133
|
-
let firstPaint = 0;
|
|
2134
|
-
let firstContentfulPaint = 0;
|
|
2135
|
-
let sent = false;
|
|
2136
|
-
const startTime = performance.now();
|
|
2137
|
-
this.pageLoadTimer = setInterval(() => {
|
|
2138
|
-
if (sent) {
|
|
2139
|
-
if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
|
|
2140
|
-
return;
|
|
2141
|
-
}
|
|
2142
|
-
if (firstPaint === 0 || firstContentfulPaint === 0) {
|
|
2143
|
-
for (const entry of performance.getEntriesByType("paint")) {
|
|
2144
|
-
if (entry.name === "first-paint") firstPaint = entry.startTime;
|
|
2145
|
-
if (entry.name === "first-contentful-paint") firstContentfulPaint = entry.startTime;
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
const nav = performance.getEntriesByType("navigation")[0];
|
|
2149
|
-
const timedOut = performance.now() - startTime > PAGE_TIMING_TIMEOUT;
|
|
2150
|
-
if (nav && nav.loadEventEnd > 0 || timedOut) {
|
|
2151
|
-
sent = true;
|
|
2152
|
-
if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
|
|
2153
|
-
if (nav) {
|
|
2154
|
-
const navigationStart = nav.startTime;
|
|
2155
|
-
this.addMetric({
|
|
2156
|
-
type: "navigation",
|
|
2157
|
-
name: "page_load",
|
|
2158
|
-
value: nav.loadEventEnd - navigationStart,
|
|
2159
|
-
unit: "ms",
|
|
2160
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2161
|
-
url: window.location.href,
|
|
2162
|
-
data: {
|
|
2163
|
-
requestStart: nav.requestStart - navigationStart,
|
|
2164
|
-
responseStart: nav.responseStart - navigationStart,
|
|
2165
|
-
responseEnd: nav.responseEnd - navigationStart,
|
|
2166
|
-
domContentLoadedEventStart: nav.domContentLoadedEventEnd - navigationStart,
|
|
2167
|
-
domContentLoadedEventEnd: nav.domContentLoadedEventEnd - navigationStart,
|
|
2168
|
-
loadEventStart: nav.loadEventStart - navigationStart,
|
|
2169
|
-
loadEventEnd: nav.loadEventEnd - navigationStart,
|
|
2170
|
-
firstPaint,
|
|
2171
|
-
firstContentfulPaint,
|
|
2172
|
-
dns: nav.domainLookupEnd - nav.domainLookupStart,
|
|
2173
|
-
tcp: nav.connectEnd - nav.connectStart,
|
|
2174
|
-
ttfb: nav.responseStart - nav.requestStart,
|
|
2175
|
-
domComplete: nav.domComplete - navigationStart,
|
|
2176
|
-
transferSize: nav.transferSize,
|
|
2177
|
-
encodedBodySize: nav.encodedBodySize,
|
|
2178
|
-
decodedBodySize: nav.decodedBodySize
|
|
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;
|
|
2179
2073
|
}
|
|
2180
|
-
}
|
|
2181
|
-
|
|
2182
|
-
}
|
|
2183
|
-
}, PAGE_TIMING_POLL_INTERVAL);
|
|
2184
|
-
}
|
|
2185
|
-
// ---- Page Render Timing: Speed Index, Visually Complete, TTI (adapted from OpenReplay) ----
|
|
2186
|
-
initPageRenderTiming() {
|
|
2187
|
-
if (typeof window === "undefined" || !window.performance) return;
|
|
2188
|
-
let firstContentfulPaint = 0;
|
|
2189
|
-
let visuallyComplete = 0;
|
|
2190
|
-
let interactiveWindowStartTime = 0;
|
|
2191
|
-
let interactiveWindowTickTime = 0;
|
|
2192
|
-
let paintBlocks = null;
|
|
2193
|
-
let sent = false;
|
|
2194
|
-
const startTime = performance.now();
|
|
2195
|
-
this.pageRenderTimer = setInterval(() => {
|
|
2196
|
-
if (sent) {
|
|
2197
|
-
if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
|
|
2198
|
-
return;
|
|
2199
|
-
}
|
|
2200
|
-
const time = performance.now();
|
|
2201
|
-
if (firstContentfulPaint === 0) {
|
|
2202
|
-
for (const entry of performance.getEntriesByType("paint")) {
|
|
2203
|
-
if (entry.name === "first-contentful-paint") {
|
|
2204
|
-
firstContentfulPaint = entry.startTime;
|
|
2074
|
+
} else {
|
|
2075
|
+
Object.assign(existingHeaders, init2.headers);
|
|
2205
2076
|
}
|
|
2206
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);
|
|
2207
2087
|
}
|
|
2208
|
-
|
|
2209
|
-
const times = Object.values(this.resourceTimeMap);
|
|
2210
|
-
if (times.length > 0) {
|
|
2211
|
-
visuallyComplete = Math.max(...times);
|
|
2212
|
-
}
|
|
2213
|
-
if (time - visuallyComplete > 1e3) {
|
|
2214
|
-
paintBlocks = getPaintBlocks(this.resourceTimeMap);
|
|
2215
|
-
this.resourceTimeMap = null;
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
if (interactiveWindowTickTime !== null) {
|
|
2219
|
-
if (time - interactiveWindowTickTime > 50) {
|
|
2220
|
-
interactiveWindowStartTime = time;
|
|
2221
|
-
}
|
|
2222
|
-
interactiveWindowTickTime = time - interactiveWindowStartTime > 5e3 ? null : time;
|
|
2223
|
-
}
|
|
2224
|
-
const timedOut = time - startTime > PAGE_TIMING_TIMEOUT;
|
|
2225
|
-
if (paintBlocks !== null && interactiveWindowTickTime === null || timedOut) {
|
|
2226
|
-
sent = true;
|
|
2227
|
-
if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
|
|
2228
|
-
this.resourceTimeMap = null;
|
|
2229
|
-
const speedIndex = paintBlocks === null ? 0 : calculateSpeedIndex(firstContentfulPaint || 0, paintBlocks);
|
|
2230
|
-
const nav = performance.getEntriesByType("navigation")[0];
|
|
2231
|
-
const domContentLoadedEnd = nav ? nav.domContentLoadedEventEnd - nav.startTime : 0;
|
|
2232
|
-
const timeToInteractive = interactiveWindowTickTime === null ? Math.max(interactiveWindowStartTime, firstContentfulPaint, domContentLoadedEnd) : 0;
|
|
2233
|
-
const url = window.location.href;
|
|
2234
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2235
|
-
if (speedIndex > 0) {
|
|
2236
|
-
this.addMetric({
|
|
2237
|
-
type: "page_render",
|
|
2238
|
-
name: "speed_index",
|
|
2239
|
-
value: Math.round(speedIndex),
|
|
2240
|
-
unit: "ms",
|
|
2241
|
-
timestamp,
|
|
2242
|
-
url
|
|
2243
|
-
});
|
|
2244
|
-
}
|
|
2245
|
-
if (visuallyComplete > 0) {
|
|
2246
|
-
this.addMetric({
|
|
2247
|
-
type: "page_render",
|
|
2248
|
-
name: "visually_complete",
|
|
2249
|
-
value: Math.round(visuallyComplete),
|
|
2250
|
-
unit: "ms",
|
|
2251
|
-
timestamp,
|
|
2252
|
-
url
|
|
2253
|
-
});
|
|
2254
|
-
}
|
|
2255
|
-
if (timeToInteractive > 0) {
|
|
2256
|
-
this.addMetric({
|
|
2257
|
-
type: "page_render",
|
|
2258
|
-
name: "tti",
|
|
2259
|
-
value: Math.round(timeToInteractive),
|
|
2260
|
-
unit: "ms",
|
|
2261
|
-
timestamp,
|
|
2262
|
-
url
|
|
2263
|
-
});
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
}, PAGE_TIMING_POLL_INTERVAL);
|
|
2267
|
-
}
|
|
2268
|
-
// ---- Transactions / Spans (unchanged) ----
|
|
2269
|
-
startTransaction(name, data) {
|
|
2270
|
-
const id = `txn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
2271
|
-
const transaction = {
|
|
2272
|
-
id,
|
|
2273
|
-
name,
|
|
2274
|
-
startTime: performance.now(),
|
|
2275
|
-
status: "running",
|
|
2276
|
-
spans: [],
|
|
2277
|
-
data
|
|
2088
|
+
return self.originalFetch.call(window, input, init2);
|
|
2278
2089
|
};
|
|
2279
|
-
this.activeTransactions.set(id, transaction);
|
|
2280
|
-
this.log("Transaction started:", name, id);
|
|
2281
|
-
return id;
|
|
2282
2090
|
}
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
if (!transaction) {
|
|
2286
|
-
this.log("Transaction not found:", id);
|
|
2091
|
+
stop() {
|
|
2092
|
+
if (typeof window === "undefined") {
|
|
2287
2093
|
return;
|
|
2288
2094
|
}
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
for (const span of transaction.spans) {
|
|
2293
|
-
if (span.status === "running") {
|
|
2294
|
-
span.endTime = transaction.endTime;
|
|
2295
|
-
span.duration = span.endTime - span.startTime;
|
|
2296
|
-
span.status = status;
|
|
2297
|
-
}
|
|
2298
|
-
}
|
|
2299
|
-
this.activeTransactions.delete(id);
|
|
2300
|
-
this.addMetric({
|
|
2301
|
-
type: "transaction",
|
|
2302
|
-
name: transaction.name,
|
|
2303
|
-
value: transaction.duration,
|
|
2304
|
-
unit: "ms",
|
|
2305
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2306
|
-
url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
|
|
2307
|
-
transaction
|
|
2308
|
-
});
|
|
2309
|
-
this.log("Transaction ended:", transaction.name, `${transaction.duration.toFixed(1)}ms`);
|
|
2310
|
-
}
|
|
2311
|
-
startSpan(transactionId, name, data) {
|
|
2312
|
-
const transaction = this.activeTransactions.get(transactionId);
|
|
2313
|
-
if (!transaction) {
|
|
2314
|
-
this.log("Transaction not found for span:", transactionId);
|
|
2315
|
-
return "";
|
|
2316
|
-
}
|
|
2317
|
-
const span = {
|
|
2318
|
-
id: `span_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
|
|
2319
|
-
name,
|
|
2320
|
-
startTime: performance.now(),
|
|
2321
|
-
status: "running",
|
|
2322
|
-
data
|
|
2323
|
-
};
|
|
2324
|
-
transaction.spans.push(span);
|
|
2325
|
-
return span.id;
|
|
2326
|
-
}
|
|
2327
|
-
endSpan(transactionId, spanId, status = "ok") {
|
|
2328
|
-
const transaction = this.activeTransactions.get(transactionId);
|
|
2329
|
-
if (!transaction) return;
|
|
2330
|
-
const span = transaction.spans.find((s) => s.id === spanId);
|
|
2331
|
-
if (!span) return;
|
|
2332
|
-
span.endTime = performance.now();
|
|
2333
|
-
span.duration = span.endTime - span.startTime;
|
|
2334
|
-
span.status = status;
|
|
2335
|
-
}
|
|
2336
|
-
// ---- Public API ----
|
|
2337
|
-
getVitals() {
|
|
2338
|
-
return { ...this.vitals };
|
|
2339
|
-
}
|
|
2340
|
-
destroy() {
|
|
2341
|
-
if (this.flushTimer) {
|
|
2342
|
-
clearInterval(this.flushTimer);
|
|
2343
|
-
this.flushTimer = null;
|
|
2344
|
-
}
|
|
2345
|
-
if (this.pageLoadTimer) {
|
|
2346
|
-
clearInterval(this.pageLoadTimer);
|
|
2347
|
-
this.pageLoadTimer = null;
|
|
2348
|
-
}
|
|
2349
|
-
if (this.pageRenderTimer) {
|
|
2350
|
-
clearInterval(this.pageRenderTimer);
|
|
2351
|
-
this.pageRenderTimer = null;
|
|
2352
|
-
}
|
|
2353
|
-
if (this.resourceObserver) {
|
|
2354
|
-
this.resourceObserver.disconnect();
|
|
2355
|
-
this.resourceObserver = null;
|
|
2356
|
-
}
|
|
2357
|
-
this.flush();
|
|
2358
|
-
}
|
|
2359
|
-
// ---- Internal ----
|
|
2360
|
-
reportVital(name, value, unit = "ms") {
|
|
2361
|
-
this.addMetric({
|
|
2362
|
-
type: "web_vital",
|
|
2363
|
-
name,
|
|
2364
|
-
value,
|
|
2365
|
-
unit,
|
|
2366
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2367
|
-
url: typeof window !== "undefined" && window.location ? window.location.href : void 0
|
|
2368
|
-
});
|
|
2369
|
-
}
|
|
2370
|
-
addMetric(metric) {
|
|
2371
|
-
this.metrics.push(metric);
|
|
2372
|
-
}
|
|
2373
|
-
startFlushTimer() {
|
|
2374
|
-
this.flushTimer = setInterval(() => {
|
|
2375
|
-
this.flush();
|
|
2376
|
-
}, this.config.flushInterval);
|
|
2377
|
-
if (typeof document !== "undefined") {
|
|
2378
|
-
document.addEventListener("visibilitychange", () => {
|
|
2379
|
-
if (document.visibilityState === "hidden") {
|
|
2380
|
-
this.flush();
|
|
2381
|
-
}
|
|
2382
|
-
});
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
async flush() {
|
|
2386
|
-
if (this.metrics.length === 0) return;
|
|
2387
|
-
if (this.disabled) {
|
|
2388
|
-
this.metrics = [];
|
|
2389
|
-
return;
|
|
2390
|
-
}
|
|
2391
|
-
const metrics = [...this.metrics];
|
|
2392
|
-
this.metrics = [];
|
|
2393
|
-
this.resourceCount = 0;
|
|
2394
|
-
const endpoint = this.config.performanceEndpoint || `${this.config.endpoint}/api/performance`;
|
|
2395
|
-
try {
|
|
2396
|
-
const response = await fetch(endpoint, {
|
|
2397
|
-
method: "POST",
|
|
2398
|
-
headers: {
|
|
2399
|
-
"Content-Type": "application/json",
|
|
2400
|
-
"X-API-Key": this.config.apiKey
|
|
2401
|
-
},
|
|
2402
|
-
body: JSON.stringify({ metrics })
|
|
2403
|
-
});
|
|
2404
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
2405
|
-
this.flushFailures = 0;
|
|
2406
|
-
this.log(`Flushed ${metrics.length} performance metrics`);
|
|
2407
|
-
} catch (err) {
|
|
2408
|
-
this.flushFailures++;
|
|
2409
|
-
if (this.flushFailures >= MAX_FLUSH_FAILURES2) {
|
|
2410
|
-
this.disabled = true;
|
|
2411
|
-
console.warn(
|
|
2412
|
-
`[SitePong Performance] Disabled after ${MAX_FLUSH_FAILURES2} consecutive failures. Check that ${endpoint} is accessible and has proper CORS headers.`
|
|
2413
|
-
);
|
|
2414
|
-
return;
|
|
2415
|
-
}
|
|
2416
|
-
this.metrics.unshift(...metrics);
|
|
2417
|
-
this.log(`Failed to flush metrics (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES2}):`, err);
|
|
2418
|
-
}
|
|
2419
|
-
}
|
|
2420
|
-
log(...args) {
|
|
2421
|
-
if (this.config.debug) {
|
|
2422
|
-
console.log("[SitePong Performance]", ...args);
|
|
2423
|
-
}
|
|
2424
|
-
}
|
|
2425
|
-
};
|
|
2426
|
-
|
|
2427
|
-
// src/performance/tracing.ts
|
|
2428
|
-
function randomHex(byteLength) {
|
|
2429
|
-
try {
|
|
2430
|
-
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
2431
|
-
const bytes = new Uint8Array(byteLength);
|
|
2432
|
-
crypto.getRandomValues(bytes);
|
|
2433
|
-
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2434
|
-
}
|
|
2435
|
-
} catch {
|
|
2436
|
-
}
|
|
2437
|
-
let hex = "";
|
|
2438
|
-
for (let i = 0; i < byteLength; i++) {
|
|
2439
|
-
hex += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
|
|
2440
|
-
}
|
|
2441
|
-
return hex;
|
|
2442
|
-
}
|
|
2443
|
-
function generateTraceId() {
|
|
2444
|
-
return randomHex(16);
|
|
2445
|
-
}
|
|
2446
|
-
function generateSpanId() {
|
|
2447
|
-
return randomHex(8);
|
|
2448
|
-
}
|
|
2449
|
-
function createTraceContext() {
|
|
2450
|
-
return {
|
|
2451
|
-
traceId: generateTraceId(),
|
|
2452
|
-
spanId: generateSpanId(),
|
|
2453
|
-
parentSpanId: null,
|
|
2454
|
-
sampled: true
|
|
2455
|
-
};
|
|
2456
|
-
}
|
|
2457
|
-
function propagateTrace(context) {
|
|
2458
|
-
const sampledFlag = context.sampled ? "01" : "00";
|
|
2459
|
-
return {
|
|
2460
|
-
traceparent: `00-${context.traceId}-${context.spanId}-${sampledFlag}`,
|
|
2461
|
-
tracestate: `sitepong=${context.spanId}`
|
|
2462
|
-
};
|
|
2463
|
-
}
|
|
2464
|
-
function extractTrace(headers) {
|
|
2465
|
-
const traceparent = headers["traceparent"] || headers["Traceparent"];
|
|
2466
|
-
if (!traceparent) {
|
|
2467
|
-
return null;
|
|
2468
|
-
}
|
|
2469
|
-
const parts = traceparent.split("-");
|
|
2470
|
-
if (parts.length !== 4) {
|
|
2471
|
-
return null;
|
|
2472
|
-
}
|
|
2473
|
-
const [version, traceId, parentSpanId, flags] = parts;
|
|
2474
|
-
if (version !== "00") {
|
|
2475
|
-
return null;
|
|
2476
|
-
}
|
|
2477
|
-
if (traceId.length !== 32 || !/^[0-9a-f]{32}$/.test(traceId)) {
|
|
2478
|
-
return null;
|
|
2479
|
-
}
|
|
2480
|
-
if (parentSpanId.length !== 16 || !/^[0-9a-f]{16}$/.test(parentSpanId)) {
|
|
2481
|
-
return null;
|
|
2482
|
-
}
|
|
2483
|
-
if (flags.length !== 2 || !/^[0-9a-f]{2}$/.test(flags)) {
|
|
2484
|
-
return null;
|
|
2485
|
-
}
|
|
2486
|
-
const sampled = (parseInt(flags, 16) & 1) === 1;
|
|
2487
|
-
return {
|
|
2488
|
-
traceId,
|
|
2489
|
-
spanId: generateSpanId(),
|
|
2490
|
-
parentSpanId,
|
|
2491
|
-
sampled
|
|
2492
|
-
};
|
|
2493
|
-
}
|
|
2494
|
-
var TracePropagator = class {
|
|
2495
|
-
constructor(targets) {
|
|
2496
|
-
this.originalFetch = null;
|
|
2497
|
-
this.targets = targets;
|
|
2498
|
-
}
|
|
2499
|
-
shouldPropagate(url) {
|
|
2500
|
-
return this.targets.some((pattern) => {
|
|
2501
|
-
if (pattern.includes("*")) {
|
|
2502
|
-
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2503
|
-
const regex = new RegExp(`^${escaped}$`);
|
|
2504
|
-
return regex.test(url);
|
|
2505
|
-
}
|
|
2506
|
-
return url.startsWith(pattern);
|
|
2507
|
-
});
|
|
2508
|
-
}
|
|
2509
|
-
injectHeaders(url, headers, context) {
|
|
2510
|
-
if (!this.shouldPropagate(url)) {
|
|
2511
|
-
return headers;
|
|
2512
|
-
}
|
|
2513
|
-
const traceHeaders = propagateTrace(context);
|
|
2514
|
-
return {
|
|
2515
|
-
...headers,
|
|
2516
|
-
...traceHeaders
|
|
2517
|
-
};
|
|
2518
|
-
}
|
|
2519
|
-
start() {
|
|
2520
|
-
if (typeof window === "undefined" || !window.fetch) {
|
|
2521
|
-
return;
|
|
2522
|
-
}
|
|
2523
|
-
if (this.originalFetch) {
|
|
2524
|
-
return;
|
|
2525
|
-
}
|
|
2526
|
-
this.originalFetch = window.fetch;
|
|
2527
|
-
const self = this;
|
|
2528
|
-
window.fetch = function(input, init2) {
|
|
2529
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
2530
|
-
if (self.shouldPropagate(url)) {
|
|
2531
|
-
const context = createTraceContext();
|
|
2532
|
-
const traceHeaders = propagateTrace(context);
|
|
2533
|
-
const existingHeaders = {};
|
|
2534
|
-
if (init2?.headers) {
|
|
2535
|
-
if (init2.headers instanceof Headers) {
|
|
2536
|
-
init2.headers.forEach((value, key) => {
|
|
2537
|
-
existingHeaders[key] = value;
|
|
2538
|
-
});
|
|
2539
|
-
} else if (Array.isArray(init2.headers)) {
|
|
2540
|
-
for (const [key, value] of init2.headers) {
|
|
2541
|
-
existingHeaders[key] = value;
|
|
2542
|
-
}
|
|
2543
|
-
} else {
|
|
2544
|
-
Object.assign(existingHeaders, init2.headers);
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
const mergedHeaders = {
|
|
2548
|
-
...existingHeaders,
|
|
2549
|
-
...traceHeaders
|
|
2550
|
-
};
|
|
2551
|
-
const patchedInit = {
|
|
2552
|
-
...init2,
|
|
2553
|
-
headers: mergedHeaders
|
|
2554
|
-
};
|
|
2555
|
-
return self.originalFetch.call(window, input, patchedInit);
|
|
2556
|
-
}
|
|
2557
|
-
return self.originalFetch.call(window, input, init2);
|
|
2558
|
-
};
|
|
2559
|
-
}
|
|
2560
|
-
stop() {
|
|
2561
|
-
if (typeof window === "undefined") {
|
|
2562
|
-
return;
|
|
2563
|
-
}
|
|
2564
|
-
if (this.originalFetch) {
|
|
2565
|
-
window.fetch = this.originalFetch;
|
|
2566
|
-
this.originalFetch = null;
|
|
2095
|
+
if (this.originalFetch) {
|
|
2096
|
+
window.fetch = this.originalFetch;
|
|
2097
|
+
this.originalFetch = null;
|
|
2567
2098
|
}
|
|
2568
2099
|
}
|
|
2569
2100
|
};
|
|
@@ -2961,10 +2492,10 @@ var webManagerFactories = {};
|
|
|
2961
2492
|
function registerWebManagerFactories(factories) {
|
|
2962
2493
|
webManagerFactories = { ...webManagerFactories, ...factories };
|
|
2963
2494
|
}
|
|
2964
|
-
var
|
|
2495
|
+
var DEFAULT_ENDPOINT3 = "https://ingest.sitepong.com";
|
|
2965
2496
|
var DEFAULT_BATCH_SIZE2 = 10;
|
|
2966
|
-
var
|
|
2967
|
-
var
|
|
2497
|
+
var DEFAULT_FLUSH_INTERVAL2 = 5e3;
|
|
2498
|
+
var MAX_FLUSH_FAILURES2 = 3;
|
|
2968
2499
|
var SitePongClient = class {
|
|
2969
2500
|
constructor() {
|
|
2970
2501
|
this.errorQueue = [];
|
|
@@ -2983,16 +2514,18 @@ var SitePongClient = class {
|
|
|
2983
2514
|
this.databaseTracker = null;
|
|
2984
2515
|
this.profiler = null;
|
|
2985
2516
|
this.remoteConfigManager = null;
|
|
2517
|
+
this.watchtowerCapture = null;
|
|
2518
|
+
this.watchtowerOptions = void 0;
|
|
2986
2519
|
this.envUnsubs = [];
|
|
2987
2520
|
this.identifyHooks = [];
|
|
2988
2521
|
this.config = {
|
|
2989
2522
|
apiKey: "",
|
|
2990
|
-
endpoint:
|
|
2523
|
+
endpoint: DEFAULT_ENDPOINT3,
|
|
2991
2524
|
environment: "production",
|
|
2992
2525
|
release: "",
|
|
2993
2526
|
autoCapture: true,
|
|
2994
2527
|
maxBatchSize: DEFAULT_BATCH_SIZE2,
|
|
2995
|
-
flushInterval:
|
|
2528
|
+
flushInterval: DEFAULT_FLUSH_INTERVAL2,
|
|
2996
2529
|
debug: false
|
|
2997
2530
|
};
|
|
2998
2531
|
}
|
|
@@ -3014,7 +2547,7 @@ var SitePongClient = class {
|
|
|
3014
2547
|
if (config.enableFlags) {
|
|
3015
2548
|
this.flagManager = new FlagManager({
|
|
3016
2549
|
apiKey: config.apiKey,
|
|
3017
|
-
endpoint: config.flagsEndpoint || config.endpoint ||
|
|
2550
|
+
endpoint: config.flagsEndpoint || config.endpoint || DEFAULT_ENDPOINT3,
|
|
3018
2551
|
debug: config.debug
|
|
3019
2552
|
});
|
|
3020
2553
|
this.flagManager.init().catch((err) => {
|
|
@@ -3027,7 +2560,7 @@ var SitePongClient = class {
|
|
|
3027
2560
|
}
|
|
3028
2561
|
this.analyticsManager = new AnalyticsManager({
|
|
3029
2562
|
apiKey: config.apiKey,
|
|
3030
|
-
endpoint: config.endpoint ||
|
|
2563
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3031
2564
|
enabled: true,
|
|
3032
2565
|
autocapturePageviews: config.analytics.autocapturePageviews,
|
|
3033
2566
|
autocaptureClicks: config.analytics.autocaptureClicks,
|
|
@@ -3043,7 +2576,7 @@ var SitePongClient = class {
|
|
|
3043
2576
|
if (config.fingerprint?.enabled && webManagerFactories.createFingerprint) {
|
|
3044
2577
|
this.fingerprintManager = webManagerFactories.createFingerprint({
|
|
3045
2578
|
apiKey: config.apiKey,
|
|
3046
|
-
endpoint: config.endpoint ||
|
|
2579
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3047
2580
|
enabled: true,
|
|
3048
2581
|
extendedSignals: config.fingerprint.extendedSignals,
|
|
3049
2582
|
visitorEndpoint: config.fingerprint.visitorEndpoint,
|
|
@@ -3058,7 +2591,7 @@ var SitePongClient = class {
|
|
|
3058
2591
|
}
|
|
3059
2592
|
this.performanceManager = webManagerFactories.createPerformance({
|
|
3060
2593
|
apiKey: config.apiKey,
|
|
3061
|
-
endpoint: config.endpoint ||
|
|
2594
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3062
2595
|
enabled: true,
|
|
3063
2596
|
webVitals: config.performance.webVitals,
|
|
3064
2597
|
navigationTiming: config.performance.navigationTiming,
|
|
@@ -3082,7 +2615,7 @@ var SitePongClient = class {
|
|
|
3082
2615
|
}
|
|
3083
2616
|
this.replayManager = webManagerFactories.createReplay({
|
|
3084
2617
|
apiKey: config.apiKey,
|
|
3085
|
-
endpoint: config.endpoint ||
|
|
2618
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3086
2619
|
enabled: true,
|
|
3087
2620
|
maskInputs: config.replay.maskInputs,
|
|
3088
2621
|
blockSelector: config.replay.blockSelector,
|
|
@@ -3102,17 +2635,23 @@ var SitePongClient = class {
|
|
|
3102
2635
|
} else if (config.replay?.enabled) {
|
|
3103
2636
|
this.log("Replay requested but not available on this platform (web only).");
|
|
3104
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
|
+
}
|
|
3105
2644
|
if (config.crons?.enabled) {
|
|
3106
2645
|
this.cronManager = new CronMonitorManager({
|
|
3107
2646
|
apiKey: config.apiKey,
|
|
3108
|
-
endpoint: config.endpoint ||
|
|
2647
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3109
2648
|
debug: config.debug
|
|
3110
2649
|
});
|
|
3111
2650
|
}
|
|
3112
2651
|
if (config.database?.enabled) {
|
|
3113
2652
|
this.databaseTracker = new DatabaseTracker({
|
|
3114
2653
|
apiKey: config.apiKey,
|
|
3115
|
-
endpoint: config.endpoint ||
|
|
2654
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3116
2655
|
debug: config.debug,
|
|
3117
2656
|
slowQueryThreshold: config.database.slowQueryThreshold,
|
|
3118
2657
|
redactParams: config.database.redactParams
|
|
@@ -3121,7 +2660,7 @@ var SitePongClient = class {
|
|
|
3121
2660
|
if (config.metrics?.enabled) {
|
|
3122
2661
|
this.metricsManager = new MetricsManager({
|
|
3123
2662
|
apiKey: config.apiKey,
|
|
3124
|
-
endpoint: config.endpoint ||
|
|
2663
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3125
2664
|
debug: config.debug,
|
|
3126
2665
|
flushInterval: config.metrics.flushInterval,
|
|
3127
2666
|
maxBatchSize: config.metrics.maxBatchSize
|
|
@@ -3130,7 +2669,7 @@ var SitePongClient = class {
|
|
|
3130
2669
|
if (config.profiling?.enabled) {
|
|
3131
2670
|
this.profiler = new Profiler({
|
|
3132
2671
|
apiKey: config.apiKey,
|
|
3133
|
-
endpoint: config.endpoint ||
|
|
2672
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3134
2673
|
debug: config.debug,
|
|
3135
2674
|
sampleRate: config.profiling.sampleRate,
|
|
3136
2675
|
maxDuration: config.profiling.maxDuration,
|
|
@@ -3141,7 +2680,7 @@ var SitePongClient = class {
|
|
|
3141
2680
|
const storage = config._storageAdapter || getStorageAdapter();
|
|
3142
2681
|
this.remoteConfigManager = new RemoteConfigManager({
|
|
3143
2682
|
apiKey: config.apiKey,
|
|
3144
|
-
endpoint: config.endpoint ||
|
|
2683
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3145
2684
|
storage,
|
|
3146
2685
|
debug: config.debug
|
|
3147
2686
|
});
|
|
@@ -3175,7 +2714,7 @@ var SitePongClient = class {
|
|
|
3175
2714
|
if (config.enableFlags) {
|
|
3176
2715
|
this.flagManager = new FlagManager({
|
|
3177
2716
|
apiKey: config.apiKey,
|
|
3178
|
-
endpoint: config.flagsEndpoint || config.endpoint ||
|
|
2717
|
+
endpoint: config.flagsEndpoint || config.endpoint || DEFAULT_ENDPOINT3,
|
|
3179
2718
|
debug: config.debug
|
|
3180
2719
|
});
|
|
3181
2720
|
this.flagManager.init().catch((err) => {
|
|
@@ -3188,7 +2727,7 @@ var SitePongClient = class {
|
|
|
3188
2727
|
}
|
|
3189
2728
|
this.analyticsManager = new AnalyticsManager({
|
|
3190
2729
|
apiKey: config.apiKey,
|
|
3191
|
-
endpoint: config.endpoint ||
|
|
2730
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3192
2731
|
enabled: true,
|
|
3193
2732
|
maxBatchSize: config.analytics.maxBatchSize,
|
|
3194
2733
|
flushInterval: config.analytics.flushInterval,
|
|
@@ -3200,14 +2739,14 @@ var SitePongClient = class {
|
|
|
3200
2739
|
if (config.crons?.enabled) {
|
|
3201
2740
|
this.cronManager = new CronMonitorManager({
|
|
3202
2741
|
apiKey: config.apiKey,
|
|
3203
|
-
endpoint: config.endpoint ||
|
|
2742
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3204
2743
|
debug: config.debug
|
|
3205
2744
|
});
|
|
3206
2745
|
}
|
|
3207
2746
|
if (config.metrics?.enabled) {
|
|
3208
2747
|
this.metricsManager = new MetricsManager({
|
|
3209
2748
|
apiKey: config.apiKey,
|
|
3210
|
-
endpoint: config.endpoint ||
|
|
2749
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3211
2750
|
debug: config.debug,
|
|
3212
2751
|
flushInterval: config.metrics.flushInterval,
|
|
3213
2752
|
maxBatchSize: config.metrics.maxBatchSize
|
|
@@ -3216,7 +2755,7 @@ var SitePongClient = class {
|
|
|
3216
2755
|
if (config.database?.enabled) {
|
|
3217
2756
|
this.databaseTracker = new DatabaseTracker({
|
|
3218
2757
|
apiKey: config.apiKey,
|
|
3219
|
-
endpoint: config.endpoint ||
|
|
2758
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3220
2759
|
debug: config.debug,
|
|
3221
2760
|
slowQueryThreshold: config.database.slowQueryThreshold,
|
|
3222
2761
|
redactParams: config.database.redactParams
|
|
@@ -3225,7 +2764,7 @@ var SitePongClient = class {
|
|
|
3225
2764
|
if (config.profiling?.enabled) {
|
|
3226
2765
|
this.profiler = new Profiler({
|
|
3227
2766
|
apiKey: config.apiKey,
|
|
3228
|
-
endpoint: config.endpoint ||
|
|
2767
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3229
2768
|
debug: config.debug,
|
|
3230
2769
|
sampleRate: config.profiling.sampleRate,
|
|
3231
2770
|
maxDuration: config.profiling.maxDuration,
|
|
@@ -3236,7 +2775,7 @@ var SitePongClient = class {
|
|
|
3236
2775
|
const storage = config._storageAdapter || getStorageAdapter();
|
|
3237
2776
|
this.remoteConfigManager = new RemoteConfigManager({
|
|
3238
2777
|
apiKey: config.apiKey,
|
|
3239
|
-
endpoint: config.endpoint ||
|
|
2778
|
+
endpoint: config.endpoint || DEFAULT_ENDPOINT3,
|
|
3240
2779
|
storage,
|
|
3241
2780
|
debug: config.debug
|
|
3242
2781
|
});
|
|
@@ -3341,6 +2880,7 @@ var SitePongClient = class {
|
|
|
3341
2880
|
this.log("identify hook failed:", err);
|
|
3342
2881
|
}
|
|
3343
2882
|
}
|
|
2883
|
+
this.watchtowerCapture?.setDistinctId(userId);
|
|
3344
2884
|
if (!this.analyticsManager) {
|
|
3345
2885
|
this.log("Analytics not enabled. Set analytics.enabled: true in init()");
|
|
3346
2886
|
return;
|
|
@@ -3408,13 +2948,56 @@ var SitePongClient = class {
|
|
|
3408
2948
|
getReplaySessionId() {
|
|
3409
2949
|
return this.replayManager?.getSessionId() ?? null;
|
|
3410
2950
|
}
|
|
3411
|
-
// ---
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
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 "";
|
|
2999
|
+
}
|
|
3000
|
+
return this.performanceManager.startTransaction(name, data);
|
|
3418
3001
|
}
|
|
3419
3002
|
endTransaction(id, status = "ok") {
|
|
3420
3003
|
if (!this.performanceManager) return;
|
|
@@ -3559,6 +3142,7 @@ var SitePongClient = class {
|
|
|
3559
3142
|
if (this.replayManager) {
|
|
3560
3143
|
this.replayManager.setUser(null);
|
|
3561
3144
|
}
|
|
3145
|
+
this.watchtowerCapture?.setDistinctId(null);
|
|
3562
3146
|
}
|
|
3563
3147
|
addBreadcrumb(breadcrumb) {
|
|
3564
3148
|
this.log("Breadcrumb added:", breadcrumb);
|
|
@@ -3628,15 +3212,15 @@ var SitePongClient = class {
|
|
|
3628
3212
|
this.log(`Flushed ${errors.length} errors`);
|
|
3629
3213
|
} catch (err) {
|
|
3630
3214
|
this.flushFailures++;
|
|
3631
|
-
if (this.flushFailures >=
|
|
3215
|
+
if (this.flushFailures >= MAX_FLUSH_FAILURES2) {
|
|
3632
3216
|
this.disabled = true;
|
|
3633
3217
|
console.warn(
|
|
3634
|
-
`[SitePong] SDK disabled after ${
|
|
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.`
|
|
3635
3219
|
);
|
|
3636
3220
|
return;
|
|
3637
3221
|
}
|
|
3638
3222
|
this.errorQueue.unshift(...errors);
|
|
3639
|
-
this.log(`Failed to flush errors (attempt ${this.flushFailures}/${
|
|
3223
|
+
this.log(`Failed to flush errors (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES2}):`, err);
|
|
3640
3224
|
}
|
|
3641
3225
|
}
|
|
3642
3226
|
formatError(error, additionalContext) {
|
|
@@ -3783,6 +3367,8 @@ var getDeviceSignals = sitepong.getDeviceSignals.bind(sitepong);
|
|
|
3783
3367
|
var getFraudCheck = sitepong.getFraudCheck.bind(sitepong);
|
|
3784
3368
|
var startReplay = sitepong.startReplay.bind(sitepong);
|
|
3785
3369
|
var stopReplay = sitepong.stopReplay.bind(sitepong);
|
|
3370
|
+
var startWatchtowerCapture = sitepong.startWatchtowerCapture.bind(sitepong);
|
|
3371
|
+
var stopWatchtowerCapture = sitepong.stopWatchtowerCapture.bind(sitepong);
|
|
3786
3372
|
var isReplayRecording = sitepong.isReplayRecording.bind(sitepong);
|
|
3787
3373
|
var getReplaySessionId = sitepong.getReplaySessionId.bind(sitepong);
|
|
3788
3374
|
var startTransaction = sitepong.startTransaction.bind(sitepong);
|
|
@@ -4499,10 +4085,10 @@ var ConsoleRecorder = class {
|
|
|
4499
4085
|
};
|
|
4500
4086
|
|
|
4501
4087
|
// src/replay/manager.ts
|
|
4502
|
-
var
|
|
4088
|
+
var DEFAULT_ENDPOINT4 = "https://ingest.sitepong.com";
|
|
4503
4089
|
var DEFAULT_BUFFER_DURATION = 2e4;
|
|
4504
4090
|
var DEFAULT_MAX_SESSION_DURATION = 60 * 60 * 1e3;
|
|
4505
|
-
var
|
|
4091
|
+
var MAX_FLUSH_FAILURES3 = 3;
|
|
4506
4092
|
var TRIM_INTERVAL = 5e3;
|
|
4507
4093
|
var ReplayManager = class {
|
|
4508
4094
|
constructor(config) {
|
|
@@ -4529,7 +4115,7 @@ var ReplayManager = class {
|
|
|
4529
4115
|
/** Most recent full snapshot, kept so we can prepend it to error replays */
|
|
4530
4116
|
this.latestSnapshot = null;
|
|
4531
4117
|
this.config = {
|
|
4532
|
-
endpoint:
|
|
4118
|
+
endpoint: DEFAULT_ENDPOINT4,
|
|
4533
4119
|
enabled: true,
|
|
4534
4120
|
maskInputs: true,
|
|
4535
4121
|
maxSessionDuration: DEFAULT_MAX_SESSION_DURATION,
|
|
@@ -4665,14 +4251,14 @@ var ReplayManager = class {
|
|
|
4665
4251
|
this.log(`Flushed ${eventsToSend.length} replay events (error-triggered)`);
|
|
4666
4252
|
} catch (err) {
|
|
4667
4253
|
this.flushFailures++;
|
|
4668
|
-
if (this.flushFailures >=
|
|
4254
|
+
if (this.flushFailures >= MAX_FLUSH_FAILURES3) {
|
|
4669
4255
|
this.disabled = true;
|
|
4670
4256
|
console.warn(
|
|
4671
|
-
`[SitePong Replay] Disabled after ${
|
|
4257
|
+
`[SitePong Replay] Disabled after ${MAX_FLUSH_FAILURES3} consecutive failures. Check that ${endpoint} is accessible and has proper CORS headers.`
|
|
4672
4258
|
);
|
|
4673
4259
|
return;
|
|
4674
4260
|
}
|
|
4675
|
-
this.log(`Failed to flush replay events (attempt ${this.flushFailures}/${
|
|
4261
|
+
this.log(`Failed to flush replay events (attempt ${this.flushFailures}/${MAX_FLUSH_FAILURES3}):`, err);
|
|
4676
4262
|
}
|
|
4677
4263
|
}
|
|
4678
4264
|
/**
|
|
@@ -5967,542 +5553,1757 @@ var BehaviorAnalyzer = class {
|
|
|
5967
5553
|
const score = this.calculateHumanScore(mouse, keyboard, scroll, clicks);
|
|
5968
5554
|
return { mouse, keyboard, scroll, clicks, score };
|
|
5969
5555
|
}
|
|
5970
|
-
reset() {
|
|
5971
|
-
this.mousePoints = [];
|
|
5972
|
-
this.keyDownTimes.clear();
|
|
5973
|
-
this.keyIntervals = [];
|
|
5974
|
-
this.keyHoldTimes = [];
|
|
5975
|
-
this.lastKeyTime = 0;
|
|
5976
|
-
this.scrollEvents = [];
|
|
5977
|
-
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;
|
|
5978
6440
|
}
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
maxSpeed: 0,
|
|
5985
|
-
averageAcceleration: 0,
|
|
5986
|
-
straightLineRatio: 0,
|
|
5987
|
-
jitter: 0,
|
|
5988
|
-
hasMovement: false
|
|
5989
|
-
};
|
|
6441
|
+
endTransaction(id, status = "ok") {
|
|
6442
|
+
const transaction = this.activeTransactions.get(id);
|
|
6443
|
+
if (!transaction) {
|
|
6444
|
+
this.log("Transaction not found:", id);
|
|
6445
|
+
return;
|
|
5990
6446
|
}
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
const dt = curr.t - prev.t || 1;
|
|
6000
|
-
const dx = curr.x - prev.x;
|
|
6001
|
-
const dy = curr.y - prev.y;
|
|
6002
|
-
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
6003
|
-
const speed = distance / dt;
|
|
6004
|
-
speeds.push(speed);
|
|
6005
|
-
if (distance < 3 && distance > 0) {
|
|
6006
|
-
jitterSum++;
|
|
6007
|
-
}
|
|
6008
|
-
if (i >= 2) {
|
|
6009
|
-
const pp = this.mousePoints[i - 2];
|
|
6010
|
-
const expectedX = pp.x + (curr.x - pp.x) * ((prev.t - pp.t) / (curr.t - pp.t));
|
|
6011
|
-
const expectedY = pp.y + (curr.y - pp.y) * ((prev.t - pp.t) / (curr.t - pp.t));
|
|
6012
|
-
const deviation = Math.sqrt((prev.x - expectedX) ** 2 + (prev.y - expectedY) ** 2);
|
|
6013
|
-
totalSegments++;
|
|
6014
|
-
if (deviation < 2) straightSegments++;
|
|
6015
|
-
}
|
|
6016
|
-
if (i >= 2) {
|
|
6017
|
-
const prevSpeed = speeds[speeds.length - 2] || 0;
|
|
6018
|
-
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;
|
|
6019
6455
|
}
|
|
6020
6456
|
}
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
};
|
|
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`);
|
|
6033
6468
|
}
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
intervalVariance: 0,
|
|
6040
|
-
holdTimeAverage: 0,
|
|
6041
|
-
holdTimeVariance: 0,
|
|
6042
|
-
hasKeystrokes: false
|
|
6043
|
-
};
|
|
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 "";
|
|
6044
6474
|
}
|
|
6045
|
-
const
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
averageInterval: Math.round(avgInterval),
|
|
6052
|
-
intervalVariance: Math.round(intervalVar),
|
|
6053
|
-
holdTimeAverage: Math.round(avgHold),
|
|
6054
|
-
holdTimeVariance: Math.round(holdVar),
|
|
6055
|
-
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
|
|
6056
6481
|
};
|
|
6482
|
+
transaction.spans.push(span);
|
|
6483
|
+
return span.id;
|
|
6057
6484
|
}
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
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;
|
|
6061
6502
|
}
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
const dy = Math.abs(this.scrollEvents[i].deltaY - this.scrollEvents[i - 1].deltaY);
|
|
6066
|
-
speeds.push(dy / dt);
|
|
6503
|
+
if (this.pageLoadTimer) {
|
|
6504
|
+
clearInterval(this.pageLoadTimer);
|
|
6505
|
+
this.pageLoadTimer = null;
|
|
6067
6506
|
}
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
return {
|
|
6072
|
-
totalScrolls: this.scrollEvents.length,
|
|
6073
|
-
averageSpeed: Math.round(avgSpeed * 100) / 100,
|
|
6074
|
-
smoothness: Math.round(smoothness * 100) / 100,
|
|
6075
|
-
hasScroll: true
|
|
6076
|
-
};
|
|
6077
|
-
}
|
|
6078
|
-
analyzeClicks() {
|
|
6079
|
-
if (this.clickTimes.length < 2) {
|
|
6080
|
-
return {
|
|
6081
|
-
totalClicks: this.clickTimes.length,
|
|
6082
|
-
averageInterval: 0,
|
|
6083
|
-
intervalVariance: 0,
|
|
6084
|
-
doubleClickCount: 0,
|
|
6085
|
-
hasClicks: this.clickTimes.length > 0
|
|
6086
|
-
};
|
|
6507
|
+
if (this.pageRenderTimer) {
|
|
6508
|
+
clearInterval(this.pageRenderTimer);
|
|
6509
|
+
this.pageRenderTimer = null;
|
|
6087
6510
|
}
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
const interval = this.clickTimes[i] - this.clickTimes[i - 1];
|
|
6092
|
-
intervals.push(interval);
|
|
6093
|
-
if (interval < 500) doubleClicks++;
|
|
6511
|
+
if (this.resourceObserver) {
|
|
6512
|
+
this.resourceObserver.disconnect();
|
|
6513
|
+
this.resourceObserver = null;
|
|
6094
6514
|
}
|
|
6095
|
-
|
|
6096
|
-
const intervalVar = this.variance(intervals);
|
|
6097
|
-
return {
|
|
6098
|
-
totalClicks: this.clickTimes.length,
|
|
6099
|
-
averageInterval: Math.round(avgInterval),
|
|
6100
|
-
intervalVariance: Math.round(intervalVar),
|
|
6101
|
-
doubleClickCount: doubleClicks,
|
|
6102
|
-
hasClicks: true
|
|
6103
|
-
};
|
|
6515
|
+
this.flush();
|
|
6104
6516
|
}
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
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
|
+
});
|
|
6119
6541
|
}
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
if (cv > 0.3) keyScore += 0.15;
|
|
6127
|
-
if (keyboard.holdTimeVariance > 500) keyScore += 0.1;
|
|
6128
|
-
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;
|
|
6129
6548
|
}
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
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;
|
|
6135
6573
|
}
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
if (clicks.hasClicks) {
|
|
6139
|
-
factors++;
|
|
6140
|
-
let clickScore = 0.5;
|
|
6141
|
-
if (clicks.intervalVariance > 5e4) clickScore += 0.15;
|
|
6142
|
-
else if (clicks.intervalVariance < 100) clickScore -= 0.2;
|
|
6143
|
-
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);
|
|
6144
6576
|
}
|
|
6145
|
-
if (factors === 0) return 0.3;
|
|
6146
|
-
return Math.max(0, Math.min(1, score));
|
|
6147
6577
|
}
|
|
6148
|
-
|
|
6149
|
-
if (
|
|
6150
|
-
|
|
6151
|
-
|
|
6578
|
+
log(...args) {
|
|
6579
|
+
if (this.config.debug) {
|
|
6580
|
+
console.log("[SitePong Performance]", ...args);
|
|
6581
|
+
}
|
|
6152
6582
|
}
|
|
6153
6583
|
};
|
|
6154
6584
|
|
|
6155
|
-
//
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
let confidence = 0;
|
|
6167
|
-
let vpnDetected = false;
|
|
6168
|
-
let proxyDetected = false;
|
|
6169
|
-
let torDetected = false;
|
|
6170
|
-
signals.torBrowser = detectTorBrowser();
|
|
6171
|
-
if (signals.torBrowser) {
|
|
6172
|
-
torDetected = true;
|
|
6173
|
-
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]);
|
|
6174
6596
|
}
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
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;
|
|
6180
6647
|
}
|
|
6181
|
-
|
|
6182
|
-
signals.webrtcLeak = null;
|
|
6648
|
+
bytes[row] = byte;
|
|
6183
6649
|
}
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6650
|
+
let hex = "";
|
|
6651
|
+
for (let i = 0; i < 8; i++) {
|
|
6652
|
+
hex += bytes[i].toString(16).padStart(2, "0");
|
|
6187
6653
|
}
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6654
|
+
return hex;
|
|
6655
|
+
}
|
|
6656
|
+
function popcount8(n) {
|
|
6657
|
+
let count = 0;
|
|
6658
|
+
while (n) {
|
|
6659
|
+
n &= n - 1;
|
|
6660
|
+
count++;
|
|
6192
6661
|
}
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
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}`);
|
|
6201
6677
|
}
|
|
6678
|
+
dist += popcount8(byteA ^ byteB);
|
|
6202
6679
|
}
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
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}"`);
|
|
6207
6690
|
}
|
|
6208
6691
|
return {
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
torDetected,
|
|
6212
|
-
confidence,
|
|
6213
|
-
signals
|
|
6692
|
+
screenName: fp.slice(0, idx),
|
|
6693
|
+
pHashHex: fp.slice(idx + 1)
|
|
6214
6694
|
};
|
|
6215
6695
|
}
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
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
|
+
});
|
|
6227
6718
|
}
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6719
|
+
var SessionState = class {
|
|
6720
|
+
constructor() {
|
|
6721
|
+
this.memory = {};
|
|
6231
6722
|
}
|
|
6232
|
-
|
|
6233
|
-
const timeout = setTimeout(() => resolve(false), 3e3);
|
|
6234
|
-
const localIPs = /* @__PURE__ */ new Set();
|
|
6723
|
+
get(key) {
|
|
6235
6724
|
try {
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
}
|
|
6239
|
-
pc.createDataChannel("");
|
|
6240
|
-
pc.onicecandidate = (event) => {
|
|
6241
|
-
if (!event.candidate) {
|
|
6242
|
-
clearTimeout(timeout);
|
|
6243
|
-
pc.close();
|
|
6244
|
-
resolve(localIPs.size > 1);
|
|
6245
|
-
return;
|
|
6246
|
-
}
|
|
6247
|
-
const candidate = event.candidate.candidate;
|
|
6248
|
-
const ipMatch = candidate.match(/(\d{1,3}\.){3}\d{1,3}/);
|
|
6249
|
-
if (ipMatch) {
|
|
6250
|
-
localIPs.add(ipMatch[0]);
|
|
6251
|
-
}
|
|
6252
|
-
};
|
|
6253
|
-
pc.createOffer().then((offer) => pc.setLocalDescription(offer)).catch(() => {
|
|
6254
|
-
clearTimeout(timeout);
|
|
6255
|
-
resolve(false);
|
|
6256
|
-
});
|
|
6725
|
+
if (typeof sessionStorage !== "undefined") {
|
|
6726
|
+
return sessionStorage.getItem(key);
|
|
6727
|
+
}
|
|
6257
6728
|
} catch {
|
|
6258
|
-
clearTimeout(timeout);
|
|
6259
|
-
resolve(false);
|
|
6260
6729
|
}
|
|
6261
|
-
|
|
6262
|
-
}
|
|
6263
|
-
function getConnectionType2() {
|
|
6264
|
-
if (typeof navigator === "undefined") return null;
|
|
6265
|
-
const nav = navigator;
|
|
6266
|
-
if (nav.connection) {
|
|
6267
|
-
return nav.connection.type || nav.connection.effectiveType || null;
|
|
6730
|
+
return this.memory[key] ?? null;
|
|
6268
6731
|
}
|
|
6269
|
-
|
|
6270
|
-
}
|
|
6271
|
-
function detectVPNExtensions() {
|
|
6272
|
-
if (typeof document === "undefined") return false;
|
|
6273
|
-
const body = document.body;
|
|
6274
|
-
if (!body) return false;
|
|
6275
|
-
const vpnIndicators = [
|
|
6276
|
-
"[data-windscribe]",
|
|
6277
|
-
"[data-nord]",
|
|
6278
|
-
".surfshark-extension",
|
|
6279
|
-
"#vpn-indicator",
|
|
6280
|
-
"[data-expressvpn]"
|
|
6281
|
-
];
|
|
6282
|
-
for (const selector of vpnIndicators) {
|
|
6732
|
+
set(key, value) {
|
|
6283
6733
|
try {
|
|
6284
|
-
if (
|
|
6734
|
+
if (typeof sessionStorage !== "undefined") {
|
|
6735
|
+
sessionStorage.setItem(key, value);
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6285
6738
|
} catch {
|
|
6286
6739
|
}
|
|
6740
|
+
this.memory[key] = value;
|
|
6287
6741
|
}
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
}
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
var
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
this.
|
|
6330
|
-
|
|
6331
|
-
|
|
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();
|
|
6332
6849
|
}
|
|
6333
|
-
this.loadFromStorage();
|
|
6334
6850
|
}
|
|
6335
|
-
|
|
6336
|
-
if (!this.
|
|
6337
|
-
|
|
6851
|
+
stop() {
|
|
6852
|
+
if (!this.active) return;
|
|
6853
|
+
this.active = false;
|
|
6854
|
+
if (this.detachAutocapture) {
|
|
6855
|
+
this.detachAutocapture();
|
|
6856
|
+
this.detachAutocapture = null;
|
|
6338
6857
|
}
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
this.
|
|
6342
|
-
return this.cachedVisitor.result;
|
|
6858
|
+
if (this.ownAutocapture) {
|
|
6859
|
+
this.ownAutocapture.stop();
|
|
6860
|
+
this.ownAutocapture = null;
|
|
6343
6861
|
}
|
|
6344
|
-
if (this.
|
|
6345
|
-
|
|
6862
|
+
if (this.flushTimer) {
|
|
6863
|
+
clearInterval(this.flushTimer);
|
|
6864
|
+
this.flushTimer = null;
|
|
6346
6865
|
}
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
this.
|
|
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
|
+
}
|
|
6353
6883
|
}
|
|
6884
|
+
void this.flush();
|
|
6885
|
+
this.log("Stopped");
|
|
6354
6886
|
}
|
|
6355
|
-
|
|
6356
|
-
return
|
|
6887
|
+
isActive() {
|
|
6888
|
+
return this.active;
|
|
6357
6889
|
}
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
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();
|
|
6372
6950
|
};
|
|
6373
|
-
|
|
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) {
|
|
6374
6987
|
return {
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
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;
|
|
6383
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
|
|
6384
7045
|
};
|
|
6385
7046
|
}
|
|
6386
|
-
|
|
6387
|
-
|
|
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
|
+
});
|
|
6388
7060
|
}
|
|
6389
|
-
async
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
const endpoint = this.config.visitorEndpoint || `${this.config.endpoint}/api/visitors`;
|
|
6393
|
-
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;
|
|
6394
7064
|
try {
|
|
6395
|
-
const
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
6400
|
-
|
|
6401
|
-
credentials: "include",
|
|
6402
|
-
body: JSON.stringify({
|
|
6403
|
-
signals,
|
|
6404
|
-
stableFingerprint,
|
|
6405
|
-
previousVisitorId
|
|
6406
|
-
})
|
|
6407
|
-
});
|
|
6408
|
-
if (!response.ok) {
|
|
6409
|
-
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;
|
|
6410
7071
|
}
|
|
6411
|
-
const
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
cachedAt: Date.now()
|
|
6416
|
-
};
|
|
6417
|
-
this.saveToStorage();
|
|
6418
|
-
this.setCookieVisitorId(result.visitorId);
|
|
6419
|
-
this.log("Visitor ID fetched:", result.visitorId);
|
|
6420
|
-
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);
|
|
6421
7076
|
} catch (err) {
|
|
6422
|
-
this.log("
|
|
6423
|
-
|
|
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;
|
|
6424
7083
|
}
|
|
6425
7084
|
}
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
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
|
+
}
|
|
6433
7091
|
}
|
|
6434
|
-
if (
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
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);
|
|
6441
7111
|
}
|
|
6442
|
-
if (!signals.cookieEnabled) score += 0.1;
|
|
6443
|
-
if (smart?.doNotTrack) score += 0.05;
|
|
6444
|
-
if (smart?.deviceVisitCount === 1) score += 0.05;
|
|
6445
|
-
return Math.round(Math.min(score, 1) * 100) / 100;
|
|
6446
7112
|
}
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
7113
|
+
// -------------------------------------------------------------------------
|
|
7114
|
+
// Helpers
|
|
7115
|
+
// -------------------------------------------------------------------------
|
|
7116
|
+
combinedBlockSelector() {
|
|
7117
|
+
return this.config.blockSelector ? `${DEFAULT_BLOCK_SELECTOR}, ${this.config.blockSelector}` : DEFAULT_BLOCK_SELECTOR;
|
|
6450
7118
|
}
|
|
6451
|
-
|
|
6452
|
-
if (typeof window === "undefined" ||
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
this.cachedVisitor = parsed;
|
|
6459
|
-
this.log("Loaded cached visitor from storage");
|
|
6460
|
-
} else {
|
|
6461
|
-
localStorage.removeItem(CACHE_KEY);
|
|
6462
|
-
}
|
|
6463
|
-
} 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);
|
|
6464
7126
|
}
|
|
6465
7127
|
}
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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>`;
|
|
6472
7192
|
}
|
|
7193
|
+
if (tagName === "link") return "";
|
|
6473
7194
|
}
|
|
6474
|
-
|
|
6475
|
-
|
|
7195
|
+
const computedStyle = attrs["_computedStyle"];
|
|
7196
|
+
if (typeof computedStyle === "string" && computedStyle) {
|
|
7197
|
+
attrs["style"] = attrs["style"] ? `${attrs["style"]};${computedStyle}` : computedStyle;
|
|
6476
7198
|
}
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
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);
|
|
6484
7227
|
}
|
|
6485
7228
|
}
|
|
6486
|
-
|
|
6487
|
-
|
|
7229
|
+
return `<${tagName}${attrString}>${childHtml}</${tagName}>`;
|
|
7230
|
+
}
|
|
7231
|
+
var DomRasterizer = class {
|
|
7232
|
+
constructor(options) {
|
|
7233
|
+
this.options = options;
|
|
7234
|
+
}
|
|
7235
|
+
async rasterize() {
|
|
6488
7236
|
try {
|
|
6489
|
-
document
|
|
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 };
|
|
6490
7272
|
} catch {
|
|
6491
|
-
|
|
6492
|
-
}
|
|
6493
|
-
log(...args) {
|
|
6494
|
-
if (this.config.debug) {
|
|
6495
|
-
console.log("[SitePong Fingerprint]", ...args);
|
|
7273
|
+
return null;
|
|
6496
7274
|
}
|
|
6497
7275
|
}
|
|
6498
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
|
+
}
|
|
6499
7299
|
|
|
6500
7300
|
// src/entries/web.ts
|
|
6501
7301
|
setEnvironment(createWebEnvironment());
|
|
6502
7302
|
registerWebManagerFactories({
|
|
6503
7303
|
createReplay: (cfg) => new ReplayManager(cfg),
|
|
6504
7304
|
createFingerprint: (cfg) => new FingerprintManager(cfg),
|
|
6505
|
-
createPerformance: (cfg) => new PerformanceManager(cfg)
|
|
7305
|
+
createPerformance: (cfg) => new PerformanceManager(cfg),
|
|
7306
|
+
createWatchtowerCapture: (cfg) => new WatchtowerWebCapture(cfg)
|
|
6506
7307
|
});
|
|
6507
7308
|
|
|
6508
7309
|
exports.DEFAULT_SUPERLINK_ENDPOINT = DEFAULT_SUPERLINK_ENDPOINT;
|
|
@@ -6585,7 +7386,9 @@ exports.startProfileSpan = startProfileSpan;
|
|
|
6585
7386
|
exports.startReplay = startReplay;
|
|
6586
7387
|
exports.startSpan = startSpan;
|
|
6587
7388
|
exports.startTransaction = startTransaction;
|
|
7389
|
+
exports.startWatchtowerCapture = startWatchtowerCapture;
|
|
6588
7390
|
exports.stopReplay = stopReplay;
|
|
7391
|
+
exports.stopWatchtowerCapture = stopWatchtowerCapture;
|
|
6589
7392
|
exports.superlinkClient = superlinkClient;
|
|
6590
7393
|
exports.superlinkIdentify = identify;
|
|
6591
7394
|
exports.track = track;
|