@nexushub/client 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1315,12 +1315,6 @@ var getVisitorId = async () => {
1315
1315
  };
1316
1316
 
1317
1317
  // src/analytics/vitals.ts
1318
-
1319
-
1320
-
1321
-
1322
-
1323
-
1324
1318
  var _webvitals = require('web-vitals');
1325
1319
  var METRIC_KEY_MAP = {
1326
1320
  CLS: "cls",
@@ -1354,7 +1348,35 @@ var VitalsCollector = class {
1354
1348
  dns_lookup: navEntry.domainLookupEnd - navEntry.domainLookupStart,
1355
1349
  tcp_connect: navEntry.connectEnd - navEntry.connectStart,
1356
1350
  request_time: navEntry.responseEnd - navEntry.requestStart,
1357
- dom_load: navEntry.domComplete - navEntry.domInteractive
1351
+ dom_load: navEntry.domComplete - navEntry.domInteractive,
1352
+ // NEW: raw fields matching Rust NavigationTiming struct
1353
+ domain_lookup_start: navEntry.domainLookupStart,
1354
+ domain_lookup_end: navEntry.domainLookupEnd,
1355
+ connect_start: navEntry.connectStart,
1356
+ connect_end: navEntry.connectEnd,
1357
+ secure_connection_start: navEntry.secureConnectionStart > 0 ? navEntry.secureConnectionStart : void 0
1358
+ };
1359
+ this.hasUpdates = true;
1360
+ }
1361
+ const resourceEntries = performance.getEntriesByType(
1362
+ "resource"
1363
+ );
1364
+ if (resourceEntries.length > 0) {
1365
+ this.metrics.resources = resourceEntries.filter((r) => !r.name.includes("/api/collect")).sort((a, b) => b.duration - a.duration).slice(0, 20).map((r) => ({
1366
+ name: r.name,
1367
+ initiator_type: r.initiatorType,
1368
+ duration: Math.round(r.duration),
1369
+ transfer_size: r.transferSize > 0 ? r.transferSize : void 0,
1370
+ encoded_body_size: r.encodedBodySize > 0 ? r.encodedBodySize : void 0
1371
+ }));
1372
+ this.hasUpdates = true;
1373
+ }
1374
+ const mem = performance.memory;
1375
+ if (mem) {
1376
+ this.metrics.memory_usage = {
1377
+ used_js_heap_size: mem.usedJSHeapSize,
1378
+ total_js_heap_size: mem.totalJSHeapSize,
1379
+ js_heap_size_limit: mem.jsHeapSizeLimit
1358
1380
  };
1359
1381
  this.hasUpdates = true;
1360
1382
  }
@@ -1362,11 +1384,13 @@ var VitalsCollector = class {
1362
1384
  getMetricsSnapshot() {
1363
1385
  if (!this.hasUpdates) return null;
1364
1386
  this.hasUpdates = false;
1365
- return { ...this.metrics };
1387
+ const snapshot = { ...this.metrics };
1388
+ this.metrics.resources = void 0;
1389
+ return snapshot;
1366
1390
  }
1367
1391
  };
1368
1392
  var vitalsCollector = new VitalsCollector();
