react-native-debug-toolkit
Version:
A simple yet powerful debugging toolkit for React Native with a convenient floating UI for development
481 lines (444 loc) • 12.9 kB
JavaScript
import React, { useState, useCallback } from 'react'
import { View, Text, StyleSheet, Clipboard, Dimensions } from 'react-native'
import { ScrollView, Pressable } from 'react-native'
import JSONTree from 'react-native-json-tree'
const { width: SCREEN_WIDTH } = Dimensions.get('window')
const theme = {
scheme: 'monokai',
author: 'wimer hazenberg (http://www.monokai.nl)',
base00: '#272822',
base01: '#383830',
base02: '#49483e',
base03: '#75715e',
base04: '#a59f85',
base05: '#f8f8f2',
base06: '#f5f4f1',
base07: '#f9f8f5',
base08: '#f92672',
base09: '#fd971f',
base0A: '#f4bf75',
base0B: '#a6e22e',
base0C: '#a1efe4',
base0D: '#66d9ef',
base0E: '#ae81ff',
base0F: '#cc6633'
}
const CopyButton = ({ text, style }) => {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await Clipboard.setString(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<Pressable style={[styles.copyButton, style]} onPress={handleCopy}>
<Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text>
</Pressable>
)
}
const CollapsibleSection = ({ title, children, initiallyExpanded = false }) => {
const [expanded, setExpanded] = useState(initiallyExpanded)
return (
<View style={styles.collapsibleSection}>
<Pressable
style={styles.sectionHeader}
onPress={() => setExpanded(!expanded)}>
<Text style={styles.sectionTitle}>{title}</Text>
<Text style={styles.expandIcon}>{expanded ? '▼' : '▶'}</Text>
</Pressable>
{expanded && children}
</View>
)
}
const JSONValue = ({ value, path = '', level = 0, maxExpandLevel = 2 }) => {
if (value === null) {
return <Text style={styles.jsonNull}>null</Text>
}
if (value === undefined) {
return <Text style={styles.jsonNull}>undefined</Text>
}
if (typeof value === 'boolean') {
return <Text style={styles.jsonBoolean}>{value.toString()}</Text>
}
if (typeof value === 'number') {
return <Text style={styles.jsonNumber}>{value}</Text>
}
if (typeof value === 'string') {
return (
<Text style={styles.jsonString} selectable={true}>
{value}
</Text>
)
}
// For objects and arrays, use JSONTree for improved large data handling
if (typeof value === 'object') {
return (
<JSONTree
data={value}
theme={theme}
invertTheme={true}
hideRoot={true}
shouldExpandNode={(keyPath, nodeData, currentLevel) => currentLevel < maxExpandLevel}
/>
)
}
return <Text>{String(value)}</Text>
}
const EventTypeBadge = ({ eventName, entityType }) => {
const getEventColor = () => {
if (eventName?.includes('error')) return '#ff4444'
if (eventName?.includes('click')) return '#4CAF50'
if (eventName?.includes('view')) return '#2196F3'
if (eventName?.includes('search')) return '#FF9800'
return '#9C27B0'
}
return (
<View style={styles.badgeContainer}>
<Text style={[styles.eventBadge, { backgroundColor: getEventColor() }]}>
{eventName || 'Unknown Event'}
</Text>
{entityType && (
<Text style={[styles.entityBadge]}>
{entityType}
</Text>
)}
</View>
)
}
const TrackLogDetails = ({ log }) => {
const generateAnalyticsPayload = useCallback(() => {
if (!log) {
return 'No event data available'
}
const payload = {
eventName: log.eventName,
timestamp: log.timestamp,
...Object.fromEntries(
Object.entries(log).filter(([key, value]) =>
key !== 'timestamp' && value !== undefined && value !== null
)
)
}
return JSON.stringify(payload, null, 2)
}, [log])
if (!log) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Event data is missing</Text>
</View>
)
}
const coreProperties = {
eventName: log.eventName,
entityType: log.entityType,
entityName: log.entityName,
frontOperation: log.frontOperation
}
const identificationProperties = {
pageId: log.pageId,
objId: log.objId,
entityPath: log.entityPath,
objType: log.objType,
objPt: log.objPt
}
const locationProperties = {
refPageLocation: log.refPageLocation,
position: log.position,
entityLocation: log.entityLocation
}
const searchProperties = {
sessionId: log.sessionId,
requestId: log.requestId,
searchKeywored: log.searchKeywored,
enSearchKeywored: log.enSearchKeywored
}
const filterUndefinedProperties = (obj) => {
return Object.fromEntries(
Object.entries(obj).filter(([key, value]) => value !== undefined && value !== null && value !== '')
)
}
return (
<ScrollView
style={styles.container}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={true}
scrollEventThrottle={16}
keyboardShouldPersistTaps='handled'>
<View style={styles.header}>
<View style={styles.headerInfo}>
<EventTypeBadge eventName={log.eventName} entityType={log.entityType} />
{log.entityName && (
<Text style={styles.entityName} selectable={true}>
{log.entityName}
</Text>
)}
{log.frontOperation && (
<Text style={styles.operation}>
Operation: {log.frontOperation}
</Text>
)}
</View>
<CopyButton text={generateAnalyticsPayload()} />
</View>
<CollapsibleSection title='Core Properties' initiallyExpanded={true}>
<View style={styles.content}>
<View style={styles.propertiesGrid}>
{Object.entries(filterUndefinedProperties(coreProperties)).map(([key, value]) => (
<View key={key} style={styles.propertyRow}>
<Text style={styles.propertyKey}>{key}:</Text>
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
</View>
))}
</View>
</View>
</CollapsibleSection>
{Object.keys(filterUndefinedProperties(identificationProperties)).length > 0 && (
<CollapsibleSection title='Identification Properties'>
<View style={styles.content}>
<View style={styles.propertiesGrid}>
{Object.entries(filterUndefinedProperties(identificationProperties)).map(([key, value]) => (
<View key={key} style={styles.propertyRow}>
<Text style={styles.propertyKey}>{key}:</Text>
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
</View>
))}
</View>
</View>
</CollapsibleSection>
)}
{Object.keys(filterUndefinedProperties(locationProperties)).length > 0 && (
<CollapsibleSection title='Location Properties'>
<View style={styles.content}>
<View style={styles.propertiesGrid}>
{Object.entries(filterUndefinedProperties(locationProperties)).map(([key, value]) => (
<View key={key} style={styles.propertyRow}>
<Text style={styles.propertyKey}>{key}:</Text>
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
</View>
))}
</View>
</View>
</CollapsibleSection>
)}
{Object.keys(filterUndefinedProperties(searchProperties)).length > 0 && (
<CollapsibleSection title='Search Properties'>
<View style={styles.content}>
<View style={styles.propertiesGrid}>
{Object.entries(filterUndefinedProperties(searchProperties)).map(([key, value]) => (
<View key={key} style={styles.propertyRow}>
<Text style={styles.propertyKey}>{key}:</Text>
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
</View>
))}
</View>
</View>
</CollapsibleSection>
)}
<CollapsibleSection title='Complete Event Data'>
<View style={styles.content}>
<View style={styles.dataSectionHeader}>
<Text style={styles.dataSectionTitle}>Full JSON Payload</Text>
<CopyButton text={generateAnalyticsPayload()} />
</View>
<View style={styles.dataContentWrapper}>
<ScrollView
style={styles.dataContent}
nestedScrollEnabled={true}
bounces={false}
showsVerticalScrollIndicator={true}>
<JSONValue value={log} maxExpandLevel={2} />
</ScrollView>
</View>
</View>
</CollapsibleSection>
<CollapsibleSection title='Timing Information'>
<View style={styles.content}>
<View style={styles.propertyRow}>
<Text style={styles.propertyKey}>Timestamp:</Text>
<Text style={styles.propertyValue}>
{log.timestamp
? new Date(log.timestamp).toLocaleString()
: 'Unknown'}
</Text>
</View>
<View style={styles.propertyRow}>
<Text style={styles.propertyKey}>Time ago:</Text>
<Text style={styles.propertyValue}>
{log.timestamp
? getTimeAgo(log.timestamp)
: 'Unknown'}
</Text>
</View>
</View>
</CollapsibleSection>
</ScrollView>
)
}
const getTimeAgo = (timestamp) => {
const now = new Date()
const eventTime = new Date(timestamp)
const diffMs = now - eventTime
const diffSeconds = Math.floor(diffMs / 1000)
const diffMinutes = Math.floor(diffSeconds / 60)
const diffHours = Math.floor(diffMinutes / 60)
if (diffSeconds < 60) {
return `${diffSeconds} seconds ago`
} else if (diffMinutes < 60) {
return `${diffMinutes} minutes ago`
} else if (diffHours < 24) {
return `${diffHours} hours ago`
} else {
const diffDays = Math.floor(diffHours / 24)
return `${diffDays} days ago`
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
contentContainer: {
paddingBottom: 20,
},
header: {
flexDirection: 'row',
padding: 15,
borderBottomWidth: 1,
borderBottomColor: '#eee',
alignItems: 'flex-start',
},
headerInfo: {
flex: 1,
marginRight: 10,
},
badgeContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
},
eventBadge: {
paddingHorizontal: 12,
paddingVertical: 4,
borderRadius: 12,
color: 'white',
fontSize: 12,
fontWeight: 'bold',
marginRight: 8,
},
entityBadge: {
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 8,
backgroundColor: '#f0f0f0',
color: '#333',
fontSize: 11,
fontWeight: '500',
},
entityName: {
fontSize: 16,
color: '#333',
marginBottom: 4,
fontWeight: '600',
},
operation: {
fontSize: 14,
color: '#666',
fontStyle: 'italic',
},
collapsibleSection: {
marginBottom: 1,
backgroundColor: '#fff',
},
sectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 15,
backgroundColor: '#f5f5f5',
},
sectionTitle: {
fontSize: 15,
fontWeight: 'bold',
color: '#333',
},
expandIcon: {
fontSize: 14,
color: '#666',
},
content: {
padding: 15,
},
propertiesGrid: {
gap: 8,
},
propertyRow: {
marginBottom: 8,
},
propertyKey: {
fontSize: 14,
fontWeight: 'bold',
color: '#666',
marginBottom: 2,
},
propertyValue: {
fontSize: 14,
color: '#333',
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
},
errorText: {
color: '#ff4444',
fontSize: 16,
},
dataSectionHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 8,
},
dataSectionTitle: {
fontSize: 14,
fontWeight: 'bold',
color: '#666',
},
dataContentWrapper: {
flex: 1,
},
dataContent: {
flex: 1,
backgroundColor: '#f8f9fa',
padding: 10,
borderRadius: 4,
borderWidth: 1,
borderColor: '#e9ecef',
},
copyButton: {
backgroundColor: '#e9ecef',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 4,
},
copyButtonText: {
fontSize: 12,
color: '#666',
},
jsonString: {
color: '#CB772F',
},
jsonNumber: {
color: '#2878D0',
},
jsonBoolean: {
color: '#2878D0',
fontWeight: 'bold',
},
jsonNull: {
color: '#A0A0A0',
fontStyle: 'italic',
},
})
export default TrackLogDetails