Network Inspection
Nitro Fetch ships with a built-in NetworkInspector that records HTTP and WebSocket activity at the JS level, reports requests to the React Native DevTools Network panel in debug builds, and emits native Perfetto / Instruments traces for zero-overhead profiling in production.
JS Network Inspector
Setup
import { NetworkInspector } from 'react-native-nitro-fetch';
// Start recording
NetworkInspector.enable();
// Optionally configure limits
NetworkInspector.enable({
maxEntries: 500, // ring-buffer size (default 500)
maxBodyCapture: 4096, // max bytes captured per body (default 4096)
});
// Stop recording
NetworkInspector.disable();
Once enabled, all fetch() calls are automatically recorded. WebSocket tracking is also automatic if react-native-nitro-websockets is installed alongside react-native-nitro-fetch.
Reading entries
// All entries (HTTP + WebSocket), ordered by creation time
const all = NetworkInspector.getEntries();
// Filter by type
const httpOnly = NetworkInspector.getHttpEntries();
const wsOnly = NetworkInspector.getWebSocketEntries();
// Look up a single entry
const entry = NetworkInspector.getEntry(id);
// Clear the buffer
NetworkInspector.clear();
Live listener
Subscribe to new or updated entries in real time:
const unsubscribe = NetworkInspector.onEntry((entry) => {
if (entry.type === 'http') {
console.log(`${entry.method} ${entry.url} → ${entry.status} (${entry.duration.toFixed(0)}ms)`);
} else {
console.log(`WS ${entry.url} — ${entry.messagesSent + entry.messagesReceived} messages`);
}
});
// Later
unsubscribe();
HTTP entry shape
Each HTTP request creates a NetworkEntry:
| Field | Type | Description |
|---|---|---|
id | string | Unique request identifier |
type | 'http' | Discriminator |
url | string | Request URL |
method | string | HTTP method |
requestHeaders | Array<{ key, value }> | Request headers |
requestBody | string | undefined | Captured request body (truncated to maxBodyCapture) |
requestBodySize | number | Full body size in bytes |
status | number | HTTP status code |
statusText | string | Status text |
responseHeaders | Array<{ key, value }> | Response headers |
responseBody | string | undefined | Captured response body (truncated to maxBodyCapture) |
responseBodySize | number | Full response body size |
startTime | number | performance.now() at request start |
endTime | number | performance.now() at response |
duration | number | Total time in ms |
curl | string | Auto-generated curl command |
error | string | undefined | Error message if failed |
WebSocket entry shape
Each WebSocket connection creates a WebSocketEntry:
| Field | Type | Description |
|---|---|---|
id | string | Unique connection identifier |
type | 'websocket' | Discriminator |
url | string | WebSocket URL |
protocols | string[] | Requested subprotocols |
requestHeaders | Array<{ key, value }> | Upgrade request headers |
readyState | string | CONNECTING, OPEN, or CLOSED |
messages | WebSocketMessage[] | All recorded messages |
messagesSent | number | Total messages sent |
messagesReceived | number | Total messages received |
bytesSent | number | Total bytes sent |
bytesReceived | number | Total bytes received |
closeCode | number | undefined | WebSocket close code |
closeReason | string | undefined | Close reason |
error | string | undefined | Error message if failed |
startTime / endTime / duration | number | Timing (ms) |
Each WebSocketMessage contains:
| Field | Type | Description |
|---|---|---|
direction | 'sent' | 'received' | Message direction |
data | string | Message content (truncated to maxBodyCapture) |
size | number | Full message size in bytes |
isBinary | boolean | Whether the frame was binary |
timestamp | number | performance.now() when recorded |
Building a custom UI
You can use the inspector data to build your own network debugging screen. Here's a minimal example:
import React, { useState, useEffect } from 'react';
import { View, Text, FlatList, Pressable } from 'react-native';
import { NetworkInspector } from 'react-native-nitro-fetch';
import type { InspectorEntry } from 'react-native-nitro-fetch';
export function NetworkDebugger() {
const [entries, setEntries] = useState<readonly InspectorEntry[]>([]);
useEffect(() => {
NetworkInspector.enable();
const unsub = NetworkInspector.onEntry(() => {
setEntries([...NetworkInspector.getEntries()]);
});
return () => {
unsub();
NetworkInspector.disable();
};
}, []);
return (
<FlatList
data={entries}
keyExtractor={(e) => e.id}
renderItem={({ item }) => (
<View style={{ padding: 8, borderBottomWidth: 1, borderColor: '#eee' }}>
{item.type === 'http' ? (
<Text>
{item.method} {item.url} → {item.status} ({item.duration.toFixed(0)}ms)
</Text>
) : (
<Text>
WS {item.url} — {item.messagesSent + item.messagesReceived} msgs
</Text>
)}
</View>
)}
/>
);
}
The example app includes a full-featured inspector screen with filter tabs (All / HTTP / WS), detail views, curl export, and a live log console. See example/src/screens/NetworkInspectorScreen.tsx for the complete implementation.
React Native DevTools Network panel
In debug builds only, Nitro Fetch reports each request to the React Native DevTools Network panel natively — through InspectorNetworkReporter on Android and RCTInspectorNetworkReporter (RN 0.83+) on iOS. This is on by default and needs no setup. In release builds the whole path is compiled out.
Opting out
If another tool already reports the same traffic, every request shows up twice. The usual case is expo-dev-client on iOS: it swizzles URLSessionConfiguration.default, which Nitro Fetch's URLSession is built from, so expo-dev-launcher reports the request over its own CDP socket while Nitro Fetch reports the same request through RCTInspectorNetworkReporter.
Turn Nitro Fetch's native reporting off at build time:
Android
Add to your app's gradle.properties, then rebuild:
NitroFetch_disableDevToolsReporting=true
iOS
Set the environment variable before running pod install:
NITROFETCH_DISABLE_DEVTOOLS_REPORTING=1 bundle exec pod install
Then rebuild. Run pod install explicitly when flipping this flag — npx expo run:ios and yarn ios skip it when the Podfile hasn't changed, so the flag would otherwise appear to have no effect.
Native Tracing (Perfetto & Instruments)
Both HTTP fetch and WebSocket operations can emit native trace events, giving you flame-chart visibility in Perfetto (Android) and Instruments (iOS) with zero JS overhead. Both are opt-in via build flags.
Enabling tracing
Tracing is controlled separately for HTTP fetch and WebSockets. You can enable one or both.
Android
Add to your app's gradle.properties:
# HTTP fetch tracing (android.os.Trace async sections)
NitroFetch_enableTracing=true
# WebSocket tracing (ATrace sync sections in C++)
NitroFetchWebsockets_enableTracing=true
Rebuild your app after changing these flags.
iOS
Set environment variables before running pod install:
# HTTP fetch tracing (os_signpost intervals in Swift)
NITROFETCH_TRACING=1 bundle exec pod install
# WebSocket tracing (os_signpost events/intervals in C++)
NITRO_WS_TRACING=1 bundle exec pod install
# Both at once
NITROFETCH_TRACING=1 NITRO_WS_TRACING=1 bundle exec pod install
Then rebuild your app. The env vars inject compile-time flags (NITROFETCH_TRACING Swift condition and -DNITRO_WS_TRACING=1 C++ define) into the pod build — they have no runtime cost when disabled.
What gets traced
HTTP fetch
When NitroFetch_enableTracing=true (Android) or NITROFETCH_TRACING=1 (iOS) is set:
-
Android: Each
fetch()call emits an async section viaandroid.os.Trace.beginAsyncSection/endAsyncSection. The label is"NitroFetch <METHOD> <path>"(e.g.,NitroFetch GET /api/users). The section spans from request start to response completion (including success, failure, and cancellation). -
iOS: Each
fetch()call emits anos_signpostinterval under subsystemcom.margelo.nitrofetch, categorynetwork, name"NitroFetch". The begin event includes"<METHOD> <path>"and the end event includes"status=<code> bytes=<count>".
WebSocket
When NitroFetchWebsockets_enableTracing=true (Android) or NITRO_WS_TRACING=1 (iOS) is set, the native layer emits trace events for each WebSocket lifecycle event:
-
Android: Uses
ATrace_beginSection/ATrace_endSection(synchronous sections from<android/trace.h>in C++). These appear as slices on the thread where the event occurred. -
iOS: Uses
os_signpostunder subsystemcom.margelo.nitro.websockets, categoryNitroWS. Theconnect→connectedpair uses an interval (begin/end with a shared signpost ID). All other events (send,receive,close,error) are point events viaos_signpost_event_emit.
Capturing a Perfetto trace (Android)
Option 1: Perfetto UI (recommended)
-
Connect your Android device via USB (with USB debugging enabled).
-
Open ui.perfetto.dev in Chrome.
-
Click "Record new trace" and select your device.
-
Under "Probes", enable "Atrace userspace annotations".
-
Critical step: In the Atrace configuration, set
atrace_appsto your app's package name (e.g.,com.example.myapp) or*for all apps. Without this,android.os.TraceandATrace_beginSectionevents from your app will not be captured. -
Optionally add "Scheduling details" for CPU context.
-
Click "Start recording", use your app (make fetch requests, open WebSocket connections), then click "Stop".
-
In the trace viewer, look for your app's process. HTTP traces appear as async slices labeled
NitroFetch GET /path,NitroFetch POST /path, etc. WebSocket traces appear as sync slices labeledNitroWS connect <url>,NitroWS established,NitroWS send text,NitroWS receive, etc.
Option 2: Command-line with config file
Create a file trace_config.pbtx:
buffers {
size_kb: 65536
}
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
atrace_categories: "view"
atrace_apps: "com.example.myapp"
}
}
}
duration_ms: 15000
Replace com.example.myapp with your actual package name. Using * captures all apps but produces larger traces.
Then record:
# Push config and start trace
adb push trace_config.pbtx /data/local/tmp/
adb shell perfetto --config /data/local/tmp/trace_config.pbtx --out /data/local/tmp/trace.perfetto-trace
# After using your app, pull the trace
adb pull /data/local/tmp/trace.perfetto-trace .
Open the resulting .perfetto-trace file at ui.perfetto.dev.
Capturing an Instruments trace (iOS)
-
Open Instruments (Xcode menu → Open Developer Tool → Instruments).
-
Choose the
os_signposttemplate (or create a custom template and add the "os_signpost" instrument). -
Select your app process as the target.
-
Click the record button, use your app, then stop.
-
In the timeline, look for these subsystems:
Subsystem Category What it traces com.margelo.nitrofetchnetworkHTTP fetch requests com.margelo.nitro.websocketsNitroWSWebSocket lifecycle - HTTP fetch: Appears as intervals (begin/end pair) under name
"NitroFetch". The begin annotation shows the method and path (e.g.,GET /api/users), the end annotation showsstatus=200 bytes=1234. - WebSocket connect: Appears as an interval from
connect(begin, annotated with the URL) toconnected(end). This measures the handshake duration. - WebSocket send/receive/close/error: Appear as point events under name
"NitroWS"with details likesend text 42 bytes,receive text,close code=1000 clean=1,error <message>.
- HTTP fetch: Appears as intervals (begin/end pair) under name
Trace events reference
HTTP fetch events
| Event | Android | iOS |
|---|---|---|
| Request start | Trace.beginAsyncSection("NitroFetch GET /path", cookie) | os_signpost(.begin, "NitroFetch", "GET /path") |
| Request end (success) | Trace.endAsyncSection("NitroFetch GET /path", cookie) | os_signpost(.end, "NitroFetch", "status=200 bytes=N") |
| Request end (failure) | Trace.endAsyncSection(...) | os_signpost(.end, ...) |
| Request end (cancelled) | Trace.endAsyncSection(...) | os_signpost(.end, ...) |
WebSocket events
| Event | Android (ATrace) | iOS (os_signpost) |
|---|---|---|
| Connect start | ATrace_beginSection("NitroWS connect wss://...") | interval begin: "NitroWS" with URL |
| Connected | ATrace_beginSection("NitroWS established") | interval end: "NitroWS" "connected" |
| Send text | ATrace_beginSection("NitroWS send text") | event: "send text <N> bytes" |
| Send binary | ATrace_beginSection("NitroWS send binary") | event: "send binary <N> bytes" |
| Receive | ATrace_beginSection("NitroWS receive") | event: "receive text" or "receive binary" |
| Close | ATrace_beginSection("NitroWS close") | event: "close code=<N> clean=<0|1>" |
| Error | ATrace_beginSection("NitroWS error") | event: "error <message>" |
On Android, WebSocket events use synchronous ATrace_beginSection / ATrace_endSection pairs (not async sections) because ATrace_beginAsyncSection requires API 29+ while the library supports API 24+. HTTP fetch uses the Java android.os.Trace.beginAsyncSection API which is available on all supported API levels.
See also
- API Reference — fetch options and response API
- WebSockets — WebSocket setup and usage
- Troubleshooting — common issues