From 1e97413b615f00a0787da311db7679577c62f545 Mon Sep 17 00:00:00 2001 From: sankalpsingh Date: Wed, 20 May 2026 23:36:36 +0530 Subject: [PATCH 1/2] fix(android): gate interceptor on BuildConfig.DEBUG to prevent prod activation NETWORK_TOOLS_ENABLED was hardcoded to true in defaultConfig, meaning the OkHttp interceptor was added to the client in every build variant including release. Two-part fix: 1. android/build.gradle: move the flag out of defaultConfig and set it explicitly per buildType (true for debug, false for release). 2. example MainApplication.kt: wrap setCustomClientBuilder in a BuildConfig.DEBUG guard as a second line of defence, and to make the debug-only intent explicit. README and SETUP_GUIDE updated to show the BuildConfig.DEBUG guard pattern that consumers must apply in their own MainApplication. Closes #14 Co-Authored-By: Claude Sonnet 4.6 --- README.md | 16 +- android/build.gradle | 5 +- docs/SETUP_GUIDE.md | 54 +++---- docs/TODO.md | 141 ++++++++++++++---- .../networktools/example/MainApplication.kt | 14 +- 5 files changed, 154 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 516d2bd..634ccee 100644 --- a/README.md +++ b/README.md @@ -38,22 +38,24 @@ If you use Expo Development Builds, add the plugin to `app.json`: Add the interceptor to your `MainApplication.kt`: ```kotlin +import com.facebook.react.modules.network.NetworkingModule import com.networktools.NetworkToolsManager import okhttp3.OkHttpClient class MainApplication : Application(), ReactApplication { - override fun onCreate() { super.onCreate() - NetworkingModule.setCustomClientBuilder( - object : NetworkingModule.CustomClientBuilder { - override fun apply(builder: OkHttpClient.Builder) { - NetworkToolsManager.addInterceptor(builder) + if (BuildConfig.DEBUG) { + NetworkingModule.setCustomClientBuilder( + object : NetworkingModule.CustomClientBuilder { + override fun apply(builder: OkHttpClient.Builder) { + NetworkToolsManager.addInterceptor(builder) + } } - } - ) + ) + } // rest code } diff --git a/android/build.gradle b/android/build.gradle index b5415f9..7c9d454 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -33,7 +33,6 @@ android { defaultConfig { minSdkVersion getExtOrIntegerDefault("minSdkVersion") targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") - buildConfigField "boolean", "NETWORK_TOOLS_ENABLED", "true" } buildFeatures { @@ -41,7 +40,11 @@ android { } buildTypes { + debug { + buildConfigField "boolean", "NETWORK_TOOLS_ENABLED", "true" + } release { + buildConfigField "boolean", "NETWORK_TOOLS_ENABLED", "false" minifyEnabled false } } diff --git a/docs/SETUP_GUIDE.md b/docs/SETUP_GUIDE.md index aad7e44..153a616 100644 --- a/docs/SETUP_GUIDE.md +++ b/docs/SETUP_GUIDE.md @@ -53,6 +53,7 @@ If you're using React Native's default networking (fetch), you'll need to custom import com.facebook.react.ReactApplication import com.facebook.react.ReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost +import com.facebook.react.modules.network.NetworkingModule import com.networktools.NetworkToolsManager import okhttp3.OkHttpClient @@ -61,59 +62,52 @@ class MainApplication : Application(), ReactApplication { override fun onCreate() { super.onCreate() - NetworkingModule.setCustomClientBuilder( - object : NetworkingModule.CustomClientBuilder { - override fun apply(builder: OkHttpClient.Builder) { - NetworkToolsManager.addInterceptor(builder) + if (BuildConfig.DEBUG) { + NetworkingModule.setCustomClientBuilder( + object : NetworkingModule.CustomClientBuilder { + override fun apply(builder: OkHttpClient.Builder) { + NetworkToolsManager.addInterceptor(builder) + } } - } - ) + ) + } // rest code } } ``` +> **Why `BuildConfig.DEBUG`?** The library's own `BuildConfig.NETWORK_TOOLS_ENABLED` is already set to `false` for release builds, so `NetworkToolsManager.addInterceptor()` would be a no-op anyway. The outer `if (BuildConfig.DEBUG)` guard is a second line of defence: it prevents the `setCustomClientBuilder` call itself from running in production and makes the intent explicit to anyone reading the code. + **Java:** ```java import com.facebook.react.ReactApplication; import com.facebook.react.ReactNativeHost; import com.facebook.react.defaults.DefaultReactNativeHost; +import com.facebook.react.modules.network.NetworkingModule; import com.networktools.NetworkToolsManager; import okhttp3.OkHttpClient; public class MainApplication extends Application implements ReactApplication { - private final ReactNativeHost mReactNativeHost = - new DefaultReactNativeHost(this) { - // ... other configurations - - @Override - public boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - @Override -public void onCreate() { + @Override + public void onCreate() { super.onCreate(); - // Use NetworkingModule.setCustomClientBuilder to set the custom builder logic - NetworkingModule.setCustomClientBuilder( - new NetworkingModule.CustomClientBuilder() { // 1. Create an anonymous class - - // 2. Implement the required 'apply' method - @Override - public void apply(OkHttpClient.Builder builder) { - // 3. Call your static/utility method to add the interceptor - NetworkToolsManager.addInterceptor(builder); - } + if (BuildConfig.DEBUG) { + NetworkingModule.setCustomClientBuilder( + new NetworkingModule.CustomClientBuilder() { + @Override + public void apply(OkHttpClient.Builder builder) { + NetworkToolsManager.addInterceptor(builder); + } } - ); + ); + } // rest of your code -} - }; + } } ``` diff --git a/docs/TODO.md b/docs/TODO.md index d745fd7..97a089d 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -138,38 +138,115 @@ This checklist will help you complete the development and testing of react-nativ - [ ] Test on iOS - [ ] Update documentation -### 7. Advanced Features (Optional) - -- [ ] Request filtering - - [ ] By URL pattern - - [ ] By method - - [ ] By status code - - [ ] By time range - -- [ ] Search functionality - - [ ] Search in URLs - - [ ] Search in request/response bodies - - [ ] Search in headers - -- [ ] Export functionality - - [ ] Export to JSON file - - [ ] Share via system share sheet - - [ ] Copy to clipboard - -- [ ] UI Components - - [ ] Pre-built NetworkInspector screen - - [ ] Request detail modal - - [ ] Search and filter UI - -- [ ] Request replay - - [ ] Replay captured requests - - [ ] Modify and replay - - [ ] Batch replay - -- [ ] Mock responses - - [ ] Return mock data for testing - - [ ] Conditional mocking - - [ ] Mock response editor +### 7. Advanced Features + +#### Tier 1 — Core Inspector Completeness + +- [ ] Session summary dashboard + - [ ] Total requests, success/fail counts, total data transferred + - [ ] Average and peak latency at a glance + - [ ] Shown as a stats panel above the request list + +- [ ] Export to CSV / JSON + - [ ] Serialize all captured requests to CSV rows + - [ ] Full JSON dump of session + - [ ] Write to device file system, then trigger share sheet + +- [ ] Share individual request + - [ ] Long-press a request to open share options + - [ ] Format choices: plain text summary, JSON, cURL command + - [ ] Use RN `Share` API / `expo-sharing` + +- [ ] HAR (HTTP Archive) export + - [ ] Full HAR 1.2 format — importable into Postman, Charles, Proxyman, browser DevTools + - [ ] Include timings, headers, bodies, cookies + +- [ ] cURL copy + - [ ] One-tap copy of a request as a ready-to-run `curl` command + - [ ] Include headers, method, body, and URL + +#### Tier 2 — Analytics & Statistics + +- [ ] Data usage breakdown + - [ ] Bytes sent and received per domain + - [ ] Bytes per endpoint + - [ ] Running total for the session + +- [ ] Response time distribution + - [ ] P50 / P95 / P99 latency per domain + - [ ] Simple histogram or spark-line view + - [ ] Time-to-first-byte (TTFB) separate from total duration + +- [ ] Per-domain leaderboard + - [ ] Sorted by: slowest average, most calls, most errors + - [ ] Drillable — tap domain to see its requests + +- [ ] Slow request alerts + - [ ] Configurable threshold (e.g. > 2 s) via `NetworkMonitorProvider` prop + - [ ] Visual badge / highlight on slow requests in the list + +- [ ] Error rate tracking + - [ ] 4xx / 5xx frequency over time within a session + - [ ] Separate counters for client errors vs server errors + +#### Tier 3 — Developer Productivity + +- [ ] Persistent storage (opt-in) + - [ ] Save requests across restarts to AsyncStorage or MMKV + - [ ] Configurable via `persistRequests` prop on `NetworkMonitorProvider` + - [ ] Clear-on-launch option + +- [ ] Pinned / bookmarked requests + - [ ] Mark specific requests to prevent FIFO eviction + - [ ] Separate "Pinned" tab in the monitor UI + +- [ ] Regex search and filter + - [ ] Toggle between plain-text and regex mode in the search bar + - [ ] Filter across URL, headers, and body simultaneously + - [ ] Filter by URL pattern, method, status code, time range (migrated from old list) + +- [ ] Request diff + - [ ] Select two requests to the same endpoint and compare side-by-side + - [ ] Highlight header / body changes between calls (useful for pagination, mutations) + +- [ ] GraphQL-aware display + - [ ] Parse `operationName`, `variables`, and `data` from GraphQL POST bodies + - [ ] Show operation name as the display title instead of the raw URL + +- [ ] WebSocket monitoring + - [ ] Capture WebSocket frames via a custom OkHttp `WebSocketListener` wrapper + - [ ] Show open/close events and individual frames in a dedicated tab + +#### Tier 4 — Power Features + +- [ ] Mock / intercept responses + - [ ] Match requests by URL pattern and return a configured mock response + - [ ] No-server testing directly from the in-app tool + - [ ] Mock editor UI — status code, headers, body + - [ ] Conditional mocking (e.g. only on N-th call) + +- [ ] Replay requests + - [ ] Re-fire a captured request with one tap + - [ ] Editable replay — modify headers / body before sending + - [ ] Batch replay of a selected set of requests + +- [ ] Network condition simulation + - [ ] Artificial latency injection (configurable ms) + - [ ] Bandwidth throttling (configurable KB/s) + - [ ] Simulate offline / flaky connection + +- [ ] Sensitive header redaction + - [ ] Auto-mask `Authorization`, `Cookie`, `X-Api-Key` by default + - [ ] Configurable allowlist / blocklist via `NetworkMonitorProvider` prop + - [ ] Toggle reveal on tap in the detail view + +- [ ] Shake-to-open + - [ ] Open the network monitor on device shake — no floating button required + - [ ] Opt-in via `openOnShake` prop + +- [ ] Desktop companion / Metro plugin + - [ ] Stream captured requests to a browser panel alongside the Metro bundler + - [ ] WebSocket bridge from the app to a local dev-server listener ### 8. Pre-Release Checklist diff --git a/example/android/app/src/main/java/networktools/example/MainApplication.kt b/example/android/app/src/main/java/networktools/example/MainApplication.kt index 7342610..6c0c84a 100644 --- a/example/android/app/src/main/java/networktools/example/MainApplication.kt +++ b/example/android/app/src/main/java/networktools/example/MainApplication.kt @@ -38,13 +38,15 @@ class MainApplication : Application(), ReactApplication { override fun onCreate() { super.onCreate() - NetworkingModule.setCustomClientBuilder( - object : NetworkingModule.CustomClientBuilder { - override fun apply(builder: OkHttpClient.Builder) { - NetworkToolsManager.addInterceptor(builder) + if (BuildConfig.DEBUG) { + NetworkingModule.setCustomClientBuilder( + object : NetworkingModule.CustomClientBuilder { + override fun apply(builder: OkHttpClient.Builder) { + NetworkToolsManager.addInterceptor(builder) + } } - } - ) + ) + } loadReactNative(this) } From 6442c2f2711f55155555328dfcc1036b958d5c99 Mon Sep 17 00:00:00 2001 From: sankalpsingh Date: Wed, 20 May 2026 23:56:49 +0530 Subject: [PATCH 2/2] fix(plugin): wrap setCustomClientBuilder in BuildConfig.DEBUG guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Expo config plugin was patching MainApplication unconditionally, causing setCustomClientBuilder to run in every EAS build profile including production. Both KOTLIN_SNIPPET and JAVA_SNIPPET now wrap the interceptor setup in if (BuildConfig.DEBUG) { ... }, matching the guard pattern required in bare RN apps (see #14). Tests expanded from 2 to 8: guard presence, guard ordering (addInterceptor must appear after the if), idempotency, and null return when onCreate pattern is absent — for both Kotlin and Java. Closes #15 Co-Authored-By: Claude Sonnet 4.6 --- plugin/src/index.js | 32 ++++++++++++--------- plugin/src/index.test.js | 62 +++++++++++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 24 deletions(-) diff --git a/plugin/src/index.js b/plugin/src/index.js index c45a750..6bf425a 100644 --- a/plugin/src/index.js +++ b/plugin/src/index.js @@ -31,22 +31,26 @@ const JAVA_IMPORTS = [ 'import okhttp3.OkHttpClient;', ]; -const KOTLIN_SNIPPET = ` NetworkingModule.setCustomClientBuilder( - object : NetworkingModule.CustomClientBuilder { - override fun apply(builder: OkHttpClient.Builder) { - NetworkToolsManager.addInterceptor(builder) +const KOTLIN_SNIPPET = ` if (BuildConfig.DEBUG) { + NetworkingModule.setCustomClientBuilder( + object : NetworkingModule.CustomClientBuilder { + override fun apply(builder: OkHttpClient.Builder) { + NetworkToolsManager.addInterceptor(builder) + } } - } - )`; - -const JAVA_SNIPPET = ` NetworkingModule.setCustomClientBuilder( - new NetworkingModule.CustomClientBuilder() { - @Override - public void apply(OkHttpClient.Builder builder) { - NetworkToolsManager.addInterceptor(builder); + ) + }`; + +const JAVA_SNIPPET = ` if (BuildConfig.DEBUG) { + NetworkingModule.setCustomClientBuilder( + new NetworkingModule.CustomClientBuilder() { + @Override + public void apply(OkHttpClient.Builder builder) { + NetworkToolsManager.addInterceptor(builder); + } } - } - );`; + ); + }`; function ensureImport(src, importLine) { if (src.includes(importLine)) { diff --git a/plugin/src/index.test.js b/plugin/src/index.test.js index 0453a03..b2c8603 100644 --- a/plugin/src/index.test.js +++ b/plugin/src/index.test.js @@ -1,7 +1,7 @@ const { applyJavaPatch, applyKotlinPatch } = require('./index'); describe('expo config plugin MainApplication patch', () => { - it('applies kotlin patch and is idempotent', () => { + describe('Kotlin', () => { const source = `package networktools.example import android.app.Application @@ -13,14 +13,35 @@ class MainApplication : Application() { } `; - const once = applyKotlinPatch(source); - expect(once).toContain('NetworkToolsManager.addInterceptor(builder)'); + it('wraps interceptor setup in a BuildConfig.DEBUG guard', () => { + const patched = applyKotlinPatch(source); + expect(patched).toContain('if (BuildConfig.DEBUG)'); + expect(patched).toContain('NetworkToolsManager.addInterceptor(builder)'); + }); - const twice = applyKotlinPatch(once); - expect(twice).toBe(once); + it('places the addInterceptor call inside the DEBUG guard', () => { + const patched = applyKotlinPatch(source); + const debugGuardIndex = patched.indexOf('if (BuildConfig.DEBUG)'); + const interceptorIndex = patched.indexOf( + 'NetworkToolsManager.addInterceptor(builder)' + ); + expect(debugGuardIndex).toBeGreaterThanOrEqual(0); + expect(interceptorIndex).toBeGreaterThan(debugGuardIndex); + }); + + it('is idempotent — applying the patch twice produces the same result', () => { + const once = applyKotlinPatch(source); + const twice = applyKotlinPatch(once); + expect(twice).toBe(once); + }); + + it('returns null when onCreate pattern is not found', () => { + const noOnCreate = `package networktools.example\nclass MainApplication : Application()`; + expect(applyKotlinPatch(noOnCreate)).toBeNull(); + }); }); - it('applies java patch and is idempotent', () => { + describe('Java', () => { const source = `package networktools.example; import android.app.Application; @@ -33,10 +54,31 @@ class MainApplication extends Application { } `; - const once = applyJavaPatch(source); - expect(once).toContain('NetworkToolsManager.addInterceptor(builder);'); + it('wraps interceptor setup in a BuildConfig.DEBUG guard', () => { + const patched = applyJavaPatch(source); + expect(patched).toContain('if (BuildConfig.DEBUG)'); + expect(patched).toContain('NetworkToolsManager.addInterceptor(builder);'); + }); + + it('places the addInterceptor call inside the DEBUG guard', () => { + const patched = applyJavaPatch(source); + const debugGuardIndex = patched.indexOf('if (BuildConfig.DEBUG)'); + const interceptorIndex = patched.indexOf( + 'NetworkToolsManager.addInterceptor(builder);' + ); + expect(debugGuardIndex).toBeGreaterThanOrEqual(0); + expect(interceptorIndex).toBeGreaterThan(debugGuardIndex); + }); + + it('is idempotent — applying the patch twice produces the same result', () => { + const once = applyJavaPatch(source); + const twice = applyJavaPatch(once); + expect(twice).toBe(once); + }); - const twice = applyJavaPatch(once); - expect(twice).toBe(once); + it('returns null when onCreate pattern is not found', () => { + const noOnCreate = `package networktools.example;\nclass MainApplication extends Application {}`; + expect(applyJavaPatch(noOnCreate)).toBeNull(); + }); }); });