@mushi-mushi/react-native 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.cjs +408 -14
- package/dist/index.d.cts +59 -2
- package/dist/index.d.ts +59 -2
- package/dist/index.js +405 -6
- package/package.json +9 -17
- package/CHANGELOG.md +0 -1062
- package/CODE_OF_CONDUCT.md +0 -51
- package/CONTRIBUTING.md +0 -226
- package/SECURITY.md +0 -310
package/README.md
CHANGED
|
@@ -182,4 +182,4 @@ MIT
|
|
|
182
182
|
<!-- mushi-readme-stats-footer -->
|
|
183
183
|
---
|
|
184
184
|
|
|
185
|
-
<sub>Monorepo scale (July 2026):
|
|
185
|
+
<sub>Monorepo scale (July 2026): 59 edge functions · 374 SQL migrations · 13 outbound plugins · 11 inbound adapters · 19 pipeline agents. Canonical counts: <a href="https://github.com/kensaurus/mushi-mushi/blob/master/docs/stats.md">docs/stats.md</a> · <code>pnpm docs-stats</code></sub>
|
package/dist/index.cjs
CHANGED
|
@@ -49,7 +49,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
49
49
|
|
|
50
50
|
// src/provider.tsx
|
|
51
51
|
var import_react4 = require("react");
|
|
52
|
-
var
|
|
52
|
+
var import_react_native5 = require("react-native");
|
|
53
|
+
var import_core8 = require("@mushi-mushi/core");
|
|
53
54
|
|
|
54
55
|
// src/capture/console-capture.ts
|
|
55
56
|
var import_core = require("@mushi-mushi/core");
|
|
@@ -1479,13 +1480,336 @@ var s3 = import_react_native4.StyleSheet.create({
|
|
|
1479
1480
|
}
|
|
1480
1481
|
});
|
|
1481
1482
|
|
|
1483
|
+
// src/analytics/event-tracker.ts
|
|
1484
|
+
var import_core6 = require("@mushi-mushi/core");
|
|
1485
|
+
var CONSENT_BUFFER_MAX = 50;
|
|
1486
|
+
var SPILL_MAX = 200;
|
|
1487
|
+
var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1488
|
+
var SURFACE = "mobile";
|
|
1489
|
+
async function defaultStorage() {
|
|
1490
|
+
try {
|
|
1491
|
+
const mod = await import("@react-native-async-storage/async-storage");
|
|
1492
|
+
return mod.default;
|
|
1493
|
+
} catch {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
function createRNEventTracker(opts) {
|
|
1498
|
+
const cfg = opts.config ?? {};
|
|
1499
|
+
const enabled = cfg.enabled !== false;
|
|
1500
|
+
const consentMode = cfg.consent ?? "implied";
|
|
1501
|
+
const allowlist = cfg.propertyAllowlist ?? [];
|
|
1502
|
+
const spillKey = `@mushi:events_spill_${opts.projectId}`;
|
|
1503
|
+
const consentKey = `@mushi:analytics_consent_${opts.projectId}`;
|
|
1504
|
+
let buffer = [];
|
|
1505
|
+
let consentBuffer = [];
|
|
1506
|
+
let consent = consentMode === "required" ? "pending" : "granted";
|
|
1507
|
+
let userId = null;
|
|
1508
|
+
let userTraits = null;
|
|
1509
|
+
let pendingIdentify = false;
|
|
1510
|
+
let flushing = false;
|
|
1511
|
+
let destroyed = false;
|
|
1512
|
+
let timer = null;
|
|
1513
|
+
let storage = null;
|
|
1514
|
+
const consentListeners = /* @__PURE__ */ new Set();
|
|
1515
|
+
const loadStorage = opts.storage ?? defaultStorage;
|
|
1516
|
+
const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
|
|
1517
|
+
async function readSpill() {
|
|
1518
|
+
if (!storage) return [];
|
|
1519
|
+
try {
|
|
1520
|
+
const raw = await storage.getItem(spillKey);
|
|
1521
|
+
if (!raw) return [];
|
|
1522
|
+
const parsed = JSON.parse(raw);
|
|
1523
|
+
if (!parsed || Date.now() - parsed.at > SPILL_TTL_MS) {
|
|
1524
|
+
await storage.removeItem(spillKey);
|
|
1525
|
+
return [];
|
|
1526
|
+
}
|
|
1527
|
+
return Array.isArray(parsed.events) ? parsed.events.slice(0, SPILL_MAX) : [];
|
|
1528
|
+
} catch {
|
|
1529
|
+
return [];
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
async function writeSpill(events) {
|
|
1533
|
+
if (!storage) return;
|
|
1534
|
+
try {
|
|
1535
|
+
if (events.length === 0) await storage.removeItem(spillKey);
|
|
1536
|
+
else await storage.setItem(spillKey, JSON.stringify({ at: Date.now(), events: events.slice(-SPILL_MAX) }));
|
|
1537
|
+
} catch {
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
async function flushNow() {
|
|
1541
|
+
if (destroyed && buffer.length === 0) return;
|
|
1542
|
+
await ready;
|
|
1543
|
+
if (flushing || consent !== "granted") return;
|
|
1544
|
+
if (buffer.length === 0 && !pendingIdentify) return;
|
|
1545
|
+
const client = resolveClient();
|
|
1546
|
+
if (!client) return;
|
|
1547
|
+
flushing = true;
|
|
1548
|
+
const batch = buffer.splice(0, import_core6.EVENT_PROPERTY_LIMITS.maxServerBatch);
|
|
1549
|
+
if (pendingIdentify) {
|
|
1550
|
+
batch.unshift({ name: "identify", ts: (/* @__PURE__ */ new Date()).toISOString(), properties: {} });
|
|
1551
|
+
pendingIdentify = false;
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
const anonId = await Promise.resolve(opts.getAnonId());
|
|
1555
|
+
const payload = {
|
|
1556
|
+
anon_id: anonId,
|
|
1557
|
+
user_id: userId ?? opts.getUserId?.() ?? null,
|
|
1558
|
+
user_traits: userTraits ?? opts.getUserTraits?.() ?? null,
|
|
1559
|
+
session_id: null,
|
|
1560
|
+
sdk_version: opts.sdkVersion ?? null,
|
|
1561
|
+
surface: SURFACE,
|
|
1562
|
+
events: batch
|
|
1563
|
+
};
|
|
1564
|
+
const res = await client.postProductEvents(payload);
|
|
1565
|
+
if (!res.ok) await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
|
|
1566
|
+
else await writeSpill([]);
|
|
1567
|
+
} catch {
|
|
1568
|
+
await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
|
|
1569
|
+
} finally {
|
|
1570
|
+
flushing = false;
|
|
1571
|
+
}
|
|
1572
|
+
if (buffer.length >= import_core6.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
|
|
1573
|
+
}
|
|
1574
|
+
function enqueue(ev) {
|
|
1575
|
+
if (consent === "denied") return;
|
|
1576
|
+
if (consent === "pending") {
|
|
1577
|
+
if (consentBuffer.length < CONSENT_BUFFER_MAX) consentBuffer.push(ev);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
buffer.push(ev);
|
|
1581
|
+
if (buffer.length >= import_core6.EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
|
|
1582
|
+
}
|
|
1583
|
+
const ready = enabled ? (async () => {
|
|
1584
|
+
storage = await loadStorage();
|
|
1585
|
+
if (destroyed) return;
|
|
1586
|
+
let stored = null;
|
|
1587
|
+
try {
|
|
1588
|
+
stored = storage ? await storage.getItem(consentKey) : null;
|
|
1589
|
+
} catch {
|
|
1590
|
+
stored = null;
|
|
1591
|
+
}
|
|
1592
|
+
if (stored === "denied") {
|
|
1593
|
+
consent = "denied";
|
|
1594
|
+
buffer = [];
|
|
1595
|
+
consentBuffer = [];
|
|
1596
|
+
pendingIdentify = false;
|
|
1597
|
+
await writeSpill([]);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (stored === "granted" && consent === "pending") {
|
|
1601
|
+
consent = "granted";
|
|
1602
|
+
buffer.push(...consentBuffer);
|
|
1603
|
+
consentBuffer = [];
|
|
1604
|
+
}
|
|
1605
|
+
const spill = await readSpill();
|
|
1606
|
+
if (spill.length > 0) {
|
|
1607
|
+
buffer.unshift(...spill);
|
|
1608
|
+
await writeSpill([]);
|
|
1609
|
+
}
|
|
1610
|
+
})().catch(() => void 0) : Promise.resolve();
|
|
1611
|
+
if (enabled) {
|
|
1612
|
+
const interval = Math.max(1e3, cfg.flushIntervalMs ?? 5e3);
|
|
1613
|
+
timer = setInterval(() => {
|
|
1614
|
+
void flushNow();
|
|
1615
|
+
}, interval);
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
ready,
|
|
1619
|
+
track(name, properties, trackOpts) {
|
|
1620
|
+
if (!enabled || destroyed) return false;
|
|
1621
|
+
if (!(0, import_core6.isValidEventName)(name)) return false;
|
|
1622
|
+
const { properties: props } = (0, import_core6.sanitizeEventProperties)(properties, { allowlist, scrub: opts.scrub });
|
|
1623
|
+
let merged = props;
|
|
1624
|
+
if (trackOpts?.reserved) {
|
|
1625
|
+
const { properties: res } = (0, import_core6.sanitizeEventProperties)(trackOpts.reserved, { allowReserved: true, scrub: opts.scrub });
|
|
1626
|
+
merged = { ...props, ...res };
|
|
1627
|
+
}
|
|
1628
|
+
merged.$surface = SURFACE;
|
|
1629
|
+
const route = opts.getRoute?.();
|
|
1630
|
+
if (route && merged.$route === void 0) merged.$route = route;
|
|
1631
|
+
if (!(0, import_core6.propertiesWithinByteLimit)(merged)) return false;
|
|
1632
|
+
enqueue({
|
|
1633
|
+
name,
|
|
1634
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1635
|
+
properties: merged,
|
|
1636
|
+
...trackOpts?.dedupKey ? { dedup_key: trackOpts.dedupKey } : {}
|
|
1637
|
+
});
|
|
1638
|
+
return true;
|
|
1639
|
+
},
|
|
1640
|
+
setConsent(state) {
|
|
1641
|
+
void ready.then(() => storage?.setItem(consentKey, state)).catch(() => void 0);
|
|
1642
|
+
if (state === "denied") {
|
|
1643
|
+
consent = "denied";
|
|
1644
|
+
buffer = [];
|
|
1645
|
+
consentBuffer = [];
|
|
1646
|
+
pendingIdentify = false;
|
|
1647
|
+
void writeSpill([]);
|
|
1648
|
+
} else {
|
|
1649
|
+
const wasPending = consent === "pending";
|
|
1650
|
+
consent = "granted";
|
|
1651
|
+
if (wasPending && consentBuffer.length > 0) {
|
|
1652
|
+
buffer.push(...consentBuffer);
|
|
1653
|
+
consentBuffer = [];
|
|
1654
|
+
}
|
|
1655
|
+
if (buffer.length > 0 || pendingIdentify) void flushNow();
|
|
1656
|
+
}
|
|
1657
|
+
for (const listener of [...consentListeners]) {
|
|
1658
|
+
try {
|
|
1659
|
+
listener(state);
|
|
1660
|
+
} catch {
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
},
|
|
1664
|
+
setIdentity(id, traits) {
|
|
1665
|
+
userId = id;
|
|
1666
|
+
userTraits = traits ?? null;
|
|
1667
|
+
if (id && enabled && consent !== "denied") {
|
|
1668
|
+
pendingIdentify = true;
|
|
1669
|
+
void flushNow();
|
|
1670
|
+
}
|
|
1671
|
+
},
|
|
1672
|
+
consentState() {
|
|
1673
|
+
return enabled ? consent : "denied";
|
|
1674
|
+
},
|
|
1675
|
+
onConsentChange(listener) {
|
|
1676
|
+
consentListeners.add(listener);
|
|
1677
|
+
return () => {
|
|
1678
|
+
consentListeners.delete(listener);
|
|
1679
|
+
};
|
|
1680
|
+
},
|
|
1681
|
+
async flush() {
|
|
1682
|
+
await ready;
|
|
1683
|
+
await flushNow();
|
|
1684
|
+
},
|
|
1685
|
+
async destroy() {
|
|
1686
|
+
if (timer != null) {
|
|
1687
|
+
clearInterval(timer);
|
|
1688
|
+
timer = null;
|
|
1689
|
+
}
|
|
1690
|
+
await flushNow().catch(() => void 0);
|
|
1691
|
+
destroyed = true;
|
|
1692
|
+
buffer = [];
|
|
1693
|
+
consentBuffer = [];
|
|
1694
|
+
pendingIdentify = false;
|
|
1695
|
+
}
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// src/analytics/session-tracker.ts
|
|
1700
|
+
var import_core7 = require("@mushi-mushi/core");
|
|
1701
|
+
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
1702
|
+
var RESUME_WINDOW_MS = 30 * 6e4;
|
|
1703
|
+
function createRNSessionTracker(opts) {
|
|
1704
|
+
const clock = opts.now ?? Date.now;
|
|
1705
|
+
const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
|
|
1706
|
+
let sessionId = opts.sessionId ?? `sess_${(0, import_core7.newUuid)()}`;
|
|
1707
|
+
let pageViewCount = 0;
|
|
1708
|
+
let started = false;
|
|
1709
|
+
let allowed = false;
|
|
1710
|
+
let foreground = true;
|
|
1711
|
+
let destroyed = false;
|
|
1712
|
+
let backgroundedAt = null;
|
|
1713
|
+
let heartbeat = null;
|
|
1714
|
+
function route() {
|
|
1715
|
+
return opts.getRoute?.() ?? null;
|
|
1716
|
+
}
|
|
1717
|
+
function send(kind, extra) {
|
|
1718
|
+
const client = resolveClient();
|
|
1719
|
+
if (!client || !allowed || destroyed) return;
|
|
1720
|
+
const base = {
|
|
1721
|
+
kind,
|
|
1722
|
+
session_id: sessionId,
|
|
1723
|
+
ts: new Date(clock()).toISOString(),
|
|
1724
|
+
page_view_count: pageViewCount,
|
|
1725
|
+
user_agent: opts.userAgent ?? null,
|
|
1726
|
+
sdk_version: opts.sdkVersion
|
|
1727
|
+
};
|
|
1728
|
+
void Promise.resolve(opts.getReporterToken()).then((token) => client.postSessionEvent({ ...base, reporter_token_hash: token, ...extra })).catch(() => void 0);
|
|
1729
|
+
}
|
|
1730
|
+
function stopHeartbeat() {
|
|
1731
|
+
if (heartbeat != null) {
|
|
1732
|
+
clearInterval(heartbeat);
|
|
1733
|
+
heartbeat = null;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
function startHeartbeat() {
|
|
1737
|
+
stopHeartbeat();
|
|
1738
|
+
heartbeat = setInterval(() => send("session_heartbeat", { route: route() }), HEARTBEAT_INTERVAL_MS);
|
|
1739
|
+
}
|
|
1740
|
+
function startSession() {
|
|
1741
|
+
started = true;
|
|
1742
|
+
pageViewCount = 1;
|
|
1743
|
+
send("session_start", { route: route() });
|
|
1744
|
+
startHeartbeat();
|
|
1745
|
+
}
|
|
1746
|
+
function activate() {
|
|
1747
|
+
if (allowed || destroyed) return;
|
|
1748
|
+
allowed = true;
|
|
1749
|
+
if (!foreground) return;
|
|
1750
|
+
if (!started) startSession();
|
|
1751
|
+
else startHeartbeat();
|
|
1752
|
+
}
|
|
1753
|
+
function deactivate() {
|
|
1754
|
+
allowed = false;
|
|
1755
|
+
stopHeartbeat();
|
|
1756
|
+
}
|
|
1757
|
+
const unsubscribe = opts.consent.onConsentChange(() => {
|
|
1758
|
+
if (opts.consent.consentState() === "granted") activate();
|
|
1759
|
+
else deactivate();
|
|
1760
|
+
});
|
|
1761
|
+
void opts.consent.ready.then(() => {
|
|
1762
|
+
if (opts.consent.consentState() === "granted") activate();
|
|
1763
|
+
}).catch(() => void 0);
|
|
1764
|
+
return {
|
|
1765
|
+
onAppStateChange(state) {
|
|
1766
|
+
if (destroyed) return;
|
|
1767
|
+
if (state === "active") {
|
|
1768
|
+
if (foreground) return;
|
|
1769
|
+
foreground = true;
|
|
1770
|
+
const away = backgroundedAt == null ? 0 : clock() - backgroundedAt;
|
|
1771
|
+
backgroundedAt = null;
|
|
1772
|
+
if (!allowed) return;
|
|
1773
|
+
if (!started || away >= RESUME_WINDOW_MS) {
|
|
1774
|
+
sessionId = `sess_${(0, import_core7.newUuid)()}`;
|
|
1775
|
+
startSession();
|
|
1776
|
+
} else {
|
|
1777
|
+
send("session_heartbeat", { route: route() });
|
|
1778
|
+
startHeartbeat();
|
|
1779
|
+
}
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (state !== "background" || !foreground) return;
|
|
1783
|
+
foreground = false;
|
|
1784
|
+
backgroundedAt = clock();
|
|
1785
|
+
stopHeartbeat();
|
|
1786
|
+
if (started) send("session_end", { route: route() });
|
|
1787
|
+
},
|
|
1788
|
+
pageView(next) {
|
|
1789
|
+
if (!allowed || !started || !foreground) return;
|
|
1790
|
+
pageViewCount += 1;
|
|
1791
|
+
send("page_view", { route: next });
|
|
1792
|
+
},
|
|
1793
|
+
destroy() {
|
|
1794
|
+
destroyed = true;
|
|
1795
|
+
deactivate();
|
|
1796
|
+
unsubscribe();
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1482
1801
|
// src/version.ts
|
|
1483
1802
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
|
|
1484
|
-
var MUSHI_SDK_VERSION = "0.
|
|
1803
|
+
var MUSHI_SDK_VERSION = "0.22.0" ? "0.22.0" : "0.17.0";
|
|
1485
1804
|
|
|
1486
1805
|
// src/provider.tsx
|
|
1487
1806
|
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
1488
1807
|
var MushiContext = (0, import_react4.createContext)(null);
|
|
1808
|
+
function nativeUserAgent() {
|
|
1809
|
+
const os = import_react_native5.Platform?.OS ?? "native";
|
|
1810
|
+
const version = import_react_native5.Platform?.Version;
|
|
1811
|
+
return `${MUSHI_SDK_PACKAGE}/${MUSHI_SDK_VERSION} (${version != null ? `${os} ${String(version)}` : os})`;
|
|
1812
|
+
}
|
|
1489
1813
|
var DEFAULT_SCREENSHOT_HINT = "Check the preview \u2014 remove it if any private info (balances, personal details) is visible.";
|
|
1490
1814
|
function resolveScreenshotHint(v) {
|
|
1491
1815
|
if (v === false) return null;
|
|
@@ -1528,9 +1852,9 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1528
1852
|
const networkRef = (0, import_react4.useRef)(null);
|
|
1529
1853
|
const queueRef = (0, import_react4.useRef)(null);
|
|
1530
1854
|
const apiClientRef = (0, import_react4.useRef)(null);
|
|
1531
|
-
const sessionIdRef = (0, import_react4.useRef)(`sess_${(0,
|
|
1855
|
+
const sessionIdRef = (0, import_react4.useRef)(`sess_${(0, import_core8.newUuid)()}`);
|
|
1532
1856
|
const breadcrumbsRef = (0, import_react4.useRef)(
|
|
1533
|
-
(0,
|
|
1857
|
+
(0, import_core8.createBreadcrumbBuffer)({ max: config.capture?.maxBreadcrumbs ?? 50 })
|
|
1534
1858
|
);
|
|
1535
1859
|
const [sheetVisible, setSheetVisible] = (0, import_react4.useState)(false);
|
|
1536
1860
|
const [sheetPreferredTab, setSheetPreferredTab] = (0, import_react4.useState)("report");
|
|
@@ -1555,7 +1879,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1555
1879
|
if (config.endpoint && !config.apiEndpoint && __DEV__) {
|
|
1556
1880
|
console.warn("[MushiProvider] `endpoint` is deprecated \u2014 use `apiEndpoint` instead.");
|
|
1557
1881
|
}
|
|
1558
|
-
const apiEndpoint = rawEndpoint ??
|
|
1882
|
+
const apiEndpoint = rawEndpoint ?? import_core8.DEFAULT_API_ENDPOINT;
|
|
1559
1883
|
(0, import_react4.useEffect)(() => {
|
|
1560
1884
|
if (config.capture?.console !== false) {
|
|
1561
1885
|
consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
|
|
@@ -1563,7 +1887,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1563
1887
|
if (config.capture?.network !== false) {
|
|
1564
1888
|
networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
|
|
1565
1889
|
}
|
|
1566
|
-
apiClientRef.current = (0,
|
|
1890
|
+
apiClientRef.current = (0, import_core8.createApiClient)({
|
|
1567
1891
|
projectId: config.projectId,
|
|
1568
1892
|
apiKey: config.apiKey,
|
|
1569
1893
|
apiEndpoint,
|
|
@@ -1600,14 +1924,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1600
1924
|
if (existing && existing.startsWith("mushi_")) {
|
|
1601
1925
|
reporterTokenRef.current = existing;
|
|
1602
1926
|
} else {
|
|
1603
|
-
const fresh = `mushi_${(0,
|
|
1927
|
+
const fresh = `mushi_${(0, import_core8.newUuid)()}`;
|
|
1604
1928
|
reporterTokenRef.current = fresh;
|
|
1605
1929
|
await saveReporterToken(fresh, secureStorage);
|
|
1606
1930
|
}
|
|
1607
1931
|
resolveReporterTokenReady();
|
|
1608
1932
|
} catch {
|
|
1609
1933
|
if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
|
|
1610
|
-
reporterTokenRef.current = `mushi_${(0,
|
|
1934
|
+
reporterTokenRef.current = `mushi_${(0, import_core8.newUuid)()}`;
|
|
1611
1935
|
}
|
|
1612
1936
|
resolveReporterTokenReady();
|
|
1613
1937
|
}
|
|
@@ -1727,7 +2051,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1727
2051
|
const merged = Object.keys(metadataRef.current).length > 0 ? { ...contextMeta, ...metadataRef.current } : Object.keys(contextMeta).length > 0 ? contextMeta : void 0;
|
|
1728
2052
|
const breadcrumbs = breadcrumbsRef.current.getAll().map((c) => ({
|
|
1729
2053
|
...c,
|
|
1730
|
-
message: (0,
|
|
2054
|
+
message: (0, import_core8.scrubPii)(c.message)
|
|
1731
2055
|
}));
|
|
1732
2056
|
const timeline = breadcrumbs.map((c) => ({
|
|
1733
2057
|
ts: c.timestamp,
|
|
@@ -1736,10 +2060,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1736
2060
|
}));
|
|
1737
2061
|
const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
|
|
1738
2062
|
const report = {
|
|
1739
|
-
id: (0,
|
|
2063
|
+
id: (0, import_core8.newUuid)(),
|
|
1740
2064
|
projectId: config.projectId,
|
|
1741
2065
|
category: data.category,
|
|
1742
|
-
description: (0,
|
|
2066
|
+
description: (0, import_core8.scrubPii)(data.description),
|
|
1743
2067
|
environment: {
|
|
1744
2068
|
userAgent: deviceInfo.systemName ?? "ReactNative",
|
|
1745
2069
|
platform: deviceInfo.platform ?? "mobile",
|
|
@@ -1752,7 +2076,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1752
2076
|
},
|
|
1753
2077
|
consoleLogs: (consoleRef.current?.getEntries() ?? []).map((entry) => ({
|
|
1754
2078
|
...entry,
|
|
1755
|
-
message: (0,
|
|
2079
|
+
message: (0, import_core8.scrubPii)(entry.message)
|
|
1756
2080
|
})),
|
|
1757
2081
|
networkLogs: networkRef.current?.getEntries().map((entry) => ({
|
|
1758
2082
|
...entry,
|
|
@@ -1782,7 +2106,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1782
2106
|
const code = result.error?.code ?? "";
|
|
1783
2107
|
const isCredentialError = code === "HTTP_401" || code === "HTTP_403" || code.includes("UNAUTHORIZED") || code.includes("FORBIDDEN") || code.includes("INVALID_API_KEY");
|
|
1784
2108
|
if (isCredentialError) {
|
|
1785
|
-
const where = apiEndpoint.replace(/\/$/, "") ===
|
|
2109
|
+
const where = apiEndpoint.replace(/\/$/, "") === import_core8.DEFAULT_API_ENDPOINT ? "Get the correct values at: https://kensaur.us/mushi-mushi/admin/projects" : "Get the correct values from your Mushi console's Projects page.";
|
|
1786
2110
|
console.error(
|
|
1787
2111
|
'[Mushi] Credentials rejected. Check your projectId and apiKey (must have "report:write" scope). ' + where
|
|
1788
2112
|
);
|
|
@@ -1828,6 +2152,63 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1828
2152
|
if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
|
|
1829
2153
|
};
|
|
1830
2154
|
}, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
|
|
2155
|
+
const eventTrackerRef = (0, import_react4.useRef)(null);
|
|
2156
|
+
const sessionTrackerRef = (0, import_react4.useRef)(null);
|
|
2157
|
+
(0, import_react4.useEffect)(() => {
|
|
2158
|
+
const getRoute = () => screenRef.current?.route ?? screenRef.current?.name ?? null;
|
|
2159
|
+
const tracker = createRNEventTracker({
|
|
2160
|
+
projectId: config.projectId,
|
|
2161
|
+
client: () => apiClientRef.current,
|
|
2162
|
+
getAnonId: async () => {
|
|
2163
|
+
await reporterTokenReadyRef.current;
|
|
2164
|
+
return reporterTokenRef.current;
|
|
2165
|
+
},
|
|
2166
|
+
getUserId: () => userRef.current?.id ?? null,
|
|
2167
|
+
getRoute,
|
|
2168
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2169
|
+
config: config.analytics,
|
|
2170
|
+
scrub: import_core8.scrubPii
|
|
2171
|
+
});
|
|
2172
|
+
eventTrackerRef.current = tracker;
|
|
2173
|
+
const sessions = config.trackSessions === false ? null : createRNSessionTracker({
|
|
2174
|
+
client: () => apiClientRef.current,
|
|
2175
|
+
getReporterToken: async () => {
|
|
2176
|
+
await reporterTokenReadyRef.current;
|
|
2177
|
+
return reporterTokenRef.current;
|
|
2178
|
+
},
|
|
2179
|
+
getRoute,
|
|
2180
|
+
consent: tracker,
|
|
2181
|
+
sessionId: sessionIdRef.current,
|
|
2182
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2183
|
+
userAgent: nativeUserAgent()
|
|
2184
|
+
});
|
|
2185
|
+
sessionTrackerRef.current = sessions;
|
|
2186
|
+
let sub;
|
|
2187
|
+
try {
|
|
2188
|
+
if (typeof import_react_native5.AppState?.addEventListener === "function") {
|
|
2189
|
+
sub = import_react_native5.AppState.addEventListener("change", (state) => {
|
|
2190
|
+
sessions?.onAppStateChange(state);
|
|
2191
|
+
if (state !== "active") tracker.flush().catch(() => {
|
|
2192
|
+
});
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
} catch {
|
|
2196
|
+
}
|
|
2197
|
+
return () => {
|
|
2198
|
+
sub?.remove();
|
|
2199
|
+
sessions?.destroy();
|
|
2200
|
+
sessionTrackerRef.current = null;
|
|
2201
|
+
eventTrackerRef.current = null;
|
|
2202
|
+
tracker.destroy().catch(() => {
|
|
2203
|
+
});
|
|
2204
|
+
};
|
|
2205
|
+
}, [
|
|
2206
|
+
config.projectId,
|
|
2207
|
+
config.analytics?.enabled,
|
|
2208
|
+
config.analytics?.consent,
|
|
2209
|
+
config.analytics?.flushIntervalMs,
|
|
2210
|
+
config.trackSessions
|
|
2211
|
+
]);
|
|
1831
2212
|
const instance = (0, import_react4.useMemo)(
|
|
1832
2213
|
() => ({
|
|
1833
2214
|
open,
|
|
@@ -1845,6 +2226,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1845
2226
|
name: traits?.name,
|
|
1846
2227
|
provider: traits?.provider
|
|
1847
2228
|
};
|
|
2229
|
+
eventTrackerRef.current?.setIdentity(userId, traits ?? null);
|
|
1848
2230
|
},
|
|
1849
2231
|
setUser(user) {
|
|
1850
2232
|
userRef.current = {
|
|
@@ -1853,18 +2235,22 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1853
2235
|
name: user.name,
|
|
1854
2236
|
provider: user.provider
|
|
1855
2237
|
};
|
|
2238
|
+
eventTrackerRef.current?.setIdentity(user.id, { email: user.email, name: user.name, provider: user.provider });
|
|
1856
2239
|
},
|
|
1857
2240
|
identifyWithToken(token) {
|
|
1858
2241
|
userTokenRef.current = token && typeof token === "string" ? token : null;
|
|
1859
2242
|
if (userTokenRef.current) {
|
|
1860
|
-
const claims = (0,
|
|
2243
|
+
const claims = (0, import_core8.parseIdentityToken)(userTokenRef.current);
|
|
1861
2244
|
if (claims?.sub) {
|
|
1862
2245
|
userRef.current = {
|
|
1863
2246
|
id: claims.sub,
|
|
1864
2247
|
...claims.email ? { email: claims.email } : {},
|
|
1865
2248
|
...claims.name ? { name: claims.name } : {}
|
|
1866
2249
|
};
|
|
2250
|
+
eventTrackerRef.current?.setIdentity(claims.sub, null);
|
|
1867
2251
|
}
|
|
2252
|
+
} else {
|
|
2253
|
+
eventTrackerRef.current?.setIdentity(null, null);
|
|
1868
2254
|
}
|
|
1869
2255
|
},
|
|
1870
2256
|
publishPageContext(context) {
|
|
@@ -1877,6 +2263,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1877
2263
|
const prev = screenRef.current?.name;
|
|
1878
2264
|
screenRef.current = screen;
|
|
1879
2265
|
if (screen.name && screen.name !== prev) {
|
|
2266
|
+
sessionTrackerRef.current?.pageView(screen.route ?? screen.name);
|
|
1880
2267
|
breadcrumbsRef.current.add({
|
|
1881
2268
|
category: "navigation",
|
|
1882
2269
|
level: "info",
|
|
@@ -1938,6 +2325,13 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1938
2325
|
if (!config.rewards?.enabled) return;
|
|
1939
2326
|
activityQueueRef.current.push({ action, metadata });
|
|
1940
2327
|
},
|
|
2328
|
+
// Product analytics (Users & Funnels)
|
|
2329
|
+
track(event, properties) {
|
|
2330
|
+
return eventTrackerRef.current?.track(event, properties) ?? false;
|
|
2331
|
+
},
|
|
2332
|
+
setConsent(state) {
|
|
2333
|
+
eventTrackerRef.current?.setConsent(state);
|
|
2334
|
+
},
|
|
1941
2335
|
async getTier() {
|
|
1942
2336
|
const userId = userRef.current?.id;
|
|
1943
2337
|
const client = apiClientRef.current;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode, FC, ReactElement } from 'react';
|
|
3
|
-
import { MushiRewardsConfig, MushiPageContext, MushiBreadcrumb, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiTierResult, MushiReputationResult, MushiAssistantReply } from '@mushi-mushi/core';
|
|
3
|
+
import { MushiAnalyticsConfig, MushiRewardsConfig, MushiPageContext, MushiBreadcrumb, MushiReporterReport, MushiReporterComment, MushiHallOfFameEntry, MushiPropertyValue, MushiTierResult, MushiReputationResult, MushiAssistantReply } from '@mushi-mushi/core';
|
|
4
4
|
export { MushiHallOfFameEntry, MushiReporterComment, MushiReporterReport } from '@mushi-mushi/core';
|
|
5
5
|
|
|
6
6
|
interface ConsoleEntry {
|
|
@@ -42,6 +42,38 @@ interface DeviceInfo {
|
|
|
42
42
|
}
|
|
43
43
|
declare function getDeviceInfo(): DeviceInfo;
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* FILE: packages/react-native/src/analytics/event-tracker.ts
|
|
47
|
+
* PURPOSE: Product-analytics batcher behind `useMushi().track()` on React Native.
|
|
48
|
+
*
|
|
49
|
+
* OVERVIEW:
|
|
50
|
+
* - The core tracker (`@mushi-mushi/core` event-tracker.ts) returns early
|
|
51
|
+
* without `window`, so RN gets this minimal port: in-memory batch, interval
|
|
52
|
+
* flush, explicit `flush()` for app-background, AsyncStorage spill for
|
|
53
|
+
* unsent batches (cap 200 / 24 h) and persisted consent.
|
|
54
|
+
* - Validation and PII rules are the shared ones from core
|
|
55
|
+
* (`isValidEventName`, `sanitizeEventProperties`, `propertiesWithinByteLimit`).
|
|
56
|
+
* - Transport is `MushiApiClient.postProductEvents()` — the same
|
|
57
|
+
* POST /v1/sdk/events the web SDK uses; `surface` is `'mobile'`.
|
|
58
|
+
*
|
|
59
|
+
* DEPENDENCIES:
|
|
60
|
+
* - @mushi-mushi/core — validation helpers + wire types.
|
|
61
|
+
* - @react-native-async-storage/async-storage — optional peer; without it the
|
|
62
|
+
* batch is memory-only and consent is per-launch.
|
|
63
|
+
*
|
|
64
|
+
* USAGE:
|
|
65
|
+
* - Created by MushiProvider; hosts call `useMushi().track(name, props)`.
|
|
66
|
+
* - No React import here so the module is unit-testable with fake timers.
|
|
67
|
+
*
|
|
68
|
+
* NOTES:
|
|
69
|
+
* - No DNT signal exists on native; `analytics.enabled:false` or
|
|
70
|
+
* `analytics.consent:'required'` + `setConsent('denied')` are the off switches.
|
|
71
|
+
* App Tracking Transparency prompts remain the host's responsibility.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/** The subset of the web `analytics` block that applies on native. */
|
|
75
|
+
type RNAnalyticsConfig = Pick<MushiAnalyticsConfig, 'enabled' | 'consent' | 'flushIntervalMs' | 'propertyAllowlist'>;
|
|
76
|
+
|
|
45
77
|
interface MushiRNConfig {
|
|
46
78
|
projectId: string;
|
|
47
79
|
apiKey: string;
|
|
@@ -115,6 +147,22 @@ interface MushiRNConfig {
|
|
|
115
147
|
secureStorage?: boolean;
|
|
116
148
|
};
|
|
117
149
|
rewards?: MushiRewardsConfig;
|
|
150
|
+
/**
|
|
151
|
+
* Product analytics (`useMushi().track()`). Events batch to
|
|
152
|
+
* POST /v1/sdk/events every 5 s, on app background and at 20 queued events;
|
|
153
|
+
* unsent batches spill to AsyncStorage (optional peer) and replay on the
|
|
154
|
+
* next launch. `consent: 'required'` buffers until `setConsent('granted')`.
|
|
155
|
+
* There is no DNT signal on native — `enabled: false` is the off switch.
|
|
156
|
+
*/
|
|
157
|
+
analytics?: RNAnalyticsConfig;
|
|
158
|
+
/**
|
|
159
|
+
* Session lifecycle tracking for the console's Activity and Users views:
|
|
160
|
+
* session start / end on foreground and background, a heartbeat every
|
|
161
|
+
* minute, and a page view per `setScreen()` change. Default true. Follows
|
|
162
|
+
* the `analytics` switch and consent: nothing is sent while analytics is
|
|
163
|
+
* disabled or consent is not granted.
|
|
164
|
+
*/
|
|
165
|
+
trackSessions?: boolean;
|
|
118
166
|
}
|
|
119
167
|
|
|
120
168
|
interface MushiRNInstance {
|
|
@@ -190,6 +238,15 @@ interface MushiRNInstance {
|
|
|
190
238
|
getHallOfFame(limit?: number): Promise<MushiHallOfFameEntry[]>;
|
|
191
239
|
/** Manually record a host-defined activity event. */
|
|
192
240
|
recordActivity(action: string, metadata?: Record<string, unknown>): void;
|
|
241
|
+
/**
|
|
242
|
+
* Record a product-analytics event, e.g. `track('checkout_started', { plan: 'pro' })`.
|
|
243
|
+
* Names must match `^[a-z][a-z0-9_]{1,63}$`; properties are flat, PII-looking
|
|
244
|
+
* keys are dropped and string values are scrubbed. Batched to
|
|
245
|
+
* POST /v1/sdk/events with `$surface: 'mobile'`. Returns true when queued.
|
|
246
|
+
*/
|
|
247
|
+
track(event: string, properties?: Record<string, MushiPropertyValue>): boolean;
|
|
248
|
+
/** Grant or deny analytics consent (persisted in AsyncStorage when available). */
|
|
249
|
+
setConsent(state: 'granted' | 'denied'): void;
|
|
193
250
|
/** Returns the current user's tier. */
|
|
194
251
|
getTier(): Promise<MushiTierResult | null>;
|
|
195
252
|
/** Returns the current user's reputation + point totals. */
|
|
@@ -390,4 +447,4 @@ declare class AsyncStorageQueue {
|
|
|
390
447
|
private getAsyncStorage;
|
|
391
448
|
}
|
|
392
449
|
|
|
393
|
-
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MUSHI_BANNER_DEFAULT_HEIGHT, MushiBanner, type MushiBannerProps, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, MushiTrigger, type MushiTriggerProps, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|
|
450
|
+
export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MUSHI_BANNER_DEFAULT_HEIGHT, MushiBanner, type MushiBannerProps, MushiBottomSheet, type MushiBottomSheetProps, MushiFloatingButton, type MushiFloatingButtonProps, MushiProvider, type MushiRNConfig, type MushiRNInstance, MushiTrigger, type MushiTriggerProps, type NavigationEntry, type NetworkEntry, type RNAnalyticsConfig, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
|