sitepong 0.1.13 → 0.2.1

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.
@@ -1,3 +1,5 @@
1
+ import { onCLS, onINP, onLCP, onTTFB, onFCP } from 'web-vitals';
2
+
1
3
  // src/platforms/web/environment.ts
2
4
  function makeLocalStorageAdapter() {
3
5
  try {
@@ -620,6 +622,60 @@ function clearSession() {
620
622
  memorySessionTs = null;
621
623
  }
622
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
+
623
679
  // src/analytics/autocapture.ts
624
680
  var DEFAULT_BLOCK_SELECTORS = [
625
681
  "[data-sp-no-capture]",
@@ -1026,7 +1082,9 @@ var AnalyticsManager = class {
1026
1082
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1027
1083
  url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
1028
1084
  referrer: typeof document !== "undefined" && typeof document.referrer === "string" ? document.referrer : void 0,
1029
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
1085
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1086
+ utm: getSessionUtm() || void 0,
1087
+ appVersion: this.config.appVersion || void 0
1030
1088
  };
1031
1089
  }
1032
1090
  enqueue(event) {
@@ -1724,7 +1782,7 @@ var DEFAULT_REMOTE_CONFIG = {
1724
1782
  };
1725
1783
 
1726
1784
  // src/remote-config/manager.ts
1727
- var STORAGE_KEY2 = "sitepong_remote_config";
1785
+ var STORAGE_KEY3 = "sitepong_remote_config";
1728
1786
  var STORAGE_TS_KEY = "sitepong_remote_config_ts";
1729
1787
  var RemoteConfigManager = class {
1730
1788
  constructor(options) {
@@ -1767,7 +1825,7 @@ var RemoteConfigManager = class {
1767
1825
  const storage = this.options.storage;
1768
1826
  if (!storage) return;
1769
1827
  try {
1770
- const cached = await storage.getItem(STORAGE_KEY2);
1828
+ const cached = await storage.getItem(STORAGE_KEY3);
1771
1829
  const cachedTs = await storage.getItem(STORAGE_TS_KEY);
1772
1830
  if (cached && cachedTs) {
1773
1831
  const age = Date.now() - parseInt(cachedTs, 10);
@@ -1828,7 +1886,7 @@ var RemoteConfigManager = class {
1828
1886
  const storage = this.options.storage;
1829
1887
  if (!storage) return;
1830
1888
  try {
1831
- await storage.setItem(STORAGE_KEY2, JSON.stringify(this.config));
1889
+ await storage.setItem(STORAGE_KEY3, JSON.stringify(this.config));
1832
1890
  await storage.setItem(STORAGE_TS_KEY, String(Date.now()));
1833
1891
  } catch {
1834
1892
  this.log("Failed to cache remote config");
@@ -1862,11 +1920,70 @@ var RemoteConfigManager = class {
1862
1920
  }
1863
1921
  }
1864
1922
  };
1865
-
1866
- // src/performance/manager.ts
1867
1923
  var DEFAULT_ENDPOINT3 = "https://ingest.sitepong.com";
1868
1924
  var DEFAULT_FLUSH_INTERVAL2 = 1e4;
1869
1925
  var MAX_FLUSH_FAILURES2 = 3;
1926
+ var MAX_RESOURCES_PER_FLUSH = 200;
1927
+ var PAGE_TIMING_POLL_INTERVAL = 200;
1928
+ var PAGE_TIMING_TIMEOUT = 3e4;
1929
+ function getPaintBlocks(resources) {
1930
+ const paintBlocks = [];
1931
+ const elements = document.getElementsByTagName("*");
1932
+ const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i;
1933
+ for (let i = 0; i < elements.length; i++) {
1934
+ const element = elements[i];
1935
+ let src = "";
1936
+ if (element.tagName === "IMG") {
1937
+ src = element.currentSrc || element.src;
1938
+ }
1939
+ if (!src) {
1940
+ const backgroundImage = getComputedStyle(element).getPropertyValue("background-image");
1941
+ if (backgroundImage) {
1942
+ const matches = styleURL.exec(backgroundImage);
1943
+ if (matches !== null) {
1944
+ src = matches[1];
1945
+ if (src.startsWith('"') || src.startsWith("'")) {
1946
+ src = src.substr(1, src.length - 2);
1947
+ }
1948
+ }
1949
+ }
1950
+ }
1951
+ if (!src) continue;
1952
+ const time = src.substr(0, 10) === "data:image" ? 0 : resources[src];
1953
+ if (time === void 0) continue;
1954
+ const rect = element.getBoundingClientRect();
1955
+ const top = Math.max(rect.top, 0);
1956
+ const left = Math.max(rect.left, 0);
1957
+ const bottom = Math.min(
1958
+ rect.bottom,
1959
+ window.innerHeight || document.documentElement && document.documentElement.clientHeight || 0
1960
+ );
1961
+ const right = Math.min(
1962
+ rect.right,
1963
+ window.innerWidth || document.documentElement && document.documentElement.clientWidth || 0
1964
+ );
1965
+ if (bottom <= top || right <= left) continue;
1966
+ const area = (bottom - top) * (right - left);
1967
+ paintBlocks.push({ time, area });
1968
+ }
1969
+ return paintBlocks;
1970
+ }
1971
+ function calculateSpeedIndex(firstContentfulPaint, paintBlocks) {
1972
+ let a = Math.max(
1973
+ document.documentElement && document.documentElement.clientWidth || 0,
1974
+ window.innerWidth || 0
1975
+ ) * Math.max(
1976
+ document.documentElement && document.documentElement.clientHeight || 0,
1977
+ window.innerHeight || 0
1978
+ ) / 10;
1979
+ let s = a * firstContentfulPaint;
1980
+ for (let i = 0; i < paintBlocks.length; i++) {
1981
+ const { time, area } = paintBlocks[i];
1982
+ a += area;
1983
+ s += area * (time > firstContentfulPaint ? time : firstContentfulPaint);
1984
+ }
1985
+ return a === 0 ? 0 : s / a;
1986
+ }
1870
1987
  var PerformanceManager = class {
1871
1988
  constructor(config) {
1872
1989
  this.metrics = [];
@@ -1875,12 +1992,22 @@ var PerformanceManager = class {
1875
1992
  this.disabled = false;
1876
1993
  this.vitals = {};
1877
1994
  this.activeTransactions = /* @__PURE__ */ new Map();
1995
+ // Resource timing state for Speed Index
1996
+ this.resourceObserver = null;
1997
+ this.resourceTimeMap = {};
1998
+ this.resourceCount = 0;
1999
+ // Page timing polling timers
2000
+ this.pageLoadTimer = null;
2001
+ this.pageRenderTimer = null;
1878
2002
  this.config = {
1879
2003
  endpoint: DEFAULT_ENDPOINT3,
1880
2004
  enabled: true,
1881
2005
  webVitals: true,
1882
2006
  navigationTiming: true,
1883
- resourceTiming: false,
2007
+ resourceTiming: true,
2008
+ capturePageLoadTimings: true,
2009
+ capturePageRenderTimings: true,
2010
+ excludedResourceUrls: [],
1884
2011
  flushInterval: DEFAULT_FLUSH_INTERVAL2,
1885
2012
  sampleRate: 1,
1886
2013
  debug: false,
@@ -1891,18 +2018,250 @@ var PerformanceManager = class {
1891
2018
  init() {
1892
2019
  if (!this.config.enabled || !this.sampled) return;
1893
2020
  if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
1894
- if (this.config.webVitals) {
1895
- this.observeWebVitals();
2021
+ if (this.config.webVitals !== false) {
2022
+ this.initWebVitals();
2023
+ }
2024
+ if (this.config.resourceTiming !== false) {
2025
+ this.initResourceTiming();
2026
+ }
2027
+ if (this.config.capturePageLoadTimings !== false) {
2028
+ this.initPageLoadTiming();
1896
2029
  }
1897
- if (this.config.navigationTiming) {
1898
- this.collectNavigationTiming();
2030
+ if (this.config.capturePageRenderTimings !== false) {
2031
+ this.initPageRenderTiming();
1899
2032
  }
1900
2033
  this.startFlushTimer();
1901
2034
  this.log("Performance monitoring initialized");
1902
2035
  }
1903
- /**
1904
- * Start a custom transaction for measuring operations
1905
- */
2036
+ // ---- Web Vitals (via web-vitals library) ----
2037
+ initWebVitals() {
2038
+ onCLS((m) => {
2039
+ this.vitals.cls = m.value;
2040
+ this.reportVital("CLS", m.value, "score");
2041
+ });
2042
+ onINP((m) => {
2043
+ this.vitals.inp = m.value;
2044
+ this.reportVital("INP", m.value);
2045
+ });
2046
+ onLCP((m) => {
2047
+ this.vitals.lcp = m.value;
2048
+ this.reportVital("LCP", m.value);
2049
+ });
2050
+ onTTFB((m) => {
2051
+ this.vitals.ttfb = m.value;
2052
+ this.reportVital("TTFB", m.value);
2053
+ });
2054
+ onFCP((m) => {
2055
+ this.vitals.fcp = m.value;
2056
+ this.reportVital("FCP", m.value);
2057
+ });
2058
+ }
2059
+ // ---- Resource Timing (adapted from OpenReplay timing.ts) ----
2060
+ initResourceTiming() {
2061
+ if (typeof PerformanceObserver === "undefined") return;
2062
+ try {
2063
+ this.resourceObserver = new PerformanceObserver((list) => {
2064
+ for (const entry of list.getEntries()) {
2065
+ this.processResourceEntry(entry);
2066
+ }
2067
+ });
2068
+ this.resourceObserver.observe({ type: "resource", buffered: true });
2069
+ } catch {
2070
+ this.log("PerformanceObserver for resource timing not supported");
2071
+ }
2072
+ }
2073
+ isServiceURL(url) {
2074
+ const endpoint = this.config.performanceEndpoint || this.config.endpoint;
2075
+ return url.startsWith(endpoint);
2076
+ }
2077
+ processResourceEntry(entry) {
2078
+ if (entry.duration < 0 || !entry.name.startsWith("http")) return;
2079
+ if (this.isServiceURL(entry.name)) return;
2080
+ if (this.resourceTimeMap !== null) {
2081
+ this.resourceTimeMap[entry.name] = entry.startTime + entry.duration;
2082
+ }
2083
+ for (const excluded of this.config.excludedResourceUrls) {
2084
+ if (entry.name.startsWith(excluded)) return;
2085
+ }
2086
+ if (this.resourceCount >= MAX_RESOURCES_PER_FLUSH) return;
2087
+ this.resourceCount++;
2088
+ let stalled = 0;
2089
+ if (entry.connectEnd && entry.connectEnd > entry.domainLookupEnd) {
2090
+ stalled = Math.max(0, entry.requestStart - entry.connectEnd);
2091
+ } else {
2092
+ stalled = Math.max(0, entry.requestStart - entry.domainLookupEnd);
2093
+ }
2094
+ const cached = entry.responseStatus && entry.responseStatus === 304 || entry.deliveryType && entry.deliveryType === "cache" || entry.transferSize === 0 && entry.decodedBodySize > 0;
2095
+ const responseStatus = entry.responseStatus || 0;
2096
+ const failed = responseStatus >= 400;
2097
+ const breakdown = {
2098
+ queueing: entry.requestStart - entry.fetchStart,
2099
+ dnsLookup: entry.domainLookupEnd - entry.domainLookupStart,
2100
+ initialConnection: entry.connectEnd - entry.connectStart,
2101
+ ssl: entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0,
2102
+ ttfb: entry.responseStart - entry.requestStart,
2103
+ contentDownload: entry.responseEnd - entry.responseStart,
2104
+ total: entry.duration ?? entry.responseEnd - entry.startTime,
2105
+ stalled,
2106
+ cached,
2107
+ failed,
2108
+ responseStatus,
2109
+ headerSize: entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
2110
+ encodedBodySize: entry.encodedBodySize || 0,
2111
+ decodedBodySize: failed ? -111 : entry.decodedBodySize || 0,
2112
+ transferSize: entry.transferSize,
2113
+ initiatorType: entry.initiatorType
2114
+ };
2115
+ const entryName = this.config.resourceNameSanitizer ? this.config.resourceNameSanitizer(entry.name) : entry.name;
2116
+ this.addMetric({
2117
+ type: "resource",
2118
+ name: entryName,
2119
+ value: entry.duration,
2120
+ unit: "ms",
2121
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2122
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
2123
+ data: breakdown
2124
+ });
2125
+ }
2126
+ // ---- Page Load Timing (adapted from OpenReplay timing.ts) ----
2127
+ initPageLoadTiming() {
2128
+ if (typeof window === "undefined" || !window.performance) return;
2129
+ let firstPaint = 0;
2130
+ let firstContentfulPaint = 0;
2131
+ let sent = false;
2132
+ const startTime = performance.now();
2133
+ this.pageLoadTimer = setInterval(() => {
2134
+ if (sent) {
2135
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
2136
+ return;
2137
+ }
2138
+ if (firstPaint === 0 || firstContentfulPaint === 0) {
2139
+ for (const entry of performance.getEntriesByType("paint")) {
2140
+ if (entry.name === "first-paint") firstPaint = entry.startTime;
2141
+ if (entry.name === "first-contentful-paint") firstContentfulPaint = entry.startTime;
2142
+ }
2143
+ }
2144
+ const nav = performance.getEntriesByType("navigation")[0];
2145
+ const timedOut = performance.now() - startTime > PAGE_TIMING_TIMEOUT;
2146
+ if (nav && nav.loadEventEnd > 0 || timedOut) {
2147
+ sent = true;
2148
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
2149
+ if (nav) {
2150
+ const navigationStart = nav.startTime;
2151
+ this.addMetric({
2152
+ type: "navigation",
2153
+ name: "page_load",
2154
+ value: nav.loadEventEnd - navigationStart,
2155
+ unit: "ms",
2156
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2157
+ url: window.location.href,
2158
+ data: {
2159
+ requestStart: nav.requestStart - navigationStart,
2160
+ responseStart: nav.responseStart - navigationStart,
2161
+ responseEnd: nav.responseEnd - navigationStart,
2162
+ domContentLoadedEventStart: nav.domContentLoadedEventEnd - navigationStart,
2163
+ domContentLoadedEventEnd: nav.domContentLoadedEventEnd - navigationStart,
2164
+ loadEventStart: nav.loadEventStart - navigationStart,
2165
+ loadEventEnd: nav.loadEventEnd - navigationStart,
2166
+ firstPaint,
2167
+ firstContentfulPaint,
2168
+ dns: nav.domainLookupEnd - nav.domainLookupStart,
2169
+ tcp: nav.connectEnd - nav.connectStart,
2170
+ ttfb: nav.responseStart - nav.requestStart,
2171
+ domComplete: nav.domComplete - navigationStart,
2172
+ transferSize: nav.transferSize,
2173
+ encodedBodySize: nav.encodedBodySize,
2174
+ decodedBodySize: nav.decodedBodySize
2175
+ }
2176
+ });
2177
+ }
2178
+ }
2179
+ }, PAGE_TIMING_POLL_INTERVAL);
2180
+ }
2181
+ // ---- Page Render Timing: Speed Index, Visually Complete, TTI (adapted from OpenReplay) ----
2182
+ initPageRenderTiming() {
2183
+ if (typeof window === "undefined" || !window.performance) return;
2184
+ let firstContentfulPaint = 0;
2185
+ let visuallyComplete = 0;
2186
+ let interactiveWindowStartTime = 0;
2187
+ let interactiveWindowTickTime = 0;
2188
+ let paintBlocks = null;
2189
+ let sent = false;
2190
+ const startTime = performance.now();
2191
+ this.pageRenderTimer = setInterval(() => {
2192
+ if (sent) {
2193
+ if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
2194
+ return;
2195
+ }
2196
+ const time = performance.now();
2197
+ if (firstContentfulPaint === 0) {
2198
+ for (const entry of performance.getEntriesByType("paint")) {
2199
+ if (entry.name === "first-contentful-paint") {
2200
+ firstContentfulPaint = entry.startTime;
2201
+ }
2202
+ }
2203
+ }
2204
+ if (this.resourceTimeMap !== null) {
2205
+ const times = Object.values(this.resourceTimeMap);
2206
+ if (times.length > 0) {
2207
+ visuallyComplete = Math.max(...times);
2208
+ }
2209
+ if (time - visuallyComplete > 1e3) {
2210
+ paintBlocks = getPaintBlocks(this.resourceTimeMap);
2211
+ this.resourceTimeMap = null;
2212
+ }
2213
+ }
2214
+ if (interactiveWindowTickTime !== null) {
2215
+ if (time - interactiveWindowTickTime > 50) {
2216
+ interactiveWindowStartTime = time;
2217
+ }
2218
+ interactiveWindowTickTime = time - interactiveWindowStartTime > 5e3 ? null : time;
2219
+ }
2220
+ const timedOut = time - startTime > PAGE_TIMING_TIMEOUT;
2221
+ if (paintBlocks !== null && interactiveWindowTickTime === null || timedOut) {
2222
+ sent = true;
2223
+ if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
2224
+ this.resourceTimeMap = null;
2225
+ const speedIndex = paintBlocks === null ? 0 : calculateSpeedIndex(firstContentfulPaint || 0, paintBlocks);
2226
+ const nav = performance.getEntriesByType("navigation")[0];
2227
+ const domContentLoadedEnd = nav ? nav.domContentLoadedEventEnd - nav.startTime : 0;
2228
+ const timeToInteractive = interactiveWindowTickTime === null ? Math.max(interactiveWindowStartTime, firstContentfulPaint, domContentLoadedEnd) : 0;
2229
+ const url = window.location.href;
2230
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2231
+ if (speedIndex > 0) {
2232
+ this.addMetric({
2233
+ type: "page_render",
2234
+ name: "speed_index",
2235
+ value: Math.round(speedIndex),
2236
+ unit: "ms",
2237
+ timestamp,
2238
+ url
2239
+ });
2240
+ }
2241
+ if (visuallyComplete > 0) {
2242
+ this.addMetric({
2243
+ type: "page_render",
2244
+ name: "visually_complete",
2245
+ value: Math.round(visuallyComplete),
2246
+ unit: "ms",
2247
+ timestamp,
2248
+ url
2249
+ });
2250
+ }
2251
+ if (timeToInteractive > 0) {
2252
+ this.addMetric({
2253
+ type: "page_render",
2254
+ name: "tti",
2255
+ value: Math.round(timeToInteractive),
2256
+ unit: "ms",
2257
+ timestamp,
2258
+ url
2259
+ });
2260
+ }
2261
+ }
2262
+ }, PAGE_TIMING_POLL_INTERVAL);
2263
+ }
2264
+ // ---- Transactions / Spans (unchanged) ----
1906
2265
  startTransaction(name, data) {
1907
2266
  const id = `txn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1908
2267
  const transaction = {
@@ -1917,9 +2276,6 @@ var PerformanceManager = class {
1917
2276
  this.log("Transaction started:", name, id);
1918
2277
  return id;
1919
2278
  }
1920
- /**
1921
- * End a transaction and report it
1922
- */
1923
2279
  endTransaction(id, status = "ok") {
1924
2280
  const transaction = this.activeTransactions.get(id);
1925
2281
  if (!transaction) {
@@ -1948,9 +2304,6 @@ var PerformanceManager = class {
1948
2304
  });
1949
2305
  this.log("Transaction ended:", transaction.name, `${transaction.duration.toFixed(1)}ms`);
1950
2306
  }
1951
- /**
1952
- * Start a span within a transaction
1953
- */
1954
2307
  startSpan(transactionId, name, data) {
1955
2308
  const transaction = this.activeTransactions.get(transactionId);
1956
2309
  if (!transaction) {
@@ -1967,9 +2320,6 @@ var PerformanceManager = class {
1967
2320
  transaction.spans.push(span);
1968
2321
  return span.id;
1969
2322
  }
1970
- /**
1971
- * End a span
1972
- */
1973
2323
  endSpan(transactionId, spanId, status = "ok") {
1974
2324
  const transaction = this.activeTransactions.get(transactionId);
1975
2325
  if (!transaction) return;
@@ -1979,9 +2329,7 @@ var PerformanceManager = class {
1979
2329
  span.duration = span.endTime - span.startTime;
1980
2330
  span.status = status;
1981
2331
  }
1982
- /**
1983
- * Get current Web Vitals
1984
- */
2332
+ // ---- Public API ----
1985
2333
  getVitals() {
1986
2334
  return { ...this.vitals };
1987
2335
  }
@@ -1990,116 +2338,21 @@ var PerformanceManager = class {
1990
2338
  clearInterval(this.flushTimer);
1991
2339
  this.flushTimer = null;
1992
2340
  }
1993
- this.flush();
1994
- }
1995
- observeWebVitals() {
1996
- if (typeof PerformanceObserver === "undefined") return;
1997
- try {
1998
- const lcpObserver = new PerformanceObserver((list) => {
1999
- const entries = list.getEntries();
2000
- const lastEntry = entries[entries.length - 1];
2001
- if (lastEntry) {
2002
- this.vitals.lcp = lastEntry.startTime;
2003
- this.reportVital("LCP", lastEntry.startTime);
2004
- }
2005
- });
2006
- lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
2007
- } catch (e) {
2008
- }
2009
- try {
2010
- const fidObserver = new PerformanceObserver((list) => {
2011
- const entry = list.getEntries()[0];
2012
- if (entry) {
2013
- const fid = entry.processingStart - entry.startTime;
2014
- this.vitals.fid = fid;
2015
- this.reportVital("FID", fid);
2016
- }
2017
- });
2018
- fidObserver.observe({ type: "first-input", buffered: true });
2019
- } catch (e) {
2020
- }
2021
- try {
2022
- let clsValue = 0;
2023
- const clsObserver = new PerformanceObserver((list) => {
2024
- for (const entry of list.getEntries()) {
2025
- const layoutShift = entry;
2026
- if (!layoutShift.hadRecentInput) {
2027
- clsValue += layoutShift.value;
2028
- }
2029
- }
2030
- this.vitals.cls = clsValue;
2031
- });
2032
- clsObserver.observe({ type: "layout-shift", buffered: true });
2033
- if (typeof document !== "undefined") {
2034
- document.addEventListener("visibilitychange", () => {
2035
- if (document.visibilityState === "hidden" && clsValue > 0) {
2036
- this.reportVital("CLS", clsValue, "score");
2037
- }
2038
- });
2039
- }
2040
- } catch (e) {
2041
- }
2042
- try {
2043
- const fcpObserver = new PerformanceObserver((list) => {
2044
- const entry = list.getEntries().find(
2045
- (e) => e.name === "first-contentful-paint"
2046
- );
2047
- if (entry) {
2048
- this.vitals.fcp = entry.startTime;
2049
- this.reportVital("FCP", entry.startTime);
2050
- }
2051
- });
2052
- fcpObserver.observe({ type: "paint", buffered: true });
2053
- } catch (e) {
2341
+ if (this.pageLoadTimer) {
2342
+ clearInterval(this.pageLoadTimer);
2343
+ this.pageLoadTimer = null;
2054
2344
  }
2055
- try {
2056
- let maxINP = 0;
2057
- const inpObserver = new PerformanceObserver((list) => {
2058
- for (const entry of list.getEntries()) {
2059
- const duration = entry.duration;
2060
- if (duration > maxINP) {
2061
- maxINP = duration;
2062
- this.vitals.inp = maxINP;
2063
- }
2064
- }
2065
- });
2066
- inpObserver.observe({ type: "event", buffered: true });
2067
- } catch (e) {
2345
+ if (this.pageRenderTimer) {
2346
+ clearInterval(this.pageRenderTimer);
2347
+ this.pageRenderTimer = null;
2068
2348
  }
2069
- }
2070
- collectNavigationTiming() {
2071
- if (typeof window === "undefined" || !window.performance || !window.location) return;
2072
- const collect = () => {
2073
- const nav = performance.getEntriesByType("navigation")[0];
2074
- if (!nav) return;
2075
- const ttfb = nav.responseStart - nav.requestStart;
2076
- this.vitals.ttfb = ttfb;
2077
- this.reportVital("TTFB", ttfb);
2078
- this.addMetric({
2079
- type: "navigation",
2080
- name: "page_load",
2081
- value: nav.loadEventEnd - nav.startTime,
2082
- unit: "ms",
2083
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2084
- url: window.location.href,
2085
- data: {
2086
- dns: nav.domainLookupEnd - nav.domainLookupStart,
2087
- tcp: nav.connectEnd - nav.connectStart,
2088
- ttfb,
2089
- domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
2090
- domComplete: nav.domComplete - nav.startTime,
2091
- transferSize: nav.transferSize,
2092
- encodedBodySize: nav.encodedBodySize,
2093
- decodedBodySize: nav.decodedBodySize
2094
- }
2095
- });
2096
- };
2097
- if (document.readyState === "complete") {
2098
- setTimeout(collect, 0);
2099
- } else {
2100
- window.addEventListener("load", () => setTimeout(collect, 0));
2349
+ if (this.resourceObserver) {
2350
+ this.resourceObserver.disconnect();
2351
+ this.resourceObserver = null;
2101
2352
  }
2353
+ this.flush();
2102
2354
  }
2355
+ // ---- Internal ----
2103
2356
  reportVital(name, value, unit = "ms") {
2104
2357
  this.addMetric({
2105
2358
  type: "web_vital",
@@ -2133,6 +2386,7 @@ var PerformanceManager = class {
2133
2386
  }
2134
2387
  const metrics = [...this.metrics];
2135
2388
  this.metrics = [];
2389
+ this.resourceCount = 0;
2136
2390
  const endpoint = this.config.performanceEndpoint || `${this.config.endpoint}/api/performance`;
2137
2391
  try {
2138
2392
  const response = await fetch(endpoint, {
@@ -2310,6 +2564,292 @@ var TracePropagator = class {
2310
2564
  }
2311
2565
  };
2312
2566
 
2567
+ // src/superlink/client.ts
2568
+ var DEFAULT_SUPERLINK_ENDPOINT = "https://pongl.ink";
2569
+ var SuperLinkClient = class {
2570
+ constructor(config = {}) {
2571
+ this.config = {
2572
+ endpoint: stripTrailingSlash(config.endpoint || DEFAULT_SUPERLINK_ENDPOINT),
2573
+ appId: config.appId,
2574
+ installId: config.installId,
2575
+ debug: config.debug ?? false
2576
+ };
2577
+ }
2578
+ /** Replace config in place (used by init() so module-level helpers see updates). */
2579
+ configure(config) {
2580
+ if (config.endpoint) this.config.endpoint = stripTrailingSlash(config.endpoint);
2581
+ if (config.appId !== void 0) this.config.appId = config.appId;
2582
+ if (config.installId !== void 0) this.config.installId = config.installId;
2583
+ if (config.debug !== void 0) this.config.debug = config.debug;
2584
+ }
2585
+ log(...args) {
2586
+ if (this.config.debug) console.warn("[SuperLink]", ...args);
2587
+ }
2588
+ /**
2589
+ * POST /match — ask the redirect engine to resolve a deferred deep link from
2590
+ * a click token (Android referrer / iOS clipboard) and/or a device
2591
+ * fingerprint. Returns `{ matched: false }` on any error.
2592
+ */
2593
+ async match(body) {
2594
+ const payload = {
2595
+ ...body,
2596
+ install_id: body.install_id ?? this.config.installId
2597
+ };
2598
+ try {
2599
+ const res = await fetch(`${this.config.endpoint}/match`, {
2600
+ method: "POST",
2601
+ headers: { "Content-Type": "application/json" },
2602
+ body: JSON.stringify(payload)
2603
+ });
2604
+ if (!res.ok) {
2605
+ this.log("match failed", res.status);
2606
+ return { matched: false };
2607
+ }
2608
+ const data = await res.json();
2609
+ return data ?? { matched: false };
2610
+ } catch (err) {
2611
+ this.log("match error", err);
2612
+ return { matched: false };
2613
+ }
2614
+ }
2615
+ /**
2616
+ * POST /events — report a lifecycle event (opened / converted) back to the
2617
+ * redirect engine, which fans out to analytics + webhooks. Best-effort.
2618
+ */
2619
+ async reportEvent(input) {
2620
+ try {
2621
+ const res = await fetch(`${this.config.endpoint}/events`, {
2622
+ method: "POST",
2623
+ headers: { "Content-Type": "application/json" },
2624
+ body: JSON.stringify({
2625
+ app_id: this.config.appId,
2626
+ install_id: this.config.installId,
2627
+ ...input
2628
+ })
2629
+ });
2630
+ if (!res.ok) this.log("event failed", input.type, res.status);
2631
+ return res.ok;
2632
+ } catch (err) {
2633
+ this.log("event error", err);
2634
+ return false;
2635
+ }
2636
+ }
2637
+ };
2638
+ function stripTrailingSlash(url) {
2639
+ return url.endsWith("/") ? url.slice(0, -1) : url;
2640
+ }
2641
+ var superlinkClient = new SuperLinkClient();
2642
+
2643
+ // src/superlink/parse.ts
2644
+ var UTM_KEYS2 = [
2645
+ ["source", "utm_source"],
2646
+ ["medium", "utm_medium"],
2647
+ ["campaign", "utm_campaign"],
2648
+ ["term", "utm_term"],
2649
+ ["content", "utm_content"]
2650
+ ];
2651
+ function parseUniversalLink(url) {
2652
+ let parsed;
2653
+ try {
2654
+ parsed = new URL(url);
2655
+ } catch {
2656
+ return null;
2657
+ }
2658
+ const params = parsed.searchParams;
2659
+ const utm = {};
2660
+ for (const [key, queryKey] of UTM_KEYS2) {
2661
+ const value = params.get(queryKey);
2662
+ if (value) utm[key] = value;
2663
+ }
2664
+ const referral = {};
2665
+ const deep_link_data = {};
2666
+ params.forEach((value, key) => {
2667
+ if (key.startsWith("r_")) {
2668
+ referral[key.slice(2)] = value;
2669
+ } else if (key.startsWith("~")) {
2670
+ deep_link_data[key.slice(1)] = value;
2671
+ }
2672
+ });
2673
+ const explicitPath = params.get("$deep_link_path") ?? params.get("dlp");
2674
+ const deep_link_path = explicitPath ?? (parsed.pathname && parsed.pathname !== "/" ? parsed.pathname : null);
2675
+ return {
2676
+ deep_link_path,
2677
+ deep_link_data,
2678
+ utm,
2679
+ referral,
2680
+ click_id: params.get("click_id"),
2681
+ match_type: "none",
2682
+ confidence: 1
2683
+ };
2684
+ }
2685
+
2686
+ // src/superlink/deferred.ts
2687
+ var handlers = /* @__PURE__ */ new Set();
2688
+ var lastMatched = null;
2689
+ function toDeepLink(res) {
2690
+ return {
2691
+ deep_link_path: res.deep_link_path ?? null,
2692
+ deep_link_data: res.deep_link_data ?? {},
2693
+ utm: res.utm ?? {},
2694
+ referral: res.referral ?? {},
2695
+ click_id: res.click_id ?? null,
2696
+ match_type: res.match_type ?? "fingerprint",
2697
+ confidence: res.confidence ?? (res.match_type && res.match_type !== "none" ? 1 : 0)
2698
+ };
2699
+ }
2700
+ function emitDeferredDeepLink(link) {
2701
+ lastMatched = link;
2702
+ for (const handler of handlers) {
2703
+ try {
2704
+ handler(link);
2705
+ } catch (err) {
2706
+ if (superlinkClient.config.debug) console.warn("[SuperLink] handler threw", err);
2707
+ }
2708
+ }
2709
+ }
2710
+ function onDeferredDeepLink(handler) {
2711
+ handlers.add(handler);
2712
+ if (lastMatched) {
2713
+ try {
2714
+ handler(lastMatched);
2715
+ } catch {
2716
+ }
2717
+ }
2718
+ return () => {
2719
+ handlers.delete(handler);
2720
+ };
2721
+ }
2722
+ function getMatchedDeepLink() {
2723
+ return lastMatched;
2724
+ }
2725
+
2726
+ // src/superlink/web.ts
2727
+ var STORAGE_KEY4 = "sp_superlink";
2728
+ var captured = null;
2729
+ var didCapture = false;
2730
+ function readStored2() {
2731
+ if (typeof sessionStorage === "undefined") return null;
2732
+ try {
2733
+ const raw = sessionStorage.getItem(STORAGE_KEY4);
2734
+ if (!raw) return null;
2735
+ const parsed = JSON.parse(raw);
2736
+ return parsed && typeof parsed === "object" ? parsed : null;
2737
+ } catch {
2738
+ return null;
2739
+ }
2740
+ }
2741
+ function writeStored2(link) {
2742
+ if (typeof sessionStorage === "undefined") return;
2743
+ try {
2744
+ sessionStorage.setItem(STORAGE_KEY4, JSON.stringify(link));
2745
+ } catch {
2746
+ }
2747
+ }
2748
+ function hasPayload(link) {
2749
+ return Boolean(
2750
+ link.deep_link_path || link.click_id || Object.keys(link.deep_link_data).length > 0 || Object.keys(link.referral).length > 0 || Object.keys(link.utm).length > 0
2751
+ );
2752
+ }
2753
+ function writeClipboardToken(clickId) {
2754
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
2755
+ const url = `${superlinkClient.config.endpoint}/c/${clickId}`;
2756
+ navigator.clipboard.writeText(url).catch(() => {
2757
+ });
2758
+ }
2759
+ function extractIdentityMetadata(url) {
2760
+ const href = url ?? (typeof window !== "undefined" && window.location ? window.location.href : null);
2761
+ if (!href) return void 0;
2762
+ let loc;
2763
+ try {
2764
+ loc = new URL(href);
2765
+ } catch {
2766
+ return void 0;
2767
+ }
2768
+ const email = loc.searchParams.get("email");
2769
+ const phone = loc.searchParams.get("phone");
2770
+ const userId = loc.searchParams.get("user_id");
2771
+ const id = loc.searchParams.get("id");
2772
+ const customRaw = loc.searchParams.get("custom");
2773
+ const identity = {};
2774
+ if (email) identity.email = email;
2775
+ if (phone) identity.phone = phone;
2776
+ if (userId) identity.user_id = userId;
2777
+ if (id) identity.id = id;
2778
+ if (customRaw) {
2779
+ try {
2780
+ const custom = JSON.parse(decodeURIComponent(customRaw));
2781
+ if (typeof custom === "object" && custom !== null) {
2782
+ identity.custom = custom;
2783
+ }
2784
+ } catch {
2785
+ }
2786
+ }
2787
+ return Object.keys(identity).length > 0 ? identity : void 0;
2788
+ }
2789
+ function captureWebDeepLink() {
2790
+ if (didCapture) return captured;
2791
+ didCapture = true;
2792
+ const stored = readStored2();
2793
+ if (stored) {
2794
+ captured = stored;
2795
+ return captured;
2796
+ }
2797
+ if (typeof window === "undefined" || !window.location) return null;
2798
+ const link = parseUniversalLink(window.location.href);
2799
+ if (link && hasPayload(link)) {
2800
+ captured = link;
2801
+ writeStored2(link);
2802
+ void superlinkClient.reportEvent({
2803
+ type: "opened",
2804
+ click_id: link.click_id,
2805
+ platform: "desktop",
2806
+ properties: { surface: "web" }
2807
+ });
2808
+ }
2809
+ return captured;
2810
+ }
2811
+ function getDeepLink() {
2812
+ if (!didCapture) return captureWebDeepLink();
2813
+ return captured;
2814
+ }
2815
+ function webFingerprint() {
2816
+ const fp = { platform: "desktop" };
2817
+ if (typeof navigator !== "undefined") {
2818
+ if (navigator.userAgent) fp.user_agent = navigator.userAgent;
2819
+ const lang = navigator.language || navigator.languages && navigator.languages[0];
2820
+ if (lang) fp.language = lang;
2821
+ }
2822
+ return fp;
2823
+ }
2824
+ async function identify(identity) {
2825
+ if (!identity || Object.keys(identity).length === 0) return null;
2826
+ const res = await superlinkClient.match({
2827
+ identity,
2828
+ fingerprint: webFingerprint()
2829
+ });
2830
+ if (!res.matched) return null;
2831
+ const link = toDeepLink(res);
2832
+ emitDeferredDeepLink(link);
2833
+ return link;
2834
+ }
2835
+ async function completeFromScan(scanned) {
2836
+ if (!scanned) return null;
2837
+ const res = await superlinkClient.match({
2838
+ qr_token: scanned,
2839
+ fingerprint: webFingerprint()
2840
+ });
2841
+ if (!res.matched) return null;
2842
+ const link = toDeepLink(res);
2843
+ emitDeferredDeepLink(link);
2844
+ return link;
2845
+ }
2846
+
2847
+ // src/superlink/index.ts
2848
+ function initSuperLink(config = {}) {
2849
+ superlinkClient.configure(config);
2850
+ captureWebDeepLink();
2851
+ }
2852
+
2313
2853
  // src/index.ts
2314
2854
  function installFallbackEnvironment() {
2315
2855
  if (getEnvironment()) return;
@@ -2519,6 +3059,10 @@ var SitePongClient = class {
2519
3059
  webVitals: config.performance.webVitals,
2520
3060
  navigationTiming: config.performance.navigationTiming,
2521
3061
  resourceTiming: config.performance.resourceTiming,
3062
+ capturePageLoadTimings: config.performance.capturePageLoadTimings,
3063
+ capturePageRenderTimings: config.performance.capturePageRenderTimings,
3064
+ excludedResourceUrls: config.performance.excludedResourceUrls,
3065
+ resourceNameSanitizer: config.performance.resourceNameSanitizer,
2522
3066
  sampleRate: config.performance.sampleRate,
2523
3067
  flushInterval: config.performance.flushInterval,
2524
3068
  performanceEndpoint: config.performance.performanceEndpoint,
@@ -3227,7 +3771,7 @@ var areFlagsReady = sitepong.areFlagsReady.bind(sitepong);
3227
3771
  var refreshFlags = sitepong.refreshFlags.bind(sitepong);
3228
3772
  var track = sitepong.track.bind(sitepong);
3229
3773
  var trackPageView = sitepong.trackPageView.bind(sitepong);
3230
- var identify = sitepong.identify.bind(sitepong);
3774
+ var identify2 = sitepong.identify.bind(sitepong);
3231
3775
  var group = sitepong.group.bind(sitepong);
3232
3776
  var resetAnalytics = sitepong.resetAnalytics.bind(sitepong);
3233
3777
  var getVisitorId = sitepong.getVisitorId.bind(sitepong);
@@ -5163,11 +5707,11 @@ function getNotificationsPermission() {
5163
5707
  return Notification.permission === "granted";
5164
5708
  }
5165
5709
  function getDeviceAge() {
5166
- const STORAGE_KEY3 = "sitepong_device_age";
5710
+ const STORAGE_KEY5 = "sitepong_device_age";
5167
5711
  const result = { visitCount: 1 };
5168
5712
  if (typeof localStorage === "undefined") return result;
5169
5713
  try {
5170
- const stored = localStorage.getItem(STORAGE_KEY3);
5714
+ const stored = localStorage.getItem(STORAGE_KEY5);
5171
5715
  if (stored) {
5172
5716
  const parsed = JSON.parse(stored);
5173
5717
  result.firstSeen = parsed.firstSeen;
@@ -5175,7 +5719,7 @@ function getDeviceAge() {
5175
5719
  } else {
5176
5720
  result.firstSeen = (/* @__PURE__ */ new Date()).toISOString();
5177
5721
  }
5178
- localStorage.setItem(STORAGE_KEY3, JSON.stringify({
5722
+ localStorage.setItem(STORAGE_KEY5, JSON.stringify({
5179
5723
  firstSeen: result.firstSeen,
5180
5724
  visitCount: result.visitCount
5181
5725
  }));
@@ -5957,6 +6501,6 @@ registerWebManagerFactories({
5957
6501
  createPerformance: (cfg) => new PerformanceManager(cfg)
5958
6502
  });
5959
6503
 
5960
- export { TracePropagator, addBreadcrumb, areFlagsReady, captureError, captureMessage, clearAnonymousId, clearUser, sitepong as client, createTraceContext, cronCheckin, cronStart, cronWrap, dbTrack, dbTrackSync, src_default as default, endSpan, endTransaction, extractTrace, flush, flushMetrics, flushProfiles, generateSpanId, generateTraceId, getAllFlags, getAnonymousId, getDbNPlusOnePatterns, getDbQueryCount, getDeviceSignals, getFlag, getFraudCheck, getLatestProfile, getProfiles, getRemoteConfig, getReplaySessionId, getVariant, getVariantPayload2 as getVariantPayload, getVisitorId, getWebVitals, group, identify, init, initRN, isInitialized, isRemoteConfigFeatureEnabled, isReplayRecording, metricDistribution, metricGauge, metricHistogram, metricIncrement, metricStartTimer, metricTime, onRemoteConfigChange, profile, propagateTrace, refreshFlags, registerIdentifyHook, registerWebManagerFactories, resetAnalytics, resetDbQueryCount, setAnonymousId, setContext, setCurrentScreen, setEnvironment, setRNGetCurrentScreen, setTags, setUser, startProfileSpan, startReplay, startSpan, startTransaction, stopReplay, track, trackPageView, waitForFlags };
6504
+ export { DEFAULT_SUPERLINK_ENDPOINT, SuperLinkClient, TracePropagator, addBreadcrumb, areFlagsReady, captureError, captureMessage, captureWebDeepLink, clearAnonymousId, clearUser, sitepong as client, completeFromScan, createTraceContext, cronCheckin, cronStart, cronWrap, dbTrack, dbTrackSync, src_default as default, endSpan, endTransaction, extractIdentityMetadata, extractTrace, flush, flushMetrics, flushProfiles, generateSpanId, generateTraceId, getAllFlags, getAnonymousId, getDbNPlusOnePatterns, getDbQueryCount, getDeepLink, getDeviceSignals, getFlag, getFraudCheck, getLatestProfile, getMatchedDeepLink, getProfiles, getRemoteConfig, getReplaySessionId, getVariant, getVariantPayload2 as getVariantPayload, getVisitorId, getWebVitals, group, identify2 as identify, init, initRN, initSuperLink, isInitialized, isRemoteConfigFeatureEnabled, isReplayRecording, metricDistribution, metricGauge, metricHistogram, metricIncrement, metricStartTimer, metricTime, onDeferredDeepLink, onRemoteConfigChange, parseUniversalLink, profile, propagateTrace, refreshFlags, registerIdentifyHook, registerWebManagerFactories, resetAnalytics, resetDbQueryCount, setAnonymousId, setContext, setCurrentScreen, setEnvironment, setRNGetCurrentScreen, setTags, setUser, startProfileSpan, startReplay, startSpan, startTransaction, stopReplay, superlinkClient, identify as superlinkIdentify, track, trackPageView, waitForFlags, writeClipboardToken };
5961
6505
  //# sourceMappingURL=web.mjs.map
5962
6506
  //# sourceMappingURL=web.mjs.map