@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 +520 -117
- package/dist/index.d.cts +38 -32
- package/dist/index.d.ts +38 -32
- package/dist/index.js +515 -112
- package/package.json +2 -1
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
|
-
|
|
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 = (
|
|
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
|
|
1505
|
-
|
|
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 =
|
|
1571
|
+
const uuid = generateUUID().replace(/-/g, "").substring(0, 16);
|
|
1511
1572
|
sid = `sess_${uuid}_${now}`;
|
|
1512
|
-
|
|
1573
|
+
safeSetItem("nexus_sid", sid);
|
|
1513
1574
|
this.sessionStart = now;
|
|
1514
1575
|
}
|
|
1515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
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
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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',
|
|
1875
|
-
cacheStrategy: _nullishCoalesce(_optionalChain([config, 'optionalAccess',
|
|
1876
|
-
revalidateTime: _nullishCoalesce(_optionalChain([config, 'optionalAccess',
|
|
1877
|
-
timeout: _nullishCoalesce(_optionalChain([config, 'optionalAccess',
|
|
1878
|
-
retries: _nullishCoalesce(_optionalChain([config, 'optionalAccess',
|
|
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 (
|
|
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)
|
|
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
|
-
|
|
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);
|