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.
@@ -2,6 +2,8 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var webVitals = require('web-vitals');
6
+
5
7
  // src/platforms/web/environment.ts
6
8
  function makeLocalStorageAdapter() {
7
9
  try {
@@ -624,6 +626,60 @@ function clearSession() {
624
626
  memorySessionTs = null;
625
627
  }
626
628
 
629
+ // src/analytics/utm.ts
630
+ var STORAGE_KEY2 = "sp_utm";
631
+ var UTM_KEYS = [
632
+ ["source", "utm_source"],
633
+ ["medium", "utm_medium"],
634
+ ["campaign", "utm_campaign"],
635
+ ["term", "utm_term"],
636
+ ["content", "utm_content"]
637
+ ];
638
+ function parseFromLocation() {
639
+ if (typeof window === "undefined" || !window.location || !window.location.search) return null;
640
+ let params;
641
+ try {
642
+ params = new URLSearchParams(window.location.search);
643
+ } catch {
644
+ return null;
645
+ }
646
+ const result = {};
647
+ let found = false;
648
+ for (const [key, queryKey] of UTM_KEYS) {
649
+ const value = params.get(queryKey);
650
+ if (value) {
651
+ result[key] = value;
652
+ found = true;
653
+ }
654
+ }
655
+ return found ? result : null;
656
+ }
657
+ function readStored() {
658
+ if (typeof sessionStorage === "undefined") return null;
659
+ try {
660
+ const raw = sessionStorage.getItem(STORAGE_KEY2);
661
+ if (!raw) return null;
662
+ const parsed = JSON.parse(raw);
663
+ return parsed && typeof parsed === "object" ? parsed : null;
664
+ } catch {
665
+ return null;
666
+ }
667
+ }
668
+ function writeStored(utm) {
669
+ if (typeof sessionStorage === "undefined") return;
670
+ try {
671
+ sessionStorage.setItem(STORAGE_KEY2, JSON.stringify(utm));
672
+ } catch {
673
+ }
674
+ }
675
+ function getSessionUtm() {
676
+ const stored = readStored();
677
+ if (stored) return stored;
678
+ const fresh = parseFromLocation();
679
+ if (fresh) writeStored(fresh);
680
+ return fresh;
681
+ }
682
+
627
683
  // src/analytics/autocapture.ts
628
684
  var DEFAULT_BLOCK_SELECTORS = [
629
685
  "[data-sp-no-capture]",
@@ -1030,7 +1086,9 @@ var AnalyticsManager = class {
1030
1086
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1031
1087
  url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
1032
1088
  referrer: typeof document !== "undefined" && typeof document.referrer === "string" ? document.referrer : void 0,
1033
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
1089
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
1090
+ utm: getSessionUtm() || void 0,
1091
+ appVersion: this.config.appVersion || void 0
1034
1092
  };
1035
1093
  }
1036
1094
  enqueue(event) {
@@ -1728,7 +1786,7 @@ var DEFAULT_REMOTE_CONFIG = {
1728
1786
  };
1729
1787
 
1730
1788
  // src/remote-config/manager.ts
1731
- var STORAGE_KEY2 = "sitepong_remote_config";
1789
+ var STORAGE_KEY3 = "sitepong_remote_config";
1732
1790
  var STORAGE_TS_KEY = "sitepong_remote_config_ts";
1733
1791
  var RemoteConfigManager = class {
1734
1792
  constructor(options) {
@@ -1771,7 +1829,7 @@ var RemoteConfigManager = class {
1771
1829
  const storage = this.options.storage;
1772
1830
  if (!storage) return;
1773
1831
  try {
1774
- const cached = await storage.getItem(STORAGE_KEY2);
1832
+ const cached = await storage.getItem(STORAGE_KEY3);
1775
1833
  const cachedTs = await storage.getItem(STORAGE_TS_KEY);
1776
1834
  if (cached && cachedTs) {
1777
1835
  const age = Date.now() - parseInt(cachedTs, 10);
@@ -1832,7 +1890,7 @@ var RemoteConfigManager = class {
1832
1890
  const storage = this.options.storage;
1833
1891
  if (!storage) return;
1834
1892
  try {
1835
- await storage.setItem(STORAGE_KEY2, JSON.stringify(this.config));
1893
+ await storage.setItem(STORAGE_KEY3, JSON.stringify(this.config));
1836
1894
  await storage.setItem(STORAGE_TS_KEY, String(Date.now()));
1837
1895
  } catch {
1838
1896
  this.log("Failed to cache remote config");
@@ -1866,11 +1924,70 @@ var RemoteConfigManager = class {
1866
1924
  }
1867
1925
  }
1868
1926
  };
1869
-
1870
- // src/performance/manager.ts
1871
1927
  var DEFAULT_ENDPOINT3 = "https://ingest.sitepong.com";
1872
1928
  var DEFAULT_FLUSH_INTERVAL2 = 1e4;
1873
1929
  var MAX_FLUSH_FAILURES2 = 3;
1930
+ var MAX_RESOURCES_PER_FLUSH = 200;
1931
+ var PAGE_TIMING_POLL_INTERVAL = 200;
1932
+ var PAGE_TIMING_TIMEOUT = 3e4;
1933
+ function getPaintBlocks(resources) {
1934
+ const paintBlocks = [];
1935
+ const elements = document.getElementsByTagName("*");
1936
+ const styleURL = /url\(("[^"]*"|'[^']*'|[^)]*)\)/i;
1937
+ for (let i = 0; i < elements.length; i++) {
1938
+ const element = elements[i];
1939
+ let src = "";
1940
+ if (element.tagName === "IMG") {
1941
+ src = element.currentSrc || element.src;
1942
+ }
1943
+ if (!src) {
1944
+ const backgroundImage = getComputedStyle(element).getPropertyValue("background-image");
1945
+ if (backgroundImage) {
1946
+ const matches = styleURL.exec(backgroundImage);
1947
+ if (matches !== null) {
1948
+ src = matches[1];
1949
+ if (src.startsWith('"') || src.startsWith("'")) {
1950
+ src = src.substr(1, src.length - 2);
1951
+ }
1952
+ }
1953
+ }
1954
+ }
1955
+ if (!src) continue;
1956
+ const time = src.substr(0, 10) === "data:image" ? 0 : resources[src];
1957
+ if (time === void 0) continue;
1958
+ const rect = element.getBoundingClientRect();
1959
+ const top = Math.max(rect.top, 0);
1960
+ const left = Math.max(rect.left, 0);
1961
+ const bottom = Math.min(
1962
+ rect.bottom,
1963
+ window.innerHeight || document.documentElement && document.documentElement.clientHeight || 0
1964
+ );
1965
+ const right = Math.min(
1966
+ rect.right,
1967
+ window.innerWidth || document.documentElement && document.documentElement.clientWidth || 0
1968
+ );
1969
+ if (bottom <= top || right <= left) continue;
1970
+ const area = (bottom - top) * (right - left);
1971
+ paintBlocks.push({ time, area });
1972
+ }
1973
+ return paintBlocks;
1974
+ }
1975
+ function calculateSpeedIndex(firstContentfulPaint, paintBlocks) {
1976
+ let a = Math.max(
1977
+ document.documentElement && document.documentElement.clientWidth || 0,
1978
+ window.innerWidth || 0
1979
+ ) * Math.max(
1980
+ document.documentElement && document.documentElement.clientHeight || 0,
1981
+ window.innerHeight || 0
1982
+ ) / 10;
1983
+ let s = a * firstContentfulPaint;
1984
+ for (let i = 0; i < paintBlocks.length; i++) {
1985
+ const { time, area } = paintBlocks[i];
1986
+ a += area;
1987
+ s += area * (time > firstContentfulPaint ? time : firstContentfulPaint);
1988
+ }
1989
+ return a === 0 ? 0 : s / a;
1990
+ }
1874
1991
  var PerformanceManager = class {
1875
1992
  constructor(config) {
1876
1993
  this.metrics = [];
@@ -1879,12 +1996,22 @@ var PerformanceManager = class {
1879
1996
  this.disabled = false;
1880
1997
  this.vitals = {};
1881
1998
  this.activeTransactions = /* @__PURE__ */ new Map();
1999
+ // Resource timing state for Speed Index
2000
+ this.resourceObserver = null;
2001
+ this.resourceTimeMap = {};
2002
+ this.resourceCount = 0;
2003
+ // Page timing polling timers
2004
+ this.pageLoadTimer = null;
2005
+ this.pageRenderTimer = null;
1882
2006
  this.config = {
1883
2007
  endpoint: DEFAULT_ENDPOINT3,
1884
2008
  enabled: true,
1885
2009
  webVitals: true,
1886
2010
  navigationTiming: true,
1887
- resourceTiming: false,
2011
+ resourceTiming: true,
2012
+ capturePageLoadTimings: true,
2013
+ capturePageRenderTimings: true,
2014
+ excludedResourceUrls: [],
1888
2015
  flushInterval: DEFAULT_FLUSH_INTERVAL2,
1889
2016
  sampleRate: 1,
1890
2017
  debug: false,
@@ -1895,18 +2022,250 @@ var PerformanceManager = class {
1895
2022
  init() {
1896
2023
  if (!this.config.enabled || !this.sampled) return;
1897
2024
  if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
1898
- if (this.config.webVitals) {
1899
- this.observeWebVitals();
2025
+ if (this.config.webVitals !== false) {
2026
+ this.initWebVitals();
2027
+ }
2028
+ if (this.config.resourceTiming !== false) {
2029
+ this.initResourceTiming();
2030
+ }
2031
+ if (this.config.capturePageLoadTimings !== false) {
2032
+ this.initPageLoadTiming();
1900
2033
  }
1901
- if (this.config.navigationTiming) {
1902
- this.collectNavigationTiming();
2034
+ if (this.config.capturePageRenderTimings !== false) {
2035
+ this.initPageRenderTiming();
1903
2036
  }
1904
2037
  this.startFlushTimer();
1905
2038
  this.log("Performance monitoring initialized");
1906
2039
  }
1907
- /**
1908
- * Start a custom transaction for measuring operations
1909
- */
2040
+ // ---- Web Vitals (via web-vitals library) ----
2041
+ initWebVitals() {
2042
+ webVitals.onCLS((m) => {
2043
+ this.vitals.cls = m.value;
2044
+ this.reportVital("CLS", m.value, "score");
2045
+ });
2046
+ webVitals.onINP((m) => {
2047
+ this.vitals.inp = m.value;
2048
+ this.reportVital("INP", m.value);
2049
+ });
2050
+ webVitals.onLCP((m) => {
2051
+ this.vitals.lcp = m.value;
2052
+ this.reportVital("LCP", m.value);
2053
+ });
2054
+ webVitals.onTTFB((m) => {
2055
+ this.vitals.ttfb = m.value;
2056
+ this.reportVital("TTFB", m.value);
2057
+ });
2058
+ webVitals.onFCP((m) => {
2059
+ this.vitals.fcp = m.value;
2060
+ this.reportVital("FCP", m.value);
2061
+ });
2062
+ }
2063
+ // ---- Resource Timing (adapted from OpenReplay timing.ts) ----
2064
+ initResourceTiming() {
2065
+ if (typeof PerformanceObserver === "undefined") return;
2066
+ try {
2067
+ this.resourceObserver = new PerformanceObserver((list) => {
2068
+ for (const entry of list.getEntries()) {
2069
+ this.processResourceEntry(entry);
2070
+ }
2071
+ });
2072
+ this.resourceObserver.observe({ type: "resource", buffered: true });
2073
+ } catch {
2074
+ this.log("PerformanceObserver for resource timing not supported");
2075
+ }
2076
+ }
2077
+ isServiceURL(url) {
2078
+ const endpoint = this.config.performanceEndpoint || this.config.endpoint;
2079
+ return url.startsWith(endpoint);
2080
+ }
2081
+ processResourceEntry(entry) {
2082
+ if (entry.duration < 0 || !entry.name.startsWith("http")) return;
2083
+ if (this.isServiceURL(entry.name)) return;
2084
+ if (this.resourceTimeMap !== null) {
2085
+ this.resourceTimeMap[entry.name] = entry.startTime + entry.duration;
2086
+ }
2087
+ for (const excluded of this.config.excludedResourceUrls) {
2088
+ if (entry.name.startsWith(excluded)) return;
2089
+ }
2090
+ if (this.resourceCount >= MAX_RESOURCES_PER_FLUSH) return;
2091
+ this.resourceCount++;
2092
+ let stalled = 0;
2093
+ if (entry.connectEnd && entry.connectEnd > entry.domainLookupEnd) {
2094
+ stalled = Math.max(0, entry.requestStart - entry.connectEnd);
2095
+ } else {
2096
+ stalled = Math.max(0, entry.requestStart - entry.domainLookupEnd);
2097
+ }
2098
+ const cached = entry.responseStatus && entry.responseStatus === 304 || entry.deliveryType && entry.deliveryType === "cache" || entry.transferSize === 0 && entry.decodedBodySize > 0;
2099
+ const responseStatus = entry.responseStatus || 0;
2100
+ const failed = responseStatus >= 400;
2101
+ const breakdown = {
2102
+ queueing: entry.requestStart - entry.fetchStart,
2103
+ dnsLookup: entry.domainLookupEnd - entry.domainLookupStart,
2104
+ initialConnection: entry.connectEnd - entry.connectStart,
2105
+ ssl: entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart : 0,
2106
+ ttfb: entry.responseStart - entry.requestStart,
2107
+ contentDownload: entry.responseEnd - entry.responseStart,
2108
+ total: entry.duration ?? entry.responseEnd - entry.startTime,
2109
+ stalled,
2110
+ cached,
2111
+ failed,
2112
+ responseStatus,
2113
+ headerSize: entry.transferSize > entry.encodedBodySize ? entry.transferSize - entry.encodedBodySize : 0,
2114
+ encodedBodySize: entry.encodedBodySize || 0,
2115
+ decodedBodySize: failed ? -111 : entry.decodedBodySize || 0,
2116
+ transferSize: entry.transferSize,
2117
+ initiatorType: entry.initiatorType
2118
+ };
2119
+ const entryName = this.config.resourceNameSanitizer ? this.config.resourceNameSanitizer(entry.name) : entry.name;
2120
+ this.addMetric({
2121
+ type: "resource",
2122
+ name: entryName,
2123
+ value: entry.duration,
2124
+ unit: "ms",
2125
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2126
+ url: typeof window !== "undefined" && window.location ? window.location.href : void 0,
2127
+ data: breakdown
2128
+ });
2129
+ }
2130
+ // ---- Page Load Timing (adapted from OpenReplay timing.ts) ----
2131
+ initPageLoadTiming() {
2132
+ if (typeof window === "undefined" || !window.performance) return;
2133
+ let firstPaint = 0;
2134
+ let firstContentfulPaint = 0;
2135
+ let sent = false;
2136
+ const startTime = performance.now();
2137
+ this.pageLoadTimer = setInterval(() => {
2138
+ if (sent) {
2139
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
2140
+ return;
2141
+ }
2142
+ if (firstPaint === 0 || firstContentfulPaint === 0) {
2143
+ for (const entry of performance.getEntriesByType("paint")) {
2144
+ if (entry.name === "first-paint") firstPaint = entry.startTime;
2145
+ if (entry.name === "first-contentful-paint") firstContentfulPaint = entry.startTime;
2146
+ }
2147
+ }
2148
+ const nav = performance.getEntriesByType("navigation")[0];
2149
+ const timedOut = performance.now() - startTime > PAGE_TIMING_TIMEOUT;
2150
+ if (nav && nav.loadEventEnd > 0 || timedOut) {
2151
+ sent = true;
2152
+ if (this.pageLoadTimer) clearInterval(this.pageLoadTimer);
2153
+ if (nav) {
2154
+ const navigationStart = nav.startTime;
2155
+ this.addMetric({
2156
+ type: "navigation",
2157
+ name: "page_load",
2158
+ value: nav.loadEventEnd - navigationStart,
2159
+ unit: "ms",
2160
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2161
+ url: window.location.href,
2162
+ data: {
2163
+ requestStart: nav.requestStart - navigationStart,
2164
+ responseStart: nav.responseStart - navigationStart,
2165
+ responseEnd: nav.responseEnd - navigationStart,
2166
+ domContentLoadedEventStart: nav.domContentLoadedEventEnd - navigationStart,
2167
+ domContentLoadedEventEnd: nav.domContentLoadedEventEnd - navigationStart,
2168
+ loadEventStart: nav.loadEventStart - navigationStart,
2169
+ loadEventEnd: nav.loadEventEnd - navigationStart,
2170
+ firstPaint,
2171
+ firstContentfulPaint,
2172
+ dns: nav.domainLookupEnd - nav.domainLookupStart,
2173
+ tcp: nav.connectEnd - nav.connectStart,
2174
+ ttfb: nav.responseStart - nav.requestStart,
2175
+ domComplete: nav.domComplete - navigationStart,
2176
+ transferSize: nav.transferSize,
2177
+ encodedBodySize: nav.encodedBodySize,
2178
+ decodedBodySize: nav.decodedBodySize
2179
+ }
2180
+ });
2181
+ }
2182
+ }
2183
+ }, PAGE_TIMING_POLL_INTERVAL);
2184
+ }
2185
+ // ---- Page Render Timing: Speed Index, Visually Complete, TTI (adapted from OpenReplay) ----
2186
+ initPageRenderTiming() {
2187
+ if (typeof window === "undefined" || !window.performance) return;
2188
+ let firstContentfulPaint = 0;
2189
+ let visuallyComplete = 0;
2190
+ let interactiveWindowStartTime = 0;
2191
+ let interactiveWindowTickTime = 0;
2192
+ let paintBlocks = null;
2193
+ let sent = false;
2194
+ const startTime = performance.now();
2195
+ this.pageRenderTimer = setInterval(() => {
2196
+ if (sent) {
2197
+ if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
2198
+ return;
2199
+ }
2200
+ const time = performance.now();
2201
+ if (firstContentfulPaint === 0) {
2202
+ for (const entry of performance.getEntriesByType("paint")) {
2203
+ if (entry.name === "first-contentful-paint") {
2204
+ firstContentfulPaint = entry.startTime;
2205
+ }
2206
+ }
2207
+ }
2208
+ if (this.resourceTimeMap !== null) {
2209
+ const times = Object.values(this.resourceTimeMap);
2210
+ if (times.length > 0) {
2211
+ visuallyComplete = Math.max(...times);
2212
+ }
2213
+ if (time - visuallyComplete > 1e3) {
2214
+ paintBlocks = getPaintBlocks(this.resourceTimeMap);
2215
+ this.resourceTimeMap = null;
2216
+ }
2217
+ }
2218
+ if (interactiveWindowTickTime !== null) {
2219
+ if (time - interactiveWindowTickTime > 50) {
2220
+ interactiveWindowStartTime = time;
2221
+ }
2222
+ interactiveWindowTickTime = time - interactiveWindowStartTime > 5e3 ? null : time;
2223
+ }
2224
+ const timedOut = time - startTime > PAGE_TIMING_TIMEOUT;
2225
+ if (paintBlocks !== null && interactiveWindowTickTime === null || timedOut) {
2226
+ sent = true;
2227
+ if (this.pageRenderTimer) clearInterval(this.pageRenderTimer);
2228
+ this.resourceTimeMap = null;
2229
+ const speedIndex = paintBlocks === null ? 0 : calculateSpeedIndex(firstContentfulPaint || 0, paintBlocks);
2230
+ const nav = performance.getEntriesByType("navigation")[0];
2231
+ const domContentLoadedEnd = nav ? nav.domContentLoadedEventEnd - nav.startTime : 0;
2232
+ const timeToInteractive = interactiveWindowTickTime === null ? Math.max(interactiveWindowStartTime, firstContentfulPaint, domContentLoadedEnd) : 0;
2233
+ const url = window.location.href;
2234
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2235
+ if (speedIndex > 0) {
2236
+ this.addMetric({
2237
+ type: "page_render",
2238
+ name: "speed_index",
2239
+ value: Math.round(speedIndex),
2240
+ unit: "ms",
2241
+ timestamp,
2242
+ url
2243
+ });
2244
+ }
2245
+ if (visuallyComplete > 0) {
2246
+ this.addMetric({
2247
+ type: "page_render",
2248
+ name: "visually_complete",
2249
+ value: Math.round(visuallyComplete),
2250
+ unit: "ms",
2251
+ timestamp,
2252
+ url
2253
+ });
2254
+ }
2255
+ if (timeToInteractive > 0) {
2256
+ this.addMetric({
2257
+ type: "page_render",
2258
+ name: "tti",
2259
+ value: Math.round(timeToInteractive),
2260
+ unit: "ms",
2261
+ timestamp,
2262
+ url
2263
+ });
2264
+ }
2265
+ }
2266
+ }, PAGE_TIMING_POLL_INTERVAL);
2267
+ }
2268
+ // ---- Transactions / Spans (unchanged) ----
1910
2269
  startTransaction(name, data) {
1911
2270
  const id = `txn_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1912
2271
  const transaction = {
@@ -1921,9 +2280,6 @@ var PerformanceManager = class {
1921
2280
  this.log("Transaction started:", name, id);
1922
2281
  return id;
1923
2282
  }
1924
- /**
1925
- * End a transaction and report it
1926
- */
1927
2283
  endTransaction(id, status = "ok") {
1928
2284
  const transaction = this.activeTransactions.get(id);
1929
2285
  if (!transaction) {
@@ -1952,9 +2308,6 @@ var PerformanceManager = class {
1952
2308
  });
1953
2309
  this.log("Transaction ended:", transaction.name, `${transaction.duration.toFixed(1)}ms`);
1954
2310
  }
1955
- /**
1956
- * Start a span within a transaction
1957
- */
1958
2311
  startSpan(transactionId, name, data) {
1959
2312
  const transaction = this.activeTransactions.get(transactionId);
1960
2313
  if (!transaction) {
@@ -1971,9 +2324,6 @@ var PerformanceManager = class {
1971
2324
  transaction.spans.push(span);
1972
2325
  return span.id;
1973
2326
  }
1974
- /**
1975
- * End a span
1976
- */
1977
2327
  endSpan(transactionId, spanId, status = "ok") {
1978
2328
  const transaction = this.activeTransactions.get(transactionId);
1979
2329
  if (!transaction) return;
@@ -1983,9 +2333,7 @@ var PerformanceManager = class {
1983
2333
  span.duration = span.endTime - span.startTime;
1984
2334
  span.status = status;
1985
2335
  }
1986
- /**
1987
- * Get current Web Vitals
1988
- */
2336
+ // ---- Public API ----
1989
2337
  getVitals() {
1990
2338
  return { ...this.vitals };
1991
2339
  }
@@ -1994,116 +2342,21 @@ var PerformanceManager = class {
1994
2342
  clearInterval(this.flushTimer);
1995
2343
  this.flushTimer = null;
1996
2344
  }
1997
- this.flush();
1998
- }
1999
- observeWebVitals() {
2000
- if (typeof PerformanceObserver === "undefined") return;
2001
- try {
2002
- const lcpObserver = new PerformanceObserver((list) => {
2003
- const entries = list.getEntries();
2004
- const lastEntry = entries[entries.length - 1];
2005
- if (lastEntry) {
2006
- this.vitals.lcp = lastEntry.startTime;
2007
- this.reportVital("LCP", lastEntry.startTime);
2008
- }
2009
- });
2010
- lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
2011
- } catch (e) {
2012
- }
2013
- try {
2014
- const fidObserver = new PerformanceObserver((list) => {
2015
- const entry = list.getEntries()[0];
2016
- if (entry) {
2017
- const fid = entry.processingStart - entry.startTime;
2018
- this.vitals.fid = fid;
2019
- this.reportVital("FID", fid);
2020
- }
2021
- });
2022
- fidObserver.observe({ type: "first-input", buffered: true });
2023
- } catch (e) {
2024
- }
2025
- try {
2026
- let clsValue = 0;
2027
- const clsObserver = new PerformanceObserver((list) => {
2028
- for (const entry of list.getEntries()) {
2029
- const layoutShift = entry;
2030
- if (!layoutShift.hadRecentInput) {
2031
- clsValue += layoutShift.value;
2032
- }
2033
- }
2034
- this.vitals.cls = clsValue;
2035
- });
2036
- clsObserver.observe({ type: "layout-shift", buffered: true });
2037
- if (typeof document !== "undefined") {
2038
- document.addEventListener("visibilitychange", () => {
2039
- if (document.visibilityState === "hidden" && clsValue > 0) {
2040
- this.reportVital("CLS", clsValue, "score");
2041
- }
2042
- });
2043
- }
2044
- } catch (e) {
2045
- }
2046
- try {
2047
- const fcpObserver = new PerformanceObserver((list) => {
2048
- const entry = list.getEntries().find(
2049
- (e) => e.name === "first-contentful-paint"
2050
- );
2051
- if (entry) {
2052
- this.vitals.fcp = entry.startTime;
2053
- this.reportVital("FCP", entry.startTime);
2054
- }
2055
- });
2056
- fcpObserver.observe({ type: "paint", buffered: true });
2057
- } catch (e) {
2345
+ if (this.pageLoadTimer) {
2346
+ clearInterval(this.pageLoadTimer);
2347
+ this.pageLoadTimer = null;
2058
2348
  }
2059
- try {
2060
- let maxINP = 0;
2061
- const inpObserver = new PerformanceObserver((list) => {
2062
- for (const entry of list.getEntries()) {
2063
- const duration = entry.duration;
2064
- if (duration > maxINP) {
2065
- maxINP = duration;
2066
- this.vitals.inp = maxINP;
2067
- }
2068
- }
2069
- });
2070
- inpObserver.observe({ type: "event", buffered: true });
2071
- } catch (e) {
2349
+ if (this.pageRenderTimer) {
2350
+ clearInterval(this.pageRenderTimer);
2351
+ this.pageRenderTimer = null;
2072
2352
  }
2073
- }
2074
- collectNavigationTiming() {
2075
- if (typeof window === "undefined" || !window.performance || !window.location) return;
2076
- const collect = () => {
2077
- const nav = performance.getEntriesByType("navigation")[0];
2078
- if (!nav) return;
2079
- const ttfb = nav.responseStart - nav.requestStart;
2080
- this.vitals.ttfb = ttfb;
2081
- this.reportVital("TTFB", ttfb);
2082
- this.addMetric({
2083
- type: "navigation",
2084
- name: "page_load",
2085
- value: nav.loadEventEnd - nav.startTime,
2086
- unit: "ms",
2087
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2088
- url: window.location.href,
2089
- data: {
2090
- dns: nav.domainLookupEnd - nav.domainLookupStart,
2091
- tcp: nav.connectEnd - nav.connectStart,
2092
- ttfb,
2093
- domContentLoaded: nav.domContentLoadedEventEnd - nav.startTime,
2094
- domComplete: nav.domComplete - nav.startTime,
2095
- transferSize: nav.transferSize,
2096
- encodedBodySize: nav.encodedBodySize,
2097
- decodedBodySize: nav.decodedBodySize
2098
- }
2099
- });
2100
- };
2101
- if (document.readyState === "complete") {
2102
- setTimeout(collect, 0);
2103
- } else {
2104
- window.addEventListener("load", () => setTimeout(collect, 0));
2353
+ if (this.resourceObserver) {
2354
+ this.resourceObserver.disconnect();
2355
+ this.resourceObserver = null;
2105
2356
  }
2357
+ this.flush();
2106
2358
  }
2359
+ // ---- Internal ----
2107
2360
  reportVital(name, value, unit = "ms") {
2108
2361
  this.addMetric({
2109
2362
  type: "web_vital",
@@ -2137,6 +2390,7 @@ var PerformanceManager = class {
2137
2390
  }
2138
2391
  const metrics = [...this.metrics];
2139
2392
  this.metrics = [];
2393
+ this.resourceCount = 0;
2140
2394
  const endpoint = this.config.performanceEndpoint || `${this.config.endpoint}/api/performance`;
2141
2395
  try {
2142
2396
  const response = await fetch(endpoint, {
@@ -2314,6 +2568,292 @@ var TracePropagator = class {
2314
2568
  }
2315
2569
  };
2316
2570
 
2571
+ // src/superlink/client.ts
2572
+ var DEFAULT_SUPERLINK_ENDPOINT = "https://pongl.ink";
2573
+ var SuperLinkClient = class {
2574
+ constructor(config = {}) {
2575
+ this.config = {
2576
+ endpoint: stripTrailingSlash(config.endpoint || DEFAULT_SUPERLINK_ENDPOINT),
2577
+ appId: config.appId,
2578
+ installId: config.installId,
2579
+ debug: config.debug ?? false
2580
+ };
2581
+ }
2582
+ /** Replace config in place (used by init() so module-level helpers see updates). */
2583
+ configure(config) {
2584
+ if (config.endpoint) this.config.endpoint = stripTrailingSlash(config.endpoint);
2585
+ if (config.appId !== void 0) this.config.appId = config.appId;
2586
+ if (config.installId !== void 0) this.config.installId = config.installId;
2587
+ if (config.debug !== void 0) this.config.debug = config.debug;
2588
+ }
2589
+ log(...args) {
2590
+ if (this.config.debug) console.warn("[SuperLink]", ...args);
2591
+ }
2592
+ /**
2593
+ * POST /match — ask the redirect engine to resolve a deferred deep link from
2594
+ * a click token (Android referrer / iOS clipboard) and/or a device
2595
+ * fingerprint. Returns `{ matched: false }` on any error.
2596
+ */
2597
+ async match(body) {
2598
+ const payload = {
2599
+ ...body,
2600
+ install_id: body.install_id ?? this.config.installId
2601
+ };
2602
+ try {
2603
+ const res = await fetch(`${this.config.endpoint}/match`, {
2604
+ method: "POST",
2605
+ headers: { "Content-Type": "application/json" },
2606
+ body: JSON.stringify(payload)
2607
+ });
2608
+ if (!res.ok) {
2609
+ this.log("match failed", res.status);
2610
+ return { matched: false };
2611
+ }
2612
+ const data = await res.json();
2613
+ return data ?? { matched: false };
2614
+ } catch (err) {
2615
+ this.log("match error", err);
2616
+ return { matched: false };
2617
+ }
2618
+ }
2619
+ /**
2620
+ * POST /events — report a lifecycle event (opened / converted) back to the
2621
+ * redirect engine, which fans out to analytics + webhooks. Best-effort.
2622
+ */
2623
+ async reportEvent(input) {
2624
+ try {
2625
+ const res = await fetch(`${this.config.endpoint}/events`, {
2626
+ method: "POST",
2627
+ headers: { "Content-Type": "application/json" },
2628
+ body: JSON.stringify({
2629
+ app_id: this.config.appId,
2630
+ install_id: this.config.installId,
2631
+ ...input
2632
+ })
2633
+ });
2634
+ if (!res.ok) this.log("event failed", input.type, res.status);
2635
+ return res.ok;
2636
+ } catch (err) {
2637
+ this.log("event error", err);
2638
+ return false;
2639
+ }
2640
+ }
2641
+ };
2642
+ function stripTrailingSlash(url) {
2643
+ return url.endsWith("/") ? url.slice(0, -1) : url;
2644
+ }
2645
+ var superlinkClient = new SuperLinkClient();
2646
+
2647
+ // src/superlink/parse.ts
2648
+ var UTM_KEYS2 = [
2649
+ ["source", "utm_source"],
2650
+ ["medium", "utm_medium"],
2651
+ ["campaign", "utm_campaign"],
2652
+ ["term", "utm_term"],
2653
+ ["content", "utm_content"]
2654
+ ];
2655
+ function parseUniversalLink(url) {
2656
+ let parsed;
2657
+ try {
2658
+ parsed = new URL(url);
2659
+ } catch {
2660
+ return null;
2661
+ }
2662
+ const params = parsed.searchParams;
2663
+ const utm = {};
2664
+ for (const [key, queryKey] of UTM_KEYS2) {
2665
+ const value = params.get(queryKey);
2666
+ if (value) utm[key] = value;
2667
+ }
2668
+ const referral = {};
2669
+ const deep_link_data = {};
2670
+ params.forEach((value, key) => {
2671
+ if (key.startsWith("r_")) {
2672
+ referral[key.slice(2)] = value;
2673
+ } else if (key.startsWith("~")) {
2674
+ deep_link_data[key.slice(1)] = value;
2675
+ }
2676
+ });
2677
+ const explicitPath = params.get("$deep_link_path") ?? params.get("dlp");
2678
+ const deep_link_path = explicitPath ?? (parsed.pathname && parsed.pathname !== "/" ? parsed.pathname : null);
2679
+ return {
2680
+ deep_link_path,
2681
+ deep_link_data,
2682
+ utm,
2683
+ referral,
2684
+ click_id: params.get("click_id"),
2685
+ match_type: "none",
2686
+ confidence: 1
2687
+ };
2688
+ }
2689
+
2690
+ // src/superlink/deferred.ts
2691
+ var handlers = /* @__PURE__ */ new Set();
2692
+ var lastMatched = null;
2693
+ function toDeepLink(res) {
2694
+ return {
2695
+ deep_link_path: res.deep_link_path ?? null,
2696
+ deep_link_data: res.deep_link_data ?? {},
2697
+ utm: res.utm ?? {},
2698
+ referral: res.referral ?? {},
2699
+ click_id: res.click_id ?? null,
2700
+ match_type: res.match_type ?? "fingerprint",
2701
+ confidence: res.confidence ?? (res.match_type && res.match_type !== "none" ? 1 : 0)
2702
+ };
2703
+ }
2704
+ function emitDeferredDeepLink(link) {
2705
+ lastMatched = link;
2706
+ for (const handler of handlers) {
2707
+ try {
2708
+ handler(link);
2709
+ } catch (err) {
2710
+ if (superlinkClient.config.debug) console.warn("[SuperLink] handler threw", err);
2711
+ }
2712
+ }
2713
+ }
2714
+ function onDeferredDeepLink(handler) {
2715
+ handlers.add(handler);
2716
+ if (lastMatched) {
2717
+ try {
2718
+ handler(lastMatched);
2719
+ } catch {
2720
+ }
2721
+ }
2722
+ return () => {
2723
+ handlers.delete(handler);
2724
+ };
2725
+ }
2726
+ function getMatchedDeepLink() {
2727
+ return lastMatched;
2728
+ }
2729
+
2730
+ // src/superlink/web.ts
2731
+ var STORAGE_KEY4 = "sp_superlink";
2732
+ var captured = null;
2733
+ var didCapture = false;
2734
+ function readStored2() {
2735
+ if (typeof sessionStorage === "undefined") return null;
2736
+ try {
2737
+ const raw = sessionStorage.getItem(STORAGE_KEY4);
2738
+ if (!raw) return null;
2739
+ const parsed = JSON.parse(raw);
2740
+ return parsed && typeof parsed === "object" ? parsed : null;
2741
+ } catch {
2742
+ return null;
2743
+ }
2744
+ }
2745
+ function writeStored2(link) {
2746
+ if (typeof sessionStorage === "undefined") return;
2747
+ try {
2748
+ sessionStorage.setItem(STORAGE_KEY4, JSON.stringify(link));
2749
+ } catch {
2750
+ }
2751
+ }
2752
+ function hasPayload(link) {
2753
+ return Boolean(
2754
+ 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
2755
+ );
2756
+ }
2757
+ function writeClipboardToken(clickId) {
2758
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
2759
+ const url = `${superlinkClient.config.endpoint}/c/${clickId}`;
2760
+ navigator.clipboard.writeText(url).catch(() => {
2761
+ });
2762
+ }
2763
+ function extractIdentityMetadata(url) {
2764
+ const href = url ?? (typeof window !== "undefined" && window.location ? window.location.href : null);
2765
+ if (!href) return void 0;
2766
+ let loc;
2767
+ try {
2768
+ loc = new URL(href);
2769
+ } catch {
2770
+ return void 0;
2771
+ }
2772
+ const email = loc.searchParams.get("email");
2773
+ const phone = loc.searchParams.get("phone");
2774
+ const userId = loc.searchParams.get("user_id");
2775
+ const id = loc.searchParams.get("id");
2776
+ const customRaw = loc.searchParams.get("custom");
2777
+ const identity = {};
2778
+ if (email) identity.email = email;
2779
+ if (phone) identity.phone = phone;
2780
+ if (userId) identity.user_id = userId;
2781
+ if (id) identity.id = id;
2782
+ if (customRaw) {
2783
+ try {
2784
+ const custom = JSON.parse(decodeURIComponent(customRaw));
2785
+ if (typeof custom === "object" && custom !== null) {
2786
+ identity.custom = custom;
2787
+ }
2788
+ } catch {
2789
+ }
2790
+ }
2791
+ return Object.keys(identity).length > 0 ? identity : void 0;
2792
+ }
2793
+ function captureWebDeepLink() {
2794
+ if (didCapture) return captured;
2795
+ didCapture = true;
2796
+ const stored = readStored2();
2797
+ if (stored) {
2798
+ captured = stored;
2799
+ return captured;
2800
+ }
2801
+ if (typeof window === "undefined" || !window.location) return null;
2802
+ const link = parseUniversalLink(window.location.href);
2803
+ if (link && hasPayload(link)) {
2804
+ captured = link;
2805
+ writeStored2(link);
2806
+ void superlinkClient.reportEvent({
2807
+ type: "opened",
2808
+ click_id: link.click_id,
2809
+ platform: "desktop",
2810
+ properties: { surface: "web" }
2811
+ });
2812
+ }
2813
+ return captured;
2814
+ }
2815
+ function getDeepLink() {
2816
+ if (!didCapture) return captureWebDeepLink();
2817
+ return captured;
2818
+ }
2819
+ function webFingerprint() {
2820
+ const fp = { platform: "desktop" };
2821
+ if (typeof navigator !== "undefined") {
2822
+ if (navigator.userAgent) fp.user_agent = navigator.userAgent;
2823
+ const lang = navigator.language || navigator.languages && navigator.languages[0];
2824
+ if (lang) fp.language = lang;
2825
+ }
2826
+ return fp;
2827
+ }
2828
+ async function identify(identity) {
2829
+ if (!identity || Object.keys(identity).length === 0) return null;
2830
+ const res = await superlinkClient.match({
2831
+ identity,
2832
+ fingerprint: webFingerprint()
2833
+ });
2834
+ if (!res.matched) return null;
2835
+ const link = toDeepLink(res);
2836
+ emitDeferredDeepLink(link);
2837
+ return link;
2838
+ }
2839
+ async function completeFromScan(scanned) {
2840
+ if (!scanned) return null;
2841
+ const res = await superlinkClient.match({
2842
+ qr_token: scanned,
2843
+ fingerprint: webFingerprint()
2844
+ });
2845
+ if (!res.matched) return null;
2846
+ const link = toDeepLink(res);
2847
+ emitDeferredDeepLink(link);
2848
+ return link;
2849
+ }
2850
+
2851
+ // src/superlink/index.ts
2852
+ function initSuperLink(config = {}) {
2853
+ superlinkClient.configure(config);
2854
+ captureWebDeepLink();
2855
+ }
2856
+
2317
2857
  // src/index.ts
2318
2858
  function installFallbackEnvironment() {
2319
2859
  if (getEnvironment()) return;
@@ -2523,6 +3063,10 @@ var SitePongClient = class {
2523
3063
  webVitals: config.performance.webVitals,
2524
3064
  navigationTiming: config.performance.navigationTiming,
2525
3065
  resourceTiming: config.performance.resourceTiming,
3066
+ capturePageLoadTimings: config.performance.capturePageLoadTimings,
3067
+ capturePageRenderTimings: config.performance.capturePageRenderTimings,
3068
+ excludedResourceUrls: config.performance.excludedResourceUrls,
3069
+ resourceNameSanitizer: config.performance.resourceNameSanitizer,
2526
3070
  sampleRate: config.performance.sampleRate,
2527
3071
  flushInterval: config.performance.flushInterval,
2528
3072
  performanceEndpoint: config.performance.performanceEndpoint,
@@ -3231,7 +3775,7 @@ var areFlagsReady = sitepong.areFlagsReady.bind(sitepong);
3231
3775
  var refreshFlags = sitepong.refreshFlags.bind(sitepong);
3232
3776
  var track = sitepong.track.bind(sitepong);
3233
3777
  var trackPageView = sitepong.trackPageView.bind(sitepong);
3234
- var identify = sitepong.identify.bind(sitepong);
3778
+ var identify2 = sitepong.identify.bind(sitepong);
3235
3779
  var group = sitepong.group.bind(sitepong);
3236
3780
  var resetAnalytics = sitepong.resetAnalytics.bind(sitepong);
3237
3781
  var getVisitorId = sitepong.getVisitorId.bind(sitepong);
@@ -5167,11 +5711,11 @@ function getNotificationsPermission() {
5167
5711
  return Notification.permission === "granted";
5168
5712
  }
5169
5713
  function getDeviceAge() {
5170
- const STORAGE_KEY3 = "sitepong_device_age";
5714
+ const STORAGE_KEY5 = "sitepong_device_age";
5171
5715
  const result = { visitCount: 1 };
5172
5716
  if (typeof localStorage === "undefined") return result;
5173
5717
  try {
5174
- const stored = localStorage.getItem(STORAGE_KEY3);
5718
+ const stored = localStorage.getItem(STORAGE_KEY5);
5175
5719
  if (stored) {
5176
5720
  const parsed = JSON.parse(stored);
5177
5721
  result.firstSeen = parsed.firstSeen;
@@ -5179,7 +5723,7 @@ function getDeviceAge() {
5179
5723
  } else {
5180
5724
  result.firstSeen = (/* @__PURE__ */ new Date()).toISOString();
5181
5725
  }
5182
- localStorage.setItem(STORAGE_KEY3, JSON.stringify({
5726
+ localStorage.setItem(STORAGE_KEY5, JSON.stringify({
5183
5727
  firstSeen: result.firstSeen,
5184
5728
  visitCount: result.visitCount
5185
5729
  }));
@@ -5961,14 +6505,18 @@ registerWebManagerFactories({
5961
6505
  createPerformance: (cfg) => new PerformanceManager(cfg)
5962
6506
  });
5963
6507
 
6508
+ exports.DEFAULT_SUPERLINK_ENDPOINT = DEFAULT_SUPERLINK_ENDPOINT;
6509
+ exports.SuperLinkClient = SuperLinkClient;
5964
6510
  exports.TracePropagator = TracePropagator;
5965
6511
  exports.addBreadcrumb = addBreadcrumb;
5966
6512
  exports.areFlagsReady = areFlagsReady;
5967
6513
  exports.captureError = captureError;
5968
6514
  exports.captureMessage = captureMessage;
6515
+ exports.captureWebDeepLink = captureWebDeepLink;
5969
6516
  exports.clearAnonymousId = clearAnonymousId;
5970
6517
  exports.clearUser = clearUser;
5971
6518
  exports.client = sitepong;
6519
+ exports.completeFromScan = completeFromScan;
5972
6520
  exports.createTraceContext = createTraceContext;
5973
6521
  exports.cronCheckin = cronCheckin;
5974
6522
  exports.cronStart = cronStart;
@@ -5978,6 +6526,7 @@ exports.dbTrackSync = dbTrackSync;
5978
6526
  exports.default = src_default;
5979
6527
  exports.endSpan = endSpan;
5980
6528
  exports.endTransaction = endTransaction;
6529
+ exports.extractIdentityMetadata = extractIdentityMetadata;
5981
6530
  exports.extractTrace = extractTrace;
5982
6531
  exports.flush = flush;
5983
6532
  exports.flushMetrics = flushMetrics;
@@ -5988,10 +6537,12 @@ exports.getAllFlags = getAllFlags;
5988
6537
  exports.getAnonymousId = getAnonymousId;
5989
6538
  exports.getDbNPlusOnePatterns = getDbNPlusOnePatterns;
5990
6539
  exports.getDbQueryCount = getDbQueryCount;
6540
+ exports.getDeepLink = getDeepLink;
5991
6541
  exports.getDeviceSignals = getDeviceSignals;
5992
6542
  exports.getFlag = getFlag;
5993
6543
  exports.getFraudCheck = getFraudCheck;
5994
6544
  exports.getLatestProfile = getLatestProfile;
6545
+ exports.getMatchedDeepLink = getMatchedDeepLink;
5995
6546
  exports.getProfiles = getProfiles;
5996
6547
  exports.getRemoteConfig = getRemoteConfig;
5997
6548
  exports.getReplaySessionId = getReplaySessionId;
@@ -6000,9 +6551,10 @@ exports.getVariantPayload = getVariantPayload2;
6000
6551
  exports.getVisitorId = getVisitorId;
6001
6552
  exports.getWebVitals = getWebVitals;
6002
6553
  exports.group = group;
6003
- exports.identify = identify;
6554
+ exports.identify = identify2;
6004
6555
  exports.init = init;
6005
6556
  exports.initRN = initRN;
6557
+ exports.initSuperLink = initSuperLink;
6006
6558
  exports.isInitialized = isInitialized;
6007
6559
  exports.isRemoteConfigFeatureEnabled = isRemoteConfigFeatureEnabled;
6008
6560
  exports.isReplayRecording = isReplayRecording;
@@ -6012,7 +6564,9 @@ exports.metricHistogram = metricHistogram;
6012
6564
  exports.metricIncrement = metricIncrement;
6013
6565
  exports.metricStartTimer = metricStartTimer;
6014
6566
  exports.metricTime = metricTime;
6567
+ exports.onDeferredDeepLink = onDeferredDeepLink;
6015
6568
  exports.onRemoteConfigChange = onRemoteConfigChange;
6569
+ exports.parseUniversalLink = parseUniversalLink;
6016
6570
  exports.profile = profile;
6017
6571
  exports.propagateTrace = propagateTrace;
6018
6572
  exports.refreshFlags = refreshFlags;
@@ -6032,8 +6586,11 @@ exports.startReplay = startReplay;
6032
6586
  exports.startSpan = startSpan;
6033
6587
  exports.startTransaction = startTransaction;
6034
6588
  exports.stopReplay = stopReplay;
6589
+ exports.superlinkClient = superlinkClient;
6590
+ exports.superlinkIdentify = identify;
6035
6591
  exports.track = track;
6036
6592
  exports.trackPageView = trackPageView;
6037
6593
  exports.waitForFlags = waitForFlags;
6594
+ exports.writeClipboardToken = writeClipboardToken;
6038
6595
  //# sourceMappingURL=web.js.map
6039
6596
  //# sourceMappingURL=web.js.map