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