rn-backstage 1.4.0 → 1.4.2
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/package.json +3 -2
- package/src/Backstage.tsx +227 -0
- package/src/ThemeContext.tsx +38 -0
- package/src/bug-report.ts +184 -0
- package/src/components/BackstagePanel.tsx +349 -0
- package/src/components/BugReportComposer.tsx +559 -0
- package/src/components/FlagsTab.tsx +207 -0
- package/src/components/FloatingPill.tsx +247 -0
- package/src/components/InfoTab.tsx +231 -0
- package/src/components/JsonTreeView.tsx +239 -0
- package/src/components/LogItem.tsx +153 -0
- package/src/components/LogsTab.tsx +215 -0
- package/src/components/NetworkItem.tsx +425 -0
- package/src/components/NetworkTab.tsx +239 -0
- package/src/components/StorageTab.tsx +643 -0
- package/src/components/TabBar.tsx +154 -0
- package/src/constants.ts +207 -0
- package/src/index.ts +37 -0
- package/src/log-interceptor.ts +148 -0
- package/src/network-interceptor.ts +468 -0
- package/src/types.ts +280 -0
- package/src/utils/formatTimestamp.ts +22 -0
- package/src/utils/stringify.ts +90 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import type { StyleProp, TextStyle, ViewStyle } from 'react-native'
|
|
3
|
+
|
|
4
|
+
// ─── Log Types ───────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
export enum LogLevel {
|
|
7
|
+
log = 'log',
|
|
8
|
+
debug = 'debug',
|
|
9
|
+
info = 'info',
|
|
10
|
+
warn = 'warn',
|
|
11
|
+
error = 'error',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LogEntry {
|
|
15
|
+
id: string
|
|
16
|
+
level: LogLevel
|
|
17
|
+
message: string
|
|
18
|
+
data?: unknown
|
|
19
|
+
timestamp: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Network Types ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export enum NetworkState {
|
|
25
|
+
pending = 'pending',
|
|
26
|
+
completed = 'completed',
|
|
27
|
+
error = 'error',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface NetworkEntry {
|
|
31
|
+
id: string
|
|
32
|
+
method: string
|
|
33
|
+
url: string
|
|
34
|
+
startTime: number
|
|
35
|
+
endTime?: number
|
|
36
|
+
duration?: number
|
|
37
|
+
status?: number
|
|
38
|
+
statusText?: string
|
|
39
|
+
requestHeaders?: Record<string, string>
|
|
40
|
+
responseHeaders?: Record<string, string>
|
|
41
|
+
requestBody?: string
|
|
42
|
+
responseBody?: string
|
|
43
|
+
responseSize?: number
|
|
44
|
+
error?: string
|
|
45
|
+
state: NetworkState
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── App Info ────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export interface AppInfoItem {
|
|
51
|
+
label: string
|
|
52
|
+
value: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── Quick Actions ───────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export interface QuickAction {
|
|
58
|
+
title: string
|
|
59
|
+
onPress: () => void
|
|
60
|
+
closeOnPress?: boolean
|
|
61
|
+
icon?: string
|
|
62
|
+
destructive?: boolean
|
|
63
|
+
testID?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Feature Flags ─────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export interface FeatureFlag {
|
|
69
|
+
/** Unique key for this flag */
|
|
70
|
+
key: string
|
|
71
|
+
/** Display label */
|
|
72
|
+
label: string
|
|
73
|
+
/** Current value */
|
|
74
|
+
value: boolean
|
|
75
|
+
/** Optional description shown below the label */
|
|
76
|
+
description?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Storage Adapter ─────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
export interface StorageAdapter {
|
|
82
|
+
/** Return all stored keys */
|
|
83
|
+
getAllKeys: () => Promise<string[]>
|
|
84
|
+
/** Get value for a key. Return null if not found */
|
|
85
|
+
getItem: (key: string) => Promise<string | null>
|
|
86
|
+
/** Set a value for a key */
|
|
87
|
+
setItem: (key: string, value: string) => Promise<void>
|
|
88
|
+
/** Remove an entry by key */
|
|
89
|
+
removeItem: (key: string) => Promise<void>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Bug Report ──────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export type BugReportSeverity = 'low' | 'medium' | 'high' | 'critical'
|
|
95
|
+
|
|
96
|
+
export interface BugReport {
|
|
97
|
+
/** Report title */
|
|
98
|
+
title: string
|
|
99
|
+
/** Detailed description */
|
|
100
|
+
description: string
|
|
101
|
+
/** Severity level */
|
|
102
|
+
severity: BugReportSeverity
|
|
103
|
+
/** Device & app info snapshot */
|
|
104
|
+
deviceInfo: AppInfoItem[]
|
|
105
|
+
/** Console log entries included in the report */
|
|
106
|
+
logs: LogEntry[]
|
|
107
|
+
/** Network entries included in the report */
|
|
108
|
+
networkEntries: NetworkEntry[]
|
|
109
|
+
/** State tree snapshot (if included) */
|
|
110
|
+
state?: Record<string, unknown>
|
|
111
|
+
/** Screenshot URI or base64 (if captured) */
|
|
112
|
+
screenshotUri?: string
|
|
113
|
+
/** Timestamp when the report was created */
|
|
114
|
+
timestamp: number
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface BugReportConfig {
|
|
118
|
+
/** Callback when a report is submitted. Receives the full BugReport object */
|
|
119
|
+
onSubmit?: (report: BugReport) => void
|
|
120
|
+
/** Webhook URL to POST the report to */
|
|
121
|
+
webhookUrl?: string
|
|
122
|
+
/** Optional function to capture a screenshot. Return a file URI or base64 string */
|
|
123
|
+
captureScreenshot?: () => Promise<string>
|
|
124
|
+
/** Max number of log entries to include in the report. Default: 50 */
|
|
125
|
+
maxLogsInReport?: number
|
|
126
|
+
/** Max number of network entries to include in the report. Default: 20 */
|
|
127
|
+
maxNetworkEntriesInReport?: number
|
|
128
|
+
/** Whether to include the state tree in the report. Default: true */
|
|
129
|
+
includeState?: boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Extensible Tabs ─────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export interface BackstageTab {
|
|
135
|
+
key: string
|
|
136
|
+
title: string
|
|
137
|
+
icon?: string
|
|
138
|
+
render: () => ReactNode
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── Theme ───────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
export interface BackstageTheme {
|
|
144
|
+
background: string
|
|
145
|
+
surface: string
|
|
146
|
+
surfaceElevated: string
|
|
147
|
+
border: string
|
|
148
|
+
text: string
|
|
149
|
+
textSecondary: string
|
|
150
|
+
textMuted: string
|
|
151
|
+
accent: string
|
|
152
|
+
accentDim: string
|
|
153
|
+
error: string
|
|
154
|
+
errorDim: string
|
|
155
|
+
warning: string
|
|
156
|
+
warningDim: string
|
|
157
|
+
success: string
|
|
158
|
+
info: string
|
|
159
|
+
infoDim: string
|
|
160
|
+
debugColor: string
|
|
161
|
+
debugDim: string
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Main Props ──────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
export interface BackstageProps {
|
|
167
|
+
/** Whether the floating pill trigger is visible. Default: true */
|
|
168
|
+
visible?: boolean
|
|
169
|
+
|
|
170
|
+
/** Theme preference: 'light', 'dark', or 'auto' (follows device setting). Default: 'auto' */
|
|
171
|
+
theme?: 'light' | 'dark' | 'auto'
|
|
172
|
+
|
|
173
|
+
/** App version string (e.g., "1.2.3") */
|
|
174
|
+
appVersion?: string
|
|
175
|
+
|
|
176
|
+
/** Build number (e.g., "42") */
|
|
177
|
+
buildNumber?: string
|
|
178
|
+
|
|
179
|
+
/** Bundle identifier (e.g., "com.example.app") */
|
|
180
|
+
bundleId?: string
|
|
181
|
+
|
|
182
|
+
/** Additional device/app information rows */
|
|
183
|
+
deviceInfo?: AppInfoItem[]
|
|
184
|
+
|
|
185
|
+
/** State object to display in the tree viewer (Redux store, Zustand, etc.) */
|
|
186
|
+
state?: Record<string, unknown>
|
|
187
|
+
|
|
188
|
+
/** Custom action buttons in the Info tab */
|
|
189
|
+
quickActions?: QuickAction[]
|
|
190
|
+
|
|
191
|
+
/** Feature flags to display with toggle switches */
|
|
192
|
+
featureFlags?: FeatureFlag[]
|
|
193
|
+
|
|
194
|
+
/** Callback when a feature flag is toggled */
|
|
195
|
+
onToggleFeatureFlag?: (key: string, value: boolean) => void
|
|
196
|
+
|
|
197
|
+
/** Storage adapter for the Storage Viewer tab (AsyncStorage, MMKV, etc.) */
|
|
198
|
+
storageAdapter?: StorageAdapter
|
|
199
|
+
|
|
200
|
+
/** Maximum number of logs to retain in memory. Default: 500 */
|
|
201
|
+
maxLogs?: number
|
|
202
|
+
|
|
203
|
+
/** Log messages containing these strings will be excluded */
|
|
204
|
+
logFilters?: string[]
|
|
205
|
+
|
|
206
|
+
/** Callback triggered when user copies logs */
|
|
207
|
+
onCopyLogs?: (logs: string) => void
|
|
208
|
+
|
|
209
|
+
/** Additional custom tabs beyond Info and Logs */
|
|
210
|
+
extraTabs?: BackstageTab[]
|
|
211
|
+
|
|
212
|
+
/** Extra content rendered at the bottom of the Info tab */
|
|
213
|
+
children?: ReactNode
|
|
214
|
+
|
|
215
|
+
/** Custom styles overrides */
|
|
216
|
+
styles?: BackstageStyleOverrides
|
|
217
|
+
|
|
218
|
+
/** Initial X position for the floating pill */
|
|
219
|
+
initialX?: number
|
|
220
|
+
|
|
221
|
+
/** Initial Y position for the floating pill */
|
|
222
|
+
initialY?: number
|
|
223
|
+
|
|
224
|
+
/** Text displayed on the floating pill. Defaults to appVersion or "DEV" */
|
|
225
|
+
pillText?: string
|
|
226
|
+
|
|
227
|
+
/** Width of the floating pill. Default: 60 */
|
|
228
|
+
pillWidth?: number
|
|
229
|
+
|
|
230
|
+
/** Height of the floating pill. Default: 32 */
|
|
231
|
+
pillHeight?: number
|
|
232
|
+
|
|
233
|
+
/** Whether to enable network request interception. Default: true */
|
|
234
|
+
enableNetworkInspector?: boolean
|
|
235
|
+
|
|
236
|
+
/** Maximum number of network entries to retain in memory. Default: 500 */
|
|
237
|
+
maxNetworkEntries?: number
|
|
238
|
+
|
|
239
|
+
/** Maximum body size (bytes) to capture per request/response. Default: 65536 (64KB) */
|
|
240
|
+
maxNetworkBodySize?: number
|
|
241
|
+
|
|
242
|
+
/** URL substrings to exclude from network capture */
|
|
243
|
+
networkFilters?: string[]
|
|
244
|
+
|
|
245
|
+
/** Auto-filter console.logs from network callbacks (e.g., Axios interceptors) out of the Logs tab. Default: true */
|
|
246
|
+
autoFilterNetworkLogs?: boolean
|
|
247
|
+
|
|
248
|
+
/** Max nesting depth for JSON tree views (state tree, log data, network bodies). Default: 10 */
|
|
249
|
+
jsonMaxDepth?: number
|
|
250
|
+
|
|
251
|
+
/** Bug report configuration. When provided, shows a 📸 button in the panel header */
|
|
252
|
+
bugReport?: BugReportConfig
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export interface BackstageStyleOverrides {
|
|
256
|
+
pillStyle?: StyleProp<ViewStyle>
|
|
257
|
+
pillTextStyle?: StyleProp<TextStyle>
|
|
258
|
+
panelStyle?: StyleProp<ViewStyle>
|
|
259
|
+
headerTitleStyle?: StyleProp<TextStyle>
|
|
260
|
+
sectionTitleStyle?: StyleProp<TextStyle>
|
|
261
|
+
infoLabelStyle?: StyleProp<TextStyle>
|
|
262
|
+
infoValueStyle?: StyleProp<TextStyle>
|
|
263
|
+
actionButtonStyle?: StyleProp<ViewStyle>
|
|
264
|
+
actionButtonTitleStyle?: StyleProp<TextStyle>
|
|
265
|
+
logTimestampStyle?: StyleProp<TextStyle>
|
|
266
|
+
logMessageStyle?: StyleProp<TextStyle>
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ─── Ref Methods ─────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
export interface BackstageRef {
|
|
272
|
+
/** Open the backstage panel */
|
|
273
|
+
open: () => void
|
|
274
|
+
/** Close the backstage panel */
|
|
275
|
+
close: () => void
|
|
276
|
+
/** Clear all captured logs */
|
|
277
|
+
clearLogs: () => void
|
|
278
|
+
/** Open the bug report composer programmatically */
|
|
279
|
+
submitBugReport: () => void
|
|
280
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formats a timestamp (Date.now()) into a human-readable local time string.
|
|
3
|
+
* Format: "h:mm:ss.SSS AM/PM"
|
|
4
|
+
* No external dependencies — uses native Date API.
|
|
5
|
+
*/
|
|
6
|
+
export function formatTimestamp(timestamp: number): string {
|
|
7
|
+
const date = new Date(timestamp)
|
|
8
|
+
let hours = date.getHours()
|
|
9
|
+
const minutes = date.getMinutes()
|
|
10
|
+
const seconds = date.getSeconds()
|
|
11
|
+
const milliseconds = date.getMilliseconds()
|
|
12
|
+
const ampm = hours >= 12 ? 'PM' : 'AM'
|
|
13
|
+
|
|
14
|
+
hours = hours % 12
|
|
15
|
+
hours = hours || 12
|
|
16
|
+
|
|
17
|
+
const mm = minutes.toString().padStart(2, '0')
|
|
18
|
+
const ss = seconds.toString().padStart(2, '0')
|
|
19
|
+
const ms = milliseconds.toString().padStart(3, '0')
|
|
20
|
+
|
|
21
|
+
return `${hours}:${mm}:${ss}.${ms} ${ampm}`
|
|
22
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safely stringifies a value, handling circular references and Error objects.
|
|
3
|
+
*/
|
|
4
|
+
export function safeStringify(value: unknown, indent = 2): string {
|
|
5
|
+
if (value === undefined) return 'undefined'
|
|
6
|
+
if (value === null) return 'null'
|
|
7
|
+
|
|
8
|
+
if (value instanceof Error) {
|
|
9
|
+
return JSON.stringify(
|
|
10
|
+
{
|
|
11
|
+
name: value.name,
|
|
12
|
+
message: value.message,
|
|
13
|
+
stack: value.stack,
|
|
14
|
+
},
|
|
15
|
+
null,
|
|
16
|
+
indent,
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const seen = new WeakSet()
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(
|
|
24
|
+
value,
|
|
25
|
+
(_key, val) => {
|
|
26
|
+
if (typeof val === 'object' && val !== null) {
|
|
27
|
+
if (seen.has(val)) {
|
|
28
|
+
return '[Circular]'
|
|
29
|
+
}
|
|
30
|
+
seen.add(val)
|
|
31
|
+
}
|
|
32
|
+
if (typeof val === 'bigint') {
|
|
33
|
+
return val.toString()
|
|
34
|
+
}
|
|
35
|
+
if (typeof val === 'function') {
|
|
36
|
+
return `[Function: ${val.name || 'anonymous'}]`
|
|
37
|
+
}
|
|
38
|
+
if (typeof val === 'symbol') {
|
|
39
|
+
return val.toString()
|
|
40
|
+
}
|
|
41
|
+
return val
|
|
42
|
+
},
|
|
43
|
+
indent,
|
|
44
|
+
)
|
|
45
|
+
} catch {
|
|
46
|
+
return String(value)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Formats a console message and its optional params into a single string.
|
|
52
|
+
*/
|
|
53
|
+
export function formatLogMessage(message: unknown, optionalParams?: unknown[]): string {
|
|
54
|
+
const parts: string[] = []
|
|
55
|
+
|
|
56
|
+
if (message instanceof Error) {
|
|
57
|
+
parts.push(`${message.name}: ${message.message}`)
|
|
58
|
+
} else if (typeof message === 'object' && message !== null) {
|
|
59
|
+
parts.push(safeStringify(message, 0))
|
|
60
|
+
} else if (typeof message === 'string') {
|
|
61
|
+
// Strip console color formatting (%c)
|
|
62
|
+
parts.push(message.replace(/%c/g, '').trim())
|
|
63
|
+
} else {
|
|
64
|
+
parts.push(String(message))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (optionalParams && optionalParams.length > 0) {
|
|
68
|
+
for (const param of optionalParams) {
|
|
69
|
+
if (typeof param === 'string' && isColorString(param)) {
|
|
70
|
+
continue // skip css color strings from console.log('%c ...', 'color: ...')
|
|
71
|
+
}
|
|
72
|
+
if (typeof param === 'object' && param !== null) {
|
|
73
|
+
parts.push(safeStringify(param, 0))
|
|
74
|
+
} else {
|
|
75
|
+
parts.push(String(param))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return parts.join(' ')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isColorString(str: string): boolean {
|
|
84
|
+
return (
|
|
85
|
+
str.includes('color:') ||
|
|
86
|
+
str.includes('background:') ||
|
|
87
|
+
str.includes('font-') ||
|
|
88
|
+
/^#[0-9A-Fa-f]{3,8}$/.test(str.trim())
|
|
89
|
+
)
|
|
90
|
+
}
|