# react-native-nitro-chucker

On-device HTTP(S) network inspector for React Native — [Chucker](https://github.com/ChuckerTeam/chucker) on Android, [Wormholy](https://github.com/pmusolino/Wormholy) on iOS — wrapped behind a single [Nitro](https://nitro.margelo.com) API.

Capture is **automatic** once the native module is linked and the app is rebuilt. You do not need to add an interceptor or configure anything; just call `show()` to inspect traffic.

---

## Requirements

| Requirement | Minimum |
|---|---|
| React Native | 0.76 |
| Android | minSdkVersion per your project (Chucker 4.1 supports API 21+) |
| iOS deployment target | **16.0** (Wormholy 2.x requires iOS 16+) |
| Node | 18 |

> **Not usable in Expo Go.** This module ships native code that must be compiled into your app binary. Use a [development build](https://docs.expo.dev/develop/development-builds/introduction/) or a bare workflow.

---

## Installation

```sh
npm install --save-dev react-native-nitro-chucker react-native-nitro-modules
```

Then, for iOS:

```sh
cd ios && pod install
```

Rebuild your app. No other configuration is needed — capture starts automatically when the app launches.

---

## How it works

- **Android:** An AndroidX App Startup `Initializer` (`ChuckerStartupInitializer`) runs during process creation — before React Native initializes or any network request fires — and registers a gated Chucker `OkHttpInterceptor` on RN's OkHttp stack. Chucker version: `4.1.0` (Maven Central).
- **iOS:** Wormholy auto-activates at pod load via C constructors that register a `CustomHTTPProtocol` and swizzle `NSURLSessionConfiguration`. All `URLSession` traffic is captured with zero setup. Wormholy version: `~> 2.4`.

---

## Usage

```ts
import {
  isSupported,
  show,
  clearLogs,
  setEnabled,
  dismiss,
} from 'react-native-nitro-chucker'

// Check whether the platform has a native inspector engine.
if (isSupported()) {
  show() // Open the inspector UI
}

// Pause capture (e.g. while the user is on a sensitive screen).
setEnabled(false)
// Resume capture.
setEnabled(true)

// Best-effort wipe of captured transactions.
clearLogs()

// Best-effort close of the inspector UI.
dismiss()
```

Every exported function is **always safe to call** — they never throw, and silently no-op on unsupported platforms or when the native module is not linked.

---

## API

| Function | Signature | Description |
|---|---|---|
| `isSupported` | `() => boolean` | Returns `true` on Android and iOS when the native module is linked. Never throws. |
| `show` | `() => void` | Opens the inspector UI. Android: launches the Chucker Activity. iOS: fires the `wormholy_fire` notification that presents Wormholy's request list. No-op when unsupported. |
| `setEnabled` | `(enabled: boolean) => void` | Pause or resume network capture at runtime. No-op when unsupported. |
| `clearLogs` | `() => void` | Best-effort wipe of captured transactions. See Limitations. |
| `dismiss` | `() => void` | Best-effort close of the inspector UI. See Limitations. |
| `setMaxLogCount` | `(maxCount: number) => void` | Cap retained transactions; oldest evicted first (FIFO). `0` = unlimited. **iOS: exact, defaults to 200** — see Memory. Android: no-op. |
| `setIgnoredHosts` | `(hosts: string[]) => void` | Skip capture for these hosts so their payloads are never retained. The most effective guard against large media/CDN responses. Both platforms; takes effect on the next request. Host **suffix** match on iOS, **exact** host match on Android. |

### iOS shake gesture

On iOS you can also shake the device to open Wormholy (this is a built-in Wormholy feature unrelated to this module's API). Calling `setEnabled(false)` disables request capture but does **not** disable the shake gesture.

---

## Memory

The two engines retain captured traffic very differently, and iOS is the one that needs
attention:

| | Body size cap | Storage | Retention cap |
|---|---|---|---|
| **Android** (Chucker) | 250 KB by default | SQLite database (disk) | retention period |
| **iOS** (Wormholy) | **none — bodies kept in full** | in-memory array | **none by default** |

On iOS every transaction — including complete request and response bodies — is held in a
RAM array that Wormholy does not bound by default (`Storage.limit == nil`), and it never
truncates bodies. A long session, or a handful of large media responses, will grow until
the app is OOM-killed.

To prevent that, this module installs a **default cap of 200 transactions at image load**,
before the first request can be captured. Wormholy enforces the cap as a FIFO ring buffer,
so the newest 200 are kept and older ones are evicted.

```ts
import { setMaxLogCount, setIgnoredHosts } from 'react-native-nitro-chucker'

// Keep fewer transactions if your traffic carries large payloads.
setMaxLogCount(50)

// Better: never retain the big ones in the first place.
setIgnoredHosts(['cdn.example.com', 'video.example.com'])
```

Two things to know:

- **Set the cap early.** Wormholy removes at most one entry per insert, so lowering the cap
  later makes the count plateau rather than shrink. The built-in default sidesteps this by
  being applied at load time.
- **A count cap does not bound bytes.** Because Wormholy has no body-size limit, one
  multi-megabyte response is retained in full regardless of the count. For media-heavy apps
  `setIgnoredHosts()` is the more important lever — it is the only guard that runs *before*
  a body is buffered.

Under memory pressure (`didReceiveMemoryWarning`) iOS capture is paused automatically as a
last resort, since Wormholy exposes no way to free what it has already retained. This is
logged; call `setEnabled(true)` to resume.

---

## Limitations

These are honest platform constraints, not bugs:

### `clearLogs()` — best-effort on both platforms

- **Android:** Chucker 4.x exposes no public API to programmatically clear the transaction database. `clearLogs()` is a documented no-op; clearing must be done from the Chucker UI.
- **iOS:** Wormholy's `Storage`, its `shared` singleton, `requests` and `clearRequests()` are all `internal` and none are `@objc`, so no selector is emitted and the Objective-C runtime / KVC routes are closed too. There is no clear notification. `clearLogs()` is a documented no-op — clearing must be done from Wormholy's own "Clear requests" UI. Because clearing is unavailable, memory is bounded up front instead; see [Memory](#memory).

### `dismiss()` — best-effort on both platforms

- **Android:** Chucker runs in its own task stack and cannot be force-finished from outside. `dismiss()` calls `Chucker.dismissNotifications()` to clear the persistent notification, but does not close an already-open Chucker Activity.
- **iOS:** Wormholy exposes no public dismiss API. `dismiss()` calls `dismiss(animated:)` on the topmost presented view controller on the main thread. This works when Wormholy is the topmost controller but is not guaranteed in all navigation scenarios.

### `setEnabled()` — behavior differs by platform

- **Android (exact):** Flips an `AtomicBoolean` gate in the `OkHttp` interceptor. Requests are not forwarded to Chucker while `enabled` is `false`; existing captures are unaffected.
- **iOS (functional for capture):** Calls `Wormholy.setEnabled(_:)`. `CustomHTTPProtocol` stays installed in every session configuration and short-circuits while disabled, so pausing and resuming is fully reversible — including for `URLSession`s created during the paused window. Does **not** disable the shake gesture.

---

## Production

This is a **debug tool**. The Chucker and Wormholy libraries log all HTTP(S) traffic on-device and should never ship in a production build. The consuming app is responsible for gating both the dependency and the call sites. A common pattern:

```ts
// Only wire up the inspector in non-production builds.
if (__DEV__) {
  // Optionally call show() on a debug menu button, etc.
}
```

For Android, restrict the Chucker library to `debugImplementation` in the consuming app's Gradle if you want the linker to omit it entirely from release APKs. For iOS, use a CocoaPods configuration guard.

> The `react-native-nitro-chucker` package itself performs **no environment gating**. `isSupported()` returns `true` in any build configuration as long as the native module is linked.

For a stronger guarantee — removing the native inspector code from the binary entirely — use the Expo config plugin described in the next section.

---

## Excluding from production builds (Expo)

Add the config plugin to your `app.config.js` and drive `enabled` from your
environment. When `enabled` is `false`, the native inspector (Chucker/Wormholy)
is excluded from autolinking, so **no inspector code is compiled into the build**.

```js
// app.config.js
export default ({ config }) => ({
  ...config,
  plugins: [
    ...(config.plugins ?? []),
    [
      'react-native-nitro-chucker',
      { enabled: process.env.APP_VARIANT !== 'production' },
    ],
  ],
})
```

Wire `APP_VARIANT` per build profile in `eas.json` (do not rely on `NODE_ENV`):

```json
{
  "build": {
    "development": { "env": { "APP_VARIANT": "development" } },
    "production": { "env": { "APP_VARIANT": "production" } }
  }
}
```

How it works: the plugin edits `expo.autolinking.exclude` in your app's
`package.json` during `expo prebuild`. On EAS this is a transient edit on a fresh
checkout; locally it is an idempotent edit that reverts when you prebuild with
`enabled` back to `true`. Requires Expo SDK 54+ (where `exclude` covers
React Native / Nitro modules).

### Manual fallback

If your SDK or a known Expo `exclude` bug leaves native code in the binary, add a
project-root `react-native.config.js` to force-disable autolinking:

```js
// react-native.config.js
module.exports = {
  dependencies: {
    'react-native-nitro-chucker':
      process.env.APP_VARIANT === 'production'
        ? { platforms: { ios: null, android: null } }
        : {},
  },
}
```

### Verify the strip

Always confirm a production artifact actually omits the inspector:

- iOS: build the release `.app`/`.ipa` and search for Wormholy symbols, e.g.
  `unzip -l App.ipa | grep -i wormholy` (expect no matches).
- Android: `unzip -l app-release.apk | grep -i chucker` (expect no matches).
- Build a development variant and confirm the inspector still opens.

---

## Example app

An example React Native app is included in the `example/` directory. It provides a QA screen that exercises the inspector API against a live HTTPS endpoint.

```sh
cd example
npm install
# Android
npx react-native run-android
# iOS
cd ios && pod install && cd ..
npx react-native run-ios
```

---

## Credits

- Android: [ChuckerTeam/chucker](https://github.com/ChuckerTeam/chucker) — Apache 2.0
- iOS: [pmusolino/Wormholy](https://github.com/pmusolino/Wormholy) — MIT
- Nitro module scaffold: [patrickkabwe/create-nitro-module](https://github.com/patrickkabwe/create-nitro-module)

---

## License

MIT © fluxlabs