1369
- var initVitals = (tracker) => {
1393
+ var initVitals = (_tracker) => {
1370
1394
  if (process.env.NODE_ENV === "development") {
1371
1395
  console.log("[NexusHub] Web Vitals monitoring active");
1372
1396
  }
@@ -1485,10 +1509,41 @@ var EventStorage = class {
1485
1509
  var eventStorage = new EventStorage();
1486
1510
 
1487
1511
  // src/analytics/tracker.ts
1512
+ var SDK_VERSION = "0.1.0";
1513
+ var safeGetItem = (key) => {
1514
+ try {
1515
+ return localStorage.getItem(key);
1516
+ } catch (e6) {
1517
+ return null;
1518
+ }
1519
+ };
1520
+ var safeSetItem = (key, value) => {
1521
+ try {
1522
+ localStorage.setItem(key, value);
1523
+ } catch (e7) {
1524
+ }
1525
+ };
1526
+ var safeRemoveItem = (key) => {
1527
+ try {
1528
+ localStorage.removeItem(key);
1529
+ } catch (e8) {
1530
+ }
1531
+ };
1532
+ var generateUUID = () => {
1533
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1534
+ return crypto.randomUUID();
1535
+ }
1536
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1537
+ const r = Math.random() * 16 | 0;
1538
+ const v = c === "x" ? r : r & 3 | 8;
1539
+ return v.toString(16);
1540
+ });
1541
+ };
1488
1542
  var Tracker = class {
1489
1543
  constructor(config) {
1490
1544
  this.sessionId = "";
1491
1545
  this.visitorId = "";
1546
+ this.anonymousId = "";
1492
1547
  this.isFlushing = false;
1493
1548
  this.config = config;
1494
1549
  this.endpoint = `${this.config.analyticsUrl}/api/collect`;
@@ -1501,28 +1556,40 @@ var Tracker = class {
1501
1556
  }
1502
1557
  async initSession() {
1503
1558
  this.visitorId = await getVisitorId();
1504
- let sid = localStorage.getItem("nexus_sid");
1505
- const lastActivity = localStorage.getItem("nexus_last_active");
1559
+ let anonId = safeGetItem("nexus_anon_id");
1560
+ if (!anonId) {
1561
+ anonId = `anon_${generateUUID().replace(/-/g, "")}`;
1562
+ safeSetItem("nexus_anon_id", anonId);
1563
+ }
1564
+ this.anonymousId = anonId;
1565
+ let sid = safeGetItem("nexus_sid");
1566
+ const lastActivity = safeGetItem("nexus_last_active");
1506
1567
  const now = Date.now();
1507
1568
  const SESSION_TIMEOUT = 30 * 60 * 1e3;
1508
1569
  const isExpired = !sid || !lastActivity || now - parseInt(lastActivity, 10) > SESSION_TIMEOUT;
1509
1570
  if (isExpired) {
1510
- const uuid = crypto.randomUUID().replace(/-/g, "").substring(0, 16);
1571
+ const uuid = generateUUID().replace(/-/g, "").substring(0, 16);
1511
1572
  sid = `sess_${uuid}_${now}`;
1512
- localStorage.setItem("nexus_sid", sid);
1573
+ safeSetItem("nexus_sid", sid);
1513
1574
  this.sessionStart = now;
1514
1575
  }
1515
- localStorage.setItem("nexus_last_active", now.toString());
1576
+ safeSetItem("nexus_last_active", now.toString());
1516
1577
  this.sessionId = sid;
1517
1578
  }
1518
- async send(eventType, data = {}, eventName) {
1579
+ async send(eventType, data = {}, eventName, ecommerce) {
1519
1580
  if (typeof window === "undefined") return;
1520
- localStorage.setItem("nexus_last_active", Date.now().toString());
1581
+ safeSetItem("nexus_last_active", Date.now().toString());
1521
1582
  const entropy = await getDeviceEntropy();
1522
1583
  const perfMetrics = vitalsCollector.getMetricsSnapshot();
1584
+ const utmParams = extractUtmParams(window.location.href);
1523
1585
  const payload = {
1524
1586
  projectId: this.config.projectId,
1525
1587
  sessionId: this.sessionId,
1588
+ visitorId: this.visitorId,
1589
+ anonymousId: this.anonymousId,
1590
+ messageId: generateUUID(),
1591
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
1592
+ version: SDK_VERSION,
1526
1593
  url: window.location.href,
1527
1594
  referrer: document.referrer,
1528
1595
  userAgent: window.navigator.userAgent,
@@ -1533,6 +1600,7 @@ var Tracker = class {
1533
1600
  eventType,
1534
1601
  eventName,
1535
1602
  eventData: data,
1603
+ ecommerce: _nullishCoalesce(ecommerce, () => ( void 0)),
1536
1604
  performance: perfMetrics || void 0,
1537
1605
  context: {
1538
1606
  device: {
@@ -1542,7 +1610,8 @@ var Tracker = class {
1542
1610
  canvasFingerprint: entropy.canvas_hash,
1543
1611
  platform: entropy.platform
1544
1612
  },
1545
- visitorIdLocal: this.visitorId
1613
+ visitorIdLocal: this.visitorId,
1614
+ utm: utmParams
1546
1615
  },
1547
1616
  clientTimestamp: (/* @__PURE__ */ new Date()).toISOString()
1548
1617
  };
@@ -1610,9 +1679,19 @@ var Tracker = class {
1610
1679
  getSession() {
1611
1680
  return this.sessionId;
1612
1681
  }
1682
+ getVisitorId() {
1683
+ return this.visitorId;
1684
+ }
1685
+ getAnonymousId() {
1686
+ return this.anonymousId;
1687
+ }
1613
1688
  getSessionDuration() {
1614
1689
  return Date.now() - this.sessionStart;
1615
1690
  }
1691
+ clearIdentity() {
1692
+ safeRemoveItem("nexus_anon_id");
1693
+ this.anonymousId = "";
1694
+ }
1616
1695
  stop() {
1617
1696
  if (this.flushInterval) {
1618
1697
  clearInterval(this.flushInterval);
@@ -1620,13 +1699,42 @@ var Tracker = class {
1620
1699
  this.flushQueue(true);
1621
1700
  }
1622
1701
  };
1702
+ function extractUtmParams(url) {
1703
+ try {
1704
+ const params = new URL(url).searchParams;
1705
+ const utmKeys = [
1706
+ "utm_source",
1707
+ "utm_medium",
1708
+ "utm_campaign",
1709
+ "utm_term",
1710
+ "utm_content"
1711
+ ];
1712
+ const result = {};
1713
+ utmKeys.forEach((key) => {
1714
+ const val = params.get(key);
1715
+ if (val) result[key] = val;
1716
+ });
1717
+ return result;
1718
+ } catch (e9) {
1719
+ return {};
1720
+ }
1721
+ }
1623
1722
 
1624
1723
  // src/analytics/index.ts
1724
+ var safeRemoveItem2 = (key) => {
1725
+ try {
1726
+ localStorage.removeItem(key);
1727
+ } catch (e10) {
1728
+ }
1729
+ };
1625
1730
  var AnalyticsEngine = class {
1626
1731
  constructor(config) {
1627
1732
  this.cleanupFns = [];
1628
1733
  this.isInitialized = false;
1734
+ this.maxScrollDepth = 0;
1735
+ this.scrollThresholdsFired = /* @__PURE__ */ new Set();
1629
1736
  this.lastPath = typeof window !== "undefined" ? window.location.pathname : "";
1737
+ this.clickBuffer = [];
1630
1738
  this.tracker = new Tracker(config);
1631
1739
  }
1632
1740
  start() {
@@ -1638,6 +1746,10 @@ var AnalyticsEngine = class {
1638
1746
  this.setupFormTracking();
1639
1747
  this.setupRouteTracking();
1640
1748
  this.setupShareTracking();
1749
+ this.setupScrollTracking();
1750
+ this.setupOutboundTracking();
1751
+ this.setupVideoTracking();
1752
+ this.setupErrorTracking();
1641
1753
  if (process.env.NODE_ENV === "development") {
1642
1754
  console.log("[NexusHub] \u{1F680} Analytics Engine Started");
1643
1755
  }
@@ -1645,17 +1757,17 @@ var AnalyticsEngine = class {
1645
1757
  pageView(customReferrer) {
1646
1758
  if (typeof window === "undefined") return;
1647
1759
  const currentPath = window.location.pathname;
1760
+ this.maxScrollDepth = 0;
1761
+ this.scrollThresholdsFired.clear();
1648
1762
  this.tracker.send("page_view", {
1649
1763
  path: currentPath,
1650
1764
  search: window.location.search,
1651
1765
  title: document.title,
1652
1766
  timezone_offset: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
1653
- // Use the virtual path as referrer if navigating internally
1654
1767
  referrer: customReferrer || (this.lastPath !== currentPath ? this.lastPath : document.referrer)
1655
1768
  });
1656
1769
  this.lastPath = currentPath;
1657
1770
  }
1658
- // --- SOCIAL / DARK SOCIAL TRACKING ---
1659
1771
  setupShareTracking() {
1660
1772
  if (typeof window === "undefined") return;
1661
1773
  const copyHandler = () => {
@@ -1681,92 +1793,36 @@ var AnalyticsEngine = class {
1681
1793
  });
1682
1794
  }
1683
1795
  }
1684
- // --- IDENTITY & B2B ---
1685
- async identify(userId, traits = {}) {
1686
- return this.sendIdentityRequest("identify", { user_id: userId, traits });
1687
- }
1688
- async group(groupId, traits = {}) {
1689
- return this.sendIdentityRequest("group", { group_id: groupId, traits });
1690
- }
1691
- async alias(newId) {
1692
- return this.sendIdentityRequest("alias", {
1693
- previous_id: this.tracker.getSession(),
1694
- user_id: newId
1695
- });
1696
- }
1697
- reset(performGdprScrub = false) {
1698
- const config = this.tracker.config;
1699
- this.tracker.stop();
1700
- localStorage.removeItem("nexus_sid");
1701
- localStorage.removeItem("nexus_vid");
1702
- if (performGdprScrub) {
1703
- const endpoint = `${config.analyticsUrl}/api/privacy/scrub`;
1704
- const userId = this.tracker.visitorId;
1705
- fetch(endpoint, {
1706
- method: "DELETE",
1707
- headers: {
1708
- "Content-Type": "application/json",
1709
- Authorization: `Bearer ${config.apiKey}`
1710
- },
1711
- body: JSON.stringify({
1712
- project_id: config.projectId,
1713
- user_id: userId
1714
- })
1715
- }).catch((err) => console.error("[NexusHub] Privacy scrub failed:", err));
1716
- }
1717
- window.location.reload();
1718
- }
1719
- // --- E-COMMERCE & CUSTOM ---
1720
- track(eventName, properties = {}) {
1721
- this.tracker.send(
1722
- "custom_event",
1723
- { event_name: eventName, ...properties },
1724
- eventName
1725
- );
1726
- }
1727
- trackPurchase(orderData) {
1728
- this.tracker.send("purchase", {
1729
- ...orderData,
1730
- currency: orderData.currency || "USD",
1731
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
1732
- });
1733
- }
1734
- trackError(error, context) {
1735
- this.tracker.send("error", {
1736
- message: error.message,
1737
- stack: error.stack,
1738
- context,
1739
- url: typeof window !== "undefined" ? window.location.href : ""
1740
- });
1741
- }
1742
- // --- INTERNAL ---
1743
- async sendIdentityRequest(type, data) {
1744
- try {
1745
- const config = this.tracker.config;
1746
- const endpoint = `${config.analyticsUrl}/api/${type}`;
1747
- const payload = {
1748
- projectId: config.projectId,
1749
- sessionId: this.tracker.getSession(),
1750
- visitorId: this.tracker.visitorId,
1751
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1752
- ...data
1753
- };
1754
- const response = await fetch(endpoint, {
1755
- method: "POST",
1756
- headers: {
1757
- "Content-Type": "application/json",
1758
- Authorization: `Bearer ${config.apiKey}`
1759
- },
1760
- body: JSON.stringify(payload)
1761
- });
1762
- if (response.ok) {
1763
- return true;
1796
+ setupScrollTracking() {
1797
+ if (typeof window === "undefined") return;
1798
+ const THRESHOLDS = [25, 50, 75, 100];
1799
+ const scrollHandler = () => {
1800
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
1801
+ const docHeight = document.documentElement.scrollHeight - document.documentElement.clientHeight;
1802
+ if (docHeight <= 0) return;
1803
+ const pct = Math.round(scrollTop / docHeight * 100);
1804
+ if (pct > this.maxScrollDepth) {
1805
+ this.maxScrollDepth = pct;
1764
1806
  }
1765
- return false;
1766
- } catch (error) {
1767
- console.error(`[NexusHub] ${type} failed:`, error);
1768
- return false;
1769
- }
1807
+ clearTimeout(this.scrollTimer);
1808
+ this.scrollTimer = setTimeout(() => {
1809
+ for (const threshold of THRESHOLDS) {
1810
+ if (this.maxScrollDepth >= threshold && !this.scrollThresholdsFired.has(threshold)) {
1811
+ this.scrollThresholdsFired.add(threshold);
1812
+ this.tracker.send("scroll_depth", {
1813
+ depth: threshold / 100,
1814
+ depth_percent: threshold,
1815
+ path: window.location.pathname
1816
+ });
1817
+ }
1818
+ }
1819
+ }, 300);
1820
+ };
1821
+ window.addEventListener("scroll", scrollHandler, { passive: true });
1822
+ this.cleanupFns.push(() => {
1823
+ window.removeEventListener("scroll", scrollHandler);
1824
+ clearTimeout(this.scrollTimer);
1825
+ });
1770
1826
  }
1771
1827
  setupClickTracking() {
1772
1828
  if (typeof window === "undefined") return;
@@ -1794,6 +1850,55 @@ var AnalyticsEngine = class {
1794
1850
  coordinates: { x: e.clientX, y: e.clientY }
1795
1851
  });
1796
1852
  }
1853
+ const now = Date.now();
1854
+ const ZONE = 400;
1855
+ const WINDOW_MS = 1e3;
1856
+ const RAGE_THRESHOLD = 3;
1857
+ this.clickBuffer.push({ x: e.clientX, y: e.clientY, t: now });
1858
+ this.clickBuffer = this.clickBuffer.filter((c) => now - c.t < WINDOW_MS);
1859
+ const zone = {
1860
+ x: Math.floor(e.clientX / ZONE),
1861
+ y: Math.floor(e.clientY / ZONE)
1862
+ };
1863
+ const zoneClicks = this.clickBuffer.filter(
1864
+ (c) => Math.floor(c.x / ZONE) === zone.x && Math.floor(c.y / ZONE) === zone.y
1865
+ );
1866
+ if (zoneClicks.length >= RAGE_THRESHOLD) {
1867
+ this.tracker.send(
1868
+ "click",
1869
+ {
1870
+ element_type: target.tagName.toLowerCase(),
1871
+ event_name: "rage_click",
1872
+ coordinates: { x: e.clientX, y: e.clientY },
1873
+ click_count: zoneClicks.length,
1874
+ path: window.location.pathname
1875
+ },
1876
+ "rage_click"
1877
+ );
1878
+ this.clickBuffer = [];
1879
+ }
1880
+ const isInteractive = target.closest(
1881
+ "a, button, input, select, textarea, [onclick], [role='button']"
1882
+ );
1883
+ if (!isInteractive) {
1884
+ const pathBefore = window.location.pathname;
1885
+ setTimeout(() => {
1886
+ const pathAfter = window.location.pathname;
1887
+ if (pathBefore === pathAfter) {
1888
+ this.tracker.send(
1889
+ "click",
1890
+ {
1891
+ element_type: target.tagName.toLowerCase(),
1892
+ event_name: "dead_click",
1893
+ selector: getSelector(target),
1894
+ coordinates: { x: e.clientX, y: e.clientY },
1895
+ path: window.location.pathname
1896
+ },
1897
+ "dead_click"
1898
+ );
1899
+ }
1900
+ }, 300);
1901
+ }
1797
1902
  };
1798
1903
  window.addEventListener("click", clickHandler, { passive: true });
1799
1904
  this.cleanupFns.push(
@@ -1818,6 +1923,132 @@ var AnalyticsEngine = class {
1818
1923
  () => document.removeEventListener("submit", submitHandler)
1819
1924
  );
1820
1925
  }
1926
+ setupOutboundTracking() {
1927
+ if (typeof window === "undefined") return;
1928
+ const outboundHandler = (e) => {
1929
+ const link = e.target.closest("a");
1930
+ if (!link || !link.href) return;
1931
+ try {
1932
+ const linkHost = new URL(link.href).hostname;
1933
+ if (linkHost && linkHost !== window.location.hostname) {
1934
+ this.tracker.send(
1935
+ "click",
1936
+ {
1937
+ event_name: "outbound_click",
1938
+ href: link.href,
1939
+ text: _optionalChain([link, 'access', _35 => _35.innerText, 'optionalAccess', _36 => _36.substring, 'call', _37 => _37(0, 50)]),
1940
+ destination_host: linkHost,
1941
+ coordinates: { x: e.clientX, y: e.clientY }
1942
+ },
1943
+ "outbound_click"
1944
+ );
1945
+ }
1946
+ } catch (e11) {
1947
+ }
1948
+ };
1949
+ window.addEventListener("click", outboundHandler, { passive: true });
1950
+ this.cleanupFns.push(
1951
+ () => window.removeEventListener("click", outboundHandler)
1952
+ );
1953
+ }
1954
+ setupVideoTracking() {
1955
+ if (typeof document === "undefined") return;
1956
+ const attachVideoListeners = (video) => {
1957
+ if (video.__nexus_tracked) return;
1958
+ video.__nexus_tracked = true;
1959
+ const src = video.src || video.currentSrc || "unknown";
1960
+ let milestone50Fired = false;
1961
+ video.addEventListener("play", () => {
1962
+ this.tracker.send(
1963
+ "custom_event",
1964
+ { event_name: "video_play", src },
1965
+ "video_play"
1966
+ );
1967
+ });
1968
+ video.addEventListener("pause", () => {
1969
+ this.tracker.send(
1970
+ "custom_event",
1971
+ {
1972
+ event_name: "video_pause",
1973
+ src,
1974
+ position_seconds: Math.round(video.currentTime)
1975
+ },
1976
+ "video_pause"
1977
+ );
1978
+ });
1979
+ video.addEventListener("timeupdate", () => {
1980
+ if (!video.duration || video.duration === Infinity) return;
1981
+ const pct = video.currentTime / video.duration;
1982
+ if (pct >= 0.5 && !milestone50Fired) {
1983
+ milestone50Fired = true;
1984
+ this.tracker.send(
1985
+ "custom_event",
1986
+ {
1987
+ event_name: "video_50_percent",
1988
+ src
1989
+ },
1990
+ "video_50_percent"
1991
+ );
1992
+ }
1993
+ });
1994
+ video.addEventListener("ended", () => {
1995
+ this.tracker.send(
1996
+ "custom_event",
1997
+ {
1998
+ event_name: "video_complete",
1999
+ src,
2000
+ duration_seconds: Math.round(video.duration)
2001
+ },
2002
+ "video_complete"
2003
+ );
2004
+ });
2005
+ };
2006
+ document.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2007
+ const observer = new MutationObserver((mutations) => {
2008
+ mutations.forEach((m) => {
2009
+ m.addedNodes.forEach((node) => {
2010
+ if (node instanceof HTMLVideoElement) {
2011
+ attachVideoListeners(node);
2012
+ }
2013
+ if (node instanceof Element) {
2014
+ node.querySelectorAll("video").forEach((v) => attachVideoListeners(v));
2015
+ }
2016
+ });
2017
+ });
2018
+ });
2019
+ observer.observe(document.body, { childList: true, subtree: true });
2020
+ this.cleanupFns.push(() => observer.disconnect());
2021
+ }
2022
+ setupErrorTracking() {
2023
+ if (typeof window === "undefined") return;
2024
+ const errorHandler = (e) => {
2025
+ this.tracker.send("error", {
2026
+ message: e.message,
2027
+ filename: e.filename,
2028
+ lineno: e.lineno,
2029
+ colno: e.colno,
2030
+ stack: _optionalChain([e, 'access', _38 => _38.error, 'optionalAccess', _39 => _39.stack]),
2031
+ url: window.location.href,
2032
+ type: "uncaught_error"
2033
+ });
2034
+ };
2035
+ const rejectionHandler = (e) => {
2036
+ const reason = e.reason instanceof Error ? e.reason.message : String(e.reason);
2037
+ const stack = e.reason instanceof Error ? e.reason.stack : void 0;
2038
+ this.tracker.send("error", {
2039
+ message: reason,
2040
+ stack,
2041
+ url: window.location.href,
2042
+ type: "unhandled_rejection"
2043
+ });
2044
+ };
2045
+ window.addEventListener("error", errorHandler);
2046
+ window.addEventListener("unhandledrejection", rejectionHandler);
2047
+ this.cleanupFns.push(() => {
2048
+ window.removeEventListener("error", errorHandler);
2049
+ window.removeEventListener("unhandledrejection", rejectionHandler);
2050
+ });
2051
+ }
1821
2052
  setupRouteTracking() {
1822
2053
  if (typeof window === "undefined" || typeof window.history === "undefined")
1823
2054
  return;
@@ -1845,10 +2076,108 @@ var AnalyticsEngine = class {
1845
2076
  window.removeEventListener("popstate", popStateHandler);
1846
2077
  });
1847
2078
  }
1848
- // Simple flag to check if NexusProvider is active
1849
2079
  isInitializedByReactProvider() {
1850
2080
  return !!document.getElementById("__nexus_react_active");
1851
2081
  }
2082
+ async identify(userId, traits = {}) {
2083
+ return this.sendIdentityRequest("identify", { user_id: userId, traits });
2084
+ }
2085
+ async group(groupId, traits = {}) {
2086
+ return this.sendIdentityRequest("group", { group_id: groupId, traits });
2087
+ }
2088
+ async alias(newId) {
2089
+ return this.sendIdentityRequest("alias", {
2090
+ previous_id: this.tracker.getSession(),
2091
+ user_id: newId
2092
+ });
2093
+ }
2094
+ reset(performGdprScrub = false) {
2095
+ const config = this.tracker.config;
2096
+ this.tracker.stop();
2097
+ localStorage.removeItem("nexus_sid");
2098
+ localStorage.removeItem("nexus_vid");
2099
+ safeRemoveItem2("nexus_anon_id");
2100
+ this.tracker.clearIdentity();
2101
+ if (performGdprScrub) {
2102
+ const endpoint = `${config.analyticsUrl}/api/privacy/scrub`;
2103
+ const userId = this.tracker.getVisitorId();
2104
+ fetch(endpoint, {
2105
+ method: "DELETE",
2106
+ headers: {
2107
+ "Content-Type": "application/json",
2108
+ Authorization: `Bearer ${config.apiKey}`
2109
+ },
2110
+ body: JSON.stringify({
2111
+ project_id: config.projectId,
2112
+ user_id: userId
2113
+ })
2114
+ }).catch((err) => console.error("[NexusHub] Privacy scrub failed:", err));
2115
+ }
2116
+ window.location.reload();
2117
+ }
2118
+ track(eventName, properties = {}) {
2119
+ this.tracker.send(
2120
+ "custom_event",
2121
+ { event_name: eventName, ...properties },
2122
+ eventName
2123
+ );
2124
+ }
2125
+ trackPurchase(orderData) {
2126
+ const ecommerce = {
2127
+ orderId: orderData.orderId,
2128
+ total: orderData.total,
2129
+ revenue: _nullishCoalesce(orderData.revenue, () => ( orderData.total)),
2130
+ currency: orderData.currency || "USD",
2131
+ products: orderData.products.map((p) => ({
2132
+ productId: p.id,
2133
+ sku: p.sku,
2134
+ name: p.name,
2135
+ price: p.price,
2136
+ quantity: p.quantity
2137
+ }))
2138
+ };
2139
+ this.tracker.send(
2140
+ "purchase",
2141
+ { order_id: orderData.orderId },
2142
+ "purchase",
2143
+ ecommerce
2144
+ );
2145
+ }
2146
+ trackError(error, context) {
2147
+ this.tracker.send("error", {
2148
+ message: error.message,
2149
+ stack: error.stack,
2150
+ context,
2151
+ url: typeof window !== "undefined" ? window.location.href : "",
2152
+ type: "manual"
2153
+ });
2154
+ }
2155
+ async sendIdentityRequest(type, data) {
2156
+ try {
2157
+ const config = this.tracker.config;
2158
+ const endpoint = `${config.analyticsUrl}/api/${type}`;
2159
+ const payload = {
2160
+ projectId: config.projectId,
2161
+ sessionId: this.tracker.getSession(),
2162
+ visitorId: this.tracker.getVisitorId(),
2163
+ anonymousId: this.tracker.getAnonymousId(),
2164
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2165
+ ...data
2166
+ };
2167
+ const response = await fetch(endpoint, {
2168
+ method: "POST",
2169
+ headers: {
2170
+ "Content-Type": "application/json",
2171
+ Authorization: `Bearer ${config.apiKey}`
2172
+ },
2173
+ body: JSON.stringify(payload)
2174
+ });
2175
+ return response.ok;
2176
+ } catch (error) {
2177
+ console.error(`[NexusHub] ${type} failed:`, error);
2178
+ return false;
2179
+ }
2180
+ }
1852
2181
  getSessionId() {
1853
2182
  return this.tracker.getSession();
1854
2183
  }
@@ -1858,24 +2187,34 @@ var AnalyticsEngine = class {
1858
2187
  if (isFinalShutdown) {
1859
2188
  this.tracker.send("session_end", {
1860
2189
  session_id: this.tracker.getSession(),
1861
- duration: this.tracker.getSessionDuration()
2190
+ duration: this.tracker.getSessionDuration(),
2191
+ max_scroll_depth: this.maxScrollDepth / 100
1862
2192
  });
1863
2193
  }
1864
2194
  this.tracker.stop();
1865
2195
  this.isInitialized = false;
1866
2196
  }
1867
2197
  };
2198
+ function getSelector(el, depth = 0) {
2199
+ if (!el || depth > 2) return "";
2200
+ const tag = el.tagName.toLowerCase();
2201
+ const id = el.id ? `#${el.id}` : "";
2202
+ const cls = el.className && typeof el.className === "string" ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
2203
+ const self = `${tag}${id}${cls}`;
2204
+ const parent = el.parentElement && depth < 2 ? `${getSelector(el.parentElement, depth + 1)} > ` : "";
2205
+ return `${parent}${self}`;
2206
+ }
1868
2207
 
1869
2208
  // src/client.ts
1870
2209
  var NexusClient = class {
1871
2210
  constructor(config) {
1872
2211
  const fullConfig = getFullConfig(config);
1873
2212
  this.config = {
1874
- debug: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _35 => _35.debug]), () => ( false)),
1875
- cacheStrategy: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _36 => _36.cacheStrategy]), () => ( "memory")),
1876
- revalidateTime: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _37 => _37.revalidateTime]), () => ( 60)),
1877
- timeout: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _38 => _38.timeout]), () => ( 1e4)),
1878
- retries: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _39 => _39.retries]), () => ( 3)),
2213
+ debug: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _40 => _40.debug]), () => ( false)),
2214
+ cacheStrategy: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _41 => _41.cacheStrategy]), () => ( "memory")),
2215
+ revalidateTime: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _42 => _42.revalidateTime]), () => ( 60)),
2216
+ timeout: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _43 => _43.timeout]), () => ( 1e4)),
2217
+ retries: _nullishCoalesce(_optionalChain([config, 'optionalAccess', _44 => _44.retries]), () => ( 3)),
1879
2218
  ...fullConfig
