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