react-native-ai-devtools-sdk 0.2.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 +215 -0
- package/dist/consoleBuffer.d.ts +13 -0
- package/dist/consoleBuffer.js +42 -0
- package/dist/consoleInterceptor.d.ts +3 -0
- package/dist/consoleInterceptor.js +53 -0
- package/dist/global.d.ts +13 -0
- package/dist/global.js +18 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +41 -0
- package/dist/networkBuffer.d.ts +13 -0
- package/dist/networkBuffer.js +98 -0
- package/dist/networkInterceptor.d.ts +3 -0
- package/dist/networkInterceptor.js +136 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.js +2 -0
- package/package.json +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# react-native-ai-devtools-sdk
|
|
2
|
+
|
|
3
|
+
Lightweight companion SDK for [react-native-ai-devtools](https://www.npmjs.com/package/react-native-ai-devtools) — captures network requests, console logs, and state store references from your React Native app for AI-powered debugging.
|
|
4
|
+
|
|
5
|
+
## Why use this SDK?
|
|
6
|
+
|
|
7
|
+
The MCP server (`react-native-ai-devtools`) connects to your app via Chrome DevTools Protocol (CDP). This works great for most features, but CDP has limitations on newer React Native architectures (Expo SDK 52+, Bridgeless):
|
|
8
|
+
|
|
9
|
+
| | Without SDK | With SDK |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| Startup network requests (auth, config) | Missed | Captured from first fetch |
|
|
12
|
+
| Request/response headers | Partial | Full |
|
|
13
|
+
| Request/response bodies | Not available | Full (including GraphQL) |
|
|
14
|
+
| Console logs from startup | May miss early logs | Captured from first log |
|
|
15
|
+
| State store access | Manual via `execute_in_app` | Direct references exposed |
|
|
16
|
+
| Works on Bridgeless (Expo SDK 52+) | Partial | Full |
|
|
17
|
+
| Setup | None | One import |
|
|
18
|
+
|
|
19
|
+
The SDK patches `fetch` and `console` at import time and stores everything in an in-app buffer. The MCP server automatically detects the SDK and reads from it — no extra configuration needed.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install react-native-ai-devtools-sdk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
Add to your app's entry file (`index.js`, `App.tsx`, or `app/_layout.tsx` for Expo Router) — **must be the first import**:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { init } from 'react-native-ai-devtools-sdk';
|
|
33
|
+
if (__DEV__) {
|
|
34
|
+
init();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ... rest of your imports
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
That's it. The MCP tools (`get_network_requests`, `get_logs`, etc.) will automatically use the SDK data when available.
|
|
41
|
+
|
|
42
|
+
### With state stores
|
|
43
|
+
|
|
44
|
+
Pass references to your state management stores for direct AI access:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { init } from 'react-native-ai-devtools-sdk';
|
|
48
|
+
import { store } from './store'; // Redux store
|
|
49
|
+
import { queryClient } from './queryClient'; // TanStack Query
|
|
50
|
+
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
init({
|
|
53
|
+
stores: {
|
|
54
|
+
redux: store,
|
|
55
|
+
queryClient: queryClient,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The AI assistant can then inspect store state directly:
|
|
62
|
+
```
|
|
63
|
+
execute_in_app with expression="globalThis.__RN_AI_DEVTOOLS__.stores.redux.getState()"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Configuration options
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
init({
|
|
70
|
+
// Max network entries to buffer (default: 500)
|
|
71
|
+
maxNetworkEntries: 500,
|
|
72
|
+
|
|
73
|
+
// Max console entries to buffer (default: 500)
|
|
74
|
+
maxConsoleEntries: 500,
|
|
75
|
+
|
|
76
|
+
// State store references for AI access
|
|
77
|
+
stores: {
|
|
78
|
+
redux: reduxStore,
|
|
79
|
+
queryClient: queryClient,
|
|
80
|
+
userStore: useUserStore,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## How it works
|
|
86
|
+
|
|
87
|
+
### Architecture
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
React Native App
|
|
91
|
+
|
|
|
92
|
+
| 1. import { init } from 'react-native-ai-devtools-sdk'
|
|
93
|
+
| → patches globalThis.fetch (captures all network requests)
|
|
94
|
+
| → patches console.log/warn/error/info/debug (captures all logs)
|
|
95
|
+
| → stores references to state management stores
|
|
96
|
+
| → exposes globalThis.__RN_AI_DEVTOOLS__ with query methods
|
|
97
|
+
|
|
|
98
|
+
| 2. App runs normally — all fetch() calls and console output
|
|
99
|
+
| are intercepted, stored in circular buffers, and passed
|
|
100
|
+
| through to their original implementations unchanged
|
|
101
|
+
|
|
|
102
|
+
v
|
|
103
|
+
MCP Server (react-native-ai-devtools)
|
|
104
|
+
|
|
|
105
|
+
| 3. Connects to app via CDP (Chrome DevTools Protocol)
|
|
106
|
+
| Detects SDK: typeof globalThis.__RN_AI_DEVTOOLS__?.getNetworkRequests === "function"
|
|
107
|
+
|
|
|
108
|
+
| 4. MCP tools read SDK data via Runtime.evaluate:
|
|
109
|
+
| get_network_requests → globalThis.__RN_AI_DEVTOOLS__.getNetworkRequests()
|
|
110
|
+
| get_request_details → globalThis.__RN_AI_DEVTOOLS__.getNetworkRequest(id)
|
|
111
|
+
| get_logs (future) → globalThis.__RN_AI_DEVTOOLS__.getConsoleLogs()
|
|
112
|
+
|
|
|
113
|
+
v
|
|
114
|
+
AI Assistant (Claude Code, Cursor, VS Code Copilot, etc.)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### What gets captured
|
|
118
|
+
|
|
119
|
+
**Network requests** — Every `fetch()` call is intercepted. The SDK captures:
|
|
120
|
+
- Method, URL, status, statusText, duration
|
|
121
|
+
- Full request and response headers
|
|
122
|
+
- Full request and response bodies (via `response.clone().text()` — the original response is untouched)
|
|
123
|
+
- Errors and timing
|
|
124
|
+
|
|
125
|
+
**Console output** — Every `console.log/warn/error/info/debug` call is captured with:
|
|
126
|
+
- Log level, timestamp, formatted message
|
|
127
|
+
- Original console methods still work — logs appear in Xcode/Metro/DevTools as normal
|
|
128
|
+
|
|
129
|
+
**State stores** — References passed via `stores` option are exposed globally for the MCP server to query on demand.
|
|
130
|
+
|
|
131
|
+
### Why it must be the first import
|
|
132
|
+
|
|
133
|
+
The SDK patches `globalThis.fetch` and `console` when `init()` is called. If other code (your app, libraries like Apollo/Axios) calls `fetch` before the SDK patches it, those requests won't be captured. Placing the import first ensures the SDK intercepts everything from the very beginning, including:
|
|
134
|
+
|
|
135
|
+
- OAuth token refresh on app launch
|
|
136
|
+
- Initial GraphQL/REST API calls
|
|
137
|
+
- Config/feature flag fetches
|
|
138
|
+
- Early console output during initialization
|
|
139
|
+
|
|
140
|
+
### Production safety
|
|
141
|
+
|
|
142
|
+
The SDK is a no-op in production builds:
|
|
143
|
+
|
|
144
|
+
1. The `if (__DEV__)` guard in your code prevents `init()` from being called
|
|
145
|
+
2. Even if called without the guard, `init()` checks `__DEV__` internally as a safety net
|
|
146
|
+
3. Tree-shaking removes the SDK code from production bundles when wrapped in `if (__DEV__)`
|
|
147
|
+
|
|
148
|
+
### Circular buffers
|
|
149
|
+
|
|
150
|
+
Both network and console data are stored in circular buffers (default: 500 entries each). When the buffer is full, the oldest entries are evicted. This bounds memory usage regardless of how many requests or logs the app produces.
|
|
151
|
+
|
|
152
|
+
## Exposed global API
|
|
153
|
+
|
|
154
|
+
The SDK exposes `globalThis.__RN_AI_DEVTOOLS__` with these methods. You don't need to call these directly — the MCP tools use them automatically.
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
globalThis.__RN_AI_DEVTOOLS__ = {
|
|
158
|
+
version: '0.2.0',
|
|
159
|
+
|
|
160
|
+
// Capabilities — tells MCP server what's available
|
|
161
|
+
capabilities: {
|
|
162
|
+
network: true,
|
|
163
|
+
console: true,
|
|
164
|
+
stores: true, // true if stores were passed
|
|
165
|
+
render: false, // future: render profiling
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// State store references
|
|
169
|
+
stores: { redux: store, queryClient: qc, ... },
|
|
170
|
+
|
|
171
|
+
// Network
|
|
172
|
+
getNetworkRequests(options?), // { count, method, urlPattern, status }
|
|
173
|
+
getNetworkRequest(id), // full details including bodies
|
|
174
|
+
getNetworkStats(),
|
|
175
|
+
clearNetwork(),
|
|
176
|
+
|
|
177
|
+
// Console
|
|
178
|
+
getConsoleLogs(options?), // { count, level, text }
|
|
179
|
+
clearConsole(),
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Compatibility
|
|
184
|
+
|
|
185
|
+
| React Native | Architecture | Status |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| Expo SDK 54+ (RN 0.79+) | Bridgeless | Fully supported |
|
|
188
|
+
| Expo SDK 52-53 (RN 0.76-0.78) | Bridgeless | Fully supported |
|
|
189
|
+
| RN 0.73-0.75 | Hermes + Bridge | Fully supported |
|
|
190
|
+
| RN 0.70-0.72 | Hermes + Bridge | Should work (untested) |
|
|
191
|
+
| RN < 0.70 | JSC | Not tested |
|
|
192
|
+
|
|
193
|
+
The SDK has zero native dependencies — it's pure JavaScript that patches standard globals (`fetch`, `console`). It works on any React Native version that supports these globals.
|
|
194
|
+
|
|
195
|
+
## Relationship to react-native-ai-devtools
|
|
196
|
+
|
|
197
|
+
This SDK is an **optional companion** to the [react-native-ai-devtools](https://github.com/nickmcdonnough/react-native-ai-devtools) MCP server. The MCP server works without the SDK — it connects via CDP and provides console logs, component inspection, UI interaction, and basic network tracking out of the box.
|
|
198
|
+
|
|
199
|
+
The SDK enhances network and console capture for cases where CDP alone isn't sufficient (Bridgeless architecture, startup request capture, response bodies). When the MCP server detects the SDK, it automatically prefers SDK data. When the SDK is absent, it falls back to CDP.
|
|
200
|
+
|
|
201
|
+
**You do NOT need the SDK for:**
|
|
202
|
+
- Console log viewing (`get_logs`)
|
|
203
|
+
- Component tree inspection (`get_component_tree`, `inspect_component`)
|
|
204
|
+
- UI interaction (`tap`, `swipe`, screenshots)
|
|
205
|
+
- JavaScript execution (`execute_in_app`)
|
|
206
|
+
- App reload, bundle error detection, device management
|
|
207
|
+
|
|
208
|
+
**The SDK improves:**
|
|
209
|
+
- Network request capture (especially startup requests and response bodies)
|
|
210
|
+
- Console log capture (startup logs that CDP might miss)
|
|
211
|
+
- State store access (direct references vs manual global inspection)
|
|
212
|
+
|
|
213
|
+
## License
|
|
214
|
+
|
|
215
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ConsoleEntry, ConsoleQueryOptions } from './types';
|
|
2
|
+
export declare class ConsoleBuffer {
|
|
3
|
+
private entries;
|
|
4
|
+
private maxSize;
|
|
5
|
+
constructor(maxSize?: number);
|
|
6
|
+
add(entry: ConsoleEntry): void;
|
|
7
|
+
query(options?: ConsoleQueryOptions): ConsoleEntry[];
|
|
8
|
+
getStats(): {
|
|
9
|
+
total: number;
|
|
10
|
+
byLevel: Record<string, number>;
|
|
11
|
+
};
|
|
12
|
+
clear(): number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConsoleBuffer = void 0;
|
|
4
|
+
class ConsoleBuffer {
|
|
5
|
+
constructor(maxSize = 500) {
|
|
6
|
+
this.entries = [];
|
|
7
|
+
this.maxSize = maxSize;
|
|
8
|
+
}
|
|
9
|
+
add(entry) {
|
|
10
|
+
if (this.entries.length >= this.maxSize) {
|
|
11
|
+
this.entries.shift();
|
|
12
|
+
}
|
|
13
|
+
this.entries.push(entry);
|
|
14
|
+
}
|
|
15
|
+
query(options) {
|
|
16
|
+
let results = [...this.entries].reverse();
|
|
17
|
+
if (options?.level) {
|
|
18
|
+
results = results.filter((e) => e.level === options.level);
|
|
19
|
+
}
|
|
20
|
+
if (options?.text) {
|
|
21
|
+
const text = options.text.toLowerCase();
|
|
22
|
+
results = results.filter((e) => e.message.toLowerCase().includes(text));
|
|
23
|
+
}
|
|
24
|
+
if (options?.count != null && options.count > 0) {
|
|
25
|
+
results = results.slice(0, options.count);
|
|
26
|
+
}
|
|
27
|
+
return results;
|
|
28
|
+
}
|
|
29
|
+
getStats() {
|
|
30
|
+
const byLevel = {};
|
|
31
|
+
for (const entry of this.entries) {
|
|
32
|
+
byLevel[entry.level] = (byLevel[entry.level] || 0) + 1;
|
|
33
|
+
}
|
|
34
|
+
return { total: this.entries.length, byLevel };
|
|
35
|
+
}
|
|
36
|
+
clear() {
|
|
37
|
+
const count = this.entries.length;
|
|
38
|
+
this.entries = [];
|
|
39
|
+
return count;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.ConsoleBuffer = ConsoleBuffer;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.patchConsole = patchConsole;
|
|
4
|
+
exports.unpatchConsole = unpatchConsole;
|
|
5
|
+
const LEVELS = ['log', 'warn', 'error', 'info', 'debug'];
|
|
6
|
+
let originals = null;
|
|
7
|
+
let idCounter = 0;
|
|
8
|
+
function generateId() {
|
|
9
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
10
|
+
return `con-${random}-${++idCounter}`;
|
|
11
|
+
}
|
|
12
|
+
function formatArgs(args) {
|
|
13
|
+
return args
|
|
14
|
+
.map((arg) => {
|
|
15
|
+
if (typeof arg === 'string')
|
|
16
|
+
return arg;
|
|
17
|
+
try {
|
|
18
|
+
return JSON.stringify(arg);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return String(arg);
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.join(' ');
|
|
25
|
+
}
|
|
26
|
+
function patchConsole(buffer) {
|
|
27
|
+
if (originals) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
originals = {};
|
|
31
|
+
for (const level of LEVELS) {
|
|
32
|
+
originals[level] = console[level];
|
|
33
|
+
console[level] = (...args) => {
|
|
34
|
+
const entry = {
|
|
35
|
+
id: generateId(),
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
level,
|
|
38
|
+
message: formatArgs(args),
|
|
39
|
+
};
|
|
40
|
+
buffer.add(entry);
|
|
41
|
+
originals[level].apply(console, args);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function unpatchConsole() {
|
|
46
|
+
if (originals) {
|
|
47
|
+
for (const level of LEVELS) {
|
|
48
|
+
console[level] = originals[level];
|
|
49
|
+
}
|
|
50
|
+
originals = null;
|
|
51
|
+
}
|
|
52
|
+
idCounter = 0;
|
|
53
|
+
}
|
package/dist/global.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NetworkBuffer } from './networkBuffer';
|
|
2
|
+
import { ConsoleBuffer } from './consoleBuffer';
|
|
3
|
+
import { DevToolsGlobal, Capabilities } from './types';
|
|
4
|
+
declare global {
|
|
5
|
+
var __RN_AI_DEVTOOLS__: DevToolsGlobal | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface ExposeGlobalOptions {
|
|
8
|
+
networkBuffer: NetworkBuffer;
|
|
9
|
+
consoleBuffer: ConsoleBuffer;
|
|
10
|
+
stores: Record<string, unknown>;
|
|
11
|
+
capabilities: Capabilities;
|
|
12
|
+
}
|
|
13
|
+
export declare function exposeGlobal(options: ExposeGlobalOptions): void;
|
package/dist/global.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exposeGlobal = exposeGlobal;
|
|
4
|
+
function exposeGlobal(options) {
|
|
5
|
+
const { networkBuffer, consoleBuffer, stores, capabilities } = options;
|
|
6
|
+
const devtools = {
|
|
7
|
+
version: '0.2.0',
|
|
8
|
+
capabilities,
|
|
9
|
+
stores,
|
|
10
|
+
getNetworkRequests: (opts) => networkBuffer.query(opts),
|
|
11
|
+
getNetworkRequest: (id) => networkBuffer.get(id),
|
|
12
|
+
getNetworkStats: () => networkBuffer.getStats(),
|
|
13
|
+
clearNetwork: () => networkBuffer.clear(),
|
|
14
|
+
getConsoleLogs: (opts) => consoleBuffer.query(opts),
|
|
15
|
+
clearConsole: () => consoleBuffer.clear(),
|
|
16
|
+
};
|
|
17
|
+
globalThis.__RN_AI_DEVTOOLS__ = devtools;
|
|
18
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { InitOptions } from './types';
|
|
2
|
+
export type { InitOptions, NetworkEntry, NetworkQueryOptions, NetworkStats, ConsoleEntry, ConsoleQueryOptions, Capabilities, DevToolsGlobal, } from './types';
|
|
3
|
+
export declare function init(options?: InitOptions): void;
|
|
4
|
+
export declare function _resetForTesting(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.init = init;
|
|
4
|
+
exports._resetForTesting = _resetForTesting;
|
|
5
|
+
const networkBuffer_1 = require("./networkBuffer");
|
|
6
|
+
const consoleBuffer_1 = require("./consoleBuffer");
|
|
7
|
+
const networkInterceptor_1 = require("./networkInterceptor");
|
|
8
|
+
const consoleInterceptor_1 = require("./consoleInterceptor");
|
|
9
|
+
const global_1 = require("./global");
|
|
10
|
+
let initialized = false;
|
|
11
|
+
function init(options) {
|
|
12
|
+
if (initialized) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
// Safety net: no-op in production
|
|
16
|
+
if (typeof __DEV__ !== 'undefined' && !__DEV__) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const networkBuffer = new networkBuffer_1.NetworkBuffer(options?.maxNetworkEntries ?? 500);
|
|
20
|
+
const consoleBuffer = new consoleBuffer_1.ConsoleBuffer(options?.maxConsoleEntries ?? 500);
|
|
21
|
+
const stores = options?.stores ?? {};
|
|
22
|
+
(0, networkInterceptor_1.patchFetch)(networkBuffer);
|
|
23
|
+
(0, consoleInterceptor_1.patchConsole)(consoleBuffer);
|
|
24
|
+
(0, global_1.exposeGlobal)({
|
|
25
|
+
networkBuffer,
|
|
26
|
+
consoleBuffer,
|
|
27
|
+
stores,
|
|
28
|
+
capabilities: {
|
|
29
|
+
network: true,
|
|
30
|
+
console: true,
|
|
31
|
+
stores: Object.keys(stores).length > 0,
|
|
32
|
+
render: false,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
initialized = true;
|
|
36
|
+
}
|
|
37
|
+
// Exported for testing purposes
|
|
38
|
+
function _resetForTesting() {
|
|
39
|
+
initialized = false;
|
|
40
|
+
delete globalThis.__RN_AI_DEVTOOLS__;
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NetworkEntry, NetworkQueryOptions, NetworkStats } from './types';
|
|
2
|
+
export declare class NetworkBuffer {
|
|
3
|
+
private entries;
|
|
4
|
+
private order;
|
|
5
|
+
private maxSize;
|
|
6
|
+
constructor(maxSize?: number);
|
|
7
|
+
add(entry: NetworkEntry): void;
|
|
8
|
+
update(id: string, updates: Partial<NetworkEntry>): void;
|
|
9
|
+
get(id: string): NetworkEntry | null;
|
|
10
|
+
query(options?: NetworkQueryOptions): NetworkEntry[];
|
|
11
|
+
getStats(): NetworkStats;
|
|
12
|
+
clear(): number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NetworkBuffer = void 0;
|
|
4
|
+
class NetworkBuffer {
|
|
5
|
+
constructor(maxSize = 500) {
|
|
6
|
+
this.entries = new Map();
|
|
7
|
+
this.order = [];
|
|
8
|
+
this.maxSize = maxSize;
|
|
9
|
+
}
|
|
10
|
+
add(entry) {
|
|
11
|
+
if (this.entries.has(entry.id)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (this.order.length >= this.maxSize) {
|
|
15
|
+
const oldestId = this.order.shift();
|
|
16
|
+
this.entries.delete(oldestId);
|
|
17
|
+
}
|
|
18
|
+
this.entries.set(entry.id, entry);
|
|
19
|
+
this.order.push(entry.id);
|
|
20
|
+
}
|
|
21
|
+
update(id, updates) {
|
|
22
|
+
const entry = this.entries.get(id);
|
|
23
|
+
if (entry) {
|
|
24
|
+
Object.assign(entry, updates);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get(id) {
|
|
28
|
+
return this.entries.get(id) ?? null;
|
|
29
|
+
}
|
|
30
|
+
query(options) {
|
|
31
|
+
let results = Array.from(this.order)
|
|
32
|
+
.map((id) => this.entries.get(id))
|
|
33
|
+
.reverse();
|
|
34
|
+
if (options?.method) {
|
|
35
|
+
const method = options.method.toUpperCase();
|
|
36
|
+
results = results.filter((e) => e.method === method);
|
|
37
|
+
}
|
|
38
|
+
if (options?.urlPattern) {
|
|
39
|
+
const pattern = options.urlPattern.toLowerCase();
|
|
40
|
+
results = results.filter((e) => e.url.toLowerCase().includes(pattern));
|
|
41
|
+
}
|
|
42
|
+
if (options?.status != null) {
|
|
43
|
+
results = results.filter((e) => e.status === options.status);
|
|
44
|
+
}
|
|
45
|
+
if (options?.count != null && options.count > 0) {
|
|
46
|
+
results = results.slice(0, options.count);
|
|
47
|
+
}
|
|
48
|
+
return results;
|
|
49
|
+
}
|
|
50
|
+
getStats() {
|
|
51
|
+
const all = Array.from(this.entries.values());
|
|
52
|
+
const completed = all.filter((e) => e.completed && !e.error);
|
|
53
|
+
const errors = all.filter((e) => !!e.error);
|
|
54
|
+
const durations = completed
|
|
55
|
+
.map((e) => e.duration)
|
|
56
|
+
.filter((d) => d != null);
|
|
57
|
+
const avgDuration = durations.length > 0
|
|
58
|
+
? durations.reduce((sum, d) => sum + d, 0) / durations.length
|
|
59
|
+
: null;
|
|
60
|
+
const byMethod = {};
|
|
61
|
+
for (const entry of all) {
|
|
62
|
+
byMethod[entry.method] = (byMethod[entry.method] || 0) + 1;
|
|
63
|
+
}
|
|
64
|
+
const byStatus = {};
|
|
65
|
+
for (const entry of all) {
|
|
66
|
+
if (entry.status != null) {
|
|
67
|
+
const group = `${Math.floor(entry.status / 100)}xx`;
|
|
68
|
+
byStatus[group] = (byStatus[group] || 0) + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const byDomain = {};
|
|
72
|
+
for (const entry of all) {
|
|
73
|
+
try {
|
|
74
|
+
const domain = new URL(entry.url).hostname;
|
|
75
|
+
byDomain[domain] = (byDomain[domain] || 0) + 1;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// skip malformed URLs
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
total: all.length,
|
|
83
|
+
completed: completed.length,
|
|
84
|
+
errors: errors.length,
|
|
85
|
+
avgDuration,
|
|
86
|
+
byMethod,
|
|
87
|
+
byStatus,
|
|
88
|
+
byDomain,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
clear() {
|
|
92
|
+
const count = this.entries.size;
|
|
93
|
+
this.entries.clear();
|
|
94
|
+
this.order = [];
|
|
95
|
+
return count;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.NetworkBuffer = NetworkBuffer;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.patchFetch = patchFetch;
|
|
4
|
+
exports.unpatchFetch = unpatchFetch;
|
|
5
|
+
let originalFetch = null;
|
|
6
|
+
let idCounter = 0;
|
|
7
|
+
function generateId() {
|
|
8
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
9
|
+
return `sdk-${random}-${++idCounter}`;
|
|
10
|
+
}
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
function extractHeaders(headers) {
|
|
13
|
+
const result = {};
|
|
14
|
+
if (!headers) {
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
if (typeof headers.forEach === 'function') {
|
|
18
|
+
headers.forEach((value, key) => {
|
|
19
|
+
result[key] = value;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
else if (Array.isArray(headers)) {
|
|
23
|
+
for (const [key, value] of headers) {
|
|
24
|
+
result[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (typeof headers === 'object') {
|
|
28
|
+
Object.assign(result, headers);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
function extractBody(body) {
|
|
34
|
+
if (body == null) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
if (typeof body === 'string') {
|
|
38
|
+
return body;
|
|
39
|
+
}
|
|
40
|
+
return '[non-string body]';
|
|
41
|
+
}
|
|
42
|
+
function responseHeadersToRecord(headers) {
|
|
43
|
+
const result = {};
|
|
44
|
+
headers.forEach((value, key) => {
|
|
45
|
+
result[key] = value;
|
|
46
|
+
});
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
function patchFetch(buffer) {
|
|
50
|
+
if (originalFetch) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
originalFetch = globalThis.fetch;
|
|
54
|
+
globalThis.fetch = async function patchedFetch(input, init) {
|
|
55
|
+
const id = generateId();
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
let url;
|
|
58
|
+
let method;
|
|
59
|
+
let requestHeaders;
|
|
60
|
+
let requestBody;
|
|
61
|
+
try {
|
|
62
|
+
if (input instanceof Request) {
|
|
63
|
+
url = input.url;
|
|
64
|
+
method = (init?.method || input.method || 'GET').toUpperCase();
|
|
65
|
+
requestHeaders = extractHeaders(init?.headers ?? input.headers);
|
|
66
|
+
requestBody = extractBody(init?.body);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
url = typeof input === 'string' ? input : input.toString();
|
|
70
|
+
method = (init?.method || 'GET').toUpperCase();
|
|
71
|
+
requestHeaders = extractHeaders(init?.headers);
|
|
72
|
+
requestBody = extractBody(init?.body);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
url = String(input);
|
|
77
|
+
method = 'GET';
|
|
78
|
+
requestHeaders = {};
|
|
79
|
+
requestBody = undefined;
|
|
80
|
+
}
|
|
81
|
+
const entry = {
|
|
82
|
+
id,
|
|
83
|
+
timestamp: startTime,
|
|
84
|
+
method,
|
|
85
|
+
url,
|
|
86
|
+
requestHeaders,
|
|
87
|
+
requestBody,
|
|
88
|
+
responseHeaders: {},
|
|
89
|
+
completed: false,
|
|
90
|
+
};
|
|
91
|
+
buffer.add(entry);
|
|
92
|
+
try {
|
|
93
|
+
const response = await originalFetch.call(globalThis, input, init);
|
|
94
|
+
const duration = Date.now() - startTime;
|
|
95
|
+
const responseHeaders = responseHeadersToRecord(response.headers);
|
|
96
|
+
const mimeType = response.headers.get('content-type') ?? undefined;
|
|
97
|
+
buffer.update(id, {
|
|
98
|
+
status: response.status,
|
|
99
|
+
statusText: response.statusText,
|
|
100
|
+
duration,
|
|
101
|
+
responseHeaders,
|
|
102
|
+
mimeType,
|
|
103
|
+
completed: true,
|
|
104
|
+
});
|
|
105
|
+
// Capture response body without consuming the original response
|
|
106
|
+
try {
|
|
107
|
+
response.clone().text().then((body) => {
|
|
108
|
+
buffer.update(id, { responseBody: body });
|
|
109
|
+
}).catch(() => {
|
|
110
|
+
// ignore body read failures
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// ignore clone failures
|
|
115
|
+
}
|
|
116
|
+
return response;
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const duration = Date.now() - startTime;
|
|
120
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
121
|
+
buffer.update(id, {
|
|
122
|
+
error: errorMessage,
|
|
123
|
+
duration,
|
|
124
|
+
completed: true,
|
|
125
|
+
});
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function unpatchFetch() {
|
|
131
|
+
if (originalFetch) {
|
|
132
|
+
globalThis.fetch = originalFetch;
|
|
133
|
+
originalFetch = null;
|
|
134
|
+
}
|
|
135
|
+
idCounter = 0;
|
|
136
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export interface InitOptions {
|
|
2
|
+
maxNetworkEntries?: number;
|
|
3
|
+
maxConsoleEntries?: number;
|
|
4
|
+
stores?: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
export interface NetworkEntry {
|
|
7
|
+
id: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
method: string;
|
|
10
|
+
url: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
statusText?: string;
|
|
13
|
+
duration?: number;
|
|
14
|
+
requestHeaders: Record<string, string>;
|
|
15
|
+
requestBody?: string;
|
|
16
|
+
responseHeaders: Record<string, string>;
|
|
17
|
+
responseBody?: string;
|
|
18
|
+
mimeType?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
completed: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface NetworkQueryOptions {
|
|
23
|
+
count?: number;
|
|
24
|
+
method?: string;
|
|
25
|
+
urlPattern?: string;
|
|
26
|
+
status?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface NetworkStats {
|
|
29
|
+
total: number;
|
|
30
|
+
completed: number;
|
|
31
|
+
errors: number;
|
|
32
|
+
avgDuration: number | null;
|
|
33
|
+
byMethod: Record<string, number>;
|
|
34
|
+
byStatus: Record<string, number>;
|
|
35
|
+
byDomain: Record<string, number>;
|
|
36
|
+
}
|
|
37
|
+
export interface ConsoleEntry {
|
|
38
|
+
id: string;
|
|
39
|
+
timestamp: number;
|
|
40
|
+
level: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ConsoleQueryOptions {
|
|
44
|
+
count?: number;
|
|
45
|
+
level?: 'log' | 'warn' | 'error' | 'info' | 'debug';
|
|
46
|
+
text?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface Capabilities {
|
|
49
|
+
network: boolean;
|
|
50
|
+
console: boolean;
|
|
51
|
+
stores: boolean;
|
|
52
|
+
render: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface DevToolsGlobal {
|
|
55
|
+
version: string;
|
|
56
|
+
capabilities: Capabilities;
|
|
57
|
+
stores: Record<string, unknown>;
|
|
58
|
+
getNetworkRequests: (options?: NetworkQueryOptions) => NetworkEntry[];
|
|
59
|
+
getNetworkRequest: (id: string) => NetworkEntry | null;
|
|
60
|
+
getNetworkStats: () => NetworkStats;
|
|
61
|
+
clearNetwork: () => number;
|
|
62
|
+
getConsoleLogs: (options?: ConsoleQueryOptions) => ConsoleEntry[];
|
|
63
|
+
clearConsole: () => number;
|
|
64
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-ai-devtools-sdk",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Lightweight SDK for react-native-ai-devtools — captures network requests for AI-powered debugging",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest --config jest.config.js",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["react-native", "debugging", "ai", "network", "mcp"],
|
|
13
|
+
"author": "Ihor Zheludkov",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@jest/globals": "^30.0.0",
|
|
17
|
+
"jest": "^30.0.0",
|
|
18
|
+
"ts-jest": "^29.0.0",
|
|
19
|
+
"typescript": "^5.0.0"
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist", "README.md", "LICENSE"]
|
|
22
|
+
}
|