1880
2219
  };
1881
2220
  const errors = validateConfig(this.config);
@@ -1922,6 +2261,8 @@ var createNexusClient = (config) => new NexusClient(config);
1922
2261
 
1923
2262
 
1924
2263
 
2264
+
2265
+
1925
2266
  var _react = require('react'); var _react2 = _interopRequireDefault(_react);
1926
2267
  var _navigation = require('next/navigation');
1927
2268
 
@@ -2111,7 +2452,7 @@ async function parseError(res) {
2111
2452
  code: json.code || "UNKNOWN_ERROR",
2112
2453
  message: json.message || "An error occurred during authentication"
2113
2454
  };
2114
- } catch (e6) {
2455
+ } catch (e12) {
2115
2456
  return {
2116
2457
  status: res.status,
2117
2458
  code: "NETWORK_ERROR",
@@ -2123,6 +2464,10 @@ async function parseError(res) {
2123
2464
  // src/components/NexusProvider.tsx
2124
2465
 
2125
2466
  var NexusContext = _react.createContext.call(void 0, nexus);
2467
+ var NexusLiveFeedContext = _react.createContext.call(void 0, {
2468
+ latestEvent: null,
2469
+ isConnected: false
2470
+ });
2126
2471
  function NexusAnalyticsTracker({
2127
2472
  disableAnalytics
2128
2473
  }) {
@@ -2141,9 +2486,24 @@ function NexusAnalyticsTracker({
2141
2486
  var NexusProvider = ({
2142
2487
  children,
2143
2488
  projectId,
2144
- disableAnalytics = false
2489
+ disableAnalytics = false,
2490
+ hasConsent = true,
2491
+ // NEW: default true to preserve existing behaviour
2492
+ enableLiveFeed = false,
2493
+ // NEW
2494
+ onLiveEvent
2495
+ // NEW
2145
2496
  }) => {
2146
2497
  const isInitialized = _react.useRef.call(void 0, false);
2498
+ const socketRef = _react.useRef.call(void 0, null);
2499
+ const [latestEvent, setLatestEvent] = _react.useState.call(void 0,
2500
+ null
2501
+ );
2502
+ const [isConnected, setIsConnected] = _react.useState.call(void 0, false);
2503
+ const liveFeedValue = _react.useMemo.call(void 0,
2504
+ () => ({ latestEvent, isConnected }),
2505
+ [latestEvent, isConnected]
2506
+ );
2147
2507
  const config = _react.useMemo.call(void 0, () => {
2148
2508
  if (projectId && nexus.getConfig().projectId !== projectId) {
2149
2509
  nexus.updateConfig({ projectId });
@@ -2151,7 +2511,8 @@ var NexusProvider = ({
2151
2511
  return nexus.getConfig();
2152
2512
  }, [projectId]);
2153
2513
  _react.useEffect.call(void 0, () => {
2154
- if (typeof window === "undefined" || disableAnalytics) return;
2514
+ if (typeof window === "undefined" || disableAnalytics || !hasConsent)
2515
+ return;
2155
2516
  if (!isInitialized.current) {
2156
2517
  if (!nexus.analytics) {
2157
2518
  nexus.analytics = new AnalyticsEngine(nexus.getConfig());
@@ -2168,11 +2529,53 @@ var NexusProvider = ({
2168
2529
  isInitialized.current = false;
2169
2530
  }
2170
2531
  };
2171
- }, [disableAnalytics]);
2172
- return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, NexusContext.Provider, { value: nexus, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, AuthProvider, { config, children: [
2532
+ }, [disableAnalytics, hasConsent]);
2533
+ const connectLiveFeed = _react.useCallback.call(void 0, async () => {
2534
+ if (typeof window === "undefined" || !enableLiveFeed) return;
2535
+ try {
2536
+ const { io } = await Promise.resolve().then(() => _interopRequireWildcard(require("socket.io-client")));
2537
+ const cfg = nexus.getConfig();
2538
+ const wsUrl = cfg.apiUrl || "http://localhost:3001";
2539
+ const socket = io(`${wsUrl}/analytics`, {
2540
+ auth: { token: cfg.apiKey },
2541
+ query: { projectId: cfg.projectId },
2542
+ transports: ["websocket"],
2543
+ reconnectionAttempts: 5,
2544
+ reconnectionDelay: 2e3
2545
+ });
2546
+ socket.on("connect", () => {
2547
+ setIsConnected(true);
2548
+ if (cfg.debug) console.log("[NexusHub] \u{1F534} Live feed connected");
2549
+ });
2550
+ socket.on("disconnect", () => {
2551
+ setIsConnected(false);
2552
+ if (cfg.debug) console.log("[NexusHub] Live feed disconnected");
2553
+ });
2554
+ socket.on("live_event", (event) => {
2555
+ setLatestEvent(event);
2556
+ _optionalChain([onLiveEvent, 'optionalCall', _45 => _45(event)]);
2557
+ });
2558
+ socketRef.current = socket;
2559
+ } catch (err) {
2560
+ console.error("[NexusHub] Live feed connection failed:", err);
2561
+ }
2562
+ }, [enableLiveFeed, onLiveEvent]);
2563
+ _react.useEffect.call(void 0, () => {
2564
+ if (enableLiveFeed) {
2565
+ connectLiveFeed();
2566
+ }
2567
+ return () => {
2568
+ if (socketRef.current) {
2569
+ socketRef.current.disconnect();
2570
+ socketRef.current = null;
2571
+ setIsConnected(false);
2572
+ }
2573
+ };
2574
+ }, [enableLiveFeed, connectLiveFeed]);
2575
+ return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, NexusContext.Provider, { value: nexus, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, NexusLiveFeedContext.Provider, { value: liveFeedValue, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, AuthProvider, { config, children: [
2173
2576
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _react.Suspense, { fallback: null, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, NexusAnalyticsTracker, { disableAnalytics }) }),
2174
2577
  children
2175
- ] }) });
2578
+ ] }) }) });
2176
2579
  };
2177
2580
  var useNexus = () => {
2178
2581
  const context = _react2.default.useContext(NexusContext);