react-native-debug-toolkit 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,330 @@
1
+ import React from 'react'
2
+
3
+ class NetworkFeature {
4
+ static instance = null
5
+ static MAX_LOGS = 100
6
+
7
+ constructor() {
8
+ if (NetworkFeature.instance) {
9
+ return NetworkFeature.instance
10
+ }
11
+
12
+ this.logs = []
13
+ this.pendingAxiosRequests = new Map()
14
+ this.originalFetch = null
15
+
16
+ NetworkFeature.instance = this
17
+ }
18
+
19
+ setup() {
20
+ if (!__DEV__) {
21
+ return
22
+ }
23
+
24
+ // this._interceptFetch()
25
+ return this
26
+ }
27
+
28
+ getData() {
29
+ return this.logs
30
+ }
31
+
32
+ cleanup() {
33
+ if (this.originalFetch) {
34
+ global.fetch = this.originalFetch
35
+ this.originalFetch = null
36
+ }
37
+
38
+ this.logs = []
39
+ this.pendingAxiosRequests.clear()
40
+ }
41
+
42
+ setupAxiosInterceptors(axiosInstance) {
43
+ if (!__DEV__) {
44
+ return
45
+ }
46
+
47
+ axiosInstance.interceptors.request.use(
48
+ (config) => {
49
+ // Generate a unique ID if one doesn't exist
50
+ if (!config.headers) {
51
+ config.headers = {}
52
+ }
53
+
54
+ if (!config.headers['X-Request-Id']) {
55
+ config.headers['X-Request-Id'] =
56
+ Date.now().toString() + Math.random().toString(36).substring(2, 10)
57
+ }
58
+
59
+ const trackId = config.headers['X-Request-Id']
60
+ this.pendingAxiosRequests.set(trackId, {
61
+ timestamp: new Date(),
62
+ startTime: Date.now(),
63
+ })
64
+
65
+ return config
66
+ },
67
+ (error) => {
68
+ this.logAxiosError(error)
69
+ return Promise.reject(error)
70
+ },
71
+ )
72
+
73
+ axiosInstance.interceptors.response.use(
74
+ (response) => {
75
+ this.logAxiosResponse(response)
76
+ return response
77
+ },
78
+ (error) => {
79
+ this.logAxiosError(error)
80
+ return Promise.reject(error)
81
+ },
82
+ )
83
+ }
84
+
85
+ logAxiosResponse(response) {
86
+ if (!response || !response.config || !response.config.headers) {
87
+ return
88
+ }
89
+
90
+ const trackId = response.config.headers['X-Request-Id']
91
+ const pendingRequest = this.pendingAxiosRequests.get(trackId)
92
+
93
+ if (!pendingRequest) {
94
+ return
95
+ }
96
+
97
+ if (this.logs.length >= NetworkFeature.MAX_LOGS) {
98
+ this.logs.shift()
99
+ }
100
+
101
+ // Calculate duration
102
+ const duration = Date.now() - pendingRequest.startTime
103
+
104
+ const logEntry = {
105
+ timestamp: pendingRequest.timestamp,
106
+ duration: Math.round(duration),
107
+ request: {
108
+ url: `${response.config.baseURL || ''}${response.config.url}`,
109
+ method: response.config.method?.toUpperCase() || 'GET',
110
+ headers: response.config.headers,
111
+ body: response.config.data || response.config.params,
112
+ },
113
+ response: {
114
+ status: response.status,
115
+ statusText: response.statusText,
116
+ headers: response.headers,
117
+ },
118
+ }
119
+
120
+ if (response.data && typeof response.data === 'object') {
121
+ logEntry.response.success = response.data.success !== false
122
+ logEntry.response.data = response.data
123
+ } else {
124
+ logEntry.response.success =
125
+ response.status >= 200 && response.status < 300
126
+
127
+ if (response.data) {
128
+ logEntry.response.data = response.data
129
+ }
130
+ }
131
+
132
+ this.logs.push(logEntry)
133
+ this.pendingAxiosRequests.delete(trackId)
134
+ }
135
+
136
+ logAxiosError(error) {
137
+ if (!error.config) {
138
+ return
139
+ }
140
+
141
+ if (!error.config.headers) {
142
+ error.config.headers = {}
143
+ }
144
+
145
+ const trackId = error.config.headers['X-Request-Id']
146
+ const pendingRequest = this.pendingAxiosRequests.get(trackId)
147
+
148
+ const startTime = pendingRequest
149
+ ? pendingRequest.startTime
150
+ : Date.now() - 100
151
+
152
+ if (this.logs.length >= NetworkFeature.MAX_LOGS) {
153
+ this.logs.shift()
154
+ }
155
+
156
+ const duration = Date.now() - startTime
157
+
158
+ const logEntry = {
159
+ timestamp: pendingRequest ? pendingRequest.timestamp : new Date(),
160
+ duration: Math.round(duration),
161
+ request: {
162
+ url: `${error.config.baseURL || ''}${error.config.url}`,
163
+ method: error.config.method?.toUpperCase() || 'GET',
164
+ headers: error.config.headers,
165
+ body: error.config.data || error.config.params,
166
+ },
167
+ error: error.message,
168
+ success: false,
169
+ }
170
+
171
+ if (error.response && error.response.data) {
172
+ logEntry.response = {
173
+ status: error.response.status,
174
+ statusText: error.response.statusText,
175
+ headers: error.response.headers,
176
+ data: error.response.data,
177
+ success: false,
178
+ }
179
+ }
180
+
181
+ this.logs.push(logEntry)
182
+ this.pendingAxiosRequests.delete(trackId)
183
+ }
184
+
185
+ _interceptFetch() {
186
+ if (!__DEV__) {
187
+ return
188
+ }
189
+
190
+ this.originalFetch = global.fetch
191
+
192
+ global.fetch = async (...args) => {
193
+ const request = args[0]
194
+ const options = args[1] || {}
195
+ const startTime = Date.now()
196
+
197
+ try {
198
+ const response = await this.originalFetch(...args)
199
+ this._logFetchResponse(request, options, response.clone(), startTime)
200
+ return response
201
+ } catch (error) {
202
+ this._logFetchError(request, options, error, startTime)
203
+ throw error
204
+ }
205
+ }
206
+ }
207
+
208
+ _logFetchResponse(request, options, response, startTime) {
209
+ if (this.logs.length >= NetworkFeature.MAX_LOGS) {
210
+ this.logs.shift()
211
+ }
212
+
213
+ const duration = Date.now() - startTime
214
+ const requestUrl = typeof request === 'string' ? request : request.url
215
+
216
+ const commonData = {
217
+ timestamp: new Date(),
218
+ duration: Math.round(duration),
219
+ request: {
220
+ url: requestUrl,
221
+ method: options.method || 'GET',
222
+ headers: options.headers || {},
223
+ body: options.body,
224
+ },
225
+ }
226
+
227
+ // Try to parse JSON response
228
+ response
229
+ .text()
230
+ .then((text) => {
231
+ let responseData
232
+ let success = response.status >= 200 && response.status < 300
233
+
234
+ try {
235
+ // Try to parse as JSON
236
+ if (text) {
237
+ responseData = JSON.parse(text)
238
+
239
+ // Use API success flag if available
240
+ if (responseData && typeof responseData.success !== 'undefined') {
241
+ success = !!responseData.success
242
+ }
243
+ }
244
+ } catch (e) {
245
+ // Not JSON, use text response
246
+ responseData = text
247
+ }
248
+
249
+ const logEntry = {
250
+ ...commonData,
251
+ response: {
252
+ status: response.status,
253
+ statusText: response.statusText,
254
+ headers: this._headersToObject(response.headers),
255
+ data: responseData,
256
+ success: success,
257
+ },
258
+ }
259
+
260
+ this.logs.push(logEntry)
261
+ })
262
+ .catch((err) => {
263
+ // Fallback for when we can't read the response body
264
+ const logEntry = {
265
+ ...commonData,
266
+ response: {
267
+ status: response.status,
268
+ statusText: response.statusText,
269
+ headers: this._headersToObject(response.headers),
270
+ success: response.status >= 200 && response.status < 300,
271
+ },
272
+ }
273
+
274
+ this.logs.push(logEntry)
275
+ })
276
+ }
277
+
278
+ _logFetchError(request, options, error, startTime) {
279
+ if (this.logs.length >= NetworkFeature.MAX_LOGS) {
280
+ this.logs.shift()
281
+ }
282
+
283
+ const duration = Date.now() - startTime
284
+
285
+ this.logs.push({
286
+ timestamp: new Date(),
287
+ duration: Math.round(duration),
288
+ request: {
289
+ url: typeof request === 'string' ? request : request.url,
290
+ method: options.method || 'GET',
291
+ headers: options.headers || {},
292
+ body: options.body,
293
+ },
294
+ error: error.message,
295
+ success: false,
296
+ })
297
+ }
298
+
299
+ // Utility to convert Headers object to plain object
300
+ _headersToObject(headers) {
301
+ const result = {}
302
+
303
+ if (headers && typeof headers.forEach === 'function') {
304
+ headers.forEach((value, key) => {
305
+ result[key] = value
306
+ })
307
+ } else if (headers) {
308
+ // Fallback for non-standard headers object
309
+ Object.keys(headers).forEach((key) => {
310
+ result[key] = headers[key]
311
+ })
312
+ }
313
+
314
+ return result
315
+ }
316
+ }
317
+
318
+ export const createNetworkFeature = () => {
319
+ const feature = new NetworkFeature()
320
+
321
+ return {
322
+ name: 'network',
323
+ label: 'Network Logs',
324
+ setup: () => feature.setup(),
325
+ getData: () => feature.getData(),
326
+ cleanup: () => feature.cleanup(),
327
+ setupAxiosInterceptors: (axiosInstance) =>
328
+ feature.setupAxiosInterceptors(axiosInstance),
329
+ }
330
+ }
package/lib/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import DebugToolKit from './DebugToolKit'
2
+ import { createNetworkFeature } from './features/NetworkFeature'
3
+
4
+ export { DebugToolKit, createNetworkFeature }
5
+
6
+ export default DebugToolKit
@@ -0,0 +1,22 @@
1
+ import { Dimensions } from 'react-native';
2
+
3
+ const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
4
+
5
+ export const IconRadius = 50;
6
+ export const PanelWidth = screenWidth * 0.85;
7
+ export const PanelHeight = screenHeight * 0.85;
8
+
9
+ export const DebugColors = {
10
+ white: '#FFFFFF',
11
+ blue: '#007AFF',
12
+ text: '#000000',
13
+ border: '#CCCCCC',
14
+ background: '#F5F5F5',
15
+ success: '#4CD964',
16
+ error: '#FF3B30',
17
+ warning: '#FF9500'
18
+ };
19
+
20
+ export const DebugImgs = {
21
+ iconLink: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVGhD7ZgxDoMwDEV9/xvQtReoGBASYmFhYmNiYmNhYWJiQQIJqUh8y05wlAYw+T/pD5Dg9xwnhPR6vV6v1+v1er3eGvuOb7yIhxEQvjAC4hRGQJzGCIhLGAFxGSMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIWxgBcQsjIG5hBMQtjIC4hREQtzAC4hZGQNzCCIhbGAFxCyMgbmEExC2MgLiFERC3MALiFkZA3MIIiFsYAXELIyBuYQTELYyAuIURELcwAuIWRkDcwgiIW/+PYBgfhPtL9WBrfKUAAAAASUVORK5CYII='
22
+ };