react-native-debug-toolkit
Version:
A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development
390 lines (322 loc) • 9.95 kB
JavaScript
import React from 'react'
class NetworkFeature {
static instance = null
static MAX_LOGS = 200
constructor() {
if (NetworkFeature.instance) {
return NetworkFeature.instance
}
this.logs = []
this.pendingAxiosRequests = new Map()
this.originalFetch = null
this.blacklist = [] // URL patterns to exclude from logging
NetworkFeature.instance = this
}
setup() {
// this._interceptFetch()
return this
}
getData() {
return this.logs
}
cleanup() {
if (this.originalFetch) {
global.fetch = this.originalFetch
this.originalFetch = null
}
this.logs = []
this.pendingAxiosRequests.clear()
}
// Check if a URL matches any pattern in the blacklist
isUrlBlacklisted(url) {
if (!url) return false
return this.blacklist.some(pattern => {
if (pattern instanceof RegExp) {
return pattern.test(url)
}
return url.includes(pattern)
})
}
// Add a URL pattern to blacklist
addUrlToBlacklist(pattern) {
if (!this.blacklist.some(p =>
(p instanceof RegExp && pattern instanceof RegExp) ?
p.toString() === pattern.toString() : p === pattern)) {
this.blacklist.push(pattern)
}
}
// Remove a URL pattern from blacklist
removeUrlFromBlacklist(pattern) {
this.blacklist = this.blacklist.filter(p =>
(p instanceof RegExp && pattern instanceof RegExp) ?
p.toString() !== pattern.toString() : p !== pattern)
}
// Clear all patterns from blacklist
clearBlacklist() {
this.blacklist = []
}
setupAxiosInterceptors(axiosInstance) {
axiosInstance.interceptors.request.use(
(config) => {
// Generate a unique ID if one doesn't exist
if (!config.headers) {
config.headers = {}
}
if (!config.headers['X-Request-Id']) {
config.headers['X-Request-Id'] =
Date.now().toString() + Math.random().toString(36).substring(2, 10)
}
const trackId = config.headers['X-Request-Id']
this.pendingAxiosRequests.set(trackId, {
timestamp: new Date(),
startTime: Date.now(),
})
return config
},
(error) => {
this.logAxiosError(error)
return Promise.reject(error)
},
)
axiosInstance.interceptors.response.use(
(response) => {
this.logAxiosResponse(response)
return response
},
(error) => {
this.logAxiosError(error)
return Promise.reject(error)
},
)
}
logAxiosResponse(response) {
if (!response || !response.config || !response.config.headers) {
return
}
const trackId = response.config.headers['X-Request-Id']
const pendingRequest = this.pendingAxiosRequests.get(trackId)
if (!pendingRequest) {
return
}
const url = `${response.config.baseURL || ''}${response.config.url}`
// Skip logging if URL is blacklisted
if (this.isUrlBlacklisted(url)) {
this.pendingAxiosRequests.delete(trackId)
return
}
if (this.logs.length >= NetworkFeature.MAX_LOGS) {
this.logs.shift()
}
// Calculate duration
const duration = Date.now() - pendingRequest.startTime
const logEntry = {
timestamp: pendingRequest.timestamp,
duration: Math.round(duration),
request: {
url: url,
method: response.config.method?.toUpperCase() || 'GET',
headers: response.config.headers,
body: response.config.data || response.config.params,
},
response: {
status: response.status,
statusText: response.statusText,
headers: response.headers,
},
}
if (response.data && typeof response.data === 'object') {
logEntry.response.success = response.data.success !== false
logEntry.response.data = response.data
} else {
logEntry.response.success =
response.status >= 200 && response.status < 300
if (response.data) {
logEntry.response.data = response.data
}
}
this.logs.push(logEntry)
this.pendingAxiosRequests.delete(trackId)
}
logAxiosError(error) {
if (!error.config) {
return
}
if (!error.config.headers) {
error.config.headers = {}
}
const trackId = error.config.headers['X-Request-Id']
const pendingRequest = this.pendingAxiosRequests.get(trackId)
const startTime = pendingRequest
? pendingRequest.startTime
: Date.now() - 100
const url = `${error.config.baseURL || ''}${error.config.url}`
// Skip logging if URL is blacklisted
if (this.isUrlBlacklisted(url)) {
this.pendingAxiosRequests.delete(trackId)
return
}
if (this.logs.length >= NetworkFeature.MAX_LOGS) {
this.logs.shift()
}
const duration = Date.now() - startTime
const logEntry = {
timestamp: pendingRequest ? pendingRequest.timestamp : new Date(),
duration: Math.round(duration),
request: {
url: url,
method: error.config.method?.toUpperCase() || 'GET',
headers: error.config.headers,
body: error.config.data || error.config.params,
},
error: error.message,
success: false,
}
if (error.response && error.response.data) {
logEntry.response = {
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
data: error.response.data,
success: false,
}
}
this.logs.push(logEntry)
this.pendingAxiosRequests.delete(trackId)
}
_interceptFetch() {
this.originalFetch = global.fetch
global.fetch = async (...args) => {
const request = args[0]
const options = args[1] || {}
const startTime = Date.now()
try {
const response = await this.originalFetch(...args)
this._logFetchResponse(request, options, response.clone(), startTime)
return response
} catch (error) {
this._logFetchError(request, options, error, startTime)
throw error
}
}
}
_logFetchResponse(request, options, response, startTime) {
const requestUrl = typeof request === 'string' ? request : request.url
// Skip logging if URL is blacklisted
if (this.isUrlBlacklisted(requestUrl)) {
return
}
if (this.logs.length >= NetworkFeature.MAX_LOGS) {
this.logs.shift()
}
const duration = Date.now() - startTime
const commonData = {
timestamp: new Date(),
duration: Math.round(duration),
request: {
url: requestUrl,
method: options.method || 'GET',
headers: options.headers || {},
body: options.body,
},
}
// Try to parse JSON response
response
.text()
.then((text) => {
let responseData
let success = response.status >= 200 && response.status < 300
try {
// Try to parse as JSON
if (text) {
responseData = JSON.parse(text)
// Use API success flag if available
if (responseData && typeof responseData.success !== 'undefined') {
success = !!responseData.success
}
}
} catch (e) {
// Not JSON, use text response
responseData = text
}
const logEntry = {
...commonData,
response: {
status: response.status,
statusText: response.statusText,
headers: this._headersToObject(response.headers),
data: responseData,
success: success,
},
}
this.logs.push(logEntry)
})
.catch((err) => {
// Fallback for when we can't read the response body
const logEntry = {
...commonData,
response: {
status: response.status,
statusText: response.statusText,
headers: this._headersToObject(response.headers),
success: response.status >= 200 && response.status < 300,
},
}
this.logs.push(logEntry)
})
}
_logFetchError(request, options, error, startTime) {
const requestUrl = typeof request === 'string' ? request : request.url
// Skip logging if URL is blacklisted
if (this.isUrlBlacklisted(requestUrl)) {
return
}
if (this.logs.length >= NetworkFeature.MAX_LOGS) {
this.logs.shift()
}
const duration = Date.now() - startTime
this.logs.push({
timestamp: new Date(),
duration: Math.round(duration),
request: {
url: requestUrl,
method: options.method || 'GET',
headers: options.headers || {},
body: options.body,
},
error: error.message,
success: false,
})
}
// Utility to convert Headers object to plain object
_headersToObject(headers) {
const result = {}
if (headers && typeof headers.forEach === 'function') {
headers.forEach((value, key) => {
result[key] = value
})
} else if (headers) {
// Fallback for non-standard headers object
Object.keys(headers).forEach((key) => {
result[key] = headers[key]
})
}
return result
}
}
export const createNetworkFeature = () => {
const feature = new NetworkFeature()
return {
name: 'network',
label: 'Network Logs',
setup: () => feature.setup(),
getData: () => feature.getData(),
cleanup: () => feature.cleanup(),
setupAxiosInterceptors: (axiosInstance) =>
feature.setupAxiosInterceptors(axiosInstance),
// Expose blacklist management methods
addUrlToBlacklist: (pattern) => feature.addUrlToBlacklist(pattern),
removeUrlFromBlacklist: (pattern) => feature.removeUrlFromBlacklist(pattern),
clearBlacklist: () => feature.clearBlacklist(),
getBlacklist: () => feature.blacklist,
}
}