react-native-net-bubble

Self-contained in-app network inspector for React Native: a draggable floating bubble opens a live DevTools-style Network panel captured natively (OkHttp on Android, NSURLProtocol on iOS), with file-of-origin for each request. New Architecture / TurboModu


Keywords
react-native, ios, android
License
MIT
Install
npm install react-native-net-bubble@0.2.0

Documentation

react-native-net-bubble

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).


Features

  • πŸ”΅ 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:line that called it
  • ⏱ Timing tab β€” visual timeline bar + precise HH:MM:SS.mmm timestamps
  • πŸ”’ 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

How it works

Full pipeline

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
Loading

Request lifecycle (sequence)

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
Loading

Gating logic

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
Loading

UI component tree

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
Loading

Install

npm install react-native-net-bubble
# or
yarn add react-native-net-bubble

iOS β€” 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-android

Quick start

Mount <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.


Gating

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__.


Props

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).

Inspector UI

Floating bubble

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'

Request list

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  πŸ”  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.

Copy menu (long-press)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ βœ• ─┐
β”‚  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"}',
});

Request detail tabs

Tap any row to open the detail view. Five tabs:

Headers

General info (URL, method, status, duration, content-type) + collapsible Response Headers and Request Headers sections with item counts.

Payload

Pretty-printed request body. JSON is auto-formatted. Includes an inline ⎘ Copy button.

Response

Pretty-printed response body with inline ⎘ Copy button. content-type response header is used for JSON detection.

Timing

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).

Initiator

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.


File-of-origin & symbolication

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
};

In development (Metro connected)

Symbolication is automatic. The library calls Metro's /symbolicate endpoint and resolves bundle offsets to real file:line values. Zero config.

In QA / release builds (no Metro)

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.


Export session

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\"}",
    ...
  }
]

Custom UI / headless usage

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';

NetworkRecord type

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;
};

Native interception deep-dive

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
Loading
  • Android: NetBubbleInterceptor is an OkHttp Interceptor added to RN's client at startup via OkHttpClientProvider. It intercepts the full request/response cycle, capturing headers, status codes, and bodies up to maxBodyBytes.
  • iOS: NetBubbleURLProtocol is registered into NSURLSessionConfiguration via method swizzling. It proxies the request through a private NSURLSession to 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 when isInspectorEnabled() returns true.

Limitations

  • New Architecture only β€” TurboModule + Codegen event emitter. RN 0.79+.
  • WebView traffic is not captured. WKWebView (iOS) runs out of process; Android WebView and 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.

Contributing

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 android

License

MIT Β© Durgesh Kumar Dwivedi