rn-backstage 1.4.5 → 1.4.6

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 CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "rn-backstage",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "description": "A zero-dependency developer/QA debug panel for React Native apps",
5
5
  "main": "lib/commonjs/index",
6
6
  "module": "lib/module/index",
7
7
  "types": "lib/typescript/index.d.ts",
8
- "react-native": "src/index",
8
+ "react-native": "lib/module/index",
9
9
  "source": "src/index",
10
10
  "files": [
11
- "src",
12
11
  "lib",
13
12
  "!**/__tests__",
14
13
  "!**/__fixtures__",
package/src/Backstage.tsx DELETED
@@ -1,227 +0,0 @@
1
- import React, {
2
- forwardRef,
3
- useCallback,
4
- useEffect,
5
- useImperativeHandle,
6
- useRef,
7
- useState,
8
- } from 'react'
9
- import { StyleSheet, View } from 'react-native'
10
- import { DEFAULT_MAX_LOGS, DEFAULT_MAX_NETWORK_ENTRIES } from './constants'
11
- import { installInterceptor, LogBuffer, uninstallInterceptor } from './log-interceptor'
12
- import {
13
- installNetworkInterceptor,
14
- NetworkBuffer,
15
- uninstallNetworkInterceptor,
16
- } from './network-interceptor'
17
- import { FloatingPill } from './components/FloatingPill'
18
- import { BackstagePanel } from './components/BackstagePanel'
19
- import { BackstageThemeProvider } from './ThemeContext'
20
- import type { BackstageProps, BackstageRef, LogEntry, NetworkEntry } from './types'
21
-
22
- // ─── Component ───────────────────────────────────────────────────────────────
23
-
24
- export const Backstage = forwardRef<BackstageRef, BackstageProps>(
25
- (
26
- {
27
- visible = true,
28
- theme: themePreference = 'auto',
29
- appVersion,
30
- buildNumber,
31
- bundleId,
32
- deviceInfo,
33
- state,
34
- quickActions,
35
- featureFlags,
36
- onToggleFeatureFlag,
37
- storageAdapter,
38
- maxLogs = DEFAULT_MAX_LOGS,
39
- logFilters,
40
- onCopyLogs,
41
- extraTabs,
42
- children,
43
- styles: propStyles,
44
- initialX,
45
- initialY,
46
- pillText,
47
- pillWidth,
48
- pillHeight,
49
- enableNetworkInspector = true,
50
- maxNetworkEntries = DEFAULT_MAX_NETWORK_ENTRIES,
51
- maxNetworkBodySize,
52
- networkFilters,
53
- autoFilterNetworkLogs = true,
54
- jsonMaxDepth,
55
- bugReport: bugReportConfig,
56
- },
57
- ref,
58
- ) => {
59
- const [panelVisible, setPanelVisible] = useState(false)
60
- const [hasError, setHasError] = useState(false)
61
- const [logs, setLogs] = useState<LogEntry[]>([])
62
- const [networkEntries, setNetworkEntries] = useState<NetworkEntry[]>([])
63
-
64
- const logBuffer = useRef(new LogBuffer(maxLogs))
65
- const networkBuffer = useRef(new NetworkBuffer(maxNetworkEntries))
66
-
67
- // ── Panel controls ────────────────────────────────────────
68
-
69
- const openPanel = useCallback(() => {
70
- setPanelVisible(true)
71
- // Sync logs and network entries when opening panel
72
- setLogs(logBuffer.current.getAll())
73
- setNetworkEntries(networkBuffer.current.getAll())
74
- }, [])
75
-
76
- const closePanel = useCallback(() => {
77
- setPanelVisible(false)
78
- }, [])
79
-
80
- const clearLogs = useCallback(() => {
81
- logBuffer.current.clear()
82
- setLogs([])
83
- setHasError(false)
84
- }, [])
85
-
86
- // ── Refresh logs ──────────────────────────────────────────────────
87
-
88
- const refreshLogs = useCallback(() => {
89
- setLogs(logBuffer.current.getAll())
90
- }, [])
91
-
92
- // ── Network controls ──────────────────────────────────────────────
93
-
94
- const refreshNetwork = useCallback(() => {
95
- setNetworkEntries(networkBuffer.current.getAll())
96
- }, [])
97
-
98
- const clearNetwork = useCallback(() => {
99
- networkBuffer.current.clear()
100
- setNetworkEntries([])
101
- }, [])
102
-
103
- // ── Install console interceptor ───────────────────────────
104
-
105
- useEffect(() => {
106
- const handleLogEntry = (entry: LogEntry) => {
107
- logBuffer.current.push(entry)
108
-
109
- // Track error state for the floating pill
110
- if (entry.level === 'error') {
111
- setHasError(true)
112
- }
113
- }
114
-
115
- installInterceptor(handleLogEntry, logFilters, {
116
- autoFilterNetworkLogs: enableNetworkInspector && autoFilterNetworkLogs,
117
- })
118
-
119
- return () => {
120
- uninstallInterceptor()
121
- }
122
- }, [logFilters, enableNetworkInspector, autoFilterNetworkLogs])
123
-
124
- // ── Install network interceptor ───────────────────────────────
125
-
126
- useEffect(() => {
127
- if (!enableNetworkInspector) return
128
-
129
- const handleNetworkEntry = (entry: NetworkEntry) => {
130
- networkBuffer.current.upsert(entry)
131
-
132
- // Auto-refresh if panel is open
133
- if (panelVisible) {
134
- setNetworkEntries(networkBuffer.current.getAll())
135
- }
136
- }
137
-
138
- installNetworkInterceptor(handleNetworkEntry, {
139
- filters: networkFilters,
140
- maxBodySize: maxNetworkBodySize,
141
- })
142
-
143
- return () => {
144
- uninstallNetworkInterceptor()
145
- }
146
- }, [enableNetworkInspector, networkFilters, maxNetworkBodySize, panelVisible])
147
-
148
- // ── Expose ref methods ────────────────────────────────────
149
-
150
- const openBugReportRef = useRef<(() => void) | null>(null)
151
-
152
- useImperativeHandle(ref, () => ({
153
- open: openPanel,
154
- close: closePanel,
155
- clearLogs,
156
- submitBugReport: () => {
157
- if (bugReportConfig) {
158
- openPanel()
159
- // Small delay to let panel mount first
160
- setTimeout(() => openBugReportRef.current?.(), 100)
161
- }
162
- },
163
- }))
164
-
165
- // ── Render ────────────────────────────────────────────────
166
-
167
- if (!visible) return null
168
-
169
- const displayText = pillText || (appVersion ? `v${appVersion}` : 'DEV')
170
-
171
- return (
172
- <BackstageThemeProvider preference={themePreference}>
173
- <View style={styles.container} pointerEvents="box-none">
174
- <FloatingPill
175
- text={displayText}
176
- hasError={hasError}
177
- onPress={openPanel}
178
- initialX={initialX}
179
- initialY={initialY}
180
- pillWidth={pillWidth}
181
- pillHeight={pillHeight}
182
- styles={propStyles}
183
- />
184
- <BackstagePanel
185
- visible={panelVisible}
186
- onClose={closePanel}
187
- appVersion={appVersion}
188
- buildNumber={buildNumber}
189
- bundleId={bundleId}
190
- deviceInfo={deviceInfo}
191
- state={state}
192
- quickActions={quickActions}
193
- featureFlags={featureFlags}
194
- onToggleFeatureFlag={onToggleFeatureFlag}
195
- logs={logs}
196
- onRefreshLogs={refreshLogs}
197
- onCopyLogs={onCopyLogs}
198
- networkEntries={networkEntries}
199
- onRefreshNetwork={refreshNetwork}
200
- onClearNetwork={clearNetwork}
201
- onCopyNetwork={onCopyLogs}
202
- extraTabs={extraTabs}
203
- jsonMaxDepth={jsonMaxDepth}
204
- storageAdapter={storageAdapter}
205
- bugReportConfig={bugReportConfig}
206
- bugReportOpenerRef={openBugReportRef}
207
- styles={propStyles}
208
- >
209
- {children}
210
- </BackstagePanel>
211
- </View>
212
- </BackstageThemeProvider>
213
- )
214
- },
215
- )
216
-
217
- Backstage.displayName = 'Backstage'
218
-
219
- // ─── Styles ──────────────────────────────────────────────────────────────────
220
-
221
- const styles = StyleSheet.create({
222
- container: {
223
- ...StyleSheet.absoluteFillObject,
224
- zIndex: 99999,
225
- elevation: 99999,
226
- },
227
- })
@@ -1,38 +0,0 @@
1
- import React, { createContext, useContext, useMemo } from 'react'
2
- import { useColorScheme } from 'react-native'
3
- import { DarkTheme, LightTheme } from './constants'
4
- import type { BackstageTheme } from './types'
5
-
6
- // ─── Theme Preference ────────────────────────────────────────────────────────
7
-
8
- export type ThemePreference = 'light' | 'dark' | 'auto'
9
-
10
- // ─── Context ─────────────────────────────────────────────────────────────────
11
-
12
- const ThemeContext = createContext<BackstageTheme>(DarkTheme)
13
-
14
- // ─── Provider ────────────────────────────────────────────────────────────────
15
-
16
- interface ThemeProviderProps {
17
- preference: ThemePreference
18
- children: React.ReactNode
19
- }
20
-
21
- export const BackstageThemeProvider: React.FC<ThemeProviderProps> = ({ preference, children }) => {
22
- const systemScheme = useColorScheme()
23
-
24
- const theme = useMemo(() => {
25
- if (preference === 'light') return LightTheme
26
- if (preference === 'dark') return DarkTheme
27
- // 'auto' — follow system, fallback to dark
28
- return systemScheme === 'light' ? LightTheme : DarkTheme
29
- }, [preference, systemScheme])
30
-
31
- return <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
32
- }
33
-
34
- // ─── Hook ────────────────────────────────────────────────────────────────────
35
-
36
- export function useBackstageTheme(): BackstageTheme {
37
- return useContext(ThemeContext)
38
- }
package/src/bug-report.ts DELETED
@@ -1,184 +0,0 @@
1
- import { Platform } from 'react-native'
2
- import type {
3
- AppInfoItem,
4
- BugReport,
5
- BugReportSeverity,
6
- LogEntry,
7
- NetworkEntry,
8
- } from './types'
9
- import { LogLevel } from './types'
10
-
11
- // ─── Report Composition ──────────────────────────────────────────────────────
12
-
13
- const SEVERITY_LABELS: Record<BugReportSeverity, string> = {
14
- low: '🟢 Low',
15
- medium: '🟡 Medium',
16
- high: '🟠 High',
17
- critical: '🔴 Critical',
18
- }
19
-
20
- function formatTimestamp(ts: number): string {
21
- const d = new Date(ts)
22
- return d.toISOString().replace('T', ' ').substring(0, 19)
23
- }
24
-
25
- function formatLogLevel(level: LogLevel): string {
26
- switch (level) {
27
- case LogLevel.error:
28
- return '❌ ERROR'
29
- case LogLevel.warn:
30
- return '⚠️ WARN'
31
- case LogLevel.info:
32
- return 'ℹ️ INFO'
33
- case LogLevel.debug:
34
- return '🔍 DEBUG'
35
- default:
36
- return '📝 LOG'
37
- }
38
- }
39
-
40
- export function formatDeviceInfoForReport(items: AppInfoItem[]): string {
41
- if (items.length === 0) return ''
42
- const lines = items.map(i => `- **${i.label}**: ${i.value}`)
43
- return `## 📱 Device Info\n\n${lines.join('\n')}\n`
44
- }
45
-
46
- export function formatLogsForReport(logs: LogEntry[]): string {
47
- if (logs.length === 0) return ''
48
- const lines = logs.map(
49
- l => `\`${formatTimestamp(l.timestamp)}\` ${formatLogLevel(l.level)} ${l.message}`,
50
- )
51
- return `## 📋 Console Logs (${logs.length})\n\n\`\`\`\n${lines.join('\n')}\n\`\`\`\n`
52
- }
53
-
54
- export function formatNetworkForReport(entries: NetworkEntry[]): string {
55
- if (entries.length === 0) return ''
56
- const lines = entries.map(e => {
57
- const status = e.status ?? '⏳'
58
- const duration = e.duration ? `${e.duration}ms` : '...'
59
- return `- \`${e.method}\` ${e.url} → ${status} (${duration})`
60
- })
61
- return `## 🌐 Network Activity (${entries.length})\n\n${lines.join('\n')}\n`
62
- }
63
-
64
- export function formatStateForReport(state: Record<string, unknown>): string {
65
- try {
66
- const json = JSON.stringify(state, null, 2)
67
- // Truncate if too large
68
- const truncated = json.length > 5000 ? json.substring(0, 5000) + '\n... (truncated)' : json
69
- return `## 🌳 State Snapshot\n\n\`\`\`json\n${truncated}\n\`\`\`\n`
70
- } catch {
71
- return `## 🌳 State Snapshot\n\n\`[Could not serialize state]\`\n`
72
- }
73
- }
74
-
75
- export function composeBugReport(report: BugReport): string {
76
- const sections: string[] = []
77
-
78
- sections.push(`# 🐛 Bug Report: ${report.title}\n`)
79
- sections.push(`**Severity**: ${SEVERITY_LABELS[report.severity]}`)
80
- sections.push(`**Reported**: ${formatTimestamp(report.timestamp)}`)
81
- sections.push(`**Platform**: ${Platform.OS} ${Platform.Version}\n`)
82
-
83
- if (report.description) {
84
- sections.push(`## Description\n\n${report.description}\n`)
85
- }
86
-
87
- if (report.deviceInfo.length > 0) {
88
- sections.push(formatDeviceInfoForReport(report.deviceInfo))
89
- }
90
-
91
- if (report.logs.length > 0) {
92
- sections.push(formatLogsForReport(report.logs))
93
- }
94
-
95
- if (report.networkEntries.length > 0) {
96
- sections.push(formatNetworkForReport(report.networkEntries))
97
- }
98
-
99
- if (report.state && Object.keys(report.state).length > 0) {
100
- sections.push(formatStateForReport(report.state))
101
- }
102
-
103
- if (report.screenshotUri) {
104
- sections.push(`## 📸 Screenshot\n\nAttached: ${report.screenshotUri}\n`)
105
- }
106
-
107
- sections.push('---\n*Generated by rn-backstage*')
108
-
109
- return sections.join('\n')
110
- }
111
-
112
- // ─── Webhook Submission ──────────────────────────────────────────────────────
113
-
114
- export async function submitToWebhook(
115
- webhookUrl: string,
116
- report: BugReport,
117
- ): Promise<{ success: boolean; error?: string }> {
118
- try {
119
- const body: Record<string, unknown> = {
120
- title: report.title,
121
- description: report.description,
122
- severity: report.severity,
123
- platform: Platform.OS,
124
- platformVersion: String(Platform.Version),
125
- timestamp: report.timestamp,
126
- deviceInfo: report.deviceInfo,
127
- logs: report.logs.map(l => ({
128
- level: l.level,
129
- message: l.message,
130
- timestamp: l.timestamp,
131
- })),
132
- networkEntries: report.networkEntries.map(e => ({
133
- method: e.method,
134
- url: e.url,
135
- status: e.status,
136
- duration: e.duration,
137
- })),
138
- }
139
-
140
- if (report.state) {
141
- body.state = report.state
142
- }
143
-
144
- if (report.screenshotUri) {
145
- body.screenshotUri = report.screenshotUri
146
- }
147
-
148
- const response = await fetch(webhookUrl, {
149
- method: 'POST',
150
- headers: { 'Content-Type': 'application/json' },
151
- body: JSON.stringify(body),
152
- })
153
-
154
- if (!response.ok) {
155
- return { success: false, error: `HTTP ${response.status}` }
156
- }
157
-
158
- return { success: true }
159
- } catch (err) {
160
- return {
161
- success: false,
162
- error: err instanceof Error ? err.message : 'Unknown error',
163
- }
164
- }
165
- }
166
-
167
- // ─── Device Info Builder ─────────────────────────────────────────────────────
168
-
169
- export function buildDeviceInfo(
170
- appVersion?: string,
171
- buildNumber?: string,
172
- bundleId?: string,
173
- extraInfo: AppInfoItem[] = [],
174
- ): AppInfoItem[] {
175
- const info: AppInfoItem[] = [
176
- { label: 'Platform', value: `${Platform.OS} ${Platform.Version}` },
177
- ]
178
-
179
- if (appVersion) info.push({ label: 'App Version', value: appVersion })
180
- if (buildNumber) info.push({ label: 'Build Number', value: buildNumber })
181
- if (bundleId) info.push({ label: 'Bundle ID', value: bundleId })
182
-
183
- return [...info, ...extraInfo]
184
- }