react-native-debug-toolkit 0.5.3 → 0.6.1
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/index.js +4 -1
- package/lib/features/ConsoleLogFeature.js +0 -3
- package/lib/features/NavigationLogFeature.js +0 -3
- package/lib/features/NetworkFeature.js +0 -9
- package/lib/features/PerformanceFeature.js +6 -6
- package/lib/features/ThirdPartyLibsFeature.js +1 -3
- package/lib/features/TrackFeature.js +94 -0
- package/lib/features/ZustandLogFeature.js +1 -3
- package/lib/hooks/useNavigationLogger.js +1 -1
- package/lib/index.js +5 -1
- package/lib/types/TrackTypes.ts +92 -0
- package/lib/views/FloatPanelView.js +6 -0
- package/lib/views/SubViewTrackLogs.js +318 -0
- package/lib/views/TrackLogDetails.js +481 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createConsoleLogFeature } from './lib/features/ConsoleLogFeature'
|
|
|
11
11
|
import { createZustandLogFeature, addZustandLog } from './lib/features/ZustandLogFeature'
|
|
12
12
|
import { createNavigationLogFeature, addNavigationLog } from './lib/features/NavigationLogFeature'
|
|
13
13
|
import { useNavigationLogger } from './lib/hooks/useNavigationLogger'
|
|
14
|
+
import { createTrackFeature, addTrackLog } from './lib/features/TrackFeature'
|
|
14
15
|
|
|
15
16
|
// Function to clear all logs and close the panel
|
|
16
17
|
const clearAll = () => {
|
|
@@ -30,9 +31,11 @@ export {
|
|
|
30
31
|
addZustandLog,
|
|
31
32
|
createNavigationLogFeature,
|
|
32
33
|
addNavigationLog,
|
|
34
|
+
addTrackLog,
|
|
33
35
|
useNavigationLogger,
|
|
34
36
|
isDebugMode,
|
|
35
|
-
clearAll
|
|
37
|
+
clearAll,
|
|
38
|
+
createTrackFeature
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export default DebugToolKit
|
|
@@ -3,9 +3,6 @@ const logs = [];
|
|
|
3
3
|
const originalNavigationMethods = {}; // Store original navigation methods
|
|
4
4
|
|
|
5
5
|
const setup = () => {
|
|
6
|
-
if (!__DEV__) {
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
6
|
// Note: Actual navigation interception will be done in the main app code
|
|
10
7
|
// This is just a setup function for compatibility with the feature API
|
|
11
8
|
};
|
|
@@ -18,9 +18,6 @@ class NetworkFeature {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
setup() {
|
|
21
|
-
if (!__DEV__) {
|
|
22
|
-
return
|
|
23
|
-
}
|
|
24
21
|
|
|
25
22
|
// this._interceptFetch()
|
|
26
23
|
return this
|
|
@@ -74,9 +71,6 @@ class NetworkFeature {
|
|
|
74
71
|
}
|
|
75
72
|
|
|
76
73
|
setupAxiosInterceptors(axiosInstance) {
|
|
77
|
-
if (!__DEV__) {
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
74
|
|
|
81
75
|
axiosInstance.interceptors.request.use(
|
|
82
76
|
(config) => {
|
|
@@ -233,9 +227,6 @@ class NetworkFeature {
|
|
|
233
227
|
}
|
|
234
228
|
|
|
235
229
|
_interceptFetch() {
|
|
236
|
-
if (!__DEV__) {
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
230
|
|
|
240
231
|
this.originalFetch = global.fetch
|
|
241
232
|
|
|
@@ -40,7 +40,7 @@ class PerformanceFeature {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
setup(options = {}) {
|
|
43
|
-
if (
|
|
43
|
+
if (this.isSetup) {
|
|
44
44
|
return this;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -207,7 +207,7 @@ class PerformanceFeature {
|
|
|
207
207
|
|
|
208
208
|
// Mark events (following pattern from vanilla example)
|
|
209
209
|
mark(name, detail = {}) {
|
|
210
|
-
if (!
|
|
210
|
+
if (!this.isSetup) return this;
|
|
211
211
|
|
|
212
212
|
if (typeof detail !== 'object') {
|
|
213
213
|
detail = { value: detail };
|
|
@@ -218,7 +218,7 @@ class PerformanceFeature {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
measure(name, startMarkOrOptions, endMark, detail = {}) {
|
|
221
|
-
if (!
|
|
221
|
+
if (!this.isSetup) return this;
|
|
222
222
|
|
|
223
223
|
try {
|
|
224
224
|
if (typeof startMarkOrOptions === 'string' && endMark) {
|
|
@@ -236,7 +236,7 @@ class PerformanceFeature {
|
|
|
236
236
|
|
|
237
237
|
// Record custom metrics (following pattern from vanilla example)
|
|
238
238
|
metric(name, value, detail = {}) {
|
|
239
|
-
if (!
|
|
239
|
+
if (!this.isSetup) return this;
|
|
240
240
|
|
|
241
241
|
if (typeof detail !== 'object') {
|
|
242
242
|
detail = { info: detail };
|
|
@@ -248,7 +248,7 @@ class PerformanceFeature {
|
|
|
248
248
|
|
|
249
249
|
// Measure execution time of a function
|
|
250
250
|
measureFunction(name, fn, ...args) {
|
|
251
|
-
if (!
|
|
251
|
+
if (!this.isSetup) {
|
|
252
252
|
return fn(...args);
|
|
253
253
|
}
|
|
254
254
|
|
|
@@ -310,7 +310,7 @@ class PerformanceFeature {
|
|
|
310
310
|
|
|
311
311
|
// Trigger a test network request (for demo purposes)
|
|
312
312
|
generateTestNetworkRequest() {
|
|
313
|
-
if (!
|
|
313
|
+
if (!this.isSetup || !this.resourceLoggingEnabled) return this;
|
|
314
314
|
|
|
315
315
|
// Similar to the fetch in the vanilla example
|
|
316
316
|
fetch('https://jsonplaceholder.typicode.com/todos/1', { cache: 'no-cache' })
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const MAX_LOGS = 200; // Max number of track events to store
|
|
2
|
+
const logs = [];
|
|
3
|
+
|
|
4
|
+
const setup = () => {
|
|
5
|
+
|
|
6
|
+
// Note: Actual event tracking will be done in the main app code
|
|
7
|
+
// This is just a setup function for compatibility with the feature API
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Function to add track event log
|
|
11
|
+
export const addTrackLog = (eventData) => {
|
|
12
|
+
|
|
13
|
+
// Store log data
|
|
14
|
+
const logEntry = {
|
|
15
|
+
timestamp: new Date(),
|
|
16
|
+
eventName: eventData.eventName || 'unknown_event',
|
|
17
|
+
entityType: eventData.entityType,
|
|
18
|
+
entityName: eventData.entityName,
|
|
19
|
+
pageId: eventData.pageId,
|
|
20
|
+
objId: eventData.objId,
|
|
21
|
+
entityPath: eventData.entityPath,
|
|
22
|
+
objType: eventData.objType,
|
|
23
|
+
objPt: eventData.objPt,
|
|
24
|
+
refPageLocation: eventData.refPageLocation,
|
|
25
|
+
position: eventData.position,
|
|
26
|
+
entityLocation: eventData.entityLocation,
|
|
27
|
+
frontOperation: eventData.frontOperation,
|
|
28
|
+
sessionId: eventData.sessionId,
|
|
29
|
+
requestId: eventData.requestId,
|
|
30
|
+
searchKeywored: eventData.searchKeywored,
|
|
31
|
+
enSearchKeywored: eventData.enSearchKeywored,
|
|
32
|
+
// Include any additional custom properties
|
|
33
|
+
...eventData
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
logs.push(logEntry);
|
|
37
|
+
|
|
38
|
+
// Trim logs if they exceed the maximum limit
|
|
39
|
+
if (logs.length > MAX_LOGS) {
|
|
40
|
+
logs.splice(0, logs.length - MAX_LOGS);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Get event statistics
|
|
45
|
+
export const getEventStats = () => {
|
|
46
|
+
const stats = {};
|
|
47
|
+
logs.forEach(log => {
|
|
48
|
+
const eventName = log.eventName;
|
|
49
|
+
if (!stats[eventName]) {
|
|
50
|
+
stats[eventName] = {
|
|
51
|
+
count: 0,
|
|
52
|
+
lastSeen: log.timestamp,
|
|
53
|
+
entityTypes: new Set(),
|
|
54
|
+
operations: new Set()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
stats[eventName].count++;
|
|
58
|
+
if (log.timestamp > stats[eventName].lastSeen) {
|
|
59
|
+
stats[eventName].lastSeen = log.timestamp;
|
|
60
|
+
}
|
|
61
|
+
if (log.entityType) {
|
|
62
|
+
stats[eventName].entityTypes.add(log.entityType);
|
|
63
|
+
}
|
|
64
|
+
if (log.frontOperation) {
|
|
65
|
+
stats[eventName].operations.add(log.frontOperation);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Convert Sets to Arrays for display
|
|
70
|
+
Object.keys(stats).forEach(eventName => {
|
|
71
|
+
stats[eventName].entityTypes = Array.from(stats[eventName].entityTypes);
|
|
72
|
+
stats[eventName].operations = Array.from(stats[eventName].operations);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return stats;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const getData = () => {
|
|
79
|
+
return logs;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const cleanup = () => {
|
|
83
|
+
logs.length = 0; // Clear array
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const createTrackFeature = () => {
|
|
87
|
+
return {
|
|
88
|
+
name: 'track',
|
|
89
|
+
label: 'Track Events',
|
|
90
|
+
setup: setup,
|
|
91
|
+
getData: getData,
|
|
92
|
+
cleanup: cleanup,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
@@ -20,9 +20,7 @@ export const addZustandLog = (action, prevState, nextState, actionCompleteTime,
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
const setup = () => {
|
|
23
|
-
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
23
|
+
|
|
26
24
|
// Note: The actual middleware setup happens in the store creation
|
|
27
25
|
// This function is mainly for compatibility with the feature API
|
|
28
26
|
};
|
package/lib/index.js
CHANGED
|
@@ -5,11 +5,13 @@ import { createConsoleLogFeature } from './features/ConsoleLogFeature'
|
|
|
5
5
|
import { createZustandLogFeature, zustandLogMiddleware } from './features/ZustandLogFeature'
|
|
6
6
|
import { createNavigationLogFeature, addNavigationLog } from './features/NavigationLogFeature'
|
|
7
7
|
import { createThirdPartyLibsFeature } from './features/ThirdPartyLibsFeature'
|
|
8
|
+
import { createTrackFeature, addTrackLog } from './features/TrackFeature'
|
|
8
9
|
import NativeDebugLibs from './NativeDebugLibs'
|
|
9
10
|
|
|
10
11
|
// Registry mapping feature names (strings) to their creator functions
|
|
11
12
|
const featureRegistry = {
|
|
12
13
|
network: createNetworkFeature,
|
|
14
|
+
track: createTrackFeature,
|
|
13
15
|
console: createConsoleLogFeature,
|
|
14
16
|
// performance: createPerformanceFeature,
|
|
15
17
|
zustand: createZustandLogFeature,
|
|
@@ -20,7 +22,7 @@ const featureRegistry = {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
// List of default features (can use strings now)
|
|
23
|
-
const defaultFeatures = ['network', '
|
|
25
|
+
const defaultFeatures = ['network', 'track', 'navigation','zustand', 'thirdPartyLibs', 'console']
|
|
24
26
|
|
|
25
27
|
// Export the debug mode status - this is used by the app
|
|
26
28
|
// const isDebugMode = NativeDebugLibs.isDebugBuild();
|
|
@@ -104,6 +106,8 @@ export {
|
|
|
104
106
|
createNavigationLogFeature,
|
|
105
107
|
createThirdPartyLibsFeature,
|
|
106
108
|
addNavigationLog,
|
|
109
|
+
createTrackFeature,
|
|
110
|
+
addTrackLog,
|
|
107
111
|
zustandLogMiddleware, // Export middleware for use in Zustand stores
|
|
108
112
|
featureRegistry,
|
|
109
113
|
isDebugMode
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Define the entity types and names as constants for better type safety
|
|
2
|
+
export const EntityType = {
|
|
3
|
+
mine_module: 'mine_module',
|
|
4
|
+
search_module: 'search_module',
|
|
5
|
+
page_view: 'page_view',
|
|
6
|
+
error_module: 'error_module',
|
|
7
|
+
navigation: 'navigation',
|
|
8
|
+
content: 'content',
|
|
9
|
+
user_action: 'user_action',
|
|
10
|
+
} as const
|
|
11
|
+
|
|
12
|
+
export const EntityName = {
|
|
13
|
+
MINE_POST: 'MINE_POST',
|
|
14
|
+
HOME_PAGE: 'HOME_PAGE',
|
|
15
|
+
SEARCH_BAR: 'SEARCH_BAR',
|
|
16
|
+
PROFILE_PAGE: 'PROFILE_PAGE',
|
|
17
|
+
NAVIGATION_TAB: 'NAVIGATION_TAB',
|
|
18
|
+
CONTENT_ITEM: 'CONTENT_ITEM',
|
|
19
|
+
USER_BUTTON: 'USER_BUTTON',
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
export type TrackEventOptions = {
|
|
23
|
+
entityType: (typeof EntityType)[keyof typeof EntityType]
|
|
24
|
+
entityName?: (typeof EntityName)[keyof typeof EntityName]
|
|
25
|
+
pageId?: string
|
|
26
|
+
objId?: string
|
|
27
|
+
entityPath?: string
|
|
28
|
+
objType?: number
|
|
29
|
+
objPt?: string // 瀑布流作品pt
|
|
30
|
+
refPageLocation?: string
|
|
31
|
+
position?: string
|
|
32
|
+
entityLocation?: string
|
|
33
|
+
frontOperation?: string
|
|
34
|
+
sessionId?: string //搜索模块需要
|
|
35
|
+
requestId?: string //搜索模块需要
|
|
36
|
+
searchKeywored?: string //搜索模块需要
|
|
37
|
+
enSearchKeywored?: string //搜索模块需要
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Complete event structure that includes the event name and all tracking options
|
|
41
|
+
export type TrackEvent = {
|
|
42
|
+
eventName: string
|
|
43
|
+
timestamp: Date
|
|
44
|
+
} & TrackEventOptions
|
|
45
|
+
|
|
46
|
+
// Track feature interface
|
|
47
|
+
export interface ITrackFeature {
|
|
48
|
+
setup(): ITrackFeature
|
|
49
|
+
getData(): TrackEvent[]
|
|
50
|
+
cleanup(): void
|
|
51
|
+
logEvent(eventData: TrackEventOptions & { eventName: string }): void
|
|
52
|
+
getEventStats(): EventStats
|
|
53
|
+
addEventToBlacklist(pattern: string | RegExp): void
|
|
54
|
+
removeEventFromBlacklist(pattern: string | RegExp): void
|
|
55
|
+
clearBlacklist(): void
|
|
56
|
+
getBlacklist(): (string | RegExp)[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Event statistics structure
|
|
60
|
+
export type EventStats = {
|
|
61
|
+
[eventName: string]: {
|
|
62
|
+
count: number
|
|
63
|
+
lastSeen: Date
|
|
64
|
+
entityTypes: string[]
|
|
65
|
+
operations: string[]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Common event names used in the application
|
|
70
|
+
export const CommonEventNames = {
|
|
71
|
+
LOG_ACTION_FRONT: 'log_action_front',
|
|
72
|
+
LOG_ERROR_FRONT: 'log_error_front',
|
|
73
|
+
LOG_VIEW_FRONT: 'log_view_front',
|
|
74
|
+
LOG_SEARCH_FRONT: 'log_search_front',
|
|
75
|
+
LOG_NAVIGATION_FRONT: 'log_navigation_front',
|
|
76
|
+
} as const
|
|
77
|
+
|
|
78
|
+
// Common front operations
|
|
79
|
+
export const FrontOperations = {
|
|
80
|
+
CLICK: 'click',
|
|
81
|
+
VIEW: 'view',
|
|
82
|
+
SEARCH: 'search',
|
|
83
|
+
SWIPE: 'swipe',
|
|
84
|
+
SCROLL: 'scroll',
|
|
85
|
+
ERROR: 'error',
|
|
86
|
+
NAVIGATE: 'navigate',
|
|
87
|
+
} as const
|
|
88
|
+
|
|
89
|
+
// Helper type for creating events with better type safety
|
|
90
|
+
export type CreateTrackEvent<T extends keyof typeof CommonEventNames> = {
|
|
91
|
+
eventName: (typeof CommonEventNames)[T]
|
|
92
|
+
} & TrackEventOptions
|
|
@@ -21,6 +21,8 @@ import SubViewConsoleLogs from './SubViewConsoleLogs'
|
|
|
21
21
|
import SubViewZustandLogs from './SubViewZustandLogs'
|
|
22
22
|
import SubViewNavigationLogs from './SubViewNavigationLogs'
|
|
23
23
|
import SubViewThirdPartyLibs from './SubViewThirdPartyLibs'
|
|
24
|
+
import SubViewTrackLogs from './SubViewTrackLogs'
|
|
25
|
+
|
|
24
26
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
|
|
25
27
|
|
|
26
28
|
export default class FloatPanelView extends Component {
|
|
@@ -320,6 +322,10 @@ export default class FloatPanelView extends Component {
|
|
|
320
322
|
if (feature.name === 'navigation') {
|
|
321
323
|
return <SubViewNavigationLogs logs={data} />
|
|
322
324
|
}
|
|
325
|
+
|
|
326
|
+
if (feature.name === 'track') {
|
|
327
|
+
return <SubViewTrackLogs logs={data} />
|
|
328
|
+
}
|
|
323
329
|
|
|
324
330
|
if (feature.name === 'thirdPartyLibs') {
|
|
325
331
|
return <SubViewThirdPartyLibs libraries={data} />
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
FlatList,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
} from 'react-native'
|
|
9
|
+
import TrackLogDetails from './TrackLogDetails'
|
|
10
|
+
|
|
11
|
+
const SubViewTrackLogs = ({ logs = [] }) => {
|
|
12
|
+
const [selectedLog, setSelectedLog] = useState(null)
|
|
13
|
+
|
|
14
|
+
const getEventColor = (eventName) => {
|
|
15
|
+
if (eventName?.includes('error')) return '#ff4444'
|
|
16
|
+
if (eventName?.includes('click')) return '#4CAF50'
|
|
17
|
+
if (eventName?.includes('view')) return '#2196F3'
|
|
18
|
+
if (eventName?.includes('search')) return '#FF9800'
|
|
19
|
+
return '#9C27B0'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const getTimeAgo = (timestamp) => {
|
|
23
|
+
const now = new Date()
|
|
24
|
+
const eventTime = new Date(timestamp)
|
|
25
|
+
const diffMs = now - eventTime
|
|
26
|
+
const diffSeconds = Math.floor(diffMs / 1000)
|
|
27
|
+
const diffMinutes = Math.floor(diffSeconds / 60)
|
|
28
|
+
const diffHours = Math.floor(diffMinutes / 60)
|
|
29
|
+
|
|
30
|
+
if (diffSeconds < 60) {
|
|
31
|
+
return `${diffSeconds}s ago`
|
|
32
|
+
} else if (diffMinutes < 60) {
|
|
33
|
+
return `${diffMinutes}m ago`
|
|
34
|
+
} else if (diffHours < 24) {
|
|
35
|
+
return `${diffHours}h ago`
|
|
36
|
+
} else {
|
|
37
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
38
|
+
return `${diffDays}d ago`
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const renderLogItem = ({ item }) => {
|
|
43
|
+
const eventColor = getEventColor(item.eventName)
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
style={styles.logItem}
|
|
48
|
+
onPress={() => setSelectedLog(item)}>
|
|
49
|
+
<View style={styles.logItemContainer}>
|
|
50
|
+
<View
|
|
51
|
+
style={[styles.eventIndicator, { backgroundColor: eventColor }]}
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<View style={styles.logContent}>
|
|
55
|
+
<View style={styles.eventRow}>
|
|
56
|
+
<Text style={[styles.eventName, { color: eventColor }]}>
|
|
57
|
+
{item.eventName || 'Unknown Event'}
|
|
58
|
+
</Text>
|
|
59
|
+
{item.entityType && (
|
|
60
|
+
<Text style={styles.entityType}>
|
|
61
|
+
{item.entityType}
|
|
62
|
+
</Text>
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
65
|
+
|
|
66
|
+
{item.entityName && (
|
|
67
|
+
<Text style={styles.entityName} numberOfLines={1}>
|
|
68
|
+
{item.entityName}
|
|
69
|
+
</Text>
|
|
70
|
+
)}
|
|
71
|
+
|
|
72
|
+
{item.frontOperation && (
|
|
73
|
+
<Text style={styles.operation} numberOfLines={1}>
|
|
74
|
+
Operation: {item.frontOperation}
|
|
75
|
+
</Text>
|
|
76
|
+
)}
|
|
77
|
+
|
|
78
|
+
<View style={styles.logFooter}>
|
|
79
|
+
<Text style={styles.time}>
|
|
80
|
+
{item.timestamp
|
|
81
|
+
? getTimeAgo(item.timestamp)
|
|
82
|
+
: 'Unknown time'}
|
|
83
|
+
</Text>
|
|
84
|
+
|
|
85
|
+
<View style={styles.additionalInfo}>
|
|
86
|
+
{item.pageId && (
|
|
87
|
+
<Text style={styles.pageId} numberOfLines={1}>
|
|
88
|
+
Page: {item.pageId}
|
|
89
|
+
</Text>
|
|
90
|
+
)}
|
|
91
|
+
{item.position && (
|
|
92
|
+
<Text style={styles.position}>
|
|
93
|
+
Pos: {item.position}
|
|
94
|
+
</Text>
|
|
95
|
+
)}
|
|
96
|
+
</View>
|
|
97
|
+
</View>
|
|
98
|
+
</View>
|
|
99
|
+
</View>
|
|
100
|
+
</TouchableOpacity>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If a log is selected, show the details view with a back button
|
|
105
|
+
if (selectedLog) {
|
|
106
|
+
return (
|
|
107
|
+
<View style={styles.container}>
|
|
108
|
+
<View style={styles.detailsHeader}>
|
|
109
|
+
<TouchableOpacity
|
|
110
|
+
style={styles.backButton}
|
|
111
|
+
onPress={() => setSelectedLog(null)}>
|
|
112
|
+
<Text style={styles.backButtonText}>← Back</Text>
|
|
113
|
+
</TouchableOpacity>
|
|
114
|
+
<View style={styles.headerEventInfo}>
|
|
115
|
+
<Text
|
|
116
|
+
style={[
|
|
117
|
+
styles.headerEvent,
|
|
118
|
+
{ color: getEventColor(selectedLog.eventName) },
|
|
119
|
+
]}>
|
|
120
|
+
{selectedLog.eventName || 'Unknown Event'}
|
|
121
|
+
</Text>
|
|
122
|
+
{selectedLog.entityType && (
|
|
123
|
+
<Text style={styles.headerEntityType}>
|
|
124
|
+
{selectedLog.entityType}
|
|
125
|
+
</Text>
|
|
126
|
+
)}
|
|
127
|
+
</View>
|
|
128
|
+
</View>
|
|
129
|
+
<TrackLogDetails log={selectedLog} />
|
|
130
|
+
</View>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Otherwise show the list view
|
|
135
|
+
return (
|
|
136
|
+
<View style={styles.container}>
|
|
137
|
+
{logs.length === 0 ? (
|
|
138
|
+
<View style={styles.emptyContainer}>
|
|
139
|
+
<Text style={styles.emptyText}>No track events logged yet</Text>
|
|
140
|
+
<Text style={styles.emptySubtext}>
|
|
141
|
+
Track events will appear here as they occur during development
|
|
142
|
+
</Text>
|
|
143
|
+
</View>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<View style={styles.statsContainer}>
|
|
147
|
+
<Text style={styles.statsText}>
|
|
148
|
+
{logs.length} events logged
|
|
149
|
+
</Text>
|
|
150
|
+
</View>
|
|
151
|
+
<FlatList
|
|
152
|
+
data={[...logs].sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))}
|
|
153
|
+
renderItem={renderLogItem}
|
|
154
|
+
keyExtractor={(item, index) => `${item.timestamp}-${index}`}
|
|
155
|
+
style={styles.list}
|
|
156
|
+
showsVerticalScrollIndicator={true}
|
|
157
|
+
/>
|
|
158
|
+
</>
|
|
159
|
+
)}
|
|
160
|
+
</View>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const styles = StyleSheet.create({
|
|
165
|
+
container: {
|
|
166
|
+
flex: 1,
|
|
167
|
+
backgroundColor: '#fff',
|
|
168
|
+
},
|
|
169
|
+
list: {
|
|
170
|
+
flex: 1,
|
|
171
|
+
},
|
|
172
|
+
emptyContainer: {
|
|
173
|
+
flex: 1,
|
|
174
|
+
justifyContent: 'center',
|
|
175
|
+
alignItems: 'center',
|
|
176
|
+
padding: 40,
|
|
177
|
+
},
|
|
178
|
+
emptyText: {
|
|
179
|
+
textAlign: 'center',
|
|
180
|
+
color: '#666',
|
|
181
|
+
fontSize: 16,
|
|
182
|
+
fontWeight: '500',
|
|
183
|
+
marginBottom: 8,
|
|
184
|
+
},
|
|
185
|
+
emptySubtext: {
|
|
186
|
+
textAlign: 'center',
|
|
187
|
+
color: '#999',
|
|
188
|
+
fontSize: 14,
|
|
189
|
+
},
|
|
190
|
+
statsContainer: {
|
|
191
|
+
padding: 12,
|
|
192
|
+
borderBottomWidth: 1,
|
|
193
|
+
borderBottomColor: '#eee',
|
|
194
|
+
backgroundColor: '#f8f9fa',
|
|
195
|
+
},
|
|
196
|
+
statsText: {
|
|
197
|
+
fontSize: 12,
|
|
198
|
+
color: '#666',
|
|
199
|
+
fontWeight: '500',
|
|
200
|
+
},
|
|
201
|
+
logItem: {
|
|
202
|
+
borderBottomWidth: 1,
|
|
203
|
+
borderBottomColor: '#eee',
|
|
204
|
+
},
|
|
205
|
+
logItemContainer: {
|
|
206
|
+
flexDirection: 'row',
|
|
207
|
+
padding: 12,
|
|
208
|
+
},
|
|
209
|
+
eventIndicator: {
|
|
210
|
+
width: 4,
|
|
211
|
+
borderRadius: 2,
|
|
212
|
+
marginRight: 12,
|
|
213
|
+
},
|
|
214
|
+
logContent: {
|
|
215
|
+
flex: 1,
|
|
216
|
+
},
|
|
217
|
+
eventRow: {
|
|
218
|
+
flexDirection: 'row',
|
|
219
|
+
alignItems: 'center',
|
|
220
|
+
marginBottom: 4,
|
|
221
|
+
},
|
|
222
|
+
eventName: {
|
|
223
|
+
fontSize: 15,
|
|
224
|
+
fontWeight: 'bold',
|
|
225
|
+
marginRight: 8,
|
|
226
|
+
},
|
|
227
|
+
entityType: {
|
|
228
|
+
fontSize: 11,
|
|
229
|
+
backgroundColor: '#f0f0f0',
|
|
230
|
+
color: '#666',
|
|
231
|
+
paddingHorizontal: 6,
|
|
232
|
+
paddingVertical: 2,
|
|
233
|
+
borderRadius: 6,
|
|
234
|
+
fontWeight: '500',
|
|
235
|
+
},
|
|
236
|
+
entityName: {
|
|
237
|
+
fontSize: 14,
|
|
238
|
+
color: '#333',
|
|
239
|
+
marginBottom: 4,
|
|
240
|
+
fontWeight: '500',
|
|
241
|
+
},
|
|
242
|
+
operation: {
|
|
243
|
+
fontSize: 13,
|
|
244
|
+
color: '#666',
|
|
245
|
+
marginBottom: 6,
|
|
246
|
+
fontStyle: 'italic',
|
|
247
|
+
},
|
|
248
|
+
logFooter: {
|
|
249
|
+
flexDirection: 'row',
|
|
250
|
+
justifyContent: 'space-between',
|
|
251
|
+
alignItems: 'flex-end',
|
|
252
|
+
},
|
|
253
|
+
time: {
|
|
254
|
+
fontSize: 12,
|
|
255
|
+
color: '#999',
|
|
256
|
+
},
|
|
257
|
+
additionalInfo: {
|
|
258
|
+
flexDirection: 'row',
|
|
259
|
+
alignItems: 'center',
|
|
260
|
+
gap: 8,
|
|
261
|
+
},
|
|
262
|
+
pageId: {
|
|
263
|
+
fontSize: 11,
|
|
264
|
+
color: '#666',
|
|
265
|
+
backgroundColor: '#f8f9fa',
|
|
266
|
+
paddingHorizontal: 4,
|
|
267
|
+
paddingVertical: 1,
|
|
268
|
+
borderRadius: 3,
|
|
269
|
+
maxWidth: 80,
|
|
270
|
+
},
|
|
271
|
+
position: {
|
|
272
|
+
fontSize: 11,
|
|
273
|
+
color: '#666',
|
|
274
|
+
backgroundColor: '#f8f9fa',
|
|
275
|
+
paddingHorizontal: 4,
|
|
276
|
+
paddingVertical: 1,
|
|
277
|
+
borderRadius: 3,
|
|
278
|
+
},
|
|
279
|
+
detailsHeader: {
|
|
280
|
+
flexDirection: 'row',
|
|
281
|
+
alignItems: 'center',
|
|
282
|
+
padding: 15,
|
|
283
|
+
borderBottomWidth: 1,
|
|
284
|
+
borderBottomColor: '#eee',
|
|
285
|
+
backgroundColor: '#f8f9fa',
|
|
286
|
+
},
|
|
287
|
+
backButton: {
|
|
288
|
+
paddingHorizontal: 10,
|
|
289
|
+
paddingVertical: 5,
|
|
290
|
+
borderRadius: 4,
|
|
291
|
+
backgroundColor: '#eee',
|
|
292
|
+
marginRight: 10,
|
|
293
|
+
},
|
|
294
|
+
backButtonText: {
|
|
295
|
+
color: '#333',
|
|
296
|
+
fontWeight: '500',
|
|
297
|
+
},
|
|
298
|
+
headerEventInfo: {
|
|
299
|
+
flexDirection: 'row',
|
|
300
|
+
alignItems: 'center',
|
|
301
|
+
},
|
|
302
|
+
headerEvent: {
|
|
303
|
+
fontSize: 16,
|
|
304
|
+
fontWeight: 'bold',
|
|
305
|
+
marginRight: 10,
|
|
306
|
+
},
|
|
307
|
+
headerEntityType: {
|
|
308
|
+
fontSize: 12,
|
|
309
|
+
backgroundColor: '#f0f0f0',
|
|
310
|
+
color: '#666',
|
|
311
|
+
paddingHorizontal: 8,
|
|
312
|
+
paddingVertical: 3,
|
|
313
|
+
borderRadius: 8,
|
|
314
|
+
fontWeight: '500',
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
export default SubViewTrackLogs
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react'
|
|
2
|
+
import { View, Text, StyleSheet, Clipboard, Dimensions } from 'react-native'
|
|
3
|
+
import { ScrollView, Pressable } from 'react-native'
|
|
4
|
+
import JSONTree from 'react-native-json-tree'
|
|
5
|
+
|
|
6
|
+
const { width: SCREEN_WIDTH } = Dimensions.get('window')
|
|
7
|
+
|
|
8
|
+
const theme = {
|
|
9
|
+
scheme: 'monokai',
|
|
10
|
+
author: 'wimer hazenberg (http://www.monokai.nl)',
|
|
11
|
+
base00: '#272822',
|
|
12
|
+
base01: '#383830',
|
|
13
|
+
base02: '#49483e',
|
|
14
|
+
base03: '#75715e',
|
|
15
|
+
base04: '#a59f85',
|
|
16
|
+
base05: '#f8f8f2',
|
|
17
|
+
base06: '#f5f4f1',
|
|
18
|
+
base07: '#f9f8f5',
|
|
19
|
+
base08: '#f92672',
|
|
20
|
+
base09: '#fd971f',
|
|
21
|
+
base0A: '#f4bf75',
|
|
22
|
+
base0B: '#a6e22e',
|
|
23
|
+
base0C: '#a1efe4',
|
|
24
|
+
base0D: '#66d9ef',
|
|
25
|
+
base0E: '#ae81ff',
|
|
26
|
+
base0F: '#cc6633'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CopyButton = ({ text, style }) => {
|
|
30
|
+
const [copied, setCopied] = useState(false)
|
|
31
|
+
|
|
32
|
+
const handleCopy = async () => {
|
|
33
|
+
await Clipboard.setString(text)
|
|
34
|
+
setCopied(true)
|
|
35
|
+
setTimeout(() => setCopied(false), 2000)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Pressable style={[styles.copyButton, style]} onPress={handleCopy}>
|
|
40
|
+
<Text style={styles.copyButtonText}>{copied ? 'Copied!' : 'Copy'}</Text>
|
|
41
|
+
</Pressable>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CollapsibleSection = ({ title, children, initiallyExpanded = false }) => {
|
|
46
|
+
const [expanded, setExpanded] = useState(initiallyExpanded)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={styles.collapsibleSection}>
|
|
50
|
+
<Pressable
|
|
51
|
+
style={styles.sectionHeader}
|
|
52
|
+
onPress={() => setExpanded(!expanded)}>
|
|
53
|
+
<Text style={styles.sectionTitle}>{title}</Text>
|
|
54
|
+
<Text style={styles.expandIcon}>{expanded ? '▼' : '▶'}</Text>
|
|
55
|
+
</Pressable>
|
|
56
|
+
{expanded && children}
|
|
57
|
+
</View>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const JSONValue = ({ value, path = '', level = 0, maxExpandLevel = 2 }) => {
|
|
62
|
+
if (value === null) {
|
|
63
|
+
return <Text style={styles.jsonNull}>null</Text>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
return <Text style={styles.jsonNull}>undefined</Text>
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof value === 'boolean') {
|
|
71
|
+
return <Text style={styles.jsonBoolean}>{value.toString()}</Text>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof value === 'number') {
|
|
75
|
+
return <Text style={styles.jsonNumber}>{value}</Text>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
return (
|
|
80
|
+
<Text style={styles.jsonString} selectable={true}>
|
|
81
|
+
{value}
|
|
82
|
+
</Text>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// For objects and arrays, use JSONTree for improved large data handling
|
|
87
|
+
if (typeof value === 'object') {
|
|
88
|
+
return (
|
|
89
|
+
<JSONTree
|
|
90
|
+
data={value}
|
|
91
|
+
theme={theme}
|
|
92
|
+
invertTheme={true}
|
|
93
|
+
hideRoot={true}
|
|
94
|
+
shouldExpandNode={(keyPath, nodeData, currentLevel) => currentLevel < maxExpandLevel}
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return <Text>{String(value)}</Text>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const EventTypeBadge = ({ eventName, entityType }) => {
|
|
103
|
+
const getEventColor = () => {
|
|
104
|
+
if (eventName?.includes('error')) return '#ff4444'
|
|
105
|
+
if (eventName?.includes('click')) return '#4CAF50'
|
|
106
|
+
if (eventName?.includes('view')) return '#2196F3'
|
|
107
|
+
if (eventName?.includes('search')) return '#FF9800'
|
|
108
|
+
return '#9C27B0'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View style={styles.badgeContainer}>
|
|
113
|
+
<Text style={[styles.eventBadge, { backgroundColor: getEventColor() }]}>
|
|
114
|
+
{eventName || 'Unknown Event'}
|
|
115
|
+
</Text>
|
|
116
|
+
{entityType && (
|
|
117
|
+
<Text style={[styles.entityBadge]}>
|
|
118
|
+
{entityType}
|
|
119
|
+
</Text>
|
|
120
|
+
)}
|
|
121
|
+
</View>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const TrackLogDetails = ({ log }) => {
|
|
126
|
+
const generateAnalyticsPayload = useCallback(() => {
|
|
127
|
+
if (!log) {
|
|
128
|
+
return 'No event data available'
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const payload = {
|
|
132
|
+
eventName: log.eventName,
|
|
133
|
+
timestamp: log.timestamp,
|
|
134
|
+
...Object.fromEntries(
|
|
135
|
+
Object.entries(log).filter(([key, value]) =>
|
|
136
|
+
key !== 'timestamp' && value !== undefined && value !== null
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return JSON.stringify(payload, null, 2)
|
|
142
|
+
}, [log])
|
|
143
|
+
|
|
144
|
+
if (!log) {
|
|
145
|
+
return (
|
|
146
|
+
<View style={styles.errorContainer}>
|
|
147
|
+
<Text style={styles.errorText}>Event data is missing</Text>
|
|
148
|
+
</View>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const coreProperties = {
|
|
153
|
+
eventName: log.eventName,
|
|
154
|
+
entityType: log.entityType,
|
|
155
|
+
entityName: log.entityName,
|
|
156
|
+
frontOperation: log.frontOperation
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const identificationProperties = {
|
|
160
|
+
pageId: log.pageId,
|
|
161
|
+
objId: log.objId,
|
|
162
|
+
entityPath: log.entityPath,
|
|
163
|
+
objType: log.objType,
|
|
164
|
+
objPt: log.objPt
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const locationProperties = {
|
|
168
|
+
refPageLocation: log.refPageLocation,
|
|
169
|
+
position: log.position,
|
|
170
|
+
entityLocation: log.entityLocation
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const searchProperties = {
|
|
174
|
+
sessionId: log.sessionId,
|
|
175
|
+
requestId: log.requestId,
|
|
176
|
+
searchKeywored: log.searchKeywored,
|
|
177
|
+
enSearchKeywored: log.enSearchKeywored
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const filterUndefinedProperties = (obj) => {
|
|
181
|
+
return Object.fromEntries(
|
|
182
|
+
Object.entries(obj).filter(([key, value]) => value !== undefined && value !== null && value !== '')
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return (
|
|
187
|
+
<ScrollView
|
|
188
|
+
style={styles.container}
|
|
189
|
+
contentContainerStyle={styles.contentContainer}
|
|
190
|
+
showsVerticalScrollIndicator={true}
|
|
191
|
+
scrollEventThrottle={16}
|
|
192
|
+
keyboardShouldPersistTaps='handled'>
|
|
193
|
+
|
|
194
|
+
<View style={styles.header}>
|
|
195
|
+
<View style={styles.headerInfo}>
|
|
196
|
+
<EventTypeBadge eventName={log.eventName} entityType={log.entityType} />
|
|
197
|
+
{log.entityName && (
|
|
198
|
+
<Text style={styles.entityName} selectable={true}>
|
|
199
|
+
{log.entityName}
|
|
200
|
+
</Text>
|
|
201
|
+
)}
|
|
202
|
+
{log.frontOperation && (
|
|
203
|
+
<Text style={styles.operation}>
|
|
204
|
+
Operation: {log.frontOperation}
|
|
205
|
+
</Text>
|
|
206
|
+
)}
|
|
207
|
+
</View>
|
|
208
|
+
<CopyButton text={generateAnalyticsPayload()} />
|
|
209
|
+
</View>
|
|
210
|
+
|
|
211
|
+
<CollapsibleSection title='Core Properties' initiallyExpanded={true}>
|
|
212
|
+
<View style={styles.content}>
|
|
213
|
+
<View style={styles.propertiesGrid}>
|
|
214
|
+
{Object.entries(filterUndefinedProperties(coreProperties)).map(([key, value]) => (
|
|
215
|
+
<View key={key} style={styles.propertyRow}>
|
|
216
|
+
<Text style={styles.propertyKey}>{key}:</Text>
|
|
217
|
+
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
|
|
218
|
+
</View>
|
|
219
|
+
))}
|
|
220
|
+
</View>
|
|
221
|
+
</View>
|
|
222
|
+
</CollapsibleSection>
|
|
223
|
+
|
|
224
|
+
{Object.keys(filterUndefinedProperties(identificationProperties)).length > 0 && (
|
|
225
|
+
<CollapsibleSection title='Identification Properties'>
|
|
226
|
+
<View style={styles.content}>
|
|
227
|
+
<View style={styles.propertiesGrid}>
|
|
228
|
+
{Object.entries(filterUndefinedProperties(identificationProperties)).map(([key, value]) => (
|
|
229
|
+
<View key={key} style={styles.propertyRow}>
|
|
230
|
+
<Text style={styles.propertyKey}>{key}:</Text>
|
|
231
|
+
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
|
|
232
|
+
</View>
|
|
233
|
+
))}
|
|
234
|
+
</View>
|
|
235
|
+
</View>
|
|
236
|
+
</CollapsibleSection>
|
|
237
|
+
)}
|
|
238
|
+
|
|
239
|
+
{Object.keys(filterUndefinedProperties(locationProperties)).length > 0 && (
|
|
240
|
+
<CollapsibleSection title='Location Properties'>
|
|
241
|
+
<View style={styles.content}>
|
|
242
|
+
<View style={styles.propertiesGrid}>
|
|
243
|
+
{Object.entries(filterUndefinedProperties(locationProperties)).map(([key, value]) => (
|
|
244
|
+
<View key={key} style={styles.propertyRow}>
|
|
245
|
+
<Text style={styles.propertyKey}>{key}:</Text>
|
|
246
|
+
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
|
|
247
|
+
</View>
|
|
248
|
+
))}
|
|
249
|
+
</View>
|
|
250
|
+
</View>
|
|
251
|
+
</CollapsibleSection>
|
|
252
|
+
)}
|
|
253
|
+
|
|
254
|
+
{Object.keys(filterUndefinedProperties(searchProperties)).length > 0 && (
|
|
255
|
+
<CollapsibleSection title='Search Properties'>
|
|
256
|
+
<View style={styles.content}>
|
|
257
|
+
<View style={styles.propertiesGrid}>
|
|
258
|
+
{Object.entries(filterUndefinedProperties(searchProperties)).map(([key, value]) => (
|
|
259
|
+
<View key={key} style={styles.propertyRow}>
|
|
260
|
+
<Text style={styles.propertyKey}>{key}:</Text>
|
|
261
|
+
<Text style={styles.propertyValue} selectable={true}>{value}</Text>
|
|
262
|
+
</View>
|
|
263
|
+
))}
|
|
264
|
+
</View>
|
|
265
|
+
</View>
|
|
266
|
+
</CollapsibleSection>
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
<CollapsibleSection title='Complete Event Data'>
|
|
270
|
+
<View style={styles.content}>
|
|
271
|
+
<View style={styles.dataSectionHeader}>
|
|
272
|
+
<Text style={styles.dataSectionTitle}>Full JSON Payload</Text>
|
|
273
|
+
<CopyButton text={generateAnalyticsPayload()} />
|
|
274
|
+
</View>
|
|
275
|
+
<View style={styles.dataContentWrapper}>
|
|
276
|
+
<ScrollView
|
|
277
|
+
style={styles.dataContent}
|
|
278
|
+
nestedScrollEnabled={true}
|
|
279
|
+
bounces={false}
|
|
280
|
+
showsVerticalScrollIndicator={true}>
|
|
281
|
+
<JSONValue value={log} maxExpandLevel={2} />
|
|
282
|
+
</ScrollView>
|
|
283
|
+
</View>
|
|
284
|
+
</View>
|
|
285
|
+
</CollapsibleSection>
|
|
286
|
+
|
|
287
|
+
<CollapsibleSection title='Timing Information'>
|
|
288
|
+
<View style={styles.content}>
|
|
289
|
+
<View style={styles.propertyRow}>
|
|
290
|
+
<Text style={styles.propertyKey}>Timestamp:</Text>
|
|
291
|
+
<Text style={styles.propertyValue}>
|
|
292
|
+
{log.timestamp
|
|
293
|
+
? new Date(log.timestamp).toLocaleString()
|
|
294
|
+
: 'Unknown'}
|
|
295
|
+
</Text>
|
|
296
|
+
</View>
|
|
297
|
+
<View style={styles.propertyRow}>
|
|
298
|
+
<Text style={styles.propertyKey}>Time ago:</Text>
|
|
299
|
+
<Text style={styles.propertyValue}>
|
|
300
|
+
{log.timestamp
|
|
301
|
+
? getTimeAgo(log.timestamp)
|
|
302
|
+
: 'Unknown'}
|
|
303
|
+
</Text>
|
|
304
|
+
</View>
|
|
305
|
+
</View>
|
|
306
|
+
</CollapsibleSection>
|
|
307
|
+
</ScrollView>
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const getTimeAgo = (timestamp) => {
|
|
312
|
+
const now = new Date()
|
|
313
|
+
const eventTime = new Date(timestamp)
|
|
314
|
+
const diffMs = now - eventTime
|
|
315
|
+
const diffSeconds = Math.floor(diffMs / 1000)
|
|
316
|
+
const diffMinutes = Math.floor(diffSeconds / 60)
|
|
317
|
+
const diffHours = Math.floor(diffMinutes / 60)
|
|
318
|
+
|
|
319
|
+
if (diffSeconds < 60) {
|
|
320
|
+
return `${diffSeconds} seconds ago`
|
|
321
|
+
} else if (diffMinutes < 60) {
|
|
322
|
+
return `${diffMinutes} minutes ago`
|
|
323
|
+
} else if (diffHours < 24) {
|
|
324
|
+
return `${diffHours} hours ago`
|
|
325
|
+
} else {
|
|
326
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
327
|
+
return `${diffDays} days ago`
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const styles = StyleSheet.create({
|
|
332
|
+
container: {
|
|
333
|
+
flex: 1,
|
|
334
|
+
backgroundColor: '#fff',
|
|
335
|
+
},
|
|
336
|
+
contentContainer: {
|
|
337
|
+
paddingBottom: 20,
|
|
338
|
+
},
|
|
339
|
+
header: {
|
|
340
|
+
flexDirection: 'row',
|
|
341
|
+
padding: 15,
|
|
342
|
+
borderBottomWidth: 1,
|
|
343
|
+
borderBottomColor: '#eee',
|
|
344
|
+
alignItems: 'flex-start',
|
|
345
|
+
},
|
|
346
|
+
headerInfo: {
|
|
347
|
+
flex: 1,
|
|
348
|
+
marginRight: 10,
|
|
349
|
+
},
|
|
350
|
+
badgeContainer: {
|
|
351
|
+
flexDirection: 'row',
|
|
352
|
+
alignItems: 'center',
|
|
353
|
+
marginBottom: 8,
|
|
354
|
+
},
|
|
355
|
+
eventBadge: {
|
|
356
|
+
paddingHorizontal: 12,
|
|
357
|
+
paddingVertical: 4,
|
|
358
|
+
borderRadius: 12,
|
|
359
|
+
color: 'white',
|
|
360
|
+
fontSize: 12,
|
|
361
|
+
fontWeight: 'bold',
|
|
362
|
+
marginRight: 8,
|
|
363
|
+
},
|
|
364
|
+
entityBadge: {
|
|
365
|
+
paddingHorizontal: 8,
|
|
366
|
+
paddingVertical: 2,
|
|
367
|
+
borderRadius: 8,
|
|
368
|
+
backgroundColor: '#f0f0f0',
|
|
369
|
+
color: '#333',
|
|
370
|
+
fontSize: 11,
|
|
371
|
+
fontWeight: '500',
|
|
372
|
+
},
|
|
373
|
+
entityName: {
|
|
374
|
+
fontSize: 16,
|
|
375
|
+
color: '#333',
|
|
376
|
+
marginBottom: 4,
|
|
377
|
+
fontWeight: '600',
|
|
378
|
+
},
|
|
379
|
+
operation: {
|
|
380
|
+
fontSize: 14,
|
|
381
|
+
color: '#666',
|
|
382
|
+
fontStyle: 'italic',
|
|
383
|
+
},
|
|
384
|
+
collapsibleSection: {
|
|
385
|
+
marginBottom: 1,
|
|
386
|
+
backgroundColor: '#fff',
|
|
387
|
+
},
|
|
388
|
+
sectionHeader: {
|
|
389
|
+
flexDirection: 'row',
|
|
390
|
+
justifyContent: 'space-between',
|
|
391
|
+
alignItems: 'center',
|
|
392
|
+
padding: 15,
|
|
393
|
+
backgroundColor: '#f5f5f5',
|
|
394
|
+
},
|
|
395
|
+
sectionTitle: {
|
|
396
|
+
fontSize: 15,
|
|
397
|
+
fontWeight: 'bold',
|
|
398
|
+
color: '#333',
|
|
399
|
+
},
|
|
400
|
+
expandIcon: {
|
|
401
|
+
fontSize: 14,
|
|
402
|
+
color: '#666',
|
|
403
|
+
},
|
|
404
|
+
content: {
|
|
405
|
+
padding: 15,
|
|
406
|
+
},
|
|
407
|
+
propertiesGrid: {
|
|
408
|
+
gap: 8,
|
|
409
|
+
},
|
|
410
|
+
propertyRow: {
|
|
411
|
+
marginBottom: 8,
|
|
412
|
+
},
|
|
413
|
+
propertyKey: {
|
|
414
|
+
fontSize: 14,
|
|
415
|
+
fontWeight: 'bold',
|
|
416
|
+
color: '#666',
|
|
417
|
+
marginBottom: 2,
|
|
418
|
+
},
|
|
419
|
+
propertyValue: {
|
|
420
|
+
fontSize: 14,
|
|
421
|
+
color: '#333',
|
|
422
|
+
},
|
|
423
|
+
errorContainer: {
|
|
424
|
+
flex: 1,
|
|
425
|
+
justifyContent: 'center',
|
|
426
|
+
alignItems: 'center',
|
|
427
|
+
padding: 20,
|
|
428
|
+
},
|
|
429
|
+
errorText: {
|
|
430
|
+
color: '#ff4444',
|
|
431
|
+
fontSize: 16,
|
|
432
|
+
},
|
|
433
|
+
dataSectionHeader: {
|
|
434
|
+
flexDirection: 'row',
|
|
435
|
+
justifyContent: 'space-between',
|
|
436
|
+
alignItems: 'center',
|
|
437
|
+
marginBottom: 8,
|
|
438
|
+
},
|
|
439
|
+
dataSectionTitle: {
|
|
440
|
+
fontSize: 14,
|
|
441
|
+
fontWeight: 'bold',
|
|
442
|
+
color: '#666',
|
|
443
|
+
},
|
|
444
|
+
dataContentWrapper: {
|
|
445
|
+
flex: 1,
|
|
446
|
+
},
|
|
447
|
+
dataContent: {
|
|
448
|
+
flex: 1,
|
|
449
|
+
backgroundColor: '#f8f9fa',
|
|
450
|
+
padding: 10,
|
|
451
|
+
borderRadius: 4,
|
|
452
|
+
borderWidth: 1,
|
|
453
|
+
borderColor: '#e9ecef',
|
|
454
|
+
},
|
|
455
|
+
copyButton: {
|
|
456
|
+
backgroundColor: '#e9ecef',
|
|
457
|
+
paddingHorizontal: 10,
|
|
458
|
+
paddingVertical: 5,
|
|
459
|
+
borderRadius: 4,
|
|
460
|
+
},
|
|
461
|
+
copyButtonText: {
|
|
462
|
+
fontSize: 12,
|
|
463
|
+
color: '#666',
|
|
464
|
+
},
|
|
465
|
+
jsonString: {
|
|
466
|
+
color: '#CB772F',
|
|
467
|
+
},
|
|
468
|
+
jsonNumber: {
|
|
469
|
+
color: '#2878D0',
|
|
470
|
+
},
|
|
471
|
+
jsonBoolean: {
|
|
472
|
+
color: '#2878D0',
|
|
473
|
+
fontWeight: 'bold',
|
|
474
|
+
},
|
|
475
|
+
jsonNull: {
|
|
476
|
+
color: '#A0A0A0',
|
|
477
|
+
fontStyle: 'italic',
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
export default TrackLogDetails
|
package/package.json
CHANGED