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