rn-backstage 1.0.0 → 1.1.0
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.
- package/README.md +54 -23
- package/lib/commonjs/Backstage.js +51 -5
- package/lib/commonjs/Backstage.js.map +1 -1
- package/lib/commonjs/components/BackstagePanel.js +16 -0
- package/lib/commonjs/components/BackstagePanel.js.map +1 -1
- package/lib/commonjs/components/NetworkItem.js +537 -0
- package/lib/commonjs/components/NetworkItem.js.map +1 -0
- package/lib/commonjs/components/NetworkTab.js +274 -0
- package/lib/commonjs/components/NetworkTab.js.map +1 -0
- package/lib/commonjs/constants.js +11 -3
- package/lib/commonjs/constants.js.map +1 -1
- package/lib/commonjs/index.js +6 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/log-interceptor.js +8 -1
- package/lib/commonjs/log-interceptor.js.map +1 -1
- package/lib/commonjs/network-interceptor.js +417 -0
- package/lib/commonjs/network-interceptor.js.map +1 -0
- package/lib/commonjs/types.js +8 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/module/Backstage.js +52 -6
- package/lib/module/Backstage.js.map +1 -1
- package/lib/module/components/BackstagePanel.js +16 -0
- package/lib/module/components/BackstagePanel.js.map +1 -1
- package/lib/module/components/NetworkItem.js +532 -0
- package/lib/module/components/NetworkItem.js.map +1 -0
- package/lib/module/components/NetworkTab.js +269 -0
- package/lib/module/components/NetworkTab.js.map +1 -0
- package/lib/module/constants.js +10 -2
- package/lib/module/constants.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/log-interceptor.js +8 -1
- package/lib/module/log-interceptor.js.map +1 -1
- package/lib/module/network-interceptor.js +409 -0
- package/lib/module/network-interceptor.js.map +1 -0
- package/lib/module/types.js +8 -0
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/Backstage.d.ts.map +1 -1
- package/lib/typescript/components/BackstagePanel.d.ts +5 -1
- package/lib/typescript/components/BackstagePanel.d.ts.map +1 -1
- package/lib/typescript/components/NetworkItem.d.ts +9 -0
- package/lib/typescript/components/NetworkItem.d.ts.map +1 -0
- package/lib/typescript/components/NetworkTab.d.ts +11 -0
- package/lib/typescript/components/NetworkTab.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +8 -0
- package/lib/typescript/constants.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/log-interceptor.d.ts +3 -1
- package/lib/typescript/log-interceptor.d.ts.map +1 -1
- package/lib/typescript/network-interceptor.d.ts +35 -0
- package/lib/typescript/network-interceptor.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +32 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Backstage.tsx +60 -6
- package/src/components/BackstagePanel.tsx +22 -0
- package/src/components/NetworkItem.tsx +536 -0
- package/src/components/NetworkTab.tsx +274 -0
- package/src/constants.ts +11 -2
- package/src/index.ts +2 -1
- package/src/log-interceptor.ts +12 -1
- package/src/network-interceptor.ts +468 -0
- package/src/types.ts +41 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react'
|
|
2
|
+
import { FlatList, StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'
|
|
3
|
+
import { DarkTheme, MonospaceFont, TestIDs } from '../constants'
|
|
4
|
+
import { NetworkState } from '../types'
|
|
5
|
+
import type { NetworkEntry } from '../types'
|
|
6
|
+
import { NetworkItem } from './NetworkItem'
|
|
7
|
+
|
|
8
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
interface NetworkTabProps {
|
|
11
|
+
entries: NetworkEntry[]
|
|
12
|
+
onRefresh: () => void
|
|
13
|
+
onClear: () => void
|
|
14
|
+
onCopy?: (text: string) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export const NetworkTab: React.FC<NetworkTabProps> = ({ entries, onRefresh, onClear, onCopy }) => {
|
|
20
|
+
const [searchText, setSearchText] = useState('')
|
|
21
|
+
|
|
22
|
+
// Filter entries based on search text
|
|
23
|
+
const filteredEntries = useMemo(() => {
|
|
24
|
+
if (!searchText.trim()) return entries
|
|
25
|
+
|
|
26
|
+
const query = searchText.toLowerCase()
|
|
27
|
+
return entries.filter(entry => {
|
|
28
|
+
return (
|
|
29
|
+
entry.url.toLowerCase().includes(query) ||
|
|
30
|
+
entry.method.toLowerCase().includes(query) ||
|
|
31
|
+
(entry.status !== undefined && String(entry.status).includes(query)) ||
|
|
32
|
+
(entry.error && entry.error.toLowerCase().includes(query))
|
|
33
|
+
)
|
|
34
|
+
})
|
|
35
|
+
}, [entries, searchText])
|
|
36
|
+
|
|
37
|
+
// Summary stats
|
|
38
|
+
const stats = useMemo(() => {
|
|
39
|
+
const total = entries.length
|
|
40
|
+
const errors = entries.filter(
|
|
41
|
+
e => e.state === NetworkState.error || (e.status !== undefined && e.status >= 400),
|
|
42
|
+
).length
|
|
43
|
+
const pending = entries.filter(e => e.state === NetworkState.pending).length
|
|
44
|
+
const completed = entries.filter(
|
|
45
|
+
e => e.state === NetworkState.completed && e.duration !== undefined,
|
|
46
|
+
)
|
|
47
|
+
const avgDuration =
|
|
48
|
+
completed.length > 0
|
|
49
|
+
? Math.round(completed.reduce((sum, e) => sum + (e.duration ?? 0), 0) / completed.length)
|
|
50
|
+
: 0
|
|
51
|
+
|
|
52
|
+
return { total, errors, pending, avgDuration }
|
|
53
|
+
}, [entries])
|
|
54
|
+
|
|
55
|
+
const renderItem = useCallback(
|
|
56
|
+
({ item }: { item: NetworkEntry }) => <NetworkItem item={item} onCopy={onCopy} />,
|
|
57
|
+
[onCopy],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const keyExtractor = useCallback((item: NetworkEntry) => item.id, [])
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<View style={styles.container}>
|
|
64
|
+
{/* Search Bar */}
|
|
65
|
+
<View style={styles.searchContainer}>
|
|
66
|
+
<View style={styles.searchInputWrapper}>
|
|
67
|
+
<Text style={styles.searchIcon}>⌕</Text>
|
|
68
|
+
<TextInput
|
|
69
|
+
testID={TestIDs.networkTab.searchInput}
|
|
70
|
+
style={styles.searchInput}
|
|
71
|
+
placeholder="Filter by URL, method, status..."
|
|
72
|
+
placeholderTextColor={DarkTheme.textMuted}
|
|
73
|
+
value={searchText}
|
|
74
|
+
onChangeText={setSearchText}
|
|
75
|
+
autoCapitalize="none"
|
|
76
|
+
autoCorrect={false}
|
|
77
|
+
clearButtonMode="while-editing"
|
|
78
|
+
returnKeyType="search"
|
|
79
|
+
/>
|
|
80
|
+
</View>
|
|
81
|
+
<TouchableOpacity
|
|
82
|
+
testID={TestIDs.networkTab.clearButton}
|
|
83
|
+
style={styles.clearButton}
|
|
84
|
+
onPress={onClear}
|
|
85
|
+
activeOpacity={0.7}
|
|
86
|
+
>
|
|
87
|
+
<Text style={styles.clearButtonText}>Clear</Text>
|
|
88
|
+
</TouchableOpacity>
|
|
89
|
+
</View>
|
|
90
|
+
|
|
91
|
+
{/* Stats Bar */}
|
|
92
|
+
<View style={styles.statsBar}>
|
|
93
|
+
<View style={styles.statsRow}>
|
|
94
|
+
<Text style={styles.statText}>
|
|
95
|
+
{filteredEntries.length === entries.length
|
|
96
|
+
? `${stats.total} request${stats.total !== 1 ? 's' : ''}`
|
|
97
|
+
: `${filteredEntries.length} of ${stats.total}`}
|
|
98
|
+
</Text>
|
|
99
|
+
{stats.errors > 0 && (
|
|
100
|
+
<View style={styles.statBadge}>
|
|
101
|
+
<Text style={styles.statBadgeError}>
|
|
102
|
+
{stats.errors} error{stats.errors !== 1 ? 's' : ''}
|
|
103
|
+
</Text>
|
|
104
|
+
</View>
|
|
105
|
+
)}
|
|
106
|
+
{stats.pending > 0 && (
|
|
107
|
+
<View style={[styles.statBadge, styles.statBadgePending]}>
|
|
108
|
+
<Text style={styles.statBadgePendingText}>{stats.pending} pending</Text>
|
|
109
|
+
</View>
|
|
110
|
+
)}
|
|
111
|
+
</View>
|
|
112
|
+
{stats.avgDuration > 0 && <Text style={styles.avgText}>avg {stats.avgDuration}ms</Text>}
|
|
113
|
+
</View>
|
|
114
|
+
|
|
115
|
+
{/* Request List */}
|
|
116
|
+
<FlatList
|
|
117
|
+
testID={TestIDs.networkTab.list}
|
|
118
|
+
data={filteredEntries}
|
|
119
|
+
renderItem={renderItem}
|
|
120
|
+
keyExtractor={keyExtractor}
|
|
121
|
+
refreshing={false}
|
|
122
|
+
onRefresh={onRefresh}
|
|
123
|
+
style={styles.list}
|
|
124
|
+
contentContainerStyle={filteredEntries.length === 0 ? styles.emptyContainer : undefined}
|
|
125
|
+
ListEmptyComponent={
|
|
126
|
+
<View style={styles.emptyState}>
|
|
127
|
+
<Text style={styles.emptyIcon}>🌐</Text>
|
|
128
|
+
<Text style={styles.emptyTitle}>No network requests</Text>
|
|
129
|
+
<Text style={styles.emptySubtitle}>
|
|
130
|
+
HTTP requests made via fetch or XMLHttpRequest will appear here
|
|
131
|
+
</Text>
|
|
132
|
+
</View>
|
|
133
|
+
}
|
|
134
|
+
// Perf optimizations
|
|
135
|
+
maxToRenderPerBatch={20}
|
|
136
|
+
windowSize={10}
|
|
137
|
+
initialNumToRender={20}
|
|
138
|
+
removeClippedSubviews={true}
|
|
139
|
+
/>
|
|
140
|
+
</View>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ─── Styles ──────────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
const styles = StyleSheet.create({
|
|
147
|
+
container: {
|
|
148
|
+
flex: 1,
|
|
149
|
+
},
|
|
150
|
+
searchContainer: {
|
|
151
|
+
flexDirection: 'row',
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
paddingHorizontal: 14,
|
|
154
|
+
paddingVertical: 10,
|
|
155
|
+
gap: 10,
|
|
156
|
+
borderBottomWidth: 1,
|
|
157
|
+
borderBottomColor: DarkTheme.border,
|
|
158
|
+
},
|
|
159
|
+
searchInputWrapper: {
|
|
160
|
+
flex: 1,
|
|
161
|
+
flexDirection: 'row',
|
|
162
|
+
alignItems: 'center',
|
|
163
|
+
backgroundColor: DarkTheme.surfaceElevated,
|
|
164
|
+
borderRadius: 10,
|
|
165
|
+
borderWidth: 1,
|
|
166
|
+
borderColor: DarkTheme.border,
|
|
167
|
+
paddingHorizontal: 12,
|
|
168
|
+
height: 38,
|
|
169
|
+
},
|
|
170
|
+
searchIcon: {
|
|
171
|
+
fontSize: 16,
|
|
172
|
+
color: DarkTheme.textMuted,
|
|
173
|
+
marginRight: 8,
|
|
174
|
+
},
|
|
175
|
+
searchInput: {
|
|
176
|
+
flex: 1,
|
|
177
|
+
fontFamily: MonospaceFont,
|
|
178
|
+
fontSize: 13,
|
|
179
|
+
color: DarkTheme.text,
|
|
180
|
+
padding: 0,
|
|
181
|
+
},
|
|
182
|
+
clearButton: {
|
|
183
|
+
backgroundColor: DarkTheme.errorDim,
|
|
184
|
+
borderRadius: 8,
|
|
185
|
+
borderWidth: 1,
|
|
186
|
+
borderColor: DarkTheme.error,
|
|
187
|
+
paddingHorizontal: 14,
|
|
188
|
+
height: 38,
|
|
189
|
+
justifyContent: 'center',
|
|
190
|
+
},
|
|
191
|
+
clearButtonText: {
|
|
192
|
+
fontFamily: MonospaceFont,
|
|
193
|
+
fontSize: 12,
|
|
194
|
+
fontWeight: '700',
|
|
195
|
+
color: DarkTheme.error,
|
|
196
|
+
letterSpacing: 0.5,
|
|
197
|
+
},
|
|
198
|
+
statsBar: {
|
|
199
|
+
flexDirection: 'row',
|
|
200
|
+
justifyContent: 'space-between',
|
|
201
|
+
alignItems: 'center',
|
|
202
|
+
paddingHorizontal: 16,
|
|
203
|
+
paddingVertical: 6,
|
|
204
|
+
backgroundColor: DarkTheme.surface,
|
|
205
|
+
},
|
|
206
|
+
statsRow: {
|
|
207
|
+
flexDirection: 'row',
|
|
208
|
+
alignItems: 'center',
|
|
209
|
+
gap: 8,
|
|
210
|
+
},
|
|
211
|
+
statText: {
|
|
212
|
+
fontFamily: MonospaceFont,
|
|
213
|
+
fontSize: 11,
|
|
214
|
+
color: DarkTheme.textMuted,
|
|
215
|
+
},
|
|
216
|
+
statBadge: {
|
|
217
|
+
backgroundColor: DarkTheme.errorDim,
|
|
218
|
+
borderRadius: 4,
|
|
219
|
+
paddingHorizontal: 6,
|
|
220
|
+
paddingVertical: 1,
|
|
221
|
+
},
|
|
222
|
+
statBadgeError: {
|
|
223
|
+
fontFamily: MonospaceFont,
|
|
224
|
+
fontSize: 10,
|
|
225
|
+
fontWeight: '700',
|
|
226
|
+
color: DarkTheme.error,
|
|
227
|
+
},
|
|
228
|
+
statBadgePending: {
|
|
229
|
+
backgroundColor: DarkTheme.warningDim,
|
|
230
|
+
},
|
|
231
|
+
statBadgePendingText: {
|
|
232
|
+
fontFamily: MonospaceFont,
|
|
233
|
+
fontSize: 10,
|
|
234
|
+
fontWeight: '700',
|
|
235
|
+
color: DarkTheme.warning,
|
|
236
|
+
},
|
|
237
|
+
avgText: {
|
|
238
|
+
fontFamily: MonospaceFont,
|
|
239
|
+
fontSize: 10,
|
|
240
|
+
color: DarkTheme.textMuted,
|
|
241
|
+
fontStyle: 'italic',
|
|
242
|
+
},
|
|
243
|
+
list: {
|
|
244
|
+
flex: 1,
|
|
245
|
+
},
|
|
246
|
+
emptyContainer: {
|
|
247
|
+
flex: 1,
|
|
248
|
+
justifyContent: 'center',
|
|
249
|
+
},
|
|
250
|
+
emptyState: {
|
|
251
|
+
alignItems: 'center',
|
|
252
|
+
justifyContent: 'center',
|
|
253
|
+
padding: 40,
|
|
254
|
+
},
|
|
255
|
+
emptyIcon: {
|
|
256
|
+
fontSize: 48,
|
|
257
|
+
marginBottom: 16,
|
|
258
|
+
opacity: 0.5,
|
|
259
|
+
},
|
|
260
|
+
emptyTitle: {
|
|
261
|
+
fontFamily: MonospaceFont,
|
|
262
|
+
fontSize: 16,
|
|
263
|
+
fontWeight: '700',
|
|
264
|
+
color: DarkTheme.textSecondary,
|
|
265
|
+
marginBottom: 8,
|
|
266
|
+
},
|
|
267
|
+
emptySubtitle: {
|
|
268
|
+
fontFamily: MonospaceFont,
|
|
269
|
+
fontSize: 13,
|
|
270
|
+
color: DarkTheme.textMuted,
|
|
271
|
+
textAlign: 'center',
|
|
272
|
+
lineHeight: 20,
|
|
273
|
+
},
|
|
274
|
+
})
|
package/src/constants.ts
CHANGED
|
@@ -41,10 +41,13 @@ export const DarkTheme: BackstageTheme = {
|
|
|
41
41
|
debugDim: 'rgba(167, 139, 250, 0.12)',
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// ─── Max Logs ────────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
44
|
export const DEFAULT_MAX_LOGS = 500
|
|
47
45
|
|
|
46
|
+
// ─── Network Defaults ────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export const DEFAULT_MAX_NETWORK_ENTRIES = 500
|
|
49
|
+
export const DEFAULT_MAX_NETWORK_BODY_SIZE = 65536 // 64 KB
|
|
50
|
+
|
|
48
51
|
// ─── Monospace Font ──────────────────────────────────────────────────────────
|
|
49
52
|
|
|
50
53
|
export const MonospaceFont = Platform.select({
|
|
@@ -79,4 +82,10 @@ export const TestIDs = {
|
|
|
79
82
|
copyButton: 'backstage.logs.copy',
|
|
80
83
|
list: 'backstage.logs.list',
|
|
81
84
|
},
|
|
85
|
+
networkTab: {
|
|
86
|
+
section: 'backstage.network',
|
|
87
|
+
searchInput: 'backstage.network.search',
|
|
88
|
+
list: 'backstage.network.list',
|
|
89
|
+
clearButton: 'backstage.network.clear',
|
|
90
|
+
},
|
|
82
91
|
}
|
package/src/index.ts
CHANGED
package/src/log-interceptor.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { LogLevel } from './types'
|
|
|
2
2
|
import type { LogEntry } from './types'
|
|
3
3
|
import { formatLogMessage } from './utils/stringify'
|
|
4
4
|
import { DEFAULT_MAX_LOGS } from './constants'
|
|
5
|
+
import { isInsideNetworkCallback } from './network-interceptor'
|
|
5
6
|
|
|
6
7
|
// ─── Original Console Methods ────────────────────────────────────────────────
|
|
7
8
|
|
|
@@ -17,6 +18,7 @@ type LogCallback = (entry: LogEntry) => void
|
|
|
17
18
|
|
|
18
19
|
let activeCallback: LogCallback | null = null
|
|
19
20
|
let isInstalled = false
|
|
21
|
+
let autoFilterNetwork = false
|
|
20
22
|
|
|
21
23
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
22
24
|
|
|
@@ -54,14 +56,20 @@ function shouldLog(message: unknown, filters: string[]): boolean {
|
|
|
54
56
|
|
|
55
57
|
// ─── Install / Uninstall ─────────────────────────────────────────────────────
|
|
56
58
|
|
|
57
|
-
export function installInterceptor(
|
|
59
|
+
export function installInterceptor(
|
|
60
|
+
callback: LogCallback,
|
|
61
|
+
filters: string[] = [],
|
|
62
|
+
options: { autoFilterNetworkLogs?: boolean } = {},
|
|
63
|
+
): void {
|
|
58
64
|
if (isInstalled) {
|
|
59
65
|
// Just update callback if already installed
|
|
60
66
|
activeCallback = callback
|
|
67
|
+
autoFilterNetwork = options.autoFilterNetworkLogs ?? true
|
|
61
68
|
return
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
activeCallback = callback
|
|
72
|
+
autoFilterNetwork = options.autoFilterNetworkLogs ?? true
|
|
65
73
|
isInstalled = true
|
|
66
74
|
|
|
67
75
|
const levels: Array<[LogLevel, keyof typeof originalConsole]> = [
|
|
@@ -80,6 +88,9 @@ export function installInterceptor(callback: LogCallback, filters: string[] = []
|
|
|
80
88
|
// Check filters
|
|
81
89
|
if (!shouldLog(message, filters)) return
|
|
82
90
|
|
|
91
|
+
// Skip logs from inside network callbacks (e.g., Axios interceptors)
|
|
92
|
+
if (autoFilterNetwork && isInsideNetworkCallback()) return
|
|
93
|
+
|
|
83
94
|
// Create log entry and notify
|
|
84
95
|
if (activeCallback) {
|
|
85
96
|
const entry = createLogEntry(level, message, optionalParams)
|