diff --git a/README.md b/README.md
index 471f2b0..29d1396 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# React Native API Debugger
-A network request debugging tool for React Native applications. Monitor, inspect, and debug HTTP requests with a draggable overlay interface.
+A comprehensive network debugging tool for React Native applications. Monitor, inspect, and debug HTTP requests and WebSocket connections with a unified tabbed overlay interface.
[](https://www.npmjs.com/package/react-native-api-debugger)
[](https://www.npmjs.com/package/react-native-api-debugger)
@@ -10,16 +10,29 @@ A network request debugging tool for React Native applications. Monitor, inspect
## Features
+### HTTP Debugging
- **Network Interception** - Automatically captures all `fetch()` and `XMLHttpRequest` calls
-- **Draggable Overlay** - Floating button that can be positioned anywhere on screen
- **Request Details** - View headers, body, response, timing, and status codes
- **cURL Export** - Copy requests as cURL commands
- **Advanced Filtering** - Filter by status code (2xx, 3xx, 4xx, 5xx), search by URL/method/body
- **Export Logs** - Export to HAR, Postman Collection, or JSON formats
- **Request Replay** - Re-execute captured requests with optional modifications
-- **Dark/Light Theme** - Toggle between themes
- **Slow Request Detection** - Visual indicator for requests exceeding threshold
- **Sensitive Data Redaction** - Detect and mask sensitive headers and body fields
+
+### WebSocket Debugging
+- **WebSocket Interception** - Automatically captures all WebSocket connections
+- **Connection Monitoring** - Track connection state (connecting, open, closing, closed)
+- **Message Logging** - View sent and received messages with timestamps
+- **Message Filtering** - Filter by direction (sent/received) or search by content
+- **Binary Support** - Handles text and binary message types
+- **Connection Metrics** - Track message counts, bytes transferred, and connection duration
+- **Live Indicators** - Visual indicators for active connections
+
+### General
+- **Unified Interface** - Tabbed modal with HTTP and WebSocket views
+- **Draggable Overlay** - Floating button that can be positioned anywhere on screen
+- **Dark/Light Theme** - Toggle between themes
- **Individual Log Deletion** - Remove specific entries without clearing all logs
- **Device Shake Support** - Shake to show/hide the debugger
- **TypeScript Support** - Full type definitions included
@@ -67,7 +80,7 @@ npm install react-native-svg
## Quick Setup
-### 1. Basic Usage (No Dependencies)
+### 1. Basic Usage (HTTP Only)
```tsx
import React, { useEffect } from 'react';
@@ -94,17 +107,59 @@ export default function App() {
}
```
-### 2. With Draggable Button
+### 2. Full Setup (HTTP + WebSocket)
+
+Use `NetworkLoggerOverlay` with `enableWebSocket={true}` for the unified tabbed interface.
+
+```tsx
+import React, { useEffect } from 'react';
+import { View } from 'react-native';
+import {
+ networkLogger,
+ webSocketLogger,
+ NetworkLoggerOverlay
+} from 'react-native-api-debugger';
+
+export default function App() {
+ useEffect(() => {
+ // Setup HTTP interceptor
+ networkLogger.setupInterceptor();
+
+ // Setup WebSocket interceptor
+ webSocketLogger.setupInterceptor();
+ }, []);
+
+ return (
+
+ {/* Your app content */}
+
+
+
+ );
+}
+```
+
+### 3. With Draggable Button
Requires `react-native-gesture-handler` and `react-native-reanimated`.
```tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
-import { networkLogger, NetworkLoggerOverlay } from 'react-native-api-debugger';
+import {
+ networkLogger,
+ webSocketLogger,
+ NetworkLoggerOverlay
+} from 'react-native-api-debugger';
export default function App() {
useEffect(() => {
networkLogger.setupInterceptor();
+ webSocketLogger.setupInterceptor();
}, []);
return (
@@ -113,6 +168,8 @@ export default function App() {
@@ -120,17 +177,19 @@ export default function App() {
}
```
-### 3. Full Featured Setup
+### 4. Full Featured Setup
```tsx
console.log('Theme:', theme)}
/>
```
@@ -197,11 +256,18 @@ const config = networkLogger.getConfig();
```tsx
useEffect(() => {
- const unsubscribe = networkLogger.subscribe((logs) => {
- console.log('Logs updated:', logs.length);
+ const unsubscribeHttp = networkLogger.subscribe((logs) => {
+ console.log('HTTP logs updated:', logs.length);
+ });
+
+ const unsubscribeWs = webSocketLogger.subscribe((logs) => {
+ console.log('WebSocket logs updated:', logs.length);
});
- return () => unsubscribe();
+ return () => {
+ unsubscribeHttp();
+ unsubscribeWs();
+ };
}, []);
```
@@ -273,16 +339,62 @@ const curlCommand = generateCurl(log);
// curl -X POST 'https://api.example.com/users' -H 'Content-Type: application/json' -d '{"name":"John"}'
```
+### WebSocket Logger Methods
+
+```tsx
+import { webSocketLogger } from 'react-native-api-debugger';
+
+// Initialize interception
+webSocketLogger.setupInterceptor();
+
+// Configure options
+webSocketLogger.configure({
+ maxConnections: 50, // Maximum connections to store
+ captureMessages: true, // Log individual messages
+ maxMessagesPerConnection: 100, // Max messages per connection
+ ignoredUrls: ['*/health'], // URL patterns to ignore
+});
+
+// Get all logs
+const logs = webSocketLogger.getLogs();
+
+// Get log count
+const count = webSocketLogger.getLogCount();
+
+// Clear all logs
+webSocketLogger.clearLogs();
+
+// Delete a specific log
+webSocketLogger.deleteLog(logId);
+
+// Close an active connection
+webSocketLogger.closeConnection(logId);
+
+// Enable/disable logging
+webSocketLogger.enable();
+webSocketLogger.disable();
+
+// Get active connections count
+const activeCount = webSocketLogger.getActiveConnectionsCount();
+
+// Restore original WebSocket (remove interception)
+webSocketLogger.restoreInterceptor();
+```
+
---
## API Reference
### NetworkLoggerOverlay Props
+The unified overlay component with HTTP and optional WebSocket tabs.
+
| Prop | Type | Default | Description |
|------|------|---------|-------------|
-| `networkLogger` | `NetworkLogger` | Required | The logger instance |
+| `networkLogger` | `NetworkLogger` | Required | HTTP logger instance |
+| `webSocketLogger` | `WebSocketLogger` | - | WebSocket logger instance (required if `enableWebSocket` is true) |
| `enabled` | `boolean` | `__DEV__` | Enable/disable the overlay |
+| `enableWebSocket` | `boolean` | `false` | Enable WebSocket debugging tab |
| `draggable` | `boolean` | `false` | Enable draggable button |
| `enableDeviceShake` | `boolean` | `false` | Show on device shake |
| `useCopyToClipboard` | `boolean` | `false` | Enable clipboard copy |
@@ -328,6 +440,46 @@ interface NetworkLog {
}
```
+### WebSocketLog Type
+
+```typescript
+interface WebSocketLog {
+ id: number;
+ url: string;
+ state: 'connecting' | 'open' | 'closing' | 'closed';
+ protocols?: string[];
+ connectTime: string;
+ openTime?: string;
+ closeTime?: string;
+ closeCode?: number;
+ closeReason?: string;
+ error?: string;
+ handshakeDuration?: number;
+ messages: WebSocketMessage[];
+ messageCount: { sent: number; received: number };
+ bytesReceived: number;
+ bytesSent: number;
+}
+
+interface WebSocketMessage {
+ id: number;
+ direction: 'sent' | 'received';
+ data: string;
+ dataType: 'text' | 'binary';
+ timestamp: string;
+ size: number;
+}
+```
+
+### WebSocketLoggerConfig
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `maxConnections` | `number` | `100` | Maximum connections to store |
+| `captureMessages` | `boolean` | `true` | Log individual messages |
+| `maxMessagesPerConnection` | `number` | `100` | Max messages per connection |
+| `ignoredUrls` | `string[]` | `[]` | URL patterns to ignore |
+
---
## Production Safety
@@ -383,9 +535,35 @@ Make sure `setupInterceptor()` is called before any network requests:
```tsx
useEffect(() => {
networkLogger.setupInterceptor();
+ webSocketLogger.setupInterceptor();
}, []); // Empty dependency array - runs once on mount
```
+### WebSocket connections not appearing
+
+Ensure you have:
+1. Called `webSocketLogger.setupInterceptor()` before any WebSocket connections
+2. Set `enableWebSocket={true}` on the NetworkLoggerOverlay
+3. Passed the `webSocketLogger` prop
+
+```tsx
+
+```
+
+### Setup error messages
+
+The debugger will throw helpful error messages if interceptors aren't initialized:
+
+- **HTTP**: If `networkLogger.setupInterceptor()` wasn't called before rendering
+- **WebSocket**: If `enableWebSocket={true}` but `webSocketLogger.setupInterceptor()` wasn't called
+
+These errors include code examples to help you fix the issue quickly.
+
---
## Contributing
diff --git a/example/src/App.tsx b/example/src/App.tsx
index eee236c..de1b266 100755
--- a/example/src/App.tsx
+++ b/example/src/App.tsx
@@ -1,78 +1,22 @@
import React, { useEffect, useState } from 'react';
+import { StyleSheet, SafeAreaView, View } from 'react-native';
import {
- View,
- StyleSheet,
- Text,
- TouchableOpacity,
- SafeAreaView,
- ScrollView,
- Alert,
- ActivityIndicator,
- Switch,
-} from 'react-native';
-import { networkLogger, NetworkLoggerOverlay } from 'react-native-api-debugger';
+ NetworkLoggerOverlay,
+ networkLogger,
+ webSocketLogger,
+} from 'react-native-api-debugger';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
+import { NetworkScreen } from './screens/NetworkScreen';
+import { WebSocketScreen } from './screens/WebSocketScreen';
+import { BottomTabs, type Tab } from './components/BottomTabs';
-interface LoadingState {
- [key: string]: boolean;
-}
-
-interface Post {
- id: number;
- title: string;
- body: string;
- userId: number;
-}
-
-interface User {
- id: number;
- name: string;
- username: string;
- email: string;
- phone: string;
- website: string;
- company: {
- name: string;
- catchPhrase: string;
- };
-}
-
-interface ButtonComponentProps {
- title: string;
- onPress: () => void;
- buttonId: string;
- color?: string;
- loading?: boolean;
-}
-
-interface APIResponse {
- data?: T;
- error?: string;
- success: boolean;
-}
-
-const ButtonComponent: React.FC = ({
- title,
- onPress,
- color = '#007AFF',
- loading = false,
-}) => (
-
- {loading ? (
-
- ) : (
- {title}
- )}
-
-);
+const TABS: Tab[] = [
+ { key: 'network', label: 'Network', icon: 'H' },
+ { key: 'websocket', label: 'WebSocket', icon: 'WS' },
+];
const App: React.FC = () => {
- const [loading, setLoading] = useState({});
+ const [activeTab, setActiveTab] = useState('network');
const [showHeaders, setShowHeaders] = useState(false);
useEffect(() => {
@@ -83,332 +27,33 @@ const App: React.FC = () => {
slowRequestThreshold: 2000,
});
networkLogger.setupInterceptor();
+ webSocketLogger.setupInterceptor();
}, []);
- const setButtonLoading = (buttonId: string, isLoading: boolean): void => {
- setLoading((prev) => ({ ...prev, [buttonId]: isLoading }));
- };
-
- const showResult = (title: string, success: boolean, data: any): void => {
- Alert.alert(
- title,
- success ? `Success: ${JSON.stringify(data, null, 2)}` : `Error: ${data}`,
- [{ text: 'OK' }]
- );
- };
-
- const makeAPIRequest = async (
- url: string,
- options: RequestInit = {},
- buttonId: string,
- successMessage: (data: T) => string
- ): Promise> => {
- setButtonLoading(buttonId, true);
-
- try {
- const response: Response = await fetch(url, {
- ...options,
- headers: {
- 'Content-Type': 'application/json',
- ...options.headers,
- },
- });
-
- const data: T = await response.json();
-
- if (response.ok) {
- showResult(
- `${options.method || 'GET'} Success`,
- true,
- successMessage(data)
+ const renderScreen = () => {
+ switch (activeTab) {
+ case 'websocket':
+ return ;
+ case 'network':
+ default:
+ return (
+
);
- return { data, success: true };
- } else {
- showResult(`${options.method || 'GET'} Error`, false, data);
- return { error: `HTTP ${response.status}`, success: false };
- }
- } catch (error) {
- const errorMessage =
- error instanceof Error ? error.message : 'Unknown error';
- showResult(`${options.method || 'GET'} Error`, false, errorMessage);
- return { error: errorMessage, success: false };
- } finally {
- setButtonLoading(buttonId, false);
- }
- };
-
- // GET Request 1 - JSONPlaceholder Posts
- const handleGetPosts = async (): Promise => {
- await makeAPIRequest(
- 'https://jsonplaceholder.typicode.com/posts?_limit=5',
- {
- method: 'GET',
- headers: {
- 'Accept': 'application/json',
- 'Cache-Control': 'no-cache',
- },
- },
- 'getPosts',
- (data) => `Fetched ${data.length} posts`
- );
- };
-
- // GET Request 2 - JSONPlaceholder Users
- const handleGetUsers = async (): Promise => {
- await makeAPIRequest(
- 'https://jsonplaceholder.typicode.com/users',
- {
- method: 'GET',
- headers: {
- 'Accept': 'application/json',
- 'User-Agent': 'ReactNative-TestApp/1.0',
- },
- },
- 'getUsers',
- (data) => `Fetched ${data.length} users`
- );
- };
-
- // POST Request 1 - Create Post
- const handleCreatePost = async (): Promise => {
- const postData = {
- title: 'Test Post from React Native',
- body: 'This is a test post created from the mobile app',
- userId: 1,
- };
-
- await makeAPIRequest(
- 'https://jsonplaceholder.typicode.com/posts',
- {
- method: 'POST',
- headers: {
- 'Authorization': 'Bearer test-token-123',
- 'X-Client-Version': '1.0.0',
- },
- body: JSON.stringify(postData),
- },
- 'createPost',
- (data) => `Created post with ID: ${data.id}`
- );
- };
-
- // POST Request 2 - Create User
- const handleCreateUser = async (): Promise => {
- const userData = {
- name: 'John Doe',
- username: 'johndoe',
- email: 'john@example.com',
- phone: '1-770-736-8031',
- website: 'johndoe.org',
- company: {
- name: 'Doe Enterprises',
- catchPhrase: 'Testing API endpoints',
- },
- };
-
- await makeAPIRequest(
- 'https://jsonplaceholder.typicode.com/users',
- {
- method: 'POST',
- headers: {
- 'X-API-Key': 'mobile-app-key',
- 'User-Agent': 'ReactNative/TestApp',
- 'X-Request-ID': `req-${Date.now()}`,
- },
- body: JSON.stringify(userData),
- },
- 'createUser',
- (data) => `Created user with ID: ${data.id}`
- );
- };
-
- // Test Error Request (404)
- const handleErrorRequest = async (): Promise => {
- await makeAPIRequest(
- 'https://jsonplaceholder.typicode.com/posts/999999',
- {
- method: 'GET',
- headers: {
- Accept: 'application/json',
- },
- },
- 'errorRequest',
- () => 'Unexpected success'
- );
- };
-
- // Test Server Error (500)
- const handleServerError = async (): Promise => {
- await makeAPIRequest(
- 'https://httpstat.us/500',
- {
- method: 'GET',
- headers: {
- Accept: 'application/json',
- },
- },
- 'serverError',
- () => 'Unexpected success'
- );
- };
-
- // Test Slow Request
- const handleSlowRequest = async (): Promise => {
- await makeAPIRequest(
- 'https://httpstat.us/200?sleep=3000',
- {
- method: 'GET',
- headers: {
- Accept: 'application/json',
- },
- },
- 'slowRequest',
- () => 'Slow request completed'
- );
- };
-
- // Test Ignored URL (should not appear in logs)
- const handleIgnoredRequest = async (): Promise => {
- setButtonLoading('ignoredRequest', true);
- try {
- await fetch('https://jsonplaceholder.typicode.com/health');
- Alert.alert(
- 'Ignored Request',
- 'This request should NOT appear in the network logger because "/health" is in the ignored URLs list.'
- );
- } catch (error) {
- Alert.alert('Error', 'Request failed');
- } finally {
- setButtonLoading('ignoredRequest', false);
}
};
return (
-
-
- API Logger Test App
-
- Test network requests and view them in the logger
-
-
-
-
- GET Requests
-
-
-
-
-
- POST Requests
-
-
-
-
-
- Error & Edge Cases
-
-
-
-
-
-
-
-
-
- Display Settings
-
- Show Headers
-
-
-
-
- 📊 How to use:
-
- 1. Tap any button to make an API request{'\n'}
- 2. Check the floating network logger button{'\n'}
- 3. Tap the logger to view request details{'\n'}
- 4. Use filter chips (2xx, 4xx, 5xx, Errors) to filter logs{'\n'}
- 5. Search by URL, method, or response body{'\n'}
- 6. Expand requests to see headers & responses{'\n'}
- 7. Use cURL generation for debugging
-
-
-
-
- ⚙️ Current Config:
-
- • Max Logs: 50{'\n'}• Ignored URLs: /health, /ping{'\n'}• Slow
- Request Threshold: 2000ms{'\n'}• Dev Mode Only: enabled
- (default)
-
-
-
-
- 🔒 Production Safety:
-
- The network logger only shows in development builds by default
- (__DEV__). This prevents accidental exposure of sensitive API
- data in production apps.
-
-
-
-
- {/* enabled defaults to __DEV__, so it only shows in development builds */}
+ {renderScreen()}
+
{
showRequestHeader={showHeaders}
showResponseHeader={showHeaders}
networkLogger={networkLogger}
- // enabled={true} // Uncomment to force enable in production (not recommended)
+ webSocketLogger={webSocketLogger}
+ enableWebSocket={true}
/>
@@ -432,138 +78,8 @@ const styles = StyleSheet.create({
backgroundColor: '#f5f5f5',
paddingTop: '5%',
},
- scrollContent: {
- flexGrow: 1,
- },
- header: {
- backgroundColor: '#fff',
- padding: 20,
- alignItems: 'center',
- borderBottomWidth: 1,
- borderBottomColor: '#e0e0e0',
- },
- headerTitle: {
- fontSize: 24,
- fontWeight: 'bold',
- color: '#333',
- marginBottom: 8,
- },
- headerSubtitle: {
- fontSize: 16,
- color: '#666',
- textAlign: 'center',
- },
- body: {
- flex: 1,
- padding: 20,
- },
- sectionTitle: {
- fontSize: 18,
- fontWeight: '600',
- color: '#333',
- marginTop: 20,
- marginBottom: 15,
- },
- buttonRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginBottom: 15,
- },
- button: {
+ content: {
flex: 1,
- backgroundColor: '#007AFF',
- paddingVertical: 15,
- paddingHorizontal: 20,
- borderRadius: 8,
- marginHorizontal: 5,
- alignItems: 'center',
- justifyContent: 'center',
- minHeight: 50,
- elevation: 2,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.2,
- shadowRadius: 2,
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- instructions: {
- backgroundColor: '#fff',
- padding: 20,
- borderRadius: 12,
- marginTop: 30,
- elevation: 1,
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 1 },
- shadowOpacity: 0.1,
- shadowRadius: 2,
- },
- instructionsTitle: {
- fontSize: 18,
- fontWeight: '600',
- color: '#333',
- marginBottom: 10,
- },
- instructionsText: {
- fontSize: 14,
- color: '#666',
- lineHeight: 22,
- },
- configInfo: {
- backgroundColor: '#E3F2FD',
- padding: 20,
- borderRadius: 12,
- marginTop: 16,
- borderWidth: 1,
- borderColor: '#BBDEFB',
- },
- configTitle: {
- fontSize: 16,
- fontWeight: '600',
- color: '#1976D2',
- marginBottom: 10,
- },
- configText: {
- fontSize: 13,
- color: '#1565C0',
- lineHeight: 20,
- },
- settingRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- backgroundColor: '#fff',
- padding: 16,
- borderRadius: 8,
- marginBottom: 8,
- },
- settingLabel: {
- fontSize: 16,
- color: '#333',
- fontWeight: '500',
- },
- devModeInfo: {
- backgroundColor: '#FCE4EC',
- padding: 20,
- borderRadius: 12,
- marginTop: 16,
- marginBottom: 40,
- borderWidth: 1,
- borderColor: '#F8BBD9',
- },
- devModeTitle: {
- fontSize: 16,
- fontWeight: '600',
- color: '#C2185B',
- marginBottom: 10,
- },
- devModeText: {
- fontSize: 13,
- color: '#AD1457',
- lineHeight: 20,
},
});
diff --git a/example/src/components/BottomTabs.tsx b/example/src/components/BottomTabs.tsx
new file mode 100644
index 0000000..ca646da
--- /dev/null
+++ b/example/src/components/BottomTabs.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
+
+export interface Tab {
+ key: string;
+ label: string;
+ icon: string;
+}
+
+interface BottomTabsProps {
+ tabs: Tab[];
+ activeTab: string;
+ onTabPress: (key: string) => void;
+}
+
+export const BottomTabs: React.FC = ({
+ tabs,
+ activeTab,
+ onTabPress,
+}) => {
+ return (
+
+ {tabs.map((tab) => {
+ const isActive = tab.key === activeTab;
+ return (
+ onTabPress(tab.key)}
+ activeOpacity={0.7}
+ >
+
+
+ {tab.icon}
+
+
+
+ {tab.label}
+
+
+ );
+ })}
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flexDirection: 'row',
+ backgroundColor: '#fff',
+ borderTopWidth: 1,
+ borderTopColor: '#e0e0e0',
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ },
+ tab: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ paddingVertical: 4,
+ },
+ iconContainer: {
+ width: 32,
+ height: 32,
+ borderRadius: 8,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#f0f0f0',
+ marginBottom: 4,
+ },
+ iconContainerActive: {
+ backgroundColor: '#E3F2FD',
+ },
+ icon: {
+ fontSize: 14,
+ fontWeight: '700',
+ color: '#8E8E93',
+ },
+ iconActive: {
+ color: '#007AFF',
+ },
+ label: {
+ fontSize: 12,
+ fontWeight: '500',
+ color: '#8E8E93',
+ },
+ labelActive: {
+ color: '#007AFF',
+ fontWeight: '600',
+ },
+});
diff --git a/example/src/screens/NetworkScreen.tsx b/example/src/screens/NetworkScreen.tsx
new file mode 100644
index 0000000..8af4e52
--- /dev/null
+++ b/example/src/screens/NetworkScreen.tsx
@@ -0,0 +1,503 @@
+import React, { useState } from 'react';
+import {
+ View,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ ScrollView,
+ Alert,
+ ActivityIndicator,
+ Switch,
+} from 'react-native';
+
+interface LoadingState {
+ [key: string]: boolean;
+}
+
+interface Post {
+ id: number;
+ title: string;
+ body: string;
+ userId: number;
+}
+
+interface User {
+ id: number;
+ name: string;
+ username: string;
+ email: string;
+ phone: string;
+ website: string;
+ company: {
+ name: string;
+ catchPhrase: string;
+ };
+}
+
+interface ButtonComponentProps {
+ title: string;
+ onPress: () => void;
+ buttonId: string;
+ color?: string;
+ loading?: boolean;
+}
+
+interface APIResponse {
+ data?: T;
+ error?: string;
+ success: boolean;
+}
+
+interface NetworkScreenProps {
+ showHeaders: boolean;
+ setShowHeaders: (value: boolean) => void;
+}
+
+const ButtonComponent: React.FC = ({
+ title,
+ onPress,
+ color = '#007AFF',
+ loading = false,
+}) => (
+
+ {loading ? (
+
+ ) : (
+ {title}
+ )}
+
+);
+
+export const NetworkScreen: React.FC = ({
+ showHeaders,
+ setShowHeaders,
+}) => {
+ const [loading, setLoading] = useState({});
+
+ const setButtonLoading = (buttonId: string, isLoading: boolean): void => {
+ setLoading((prev) => ({ ...prev, [buttonId]: isLoading }));
+ };
+
+ const showResult = (title: string, success: boolean, data: any): void => {
+ Alert.alert(
+ title,
+ success ? `Success: ${JSON.stringify(data, null, 2)}` : `Error: ${data}`,
+ [{ text: 'OK' }]
+ );
+ };
+
+ const makeAPIRequest = async (
+ url: string,
+ options: RequestInit = {},
+ buttonId: string,
+ successMessage: (data: T) => string
+ ): Promise> => {
+ setButtonLoading(buttonId, true);
+
+ try {
+ const response: Response = await fetch(url, {
+ ...options,
+ headers: {
+ 'Content-Type': 'application/json',
+ ...options.headers,
+ },
+ });
+
+ const data: T = await response.json();
+
+ if (response.ok) {
+ showResult(
+ `${options.method || 'GET'} Success`,
+ true,
+ successMessage(data)
+ );
+ return { data, success: true };
+ } else {
+ showResult(`${options.method || 'GET'} Error`, false, data);
+ return { error: `HTTP ${response.status}`, success: false };
+ }
+ } catch (error) {
+ const errorMessage =
+ error instanceof Error ? error.message : 'Unknown error';
+ showResult(`${options.method || 'GET'} Error`, false, errorMessage);
+ return { error: errorMessage, success: false };
+ } finally {
+ setButtonLoading(buttonId, false);
+ }
+ };
+
+ const handleGetPosts = async (): Promise => {
+ await makeAPIRequest(
+ 'https://jsonplaceholder.typicode.com/posts?_limit=5',
+ {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ 'Cache-Control': 'no-cache',
+ },
+ },
+ 'getPosts',
+ (data) => `Fetched ${data.length} posts`
+ );
+ };
+
+ const handleGetUsers = async (): Promise => {
+ await makeAPIRequest(
+ 'https://jsonplaceholder.typicode.com/users',
+ {
+ method: 'GET',
+ headers: {
+ 'Accept': 'application/json',
+ 'User-Agent': 'ReactNative-TestApp/1.0',
+ },
+ },
+ 'getUsers',
+ (data) => `Fetched ${data.length} users`
+ );
+ };
+
+ const handleCreatePost = async (): Promise => {
+ const postData = {
+ title: 'Test Post from React Native',
+ body: 'This is a test post created from the mobile app',
+ userId: 1,
+ };
+
+ await makeAPIRequest(
+ 'https://jsonplaceholder.typicode.com/posts',
+ {
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Bearer test-token-123',
+ 'X-Client-Version': '1.0.0',
+ },
+ body: JSON.stringify(postData),
+ },
+ 'createPost',
+ (data) => `Created post with ID: ${data.id}`
+ );
+ };
+
+ const handleCreateUser = async (): Promise => {
+ const userData = {
+ name: 'John Doe',
+ username: 'johndoe',
+ email: 'john@example.com',
+ phone: '1-770-736-8031',
+ website: 'johndoe.org',
+ company: {
+ name: 'Doe Enterprises',
+ catchPhrase: 'Testing API endpoints',
+ },
+ };
+
+ await makeAPIRequest(
+ 'https://jsonplaceholder.typicode.com/users',
+ {
+ method: 'POST',
+ headers: {
+ 'X-API-Key': 'mobile-app-key',
+ 'User-Agent': 'ReactNative/TestApp',
+ 'X-Request-ID': `req-${Date.now()}`,
+ },
+ body: JSON.stringify(userData),
+ },
+ 'createUser',
+ (data) => `Created user with ID: ${data.id}`
+ );
+ };
+
+ const handleErrorRequest = async (): Promise => {
+ await makeAPIRequest(
+ 'https://jsonplaceholder.typicode.com/posts/999999',
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ },
+ },
+ 'errorRequest',
+ () => 'Unexpected success'
+ );
+ };
+
+ const handleServerError = async (): Promise => {
+ await makeAPIRequest(
+ 'https://httpstat.us/500',
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ },
+ },
+ 'serverError',
+ () => 'Unexpected success'
+ );
+ };
+
+ const handleSlowRequest = async (): Promise => {
+ await makeAPIRequest(
+ 'https://httpstat.us/200?sleep=3000',
+ {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ },
+ },
+ 'slowRequest',
+ () => 'Slow request completed'
+ );
+ };
+
+ const handleIgnoredRequest = async (): Promise => {
+ setButtonLoading('ignoredRequest', true);
+ try {
+ await fetch('https://jsonplaceholder.typicode.com/health');
+ Alert.alert(
+ 'Ignored Request',
+ 'This request should NOT appear in the network logger because "/health" is in the ignored URLs list.'
+ );
+ } catch (error) {
+ Alert.alert('Error', 'Request failed');
+ } finally {
+ setButtonLoading('ignoredRequest', false);
+ }
+ };
+
+ return (
+
+
+ HTTP Requests
+
+ Test network requests and view them in the logger
+
+
+
+
+ GET Requests
+
+
+
+
+
+ POST Requests
+
+
+
+
+
+ Error & Edge Cases
+
+
+
+
+
+
+
+
+
+ Display Settings
+
+ Show Headers
+
+
+
+
+ How to use:
+
+ 1. Tap any button to make an API request{'\n'}
+ 2. Check the floating network logger button{'\n'}
+ 3. Tap the logger to view request details{'\n'}
+ 4. Use filter chips (2xx, 4xx, 5xx, Errors) to filter logs{'\n'}
+ 5. Search by URL, method, or response body{'\n'}
+ 6. Expand requests to see headers & responses{'\n'}
+ 7. Use cURL generation for debugging
+
+
+
+
+ Current Config:
+
+ • Max Logs: 50{'\n'}• Ignored URLs: /health, /ping{'\n'}• Slow
+ Request Threshold: 2000ms{'\n'}• Dev Mode Only: enabled (default)
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ scrollContent: {
+ flexGrow: 1,
+ paddingBottom: 100,
+ },
+ header: {
+ backgroundColor: '#fff',
+ padding: 20,
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ borderBottomColor: '#e0e0e0',
+ },
+ headerTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#333',
+ marginBottom: 8,
+ },
+ headerSubtitle: {
+ fontSize: 16,
+ color: '#666',
+ textAlign: 'center',
+ },
+ body: {
+ flex: 1,
+ padding: 20,
+ },
+ sectionTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#333',
+ marginTop: 20,
+ marginBottom: 15,
+ },
+ buttonRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 15,
+ },
+ button: {
+ flex: 1,
+ backgroundColor: '#007AFF',
+ paddingVertical: 15,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ marginHorizontal: 5,
+ alignItems: 'center',
+ justifyContent: 'center',
+ minHeight: 50,
+ elevation: 2,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.2,
+ shadowRadius: 2,
+ },
+ buttonText: {
+ color: '#fff',
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ instructions: {
+ backgroundColor: '#fff',
+ padding: 20,
+ borderRadius: 12,
+ marginTop: 30,
+ elevation: 1,
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 1 },
+ shadowOpacity: 0.1,
+ shadowRadius: 2,
+ },
+ instructionsTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: '#333',
+ marginBottom: 10,
+ },
+ instructionsText: {
+ fontSize: 14,
+ color: '#666',
+ lineHeight: 22,
+ },
+ configInfo: {
+ backgroundColor: '#E3F2FD',
+ padding: 20,
+ borderRadius: 12,
+ marginTop: 16,
+ borderWidth: 1,
+ borderColor: '#BBDEFB',
+ },
+ configTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#1976D2',
+ marginBottom: 10,
+ },
+ configText: {
+ fontSize: 13,
+ color: '#1565C0',
+ lineHeight: 20,
+ },
+ settingRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ backgroundColor: '#fff',
+ padding: 16,
+ borderRadius: 8,
+ marginBottom: 8,
+ },
+ settingLabel: {
+ fontSize: 16,
+ color: '#333',
+ fontWeight: '500',
+ },
+});
diff --git a/example/src/screens/WebSocketScreen.tsx b/example/src/screens/WebSocketScreen.tsx
new file mode 100644
index 0000000..131d2e6
--- /dev/null
+++ b/example/src/screens/WebSocketScreen.tsx
@@ -0,0 +1,539 @@
+import React, { useState, useRef, useCallback, useEffect } from 'react';
+import {
+ View,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ TextInput,
+ ActivityIndicator,
+ FlatList,
+} from 'react-native';
+
+type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
+
+interface WebSocketMessage {
+ id: string;
+ type: 'sent' | 'received';
+ data: string;
+ timestamp: Date;
+}
+
+interface ButtonComponentProps {
+ title: string;
+ onPress: () => void;
+ color?: string;
+ loading?: boolean;
+ disabled?: boolean;
+}
+
+const ButtonComponent: React.FC = ({
+ title,
+ onPress,
+ color = '#007AFF',
+ loading = false,
+ disabled = false,
+}) => (
+
+ {loading ? (
+
+ ) : (
+ {title}
+ )}
+
+);
+
+const getStatusColor = (status: ConnectionStatus): string => {
+ switch (status) {
+ case 'connected':
+ return '#4CAF50';
+ case 'connecting':
+ return '#FF9800';
+ case 'error':
+ return '#F44336';
+ default:
+ return '#9E9E9E';
+ }
+};
+
+const getStatusText = (status: ConnectionStatus): string => {
+ switch (status) {
+ case 'connected':
+ return 'Connected';
+ case 'connecting':
+ return 'Connecting...';
+ case 'error':
+ return 'Error';
+ default:
+ return 'Disconnected';
+ }
+};
+
+export const WebSocketScreen: React.FC = () => {
+ const [status, setStatus] = useState('disconnected');
+ const [messages, setMessages] = useState([]);
+ const [inputMessage, setInputMessage] = useState('');
+ const [serverUrl, setServerUrl] = useState('wss://echo.websocket.org');
+ const wsRef = useRef(null);
+ const messageIdRef = useRef(0);
+
+ const addMessage = useCallback((type: 'sent' | 'received', data: string) => {
+ const newMessage: WebSocketMessage = {
+ id: `msg-${messageIdRef.current++}`,
+ type,
+ data,
+ timestamp: new Date(),
+ };
+ setMessages((prev) => [newMessage, ...prev]);
+ }, []);
+
+ const connect = useCallback(() => {
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
+ return;
+ }
+
+ setStatus('connecting');
+
+ try {
+ const ws = new WebSocket(serverUrl);
+
+ ws.onopen = () => {
+ setStatus('connected');
+ addMessage('received', '[System] Connection established');
+ };
+
+ ws.onmessage = (event) => {
+ addMessage('received', event.data);
+ };
+
+ ws.onerror = () => {
+ setStatus('error');
+ addMessage('received', '[System] Connection error occurred');
+ };
+
+ ws.onclose = (event) => {
+ setStatus('disconnected');
+ addMessage(
+ 'received',
+ `[System] Connection closed (code: ${event.code})`
+ );
+ wsRef.current = null;
+ };
+
+ wsRef.current = ws;
+ } catch (error) {
+ setStatus('error');
+ addMessage(
+ 'received',
+ `[System] Failed to connect: ${error instanceof Error ? error.message : 'Unknown error'}`
+ );
+ }
+ }, [serverUrl, addMessage]);
+
+ const disconnect = useCallback(() => {
+ if (wsRef.current) {
+ wsRef.current.close();
+ wsRef.current = null;
+ }
+ }, []);
+
+ const sendMessage = useCallback(() => {
+ if (!inputMessage.trim() || wsRef.current?.readyState !== WebSocket.OPEN) {
+ return;
+ }
+
+ wsRef.current.send(inputMessage);
+ addMessage('sent', inputMessage);
+ setInputMessage('');
+ }, [inputMessage, addMessage]);
+
+ const sendPing = useCallback(() => {
+ if (wsRef.current?.readyState !== WebSocket.OPEN) {
+ return;
+ }
+
+ const pingMessage = JSON.stringify({ type: 'ping', timestamp: Date.now() });
+ wsRef.current.send(pingMessage);
+ addMessage('sent', pingMessage);
+ }, [addMessage]);
+
+ const sendJsonMessage = useCallback(() => {
+ if (wsRef.current?.readyState !== WebSocket.OPEN) {
+ return;
+ }
+
+ const jsonMessage = JSON.stringify({
+ action: 'subscribe',
+ channel: 'updates',
+ data: {
+ userId: 123,
+ preferences: ['notifications', 'alerts'],
+ },
+ });
+ wsRef.current.send(jsonMessage);
+ addMessage('sent', jsonMessage);
+ }, [addMessage]);
+
+ const clearMessages = useCallback(() => {
+ setMessages([]);
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ if (wsRef.current) {
+ wsRef.current.close();
+ }
+ };
+ }, []);
+
+ const renderMessage = ({ item }: { item: WebSocketMessage }) => (
+
+
+
+ {item.type === 'sent' ? 'SENT' : 'RECEIVED'}
+
+
+ {item.timestamp.toLocaleTimeString()}
+
+
+ {item.data}
+
+ );
+
+ const isConnected = status === 'connected';
+
+ return (
+
+
+ WebSocket Testing
+
+ Connect, send messages, and view WebSocket traffic
+
+
+
+
+
+
+
+ {getStatusText(status)}
+
+
+
+ Server URL
+
+
+
+
+
+
+
+ Send Message
+
+
+
+ Send
+
+
+
+ Quick Actions
+
+
+
+
+
+
+ Messages
+
+ Clear
+
+
+
+
+ {messages.length === 0 ? (
+
+
+ No messages yet. Connect and send a message to see it here.
+
+
+ ) : (
+ item.id}
+ showsVerticalScrollIndicator={false}
+ contentContainerStyle={styles.messagesList}
+ />
+ )}
+
+
+
+ How to use:
+
+ 1. Enter a WebSocket server URL (default is echo server){'\n'}
+ 2. Tap "Connect" to establish connection{'\n'}
+ 3. Type messages and tap "Send" to transmit{'\n'}
+ 4. Use "Send Ping" or "Send JSON" for quick tests{'\n'}
+ 5. View sent/received messages in real-time{'\n'}
+ 6. Check the network logger for WebSocket frames
+
+
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: '#f5f5f5',
+ },
+ header: {
+ backgroundColor: '#fff',
+ padding: 20,
+ alignItems: 'center',
+ borderBottomWidth: 1,
+ borderBottomColor: '#e0e0e0',
+ },
+ headerTitle: {
+ fontSize: 24,
+ fontWeight: 'bold',
+ color: '#333',
+ marginBottom: 8,
+ },
+ headerSubtitle: {
+ fontSize: 16,
+ color: '#666',
+ textAlign: 'center',
+ },
+ body: {
+ flex: 1,
+ padding: 20,
+ },
+ statusBar: {
+ flexDirection: 'row',
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#fff',
+ padding: 12,
+ borderRadius: 8,
+ marginBottom: 16,
+ },
+ statusIndicator: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ statusDot: {
+ width: 12,
+ height: 12,
+ borderRadius: 6,
+ marginRight: 8,
+ },
+ statusText: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#333',
+ },
+ sectionTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#333',
+ marginTop: 16,
+ marginBottom: 8,
+ },
+ urlInput: {
+ backgroundColor: '#fff',
+ padding: 12,
+ borderRadius: 8,
+ fontSize: 14,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ },
+ buttonRow: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginTop: 12,
+ },
+ button: {
+ flex: 1,
+ backgroundColor: '#007AFF',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ borderRadius: 8,
+ marginHorizontal: 4,
+ alignItems: 'center',
+ justifyContent: 'center',
+ minHeight: 44,
+ },
+ buttonDisabled: {
+ opacity: 0.5,
+ },
+ buttonText: {
+ color: '#fff',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ inputRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ messageInput: {
+ flex: 1,
+ backgroundColor: '#fff',
+ padding: 12,
+ borderRadius: 8,
+ fontSize: 14,
+ borderWidth: 1,
+ borderColor: '#e0e0e0',
+ marginRight: 8,
+ },
+ sendButton: {
+ backgroundColor: '#007AFF',
+ paddingVertical: 12,
+ paddingHorizontal: 20,
+ borderRadius: 8,
+ },
+ sendButtonText: {
+ color: '#fff',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ messagesHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ },
+ clearButton: {
+ color: '#007AFF',
+ fontSize: 14,
+ fontWeight: '600',
+ },
+ messagesContainer: {
+ flex: 1,
+ backgroundColor: '#fff',
+ borderRadius: 8,
+ minHeight: 150,
+ maxHeight: 250,
+ },
+ messagesList: {
+ padding: 8,
+ },
+ emptyState: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ },
+ emptyStateText: {
+ color: '#999',
+ textAlign: 'center',
+ fontSize: 14,
+ },
+ messageContainer: {
+ padding: 10,
+ borderRadius: 8,
+ marginBottom: 8,
+ },
+ sentMessage: {
+ backgroundColor: '#E3F2FD',
+ marginLeft: 20,
+ },
+ receivedMessage: {
+ backgroundColor: '#F5F5F5',
+ marginRight: 20,
+ },
+ messageHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ marginBottom: 4,
+ },
+ messageType: {
+ fontSize: 10,
+ fontWeight: '700',
+ color: '#666',
+ },
+ messageTime: {
+ fontSize: 10,
+ color: '#999',
+ },
+ messageData: {
+ fontSize: 13,
+ color: '#333',
+ },
+ instructions: {
+ backgroundColor: '#FFF3E0',
+ padding: 16,
+ borderRadius: 12,
+ marginTop: 16,
+ marginBottom: 100,
+ borderWidth: 1,
+ borderColor: '#FFE0B2',
+ },
+ instructionsTitle: {
+ fontSize: 16,
+ fontWeight: '600',
+ color: '#E65100',
+ marginBottom: 8,
+ },
+ instructionsText: {
+ fontSize: 13,
+ color: '#F57C00',
+ lineHeight: 20,
+ },
+});
diff --git a/src/NetworkLoggerOverlay.tsx b/src/NetworkLoggerOverlay.tsx
deleted file mode 100644
index d770fa3..0000000
--- a/src/NetworkLoggerOverlay.tsx
+++ /dev/null
@@ -1,585 +0,0 @@
-import React, { useEffect, useState, useMemo, useCallback } from 'react';
-import {
- Alert,
- FlatList,
- Modal,
- Platform,
- StatusBar,
- Text,
- TextInput,
- TouchableOpacity,
- View,
- type ListRenderItem,
- type ViewStyle,
- type TextStyle,
-} from 'react-native';
-
-import NetworkLogItem from './NetworkLogItem';
-import type { NetworkLog, NetworkLogger } from './types';
-import type { StatusFilterKey } from './utils/filters';
-import FloatingButton from './FloatingButton';
-import NonFloatingButton from './NonFloatingButton';
-import {
- colors,
- getThemeColors,
- type ThemeMode,
- type ThemeColors,
-} from './constants/colors';
-import { filterLogs, getLogStats, defaultFilterState } from './utils/filters';
-import type { FilterState } from './utils/filters';
-import { ExportModal } from './components/ExportModal';
-import { SvgIcon } from './SvgIcon';
-
-let RNShake: any = null;
-
-try {
- RNShake = require('react-native-shake').default;
-} catch (error) {
- RNShake = null;
-}
-
-interface NetworkLoggerOverlayProps {
- networkLogger: NetworkLogger;
- enabled?: boolean;
- enableDeviceShake?: boolean;
- showRequestHeader?: boolean;
- showResponseHeader?: boolean;
- draggable?: boolean;
- useCopyToClipboard?: boolean;
- theme?: ThemeMode;
- onThemeChange?: (theme: ThemeMode) => void;
-}
-
-const STATUS_FILTERS: { key: StatusFilterKey; label: string }[] = [
- { key: 'all', label: 'All' },
- { key: 'success', label: '2xx' },
- { key: 'redirect', label: '3xx' },
- { key: 'clientError', label: '4xx' },
- { key: 'serverError', label: '5xx' },
-];
-
-export const NetworkLoggerOverlay: React.FC = ({
- draggable,
- networkLogger,
- enabled = __DEV__,
- enableDeviceShake,
- showRequestHeader,
- showResponseHeader,
- useCopyToClipboard,
- theme: themeProp,
- onThemeChange,
-}) => {
- const [visible, setVisible] = useState(false);
- const [logs, setLogs] = useState([]);
- const [filters, setFilters] = useState(defaultFilterState);
- const [showButton, setShowButton] = useState(!enableDeviceShake);
- const [internalTheme, setInternalTheme] = useState(
- themeProp ?? 'dark'
- );
- const [exportModalVisible, setExportModalVisible] = useState(false);
-
- const theme = themeProp ?? internalTheme;
- const themeColors = useMemo(() => getThemeColors(theme), [theme]);
-
- const toggleTheme = useCallback(() => {
- const newTheme = internalTheme === 'light' ? 'dark' : 'light';
- setInternalTheme(newTheme);
- onThemeChange?.(newTheme);
- }, [internalTheme, onThemeChange]);
-
- const themedStyles = useMemo(
- () => createThemedStyles(themeColors),
- [themeColors]
- );
-
- useEffect(() => {
- if (enabled && !__DEV__) {
- console.warn(
- '[NetworkLoggerOverlay] Warning: Network logger is enabled in production mode. ' +
- 'This may expose sensitive data. Set enabled={false} or remove the prop to disable in production.'
- );
- }
- }, [enabled]);
-
- useEffect(() => {
- if (!enabled) return;
-
- const unsubscribe = networkLogger.subscribe(setLogs);
-
- if (enableDeviceShake && !RNShake) {
- throw new Error(
- 'react-native-shake is required to enableDeviceShake but module is not installed. Please install it with: npm install react-native-shake'
- );
- }
-
- if (!enableDeviceShake) {
- return () => {
- unsubscribe();
- };
- }
- const subscription = RNShake.addListener(() => {
- setShowButton(true);
- });
- return () => {
- unsubscribe();
- subscription?.remove();
- };
- }, [networkLogger, enableDeviceShake, enabled]);
-
- const handleCloseIcon = useCallback(() => {
- setShowButton(false);
- }, []);
-
- const filteredLogs = useMemo(
- () => filterLogs(logs, filters),
- [logs, filters]
- );
-
- const logStats = useMemo(() => getLogStats(logs), [logs]);
-
- const updateFilter = useCallback(
- (key: K, value: FilterState[K]) => {
- setFilters((prev) => ({ ...prev, [key]: value }));
- },
- []
- );
-
- const clearFilters = useCallback(() => {
- setFilters(defaultFilterState);
- }, []);
-
- const hasActiveFilters = useMemo(() => {
- return (
- filters.searchTerm !== '' ||
- filters.statusFilter !== 'all' ||
- filters.methodFilter !== null
- );
- }, [filters]);
-
- const handleClearLogs = useCallback((): void => {
- Alert.alert(
- 'Clear Logs',
- 'Are you sure you want to clear all network logs?',
- [
- { text: 'Cancel', style: 'cancel' },
- { text: 'Clear', onPress: () => networkLogger.clearLogs() },
- ]
- );
- }, [networkLogger]);
-
- const handleDeleteLog = useCallback(
- (logId: number): void => {
- networkLogger.deleteLog(logId);
- },
- [networkLogger]
- );
-
- const handleModalOpen = useCallback((): void => {
- setVisible(true);
- }, []);
-
- const handleModalClose = useCallback((): void => {
- setVisible(false);
- }, []);
-
- const handleSearchChange = useCallback(
- (text: string): void => {
- updateFilter('searchTerm', text);
- },
- [updateFilter]
- );
-
- const handleStatusFilterChange = useCallback(
- (key: StatusFilterKey): void => {
- updateFilter('statusFilter', key);
- },
- [updateFilter]
- );
-
- const renderLogItem: ListRenderItem = ({ item, index }) => (
-
- );
-
- const renderEmptyList = (): React.ReactElement => (
-
-
-
-
- Listening for traffic...
-
- No network requests logged yet. Trigger an API call in your app to see
- it here.
-
-
-
- Interceptor Active
-
-
- );
-
- const keyExtractor = (item: NetworkLog): string => item.id.toString();
-
- const buttonProps = {
- draggable,
- enableDeviceShake,
- hideIcon: handleCloseIcon,
- openModal: handleModalOpen,
- logsLength: logs.length,
- errorCount: logStats.errors,
- theme,
- };
-
- if (!enabled) {
- return null;
- }
-
- return (
- <>
- {showButton &&
- (draggable ? (
-
- ) : (
-
- ))}
-
-
-
- {/* Header */}
-
-
-
- NETWORK LOGS
-
-
-
-
-
- {
- setVisible(false);
- setExportModalVisible(true);
- }}
- activeOpacity={0.7}
- >
-
-
-
-
-
-
-
-
-
-
-
- {/* Search & Filters */}
-
-
-
-
-
-
-
- {STATUS_FILTERS.map((filter) => (
- handleStatusFilterChange(filter.key)}
- activeOpacity={0.7}
- >
-
- {filter.label}
-
-
- ))}
-
- {hasActiveFilters && (
-
- Clear
-
- )}
-
-
-
- {/* Stats Bar */}
- {logs.length > 0 && (
-
-
- {filteredLogs.length} of {logs.length} requests
-
- {logStats.errors > 0 && (
-
- {logStats.errors} errors
-
- )}
-
- )}
-
- {/* Log List */}
-
-
-
- setExportModalVisible(false)}
- logs={filteredLogs}
- theme={theme}
- />
- >
- );
-};
-
-const createThemedStyles = (themeColors: ThemeColors) => ({
- modalContainer: {
- flex: 1,
- backgroundColor: themeColors.background,
- paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 44,
- } as ViewStyle,
- header: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 12,
- paddingHorizontal: 12,
- backgroundColor: themeColors.surfaceContainer,
- borderBottomWidth: 1,
- borderBottomColor: themeColors.border,
- } as ViewStyle,
- title: {
- fontSize: 14,
- fontWeight: '800',
- color: themeColors.text,
- letterSpacing: 1,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- } as TextStyle,
- searchContainer: {
- paddingHorizontal: 12,
- paddingVertical: 12,
- backgroundColor: themeColors.surfaceContainer,
- borderBottomWidth: 1,
- borderBottomColor: themeColors.border,
- gap: 10,
- } as ViewStyle,
- searchInputContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: themeColors.surfaceContainerLow,
- borderWidth: 1,
- borderColor: themeColors.border,
- paddingHorizontal: 12,
- gap: 8,
- } as ViewStyle,
- searchInput: {
- flex: 1,
- paddingVertical: 10,
- fontSize: 12,
- color: themeColors.text,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- } as TextStyle,
- filterChip: {
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderWidth: 1,
- borderColor: themeColors.border,
- backgroundColor: 'transparent',
- } as ViewStyle,
- filterChipActive: {
- backgroundColor: themeColors.primaryContainer,
- borderColor: themeColors.primaryContainer,
- } as ViewStyle,
- filterChipText: {
- fontSize: 11,
- fontWeight: '700',
- color: themeColors.textMuted,
- letterSpacing: 0.5,
- } as TextStyle,
- clearFiltersText: {
- fontSize: 11,
- color: themeColors.primary,
- fontWeight: '600',
- } as TextStyle,
- statsBar: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingHorizontal: 12,
- paddingVertical: 8,
- backgroundColor: themeColors.surfaceContainerLow,
- borderBottomWidth: 1,
- borderBottomColor: themeColors.borderSubtle,
- } as ViewStyle,
- statsText: {
- fontSize: 11,
- color: themeColors.textMuted,
- fontWeight: '500',
- } as TextStyle,
- emptyContainer: {
- // flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- paddingHorizontal: 40,
- marginTop: '25%',
- } as ViewStyle,
- emptyIconContainer: {
- width: 80,
- height: 80,
- borderWidth: 2,
- borderStyle: 'dashed',
- borderColor: themeColors.border,
- borderRadius: 40,
- justifyContent: 'center',
- alignItems: 'center',
- marginBottom: 24,
- backgroundColor: themeColors.surfaceContainerLow,
- } as ViewStyle,
- emptyTitle: {
- fontSize: 18,
- fontWeight: '600',
- color: themeColors.text,
- marginBottom: 8,
- } as TextStyle,
- emptyText: {
- textAlign: 'center',
- color: themeColors.textMuted,
- fontSize: 14,
- lineHeight: 20,
- marginBottom: 24,
- } as TextStyle,
- statusPill: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: themeColors.surfaceContainer,
- borderWidth: 1,
- borderColor: themeColors.border,
- paddingHorizontal: 12,
- paddingVertical: 8,
- gap: 8,
- } as ViewStyle,
- statusPillText: {
- fontSize: 12,
- color: themeColors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- } as TextStyle,
-});
-
-const staticStyles = {
- headerLeft: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- } as ViewStyle,
- headerRight: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 4,
- } as ViewStyle,
- iconButton: {
- padding: 8,
- } as ViewStyle,
- filterRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- } as ViewStyle,
- filterChips: {
- flexDirection: 'row',
- gap: 6,
- } as ViewStyle,
- filterChipTextActive: {
- color: '#FFFFFF',
- } as TextStyle,
- errorStats: {
- fontSize: 11,
- color: colors.error,
- fontWeight: '600',
- } as TextStyle,
- listContainer: {
- paddingBottom: 34,
- } as ViewStyle,
- emptyListContainer: {
- flexGrow: 1,
- } as ViewStyle,
- statusDot: {
- width: 8,
- height: 8,
- borderRadius: 4,
- backgroundColor: colors.success,
- } as ViewStyle,
-};
-
-export type { NetworkLoggerOverlayProps, NetworkLogger };
diff --git a/src/components/Badge.tsx b/src/components/common/Badge.tsx
similarity index 98%
rename from src/components/Badge.tsx
rename to src/components/common/Badge.tsx
index 277d8ee..30d49c8 100644
--- a/src/components/Badge.tsx
+++ b/src/components/common/Badge.tsx
@@ -6,7 +6,7 @@ import {
type StyleProp,
type ViewStyle,
} from 'react-native';
-import { colors } from '../constants/colors';
+import { colors } from '../../constants/colors';
export type BadgeVariant = 'success' | 'warning' | 'error' | 'info' | 'pending';
diff --git a/src/CopyItem.tsx b/src/components/common/CopyItem.tsx
similarity index 96%
rename from src/CopyItem.tsx
rename to src/components/common/CopyItem.tsx
index 3906489..8940b29 100644
--- a/src/CopyItem.tsx
+++ b/src/components/common/CopyItem.tsx
@@ -8,8 +8,8 @@ import {
type StyleProp,
type ViewStyle,
} from 'react-native';
-import { SvgIcon } from './SvgIcon';
-import { colors } from './constants/colors';
+import { SvgIcon } from '../../icons';
+import { colors } from '../../constants/colors';
let Clipboard: any = null;
try {
diff --git a/src/components/ExportModal.tsx b/src/components/common/ExportModal.tsx
similarity index 96%
rename from src/components/ExportModal.tsx
rename to src/components/common/ExportModal.tsx
index ff93688..567ae2e 100644
--- a/src/components/ExportModal.tsx
+++ b/src/components/common/ExportModal.tsx
@@ -11,15 +11,15 @@ import {
type ViewStyle,
type TextStyle,
} from 'react-native';
-import type { NetworkLog } from '../types';
-import type { ThemeMode, ThemeColors } from '../constants/colors';
-import { getThemeColors } from '../constants/colors';
+import type { NetworkLog } from '../../types';
+import type { ThemeMode, ThemeColors } from '../../constants/colors';
+import { getThemeColors } from '../../constants/colors';
import {
exportLogs,
getExportFileName,
type ExportFormat,
-} from '../utils/export';
-import { SvgIcon, type IconName } from '../SvgIcon';
+} from '../../utils/export';
+import { SvgIcon, type IconName } from '../../icons';
interface ExportModalProps {
visible: boolean;
diff --git a/src/components/FilterChip.tsx b/src/components/common/FilterChip.tsx
similarity index 94%
rename from src/components/FilterChip.tsx
rename to src/components/common/FilterChip.tsx
index 9a3eccb..dc7c384 100644
--- a/src/components/FilterChip.tsx
+++ b/src/components/common/FilterChip.tsx
@@ -6,8 +6,8 @@ import {
type StyleProp,
type ViewStyle,
} from 'react-native';
-import type { ThemeMode } from '../constants/colors';
-import { colors, getThemeColors } from '../constants/colors';
+import type { ThemeMode } from '../../constants/colors';
+import { colors, getThemeColors } from '../../constants/colors';
interface FilterChipProps {
label: string;
diff --git a/src/FloatingButton.tsx b/src/components/common/FloatingButton.tsx
similarity index 98%
rename from src/FloatingButton.tsx
rename to src/components/common/FloatingButton.tsx
index ff4b339..072cf6b 100644
--- a/src/FloatingButton.tsx
+++ b/src/components/common/FloatingButton.tsx
@@ -6,9 +6,9 @@ import {
Dimensions,
} from 'react-native';
import React, { useEffect } from 'react';
-import { SvgIcon } from './SvgIcon';
+import { SvgIcon } from '../../icons';
import NonFloatingButton from './NonFloatingButton';
-import { colors, getThemeColors, type ThemeMode } from './constants/colors';
+import { colors, getThemeColors, type ThemeMode } from '../../constants/colors';
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
diff --git a/src/components/JsonViewer.tsx b/src/components/common/JsonViewer.tsx
similarity index 98%
rename from src/components/JsonViewer.tsx
rename to src/components/common/JsonViewer.tsx
index 8aa3220..9bbca37 100644
--- a/src/components/JsonViewer.tsx
+++ b/src/components/common/JsonViewer.tsx
@@ -7,8 +7,8 @@ import {
type StyleProp,
type ViewStyle,
} from 'react-native';
-import type { ThemeMode } from '../constants/colors';
-import { getThemeColors } from '../constants/colors';
+import type { ThemeMode } from '../../constants/colors';
+import { getThemeColors } from '../../constants/colors';
interface JsonViewerProps {
data: string | object | null;
diff --git a/src/NonFloatingButton.tsx b/src/components/common/NonFloatingButton.tsx
similarity index 96%
rename from src/NonFloatingButton.tsx
rename to src/components/common/NonFloatingButton.tsx
index 0c65cb0..5408fd1 100644
--- a/src/NonFloatingButton.tsx
+++ b/src/components/common/NonFloatingButton.tsx
@@ -1,7 +1,7 @@
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import React from 'react';
-import { SvgIcon } from './SvgIcon';
-import { colors, getThemeColors, type ThemeMode } from './constants/colors';
+import { SvgIcon } from '../../icons';
+import { colors, getThemeColors, type ThemeMode } from '../../constants/colors';
interface NonFloatingButtonProps {
openModal: () => void;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
new file mode 100644
index 0000000..b7d8db8
--- /dev/null
+++ b/src/components/common/index.ts
@@ -0,0 +1,7 @@
+export * from './Badge';
+export * from './FilterChip';
+export * from './JsonViewer';
+export * from './CopyItem';
+export * from './ExportModal';
+export { default as FloatingButton } from './FloatingButton';
+export { default as NonFloatingButton } from './NonFloatingButton';
diff --git a/src/components/index.ts b/src/components/index.ts
index 49d0654..d0b9323 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,4 +1 @@
-export * from './Badge';
-export * from './FilterChip';
-export * from './JsonViewer';
-export * from './ExportModal';
+export * from './common';
diff --git a/src/constants/colors.ts b/src/constants/colors.ts
index cbd465e..3494505 100644
--- a/src/constants/colors.ts
+++ b/src/constants/colors.ts
@@ -65,6 +65,16 @@ export const colors = {
clientError: '#EF4444', // 4xx
serverError: '#A855F7', // 5xx
},
+
+ // WebSocket colors
+ websocket: {
+ connecting: '#F59E0B',
+ open: '#10B981',
+ closing: '#F97316',
+ closed: '#94A3B8',
+ sent: '#3B82F6',
+ received: '#10B981',
+ },
} as const;
export type ThemeMode = 'light' | 'dark';
@@ -105,3 +115,15 @@ export const getMethodColor = (method: string): string => {
const upperMethod = method.toUpperCase() as keyof typeof colors.methods;
return colors.methods[upperMethod] || colors.info;
};
+
+export const getWebSocketStateColor = (
+ state: 'connecting' | 'open' | 'closing' | 'closed'
+): string => {
+ return colors.websocket[state] || colors.pending;
+};
+
+export const getWebSocketMessageColor = (
+ direction: 'sent' | 'received'
+): string => {
+ return colors.websocket[direction];
+};
diff --git a/src/constants/config.ts b/src/constants/config.ts
index 4356efd..8270c25 100644
--- a/src/constants/config.ts
+++ b/src/constants/config.ts
@@ -1,4 +1,4 @@
-import type { NetworkLoggerConfig } from '../types';
+import type { NetworkLoggerConfig, WebSocketLoggerConfig } from '../types';
export const DEFAULT_CONFIG: NetworkLoggerConfig = {
maxLogs: 100,
@@ -46,6 +46,13 @@ export const FILTER_OPTIONS = {
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
} as const;
+export const DEFAULT_WS_CONFIG: WebSocketLoggerConfig = {
+ maxConnections: 50,
+ maxMessagesPerConnection: 500,
+ ignoredUrls: [],
+ captureMessages: true,
+};
+
export const UI_CONFIG = {
floatingButton: {
size: 58,
diff --git a/src/NetworkLogItem.tsx b/src/features/http/NetworkLogItem.tsx
similarity index 97%
rename from src/NetworkLogItem.tsx
rename to src/features/http/NetworkLogItem.tsx
index 23eed1b..04c88c2 100644
--- a/src/NetworkLogItem.tsx
+++ b/src/features/http/NetworkLogItem.tsx
@@ -11,26 +11,26 @@ import {
type ViewStyle,
type TextStyle,
} from 'react-native';
-import type { NetworkLog, NetworkRequestHeaders } from './types';
-import type { ThemeMode, ThemeColors } from './constants/colors';
+import type { NetworkLog, NetworkRequestHeaders } from '../../types';
+import type { ThemeMode, ThemeColors } from '../../constants/colors';
import {
colors,
getThemeColors,
getStatusColor,
getMethodColor,
-} from './constants/colors';
-import { CopyItem } from './CopyItem';
-import { JsonViewer } from './components/JsonViewer';
-import { SvgIcon } from './SvgIcon';
-import { formatTimestamp } from './utils/formatters';
-import { generateCurl } from './utils/curl';
+} from '../../constants/colors';
+import { CopyItem } from '../../components/common';
+import { JsonViewer } from '../../components/common';
+import { SvgIcon } from '../../icons';
+import { formatTimestamp } from '../../utils/formatters';
+import { generateCurl } from '../../utils/curl';
import {
replayRequest,
canReplayRequest,
getReplayWarnings,
-} from './utils/replay';
-import { exportSingleRequestAsJSON } from './utils/export';
-import { detectSensitiveData, redactHeaders } from './utils/redact';
+} from '../../utils/replay';
+import { exportSingleRequestAsJSON } from '../../utils/export';
+import { detectSensitiveData, redactHeaders } from '../../utils/redact';
interface NetworkLogItemProps {
log: NetworkLog;
diff --git a/src/features/http/NetworkLoggerOverlay.tsx b/src/features/http/NetworkLoggerOverlay.tsx
new file mode 100644
index 0000000..9b8c3e3
--- /dev/null
+++ b/src/features/http/NetworkLoggerOverlay.tsx
@@ -0,0 +1,960 @@
+import React, { useEffect, useState, useMemo, useCallback } from 'react';
+import {
+ Alert,
+ FlatList,
+ Modal,
+ Platform,
+ StatusBar,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+ type ListRenderItem,
+ type ViewStyle,
+ type TextStyle,
+} from 'react-native';
+
+import NetworkLogItem from './NetworkLogItem';
+import { WebSocketLogItem } from '../websocket';
+import type {
+ NetworkLog,
+ NetworkLogger,
+ WebSocketLog,
+ IWebSocketLogger,
+} from '../../types';
+import type { StatusFilterKey } from '../../utils/filters';
+import {
+ FloatingButton,
+ NonFloatingButton,
+ ExportModal,
+} from '../../components/common';
+import {
+ colors,
+ getThemeColors,
+ type ThemeMode,
+ type ThemeColors,
+} from '../../constants/colors';
+import {
+ filterLogs,
+ getLogStats,
+ defaultFilterState,
+} from '../../utils/filters';
+import type { FilterState } from '../../utils/filters';
+import { SvgIcon } from '../../icons';
+
+let RNShake: any = null;
+
+try {
+ RNShake = require('react-native-shake').default;
+} catch (error) {
+ RNShake = null;
+}
+
+type ActiveTab = 'http' | 'websocket';
+
+interface NetworkLoggerOverlayProps {
+ networkLogger: NetworkLogger;
+ webSocketLogger?: IWebSocketLogger;
+ enabled?: boolean;
+ enableWebSocket?: boolean;
+ enableDeviceShake?: boolean;
+ showRequestHeader?: boolean;
+ showResponseHeader?: boolean;
+ draggable?: boolean;
+ useCopyToClipboard?: boolean;
+ theme?: ThemeMode;
+ onThemeChange?: (theme: ThemeMode) => void;
+}
+
+class NetworkLoggerSetupError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = 'NetworkLoggerSetupError';
+ }
+}
+
+const STATUS_FILTERS: { key: StatusFilterKey; label: string }[] = [
+ { key: 'all', label: 'All' },
+ { key: 'success', label: '2xx' },
+ { key: 'redirect', label: '3xx' },
+ { key: 'clientError', label: '4xx' },
+ { key: 'serverError', label: '5xx' },
+];
+
+export const NetworkLoggerOverlay: React.FC = ({
+ draggable,
+ networkLogger,
+ webSocketLogger,
+ enabled = __DEV__,
+ enableWebSocket = false,
+ enableDeviceShake,
+ showRequestHeader,
+ showResponseHeader,
+ useCopyToClipboard,
+ theme: themeProp,
+ onThemeChange,
+}) => {
+ const [visible, setVisible] = useState(false);
+ const [activeTab, setActiveTab] = useState('http');
+ const [logs, setLogs] = useState([]);
+ const [wsLogs, setWsLogs] = useState([]);
+ const [filters, setFilters] = useState(defaultFilterState);
+ const [wsSearchTerm, setWsSearchTerm] = useState('');
+ const [showButton, setShowButton] = useState(!enableDeviceShake);
+ const [internalTheme, setInternalTheme] = useState(
+ themeProp ?? 'dark'
+ );
+ const [exportModalVisible, setExportModalVisible] = useState(false);
+
+ const theme = themeProp ?? internalTheme;
+ const themeColors = useMemo(() => getThemeColors(theme), [theme]);
+
+ const toggleTheme = useCallback(() => {
+ const newTheme = internalTheme === 'light' ? 'dark' : 'light';
+ setInternalTheme(newTheme);
+ onThemeChange?.(newTheme);
+ }, [internalTheme, onThemeChange]);
+
+ const themedStyles = useMemo(
+ () => createThemedStyles(themeColors),
+ [themeColors]
+ );
+
+ useEffect(() => {
+ if (enabled && !__DEV__) {
+ console.warn(
+ '[NetworkLoggerOverlay] Warning: Debugger is enabled in production mode. ' +
+ 'This may expose sensitive data. Set enabled={false} or remove the prop to disable in production.'
+ );
+ }
+ }, [enabled]);
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ // Validate HTTP interceptor setup
+ if (!networkLogger.isSetup()) {
+ throw new NetworkLoggerSetupError(
+ '[NetworkLoggerOverlay] HTTP interceptor not initialized.\n\n' +
+ 'Please call networkLogger.setupInterceptor() before rendering DebuggerOverlay.\n\n' +
+ 'Example:\n' +
+ ' useEffect(() => {\n' +
+ ' networkLogger.setupInterceptor();\n' +
+ ' }, []);\n\n' +
+ 'Make sure this is called BEFORE any network requests are made.'
+ );
+ }
+
+ // Validate WebSocket interceptor setup if enabled
+ if (enableWebSocket) {
+ if (!webSocketLogger) {
+ throw new NetworkLoggerSetupError(
+ '[NetworkLoggerOverlay] WebSocket debugging is enabled but webSocketLogger prop is missing.\n\n' +
+ 'Please pass the webSocketLogger prop when enableWebSocket={true}.\n\n' +
+ 'Example:\n' +
+ ' import { webSocketLogger, DebuggerOverlay } from "react-native-api-debugger";\n\n' +
+ ' '
+ );
+ }
+
+ if (!webSocketLogger.isSetup()) {
+ throw new NetworkLoggerSetupError(
+ '[NetworkLoggerOverlay] WebSocket interceptor not initialized.\n\n' +
+ 'Please call webSocketLogger.setupInterceptor() before rendering DebuggerOverlay with enableWebSocket={true}.\n\n' +
+ 'Example:\n' +
+ ' useEffect(() => {\n' +
+ ' networkLogger.setupInterceptor();\n' +
+ ' webSocketLogger.setupInterceptor();\n' +
+ ' }, []);\n\n' +
+ 'Make sure this is called BEFORE any WebSocket connections are made.'
+ );
+ }
+ }
+
+ const unsubscribeNetwork = networkLogger.subscribe(setLogs);
+ const unsubscribeWs = enableWebSocket
+ ? webSocketLogger?.subscribe(setWsLogs)
+ : undefined;
+
+ if (enableDeviceShake && !RNShake) {
+ throw new NetworkLoggerSetupError(
+ '[NetworkLoggerOverlay] react-native-shake is required for enableDeviceShake but module is not installed.\n\n' +
+ 'Please install it with:\n' +
+ ' npm install react-native-shake\n\n' +
+ 'Or disable device shake:\n' +
+ ' '
+ );
+ }
+
+ if (!enableDeviceShake) {
+ return () => {
+ unsubscribeNetwork();
+ unsubscribeWs?.();
+ };
+ }
+ const subscription = RNShake.addListener(() => {
+ setShowButton(true);
+ });
+ return () => {
+ unsubscribeNetwork();
+ unsubscribeWs?.();
+ subscription?.remove();
+ };
+ }, [
+ networkLogger,
+ webSocketLogger,
+ enableWebSocket,
+ enableDeviceShake,
+ enabled,
+ ]);
+
+ const handleCloseIcon = useCallback(() => {
+ setShowButton(false);
+ }, []);
+
+ const filteredLogs = useMemo(
+ () => filterLogs(logs, filters),
+ [logs, filters]
+ );
+
+ const filteredWsLogs = useMemo(() => {
+ if (!wsSearchTerm) return wsLogs;
+ const term = wsSearchTerm.toLowerCase();
+ return wsLogs.filter(
+ (log) =>
+ log.url.toLowerCase().includes(term) ||
+ log.state.toLowerCase().includes(term)
+ );
+ }, [wsLogs, wsSearchTerm]);
+
+ const logStats = useMemo(() => getLogStats(logs), [logs]);
+
+ const wsStats = useMemo(() => {
+ const active = wsLogs.filter(
+ (l) => l.state === 'open' || l.state === 'connecting'
+ ).length;
+ const errors = wsLogs.filter((l) => l.error).length;
+ return { active, errors, total: wsLogs.length };
+ }, [wsLogs]);
+
+ const updateFilter = useCallback(
+ (key: K, value: FilterState[K]) => {
+ setFilters((prev) => ({ ...prev, [key]: value }));
+ },
+ []
+ );
+
+ const clearFilters = useCallback(() => {
+ setFilters(defaultFilterState);
+ setWsSearchTerm('');
+ }, []);
+
+ const hasActiveFilters = useMemo(() => {
+ return (
+ filters.searchTerm !== '' ||
+ filters.statusFilter !== 'all' ||
+ filters.methodFilter !== null ||
+ wsSearchTerm !== ''
+ );
+ }, [filters, wsSearchTerm]);
+
+ const handleClearLogs = useCallback((): void => {
+ const isHttp = activeTab === 'http';
+ Alert.alert(
+ 'Clear Logs',
+ `Are you sure you want to clear all ${isHttp ? 'HTTP' : 'WebSocket'} logs?`,
+ [
+ { text: 'Cancel', style: 'cancel' },
+ {
+ text: 'Clear',
+ onPress: () => {
+ if (isHttp) {
+ networkLogger.clearLogs();
+ } else {
+ webSocketLogger?.clearLogs();
+ }
+ },
+ },
+ ]
+ );
+ }, [activeTab, networkLogger, webSocketLogger]);
+
+ const handleDeleteLog = useCallback(
+ (logId: number): void => {
+ networkLogger.deleteLog(logId);
+ },
+ [networkLogger]
+ );
+
+ const handleDeleteWsLog = useCallback(
+ (logId: number): void => {
+ webSocketLogger?.deleteLog(logId);
+ },
+ [webSocketLogger]
+ );
+
+ const handleCloseWsConnection = useCallback(
+ (logId: number): void => {
+ webSocketLogger?.closeConnection?.(logId);
+ },
+ [webSocketLogger]
+ );
+
+ const handleModalOpen = useCallback((): void => {
+ setVisible(true);
+ }, []);
+
+ const handleModalClose = useCallback((): void => {
+ setVisible(false);
+ }, []);
+
+ const handleSearchChange = useCallback(
+ (text: string): void => {
+ if (activeTab === 'http') {
+ updateFilter('searchTerm', text);
+ } else {
+ setWsSearchTerm(text);
+ }
+ },
+ [activeTab, updateFilter]
+ );
+
+ const handleStatusFilterChange = useCallback(
+ (key: StatusFilterKey): void => {
+ updateFilter('statusFilter', key);
+ },
+ [updateFilter]
+ );
+
+ const renderLogItem: ListRenderItem = useCallback(
+ ({ item, index }) => (
+
+ ),
+ [
+ useCopyToClipboard,
+ showResponseHeader,
+ showRequestHeader,
+ theme,
+ handleDeleteLog,
+ handleModalClose,
+ ]
+ );
+
+ const renderWsLogItem: ListRenderItem = useCallback(
+ ({ item, index }) => (
+
+ ),
+ [useCopyToClipboard, theme, handleDeleteWsLog, handleCloseWsConnection]
+ );
+
+ const renderEmptyHttpList = useCallback(
+ (): React.ReactElement => (
+
+
+
+
+ Listening for traffic...
+
+ No HTTP requests logged yet. Trigger an API call in your app to see it
+ here.
+
+
+
+ Interceptor Active
+
+
+ ),
+ [themedStyles, themeColors]
+ );
+
+ const renderEmptyWsList = useCallback(
+ (): React.ReactElement => (
+
+
+
+
+ No WebSocket connections
+
+ {webSocketLogger
+ ? 'Open a WebSocket connection in your app to see it here.'
+ : 'WebSocket logger not configured. Pass webSocketLogger prop to enable.'}
+
+ {webSocketLogger && (
+
+
+ Interceptor Active
+
+ )}
+
+ ),
+ [themedStyles, themeColors, webSocketLogger]
+ );
+
+ const totalLogs = enableWebSocket ? logs.length + wsLogs.length : logs.length;
+ const totalErrors = enableWebSocket
+ ? logStats.errors + wsStats.errors
+ : logStats.errors;
+
+ const buttonProps = {
+ draggable,
+ enableDeviceShake,
+ hideIcon: handleCloseIcon,
+ openModal: handleModalOpen,
+ logsLength: totalLogs,
+ errorCount: totalErrors,
+ theme,
+ };
+
+ if (!enabled) {
+ return null;
+ }
+
+ return (
+ <>
+ {showButton &&
+ (draggable ? (
+
+ ) : (
+
+ ))}
+
+
+
+ {/* Header */}
+
+
+
+ DEBUGGER
+
+
+
+
+
+ {activeTab === 'http' && (
+ {
+ setVisible(false);
+ setExportModalVisible(true);
+ }}
+ activeOpacity={0.7}
+ >
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {/* Tab Bar - Only show if WebSocket is enabled */}
+ {enableWebSocket && (
+
+ setActiveTab('http')}
+ activeOpacity={0.7}
+ >
+
+ HTTP
+
+
+
+ {logs.length}
+
+
+
+
+ setActiveTab('websocket')}
+ activeOpacity={0.7}
+ >
+
+ WS
+
+ 0 && themedStyles.tabBadgeLive,
+ ]}
+ >
+ 0 && themedStyles.tabBadgeTextLive,
+ ]}
+ >
+ {wsLogs.length}
+
+
+ {wsStats.active > 0 && }
+
+
+ )}
+
+ {/* Search & Filters */}
+
+
+
+
+
+ {activeTab === 'http' && (
+
+
+ {STATUS_FILTERS.map((filter) => (
+ handleStatusFilterChange(filter.key)}
+ activeOpacity={0.7}
+ >
+
+ {filter.label}
+
+
+ ))}
+
+ {hasActiveFilters && (
+
+ Clear
+
+ )}
+
+ )}
+
+
+ {/* Stats Bar */}
+ {activeTab === 'http' && logs.length > 0 && (
+
+
+ {filteredLogs.length} of {logs.length} requests
+
+ {logStats.errors > 0 && (
+
+ {logStats.errors} errors
+
+ )}
+
+ )}
+
+ {enableWebSocket &&
+ activeTab === 'websocket' &&
+ wsLogs.length > 0 && (
+
+
+ {filteredWsLogs.length} of {wsLogs.length} connections
+
+ {wsStats.active > 0 && (
+
+ {wsStats.active} active
+
+ )}
+ {wsStats.errors > 0 && (
+
+ {wsStats.errors} errors
+
+ )}
+
+ )}
+
+ {/* Log Lists */}
+ {activeTab === 'http' || !enableWebSocket ? (
+ item.id.toString()}
+ renderItem={renderLogItem}
+ contentContainerStyle={
+ logs.length === 0
+ ? staticStyles.emptyListContainer
+ : staticStyles.listContainer
+ }
+ ListEmptyComponent={renderEmptyHttpList}
+ showsVerticalScrollIndicator={true}
+ removeClippedSubviews={true}
+ maxToRenderPerBatch={10}
+ windowSize={10}
+ initialNumToRender={10}
+ />
+ ) : (
+ item.id.toString()}
+ renderItem={renderWsLogItem}
+ contentContainerStyle={
+ wsLogs.length === 0
+ ? staticStyles.emptyListContainer
+ : staticStyles.listContainer
+ }
+ ListEmptyComponent={renderEmptyWsList}
+ showsVerticalScrollIndicator={true}
+ removeClippedSubviews={true}
+ maxToRenderPerBatch={10}
+ windowSize={10}
+ initialNumToRender={10}
+ />
+ )}
+
+
+ setExportModalVisible(false)}
+ logs={filteredLogs}
+ theme={theme}
+ />
+ >
+ );
+};
+
+const createThemedStyles = (themeColors: ThemeColors) => ({
+ modalContainer: {
+ flex: 1,
+ backgroundColor: themeColors.background,
+ paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 44,
+ } as ViewStyle,
+ header: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingVertical: 12,
+ paddingHorizontal: 12,
+ backgroundColor: themeColors.surfaceContainer,
+ borderBottomWidth: 1,
+ borderBottomColor: themeColors.border,
+ } as ViewStyle,
+ title: {
+ fontSize: 14,
+ fontWeight: '800',
+ color: themeColors.text,
+ letterSpacing: 1,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ } as TextStyle,
+ tabBar: {
+ flexDirection: 'row',
+ backgroundColor: themeColors.surfaceContainer,
+ borderBottomWidth: 1,
+ borderBottomColor: themeColors.border,
+ paddingHorizontal: 12,
+ } as ViewStyle,
+ tab: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 12,
+ paddingHorizontal: 16,
+ marginRight: 8,
+ borderBottomWidth: 2,
+ borderBottomColor: 'transparent',
+ } as ViewStyle,
+ tabActive: {
+ borderBottomColor: themeColors.primaryContainer,
+ } as ViewStyle,
+ tabText: {
+ fontSize: 13,
+ fontWeight: '600',
+ color: themeColors.textMuted,
+ letterSpacing: 0.5,
+ } as TextStyle,
+ tabTextActive: {
+ color: themeColors.text,
+ } as TextStyle,
+ tabBadge: {
+ marginLeft: 8,
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ backgroundColor: themeColors.surfaceContainerHigh,
+ borderRadius: 10,
+ minWidth: 24,
+ alignItems: 'center',
+ } as ViewStyle,
+ tabBadgeActive: {
+ backgroundColor: themeColors.primaryContainer,
+ } as ViewStyle,
+ tabBadgeLive: {
+ backgroundColor: colors.websocket.open,
+ } as ViewStyle,
+ tabBadgeText: {
+ fontSize: 10,
+ fontWeight: '700',
+ color: themeColors.textMuted,
+ } as TextStyle,
+ tabBadgeTextActive: {
+ color: '#FFFFFF',
+ } as TextStyle,
+ tabBadgeTextLive: {
+ color: '#FFFFFF',
+ } as TextStyle,
+ searchContainer: {
+ paddingHorizontal: 12,
+ paddingVertical: 12,
+ backgroundColor: themeColors.surfaceContainer,
+ borderBottomWidth: 1,
+ borderBottomColor: themeColors.border,
+ gap: 10,
+ } as ViewStyle,
+ searchInputContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: themeColors.surfaceContainerLow,
+ borderWidth: 1,
+ borderColor: themeColors.border,
+ paddingHorizontal: 12,
+ gap: 8,
+ } as ViewStyle,
+ searchInput: {
+ flex: 1,
+ paddingVertical: 10,
+ fontSize: 12,
+ color: themeColors.text,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ } as TextStyle,
+ filterChip: {
+ paddingHorizontal: 12,
+ paddingVertical: 6,
+ borderWidth: 1,
+ borderColor: themeColors.border,
+ backgroundColor: 'transparent',
+ } as ViewStyle,
+ filterChipActive: {
+ backgroundColor: themeColors.primaryContainer,
+ borderColor: themeColors.primaryContainer,
+ } as ViewStyle,
+ filterChipText: {
+ fontSize: 11,
+ fontWeight: '700',
+ color: themeColors.textMuted,
+ letterSpacing: 0.5,
+ } as TextStyle,
+ clearFiltersText: {
+ fontSize: 11,
+ color: themeColors.primary,
+ fontWeight: '600',
+ } as TextStyle,
+ statsBar: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ backgroundColor: themeColors.surfaceContainerLow,
+ borderBottomWidth: 1,
+ borderBottomColor: themeColors.borderSubtle,
+ } as ViewStyle,
+ statsText: {
+ fontSize: 11,
+ color: themeColors.textMuted,
+ fontWeight: '500',
+ } as TextStyle,
+ emptyContainer: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingHorizontal: 40,
+ marginTop: '25%',
+ } as ViewStyle,
+ emptyIconContainer: {
+ width: 80,
+ height: 80,
+ borderWidth: 2,
+ borderStyle: 'dashed',
+ borderColor: themeColors.border,
+ borderRadius: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginBottom: 24,
+ backgroundColor: themeColors.surfaceContainerLow,
+ } as ViewStyle,
+ emptyTitle: {
+ fontSize: 18,
+ fontWeight: '600',
+ color: themeColors.text,
+ marginBottom: 8,
+ } as TextStyle,
+ emptyText: {
+ textAlign: 'center',
+ color: themeColors.textMuted,
+ fontSize: 14,
+ lineHeight: 20,
+ marginBottom: 24,
+ } as TextStyle,
+ statusPill: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ backgroundColor: themeColors.surfaceContainer,
+ borderWidth: 1,
+ borderColor: themeColors.border,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ gap: 8,
+ } as ViewStyle,
+ statusPillText: {
+ fontSize: 12,
+ color: themeColors.textSecondary,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ } as TextStyle,
+});
+
+const staticStyles = {
+ headerLeft: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ } as ViewStyle,
+ headerRight: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ } as ViewStyle,
+ iconButton: {
+ padding: 8,
+ } as ViewStyle,
+ filterRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ } as ViewStyle,
+ filterChips: {
+ flexDirection: 'row',
+ gap: 6,
+ } as ViewStyle,
+ filterChipTextActive: {
+ color: '#FFFFFF',
+ } as TextStyle,
+ errorStats: {
+ fontSize: 11,
+ color: colors.error,
+ fontWeight: '600',
+ } as TextStyle,
+ activeStats: {
+ fontSize: 11,
+ color: colors.websocket.open,
+ fontWeight: '600',
+ marginRight: 8,
+ } as TextStyle,
+ listContainer: {
+ paddingBottom: 34,
+ } as ViewStyle,
+ emptyListContainer: {
+ flexGrow: 1,
+ } as ViewStyle,
+ statusDot: {
+ width: 8,
+ height: 8,
+ borderRadius: 4,
+ backgroundColor: colors.success,
+ } as ViewStyle,
+ liveDot: {
+ width: 6,
+ height: 6,
+ borderRadius: 3,
+ backgroundColor: colors.websocket.open,
+ marginLeft: 4,
+ } as ViewStyle,
+};
+
+export type { NetworkLoggerOverlayProps };
diff --git a/src/features/http/index.ts b/src/features/http/index.ts
new file mode 100644
index 0000000..2d31818
--- /dev/null
+++ b/src/features/http/index.ts
@@ -0,0 +1,3 @@
+export * from './NetworkLoggerOverlay';
+export { default as NetworkLogItem } from './NetworkLogItem';
+export type { NetworkLogItemProps } from './NetworkLogItem';
diff --git a/src/features/websocket/WebSocketLogItem.tsx b/src/features/websocket/WebSocketLogItem.tsx
new file mode 100644
index 0000000..456488f
--- /dev/null
+++ b/src/features/websocket/WebSocketLogItem.tsx
@@ -0,0 +1,673 @@
+import React, { useState, useMemo, useCallback } from 'react';
+import {
+ FlatList,
+ Text,
+ TouchableOpacity,
+ View,
+ Platform,
+ type ViewStyle,
+ type TextStyle,
+ type ListRenderItem,
+} from 'react-native';
+import type { WebSocketLog, WebSocketMessage } from '../../types';
+import type { ThemeMode, ThemeColors } from '../../constants/colors';
+import {
+ colors,
+ getThemeColors,
+ getWebSocketStateColor,
+ getWebSocketMessageColor,
+} from '../../constants/colors';
+import { CopyItem, JsonViewer } from '../../components/common';
+import { SvgIcon } from '../../icons';
+
+interface WebSocketLogItemProps {
+ log: WebSocketLog;
+ useCopyToClipboard?: boolean;
+ theme?: ThemeMode;
+ onDelete?: (logId: number) => void;
+ onClose?: (logId: number) => void;
+ isAlternate?: boolean;
+}
+
+type MessageFilter = 'all' | 'sent' | 'received';
+
+const WebSocketLogItem: React.FC = ({
+ log,
+ useCopyToClipboard,
+ theme = 'dark',
+ onDelete,
+ onClose,
+ isAlternate = false,
+}) => {
+ const [expanded, setExpanded] = useState(false);
+ const [messageFilter, setMessageFilter] = useState('all');
+ const [expandedMessageId, setExpandedMessageId] = useState(
+ null
+ );
+
+ const themeColors = useMemo(() => getThemeColors(theme), [theme]);
+ const themedStyles = useMemo(
+ () => createThemedStyles(themeColors),
+ [themeColors]
+ );
+
+ const stateColor = getWebSocketStateColor(log.state);
+ const isActive = log.state === 'open' || log.state === 'connecting';
+
+ const filteredMessages = useMemo(() => {
+ if (messageFilter === 'all') return log.messages;
+ return log.messages.filter((m) => m.direction === messageFilter);
+ }, [log.messages, messageFilter]);
+
+ const formatBytes = (bytes: number): string => {
+ if (bytes < 1024) return `${bytes}B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
+ };
+
+ const formatDuration = (startTime: string, endTime?: string): string => {
+ const start = new Date(startTime).getTime();
+ const end = endTime ? new Date(endTime).getTime() : Date.now();
+ const duration = end - start;
+
+ if (duration < 1000) return `${duration}ms`;
+ if (duration < 60000) return `${(duration / 1000).toFixed(1)}s`;
+ const minutes = Math.floor(duration / 60000);
+ const seconds = Math.floor((duration % 60000) / 1000);
+ return `${minutes}m ${seconds}s`;
+ };
+
+ const formatTime = (timestamp: string): string => {
+ const date = new Date(timestamp);
+ return date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+ };
+
+ const getUrlPath = (): string => {
+ try {
+ const url = new URL(log.url);
+ return url.pathname;
+ } catch {
+ return log.url;
+ }
+ };
+
+ const getUrlHost = (): string => {
+ try {
+ const url = new URL(log.url);
+ return url.hostname;
+ } catch {
+ return '';
+ }
+ };
+
+ const handleHeaderPress = useCallback(() => {
+ setExpanded(!expanded);
+ }, [expanded]);
+
+ const handleDelete = useCallback(() => {
+ onDelete?.(log.id);
+ }, [log.id, onDelete]);
+
+ const handleCloseConnection = useCallback(() => {
+ onClose?.(log.id);
+ }, [log.id, onClose]);
+
+ const rowBackgroundColor = isAlternate
+ ? themeColors.surfaceContainerLow
+ : themeColors.surface;
+
+ const borderColor = expanded ? stateColor : 'transparent';
+
+ const renderMessageItem: ListRenderItem = useCallback(
+ ({ item: message }) => {
+ const messageColor = getWebSocketMessageColor(message.direction);
+ const isExpanded = expandedMessageId === message.id;
+ const isJson =
+ message.data.trim().startsWith('{') ||
+ message.data.trim().startsWith('[');
+
+ return (
+ setExpandedMessageId(isExpanded ? null : message.id)}
+ activeOpacity={0.7}
+ >
+
+
+
+
+ {formatTime(message.timestamp)}
+
+
+
+ {formatBytes(message.size)}
+
+
+
+ {isExpanded && isJson ? (
+
+
+
+
+
+
+ ) : (
+
+ {message.data}
+
+ )}
+
+ );
+ },
+ [expandedMessageId, theme, themeColors, themedStyles, useCopyToClipboard]
+ );
+
+ return (
+
+
+ {/* State indicator */}
+
+
+
+
+ {/* WS Badge */}
+
+
+
+ WS
+
+
+
+
+ {/* URL */}
+
+
+ {getUrlPath()}
+
+
+ {getUrlHost()}
+
+
+
+ {/* Messages count & data */}
+
+
+
+ {log.messageCount.sent}
+
+
+ {log.messageCount.received}
+
+
+
+ {formatBytes(log.bytesSent + log.bytesReceived)}
+
+
+
+
+ {expanded && (
+
+ {/* Connection Info */}
+
+
+ URL
+
+ {log.url}
+
+
+
+
+ State
+
+ {log.state.toUpperCase()}
+
+
+
+ Duration
+
+ {formatDuration(log.connectTime, log.closeTime)}
+
+
+ {log.handshakeDuration !== undefined && (
+
+ Handshake
+
+ {log.handshakeDuration}ms
+
+
+ )}
+ {log.closeCode !== undefined && (
+
+ Close Code
+
+ {log.closeCode} {log.closeReason && `(${log.closeReason})`}
+
+
+ )}
+
+
+
+ {/* Action Buttons */}
+
+ {isActive && onClose && (
+
+
+
+ CLOSE
+
+
+ )}
+
+
+
+ {onDelete && (
+
+
+
+ )}
+
+
+ {log.error && (
+
+
+ {log.error}
+
+ )}
+
+ {/* Messages Section */}
+
+
+
+ MESSAGES ({log.messages.length})
+
+
+ {(['all', 'sent', 'received'] as MessageFilter[]).map(
+ (filter) => (
+ setMessageFilter(filter)}
+ activeOpacity={0.7}
+ >
+
+ {filter === 'all'
+ ? 'All'
+ : filter === 'sent'
+ ? `↑ ${log.messageCount.sent}`
+ : `↓ ${log.messageCount.received}`}
+
+
+ )
+ )}
+
+
+
+ {filteredMessages.length > 0 ? (
+ item.id.toString()}
+ renderItem={renderMessageItem}
+ style={staticStyles.messagesList}
+ showsVerticalScrollIndicator={true}
+ nestedScrollEnabled={true}
+ />
+ ) : (
+
+
+ No messages{' '}
+ {messageFilter !== 'all' ? `(${messageFilter})` : ''}
+
+
+ )}
+
+
+ )}
+
+ );
+};
+
+const createThemedStyles = (themeColors: ThemeColors) => ({
+ logItem: {
+ backgroundColor: themeColors.surface,
+ borderLeftWidth: 2,
+ borderLeftColor: 'transparent',
+ borderBottomWidth: 1,
+ borderBottomColor: themeColors.borderSubtle,
+ } as ViewStyle,
+ logHeader: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ paddingVertical: 10,
+ paddingHorizontal: 12,
+ } as ViewStyle,
+ urlPath: {
+ fontSize: 12,
+ color: themeColors.text,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ } as TextStyle,
+ urlHost: {
+ fontSize: 10,
+ color: themeColors.textMuted,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ marginTop: 2,
+ } as TextStyle,
+ statText: {
+ fontSize: 11,
+ color: themeColors.text,
+ fontWeight: '600',
+ marginLeft: 2,
+ marginRight: 6,
+ } as TextStyle,
+ dataSize: {
+ fontSize: 10,
+ color: themeColors.textMuted,
+ marginTop: 2,
+ } as TextStyle,
+ logDetails: {
+ borderTopWidth: 1,
+ borderTopColor: themeColors.borderSubtle,
+ backgroundColor: themeColors.surfaceContainerLow,
+ } as ViewStyle,
+ infoSection: {
+ padding: 12,
+ borderBottomWidth: 1,
+ borderBottomColor: themeColors.borderSubtle,
+ } as ViewStyle,
+ infoLabel: {
+ fontSize: 10,
+ color: themeColors.textMuted,
+ fontWeight: '600',
+ letterSpacing: 0.5,
+ marginBottom: 2,
+ } as TextStyle,
+ infoValue: {
+ fontSize: 12,
+ color: themeColors.text,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ } as TextStyle,
+ actionButton: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 6,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ borderWidth: 1,
+ borderColor: themeColors.border,
+ backgroundColor: 'transparent',
+ } as ViewStyle,
+ actionButtonText: {
+ fontSize: 11,
+ fontWeight: '600',
+ color: themeColors.text,
+ letterSpacing: 0.3,
+ } as TextStyle,
+ messagesSection: {
+ padding: 12,
+ } as ViewStyle,
+ messagesHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: 12,
+ } as ViewStyle,
+ messagesTitle: {
+ fontSize: 11,
+ fontWeight: '700',
+ color: themeColors.textMuted,
+ letterSpacing: 0.5,
+ } as TextStyle,
+ filterButton: {
+ paddingHorizontal: 8,
+ paddingVertical: 4,
+ borderWidth: 1,
+ borderColor: themeColors.border,
+ marginLeft: 4,
+ } as ViewStyle,
+ filterButtonActive: {
+ backgroundColor: themeColors.primaryContainer,
+ borderColor: themeColors.primaryContainer,
+ } as ViewStyle,
+ filterButtonText: {
+ fontSize: 10,
+ fontWeight: '600',
+ color: themeColors.textMuted,
+ } as TextStyle,
+ messageItem: {
+ padding: 10,
+ marginBottom: 6,
+ borderWidth: 1,
+ borderColor: themeColors.border,
+ backgroundColor: themeColors.surfaceContainer,
+ } as ViewStyle,
+ messageSent: {
+ borderLeftWidth: 2,
+ borderLeftColor: colors.websocket.sent,
+ } as ViewStyle,
+ messageReceived: {
+ borderLeftWidth: 2,
+ borderLeftColor: colors.websocket.received,
+ } as ViewStyle,
+ messageContent: {
+ position: 'relative',
+ marginTop: 8,
+ } as ViewStyle,
+ messageData: {
+ fontSize: 11,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ lineHeight: 16,
+ marginTop: 6,
+ } as TextStyle,
+ emptyMessages: {
+ padding: 20,
+ alignItems: 'center',
+ } as ViewStyle,
+ emptyMessagesText: {
+ fontSize: 12,
+ color: themeColors.textMuted,
+ } as TextStyle,
+});
+
+const staticStyles = {
+ stateColumn: {
+ width: 24,
+ } as ViewStyle,
+ stateIndicator: {
+ width: 10,
+ height: 10,
+ borderRadius: 5,
+ } as ViewStyle,
+ stateActive: {
+ shadowColor: '#10B981',
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.8,
+ shadowRadius: 4,
+ } as ViewStyle,
+ typeColumn: {
+ width: 40,
+ } as ViewStyle,
+ typeBadge: {
+ paddingHorizontal: 6,
+ paddingVertical: 2,
+ alignSelf: 'flex-start',
+ } as ViewStyle,
+ typeText: {
+ fontSize: 10,
+ fontWeight: '700',
+ } as TextStyle,
+ urlColumn: {
+ flex: 1,
+ paddingHorizontal: 8,
+ } as ViewStyle,
+ statsColumn: {
+ alignItems: 'flex-end',
+ } as ViewStyle,
+ messageStats: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ } as ViewStyle,
+ infoRow: {
+ marginBottom: 12,
+ } as ViewStyle,
+ infoGrid: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 16,
+ } as ViewStyle,
+ infoCell: {
+ minWidth: 80,
+ } as ViewStyle,
+ actionBar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ padding: 12,
+ gap: 8,
+ borderBottomWidth: 1,
+ borderBottomColor: 'rgba(255,255,255,0.1)',
+ } as ViewStyle,
+ deleteButton: {
+ marginLeft: 'auto',
+ borderColor: colors.error,
+ } as ViewStyle,
+ errorBanner: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ paddingHorizontal: 12,
+ paddingVertical: 8,
+ backgroundColor: `${colors.error}15`,
+ } as ViewStyle,
+ errorText: {
+ fontSize: 11,
+ color: colors.error,
+ fontWeight: '500',
+ } as TextStyle,
+ filterButtons: {
+ flexDirection: 'row',
+ } as ViewStyle,
+ filterButtonTextActive: {
+ color: '#FFFFFF',
+ } as TextStyle,
+ messageHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ } as ViewStyle,
+ messageDirection: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ } as ViewStyle,
+ messageTime: {
+ fontSize: 10,
+ fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
+ } as TextStyle,
+ messageSize: {
+ fontSize: 10,
+ } as TextStyle,
+ messagesList: {
+ maxHeight: 300,
+ } as ViewStyle,
+ copyButtonFloat: {
+ position: 'absolute',
+ right: 0,
+ top: 0,
+ zIndex: 10,
+ } as ViewStyle,
+};
+
+export default WebSocketLogItem;
+export type { WebSocketLogItemProps };
diff --git a/src/features/websocket/index.ts b/src/features/websocket/index.ts
new file mode 100644
index 0000000..b31b696
--- /dev/null
+++ b/src/features/websocket/index.ts
@@ -0,0 +1,2 @@
+export { default as WebSocketLogItem } from './WebSocketLogItem';
+export type { WebSocketLogItemProps } from './WebSocketLogItem';
diff --git a/src/NetworkInterceptor.ts b/src/helpers/NetworkInterceptor.ts
similarity index 96%
rename from src/NetworkInterceptor.ts
rename to src/helpers/NetworkInterceptor.ts
index e56f1b6..9309bef 100644
--- a/src/NetworkInterceptor.ts
+++ b/src/helpers/NetworkInterceptor.ts
@@ -3,9 +3,9 @@ import type {
NetworkLog,
NetworkLoggerConfig,
NetworkRequestHeaders,
-} from './types';
-import { DEFAULT_CONFIG } from './constants/config';
-import { shouldIgnoreUrl, shouldIgnoreDomain } from './utils/filters';
+} from '../types';
+import { DEFAULT_CONFIG } from '../constants/config';
+import { shouldIgnoreUrl, shouldIgnoreDomain } from '../utils/filters';
interface CustomXMLHttpRequest extends XMLHttpRequest {
open: (method: string, url: string) => void;
@@ -18,11 +18,13 @@ class NetworkLogger {
private listeners: LogListener[] = [];
private isEnabled: boolean = true;
private config: NetworkLoggerConfig = { ...DEFAULT_CONFIG };
+ private isInterceptorSetup: boolean = false;
constructor(config?: Partial) {
this.logs = [];
this.listeners = [];
this.isEnabled = true;
+ this.isInterceptorSetup = false;
if (config) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
@@ -56,6 +58,7 @@ class NetworkLogger {
public setupInterceptor(): void {
if (!this.isEnabled) return;
+ this.isInterceptorSetup = true;
const originalFetch = global.fetch;
// eslint-disable-next-line consistent-this
@@ -338,8 +341,12 @@ class NetworkLogger {
public isLoggerEnabled(): boolean {
return this.isEnabled;
}
+
+ public isSetup(): boolean {
+ return this.isInterceptorSetup;
+ }
}
-export const networkLogger: NetworkLogger = new NetworkLogger();
+export const networkLogger = new NetworkLogger();
export type { NetworkLog, NetworkRequestHeaders, LogListener };
diff --git a/src/helpers/WebSocketInterceptor.ts b/src/helpers/WebSocketInterceptor.ts
new file mode 100644
index 0000000..5bbcdbb
--- /dev/null
+++ b/src/helpers/WebSocketInterceptor.ts
@@ -0,0 +1,402 @@
+import type {
+ WebSocketLog,
+ WebSocketLogListener,
+ WebSocketLoggerConfig,
+ WebSocketMessage,
+} from '../types';
+import { DEFAULT_WS_CONFIG } from '../constants/config';
+
+class WebSocketLogger {
+ private logs: WebSocketLog[] = [];
+ private listeners: WebSocketLogListener[] = [];
+ private isEnabled: boolean = true;
+ private config: WebSocketLoggerConfig = { ...DEFAULT_WS_CONFIG };
+ private connectionMap: Map = new Map();
+ private originalWebSocket: typeof WebSocket | null = null;
+ private isInterceptorSetup: boolean = false;
+
+ constructor(config?: Partial) {
+ this.logs = [];
+ this.listeners = [];
+ this.isEnabled = true;
+ this.isInterceptorSetup = false;
+ if (config) {
+ this.config = { ...DEFAULT_WS_CONFIG, ...config };
+ }
+ }
+
+ public configure(config: Partial): void {
+ this.config = { ...this.config, ...config };
+ }
+
+ public getConfig(): WebSocketLoggerConfig {
+ return { ...this.config };
+ }
+
+ private shouldIgnoreUrl(url: string): boolean {
+ return this.config.ignoredUrls.some((pattern) => {
+ if (pattern.includes('*')) {
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
+ return regex.test(url);
+ }
+ return url.includes(pattern);
+ });
+ }
+
+ private getMessageSize(data: unknown): number {
+ if (typeof data === 'string') {
+ return data.length * 2; // Rough estimate for UTF-16
+ }
+ if (data instanceof ArrayBuffer) {
+ return data.byteLength;
+ }
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
+ return data.size;
+ }
+ return 0;
+ }
+
+ private stringifyData(data: unknown): string {
+ if (typeof data === 'string') {
+ return data;
+ }
+ if (data instanceof ArrayBuffer) {
+ return `[ArrayBuffer: ${data.byteLength} bytes]`;
+ }
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
+ return `[Blob: ${data.size} bytes]`;
+ }
+ return String(data);
+ }
+
+ public setupInterceptor(): void {
+ if (!this.isEnabled) return;
+ if (typeof global.WebSocket === 'undefined') return;
+ this.isInterceptorSetup = true;
+
+ this.originalWebSocket = global.WebSocket;
+ // eslint-disable-next-line consistent-this
+ const self = this;
+ const OriginalWebSocket = this.originalWebSocket;
+
+ // Create wrapper class
+ function InterceptedWebSocket(
+ url: string,
+ protocols?: string | string[]
+ ): WebSocket {
+ const urlString = String(url);
+
+ // Create the actual WebSocket
+ const ws: WebSocket = protocols
+ ? new OriginalWebSocket(urlString, protocols)
+ : new OriginalWebSocket(urlString);
+
+ // Check if should ignore
+ if (self.shouldIgnoreUrl(urlString)) {
+ return ws;
+ }
+
+ const connectionId = Date.now() + Math.random();
+
+ const log: WebSocketLog = {
+ id: connectionId,
+ url: urlString,
+ state: 'connecting',
+ protocols: Array.isArray(protocols)
+ ? protocols
+ : protocols
+ ? [protocols]
+ : undefined,
+ connectTime: new Date().toISOString(),
+ messages: [],
+ messageCount: { sent: 0, received: 0 },
+ bytesReceived: 0,
+ bytesSent: 0,
+ };
+
+ self.connectionMap.set(ws, log);
+ self.addLog(log);
+
+ // Store original event handlers
+ let userOnOpen: WebSocket['onopen'] = null;
+ let userOnMessage: WebSocket['onmessage'] = null;
+ let userOnClose: WebSocket['onclose'] = null;
+ let userOnError: WebSocket['onerror'] = null;
+
+ // Override onopen property
+ Object.defineProperty(ws, 'onopen', {
+ configurable: true,
+ get: () => userOnOpen,
+ set: (handler: WebSocket['onopen']) => {
+ userOnOpen = handler;
+ },
+ });
+
+ // Override onmessage property
+ Object.defineProperty(ws, 'onmessage', {
+ configurable: true,
+ get: () => userOnMessage,
+ set: (handler: WebSocket['onmessage']) => {
+ userOnMessage = handler;
+ },
+ });
+
+ // Override onclose property
+ Object.defineProperty(ws, 'onclose', {
+ configurable: true,
+ get: () => userOnClose,
+ set: (handler: WebSocket['onclose']) => {
+ userOnClose = handler;
+ },
+ });
+
+ // Override onerror property
+ Object.defineProperty(ws, 'onerror', {
+ configurable: true,
+ get: () => userOnError,
+ set: (handler: WebSocket['onerror']) => {
+ userOnError = handler;
+ },
+ });
+
+ // Add internal event listeners
+ const originalAddEventListener = ws.addEventListener.bind(ws);
+
+ // Track events internally
+ originalAddEventListener('open', function (this: WebSocket) {
+ log.state = 'open';
+ log.openTime = new Date().toISOString();
+ log.handshakeDuration =
+ Date.now() - new Date(log.connectTime).getTime();
+ self.updateLog(log);
+ if (userOnOpen) {
+ userOnOpen.call(this);
+ }
+ });
+
+ originalAddEventListener('message', function (this: WebSocket, event) {
+ if (self.config.captureMessages) {
+ const data = (event as MessageEvent).data;
+ const size = self.getMessageSize(data);
+ const message: WebSocketMessage = {
+ id: Date.now() + Math.random(),
+ direction: 'received',
+ data: self.stringifyData(data),
+ dataType: typeof data === 'string' ? 'text' : 'binary',
+ timestamp: new Date().toISOString(),
+ size,
+ };
+
+ // Enforce max messages limit
+ if (log.messages.length >= self.config.maxMessagesPerConnection) {
+ log.messages.shift();
+ }
+
+ log.messages.push(message);
+ log.messageCount.received++;
+ log.bytesReceived += size;
+ self.updateLog(log);
+ }
+ if (userOnMessage) {
+ userOnMessage.call(this, event as MessageEvent);
+ }
+ });
+
+ originalAddEventListener('close', function (this: WebSocket, event) {
+ const closeEvent = event as CloseEvent;
+ log.state = 'closed';
+ log.closeTime = new Date().toISOString();
+ log.closeCode = closeEvent.code;
+ log.closeReason =
+ closeEvent.reason || self.getCloseCodeDescription(closeEvent.code);
+ self.updateLog(log);
+ self.connectionMap.delete(ws);
+ if (userOnClose) {
+ userOnClose.call(this, closeEvent);
+ }
+ });
+
+ originalAddEventListener('error', function (this: WebSocket, event) {
+ log.error = 'WebSocket connection error';
+ self.updateLog(log);
+ if (userOnError) {
+ userOnError.call(this, event);
+ }
+ });
+
+ // Override send method
+ const originalSend = ws.send.bind(ws);
+ (ws as { send: typeof ws.send }).send = function (data) {
+ if (self.config.captureMessages) {
+ const size = self.getMessageSize(data);
+ const message: WebSocketMessage = {
+ id: Date.now() + Math.random(),
+ direction: 'sent',
+ data: self.stringifyData(data),
+ dataType: typeof data === 'string' ? 'text' : 'binary',
+ timestamp: new Date().toISOString(),
+ size,
+ };
+
+ // Enforce max messages limit
+ if (log.messages.length >= self.config.maxMessagesPerConnection) {
+ log.messages.shift();
+ }
+
+ log.messages.push(message);
+ log.messageCount.sent++;
+ log.bytesSent += size;
+ self.updateLog(log);
+ }
+ return originalSend(data);
+ };
+
+ // Override close method
+ const originalClose = ws.close.bind(ws);
+ (ws as { close: typeof ws.close }).close = function (code, reason) {
+ log.state = 'closing';
+ self.updateLog(log);
+ return originalClose(code, reason);
+ };
+
+ return ws;
+ }
+
+ // Copy static properties and prototype
+ InterceptedWebSocket.prototype = OriginalWebSocket.prototype;
+ Object.defineProperty(InterceptedWebSocket, 'CONNECTING', {
+ value: OriginalWebSocket.CONNECTING,
+ writable: false,
+ });
+ Object.defineProperty(InterceptedWebSocket, 'OPEN', {
+ value: OriginalWebSocket.OPEN,
+ writable: false,
+ });
+ Object.defineProperty(InterceptedWebSocket, 'CLOSING', {
+ value: OriginalWebSocket.CLOSING,
+ writable: false,
+ });
+ Object.defineProperty(InterceptedWebSocket, 'CLOSED', {
+ value: OriginalWebSocket.CLOSED,
+ writable: false,
+ });
+
+ // Replace global WebSocket
+ (global as { WebSocket: typeof WebSocket }).WebSocket =
+ InterceptedWebSocket as unknown as typeof WebSocket;
+ }
+
+ private getCloseCodeDescription(code: number): string {
+ const descriptions: Record = {
+ 1000: 'Normal closure',
+ 1001: 'Going away',
+ 1002: 'Protocol error',
+ 1003: 'Unsupported data',
+ 1005: 'No status received',
+ 1006: 'Abnormal closure',
+ 1007: 'Invalid frame payload data',
+ 1008: 'Policy violation',
+ 1009: 'Message too big',
+ 1010: 'Mandatory extension',
+ 1011: 'Internal server error',
+ 1015: 'TLS handshake failure',
+ };
+ return descriptions[code] || `Unknown (${code})`;
+ }
+
+ public restoreInterceptor(): void {
+ if (this.originalWebSocket) {
+ (global as { WebSocket: typeof WebSocket }).WebSocket =
+ this.originalWebSocket;
+ this.originalWebSocket = null;
+ }
+ }
+
+ private addLog(log: WebSocketLog): void {
+ this.logs.unshift(log);
+
+ if (this.logs.length > this.config.maxConnections) {
+ this.logs = this.logs.slice(0, this.config.maxConnections);
+ }
+
+ this.notifyListeners();
+ }
+
+ private updateLog(log: WebSocketLog): void {
+ const index = this.logs.findIndex((l) => l.id === log.id);
+ if (index !== -1) {
+ this.logs[index] = { ...log };
+ this.notifyListeners();
+ }
+ }
+
+ private notifyListeners(): void {
+ this.listeners.forEach((listener) => listener([...this.logs]));
+ }
+
+ public subscribe(listener: WebSocketLogListener): () => void {
+ this.listeners.push(listener);
+ // Immediately call with current logs
+ listener([...this.logs]);
+ return () => {
+ this.listeners = this.listeners.filter((l) => l !== listener);
+ };
+ }
+
+ public clearLogs(): void {
+ this.logs = [];
+ this.notifyListeners();
+ }
+
+ public deleteLog(logId: number): void {
+ this.logs = this.logs.filter((log) => log.id !== logId);
+ this.notifyListeners();
+ }
+
+ public closeConnection(logId: number): void {
+ for (const [ws, log] of this.connectionMap.entries()) {
+ if (log.id === logId && ws.readyState === WebSocket.OPEN) {
+ ws.close(1000, 'Closed by debugger');
+ break;
+ }
+ }
+ }
+
+ public enable(): void {
+ this.isEnabled = true;
+ }
+
+ public disable(): void {
+ this.isEnabled = false;
+ }
+
+ public getLogs(): WebSocketLog[] {
+ return [...this.logs];
+ }
+
+ public getLogCount(): number {
+ return this.logs.length;
+ }
+
+ public isLoggerEnabled(): boolean {
+ return this.isEnabled;
+ }
+
+ public getActiveConnectionsCount(): number {
+ let count = 0;
+ for (const log of this.logs) {
+ if (log.state === 'open' || log.state === 'connecting') {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public isSetup(): boolean {
+ return this.isInterceptorSetup;
+ }
+}
+
+export const webSocketLogger = new WebSocketLogger();
+
+export { WebSocketLogger };
diff --git a/src/helpers/index.ts b/src/helpers/index.ts
new file mode 100644
index 0000000..27bcfbb
--- /dev/null
+++ b/src/helpers/index.ts
@@ -0,0 +1,2 @@
+export * from './NetworkInterceptor';
+export * from './WebSocketInterceptor';
diff --git a/src/Icon.tsx b/src/icons/Icon.tsx
similarity index 100%
rename from src/Icon.tsx
rename to src/icons/Icon.tsx
diff --git a/src/SvgIcon.tsx b/src/icons/SvgIcon.tsx
similarity index 100%
rename from src/SvgIcon.tsx
rename to src/icons/SvgIcon.tsx
diff --git a/src/icons/index.ts b/src/icons/index.ts
new file mode 100644
index 0000000..7bb3987
--- /dev/null
+++ b/src/icons/index.ts
@@ -0,0 +1,2 @@
+export * from './SvgIcon';
+export { default as SvgIcon } from './SvgIcon';
diff --git a/src/index.tsx b/src/index.tsx
index 22ae901..00dd30f 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,7 +1,21 @@
-export * from './NetworkInterceptor';
-export * from './NetworkLoggerOverlay';
+// Core helpers
+export * from './helpers';
+
+// Features
+export * from './features/http';
+export * from './features/websocket';
+
+// Types
export * from './types';
+
+// Constants
export * from './constants';
+
+// Utils
export * from './utils';
+
+// Components
export * from './components';
-export { SvgIcon, isSvgAvailable, type IconName } from './SvgIcon';
+
+// Icons
+export { SvgIcon, isSvgAvailable, type IconName } from './icons';
diff --git a/src/types.ts b/src/types.ts
index 340d9ab..820d023 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -46,6 +46,76 @@ export interface NetworkLogger {
getLogs?: () => NetworkLog[];
getLogCount?: () => number;
isLoggerEnabled?: () => boolean;
+ isSetup: () => boolean;
configure?: (config: Partial) => void;
getConfig?: () => NetworkLoggerConfig;
}
+
+// ============================================
+// WebSocket Types
+// ============================================
+
+export type WebSocketState = 'connecting' | 'open' | 'closing' | 'closed';
+
+export type WebSocketMessageDirection = 'sent' | 'received';
+
+export type WebSocketDataType = 'text' | 'binary';
+
+export interface WebSocketMessage {
+ id: number;
+ direction: WebSocketMessageDirection;
+ data: string;
+ dataType: WebSocketDataType;
+ timestamp: string;
+ size: number;
+}
+
+export interface WebSocketLog {
+ id: number;
+ url: string;
+ state: WebSocketState;
+ protocols?: string[];
+
+ // Connection lifecycle
+ connectTime: string;
+ openTime?: string;
+ closeTime?: string;
+ closeCode?: number;
+ closeReason?: string;
+
+ // Messages
+ messages: WebSocketMessage[];
+ messageCount: { sent: number; received: number };
+
+ // Errors
+ error?: string;
+
+ // Metadata
+ handshakeDuration?: number;
+ bytesReceived: number;
+ bytesSent: number;
+}
+
+export interface WebSocketLoggerConfig {
+ maxConnections: number;
+ maxMessagesPerConnection: number;
+ ignoredUrls: string[];
+ captureMessages: boolean;
+}
+
+export type WebSocketLogListener = (logs: WebSocketLog[]) => void;
+
+export interface IWebSocketLogger {
+ subscribe: (listener: WebSocketLogListener) => () => void;
+ clearLogs: () => void;
+ deleteLog: (logId: number) => void;
+ enable: () => void;
+ disable: () => void;
+ getLogs?: () => WebSocketLog[];
+ getLogCount?: () => number;
+ isLoggerEnabled?: () => boolean;
+ isSetup: () => boolean;
+ configure?: (config: Partial) => void;
+ getConfig?: () => WebSocketLoggerConfig;
+ closeConnection?: (logId: number) => void;
+}