rn-backstage 1.4.3 â 1.4.4
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 +1 -3
- package/package.json +3 -3
- 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/README.md
CHANGED
|
@@ -104,7 +104,6 @@ Pass a `storageAdapter` to render a **Storage** tab that lets you browse, edit,
|
|
|
104
104
|
|
|
105
105
|
```tsx
|
|
106
106
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
107
|
-
|
|
108
107
|
;<Backstage
|
|
109
108
|
storageAdapter={{
|
|
110
109
|
getAllKeys: () => AsyncStorage.getAllKeys(),
|
|
@@ -119,7 +118,6 @@ MMKV example:
|
|
|
119
118
|
|
|
120
119
|
```tsx
|
|
121
120
|
import { storage } from './mmkv'
|
|
122
|
-
|
|
123
121
|
;<Backstage
|
|
124
122
|
storageAdapter={{
|
|
125
123
|
getAllKeys: () => Promise.resolve(storage.getAllKeys()),
|
|
@@ -181,7 +179,7 @@ Add a `bugReport` config to enable one-tap bug reporting. Tapping the đ butto
|
|
|
181
179
|
| `pillWidth` | `number` | `60` | Width of the floating pill |
|
|
182
180
|
| `pillHeight` | `number` | `32` | Height of the floating pill |
|
|
183
181
|
| `extraTabs` | `BackstageTab[]` | `[]` | Additional custom tabs |
|
|
184
|
-
| `bugReport` | `BugReportConfig` | `undefined` | Bug report config (shows đ button in header)
|
|
182
|
+
| `bugReport` | `BugReportConfig` | `undefined` | Bug report config (shows đ button in header) |
|
|
185
183
|
| `styles` | `BackstageStyleOverrides` | `undefined` | Custom style overrides |
|
|
186
184
|
| `children` | `ReactNode` | `undefined` | Extra content in InfoTab |
|
|
187
185
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rn-backstage",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
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": "
|
|
8
|
+
"react-native": "src/index",
|
|
9
9
|
"source": "src/index",
|
|
10
10
|
"files": [
|
|
11
|
+
"src",
|
|
11
12
|
"lib",
|
|
12
|
-
"README.md",
|
|
13
13
|
"!**/__tests__",
|
|
14
14
|
"!**/__fixtures__",
|
|
15
15
|
"!**/__mocks__",
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
}
|