@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/dist/index.d.ts
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 };
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,12 @@ import {
|
|
|
15
15
|
useCallback as useCallback4,
|
|
16
16
|
useMemo
|
|
17
17
|
} from "react";
|
|
18
|
+
import { AppState, Platform as Platform5 } from "react-native";
|
|
18
19
|
import {
|
|
19
20
|
createApiClient,
|
|
20
21
|
createBreadcrumbBuffer,
|
|
21
22
|
DEFAULT_API_ENDPOINT,
|
|
22
|
-
newUuid,
|
|
23
|
+
newUuid as newUuid2,
|
|
23
24
|
parseIdentityToken,
|
|
24
25
|
scrubPii as scrubPii3
|
|
25
26
|
} from "@mushi-mushi/core";
|
|
@@ -1499,13 +1500,341 @@ var s3 = StyleSheet3.create({
|
|
|
1499
1500
|
}
|
|
1500
1501
|
});
|
|
1501
1502
|
|
|
1503
|
+
// src/analytics/event-tracker.ts
|
|
1504
|
+
import {
|
|
1505
|
+
EVENT_PROPERTY_LIMITS,
|
|
1506
|
+
isValidEventName,
|
|
1507
|
+
propertiesWithinByteLimit,
|
|
1508
|
+
sanitizeEventProperties
|
|
1509
|
+
} from "@mushi-mushi/core";
|
|
1510
|
+
var CONSENT_BUFFER_MAX = 50;
|
|
1511
|
+
var SPILL_MAX = 200;
|
|
1512
|
+
var SPILL_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1513
|
+
var SURFACE = "mobile";
|
|
1514
|
+
async function defaultStorage() {
|
|
1515
|
+
try {
|
|
1516
|
+
const mod = await import("@react-native-async-storage/async-storage");
|
|
1517
|
+
return mod.default;
|
|
1518
|
+
} catch {
|
|
1519
|
+
return null;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
function createRNEventTracker(opts) {
|
|
1523
|
+
const cfg = opts.config ?? {};
|
|
1524
|
+
const enabled = cfg.enabled !== false;
|
|
1525
|
+
const consentMode = cfg.consent ?? "implied";
|
|
1526
|
+
const allowlist = cfg.propertyAllowlist ?? [];
|
|
1527
|
+
const spillKey = `@mushi:events_spill_${opts.projectId}`;
|
|
1528
|
+
const consentKey = `@mushi:analytics_consent_${opts.projectId}`;
|
|
1529
|
+
let buffer = [];
|
|
1530
|
+
let consentBuffer = [];
|
|
1531
|
+
let consent = consentMode === "required" ? "pending" : "granted";
|
|
1532
|
+
let userId = null;
|
|
1533
|
+
let userTraits = null;
|
|
1534
|
+
let pendingIdentify = false;
|
|
1535
|
+
let flushing = false;
|
|
1536
|
+
let destroyed = false;
|
|
1537
|
+
let timer = null;
|
|
1538
|
+
let storage = null;
|
|
1539
|
+
const consentListeners = /* @__PURE__ */ new Set();
|
|
1540
|
+
const loadStorage = opts.storage ?? defaultStorage;
|
|
1541
|
+
const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
|
|
1542
|
+
async function readSpill() {
|
|
1543
|
+
if (!storage) return [];
|
|
1544
|
+
try {
|
|
1545
|
+
const raw = await storage.getItem(spillKey);
|
|
1546
|
+
if (!raw) return [];
|
|
1547
|
+
const parsed = JSON.parse(raw);
|
|
1548
|
+
if (!parsed || Date.now() - parsed.at > SPILL_TTL_MS) {
|
|
1549
|
+
await storage.removeItem(spillKey);
|
|
1550
|
+
return [];
|
|
1551
|
+
}
|
|
1552
|
+
return Array.isArray(parsed.events) ? parsed.events.slice(0, SPILL_MAX) : [];
|
|
1553
|
+
} catch {
|
|
1554
|
+
return [];
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
async function writeSpill(events) {
|
|
1558
|
+
if (!storage) return;
|
|
1559
|
+
try {
|
|
1560
|
+
if (events.length === 0) await storage.removeItem(spillKey);
|
|
1561
|
+
else await storage.setItem(spillKey, JSON.stringify({ at: Date.now(), events: events.slice(-SPILL_MAX) }));
|
|
1562
|
+
} catch {
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function flushNow() {
|
|
1566
|
+
if (destroyed && buffer.length === 0) return;
|
|
1567
|
+
await ready;
|
|
1568
|
+
if (flushing || consent !== "granted") return;
|
|
1569
|
+
if (buffer.length === 0 && !pendingIdentify) return;
|
|
1570
|
+
const client = resolveClient();
|
|
1571
|
+
if (!client) return;
|
|
1572
|
+
flushing = true;
|
|
1573
|
+
const batch = buffer.splice(0, EVENT_PROPERTY_LIMITS.maxServerBatch);
|
|
1574
|
+
if (pendingIdentify) {
|
|
1575
|
+
batch.unshift({ name: "identify", ts: (/* @__PURE__ */ new Date()).toISOString(), properties: {} });
|
|
1576
|
+
pendingIdentify = false;
|
|
1577
|
+
}
|
|
1578
|
+
try {
|
|
1579
|
+
const anonId = await Promise.resolve(opts.getAnonId());
|
|
1580
|
+
const payload = {
|
|
1581
|
+
anon_id: anonId,
|
|
1582
|
+
user_id: userId ?? opts.getUserId?.() ?? null,
|
|
1583
|
+
user_traits: userTraits ?? opts.getUserTraits?.() ?? null,
|
|
1584
|
+
session_id: null,
|
|
1585
|
+
sdk_version: opts.sdkVersion ?? null,
|
|
1586
|
+
surface: SURFACE,
|
|
1587
|
+
events: batch
|
|
1588
|
+
};
|
|
1589
|
+
const res = await client.postProductEvents(payload);
|
|
1590
|
+
if (!res.ok) await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
|
|
1591
|
+
else await writeSpill([]);
|
|
1592
|
+
} catch {
|
|
1593
|
+
await writeSpill([...await readSpill(), ...batch.filter((e) => e.name !== "identify")]);
|
|
1594
|
+
} finally {
|
|
1595
|
+
flushing = false;
|
|
1596
|
+
}
|
|
1597
|
+
if (buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
|
|
1598
|
+
}
|
|
1599
|
+
function enqueue(ev) {
|
|
1600
|
+
if (consent === "denied") return;
|
|
1601
|
+
if (consent === "pending") {
|
|
1602
|
+
if (consentBuffer.length < CONSENT_BUFFER_MAX) consentBuffer.push(ev);
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
buffer.push(ev);
|
|
1606
|
+
if (buffer.length >= EVENT_PROPERTY_LIMITS.maxClientBatch) void flushNow();
|
|
1607
|
+
}
|
|
1608
|
+
const ready = enabled ? (async () => {
|
|
1609
|
+
storage = await loadStorage();
|
|
1610
|
+
if (destroyed) return;
|
|
1611
|
+
let stored = null;
|
|
1612
|
+
try {
|
|
1613
|
+
stored = storage ? await storage.getItem(consentKey) : null;
|
|
1614
|
+
} catch {
|
|
1615
|
+
stored = null;
|
|
1616
|
+
}
|
|
1617
|
+
if (stored === "denied") {
|
|
1618
|
+
consent = "denied";
|
|
1619
|
+
buffer = [];
|
|
1620
|
+
consentBuffer = [];
|
|
1621
|
+
pendingIdentify = false;
|
|
1622
|
+
await writeSpill([]);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (stored === "granted" && consent === "pending") {
|
|
1626
|
+
consent = "granted";
|
|
1627
|
+
buffer.push(...consentBuffer);
|
|
1628
|
+
consentBuffer = [];
|
|
1629
|
+
}
|
|
1630
|
+
const spill = await readSpill();
|
|
1631
|
+
if (spill.length > 0) {
|
|
1632
|
+
buffer.unshift(...spill);
|
|
1633
|
+
await writeSpill([]);
|
|
1634
|
+
}
|
|
1635
|
+
})().catch(() => void 0) : Promise.resolve();
|
|
1636
|
+
if (enabled) {
|
|
1637
|
+
const interval = Math.max(1e3, cfg.flushIntervalMs ?? 5e3);
|
|
1638
|
+
timer = setInterval(() => {
|
|
1639
|
+
void flushNow();
|
|
1640
|
+
}, interval);
|
|
1641
|
+
}
|
|
1642
|
+
return {
|
|
1643
|
+
ready,
|
|
1644
|
+
track(name, properties, trackOpts) {
|
|
1645
|
+
if (!enabled || destroyed) return false;
|
|
1646
|
+
if (!isValidEventName(name)) return false;
|
|
1647
|
+
const { properties: props } = sanitizeEventProperties(properties, { allowlist, scrub: opts.scrub });
|
|
1648
|
+
let merged = props;
|
|
1649
|
+
if (trackOpts?.reserved) {
|
|
1650
|
+
const { properties: res } = sanitizeEventProperties(trackOpts.reserved, { allowReserved: true, scrub: opts.scrub });
|
|
1651
|
+
merged = { ...props, ...res };
|
|
1652
|
+
}
|
|
1653
|
+
merged.$surface = SURFACE;
|
|
1654
|
+
const route = opts.getRoute?.();
|
|
1655
|
+
if (route && merged.$route === void 0) merged.$route = route;
|
|
1656
|
+
if (!propertiesWithinByteLimit(merged)) return false;
|
|
1657
|
+
enqueue({
|
|
1658
|
+
name,
|
|
1659
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1660
|
+
properties: merged,
|
|
1661
|
+
...trackOpts?.dedupKey ? { dedup_key: trackOpts.dedupKey } : {}
|
|
1662
|
+
});
|
|
1663
|
+
return true;
|
|
1664
|
+
},
|
|
1665
|
+
setConsent(state) {
|
|
1666
|
+
void ready.then(() => storage?.setItem(consentKey, state)).catch(() => void 0);
|
|
1667
|
+
if (state === "denied") {
|
|
1668
|
+
consent = "denied";
|
|
1669
|
+
buffer = [];
|
|
1670
|
+
consentBuffer = [];
|
|
1671
|
+
pendingIdentify = false;
|
|
1672
|
+
void writeSpill([]);
|
|
1673
|
+
} else {
|
|
1674
|
+
const wasPending = consent === "pending";
|
|
1675
|
+
consent = "granted";
|
|
1676
|
+
if (wasPending && consentBuffer.length > 0) {
|
|
1677
|
+
buffer.push(...consentBuffer);
|
|
1678
|
+
consentBuffer = [];
|
|
1679
|
+
}
|
|
1680
|
+
if (buffer.length > 0 || pendingIdentify) void flushNow();
|
|
1681
|
+
}
|
|
1682
|
+
for (const listener of [...consentListeners]) {
|
|
1683
|
+
try {
|
|
1684
|
+
listener(state);
|
|
1685
|
+
} catch {
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
},
|
|
1689
|
+
setIdentity(id, traits) {
|
|
1690
|
+
userId = id;
|
|
1691
|
+
userTraits = traits ?? null;
|
|
1692
|
+
if (id && enabled && consent !== "denied") {
|
|
1693
|
+
pendingIdentify = true;
|
|
1694
|
+
void flushNow();
|
|
1695
|
+
}
|
|
1696
|
+
},
|
|
1697
|
+
consentState() {
|
|
1698
|
+
return enabled ? consent : "denied";
|
|
1699
|
+
},
|
|
1700
|
+
onConsentChange(listener) {
|
|
1701
|
+
consentListeners.add(listener);
|
|
1702
|
+
return () => {
|
|
1703
|
+
consentListeners.delete(listener);
|
|
1704
|
+
};
|
|
1705
|
+
},
|
|
1706
|
+
async flush() {
|
|
1707
|
+
await ready;
|
|
1708
|
+
await flushNow();
|
|
1709
|
+
},
|
|
1710
|
+
async destroy() {
|
|
1711
|
+
if (timer != null) {
|
|
1712
|
+
clearInterval(timer);
|
|
1713
|
+
timer = null;
|
|
1714
|
+
}
|
|
1715
|
+
await flushNow().catch(() => void 0);
|
|
1716
|
+
destroyed = true;
|
|
1717
|
+
buffer = [];
|
|
1718
|
+
consentBuffer = [];
|
|
1719
|
+
pendingIdentify = false;
|
|
1720
|
+
}
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/analytics/session-tracker.ts
|
|
1725
|
+
import { newUuid } from "@mushi-mushi/core";
|
|
1726
|
+
var HEARTBEAT_INTERVAL_MS = 6e4;
|
|
1727
|
+
var RESUME_WINDOW_MS = 30 * 6e4;
|
|
1728
|
+
function createRNSessionTracker(opts) {
|
|
1729
|
+
const clock = opts.now ?? Date.now;
|
|
1730
|
+
const resolveClient = () => typeof opts.client === "function" ? opts.client() : opts.client;
|
|
1731
|
+
let sessionId = opts.sessionId ?? `sess_${newUuid()}`;
|
|
1732
|
+
let pageViewCount = 0;
|
|
1733
|
+
let started = false;
|
|
1734
|
+
let allowed = false;
|
|
1735
|
+
let foreground = true;
|
|
1736
|
+
let destroyed = false;
|
|
1737
|
+
let backgroundedAt = null;
|
|
1738
|
+
let heartbeat = null;
|
|
1739
|
+
function route() {
|
|
1740
|
+
return opts.getRoute?.() ?? null;
|
|
1741
|
+
}
|
|
1742
|
+
function send(kind, extra) {
|
|
1743
|
+
const client = resolveClient();
|
|
1744
|
+
if (!client || !allowed || destroyed) return;
|
|
1745
|
+
const base = {
|
|
1746
|
+
kind,
|
|
1747
|
+
session_id: sessionId,
|
|
1748
|
+
ts: new Date(clock()).toISOString(),
|
|
1749
|
+
page_view_count: pageViewCount,
|
|
1750
|
+
user_agent: opts.userAgent ?? null,
|
|
1751
|
+
sdk_version: opts.sdkVersion
|
|
1752
|
+
};
|
|
1753
|
+
void Promise.resolve(opts.getReporterToken()).then((token) => client.postSessionEvent({ ...base, reporter_token_hash: token, ...extra })).catch(() => void 0);
|
|
1754
|
+
}
|
|
1755
|
+
function stopHeartbeat() {
|
|
1756
|
+
if (heartbeat != null) {
|
|
1757
|
+
clearInterval(heartbeat);
|
|
1758
|
+
heartbeat = null;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function startHeartbeat() {
|
|
1762
|
+
stopHeartbeat();
|
|
1763
|
+
heartbeat = setInterval(() => send("session_heartbeat", { route: route() }), HEARTBEAT_INTERVAL_MS);
|
|
1764
|
+
}
|
|
1765
|
+
function startSession() {
|
|
1766
|
+
started = true;
|
|
1767
|
+
pageViewCount = 1;
|
|
1768
|
+
send("session_start", { route: route() });
|
|
1769
|
+
startHeartbeat();
|
|
1770
|
+
}
|
|
1771
|
+
function activate() {
|
|
1772
|
+
if (allowed || destroyed) return;
|
|
1773
|
+
allowed = true;
|
|
1774
|
+
if (!foreground) return;
|
|
1775
|
+
if (!started) startSession();
|
|
1776
|
+
else startHeartbeat();
|
|
1777
|
+
}
|
|
1778
|
+
function deactivate() {
|
|
1779
|
+
allowed = false;
|
|
1780
|
+
stopHeartbeat();
|
|
1781
|
+
}
|
|
1782
|
+
const unsubscribe = opts.consent.onConsentChange(() => {
|
|
1783
|
+
if (opts.consent.consentState() === "granted") activate();
|
|
1784
|
+
else deactivate();
|
|
1785
|
+
});
|
|
1786
|
+
void opts.consent.ready.then(() => {
|
|
1787
|
+
if (opts.consent.consentState() === "granted") activate();
|
|
1788
|
+
}).catch(() => void 0);
|
|
1789
|
+
return {
|
|
1790
|
+
onAppStateChange(state) {
|
|
1791
|
+
if (destroyed) return;
|
|
1792
|
+
if (state === "active") {
|
|
1793
|
+
if (foreground) return;
|
|
1794
|
+
foreground = true;
|
|
1795
|
+
const away = backgroundedAt == null ? 0 : clock() - backgroundedAt;
|
|
1796
|
+
backgroundedAt = null;
|
|
1797
|
+
if (!allowed) return;
|
|
1798
|
+
if (!started || away >= RESUME_WINDOW_MS) {
|
|
1799
|
+
sessionId = `sess_${newUuid()}`;
|
|
1800
|
+
startSession();
|
|
1801
|
+
} else {
|
|
1802
|
+
send("session_heartbeat", { route: route() });
|
|
1803
|
+
startHeartbeat();
|
|
1804
|
+
}
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (state !== "background" || !foreground) return;
|
|
1808
|
+
foreground = false;
|
|
1809
|
+
backgroundedAt = clock();
|
|
1810
|
+
stopHeartbeat();
|
|
1811
|
+
if (started) send("session_end", { route: route() });
|
|
1812
|
+
},
|
|
1813
|
+
pageView(next) {
|
|
1814
|
+
if (!allowed || !started || !foreground) return;
|
|
1815
|
+
pageViewCount += 1;
|
|
1816
|
+
send("page_view", { route: next });
|
|
1817
|
+
},
|
|
1818
|
+
destroy() {
|
|
1819
|
+
destroyed = true;
|
|
1820
|
+
deactivate();
|
|
1821
|
+
unsubscribe();
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1502
1826
|
// src/version.ts
|
|
1503
1827
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/react-native";
|
|
1504
|
-
var MUSHI_SDK_VERSION = "0.
|
|
1828
|
+
var MUSHI_SDK_VERSION = "0.22.0" ? "0.22.0" : "0.17.0";
|
|
1505
1829
|
|
|
1506
1830
|
// src/provider.tsx
|
|
1507
1831
|
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1508
1832
|
var MushiContext = createContext(null);
|
|
1833
|
+
function nativeUserAgent() {
|
|
1834
|
+
const os = Platform5?.OS ?? "native";
|
|
1835
|
+
const version = Platform5?.Version;
|
|
1836
|
+
return `${MUSHI_SDK_PACKAGE}/${MUSHI_SDK_VERSION} (${version != null ? `${os} ${String(version)}` : os})`;
|
|
1837
|
+
}
|
|
1509
1838
|
var DEFAULT_SCREENSHOT_HINT = "Check the preview \u2014 remove it if any private info (balances, personal details) is visible.";
|
|
1510
1839
|
function resolveScreenshotHint(v) {
|
|
1511
1840
|
if (v === false) return null;
|
|
@@ -1548,7 +1877,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1548
1877
|
const networkRef = useRef3(null);
|
|
1549
1878
|
const queueRef = useRef3(null);
|
|
1550
1879
|
const apiClientRef = useRef3(null);
|
|
1551
|
-
const sessionIdRef = useRef3(`sess_${
|
|
1880
|
+
const sessionIdRef = useRef3(`sess_${newUuid2()}`);
|
|
1552
1881
|
const breadcrumbsRef = useRef3(
|
|
1553
1882
|
createBreadcrumbBuffer({ max: config.capture?.maxBreadcrumbs ?? 50 })
|
|
1554
1883
|
);
|
|
@@ -1620,14 +1949,14 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1620
1949
|
if (existing && existing.startsWith("mushi_")) {
|
|
1621
1950
|
reporterTokenRef.current = existing;
|
|
1622
1951
|
} else {
|
|
1623
|
-
const fresh = `mushi_${
|
|
1952
|
+
const fresh = `mushi_${newUuid2()}`;
|
|
1624
1953
|
reporterTokenRef.current = fresh;
|
|
1625
1954
|
await saveReporterToken(fresh, secureStorage);
|
|
1626
1955
|
}
|
|
1627
1956
|
resolveReporterTokenReady();
|
|
1628
1957
|
} catch {
|
|
1629
1958
|
if (reporterTokenRef.current === `rn-${config.projectId}-anon`) {
|
|
1630
|
-
reporterTokenRef.current = `mushi_${
|
|
1959
|
+
reporterTokenRef.current = `mushi_${newUuid2()}`;
|
|
1631
1960
|
}
|
|
1632
1961
|
resolveReporterTokenReady();
|
|
1633
1962
|
}
|
|
@@ -1756,7 +2085,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1756
2085
|
}));
|
|
1757
2086
|
const fingerprintHash = getDeviceFingerprintHash(deviceInfo);
|
|
1758
2087
|
const report = {
|
|
1759
|
-
id:
|
|
2088
|
+
id: newUuid2(),
|
|
1760
2089
|
projectId: config.projectId,
|
|
1761
2090
|
category: data.category,
|
|
1762
2091
|
description: scrubPii3(data.description),
|
|
@@ -1848,6 +2177,63 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1848
2177
|
if (rewardsFlushRef.current) clearInterval(rewardsFlushRef.current);
|
|
1849
2178
|
};
|
|
1850
2179
|
}, [config.rewards?.enabled, config.rewards?.trackActivity, config.rewards?.flushIntervalMs]);
|
|
2180
|
+
const eventTrackerRef = useRef3(null);
|
|
2181
|
+
const sessionTrackerRef = useRef3(null);
|
|
2182
|
+
useEffect3(() => {
|
|
2183
|
+
const getRoute = () => screenRef.current?.route ?? screenRef.current?.name ?? null;
|
|
2184
|
+
const tracker = createRNEventTracker({
|
|
2185
|
+
projectId: config.projectId,
|
|
2186
|
+
client: () => apiClientRef.current,
|
|
2187
|
+
getAnonId: async () => {
|
|
2188
|
+
await reporterTokenReadyRef.current;
|
|
2189
|
+
return reporterTokenRef.current;
|
|
2190
|
+
},
|
|
2191
|
+
getUserId: () => userRef.current?.id ?? null,
|
|
2192
|
+
getRoute,
|
|
2193
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2194
|
+
config: config.analytics,
|
|
2195
|
+
scrub: scrubPii3
|
|
2196
|
+
});
|
|
2197
|
+
eventTrackerRef.current = tracker;
|
|
2198
|
+
const sessions = config.trackSessions === false ? null : createRNSessionTracker({
|
|
2199
|
+
client: () => apiClientRef.current,
|
|
2200
|
+
getReporterToken: async () => {
|
|
2201
|
+
await reporterTokenReadyRef.current;
|
|
2202
|
+
return reporterTokenRef.current;
|
|
2203
|
+
},
|
|
2204
|
+
getRoute,
|
|
2205
|
+
consent: tracker,
|
|
2206
|
+
sessionId: sessionIdRef.current,
|
|
2207
|
+
sdkVersion: MUSHI_SDK_VERSION,
|
|
2208
|
+
userAgent: nativeUserAgent()
|
|
2209
|
+
});
|
|
2210
|
+
sessionTrackerRef.current = sessions;
|
|
2211
|
+
let sub;
|
|
2212
|
+
try {
|
|
2213
|
+
if (typeof AppState?.addEventListener === "function") {
|
|
2214
|
+
sub = AppState.addEventListener("change", (state) => {
|
|
2215
|
+
sessions?.onAppStateChange(state);
|
|
2216
|
+
if (state !== "active") tracker.flush().catch(() => {
|
|
2217
|
+
});
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
} catch {
|
|
2221
|
+
}
|
|
2222
|
+
return () => {
|
|
2223
|
+
sub?.remove();
|
|
2224
|
+
sessions?.destroy();
|
|
2225
|
+
sessionTrackerRef.current = null;
|
|
2226
|
+
eventTrackerRef.current = null;
|
|
2227
|
+
tracker.destroy().catch(() => {
|
|
2228
|
+
});
|
|
2229
|
+
};
|
|
2230
|
+
}, [
|
|
2231
|
+
config.projectId,
|
|
2232
|
+
config.analytics?.enabled,
|
|
2233
|
+
config.analytics?.consent,
|
|
2234
|
+
config.analytics?.flushIntervalMs,
|
|
2235
|
+
config.trackSessions
|
|
2236
|
+
]);
|
|
1851
2237
|
const instance = useMemo(
|
|
1852
2238
|
() => ({
|
|
1853
2239
|
open,
|
|
@@ -1865,6 +2251,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1865
2251
|
name: traits?.name,
|
|
1866
2252
|
provider: traits?.provider
|
|
1867
2253
|
};
|
|
2254
|
+
eventTrackerRef.current?.setIdentity(userId, traits ?? null);
|
|
1868
2255
|
},
|
|
1869
2256
|
setUser(user) {
|
|
1870
2257
|
userRef.current = {
|
|
@@ -1873,6 +2260,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1873
2260
|
name: user.name,
|
|
1874
2261
|
provider: user.provider
|
|
1875
2262
|
};
|
|
2263
|
+
eventTrackerRef.current?.setIdentity(user.id, { email: user.email, name: user.name, provider: user.provider });
|
|
1876
2264
|
},
|
|
1877
2265
|
identifyWithToken(token) {
|
|
1878
2266
|
userTokenRef.current = token && typeof token === "string" ? token : null;
|
|
@@ -1884,7 +2272,10 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1884
2272
|
...claims.email ? { email: claims.email } : {},
|
|
1885
2273
|
...claims.name ? { name: claims.name } : {}
|
|
1886
2274
|
};
|
|
2275
|
+
eventTrackerRef.current?.setIdentity(claims.sub, null);
|
|
1887
2276
|
}
|
|
2277
|
+
} else {
|
|
2278
|
+
eventTrackerRef.current?.setIdentity(null, null);
|
|
1888
2279
|
}
|
|
1889
2280
|
},
|
|
1890
2281
|
publishPageContext(context) {
|
|
@@ -1897,6 +2288,7 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1897
2288
|
const prev = screenRef.current?.name;
|
|
1898
2289
|
screenRef.current = screen;
|
|
1899
2290
|
if (screen.name && screen.name !== prev) {
|
|
2291
|
+
sessionTrackerRef.current?.pageView(screen.route ?? screen.name);
|
|
1900
2292
|
breadcrumbsRef.current.add({
|
|
1901
2293
|
category: "navigation",
|
|
1902
2294
|
level: "info",
|
|
@@ -1958,6 +2350,13 @@ function MushiProvider({ children, config: configProp, ...barePropConfig }) {
|
|
|
1958
2350
|
if (!config.rewards?.enabled) return;
|
|
1959
2351
|
activityQueueRef.current.push({ action, metadata });
|
|
1960
2352
|
},
|
|
2353
|
+
// Product analytics (Users & Funnels)
|
|
2354
|
+
track(event, properties) {
|
|
2355
|
+
return eventTrackerRef.current?.track(event, properties) ?? false;
|
|
2356
|
+
},
|
|
2357
|
+
setConsent(state) {
|
|
2358
|
+
eventTrackerRef.current?.setConsent(state);
|
|
2359
|
+
},
|
|
1961
2360
|
async getTier() {
|
|
1962
2361
|
const userId = userRef.current?.id;
|
|
1963
2362
|
const client = apiClientRef.current;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/react-native",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "React Native
|
|
5
|
+
"description": "Shake-to-report widget for React Native and Expo. The bug mediator for AI-built apps: plain-English diagnosis + a ready fix, in your editor.",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@mushi-mushi/core": "^1.
|
|
47
|
-
"@mushi-mushi/web": "^1.
|
|
46
|
+
"@mushi-mushi/core": "^1.29.0",
|
|
47
|
+
"@mushi-mushi/web": "^1.29.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@size-limit/file": "^12.1.0",
|
|
@@ -57,9 +57,9 @@
|
|
|
57
57
|
"typescript": "^6.0.3",
|
|
58
58
|
"vitest": "^4.1.9",
|
|
59
59
|
"@mushi-mushi/eslint-config": "0.0.0",
|
|
60
|
-
"eslint-plugin-mushi-mushi": "0.3.
|
|
60
|
+
"eslint-plugin-mushi-mushi": "0.3.4"
|
|
61
61
|
},
|
|
62
|
-
"author": "Kenji Sakuramoto (https://
|
|
62
|
+
"author": "Kenji Sakuramoto <kensaurus@gmail.com> (https://github.com/kensaurus)",
|
|
63
63
|
"repository": {
|
|
64
64
|
"type": "git",
|
|
65
65
|
"url": "https://github.com/kensaurus/mushi-mushi.git",
|
|
@@ -76,15 +76,12 @@
|
|
|
76
76
|
"files": [
|
|
77
77
|
"dist",
|
|
78
78
|
"README.md",
|
|
79
|
-
"
|
|
80
|
-
"LICENSE",
|
|
81
|
-
"CONTRIBUTING.md",
|
|
82
|
-
"CODE_OF_CONDUCT.md",
|
|
83
|
-
"SECURITY.md"
|
|
79
|
+
"LICENSE"
|
|
84
80
|
],
|
|
85
81
|
"sideEffects": false,
|
|
86
82
|
"keywords": [
|
|
87
83
|
"mushi-mushi",
|
|
84
|
+
"bug-report-widget",
|
|
88
85
|
"bug-reporting",
|
|
89
86
|
"user-feedback",
|
|
90
87
|
"user-report",
|
|
@@ -97,7 +94,6 @@
|
|
|
97
94
|
"session-replay",
|
|
98
95
|
"offline-queue",
|
|
99
96
|
"sentry-companion",
|
|
100
|
-
"sentry-alternative",
|
|
101
97
|
"error-tracking",
|
|
102
98
|
"ai-triage",
|
|
103
99
|
"auto-fix",
|
|
@@ -119,11 +115,7 @@
|
|
|
119
115
|
},
|
|
120
116
|
"type": "module",
|
|
121
117
|
"engines": {
|
|
122
|
-
"node": ">=20"
|
|
123
|
-
},
|
|
124
|
-
"funding": {
|
|
125
|
-
"type": "github",
|
|
126
|
-
"url": "https://github.com/sponsors/kensaurus"
|
|
118
|
+
"node": ">=20.19.0"
|
|
127
119
|
},
|
|
128
120
|
"scripts": {
|
|
129
121
|
"build": "tsup",
|