rn-backstage 1.0.0 → 1.2.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.
Files changed (72) hide show
  1. package/README.md +74 -23
  2. package/lib/commonjs/Backstage.js +55 -5
  3. package/lib/commonjs/Backstage.js.map +1 -1
  4. package/lib/commonjs/components/BackstagePanel.js +36 -5
  5. package/lib/commonjs/components/BackstagePanel.js.map +1 -1
  6. package/lib/commonjs/components/FlagsTab.js +255 -0
  7. package/lib/commonjs/components/FlagsTab.js.map +1 -0
  8. package/lib/commonjs/components/NetworkItem.js +537 -0
  9. package/lib/commonjs/components/NetworkItem.js.map +1 -0
  10. package/lib/commonjs/components/NetworkTab.js +274 -0
  11. package/lib/commonjs/components/NetworkTab.js.map +1 -0
  12. package/lib/commonjs/constants.js +11 -3
  13. package/lib/commonjs/constants.js.map +1 -1
  14. package/lib/commonjs/index.js +6 -0
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/log-interceptor.js +8 -1
  17. package/lib/commonjs/log-interceptor.js.map +1 -1
  18. package/lib/commonjs/network-interceptor.js +417 -0
  19. package/lib/commonjs/network-interceptor.js.map +1 -0
  20. package/lib/commonjs/types.js +9 -1
  21. package/lib/commonjs/types.js.map +1 -1
  22. package/lib/module/Backstage.js +56 -6
  23. package/lib/module/Backstage.js.map +1 -1
  24. package/lib/module/components/BackstagePanel.js +36 -5
  25. package/lib/module/components/BackstagePanel.js.map +1 -1
  26. package/lib/module/components/FlagsTab.js +250 -0
  27. package/lib/module/components/FlagsTab.js.map +1 -0
  28. package/lib/module/components/NetworkItem.js +532 -0
  29. package/lib/module/components/NetworkItem.js.map +1 -0
  30. package/lib/module/components/NetworkTab.js +269 -0
  31. package/lib/module/components/NetworkTab.js.map +1 -0
  32. package/lib/module/constants.js +10 -2
  33. package/lib/module/constants.js.map +1 -1
  34. package/lib/module/index.js +1 -1
  35. package/lib/module/index.js.map +1 -1
  36. package/lib/module/log-interceptor.js +8 -1
  37. package/lib/module/log-interceptor.js.map +1 -1
  38. package/lib/module/network-interceptor.js +409 -0
  39. package/lib/module/network-interceptor.js.map +1 -0
  40. package/lib/module/types.js +10 -0
  41. package/lib/module/types.js.map +1 -1
  42. package/lib/typescript/Backstage.d.ts.map +1 -1
  43. package/lib/typescript/components/BackstagePanel.d.ts +7 -1
  44. package/lib/typescript/components/BackstagePanel.d.ts.map +1 -1
  45. package/lib/typescript/components/FlagsTab.d.ts +9 -0
  46. package/lib/typescript/components/FlagsTab.d.ts.map +1 -0
  47. package/lib/typescript/components/NetworkItem.d.ts +9 -0
  48. package/lib/typescript/components/NetworkItem.d.ts.map +1 -0
  49. package/lib/typescript/components/NetworkTab.d.ts +11 -0
  50. package/lib/typescript/components/NetworkTab.d.ts.map +1 -0
  51. package/lib/typescript/constants.d.ts +8 -0
  52. package/lib/typescript/constants.d.ts.map +1 -1
  53. package/lib/typescript/index.d.ts +2 -2
  54. package/lib/typescript/index.d.ts.map +1 -1
  55. package/lib/typescript/log-interceptor.d.ts +3 -1
  56. package/lib/typescript/log-interceptor.d.ts.map +1 -1
  57. package/lib/typescript/network-interceptor.d.ts +35 -0
  58. package/lib/typescript/network-interceptor.d.ts.map +1 -0
  59. package/lib/typescript/types.d.ts +46 -0
  60. package/lib/typescript/types.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/Backstage.tsx +64 -6
  63. package/src/components/BackstagePanel.tsx +46 -5
  64. package/src/components/FlagsTab.tsx +249 -0
  65. package/src/components/InfoTab.tsx +1 -1
  66. package/src/components/NetworkItem.tsx +536 -0
  67. package/src/components/NetworkTab.tsx +274 -0
  68. package/src/constants.ts +11 -2
  69. package/src/index.ts +3 -1
  70. package/src/log-interceptor.ts +12 -1
  71. package/src/network-interceptor.ts +468 -0
  72. package/src/types.ts +60 -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
@@ -7,8 +7,10 @@ export type {
7
7
  BackstageStyleOverrides,
8
8
  BackstageTheme,
9
9
  QuickAction,
10
+ FeatureFlag,
10
11
  AppInfoItem,
11
12
  LogEntry,
13
+ NetworkEntry,
12
14
  } from './types'
13
15
 
14
- export { LogLevel } from './types'
16
+ export { LogLevel, NetworkState } from './types'
@@ -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(callback: LogCallback, filters: string[] = []): void {
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)