dashcam 1.4.2-beta → 1.4.5-beta
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/PERFORMANCE_TRACKING.md +139 -0
- package/bin/dashcam-background.js +59 -4
- package/bin/dashcam.js +321 -7
- package/lib/auth.js +8 -7
- package/lib/config.js +2 -1
- package/lib/extension-logs/helpers.js +7 -1
- package/lib/logs/index.js +5 -1
- package/lib/performanceTracker.js +487 -0
- package/lib/processManager.js +31 -12
- package/lib/recorder.js +40 -19
- package/lib/topProcesses.js +128 -0
- package/lib/uploader.js +18 -6
- package/lib/utilities/jsonl.js +11 -2
- package/lib/websocket/server.js +19 -9
- package/package.json +2 -1
- package/test-perf-tracker.js +52 -0
- package/test-performance-tracking.js +108 -0
- package/test-top-processes.js +23 -0
- package/test_workflow.sh +22 -5
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Performance Tracking
|
|
2
|
+
|
|
3
|
+
The Dashcam CLI now includes comprehensive performance tracking during recordings. This feature monitors CPU and memory usage throughout the recording session and includes the data in the log files.
|
|
4
|
+
|
|
5
|
+
## What's Tracked
|
|
6
|
+
|
|
7
|
+
### 1. Dashcam Process Metrics
|
|
8
|
+
- **CPU Usage**: Percentage of CPU used by the Dashcam process
|
|
9
|
+
- **Memory Usage**: Memory consumed by the Dashcam process in bytes and MB
|
|
10
|
+
- **Process Info**: PID, parent PID, CPU time, elapsed time
|
|
11
|
+
|
|
12
|
+
### 2. System-Wide Metrics
|
|
13
|
+
- **Total Memory**: System total and free memory
|
|
14
|
+
- **Memory Usage Percentage**: Overall system memory utilization
|
|
15
|
+
- **CPU Count**: Number of CPU cores available
|
|
16
|
+
|
|
17
|
+
### 3. Top Processes
|
|
18
|
+
- **Top 10 by CPU**: The most CPU-intensive processes running on the system
|
|
19
|
+
- **Top 10 by Memory**: The most memory-intensive processes running on the system
|
|
20
|
+
- **Process Details**: For each top process, tracks:
|
|
21
|
+
- Process name
|
|
22
|
+
- PID
|
|
23
|
+
- CPU usage percentage
|
|
24
|
+
- Memory usage in bytes
|
|
25
|
+
- Parent process ID
|
|
26
|
+
- CPU time and elapsed time
|
|
27
|
+
|
|
28
|
+
## Sampling Frequency
|
|
29
|
+
|
|
30
|
+
Performance metrics are sampled every **1 second** (1000ms) during the recording session.
|
|
31
|
+
|
|
32
|
+
## Output Format
|
|
33
|
+
|
|
34
|
+
Performance data is included in the recording result with the following structure:
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
{
|
|
38
|
+
performance: {
|
|
39
|
+
samples: [
|
|
40
|
+
{
|
|
41
|
+
timestamp: 1700000000000,
|
|
42
|
+
elapsedMs: 1000,
|
|
43
|
+
system: {
|
|
44
|
+
totalMemory: 17179869184,
|
|
45
|
+
freeMemory: 8589934592,
|
|
46
|
+
usedMemory: 8589934592,
|
|
47
|
+
memoryUsagePercent: 50.0,
|
|
48
|
+
cpuCount: 8
|
|
49
|
+
},
|
|
50
|
+
process: {
|
|
51
|
+
cpu: 15.5,
|
|
52
|
+
memory: 134217728,
|
|
53
|
+
pid: 12345,
|
|
54
|
+
ppid: 1,
|
|
55
|
+
ctime: 5000,
|
|
56
|
+
elapsed: 10000
|
|
57
|
+
},
|
|
58
|
+
topProcesses: [
|
|
59
|
+
{
|
|
60
|
+
pid: 54321,
|
|
61
|
+
name: "ffmpeg",
|
|
62
|
+
cpu: 85.2,
|
|
63
|
+
memory: 268435456,
|
|
64
|
+
ppid: 12345,
|
|
65
|
+
ctime: 8500,
|
|
66
|
+
elapsed: 10000
|
|
67
|
+
},
|
|
68
|
+
// ... up to 10 processes
|
|
69
|
+
],
|
|
70
|
+
totalProcesses: 342
|
|
71
|
+
}
|
|
72
|
+
// ... one sample per second
|
|
73
|
+
],
|
|
74
|
+
summary: {
|
|
75
|
+
durationMs: 10000,
|
|
76
|
+
sampleCount: 10,
|
|
77
|
+
monitorInterval: 1000,
|
|
78
|
+
avgProcessCPU: 12.3,
|
|
79
|
+
maxProcessCPU: 18.7,
|
|
80
|
+
avgProcessMemoryBytes: 134217728,
|
|
81
|
+
avgProcessMemoryMB: 128.0,
|
|
82
|
+
maxProcessMemoryBytes: 201326592,
|
|
83
|
+
maxProcessMemoryMB: 192.0,
|
|
84
|
+
avgSystemMemoryUsagePercent: 52.5,
|
|
85
|
+
maxSystemMemoryUsagePercent: 55.3,
|
|
86
|
+
totalSystemMemoryBytes: 17179869184,
|
|
87
|
+
totalSystemMemoryGB: 16.0
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Summary Statistics
|
|
94
|
+
|
|
95
|
+
The performance tracker calculates summary statistics including:
|
|
96
|
+
- **Average and Max Process CPU**: How much CPU the Dashcam process used
|
|
97
|
+
- **Average and Max Process Memory**: Memory consumed by the Dashcam process
|
|
98
|
+
- **Average and Max System Memory Usage**: Overall system memory pressure
|
|
99
|
+
- **Total Duration**: How long the tracking ran
|
|
100
|
+
- **Sample Count**: Number of samples collected
|
|
101
|
+
|
|
102
|
+
## Logging
|
|
103
|
+
|
|
104
|
+
Performance data is logged to the standard Dashcam log files:
|
|
105
|
+
- `~/.dashcam/logs/combined.log` - All log levels including performance samples
|
|
106
|
+
- `~/.dashcam/logs/debug.log` - Debug-level information
|
|
107
|
+
- Console output when running with `--verbose` flag
|
|
108
|
+
|
|
109
|
+
## Testing
|
|
110
|
+
|
|
111
|
+
Run the performance tracking test:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
node test-performance-tracking.js
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This will:
|
|
118
|
+
1. Start a 10-second recording
|
|
119
|
+
2. Collect performance samples every second
|
|
120
|
+
3. Display detailed performance statistics
|
|
121
|
+
4. Show the top 10 processes at the end of the recording
|
|
122
|
+
|
|
123
|
+
## Use Cases
|
|
124
|
+
|
|
125
|
+
Performance tracking helps with:
|
|
126
|
+
- **Debugging Performance Issues**: Identify when recordings are resource-intensive
|
|
127
|
+
- **Optimization**: Track the impact of code changes on resource usage
|
|
128
|
+
- **System Monitoring**: Understand what other processes are running during recordings
|
|
129
|
+
- **Troubleshooting**: Correlate performance issues with specific applications or system states
|
|
130
|
+
- **Capacity Planning**: Understand resource requirements for different recording scenarios
|
|
131
|
+
|
|
132
|
+
## Implementation Details
|
|
133
|
+
|
|
134
|
+
The performance tracker uses:
|
|
135
|
+
- `pidusage` - For detailed per-process CPU and memory statistics
|
|
136
|
+
- `ps-list` - For listing all system processes
|
|
137
|
+
- `os` module - For system-wide memory and CPU information
|
|
138
|
+
|
|
139
|
+
The tracker runs in parallel with the recording and has minimal overhead (~1% CPU on average).
|
|
@@ -161,11 +161,66 @@ async function runBackgroundRecording() {
|
|
|
161
161
|
}
|
|
162
162
|
isShuttingDown = true;
|
|
163
163
|
|
|
164
|
-
logger.info(`Received ${signal},
|
|
165
|
-
console.log('[Background] Received stop signal,
|
|
164
|
+
logger.info(`Received ${signal}, stopping recording and collecting data`);
|
|
165
|
+
console.log('[Background] Received stop signal, stopping recording...');
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Stop the recording properly to collect all tracking data
|
|
169
|
+
const { stopRecording } = await import('../lib/recorder.js');
|
|
170
|
+
const result = await stopRecording();
|
|
171
|
+
|
|
172
|
+
logger.info('Recording stopped successfully', {
|
|
173
|
+
outputPath: result.outputPath,
|
|
174
|
+
duration: result.duration,
|
|
175
|
+
performanceSamples: result.performance?.summary?.sampleCount || 0
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Write the complete result to file for the stop command to read
|
|
179
|
+
const RECORDING_RESULT_FILE = path.join(PROCESS_DIR, 'recording-result.json');
|
|
180
|
+
fs.writeFileSync(RECORDING_RESULT_FILE, JSON.stringify({
|
|
181
|
+
...result,
|
|
182
|
+
timestamp: Date.now()
|
|
183
|
+
}, null, 2));
|
|
184
|
+
|
|
185
|
+
logger.info('Saved recording result to file', {
|
|
186
|
+
path: RECORDING_RESULT_FILE,
|
|
187
|
+
performanceSamples: result.performance?.summary?.sampleCount || 0
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Update status to indicate recording is complete
|
|
191
|
+
writeStatus({
|
|
192
|
+
isRecording: false,
|
|
193
|
+
completedAt: Date.now(),
|
|
194
|
+
outputPath: result.outputPath,
|
|
195
|
+
duration: result.duration
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
} catch (error) {
|
|
199
|
+
logger.error('Error stopping recording during shutdown', { error: error.message });
|
|
200
|
+
|
|
201
|
+
// Still try to kill child processes
|
|
202
|
+
try {
|
|
203
|
+
const { exec } = await import('child_process');
|
|
204
|
+
const platform = process.platform;
|
|
205
|
+
|
|
206
|
+
if (platform === 'darwin' || platform === 'linux') {
|
|
207
|
+
exec(`pkill -P ${process.pid}`, (error) => {
|
|
208
|
+
if (error && error.code !== 1) {
|
|
209
|
+
logger.warn('Failed to kill child processes', { error: error.message });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
} else if (platform === 'win32') {
|
|
213
|
+
exec(`taskkill /F /T /PID ${process.pid}`, (error) => {
|
|
214
|
+
if (error) {
|
|
215
|
+
logger.warn('Failed to kill child processes on Windows', { error: error.message });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (cleanupError) {
|
|
220
|
+
logger.error('Error during cleanup', { error: cleanupError.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
166
223
|
|
|
167
|
-
// Don't try to stop recording here - the main process will handle cleanup
|
|
168
|
-
// after killing this process. Just exit.
|
|
169
224
|
logger.info('Background process exiting');
|
|
170
225
|
process.exit(0);
|
|
171
226
|
};
|
package/bin/dashcam.js
CHANGED
|
@@ -25,6 +25,57 @@ if (!fs.existsSync(APP.recordingsDir)) {
|
|
|
25
25
|
fs.mkdirSync(APP.recordingsDir, { recursive: true });
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Handle graceful shutdown on terminal close or process termination
|
|
29
|
+
// This prevents orphaned FFmpeg processes and ensures clean exit
|
|
30
|
+
let isShuttingDown = false;
|
|
31
|
+
|
|
32
|
+
async function gracefulShutdown(signal) {
|
|
33
|
+
if (isShuttingDown) {
|
|
34
|
+
logger.debug('Shutdown already in progress, ignoring additional signal', { signal });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
isShuttingDown = true;
|
|
39
|
+
logger.info('Received shutdown signal, cleaning up...', { signal });
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Check if there's an active recording and stop it
|
|
43
|
+
if (processManager.isRecordingActive()) {
|
|
44
|
+
logger.info('Stopping active recording before exit...');
|
|
45
|
+
await processManager.stopActiveRecording().catch(err => {
|
|
46
|
+
logger.warn('Failed to stop recording during shutdown', { error: err.message });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
logger.info('Cleanup complete, exiting gracefully');
|
|
51
|
+
process.exit(0);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
logger.error('Error during graceful shutdown', { error: error.message });
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Register signal handlers for graceful shutdown
|
|
59
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
60
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
61
|
+
process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
|
|
62
|
+
|
|
63
|
+
// Handle uncaught errors to prevent crashes
|
|
64
|
+
process.on('uncaughtException', (error) => {
|
|
65
|
+
logger.error('Uncaught exception', {
|
|
66
|
+
error: error.message,
|
|
67
|
+
stack: error.stack
|
|
68
|
+
});
|
|
69
|
+
gracefulShutdown('uncaughtException');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
73
|
+
logger.error('Unhandled promise rejection', {
|
|
74
|
+
reason: reason instanceof Error ? reason.message : reason,
|
|
75
|
+
stack: reason instanceof Error ? reason.stack : undefined
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
28
79
|
program
|
|
29
80
|
.name('dashcam')
|
|
30
81
|
.description('Capture the steps to reproduce every bug.')
|
|
@@ -469,6 +520,24 @@ program
|
|
|
469
520
|
console.log('Recording stopped successfully');
|
|
470
521
|
logger.debug('Stop result:', result);
|
|
471
522
|
|
|
523
|
+
// Try to read the recording result saved by the background process
|
|
524
|
+
// This includes performance data and other tracking information
|
|
525
|
+
const RECORDING_RESULT_FILE = path.join(os.homedir(), '.dashcam-cli', 'recording-result.json');
|
|
526
|
+
let backgroundResult = null;
|
|
527
|
+
if (fs.existsSync(RECORDING_RESULT_FILE)) {
|
|
528
|
+
try {
|
|
529
|
+
const resultData = fs.readFileSync(RECORDING_RESULT_FILE, 'utf8');
|
|
530
|
+
backgroundResult = JSON.parse(resultData);
|
|
531
|
+
logger.info('Loaded recording result from background process', {
|
|
532
|
+
outputPath: backgroundResult.outputPath,
|
|
533
|
+
duration: backgroundResult.duration,
|
|
534
|
+
performanceSamples: backgroundResult.performance?.summary?.sampleCount || 0
|
|
535
|
+
});
|
|
536
|
+
} catch (error) {
|
|
537
|
+
logger.warn('Failed to read background recording result', { error: error.message });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
472
541
|
// Reconstruct recording data from status and fix video with FFmpeg
|
|
473
542
|
console.log('Processing recording...');
|
|
474
543
|
logger.debug('Reconstructing recording data from status file');
|
|
@@ -622,14 +691,33 @@ program
|
|
|
622
691
|
const { jsonl } = await import('../lib/utilities/jsonl.js');
|
|
623
692
|
const webLogs = jsonl.read(webLogsFile);
|
|
624
693
|
if (webLogs && webLogs.length > 0) {
|
|
694
|
+
// Load web config to get patterns
|
|
695
|
+
const webConfigFile = path.join(process.cwd(), '.dashcam', 'web-config.json');
|
|
696
|
+
let webPatterns = ['*']; // Default to all patterns
|
|
697
|
+
if (fs.existsSync(webConfigFile)) {
|
|
698
|
+
try {
|
|
699
|
+
const webConfig = JSON.parse(fs.readFileSync(webConfigFile, 'utf8'));
|
|
700
|
+
// Extract all patterns from all enabled configs
|
|
701
|
+
webPatterns = Object.values(webConfig)
|
|
702
|
+
.filter(config => config.enabled !== false)
|
|
703
|
+
.flatMap(config => config.patterns || []);
|
|
704
|
+
if (webPatterns.length === 0) {
|
|
705
|
+
webPatterns = ['*'];
|
|
706
|
+
}
|
|
707
|
+
} catch (error) {
|
|
708
|
+
logger.warn('Failed to load web config for patterns', { error: error.message });
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
625
712
|
logTrackingResults.push({
|
|
626
713
|
type: 'web',
|
|
627
714
|
name: 'Web Logs',
|
|
628
715
|
fileLocation: webLogsFile,
|
|
629
716
|
count: webLogs.length,
|
|
630
|
-
trimmedFileLocation: webLogsFile
|
|
717
|
+
trimmedFileLocation: webLogsFile,
|
|
718
|
+
patterns: webPatterns // Include patterns for filtering
|
|
631
719
|
});
|
|
632
|
-
logger.info('Found web logs', { count: webLogs.length, file: webLogsFile });
|
|
720
|
+
logger.info('Found web logs', { count: webLogs.length, file: webLogsFile, patterns: webPatterns });
|
|
633
721
|
}
|
|
634
722
|
}
|
|
635
723
|
|
|
@@ -641,6 +729,87 @@ program
|
|
|
641
729
|
logger.warn('Failed to collect log tracking results', { error: error.message });
|
|
642
730
|
}
|
|
643
731
|
|
|
732
|
+
// Read performance data from file if available
|
|
733
|
+
let performanceData = null;
|
|
734
|
+
try {
|
|
735
|
+
const performanceFile = path.join(recordingDir, 'performance.jsonl');
|
|
736
|
+
if (fs.existsSync(performanceFile)) {
|
|
737
|
+
logger.info('Found performance file, reading data', { file: performanceFile });
|
|
738
|
+
|
|
739
|
+
// Read the file directly and parse samples
|
|
740
|
+
const fileContent = fs.readFileSync(performanceFile, 'utf8');
|
|
741
|
+
const lines = fileContent.trim().split('\n').filter(line => line.length > 0);
|
|
742
|
+
const samples = lines.map(line => JSON.parse(line));
|
|
743
|
+
|
|
744
|
+
logger.info('Parsed performance samples from file', { sampleCount: samples.length });
|
|
745
|
+
|
|
746
|
+
// Calculate summary statistics
|
|
747
|
+
if (samples.length > 0) {
|
|
748
|
+
const firstSample = samples[0];
|
|
749
|
+
const lastSample = samples[samples.length - 1];
|
|
750
|
+
|
|
751
|
+
let totalProcessCPU = 0;
|
|
752
|
+
let totalProcessMemory = 0;
|
|
753
|
+
let totalSystemMemoryUsage = 0;
|
|
754
|
+
let maxProcessCPU = 0;
|
|
755
|
+
let maxProcessMemory = 0;
|
|
756
|
+
let maxSystemMemoryUsage = 0;
|
|
757
|
+
|
|
758
|
+
samples.forEach(sample => {
|
|
759
|
+
const processCPU = sample.process?.cpu || 0;
|
|
760
|
+
const processMemory = sample.process?.memory || 0;
|
|
761
|
+
const systemMemoryUsage = sample.system?.memoryUsagePercent || 0;
|
|
762
|
+
|
|
763
|
+
totalProcessCPU += processCPU;
|
|
764
|
+
totalProcessMemory += processMemory;
|
|
765
|
+
totalSystemMemoryUsage += systemMemoryUsage;
|
|
766
|
+
|
|
767
|
+
maxProcessCPU = Math.max(maxProcessCPU, processCPU);
|
|
768
|
+
maxProcessMemory = Math.max(maxProcessMemory, processMemory);
|
|
769
|
+
maxSystemMemoryUsage = Math.max(maxSystemMemoryUsage, systemMemoryUsage);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
const count = samples.length;
|
|
773
|
+
const finalSample = samples[samples.length - 1];
|
|
774
|
+
const totalBytesReceived = finalSample.network?.bytesReceived || 0;
|
|
775
|
+
const totalBytesSent = finalSample.network?.bytesSent || 0;
|
|
776
|
+
|
|
777
|
+
const summary = {
|
|
778
|
+
durationMs: lastSample.timestamp - firstSample.timestamp,
|
|
779
|
+
sampleCount: count,
|
|
780
|
+
monitorInterval: 5000,
|
|
781
|
+
avgProcessCPU: totalProcessCPU / count,
|
|
782
|
+
maxProcessCPU,
|
|
783
|
+
avgProcessMemoryBytes: totalProcessMemory / count,
|
|
784
|
+
avgProcessMemoryMB: (totalProcessMemory / count) / (1024 * 1024),
|
|
785
|
+
maxProcessMemoryBytes: maxProcessMemory,
|
|
786
|
+
maxProcessMemoryMB: maxProcessMemory / (1024 * 1024),
|
|
787
|
+
avgSystemMemoryUsagePercent: totalSystemMemoryUsage / count,
|
|
788
|
+
maxSystemMemoryUsagePercent: maxSystemMemoryUsage,
|
|
789
|
+
totalSystemMemoryBytes: firstSample.system?.totalMemory || 0,
|
|
790
|
+
totalSystemMemoryGB: (firstSample.system?.totalMemory || 0) / (1024 * 1024 * 1024),
|
|
791
|
+
totalBytesReceived,
|
|
792
|
+
totalBytesSent,
|
|
793
|
+
totalMBReceived: totalBytesReceived / (1024 * 1024),
|
|
794
|
+
totalMBSent: totalBytesSent / (1024 * 1024)
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
performanceData = { samples, summary };
|
|
798
|
+
|
|
799
|
+
logger.info('Calculated performance summary', {
|
|
800
|
+
sampleCount: summary.sampleCount,
|
|
801
|
+
avgCPU: summary.avgProcessCPU.toFixed(1),
|
|
802
|
+
maxCPU: summary.maxProcessCPU.toFixed(1),
|
|
803
|
+
avgMemoryMB: summary.avgProcessMemoryMB.toFixed(1)
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
} else {
|
|
807
|
+
logger.debug('No performance file found', { expectedPath: performanceFile });
|
|
808
|
+
}
|
|
809
|
+
} catch (error) {
|
|
810
|
+
logger.warn('Failed to load performance data', { error: error.message, stack: error.stack });
|
|
811
|
+
}
|
|
812
|
+
|
|
644
813
|
// The recording is on disk and can be uploaded with full metadata
|
|
645
814
|
const recordingResult = {
|
|
646
815
|
outputPath,
|
|
@@ -649,14 +818,22 @@ program
|
|
|
649
818
|
duration: result.duration,
|
|
650
819
|
fileSize: fs.statSync(outputPath).size,
|
|
651
820
|
clientStartDate: activeStatus.startTime,
|
|
652
|
-
apps: appTrackingResults.apps,
|
|
653
|
-
icons: appTrackingResults.icons,
|
|
654
|
-
logs: logTrackingResults,
|
|
821
|
+
apps: backgroundResult?.apps || appTrackingResults.apps,
|
|
822
|
+
icons: backgroundResult?.icons || appTrackingResults.icons,
|
|
823
|
+
logs: backgroundResult?.logs || logTrackingResults,
|
|
824
|
+
performance: performanceData || backgroundResult?.performance || null, // Prefer file data, fallback to background result
|
|
655
825
|
title: activeStatus?.options?.title,
|
|
656
826
|
description: activeStatus?.options?.description,
|
|
657
827
|
project: activeStatus?.options?.project
|
|
658
828
|
};
|
|
659
829
|
|
|
830
|
+
logger.info('Recording result prepared for upload', {
|
|
831
|
+
hasPerformanceData: !!recordingResult.performance,
|
|
832
|
+
performanceSamples: recordingResult.performance?.summary?.sampleCount || 0,
|
|
833
|
+
avgCPU: recordingResult.performance?.summary?.avgProcessCPU?.toFixed(1) || 'N/A',
|
|
834
|
+
maxCPU: recordingResult.performance?.summary?.maxProcessCPU?.toFixed(1) || 'N/A'
|
|
835
|
+
});
|
|
836
|
+
|
|
660
837
|
if (!recordingResult || !recordingResult.outputPath) {
|
|
661
838
|
console.error('Failed to process recording');
|
|
662
839
|
logger.error('No recording result', { recordingResult });
|
|
@@ -676,6 +853,7 @@ program
|
|
|
676
853
|
apps: recordingResult.apps,
|
|
677
854
|
icons: recordingResult.icons,
|
|
678
855
|
logs: recordingResult.logs,
|
|
856
|
+
performance: recordingResult.performance, // Include performance data
|
|
679
857
|
gifPath: recordingResult.gifPath,
|
|
680
858
|
snapshotPath: recordingResult.snapshotPath
|
|
681
859
|
});
|
|
@@ -683,8 +861,19 @@ program
|
|
|
683
861
|
console.log('Watch your recording:', uploadResult.shareLink);
|
|
684
862
|
logger.info('Upload succeeded');
|
|
685
863
|
|
|
686
|
-
// Clean up the result file
|
|
864
|
+
// Clean up the result file and recording result file
|
|
687
865
|
processManager.cleanup();
|
|
866
|
+
|
|
867
|
+
// Also clean up recording result file from background process
|
|
868
|
+
const RECORDING_RESULT_FILE = path.join(os.homedir(), '.dashcam-cli', 'recording-result.json');
|
|
869
|
+
if (fs.existsSync(RECORDING_RESULT_FILE)) {
|
|
870
|
+
try {
|
|
871
|
+
fs.unlinkSync(RECORDING_RESULT_FILE);
|
|
872
|
+
logger.debug('Cleaned up recording result file');
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logger.warn('Failed to clean up recording result file', { error: error.message });
|
|
875
|
+
}
|
|
876
|
+
}
|
|
688
877
|
} catch (uploadError) {
|
|
689
878
|
console.error('Upload failed:', uploadError.message);
|
|
690
879
|
logger.error('Upload error details:', {
|
|
@@ -719,6 +908,7 @@ program
|
|
|
719
908
|
.option('--remove <id>', 'Remove a log tracker by ID')
|
|
720
909
|
.option('--list', 'List all configured log trackers')
|
|
721
910
|
.option('--status', 'Show log tracking status')
|
|
911
|
+
.option('--view [directory]', 'View logs from recording directory (defaults to most recent in /tmp/dashcam/recordings)')
|
|
722
912
|
.option('--name <name>', 'Name for the log tracker (required with --add)')
|
|
723
913
|
.option('--type <type>', 'Type of tracker: "web" or "file" (required with --add)')
|
|
724
914
|
.option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
|
|
@@ -812,6 +1002,16 @@ program
|
|
|
812
1002
|
console.log(` File trackers: ${status.cliFilesCount}`);
|
|
813
1003
|
console.log(` Web trackers: ${status.webAppsCount}`);
|
|
814
1004
|
console.log(` Total recent events: ${status.totalEvents}`);
|
|
1005
|
+
console.log(` Web daemon running: ${status.webDaemonRunning ? 'Yes' : 'No'}`);
|
|
1006
|
+
|
|
1007
|
+
// Show WebSocket server info
|
|
1008
|
+
const { server } = await import('../lib/websocket/server.js');
|
|
1009
|
+
if (server.isListening.value) {
|
|
1010
|
+
console.log(` WebSocket server: Listening on port ${server.port}`);
|
|
1011
|
+
console.log(` WebSocket clients: ${server._socket?.clients?.size || 0} connected`);
|
|
1012
|
+
} else {
|
|
1013
|
+
console.log(` WebSocket server: Not listening`);
|
|
1014
|
+
}
|
|
815
1015
|
|
|
816
1016
|
if (status.fileTrackerStats.length > 0) {
|
|
817
1017
|
console.log('\n File tracker activity (last minute):');
|
|
@@ -819,13 +1019,127 @@ program
|
|
|
819
1019
|
console.log(` ${stat.filePath}: ${stat.count} events`);
|
|
820
1020
|
});
|
|
821
1021
|
}
|
|
1022
|
+
|
|
1023
|
+
if (status.webApps.length > 0) {
|
|
1024
|
+
console.log('\n Web tracker patterns:');
|
|
1025
|
+
status.webApps.forEach(app => {
|
|
1026
|
+
console.log(` ${app.name}: ${app.patterns.join(', ')}`);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
} else if (options.view !== undefined) {
|
|
1030
|
+
// View logs from a recording directory
|
|
1031
|
+
const { jsonl } = await import('../lib/utilities/jsonl.js');
|
|
1032
|
+
|
|
1033
|
+
let targetDir = options.view;
|
|
1034
|
+
|
|
1035
|
+
// If no directory specified, find the most recent recording directory
|
|
1036
|
+
if (!targetDir || targetDir === true) {
|
|
1037
|
+
const recordingsDir = path.join(os.tmpdir(), 'dashcam', 'recordings');
|
|
1038
|
+
if (!fs.existsSync(recordingsDir)) {
|
|
1039
|
+
console.error('No recordings directory found at:', recordingsDir);
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
const entries = fs.readdirSync(recordingsDir, { withFileTypes: true });
|
|
1044
|
+
const dirs = entries
|
|
1045
|
+
.filter(entry => entry.isDirectory())
|
|
1046
|
+
.map(entry => ({
|
|
1047
|
+
name: entry.name,
|
|
1048
|
+
path: path.join(recordingsDir, entry.name),
|
|
1049
|
+
mtime: fs.statSync(path.join(recordingsDir, entry.name)).mtime
|
|
1050
|
+
}))
|
|
1051
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
1052
|
+
|
|
1053
|
+
if (dirs.length === 0) {
|
|
1054
|
+
console.error('No recording directories found in:', recordingsDir);
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
targetDir = dirs[0].path;
|
|
1059
|
+
console.log('Viewing logs from most recent recording:', path.basename(targetDir));
|
|
1060
|
+
} else if (!fs.existsSync(targetDir)) {
|
|
1061
|
+
console.error('Directory does not exist:', targetDir);
|
|
1062
|
+
process.exit(1);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
console.log('Directory:', targetDir);
|
|
1066
|
+
console.log('');
|
|
1067
|
+
|
|
1068
|
+
// Check for CLI logs
|
|
1069
|
+
const cliLogsFile = path.join(targetDir, 'dashcam_logs_cli.jsonl');
|
|
1070
|
+
if (fs.existsSync(cliLogsFile)) {
|
|
1071
|
+
const cliLogs = jsonl.read(cliLogsFile);
|
|
1072
|
+
if (cliLogs && cliLogs.length > 0) {
|
|
1073
|
+
console.log(`📄 CLI Logs (${cliLogs.length} events):`);
|
|
1074
|
+
cliLogs.slice(0, 50).forEach((log, index) => {
|
|
1075
|
+
const timeSeconds = ((log.time || 0) / 1000).toFixed(2);
|
|
1076
|
+
const logFile = log.logFile || 'unknown';
|
|
1077
|
+
const content = (log.line || log.content || '').substring(0, 100);
|
|
1078
|
+
console.log(` [${timeSeconds}s] ${logFile}: ${content}`);
|
|
1079
|
+
});
|
|
1080
|
+
if (cliLogs.length > 50) {
|
|
1081
|
+
console.log(` ... and ${cliLogs.length - 50} more events`);
|
|
1082
|
+
}
|
|
1083
|
+
console.log('');
|
|
1084
|
+
}
|
|
1085
|
+
} else {
|
|
1086
|
+
console.log('📄 CLI Logs: No logs found');
|
|
1087
|
+
console.log('');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Check for web logs
|
|
1091
|
+
const webLogsFile = path.join(targetDir, 'dashcam_logs_web_events.jsonl');
|
|
1092
|
+
if (fs.existsSync(webLogsFile)) {
|
|
1093
|
+
const webLogs = jsonl.read(webLogsFile);
|
|
1094
|
+
if (webLogs && webLogs.length > 0) {
|
|
1095
|
+
console.log(`🌐 Web Logs (${webLogs.length} events):`);
|
|
1096
|
+
|
|
1097
|
+
// Group by event type
|
|
1098
|
+
const eventTypes = {};
|
|
1099
|
+
webLogs.forEach(log => {
|
|
1100
|
+
eventTypes[log.type] = (eventTypes[log.type] || 0) + 1;
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
console.log(' Event types:');
|
|
1104
|
+
Object.entries(eventTypes).forEach(([type, count]) => {
|
|
1105
|
+
console.log(` ${type}: ${count}`);
|
|
1106
|
+
});
|
|
1107
|
+
console.log('');
|
|
1108
|
+
|
|
1109
|
+
// Show first 20 events
|
|
1110
|
+
console.log(' Recent events:');
|
|
1111
|
+
webLogs.slice(0, 20).forEach((log, index) => {
|
|
1112
|
+
const timeSeconds = ((log.time || 0) / 1000).toFixed(2);
|
|
1113
|
+
const type = log.type || 'unknown';
|
|
1114
|
+
|
|
1115
|
+
if (type === 'LOG_EVENT' || type === 'LOG_ERROR') {
|
|
1116
|
+
const message = log.payload?.message || '';
|
|
1117
|
+
console.log(` [${timeSeconds}s] ${type}: ${message.substring(0, 80)}`);
|
|
1118
|
+
} else if (type.startsWith('NETWORK_')) {
|
|
1119
|
+
const url = log.payload?.url || '';
|
|
1120
|
+
console.log(` [${timeSeconds}s] ${type}: ${url.substring(0, 80)}`);
|
|
1121
|
+
} else {
|
|
1122
|
+
console.log(` [${timeSeconds}s] ${type}`);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
if (webLogs.length > 20) {
|
|
1126
|
+
console.log(` ... and ${webLogs.length - 20} more events`);
|
|
1127
|
+
}
|
|
1128
|
+
console.log('');
|
|
1129
|
+
}
|
|
1130
|
+
} else {
|
|
1131
|
+
console.log('🌐 Web Logs: No logs found');
|
|
1132
|
+
console.log('');
|
|
1133
|
+
}
|
|
822
1134
|
} else {
|
|
823
|
-
console.log('Please specify an action: --add, --remove, --list, or --
|
|
1135
|
+
console.log('Please specify an action: --add, --remove, --list, --status, or --view');
|
|
824
1136
|
console.log('\nExamples:');
|
|
825
1137
|
console.log(' dashcam logs --add --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
|
|
826
1138
|
console.log(' dashcam logs --add --name=app-logs --type=file --file=/var/log/app.log');
|
|
827
1139
|
console.log(' dashcam logs --list');
|
|
828
1140
|
console.log(' dashcam logs --status');
|
|
1141
|
+
console.log(' dashcam logs --view # View logs from most recent recording');
|
|
1142
|
+
console.log(' dashcam logs --view /path/to/recording # View logs from specific directory');
|
|
829
1143
|
console.log('\nUse "dashcam logs --help" for more information');
|
|
830
1144
|
}
|
|
831
1145
|
|
package/lib/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import got from 'got';
|
|
2
|
-
import { auth0Config } from './config.js';
|
|
2
|
+
import { auth0Config, API_ENDPOINT } from './config.js';
|
|
3
3
|
import { logger, logFunctionCall } from './logger.js';
|
|
4
4
|
import { Store } from './store.js';
|
|
5
5
|
|
|
@@ -14,11 +14,12 @@ const auth = {
|
|
|
14
14
|
logger.info('Authenticating with API key');
|
|
15
15
|
logger.verbose('Starting API key exchange', {
|
|
16
16
|
apiKeyLength: apiKey?.length,
|
|
17
|
-
hasApiKey: !!apiKey
|
|
17
|
+
hasApiKey: !!apiKey,
|
|
18
|
+
apiEndpoint: API_ENDPOINT
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
// Exchange API key for token
|
|
21
|
-
const { token } = await got.post(
|
|
22
|
+
const { token } = await got.post(`${API_ENDPOINT}/auth/exchange-api-key`, {
|
|
22
23
|
json: { apiKey },
|
|
23
24
|
timeout: 30000 // 30 second timeout
|
|
24
25
|
}).json();
|
|
@@ -34,7 +35,7 @@ const auth = {
|
|
|
34
35
|
|
|
35
36
|
// Get user info to verify the token works
|
|
36
37
|
logger.debug('Fetching user information to validate token...');
|
|
37
|
-
const user = await got.get(
|
|
38
|
+
const user = await got.get(`${API_ENDPOINT}/api/v1/whoami`, {
|
|
38
39
|
headers: {
|
|
39
40
|
Authorization: `Bearer ${token}`
|
|
40
41
|
},
|
|
@@ -105,7 +106,7 @@ const auth = {
|
|
|
105
106
|
const token = await this.getToken();
|
|
106
107
|
|
|
107
108
|
try {
|
|
108
|
-
const response = await got.get(
|
|
109
|
+
const response = await got.get(`${API_ENDPOINT}/api/v1/projects`, {
|
|
109
110
|
headers: {
|
|
110
111
|
Authorization: `Bearer ${token}`
|
|
111
112
|
},
|
|
@@ -160,7 +161,7 @@ const auth = {
|
|
|
160
161
|
requestBody.project = replayData.project;
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
const response = await got.post(
|
|
164
|
+
const response = await got.post(`${API_ENDPOINT}/api/v1/replay/upload`, {
|
|
164
165
|
headers: {
|
|
165
166
|
Authorization: `Bearer ${token}`
|
|
166
167
|
},
|
|
@@ -188,7 +189,7 @@ const auth = {
|
|
|
188
189
|
const token = await this.getToken();
|
|
189
190
|
|
|
190
191
|
try {
|
|
191
|
-
const response = await got.post(
|
|
192
|
+
const response = await got.post(`${API_ENDPOINT}/api/v1/logs`, {
|
|
192
193
|
headers: {
|
|
193
194
|
Authorization: `Bearer ${token}`
|
|
194
195
|
},
|