@nexushub/client 0.3.1 → 0.3.3

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