@mushi-mushi/react-native 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kenji Sakuramoto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # @mushi-mushi/react-native
2
+
3
+ React Native SDK for Mushi Mushi. Shake-to-report, bottom sheet widget, offline queue.
4
+
5
+ ## Usage
6
+
7
+ ```tsx
8
+ import { MushiProvider } from '@mushi-mushi/react-native'
9
+
10
+ export default function App() {
11
+ return (
12
+ <MushiProvider projectId="proj_xxx" apiKey="mushi_xxx">
13
+ <Navigation />
14
+ </MushiProvider>
15
+ )
16
+ }
17
+ ```
18
+
19
+ ### Hooks
20
+
21
+ ```ts
22
+ import { useMushi, useMushiReport } from '@mushi-mushi/react-native'
23
+
24
+ const { submitReport } = useMushiReport()
25
+ ```
26
+
27
+ ### Captures
28
+
29
+ - Device info (platform, OS, screen dimensions)
30
+ - Console logs (monkey-patched)
31
+ - Network requests (fetch/XMLHttpRequest interceptor)
32
+ - Navigation events (React Navigation integration, optional)
33
+ - Offline queue via AsyncStorage
34
+
35
+ ## Peer Dependencies
36
+
37
+ - `react` >= 18
38
+ - `react-native` >= 0.72
39
+ - `@react-navigation/native` >= 6 (optional)
40
+ - `@react-native-async-storage/async-storage` >= 1.19 (optional)
41
+
42
+ ## License
43
+
44
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AsyncStorageQueue: () => AsyncStorageQueue,
34
+ MushiProvider: () => MushiProvider,
35
+ getDeviceInfo: () => getDeviceInfo,
36
+ setupConsoleCapture: () => setupConsoleCapture,
37
+ setupNetworkCapture: () => setupNetworkCapture,
38
+ useMushi: () => useMushi,
39
+ useMushiReport: () => useMushiReport,
40
+ useMushiWidget: () => useMushiWidget,
41
+ useNavigationCapture: () => useNavigationCapture
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/provider.tsx
46
+ var import_react = require("react");
47
+
48
+ // src/capture/console-capture.ts
49
+ function setupConsoleCapture(maxEntries = 100) {
50
+ const entries = [];
51
+ const methods = ["log", "warn", "error"];
52
+ const originals = /* @__PURE__ */ new Map();
53
+ for (const method of methods) {
54
+ const original = console[method].bind(console);
55
+ originals.set(method, original);
56
+ console[method] = (...args) => {
57
+ entries.push({
58
+ level: method,
59
+ message: args.map(String).join(" ").slice(0, 500),
60
+ timestamp: Date.now()
61
+ });
62
+ if (entries.length > maxEntries) entries.shift();
63
+ original(...args);
64
+ };
65
+ }
66
+ return {
67
+ getEntries: () => [...entries],
68
+ clear: () => {
69
+ entries.length = 0;
70
+ },
71
+ restore: () => {
72
+ for (const method of methods) {
73
+ const orig = originals.get(method);
74
+ if (orig) console[method] = orig;
75
+ }
76
+ }
77
+ };
78
+ }
79
+
80
+ // src/capture/network-capture.ts
81
+ function setupNetworkCapture(maxEntries = 50, apiEndpoint) {
82
+ const entries = [];
83
+ const origFetch = globalThis.fetch;
84
+ globalThis.fetch = async function(input, init) {
85
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
86
+ const method = init?.method ?? "GET";
87
+ if (apiEndpoint && url.includes(apiEndpoint)) {
88
+ return origFetch.apply(this, [input, init]);
89
+ }
90
+ const start = Date.now();
91
+ try {
92
+ const res = await origFetch.apply(this, [input, init]);
93
+ entries.push({
94
+ method,
95
+ url: stripQueryParams(url),
96
+ status: res.status,
97
+ duration: Date.now() - start,
98
+ timestamp: start
99
+ });
100
+ if (entries.length > maxEntries) entries.shift();
101
+ return res;
102
+ } catch (err) {
103
+ entries.push({
104
+ method,
105
+ url: stripQueryParams(url),
106
+ status: null,
107
+ duration: Date.now() - start,
108
+ error: String(err),
109
+ timestamp: start
110
+ });
111
+ if (entries.length > maxEntries) entries.shift();
112
+ throw err;
113
+ }
114
+ };
115
+ return {
116
+ getEntries: () => [...entries],
117
+ clear: () => {
118
+ entries.length = 0;
119
+ },
120
+ restore: () => {
121
+ globalThis.fetch = origFetch;
122
+ }
123
+ };
124
+ }
125
+ function stripQueryParams(url) {
126
+ try {
127
+ const u = new URL(url);
128
+ return `${u.origin}${u.pathname}`;
129
+ } catch {
130
+ return url.split("?")[0];
131
+ }
132
+ }
133
+
134
+ // src/capture/device-info.ts
135
+ var import_react_native = require("react-native");
136
+ function getDeviceInfo() {
137
+ const { width, height } = import_react_native.Dimensions.get("window");
138
+ return {
139
+ platform: import_react_native.Platform.OS,
140
+ osVersion: import_react_native.Platform.Version,
141
+ screenWidth: width,
142
+ screenHeight: height,
143
+ pixelRatio: import_react_native.PixelRatio.get(),
144
+ fontScale: import_react_native.PixelRatio.getFontScale()
145
+ };
146
+ }
147
+
148
+ // src/storage/async-storage-queue.ts
149
+ var QUEUE_KEY = "@mushi:offline_queue";
150
+ var AsyncStorageQueue = class {
151
+ maxSize;
152
+ apiEndpoint;
153
+ apiKey;
154
+ constructor(config) {
155
+ this.maxSize = config.maxSize ?? 50;
156
+ this.apiEndpoint = config.apiEndpoint;
157
+ this.apiKey = config.apiKey;
158
+ }
159
+ async enqueue(report) {
160
+ const AsyncStorage = await this.getAsyncStorage();
161
+ if (!AsyncStorage) return;
162
+ const queue = await this.getQueue(AsyncStorage);
163
+ if (queue.length >= this.maxSize) queue.shift();
164
+ queue.push({ report, enqueuedAt: Date.now() });
165
+ await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
166
+ }
167
+ async flush() {
168
+ const AsyncStorage = await this.getAsyncStorage();
169
+ if (!AsyncStorage) return 0;
170
+ const queue = await this.getQueue(AsyncStorage);
171
+ if (!queue.length) return 0;
172
+ let flushed = 0;
173
+ const remaining = [];
174
+ for (const item of queue) {
175
+ try {
176
+ const res = await fetch(`${this.apiEndpoint}/v1/reports`, {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json",
180
+ "X-Mushi-Api-Key": this.apiKey
181
+ },
182
+ body: JSON.stringify(item.report)
183
+ });
184
+ if (res.ok) {
185
+ flushed++;
186
+ } else {
187
+ remaining.push(item);
188
+ }
189
+ } catch {
190
+ remaining.push(item);
191
+ break;
192
+ }
193
+ }
194
+ await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(remaining));
195
+ return flushed;
196
+ }
197
+ async size() {
198
+ const AsyncStorage = await this.getAsyncStorage();
199
+ if (!AsyncStorage) return 0;
200
+ const queue = await this.getQueue(AsyncStorage);
201
+ return queue.length;
202
+ }
203
+ async getQueue(storage) {
204
+ try {
205
+ const raw = await storage.getItem(QUEUE_KEY);
206
+ return raw ? JSON.parse(raw) : [];
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
211
+ async getAsyncStorage() {
212
+ try {
213
+ const mod = await import("@react-native-async-storage/async-storage");
214
+ return mod.default;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+ };
220
+
221
+ // src/provider.tsx
222
+ var import_jsx_runtime = require("react/jsx-runtime");
223
+ var MushiContext = (0, import_react.createContext)(null);
224
+ function MushiProvider({ children, ...config }) {
225
+ const consoleRef = (0, import_react.useRef)(null);
226
+ const networkRef = (0, import_react.useRef)(null);
227
+ const queueRef = (0, import_react.useRef)(null);
228
+ const apiEndpoint = config.endpoint ?? "https://api.mushimushi.dev";
229
+ (0, import_react.useEffect)(() => {
230
+ if (config.capture?.console !== false) {
231
+ consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
232
+ }
233
+ if (config.capture?.network !== false) {
234
+ networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
235
+ }
236
+ queueRef.current = new AsyncStorageQueue({
237
+ maxSize: config.storage?.maxQueueSize,
238
+ apiEndpoint,
239
+ apiKey: config.apiKey
240
+ });
241
+ return () => {
242
+ consoleRef.current?.restore();
243
+ networkRef.current?.restore();
244
+ };
245
+ }, []);
246
+ const instance = {
247
+ open: () => {
248
+ },
249
+ close: () => {
250
+ },
251
+ submitReport: async (data) => {
252
+ const report = {
253
+ projectId: config.projectId,
254
+ ...data,
255
+ environment: getDeviceInfo(),
256
+ console_logs: consoleRef.current?.getEntries() ?? [],
257
+ network_logs: networkRef.current?.getEntries() ?? []
258
+ };
259
+ try {
260
+ const res = await fetch(`${apiEndpoint}/v1/reports`, {
261
+ method: "POST",
262
+ headers: { "Content-Type": "application/json", "X-Mushi-Api-Key": config.apiKey },
263
+ body: JSON.stringify(report)
264
+ });
265
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
266
+ } catch {
267
+ await queueRef.current?.enqueue(report);
268
+ }
269
+ },
270
+ getDeviceInfo,
271
+ getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
272
+ getNetworkEntries: () => networkRef.current?.getEntries() ?? []
273
+ };
274
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MushiContext.Provider, { value: instance, children });
275
+ }
276
+ function useMushiContext() {
277
+ return (0, import_react.useContext)(MushiContext);
278
+ }
279
+
280
+ // src/hooks/useMushi.ts
281
+ function useMushi() {
282
+ return useMushiContext();
283
+ }
284
+
285
+ // src/hooks/useMushiReport.ts
286
+ var import_react2 = require("react");
287
+ function useMushiReport() {
288
+ const mushi = useMushiContext();
289
+ const submitReport = (0, import_react2.useCallback)(
290
+ async (data) => {
291
+ if (!mushi) throw new Error("MushiProvider not found");
292
+ await mushi.submitReport(data);
293
+ },
294
+ [mushi]
295
+ );
296
+ return { submitReport };
297
+ }
298
+
299
+ // src/hooks/useMushiWidget.ts
300
+ var import_react3 = require("react");
301
+ function useMushiWidget() {
302
+ const mushi = useMushiContext();
303
+ return {
304
+ open: (0, import_react3.useCallback)(() => mushi?.open(), [mushi]),
305
+ close: (0, import_react3.useCallback)(() => mushi?.close(), [mushi])
306
+ };
307
+ }
308
+
309
+ // src/capture/navigation-capture.ts
310
+ var import_react4 = require("react");
311
+ function useNavigationCapture(navigationRef) {
312
+ const historyRef = (0, import_react4.useRef)([]);
313
+ (0, import_react4.useEffect)(() => {
314
+ if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
315
+ const unsubscribe = navigationRef.addListener("state", () => {
316
+ const route = navigationRef.getCurrentRoute?.();
317
+ if (route) {
318
+ historyRef.current.push({ screen: route.name, timestamp: Date.now() });
319
+ if (historyRef.current.length > 20) historyRef.current.shift();
320
+ }
321
+ });
322
+ return unsubscribe;
323
+ }, [navigationRef]);
324
+ return () => [...historyRef.current];
325
+ }
326
+ // Annotate the CommonJS export names for ESM import in node:
327
+ 0 && (module.exports = {
328
+ AsyncStorageQueue,
329
+ MushiProvider,
330
+ getDeviceInfo,
331
+ setupConsoleCapture,
332
+ setupNetworkCapture,
333
+ useMushi,
334
+ useMushiReport,
335
+ useMushiWidget,
336
+ useNavigationCapture
337
+ });
@@ -0,0 +1,116 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface ConsoleEntry {
5
+ level: 'log' | 'warn' | 'error';
6
+ message: string;
7
+ timestamp: number;
8
+ }
9
+ declare function setupConsoleCapture(maxEntries?: number): {
10
+ getEntries: () => ConsoleEntry[];
11
+ clear: () => void;
12
+ restore: () => void;
13
+ };
14
+
15
+ interface NetworkEntry {
16
+ method: string;
17
+ url: string;
18
+ status: number | null;
19
+ duration: number;
20
+ error?: string;
21
+ timestamp: number;
22
+ }
23
+ declare function setupNetworkCapture(maxEntries?: number, apiEndpoint?: string): {
24
+ getEntries: () => NetworkEntry[];
25
+ clear: () => void;
26
+ restore: () => void;
27
+ };
28
+
29
+ interface DeviceInfo {
30
+ platform: string;
31
+ osVersion: string | number;
32
+ screenWidth: number;
33
+ screenHeight: number;
34
+ pixelRatio: number;
35
+ fontScale: number;
36
+ }
37
+ declare function getDeviceInfo(): DeviceInfo;
38
+
39
+ interface MushiRNConfig {
40
+ projectId: string;
41
+ apiKey: string;
42
+ endpoint?: string;
43
+ widget?: {
44
+ trigger?: 'shake' | 'button' | 'both';
45
+ shakeThreshold?: number;
46
+ buttonPosition?: 'bottom-right' | 'bottom-left';
47
+ };
48
+ capture?: {
49
+ console?: boolean;
50
+ network?: boolean;
51
+ navigation?: boolean;
52
+ maxConsoleEntries?: number;
53
+ maxNetworkEntries?: number;
54
+ };
55
+ storage?: {
56
+ maxQueueSize?: number;
57
+ retryIntervalMs?: number;
58
+ };
59
+ }
60
+ interface MushiRNInstance {
61
+ open(): void;
62
+ close(): void;
63
+ submitReport(data: {
64
+ description: string;
65
+ category: string;
66
+ }): Promise<void>;
67
+ getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
68
+ getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
69
+ getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
70
+ }
71
+ declare function MushiProvider({ children, ...config }: MushiRNConfig & {
72
+ children: ReactNode;
73
+ }): react_jsx_runtime.JSX.Element;
74
+
75
+ declare function useMushi(): MushiRNInstance | null;
76
+
77
+ declare function useMushiReport(): {
78
+ submitReport: (data: {
79
+ description: string;
80
+ category: string;
81
+ }) => Promise<void>;
82
+ };
83
+
84
+ declare function useMushiWidget(): {
85
+ open: () => void | undefined;
86
+ close: () => void | undefined;
87
+ };
88
+
89
+ interface NavigationEntry {
90
+ screen: string;
91
+ timestamp: number;
92
+ }
93
+ declare function useNavigationCapture(navigationRef: {
94
+ getCurrentRoute?: () => {
95
+ name: string;
96
+ } | undefined;
97
+ addListener?: (event: string, cb: () => void) => () => void;
98
+ } | null): () => NavigationEntry[];
99
+
100
+ declare class AsyncStorageQueue {
101
+ private maxSize;
102
+ private apiEndpoint;
103
+ private apiKey;
104
+ constructor(config: {
105
+ maxSize?: number;
106
+ apiEndpoint: string;
107
+ apiKey: string;
108
+ });
109
+ enqueue(report: Record<string, unknown>): Promise<void>;
110
+ flush(): Promise<number>;
111
+ size(): Promise<number>;
112
+ private getQueue;
113
+ private getAsyncStorage;
114
+ }
115
+
116
+ export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiProvider, type MushiRNConfig, type MushiRNInstance, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
@@ -0,0 +1,116 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+
4
+ interface ConsoleEntry {
5
+ level: 'log' | 'warn' | 'error';
6
+ message: string;
7
+ timestamp: number;
8
+ }
9
+ declare function setupConsoleCapture(maxEntries?: number): {
10
+ getEntries: () => ConsoleEntry[];
11
+ clear: () => void;
12
+ restore: () => void;
13
+ };
14
+
15
+ interface NetworkEntry {
16
+ method: string;
17
+ url: string;
18
+ status: number | null;
19
+ duration: number;
20
+ error?: string;
21
+ timestamp: number;
22
+ }
23
+ declare function setupNetworkCapture(maxEntries?: number, apiEndpoint?: string): {
24
+ getEntries: () => NetworkEntry[];
25
+ clear: () => void;
26
+ restore: () => void;
27
+ };
28
+
29
+ interface DeviceInfo {
30
+ platform: string;
31
+ osVersion: string | number;
32
+ screenWidth: number;
33
+ screenHeight: number;
34
+ pixelRatio: number;
35
+ fontScale: number;
36
+ }
37
+ declare function getDeviceInfo(): DeviceInfo;
38
+
39
+ interface MushiRNConfig {
40
+ projectId: string;
41
+ apiKey: string;
42
+ endpoint?: string;
43
+ widget?: {
44
+ trigger?: 'shake' | 'button' | 'both';
45
+ shakeThreshold?: number;
46
+ buttonPosition?: 'bottom-right' | 'bottom-left';
47
+ };
48
+ capture?: {
49
+ console?: boolean;
50
+ network?: boolean;
51
+ navigation?: boolean;
52
+ maxConsoleEntries?: number;
53
+ maxNetworkEntries?: number;
54
+ };
55
+ storage?: {
56
+ maxQueueSize?: number;
57
+ retryIntervalMs?: number;
58
+ };
59
+ }
60
+ interface MushiRNInstance {
61
+ open(): void;
62
+ close(): void;
63
+ submitReport(data: {
64
+ description: string;
65
+ category: string;
66
+ }): Promise<void>;
67
+ getDeviceInfo(): ReturnType<typeof getDeviceInfo>;
68
+ getConsoleEntries(): ReturnType<ReturnType<typeof setupConsoleCapture>['getEntries']>;
69
+ getNetworkEntries(): ReturnType<ReturnType<typeof setupNetworkCapture>['getEntries']>;
70
+ }
71
+ declare function MushiProvider({ children, ...config }: MushiRNConfig & {
72
+ children: ReactNode;
73
+ }): react_jsx_runtime.JSX.Element;
74
+
75
+ declare function useMushi(): MushiRNInstance | null;
76
+
77
+ declare function useMushiReport(): {
78
+ submitReport: (data: {
79
+ description: string;
80
+ category: string;
81
+ }) => Promise<void>;
82
+ };
83
+
84
+ declare function useMushiWidget(): {
85
+ open: () => void | undefined;
86
+ close: () => void | undefined;
87
+ };
88
+
89
+ interface NavigationEntry {
90
+ screen: string;
91
+ timestamp: number;
92
+ }
93
+ declare function useNavigationCapture(navigationRef: {
94
+ getCurrentRoute?: () => {
95
+ name: string;
96
+ } | undefined;
97
+ addListener?: (event: string, cb: () => void) => () => void;
98
+ } | null): () => NavigationEntry[];
99
+
100
+ declare class AsyncStorageQueue {
101
+ private maxSize;
102
+ private apiEndpoint;
103
+ private apiKey;
104
+ constructor(config: {
105
+ maxSize?: number;
106
+ apiEndpoint: string;
107
+ apiKey: string;
108
+ });
109
+ enqueue(report: Record<string, unknown>): Promise<void>;
110
+ flush(): Promise<number>;
111
+ size(): Promise<number>;
112
+ private getQueue;
113
+ private getAsyncStorage;
114
+ }
115
+
116
+ export { AsyncStorageQueue, type ConsoleEntry, type DeviceInfo, MushiProvider, type MushiRNConfig, type MushiRNInstance, type NavigationEntry, type NetworkEntry, getDeviceInfo, setupConsoleCapture, setupNetworkCapture, useMushi, useMushiReport, useMushiWidget, useNavigationCapture };
package/dist/index.js ADDED
@@ -0,0 +1,292 @@
1
+ // src/provider.tsx
2
+ import { createContext, useContext, useEffect, useRef } from "react";
3
+
4
+ // src/capture/console-capture.ts
5
+ function setupConsoleCapture(maxEntries = 100) {
6
+ const entries = [];
7
+ const methods = ["log", "warn", "error"];
8
+ const originals = /* @__PURE__ */ new Map();
9
+ for (const method of methods) {
10
+ const original = console[method].bind(console);
11
+ originals.set(method, original);
12
+ console[method] = (...args) => {
13
+ entries.push({
14
+ level: method,
15
+ message: args.map(String).join(" ").slice(0, 500),
16
+ timestamp: Date.now()
17
+ });
18
+ if (entries.length > maxEntries) entries.shift();
19
+ original(...args);
20
+ };
21
+ }
22
+ return {
23
+ getEntries: () => [...entries],
24
+ clear: () => {
25
+ entries.length = 0;
26
+ },
27
+ restore: () => {
28
+ for (const method of methods) {
29
+ const orig = originals.get(method);
30
+ if (orig) console[method] = orig;
31
+ }
32
+ }
33
+ };
34
+ }
35
+
36
+ // src/capture/network-capture.ts
37
+ function setupNetworkCapture(maxEntries = 50, apiEndpoint) {
38
+ const entries = [];
39
+ const origFetch = globalThis.fetch;
40
+ globalThis.fetch = async function(input, init) {
41
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
42
+ const method = init?.method ?? "GET";
43
+ if (apiEndpoint && url.includes(apiEndpoint)) {
44
+ return origFetch.apply(this, [input, init]);
45
+ }
46
+ const start = Date.now();
47
+ try {
48
+ const res = await origFetch.apply(this, [input, init]);
49
+ entries.push({
50
+ method,
51
+ url: stripQueryParams(url),
52
+ status: res.status,
53
+ duration: Date.now() - start,
54
+ timestamp: start
55
+ });
56
+ if (entries.length > maxEntries) entries.shift();
57
+ return res;
58
+ } catch (err) {
59
+ entries.push({
60
+ method,
61
+ url: stripQueryParams(url),
62
+ status: null,
63
+ duration: Date.now() - start,
64
+ error: String(err),
65
+ timestamp: start
66
+ });
67
+ if (entries.length > maxEntries) entries.shift();
68
+ throw err;
69
+ }
70
+ };
71
+ return {
72
+ getEntries: () => [...entries],
73
+ clear: () => {
74
+ entries.length = 0;
75
+ },
76
+ restore: () => {
77
+ globalThis.fetch = origFetch;
78
+ }
79
+ };
80
+ }
81
+ function stripQueryParams(url) {
82
+ try {
83
+ const u = new URL(url);
84
+ return `${u.origin}${u.pathname}`;
85
+ } catch {
86
+ return url.split("?")[0];
87
+ }
88
+ }
89
+
90
+ // src/capture/device-info.ts
91
+ import { Platform, Dimensions, PixelRatio } from "react-native";
92
+ function getDeviceInfo() {
93
+ const { width, height } = Dimensions.get("window");
94
+ return {
95
+ platform: Platform.OS,
96
+ osVersion: Platform.Version,
97
+ screenWidth: width,
98
+ screenHeight: height,
99
+ pixelRatio: PixelRatio.get(),
100
+ fontScale: PixelRatio.getFontScale()
101
+ };
102
+ }
103
+
104
+ // src/storage/async-storage-queue.ts
105
+ var QUEUE_KEY = "@mushi:offline_queue";
106
+ var AsyncStorageQueue = class {
107
+ maxSize;
108
+ apiEndpoint;
109
+ apiKey;
110
+ constructor(config) {
111
+ this.maxSize = config.maxSize ?? 50;
112
+ this.apiEndpoint = config.apiEndpoint;
113
+ this.apiKey = config.apiKey;
114
+ }
115
+ async enqueue(report) {
116
+ const AsyncStorage = await this.getAsyncStorage();
117
+ if (!AsyncStorage) return;
118
+ const queue = await this.getQueue(AsyncStorage);
119
+ if (queue.length >= this.maxSize) queue.shift();
120
+ queue.push({ report, enqueuedAt: Date.now() });
121
+ await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
122
+ }
123
+ async flush() {
124
+ const AsyncStorage = await this.getAsyncStorage();
125
+ if (!AsyncStorage) return 0;
126
+ const queue = await this.getQueue(AsyncStorage);
127
+ if (!queue.length) return 0;
128
+ let flushed = 0;
129
+ const remaining = [];
130
+ for (const item of queue) {
131
+ try {
132
+ const res = await fetch(`${this.apiEndpoint}/v1/reports`, {
133
+ method: "POST",
134
+ headers: {
135
+ "Content-Type": "application/json",
136
+ "X-Mushi-Api-Key": this.apiKey
137
+ },
138
+ body: JSON.stringify(item.report)
139
+ });
140
+ if (res.ok) {
141
+ flushed++;
142
+ } else {
143
+ remaining.push(item);
144
+ }
145
+ } catch {
146
+ remaining.push(item);
147
+ break;
148
+ }
149
+ }
150
+ await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(remaining));
151
+ return flushed;
152
+ }
153
+ async size() {
154
+ const AsyncStorage = await this.getAsyncStorage();
155
+ if (!AsyncStorage) return 0;
156
+ const queue = await this.getQueue(AsyncStorage);
157
+ return queue.length;
158
+ }
159
+ async getQueue(storage) {
160
+ try {
161
+ const raw = await storage.getItem(QUEUE_KEY);
162
+ return raw ? JSON.parse(raw) : [];
163
+ } catch {
164
+ return [];
165
+ }
166
+ }
167
+ async getAsyncStorage() {
168
+ try {
169
+ const mod = await import("@react-native-async-storage/async-storage");
170
+ return mod.default;
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+ };
176
+
177
+ // src/provider.tsx
178
+ import { jsx } from "react/jsx-runtime";
179
+ var MushiContext = createContext(null);
180
+ function MushiProvider({ children, ...config }) {
181
+ const consoleRef = useRef(null);
182
+ const networkRef = useRef(null);
183
+ const queueRef = useRef(null);
184
+ const apiEndpoint = config.endpoint ?? "https://api.mushimushi.dev";
185
+ useEffect(() => {
186
+ if (config.capture?.console !== false) {
187
+ consoleRef.current = setupConsoleCapture(config.capture?.maxConsoleEntries);
188
+ }
189
+ if (config.capture?.network !== false) {
190
+ networkRef.current = setupNetworkCapture(config.capture?.maxNetworkEntries, apiEndpoint);
191
+ }
192
+ queueRef.current = new AsyncStorageQueue({
193
+ maxSize: config.storage?.maxQueueSize,
194
+ apiEndpoint,
195
+ apiKey: config.apiKey
196
+ });
197
+ return () => {
198
+ consoleRef.current?.restore();
199
+ networkRef.current?.restore();
200
+ };
201
+ }, []);
202
+ const instance = {
203
+ open: () => {
204
+ },
205
+ close: () => {
206
+ },
207
+ submitReport: async (data) => {
208
+ const report = {
209
+ projectId: config.projectId,
210
+ ...data,
211
+ environment: getDeviceInfo(),
212
+ console_logs: consoleRef.current?.getEntries() ?? [],
213
+ network_logs: networkRef.current?.getEntries() ?? []
214
+ };
215
+ try {
216
+ const res = await fetch(`${apiEndpoint}/v1/reports`, {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/json", "X-Mushi-Api-Key": config.apiKey },
219
+ body: JSON.stringify(report)
220
+ });
221
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
222
+ } catch {
223
+ await queueRef.current?.enqueue(report);
224
+ }
225
+ },
226
+ getDeviceInfo,
227
+ getConsoleEntries: () => consoleRef.current?.getEntries() ?? [],
228
+ getNetworkEntries: () => networkRef.current?.getEntries() ?? []
229
+ };
230
+ return /* @__PURE__ */ jsx(MushiContext.Provider, { value: instance, children });
231
+ }
232
+ function useMushiContext() {
233
+ return useContext(MushiContext);
234
+ }
235
+
236
+ // src/hooks/useMushi.ts
237
+ function useMushi() {
238
+ return useMushiContext();
239
+ }
240
+
241
+ // src/hooks/useMushiReport.ts
242
+ import { useCallback } from "react";
243
+ function useMushiReport() {
244
+ const mushi = useMushiContext();
245
+ const submitReport = useCallback(
246
+ async (data) => {
247
+ if (!mushi) throw new Error("MushiProvider not found");
248
+ await mushi.submitReport(data);
249
+ },
250
+ [mushi]
251
+ );
252
+ return { submitReport };
253
+ }
254
+
255
+ // src/hooks/useMushiWidget.ts
256
+ import { useCallback as useCallback2 } from "react";
257
+ function useMushiWidget() {
258
+ const mushi = useMushiContext();
259
+ return {
260
+ open: useCallback2(() => mushi?.open(), [mushi]),
261
+ close: useCallback2(() => mushi?.close(), [mushi])
262
+ };
263
+ }
264
+
265
+ // src/capture/navigation-capture.ts
266
+ import { useEffect as useEffect2, useRef as useRef2 } from "react";
267
+ function useNavigationCapture(navigationRef) {
268
+ const historyRef = useRef2([]);
269
+ useEffect2(() => {
270
+ if (!navigationRef?.addListener || !navigationRef?.getCurrentRoute) return;
271
+ const unsubscribe = navigationRef.addListener("state", () => {
272
+ const route = navigationRef.getCurrentRoute?.();
273
+ if (route) {
274
+ historyRef.current.push({ screen: route.name, timestamp: Date.now() });
275
+ if (historyRef.current.length > 20) historyRef.current.shift();
276
+ }
277
+ });
278
+ return unsubscribe;
279
+ }, [navigationRef]);
280
+ return () => [...historyRef.current];
281
+ }
282
+ export {
283
+ AsyncStorageQueue,
284
+ MushiProvider,
285
+ getDeviceInfo,
286
+ setupConsoleCapture,
287
+ setupNetworkCapture,
288
+ useMushi,
289
+ useMushiReport,
290
+ useMushiWidget,
291
+ useNavigationCapture
292
+ };
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@mushi-mushi/react-native",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "React Native SDK for Mushi Mushi — shake-to-report, bottom sheet widget, full context capture",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "test": "echo 'no tests yet'",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "peerDependencies": {
15
+ "react": ">=18",
16
+ "react-native": ">=0.72",
17
+ "@react-navigation/native": ">=6",
18
+ "@react-native-async-storage/async-storage": ">=1.19"
19
+ },
20
+ "peerDependenciesMeta": {
21
+ "@react-navigation/native": {
22
+ "optional": true
23
+ },
24
+ "@react-native-async-storage/async-storage": {
25
+ "optional": true
26
+ }
27
+ },
28
+ "dependencies": {},
29
+ "devDependencies": {
30
+ "@types/react": "^19.1.4",
31
+ "react": "^19.1.0",
32
+ "react-native": "^0.79.0",
33
+ "tsup": "^8.5.1",
34
+ "typescript": "^5.9.3"
35
+ },
36
+ "author": "Kenji Sakuramoto",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/kensaurus/mushi-mushi.git",
40
+ "directory": "packages/react-native"
41
+ },
42
+ "homepage": "https://github.com/kensaurus/mushi-mushi/tree/main/packages/react-native#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/kensaurus/mushi-mushi/issues"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "README.md",
52
+ "LICENSE"
53
+ ],
54
+ "sideEffects": false,
55
+ "keywords": [
56
+ "mushi-mushi",
57
+ "bug-reporting",
58
+ "sdk"
59
+ ],
60
+ "exports": {
61
+ ".": {
62
+ "import": {
63
+ "types": "./dist/index.d.ts",
64
+ "default": "./dist/index.js"
65
+ },
66
+ "require": {
67
+ "types": "./dist/index.d.cts",
68
+ "default": "./dist/index.cjs"
69
+ }
70
+ }
71
+ },
72
+ "type": "module"
73
+ }