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.
@@ -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}, background process will be killed`);
165
- console.log('[Background] Received stop signal, process will be terminated...');
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 --status');
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('https://testdriver-api.onrender.com/auth/exchange-api-key', {
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('https://testdriver-api.onrender.com/api/v1/whoami', {
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('https://testdriver-api.onrender.com/api/v1/projects', {
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('https://testdriver-api.onrender.com/api/v1/replay/upload', {
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('https://testdriver-api.onrender.com/api/v1/logs', {
192
+ const response = await got.post(`${API_ENDPOINT}/api/v1/logs`, {
192
193
  headers: {
193
194
  Authorization: `Bearer ${token}`
194
195
  },