A self-contained, in-app network inspector for React Native β no laptop, no cable, no DevTools session required.
A draggable floating bubble lives above every screen. Tap it and a live, Chrome DevTools-style Network panel slides up showing every request as it happens: URL, method, headers, request/response bodies, status, timing, and β uniquely β which file in your codebase fired the call.
npm install react-native-net-bubble
yarn add react-native-net-bubble
New Architecture only. Pure TurboModule (Codegen spec + event emitter). Requires React Native 0.79+ with the New Architecture enabled (the default on modern RN).
- π΅ Draggable floating bubble β stays out of the way, snaps to either edge, turns π΄ red when errors are present
- π Chrome DevTools-style tabs β Headers Β· Payload Β· Response Β· Timing Β· Initiator
- π Search + status filter chips β All Β· 2xx Β· 3xx Β· 4xx Β· 5xx Β· ERR, combined with free-text search
- π Copy as β long-press any request to copy as cURL (bash), cURL (cmd), fetch, or raw response body
- π€ Export β share the entire session as JSON with one tap
- π File-of-origin β every request shows the exact
file:linethat called it - β± Timing tab β visual timeline bar + precise
HH:MM:SS.mmmtimestamps - π Zero-cost in production β gating is evaluated before any component mounts; in prod nothing renders and nothing is intercepted
- π« No third-party runtime dependencies β just React Native
flowchart TD
A([App calls fetch / XHR]) --> B[JS monkey-patch\ncaptures stack trace]
B --> C[React Native\nNetworking layer]
C --> D{Platform}
D -->|Android| E[OkHttp\nNetBubbleInterceptor]
D -->|iOS| F[NSURLSession\nNetBubbleURLProtocol]
E --> G[Codegen EventEmitter\nonNetworkEvent]
F --> G
G --> H[NetworkStore.ingest]
H --> I[useNetworkRequests hook]
I --> J[FloatingBubble\nbadge count]
I --> K[InspectorPanel\nRequest list + detail]
style E fill:#3fb950,color:#000
style F fill:#4c8dff,color:#fff
style G fill:#a371f7,color:#fff
style H fill:#d29922,color:#000
sequenceDiagram
participant App
participant JS as JS Layer
participant Native as Native Interceptor
participant Store as NetworkStore
participant UI
App->>JS: fetch(url, options)
JS->>JS: Capture JS stack trace
JS->>Native: HTTP request (OkHttp / NSURLSession)
Native->>Native: Record request headers + body
Note over Native: Request is forwarded normally β<br/>zero impact on response
Native-->>App: Response (unchanged)
Native->>JS: onNetworkEvent (Codegen EventEmitter)
JS->>Store: ingest(record)
Store->>UI: useState update
UI->>UI: Badge count increments
Note over UI: FloatingBubble turns red<br/>if status β₯ 400 or error
flowchart TD
A([NetBubble mounted]) --> B{enabled prop\nprovided?}
B -->|yes| C{enabled === true?}
C -->|yes| ON([β
Inspector ON])
C -->|no| OFF([π« Inspector OFF\nNothing renders\nNo interception])
B -->|no| D{baseUrl +\nprodBaseUrl set?}
D -->|yes| E{baseUrl\n!== prodBaseUrl?}
E -->|yes| ON
E -->|no| OFF
D -->|no| F{__DEV__?}
F -->|true| ON
F -->|false| OFF
style ON fill:#3fb950,color:#000
style OFF fill:#f85149,color:#fff
flowchart TD
NB[NetBubble\nroot Β· gating Β· lifecycle]
NB --> FB[FloatingBubble\ndraggable β
button\nturns red on errors]
NB --> IP[InspectorPanel\nModal sheet]
IP --> RL[RequestList\nSearch + filter chips\nFlatList]
IP --> RD[RequestDetail\nChrome DevTools-style]
RL --> RR[RequestRow Γ N\nmethod Β· path Β· status\nhost Β· duration Β· origin]
RL --> CM[CopyMenu\nlong-press bottom sheet]
RL --> SF[Status chips\nAll 2xx 3xx 4xx 5xx ERR]
RD --> UB[URL bar\nmethod + full URL]
RD --> TB[Tab bar]
TB --> T1[Headers\nGeneral + Request/Response Headers]
TB --> T2[Payload\nRequest body]
TB --> T3[Response\nResponse body]
TB --> T4[Timing\nTimeline bar + timestamps]
TB --> T5[Initiator\nFile origin + call stack]
style NB fill:#0f1620,color:#e6edf3
style IP fill:#161f2b,color:#e6edf3
style RD fill:#161f2b,color:#e6edf3
style T1 fill:#4c8dff,color:#fff
style T2 fill:#d29922,color:#000
style T3 fill:#3fb950,color:#000
style T4 fill:#a371f7,color:#fff
style T5 fill:#db61a2,color:#fff
npm install react-native-net-bubble
# or
yarn add react-native-net-bubbleiOS β install pods after install:
cd ios && pod install && cd ..Android β no extra steps. Autolinking + Codegen handle everything. There is nothing to register in MainApplication or AppDelegate.
Then rebuild the native app:
npx react-native run-ios
npx react-native run-androidMount <NetBubble /> once, near the root of your app, as the last child so it floats above all other content:
import { NetBubble } from 'react-native-net-bubble';
export default function App() {
return (
<>
<RootNavigator />
{/* Always last so the bubble is above everything */}
<NetBubble enabled={getApiBaseUrl() !== PROD_BASE_URL} />
</>
);
}That's it. When enabled is false, NetBubble renders nothing and native interception never starts β safe to ship to production.
Three ways to control when the inspector is active:
// 1. Explicit boolean β recommended. Wire to your existing env flag.
<NetBubble enabled={getApiBaseUrl() !== PROD_BASE_URL} />
// 2. URL comparison β library does the comparison for you.
<NetBubble baseUrl={getApiBaseUrl()} prodBaseUrl={PROD_BASE_URL} />
// 3. Omit everything β defaults to __DEV__
<NetBubble />Resolution order: enabled β baseUrl !== prodBaseUrl β __DEV__.
| Prop | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
__DEV__ |
Master switch. Wins over all other gating props. |
baseUrl |
string |
β | Current API base URL. Used with prodBaseUrl. |
prodBaseUrl |
string |
β | If baseUrl === prodBaseUrl the inspector is off. |
maxBodyBytes |
number |
1048576 |
Max bytes captured per request body (1 MiB). |
maxRecords |
number |
500 |
Max records kept in memory before oldest are dropped. |
bubbleColor |
string |
#4c8dff |
Bubble background colour (overridden to red on errors). |
The bubble lives in a persistent position: absolute overlay with zIndex: 999999. Drag it anywhere β it snaps to the nearest edge on release and remembers its position across mounts.
| State | Appearance |
|---|---|
| Normal | Blue (or bubbleColor) with white request-count badge |
| Errors present | Turns red automatically when any request has status β₯ 400 or state === 'error'
|
ββββββββββββββββββββββββββββββββββββββββββββββββββ
β π Filter by URL, method, file, statusβ¦ β
ββββββββββββββββββββββββββββββββββββββββββββββββββ€
β All 2xx 3xx 4xx 5xx ERR β β status chips
ββββββββββββββββββββββββββββββββββββββββββββββββββ€
β POST /v1/users 201 β
β api.example.com 84ms β
β βΆ src/screens/Profile.tsx:42 β
ββββββββββββββββββββββββββββββββββββββββββββββββββ€
β GET /v1/feed 200 β
β api.example.com 120ms β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
- Search persists when you navigate into a detail view and back.
- Status chips filter instantly. Combine with text search.
- Long-press any row β Copy menu (see below).
- Tap a row β detail view.
βββββββββββββββββββββββββββββββββββββ β ββ
β POST /v1/users β
β api.example.com β
βββββββββββββββββββββββββββββββββββββββββββ€
β Copy URL β
β Copy as cURL β β bash / zsh / PowerShell
β Copy as cURL (cmd) β β Windows cmd.exe
β Copy as fetch β β JS fetch() snippet
β Copy Response Body β β only shown if response exists
βββββββββββββββββββββββββββββββββββββββββββ
If @react-native-clipboard/clipboard is installed in your app, text lands directly on the clipboard and a β Copied toast appears. Otherwise the native Share sheet opens.
cURL (bash) example:
curl -X POST 'https://api.example.com/v1/users' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer token' \
--data-raw '{"name":"John"}'cURL (cmd) example:
curl -X POST "https://api.example.com/v1/users" ^
-H "Content-Type: application/json" ^
-H "Authorization: Bearer token" ^
--data-raw "{\"name\":\"John\"}"fetch example:
await fetch('https://api.example.com/v1/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token',
},
body: '{"name":"John"}',
});Tap any row to open the detail view. Five tabs:
General info (URL, method, status, duration, content-type) + collapsible Response Headers and Request Headers sections with item counts.
Pretty-printed request body. JSON is auto-formatted. Includes an inline β Copy button.
Pretty-printed response body with inline β Copy button. content-type response header is used for JSON detection.
10:23:45.042 βββββββββββββββββββ 10:23:45.276
[ββββββββββββββββββββββββββββββββ]
234ms
Colour matches the status (green 2xx Β· yellow 3xx Β· red 4xx/5xx/error). Timestamps are shown at millisecond precision (HH:MM:SS.mmm).
Source file + line number + function name that triggered the request, plus the full JS call stack.
All sections inside each tab are collapsible β tap the βΎ/βΈ header row to toggle.
Every NetworkRecord carries a .origin field:
type RequestOrigin = {
file: string; // e.g. "src/screens/ProfileScreen.tsx"
line?: number; // e.g. 84
column?: number;
methodName?: string; // e.g. "loadUser"
raw: string; // raw stack frame
};Symbolication is automatic. The library calls Metro's /symbolicate endpoint and resolves bundle offsets to real file:line values. Zero config.
Bundle a source map and register a resolver once at startup:
import { configureSymbolication } from 'react-native-net-bubble';
import { SourceMapConsumer } from 'source-map-js'; // install separately
import sourceMapJson from './app.bundle.map.json';
const consumer = new SourceMapConsumer(sourceMapJson);
configureSymbolication({
resolveFrame: (frame) => {
if (frame.line == null) return undefined;
const pos = consumer.originalPositionFor({
line: frame.line,
column: frame.column ?? 0,
});
if (!pos.source) return undefined;
return {
file: pos.source.replace(/^.*\/src\//, 'src/'),
line: pos.line ?? undefined,
column: pos.column ?? undefined,
methodName: pos.name ?? frame.methodName,
raw: frame.raw,
};
},
});Because production is gated out entirely, the source map only ships in builds that already exclude real users.
The Export button (top-right of the list) calls Share.share() with a full JSON dump of all captured records β useful for attaching to bug reports or sharing with teammates:
[
{
"id": "abc123",
"method": "POST",
"url": "https://api.example.com/v1/users",
"status": 201,
"duration": 234,
"requestHeaders": { "Content-Type": "application/json" },
"responseBody": "{\"id\":\"u_1\"}",
...
}
]The default bubble + panel are optional. Subscribe directly to the captured data and build your own UI:
import { useNetworkRequests, networkStore } from 'react-native-net-bubble';
function MyInspector() {
const records = useNetworkRequests(); // live NetworkRecord[]
return (
<FlatList
data={records}
renderItem={({ item }) => <Text>{item.url}</Text>}
/>
);
}
// Clear all records
networkStore.clear();Individual components are also exported for composition:
import {
FloatingBubble,
InspectorPanel,
RequestList,
RequestDetail,
} from 'react-native-net-bubble';type NetworkRecord = {
id: string;
method: string;
url: string;
requestHeaders: Record<string, string>;
requestBody?: string;
requestBodyTruncated: boolean;
status?: number;
statusText?: string;
responseHeaders?: Record<string, string>;
responseBody?: string;
responseBodyTruncated: boolean;
contentType?: string;
startTime: number; // epoch ms
endTime?: number; // epoch ms
duration?: number; // ms
error?: string;
state: 'pending' | 'success' | 'error';
platform: string; // "android" | "ios"
stack?: string; // raw JS stack
origin?: RequestOrigin;
};flowchart LR
subgraph Android
A1[OkHttpClientProvider] -->|registers at app start| A2[NetBubbleInterceptor]
A2 -->|passthrough until start called| A3[OkHttp chain]
A3 --> A4[NetBubbleEmitter]
end
subgraph iOS
B1[swizzle defaultSessionConfiguration] -->|registers at app start| B2[NetBubbleURLProtocol]
B2 -->|declines until start called| B3[NSURLSession]
B3 --> B4[NetBubbleEmitter]
end
A4 -->|Codegen onNetworkEvent| JS[JS Β· NetworkStore]
B4 -->|Codegen onNetworkEvent| JS
-
Android:
NetBubbleInterceptoris an OkHttpInterceptoradded to RN's client at startup viaOkHttpClientProvider. It intercepts the full request/response cycle, capturing headers, status codes, and bodies up tomaxBodyBytes. -
iOS:
NetBubbleURLProtocolis registered intoNSURLSessionConfigurationvia method swizzling. It proxies the request through a privateNSURLSessionto capture the full exchange without affecting the response seen by the caller. -
Gating: Both interceptors start in a passive (pass-through) state. They only begin capturing after
native.start()is called β which only happens whenisInspectorEnabled()returnstrue.
- New Architecture only β TurboModule + Codegen event emitter. RN 0.79+.
-
WebView traffic is not captured.
WKWebView(iOS) runs out of process; AndroidWebViewand image pipelines (Fresco / Glide) use separate HTTP stacks. - File-of-origin covers JS-initiated requests only. Purely native-initiated traffic has no JS stack to attribute.
-
Bodies are capped at
maxBodyBytes(default 1 MiB). Binary content types (image/*,audio/*,video/*, etc.) are skipped entirely.
See CONTRIBUTING.md.
The example/ directory is a full React Native app wired to consume the library source directly:
# iOS
yarn example ios
# Android
yarn example androidMIT Β© Durgesh Kumar Dwivedi