sitepong 0.1.13 → 0.2.0

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 {
@@ -1862,11 +1864,70 @@ var RemoteConfigManager = class {
1862
1864
  }
1863
1865
  }
1864
1866
  };
1865
-
1866
- // src/performance/manager.ts
1867
1867
  var DEFAULT_ENDPOINT3 = "https://ingest.sitepong.com";
1868
1868
  var DEFAULT_FLUSH_INTERVAL2 = 1e4;
1869
1869
  var MAX_FLUSH_FAILURES2 = 3;
1870
+ var MAX_RESOURCES_PER_FLUSH = 200;
1871
+ var PAGE_TIMING_POLL_INTERVAL = 200;
1872
+ var PAGE_TIMING_TIMEOUT = 3e4;
1873
+ function getPaintBlocks(resources) {
1874
+ const paintBlocks = [];
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
+ }
1894
+ }
1895
+ if (!src) continue;
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 });
1912
+ }
1913
+ return paintBlocks;
1914
+ }
1915
+ function calculateSpeedIndex(firstContentfulPaint, paintBlocks) {
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);
1928
+ }
1929
+ return a === 0 ? 0 : s / a;
1930
+ }
1870
1931
  var PerformanceManager = class {
1871
1932
  constructor(config) {
1872
1933
  this.metrics = [];
@@ -1875,12 +1936,22 @@ var PerformanceManager = class {
1875
1936
  this.disabled = false;
1876
1937
  this.vitals = {};
1877
1938
  this.activeTransactions = /* @__PURE__ */ new Map();
1939
+ // Resource timing state for Speed Index
1940
+ this.resourceObserver = null;
1941
+ this.resourceTimeMap = {};
1942
+ this.resourceCount = 0;
1943
+ // Page timing polling timers
1944
+ this.pageLoadTimer = null;
1945
+ this.pageRenderTimer = null;
1878
1946
  this.config = {
1879
1947
  endpoint: DEFAULT_ENDPOINT3,
1880
1948
  enabled: true,
1881
1949
  webVitals: true,
1882
1950
  navigationTiming: true,
1883
- resourceTiming: false,
1951
+ resourceTiming: true,
1952
+ capturePageLoadTimings: true,
1953
+ capturePageRenderTimings: true,
1954
+ excludedResourceUrls: [],
1884
1955
  flushInterval: DEFAULT_FLUSH_INTERVAL2,
1885
1956
  sampleRate: 1,
1886
1957
  debug: false,
@@ -1891,18 +1962,250 @@ var PerformanceManager = class {
1891
1962
  init() {
1892
1963
  if (!this.config.enabled || !this.sampled) return;
1893
1964
  if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
1894
- if (this.config.webVitals) {
1895
- this.observeWebVitals();
1965
+ if (this.config.webVitals !== false) {
1966
+ this.initWebVitals();
1896
1967
  }
1897
- if (this.config.navigationTiming) {
1898
- this.collectNavigationTiming();
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();
1899
1976
  }
1900
1977
  this.startFlushTimer();
1901
1978
  this.log("Performance monitoring initialized");
1902
1979
  }
1903
- /**
1904
- * Start a custom transaction for measuring operations
1905
- */
1980
+ // ---- Web Vitals (via web-vitals library) ----
1981
+ initWebVitals() {
1982
+ onCLS((m) => {
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
+ });
2002
+ }
2003
+ // ---- Resource Timing (adapted from OpenReplay timing.ts) ----
2004
+ initResourceTiming() {
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
+ }
2016
+ }
2017
+ isServiceURL(url) {
2018
+ const endpoint = this.config.performanceEndpoint || this.config.endpoint;
2019
+ return url.startsWith(endpoint);
2020
+ }
2021
+ processResourceEntry(entry) {
2022
+ if (entry.duration < 0 || !entry.name.startsWith("http")) return;
2023
+ if (this.isServiceURL(entry.name)) return;
2024
+ if (this.resourceTimeMap !== null) {
2025
+ this.resourceTimeMap[entry.name] = entry.startTime + entry.duration;
2026
+ }
2027
+ for (const excluded of this.config.excludedResourceUrls) {
2028
+ if (entry.name.startsWith(excluded)) return;
2029
+ }
2030
+ if (this.resourceCount >= MAX_RESOURCES_PER_FLUSH) return;
2031
+ this.resourceCount++;
2032
+ let stalled = 0;
2033
+ if (entry.connectEnd && entry.connectEnd > entry.domainLookupEnd) {
2034
+ stalled = Math.max(0, entry.requestStart - entry.connectEnd);
2035
+ } else {
2036
+ stalled = Math.max(0, entry.requestStart - entry.domainLookupEnd);
2037
+ }
2038
+ const cached = entry.responseStatus && entry.responseStatus === 304 || entry.deliveryType && entry.deliveryType === "cache" || entry.transferSize === 0 && entry.decodedBodySize > 0;
2039
+ const responseStatus = entry.responseStatus || 0;
2040
+ const failed = responseStatus >= 400;
2041
+ const breakdown = {
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
2058
+ };
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
+ }
2070
+ // ---- Page Load Timing (adapted from OpenReplay timing.ts) ----
2071
+ initPageLoadTiming() {
2072
+ if (typeof window === "undefined" || !window.performance) return;
2073
+ let firstPaint = 0;
2074
+ let firstContentfulPaint = 0;
2075
+ let sent = false;
2076
+ const startTime = performance.now();
2077
+ this.pageLoadTimer = setInterval(() => {
2078
+ if (sent) {
2079
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
2080
+ return;
2081
+ }
2082
+ if (firstPaint === 0 || firstContentfulPaint === 0) {
2083
+ for (const entry of performance.getEntriesByType("paint")) {
2084
+ if (entry.name === "first-paint") firstPaint = entry.startTime;
2085
+ if (entry.name === "first-contentful-paint") firstContentfulPaint = entry.startTime;
2086
+ }
2087
+ }
2088
+ const nav = performance.getEntriesByType("navigation")[0];
2089
+ const timedOut = performance.now() - startTime > PAGE_TIMING_TIMEOUT;
2090
+ if (nav && nav.loadEventEnd > 0 || timedOut) {
2091
+ sent = true;
2092
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
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
2119
+ }
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;
2145
+ }
2146
+ }
2147
+ }
2148
+ if (this.resourceTimeMap !== null) {
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) ----
1906
2209
  startTransaction(name, data) {
1907
2210
  const id = `txn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1908
2211
  const transaction = {
@@ -1917,9 +2220,6 @@ var PerformanceManager = class {
1917
2220
  this.log("Transaction started:", name, id);
1918
2221
  return id;
1919
2222
  }
1920
- /**
1921
- * End a transaction and report it
1922
- */
1923
2223
  endTransaction(id, status = "ok") {
1924
2224
  const transaction = this.activeTransactions.get(id);
1925
2225
  if (!transaction) {
@@ -1948,9 +2248,6 @@ var PerformanceManager = class {
1948
2248
  });
1949
2249
  this.log("Transaction ended:", transaction.name, `${transaction.duration.toFixed(1)}ms`);
1950
2250
  }
1951
- /**
1952
- * Start a span within a transaction
1953
- */
1954
2251
  startSpan(transactionId, name, data) {
1955
2252
  const transaction = this.activeTransactions.get(transactionId);
1956
2253
  if (!transaction) {
@@ -1967,9 +2264,6 @@ var PerformanceManager = class {
1967
2264
  transaction.spans.push(span);
1968
2265
  return span.id;
1969
2266
  }
1970
- /**
1971
- * End a span
1972
- */
1973
2267
  endSpan(transactionId, spanId, status = "ok") {
1974
2268
  const transaction = this.activeTransactions.get(transactionId);
1975
2269
  if (!transaction) return;
@@ -1979,9 +2273,7 @@ var PerformanceManager = class {
1979
2273
  span.duration = span.endTime - span.startTime;
1980
2274
  span.status = status;
1981
2275
  }
1982
- /**
1983
- * Get current Web Vitals
1984
- */
2276
+ // ---- Public API ----
1985
2277
  getVitals() {
1986
2278
  return { ...this.vitals };
1987
2279
  }
@@ -1990,116 +2282,21 @@ var PerformanceManager = class {
1990
2282
  clearInterval(this.flushTimer);
1991
2283
  this.flushTimer = null;
1992
2284
  }
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) {
2285
+ if (this.pageLoadTimer) {
2286
+ clearInterval(this.pageLoadTimer);
2287
+ this.pageLoadTimer = null;
2041
2288
  }
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) {
2054
- }
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) {
2289
+ if (this.pageRenderTimer) {
2290
+ clearInterval(this.pageRenderTimer);
2291
+ this.pageRenderTimer = null;
2068
2292
  }
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));
2293
+ if (this.resourceObserver) {
2294
+ this.resourceObserver.disconnect();
2295
+ this.resourceObserver = null;
2101
2296
  }
2297
+ this.flush();
2102
2298
  }
2299
+ // ---- Internal ----
2103
2300
  reportVital(name, value, unit = "ms") {
2104
2301
  this.addMetric({
2105
2302
  type: "web_vital",
@@ -2133,6 +2330,7 @@ var PerformanceManager = class {
2133
2330
  }
2134
2331
  const metrics = [...this.metrics];
2135
2332
  this.metrics = [];
2333
+ this.resourceCount = 0;
2136
2334
  const endpoint = this.config.performanceEndpoint || `${this.config.endpoint}/api/performance`;
2137
2335
  try {
2138
2336
  const response = await fetch(endpoint, {
@@ -2310,6 +2508,292 @@ var TracePropagator = class {
2310
2508
  }
2311
2509
  };
2312
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)
2642
+ };
2643
+ }
2644
+ function emitDeferredDeepLink(link) {
2645
+ lastMatched = link;
2646
+ for (const handler of handlers) {
2647
+ try {
2648
+ handler(link);
2649
+ } catch (err) {
2650
+ if (superlinkClient.config.debug) console.warn("[SuperLink] handler threw", err);
2651
+ }
2652
+ }
2653
+ }
2654
+ function onDeferredDeepLink(handler) {
2655
+ handlers.add(handler);
2656
+ if (lastMatched) {
2657
+ try {
2658
+ handler(lastMatched);
2659
+ } catch {
2660
+ }
2661
+ }
2662
+ return () => {
2663
+ handlers.delete(handler);
2664
+ };
2665
+ }
2666
+ function getMatchedDeepLink() {
2667
+ return lastMatched;
2668
+ }
2669
+
2670
+ // src/superlink/web.ts
2671
+ var STORAGE_KEY3 = "sp_superlink";
2672
+ var captured = null;
2673
+ var didCapture = false;
2674
+ function readStored() {
2675
+ if (typeof sessionStorage === "undefined") return null;
2676
+ try {
2677
+ const raw = sessionStorage.getItem(STORAGE_KEY3);
2678
+ if (!raw) return null;
2679
+ const parsed = JSON.parse(raw);
2680
+ return parsed && typeof parsed === "object" ? parsed : null;
2681
+ } catch {
2682
+ return null;
2683
+ }
2684
+ }
2685
+ function writeStored(link) {
2686
+ if (typeof sessionStorage === "undefined") return;
2687
+ try {
2688
+ sessionStorage.setItem(STORAGE_KEY3, JSON.stringify(link));
2689
+ } catch {
2690
+ }
2691
+ }
2692
+ function hasPayload(link) {
2693
+ return Boolean(
2694
+ 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
2695
+ );
2696
+ }
2697
+ function writeClipboardToken(clickId) {
2698
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
2699
+ const url = `${superlinkClient.config.endpoint}/c/${clickId}`;
2700
+ navigator.clipboard.writeText(url).catch(() => {
2701
+ });
2702
+ }
2703
+ function extractIdentityMetadata(url) {
2704
+ const href = url ?? (typeof window !== "undefined" && window.location ? window.location.href : null);
2705
+ if (!href) return void 0;
2706
+ let loc;
2707
+ try {
2708
+ loc = new URL(href);
2709
+ } catch {
2710
+ return void 0;
2711
+ }
2712
+ const email = loc.searchParams.get("email");
2713
+ const phone = loc.searchParams.get("phone");
2714
+ const userId = loc.searchParams.get("user_id");
2715
+ const id = loc.searchParams.get("id");
2716
+ const customRaw = loc.searchParams.get("custom");
2717
+ const identity = {};
2718
+ if (email) identity.email = email;
2719
+ if (phone) identity.phone = phone;
2720
+ if (userId) identity.user_id = userId;
2721
+ if (id) identity.id = id;
2722
+ if (customRaw) {
2723
+ try {
2724
+ const custom = JSON.parse(decodeURIComponent(customRaw));
2725
+ if (typeof custom === "object" && custom !== null) {
2726
+ identity.custom = custom;
2727
+ }
2728
+ } catch {
2729
+ }
2730
+ }
2731
+ return Object.keys(identity).length > 0 ? identity : void 0;
2732
+ }
2733
+ function captureWebDeepLink() {
2734
+ if (didCapture) return captured;
2735
+ didCapture = true;
2736
+ const stored = readStored();
2737
+ if (stored) {
2738
+ captured = stored;
2739
+ return captured;
2740
+ }
2741
+ if (typeof window === "undefined" || !window.location) return null;
2742
+ const link = parseUniversalLink(window.location.href);
2743
+ if (link && hasPayload(link)) {
2744
+ captured = link;
2745
+ writeStored(link);
2746
+ void superlinkClient.reportEvent({
2747
+ type: "opened",
2748
+ click_id: link.click_id,
2749
+ platform: "desktop",
2750
+ properties: { surface: "web" }
2751
+ });
2752
+ }
2753
+ return captured;
2754
+ }
2755
+ function getDeepLink() {
2756
+ if (!didCapture) return captureWebDeepLink();
2757
+ return captured;
2758
+ }
2759
+ function webFingerprint() {
2760
+ const fp = { platform: "desktop" };
2761
+ if (typeof navigator !== "undefined") {
2762
+ if (navigator.userAgent) fp.user_agent = navigator.userAgent;
2763
+ const lang = navigator.language || navigator.languages && navigator.languages[0];
2764
+ if (lang) fp.language = lang;
2765
+ }
2766
+ return fp;
2767
+ }
2768
+ async function identify(identity) {
2769
+ if (!identity || Object.keys(identity).length === 0) return null;
2770
+ const res = await superlinkClient.match({
2771
+ identity,
2772
+ fingerprint: webFingerprint()
2773
+ });
2774
+ if (!res.matched) return null;
2775
+ const link = toDeepLink(res);
2776
+ emitDeferredDeepLink(link);
2777
+ return link;
2778
+ }
2779
+ async function completeFromScan(scanned) {
2780
+ if (!scanned) return null;
2781
+ const res = await superlinkClient.match({
2782
+ qr_token: scanned,
2783
+ fingerprint: webFingerprint()
2784
+ });
2785
+ if (!res.matched) return null;
2786
+ const link = toDeepLink(res);
2787
+ emitDeferredDeepLink(link);
2788
+ return link;
2789
+ }
2790
+
2791
+ // src/superlink/index.ts
2792
+ function initSuperLink(config = {}) {
2793
+ superlinkClient.configure(config);
2794
+ captureWebDeepLink();
2795
+ }
2796
+
2313
2797
  // src/index.ts
2314
2798
  function installFallbackEnvironment() {
2315
2799
  if (getEnvironment()) return;
@@ -2519,6 +3003,10 @@ var SitePongClient = class {
2519
3003
  webVitals: config.performance.webVitals,
2520
3004
  navigationTiming: config.performance.navigationTiming,
2521
3005
  resourceTiming: config.performance.resourceTiming,
3006
+ capturePageLoadTimings: config.performance.capturePageLoadTimings,
3007
+ capturePageRenderTimings: config.performance.capturePageRenderTimings,
3008
+ excludedResourceUrls: config.performance.excludedResourceUrls,
3009
+ resourceNameSanitizer: config.performance.resourceNameSanitizer,
2522
3010
  sampleRate: config.performance.sampleRate,
2523
3011
  flushInterval: config.performance.flushInterval,
2524
3012
  performanceEndpoint: config.performance.performanceEndpoint,
@@ -3227,7 +3715,7 @@ var areFlagsReady = sitepong.areFlagsReady.bind(sitepong);
3227
3715
  var refreshFlags = sitepong.refreshFlags.bind(sitepong);
3228
3716
  var track = sitepong.track.bind(sitepong);
3229
3717
  var trackPageView = sitepong.trackPageView.bind(sitepong);
3230
- var identify = sitepong.identify.bind(sitepong);
3718
+ var identify2 = sitepong.identify.bind(sitepong);
3231
3719
  var group = sitepong.group.bind(sitepong);
3232
3720
  var resetAnalytics = sitepong.resetAnalytics.bind(sitepong);
3233
3721
  var getVisitorId = sitepong.getVisitorId.bind(sitepong);
@@ -5163,11 +5651,11 @@ function getNotificationsPermission() {
5163
5651
  return Notification.permission === "granted";
5164
5652
  }
5165
5653
  function getDeviceAge() {
5166
- const STORAGE_KEY3 = "sitepong_device_age";
5654
+ const STORAGE_KEY4 = "sitepong_device_age";
5167
5655
  const result = { visitCount: 1 };
5168
5656
  if (typeof localStorage === "undefined") return result;
5169
5657
  try {
5170
- const stored = localStorage.getItem(STORAGE_KEY3);
5658
+ const stored = localStorage.getItem(STORAGE_KEY4);
5171
5659
  if (stored) {
5172
5660
  const parsed = JSON.parse(stored);
5173
5661
  result.firstSeen = parsed.firstSeen;
@@ -5175,7 +5663,7 @@ function getDeviceAge() {
5175
5663
  } else {
5176
5664
  result.firstSeen = (/* @__PURE__ */ new Date()).toISOString();
5177
5665
  }
5178
- localStorage.setItem(STORAGE_KEY3, JSON.stringify({
5666
+ localStorage.setItem(STORAGE_KEY4, JSON.stringify({
5179
5667
  firstSeen: result.firstSeen,
5180
5668
  visitCount: result.visitCount
5181
5669
  }));
@@ -5957,6 +6445,6 @@ registerWebManagerFactories({
5957
6445
  createPerformance: (cfg) => new PerformanceManager(cfg)
5958
6446
  });
5959
6447
 
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 };
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 };
5961
6449
  //# sourceMappingURL=web.mjs.map
5962
6450
  //# sourceMappingURL=web.mjs.map