Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,18 @@ jobs:
echo "turbo_cache_hit=1" >> $GITHUB_ENV
fi

- name: Setup Ruby
if: env.turbo_cache_hit != 1
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
working-directory: example

- name: Install cocoapods
if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true'
if: env.turbo_cache_hit != 1
run: |
cd example
bundle install
bundle exec pod repo update --verbose
bundle exec pod install --project-directory=ios

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ class NetworkToolsLegacyModule(reactContext: ReactApplicationContext) :
return NetworkToolsModuleDelegate.getRequestCount()
}

@ReactMethod
fun setMaxBodyCaptureBytes(bytes: Double) {
NetworkToolsManager.maxBodyCaptureBytes = bytes.toLong()
}

@ReactMethod
fun addListener(eventType: String?) {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ import okhttp3.OkHttpClient
object NetworkToolsManager {
private val interceptor = NetworkToolsInterceptor()

/** Maximum bytes captured per request/response body. Default: 256 KB. */
@JvmField
var maxBodyCaptureBytes: Long = 256 * 1024L

/**
* Add the NetworkTools interceptor to an OkHttpClient.Builder
* This method should be called when configuring your OkHttpClient
*
*
* Example usage:
* ```
* val client = OkHttpClient.Builder()
Expand Down
4 changes: 4 additions & 0 deletions android/src/main/java/com/networktools/NetworkToolsModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ class NetworkToolsModule(reactContext: ReactApplicationContext) :
return NetworkToolsModuleDelegate.getRequestCount()
}

override fun setMaxBodyCaptureBytes(bytes: Double) {
NetworkToolsManager.maxBodyCaptureBytes = bytes.toLong()
}

override fun addListener(eventType: String?) {

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.networktools.interceptor

import com.networktools.NetworkToolsManager
import com.networktools.models.NetworkRequest
import com.networktools.storage.NetworkRequestStorage
import okhttp3.Interceptor
Expand All @@ -15,6 +16,11 @@ import java.util.UUID
*/
class NetworkToolsInterceptor : Interceptor {

private val binaryContentTypePrefixes = listOf(
"image/", "video/", "audio/", "application/octet-stream", "application/zip",
"application/pdf", "application/x-", "font/"
)

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
Expand All @@ -37,7 +43,6 @@ class NetworkToolsInterceptor : Interceptor {
val responseHeaders = captureResponseHeaders(response)
val responseBody = captureResponseBody(response)

// Store the network request
val networkRequest = NetworkRequest(
id = requestId,
url = request.url.toString(),
Expand All @@ -56,10 +61,8 @@ class NetworkToolsInterceptor : Interceptor {
NetworkRequestStorage.addRequest(networkRequest)
NetworkToolsEventEmitter.emitNetworkRequest(networkRequest)


return response
} catch (e: Exception) {
// Capture error details
error = e.message ?: "Unknown error"
responseTime = System.currentTimeMillis()
duration = responseTime - requestTime
Expand Down Expand Up @@ -96,9 +99,16 @@ class NetworkToolsInterceptor : Interceptor {

private fun captureRequestBody(request: Request): String? {
return try {
val contentType = request.body?.contentType()?.toString().orEmpty()
if (isBinaryContentType(contentType)) return "[binary content — not captured]"

val limit = NetworkToolsManager.maxBodyCaptureBytes
val buffer = Buffer()
request.body?.writeTo(buffer)
buffer.readUtf8()
val totalSize = buffer.size
val isTruncated = totalSize > limit
val body = buffer.readUtf8(minOf(totalSize, limit))
if (isTruncated) "$body\n[... truncated — $totalSize bytes total]" else body
} catch (e: Exception) {
null
}
Expand All @@ -114,12 +124,23 @@ class NetworkToolsInterceptor : Interceptor {

private fun captureResponseBody(response: Response): String? {
return try {
val source = response.body?.source()
source?.request(Long.MAX_VALUE)
val buffer = source?.buffer
buffer?.clone()?.readUtf8()
val contentType = response.body?.contentType()?.toString().orEmpty()
if (isBinaryContentType(contentType)) return "[binary content — not captured]"

val source = response.body?.source() ?: return null
val limit = NetworkToolsManager.maxBodyCaptureBytes
// Request one extra byte so we can detect truncation without consuming it
source.request(limit + 1)
val buffer = source.buffer
val isTruncated = buffer.size > limit
val capturedBytes = minOf(buffer.size, limit).toInt()
val body = buffer.snapshot().substring(0, capturedBytes).utf8()
if (isTruncated) "$body\n[... truncated]" else body
} catch (e: Exception) {
null
}
}

private fun isBinaryContentType(contentType: String): Boolean =
binaryContentTypePrefixes.any { contentType.startsWith(it) }
}
4 changes: 4 additions & 0 deletions ios/NetworkTools.mm
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ - (NSNumber *)getRequestCount {
return @([[NetworkToolsStorage shared] count]);
}

- (void)setMaxBodyCaptureBytes:(double)bytes {
[NetworkToolsManager shared].maxBodyCaptureBytes = (NSInteger)bytes;
}

// addListener / removeListeners are inherited from RCTEventEmitter and satisfy
// the NativeNetworkToolsSpec protocol — no override needed.

Expand Down
17 changes: 10 additions & 7 deletions ios/NetworkToolsInterceptor.m
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
#import <objc/runtime.h>

static NSString *const kNTHandledKey = @"NetworkToolsHandled";
static const NSInteger kNTMaxBodyBytes = 256 * 1024;

static NSInteger ntMaxBodyBytes(void) {
return [NetworkToolsManager shared].maxBodyCaptureBytes;
}

// Original IMP saved during swizzle so we can call through to it.
static IMP gOriginalProtocolClassesIMP = nil;
Expand Down Expand Up @@ -197,7 +200,7 @@ - (NSData *)drainStream:(NSInputStream *)stream {
uint8_t chunk[4096];
NSInteger bytesRead;
NSInteger totalRead = 0;
while (totalRead < kNTMaxBodyBytes &&
while (totalRead < ntMaxBodyBytes() &&
(bytesRead = [stream read:chunk maxLength:sizeof(chunk)]) > 0) {
[buffer appendBytes:chunk length:(NSUInteger)bytesRead];
totalRead += bytesRead;
Expand All @@ -208,9 +211,9 @@ - (NSData *)drainStream:(NSInputStream *)stream {

- (nullable NSString *)bodyStringFromData:(NSData *)data {
if (data.length == 0) return nil;
NSData *capped = data.length <= (NSUInteger)kNTMaxBodyBytes
NSData *capped = data.length <= (NSUInteger)ntMaxBodyBytes()
? data
: [data subdataWithRange:NSMakeRange(0, (NSUInteger)kNTMaxBodyBytes)];
: [data subdataWithRange:NSMakeRange(0, (NSUInteger)ntMaxBodyBytes())];
return [[NSString alloc] initWithData:capped encoding:NSUTF8StringEncoding];
}

Expand All @@ -226,13 +229,13 @@ - (NSString *)responseBodyStringForResponse:(nullable NSHTTPURLResponse *)respon

if (self.responseData.length == 0) return @"";

NSData *capped = self.responseData.length <= (NSUInteger)kNTMaxBodyBytes
NSData *capped = self.responseData.length <= (NSUInteger)ntMaxBodyBytes()
? self.responseData
: [self.responseData subdataWithRange:NSMakeRange(0, (NSUInteger)kNTMaxBodyBytes)];
: [self.responseData subdataWithRange:NSMakeRange(0, (NSUInteger)ntMaxBodyBytes())];

NSString *body = [[NSString alloc] initWithData:capped encoding:NSUTF8StringEncoding] ?: @"";

if (self.responseData.length > (NSUInteger)kNTMaxBodyBytes) {
if (self.responseData.length > (NSUInteger)ntMaxBodyBytes()) {
body = [body stringByAppendingFormat:@"\n[... truncated — %lu bytes total]",
(unsigned long)self.responseData.length];
}
Expand Down
3 changes: 3 additions & 0 deletions ios/NetworkToolsManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ NS_ASSUME_NONNULL_BEGIN

@property (class, nonatomic, readonly) NetworkToolsManager *shared;

/** Maximum bytes captured per request/response body. Default: 256 KB. */
@property (nonatomic, assign) NSInteger maxBodyCaptureBytes;

/**
* Registers the URLProtocol interceptor. No-op in release builds — the entire
* method body is compiled out with #if DEBUG.
Expand Down
1 change: 1 addition & 0 deletions ios/NetworkToolsManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ + (instancetype)shared {
static dispatch_once_t token;
dispatch_once(&token, ^{
instance = [[self alloc] init];
instance->_maxBodyCaptureBytes = 256 * 1024;
});
return instance;
}
Expand Down
4 changes: 4 additions & 0 deletions src/NativeNetworkTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface Spec extends TurboModule {
getRequestById(id: string): string;
clearAllRequests(): void;
getRequestCount(): number;
setMaxBodyCaptureBytes(bytes: number): void;
addListener(eventType: string): void;
removeListeners(count: number): void;
}
Expand Down Expand Up @@ -55,6 +56,9 @@ const unavailableModule: Spec = {
warnUnavailable('getRequestCount');
return 0;
},
setMaxBodyCaptureBytes() {
warnUnavailable('setMaxBodyCaptureBytes');
},
addListener() {
warnUnavailable('addListener');
},
Expand Down
8 changes: 8 additions & 0 deletions src/context/NetworkMonitorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const NetworkMonitorProvider: React.FC<NetworkMonitorProviderProps> = ({
children,
maxRequests = 1000,
showFloatingMonitor = true,
maxBodyCaptureBytes,
}) => {
const [requests, setRequests] = useState<NetworkRequest[]>([]);
const emitterRef = useRef<NativeEventEmitter | null>(null);
Expand All @@ -37,6 +38,13 @@ export const NetworkMonitorProvider: React.FC<NetworkMonitorProviderProps> = ({
networkStore.setMaxRequests(maxRequests);
}, [maxRequests]);

// Propagate body capture limit to native
useEffect(() => {
if (maxBodyCaptureBytes !== undefined) {
NetworkTools.setMaxBodyCaptureBytes(maxBodyCaptureBytes);
}
}, [maxBodyCaptureBytes]);

// Subscribe to store changes
useEffect(() => {
const unsubscribe = networkStore.subscribe((updatedRequests) => {
Expand Down
1 change: 1 addition & 0 deletions src/context/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface NetworkMonitorProviderProps {
children: ReactNode;
maxRequests?: number;
showFloatingMonitor?: boolean;
maxBodyCaptureBytes?: number;
}

export const sample = {
Expand Down
3 changes: 3 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const getNetworkRequestById = (id: string): string =>
export const clearNetworkRequests = (): void => NetworkTools.clearAllRequests();
export const getNetworkRequestCount = (): number =>
NetworkTools.getRequestCount();
export const setMaxBodyCaptureBytes = (bytes: number): void =>
NetworkTools.setMaxBodyCaptureBytes(bytes);
export const getAllRequests = getAllNetworkRequests;
export const getRequestById = getNetworkRequestById;
export const clearAllRequests = clearNetworkRequests;
Expand Down Expand Up @@ -91,6 +93,7 @@ const ReactNativeNetworkTools = {
getRequestById: getNetworkRequestById,
clearAllRequests: clearNetworkRequests,
getRequestCount: getNetworkRequestCount,
setMaxBodyCaptureBytes,
getNetworkToolsRuntime,
isNativeNetworkToolsAvailable,
annotateNetworkRequestError,
Expand Down
Loading