UNPKG

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
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