dashcam 0.8.4 → 1.0.1-beta.10

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.
Files changed (55) hide show
  1. package/.dashcam/cli-config.json +3 -0
  2. package/.dashcam/recording.log +135 -0
  3. package/.dashcam/web-config.json +11 -0
  4. package/.github/RELEASE.md +59 -0
  5. package/.github/workflows/publish.yml +43 -0
  6. package/BACKWARD_COMPATIBILITY.md +177 -0
  7. package/LOG_TRACKING_GUIDE.md +225 -0
  8. package/README.md +709 -155
  9. package/bin/dashcam-background.js +177 -0
  10. package/bin/dashcam.cjs +8 -0
  11. package/bin/dashcam.js +696 -0
  12. package/bin/index.js +63 -0
  13. package/examples/execute-script.js +152 -0
  14. package/examples/simple-test.js +37 -0
  15. package/lib/applicationTracker.js +311 -0
  16. package/lib/auth.js +222 -0
  17. package/lib/binaries.js +21 -0
  18. package/lib/config.js +34 -0
  19. package/lib/extension-logs/helpers.js +182 -0
  20. package/lib/extension-logs/index.js +347 -0
  21. package/lib/extension-logs/manager.js +344 -0
  22. package/lib/ffmpeg.js +155 -0
  23. package/lib/logTracker.js +23 -0
  24. package/lib/logger.js +118 -0
  25. package/lib/logs/index.js +488 -0
  26. package/lib/permissions.js +85 -0
  27. package/lib/processManager.js +317 -0
  28. package/lib/recorder.js +690 -0
  29. package/lib/store.js +58 -0
  30. package/lib/tracking/FileTracker.js +105 -0
  31. package/lib/tracking/FileTrackerManager.js +62 -0
  32. package/lib/tracking/LogsTracker.js +161 -0
  33. package/lib/tracking/active-win.js +212 -0
  34. package/lib/tracking/icons/darwin.js +39 -0
  35. package/lib/tracking/icons/index.js +167 -0
  36. package/lib/tracking/icons/windows.js +27 -0
  37. package/lib/tracking/idle.js +82 -0
  38. package/lib/tracking.js +23 -0
  39. package/lib/uploader.js +456 -0
  40. package/lib/utilities/jsonl.js +77 -0
  41. package/lib/webLogsDaemon.js +234 -0
  42. package/lib/websocket/server.js +223 -0
  43. package/package.json +53 -21
  44. package/recording.log +814 -0
  45. package/sea-bundle.mjs +34595 -0
  46. package/test-page.html +15 -0
  47. package/test.log +1 -0
  48. package/test_run.log +48 -0
  49. package/test_workflow.sh +154 -0
  50. package/examples/crash-test.js +0 -11
  51. package/examples/github-issue.sh +0 -1
  52. package/examples/protocol.html +0 -22
  53. package/index.js +0 -158
  54. package/lib.js +0 -199
  55. package/recorder.js +0 -85
package/lib/auth.js ADDED
@@ -0,0 +1,222 @@
1
+ import got from 'got';
2
+ import { auth0Config } from './config.js';
3
+ import { logger, logFunctionCall } from './logger.js';
4
+ import { Store } from './store.js';
5
+
6
+ const tokenStore = new Store('auth0-store');
7
+ const TOKEN_KEY = 'tokens';
8
+
9
+ const auth = {
10
+ async login(apiKey) {
11
+ const logExit = logFunctionCall('auth.login');
12
+
13
+ try {
14
+ logger.info('Authenticating with API key');
15
+ logger.verbose('Starting API key exchange', {
16
+ apiKeyLength: apiKey?.length,
17
+ hasApiKey: !!apiKey
18
+ });
19
+
20
+ // Exchange API key for token
21
+ const { token } = await got.post('https://api.testdriver.ai/auth/exchange-api-key', {
22
+ json: { apiKey },
23
+ timeout: 30000 // 30 second timeout
24
+ }).json();
25
+
26
+ if (!token) {
27
+ throw new Error('Failed to exchange API key for token');
28
+ }
29
+
30
+ logger.verbose('Successfully exchanged API key for token', {
31
+ tokenLength: token.length,
32
+ tokenPrefix: token.substring(0, 10) + '...'
33
+ });
34
+
35
+ // Get user info to verify the token works
36
+ logger.debug('Fetching user information to validate token...');
37
+ const user = await got.get('https://api.testdriver.ai/api/v1/whoami', {
38
+ headers: {
39
+ Authorization: `Bearer ${token}`
40
+ },
41
+ timeout: 30000
42
+ }).json();
43
+
44
+ logger.verbose('User information retrieved', {
45
+ userId: user.id,
46
+ userEmail: user.email || 'not provided',
47
+ userName: user.name || 'not provided'
48
+ });
49
+
50
+ // Store both token and user info
51
+ const tokenData = {
52
+ token,
53
+ user,
54
+ expires_at: Date.now() + (24 * 60 * 60 * 1000) // 24 hours
55
+ };
56
+
57
+ tokenStore.set(TOKEN_KEY, tokenData);
58
+
59
+ logger.info('Successfully authenticated and stored token', {
60
+ expiresAt: new Date(tokenData.expires_at).toISOString(),
61
+ userId: user.id
62
+ });
63
+
64
+ logExit();
65
+ return token;
66
+
67
+ } catch (error) {
68
+ logger.error('Authentication failed:', {
69
+ message: error.message,
70
+ statusCode: error.response?.statusCode,
71
+ responseBody: error.response?.body
72
+ });
73
+ logExit();
74
+ throw error;
75
+ }
76
+ },
77
+
78
+ async logout() {
79
+ try {
80
+ tokenStore.delete(TOKEN_KEY);
81
+ logger.info('Successfully logged out');
82
+ } catch (error) {
83
+ logger.error('Failed to logout:', error);
84
+ throw error;
85
+ }
86
+ },
87
+
88
+ async getToken() {
89
+ const tokens = tokenStore.get(TOKEN_KEY);
90
+ if (!tokens || Date.now() >= tokens.expires_at) {
91
+ throw new Error('No valid token found. Please login with an API key first');
92
+ }
93
+ return tokens.token;
94
+ },
95
+
96
+ async isAuthenticated() {
97
+ const tokens = tokenStore.get(TOKEN_KEY);
98
+ return tokens && tokens.expires_at && Date.now() < tokens.expires_at;
99
+ },
100
+
101
+ async getProjects() {
102
+ const logExit = logFunctionCall('auth.getProjects');
103
+
104
+ logger.debug('Fetching user projects...');
105
+ const token = await this.getToken();
106
+
107
+ try {
108
+ const response = await got.get('https://api.testdriver.ai/api/v1/projects', {
109
+ headers: {
110
+ Authorization: `Bearer ${token}`
111
+ },
112
+ timeout: 30000
113
+ }).json();
114
+
115
+ logger.verbose('Projects fetched successfully', {
116
+ projectCount: response.length
117
+ });
118
+
119
+ logExit();
120
+ return response;
121
+ } catch (error) {
122
+ logger.error('Failed to fetch projects:', {
123
+ message: error.message,
124
+ statusCode: error.response?.statusCode
125
+ });
126
+ logExit();
127
+ throw error;
128
+ }
129
+ },
130
+
131
+ async getStsCredentials(replayData = {}) {
132
+ const logExit = logFunctionCall('auth.getStsCredentials');
133
+
134
+ logger.debug('Fetching STS credentials for upload...');
135
+ const token = await this.getToken();
136
+
137
+ logger.verbose('Making STS request', {
138
+ tokenPrefix: token.substring(0, 10) + '...',
139
+ replayData: {
140
+ id: replayData.id,
141
+ duration: replayData.duration,
142
+ title: replayData.title,
143
+ hasApps: !!replayData.apps,
144
+ hasIcons: !!replayData.icons,
145
+ hasProject: !!replayData.project
146
+ }
147
+ });
148
+
149
+ // Prepare the request body to match the desktop app
150
+ const requestBody = {
151
+ id: replayData.id,
152
+ duration: replayData.duration || 0,
153
+ apps: replayData.apps || [],
154
+ title: replayData.title || 'CLI Recording',
155
+ icons: replayData.icons || []
156
+ };
157
+
158
+ // Include project if provided
159
+ if (replayData.project) {
160
+ requestBody.project = replayData.project;
161
+ }
162
+
163
+ const response = await got.post('https://api.testdriver.ai/api/v1/replay/upload', {
164
+ headers: {
165
+ Authorization: `Bearer ${token}`
166
+ },
167
+ json: requestBody,
168
+ timeout: 30000
169
+ }).json();
170
+
171
+ logger.verbose('STS response received', {
172
+ hasVideo: !!response.video,
173
+ hasGif: !!response.gif,
174
+ hasImage: !!response.image,
175
+ hasIcons: !!response.icons
176
+ });
177
+
178
+ // The API returns separate STS credentials for video, gif, and image
179
+ // Each contains: accessKeyId, secretAccessKey, sessionToken, bucket, region, file
180
+ logExit();
181
+ return response;
182
+ },
183
+
184
+ async createLogSts(replayId, appId, name, type) {
185
+ const logExit = logFunctionCall('auth.createLogSts');
186
+
187
+ logger.debug('Creating log STS credentials', { replayId, appId, name, type });
188
+ const token = await this.getToken();
189
+
190
+ try {
191
+ const response = await got.post('https://api.testdriver.ai/api/v1/logs', {
192
+ headers: {
193
+ Authorization: `Bearer ${token}`
194
+ },
195
+ json: {
196
+ replayId,
197
+ appId,
198
+ name,
199
+ type
200
+ },
201
+ timeout: 30000
202
+ }).json();
203
+
204
+ logger.verbose('Log STS credentials created', {
205
+ logId: response.id,
206
+ hasCredentials: !!response.bucket
207
+ });
208
+
209
+ logExit();
210
+ return response;
211
+ } catch (error) {
212
+ logger.error('Failed to create log STS credentials:', {
213
+ message: error.message,
214
+ statusCode: error.response?.statusCode
215
+ });
216
+ logExit();
217
+ throw error;
218
+ } // Return the full response with video, gif, image objects
219
+ }
220
+ };
221
+
222
+ export { auth };
@@ -0,0 +1,21 @@
1
+ import ffmpegStatic from 'ffmpeg-static';
2
+ import ffprobeStatic from 'ffprobe-static';
3
+ import { logger } from './logger.js';
4
+
5
+ /**
6
+ * Get the path to the ffmpeg binary
7
+ * @returns {Promise<string>} Path to ffmpeg
8
+ */
9
+ export async function getFfmpegPath() {
10
+ logger.debug('Getting ffmpeg path from ffmpeg-static', { path: ffmpegStatic });
11
+ return ffmpegStatic;
12
+ }
13
+
14
+ /**
15
+ * Get the path to the ffprobe binary
16
+ * @returns {Promise<string>} Path to ffprobe
17
+ */
18
+ export async function getFfprobePath() {
19
+ logger.debug('Getting ffprobe path from ffprobe-static', { path: ffprobeStatic.path });
20
+ return ffprobeStatic.path;
21
+ }
package/lib/config.js ADDED
@@ -0,0 +1,34 @@
1
+ import { fileURLToPath } from 'url';
2
+ import { dirname, join } from 'path';
3
+ import { homedir } from 'os';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ export const ENV = process.env.NODE_ENV || 'production';
9
+
10
+ export const auth0Config = {
11
+ domain: 'replayable.us.auth0.com',
12
+ clientId: 'aYo59XVgKhhfrY9lFb35quLdMtF2j6WJ',
13
+ audience: 'https://replayable.us.auth0.com/api/v2/',
14
+ scopes: 'given_name profile email offline_access',
15
+ };
16
+
17
+ export const apiEndpoints = {
18
+ development: process.env.API_ENDPOINT || 'http://localhost:3000',
19
+ staging: 'https://replayable-api-staging.herokuapp.com',
20
+ production: 'https://api.testdriver.ai'
21
+ };
22
+
23
+ export const API_ENDPOINT = apiEndpoints[ENV];
24
+
25
+ // App configuration
26
+ export const APP = {
27
+ id: 'dashcam-cli',
28
+ name: ENV === 'production' ? 'Dashcam CLI' : `Dashcam CLI - ${ENV}`,
29
+ version: process.env.npm_package_version || '1.0.0',
30
+ configDir: join(homedir(), '.dashcam'),
31
+ logsDir: join(homedir(), '.dashcam', 'logs'),
32
+ recordingsDir: join(homedir(), '.dashcam', 'recordings'),
33
+ minRecordingDuration: 3000 // 3 seconds, matching desktop
34
+ };
@@ -0,0 +1,182 @@
1
+ import maskSensitiveData from 'mask-sensitive-data';
2
+
3
+ const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
4
+
5
+ const shouldCountEvent = (eventType) => {
6
+ return ['LOG_ERROR', 'LOG_EVENT', 'NETWORK_BEFORE_REQUEST'].includes(
7
+ eventType
8
+ );
9
+ };
10
+
11
+ const eventTypeToStatType = {
12
+ LOG_EVENT: 'logs',
13
+ LOG_ERROR: 'errors',
14
+ NETWORK_BEFORE_REQUEST: 'network',
15
+ };
16
+
17
+ const verifyPattern = (pattern, str = '') => {
18
+ if (typeof pattern !== 'string' || typeof str !== 'string')
19
+ throw new Error(
20
+ `verifyPattern expects two string arguments but instead got "pattern" of type ${typeof pattern} and "str" of type ${typeof str}`
21
+ );
22
+ return new RegExp(
23
+ '^' + pattern.split('*').map(escapeRegExp).join('.*'),
24
+ 'i'
25
+ ).test(str);
26
+ };
27
+
28
+ const updateTabsState = (event, tabs) => {
29
+ const { type, payload } = event;
30
+ switch (type) {
31
+ case 'INITIAL_TABS':
32
+ tabs = payload.reduce((tabs, tab) => {
33
+ if (tab.url) tabs[tab.tabId] = { ...tab, previousUrl: '' };
34
+ return tabs;
35
+ }, {});
36
+ break;
37
+ case 'TAB_REMOVED':
38
+ delete tabs[payload.tabId];
39
+ break;
40
+ case 'TAB_ACTIVATED':
41
+ tabs[payload.tabId] ??= payload;
42
+ case 'NAVIGATION_STARTED':
43
+ case 'NAVIGATION_COMPLETED':
44
+ if (tabs[payload.tabId] && tabs[payload.tabId].url !== payload.url) {
45
+ tabs[payload.tabId].previousUrl = tabs[payload.tabId].url;
46
+ tabs[payload.tabId].url = payload.url;
47
+ }
48
+ break;
49
+ default:
50
+ if (tabs[payload.tabId]) tabs[payload.tabId].previousUrl = '';
51
+ }
52
+
53
+ return tabs;
54
+ };
55
+
56
+ function sanitizeWebLogEventPayload(obj) {
57
+ let result = obj;
58
+ if (obj === null || obj === undefined) {
59
+ } else if (typeof obj === 'string')
60
+ result = maskSensitiveData.default(obj, {
61
+ ...maskSensitiveData.defaultMaskOptions,
62
+ jwtPattern: /\b(?:[A-Za-z0-9\-_=]{40,}|[A-Fa-f0-9\-_=]{40,})\b/g,
63
+ });
64
+ else if (Array.isArray(obj)) {
65
+ result = obj.map((element) => sanitizeWebLogEventPayload(element));
66
+ } else if (typeof obj === 'object') {
67
+ result = Object.entries(obj).reduce((result, [key, value]) => {
68
+ if (!key.toLowerCase().includes('url'))
69
+ result[key] = sanitizeWebLogEventPayload(value);
70
+ else result[key] = value;
71
+ return result;
72
+ }, {});
73
+ }
74
+ return result;
75
+ }
76
+
77
+ function filterWebEvents(
78
+ events,
79
+ groupLogsStatuses,
80
+ startMs = events[0]?.time ?? 0,
81
+ endMs = events[events.length - 1]?.time ?? 0
82
+ ) {
83
+ const tempEvents = events.filter(
84
+ event => event.type === 'INITIAL_TABS' || event.payload.tabId
85
+ );
86
+ const patterns = groupLogsStatuses
87
+ .map((status) => status.items.map((item) => item.item))
88
+ .flat();
89
+
90
+ const newEvents = [];
91
+ let tabs = {};
92
+ let tracked;
93
+ let map = {};
94
+
95
+ tempEvents
96
+ .filter((event) => event.time <= startMs)
97
+ .forEach((event) => (tabs = updateTabsState(event, tabs)));
98
+
99
+ tempEvents.push({
100
+ type: 'INITIAL_TABS',
101
+ time: startMs,
102
+ payload: Object.values(tabs).map(({ previousUrl, ...tab }) => tab),
103
+ });
104
+
105
+ for (const event of tempEvents.filter(
106
+ (event) => event.time >= startMs && event.time <= endMs
107
+ )) {
108
+ try {
109
+ switch (event.type) {
110
+ case 'NAVIGATION_STARTED':
111
+ case 'NAVIGATION_COMPLETED':
112
+ tracked = patterns.some((pattern) =>
113
+ verifyPattern(pattern, event.payload.url)
114
+ );
115
+ if (tracked) newEvents.push(event);
116
+ map[event.payload.tabId] = event.payload.url;
117
+ break;
118
+
119
+ case 'NETWORK_BEFORE_REQUEST':
120
+ tracked = patterns.some((pattern) =>
121
+ verifyPattern(pattern, map[event.payload.tabId])
122
+ );
123
+ if (tracked) newEvents.push(event);
124
+ break;
125
+
126
+ case 'NETWORK_COMPLETED_REQUEST':
127
+ case 'NETWORK_ERROR_REQUEST':
128
+ const startedEvent = newEvents.find(
129
+ (e) =>
130
+ e.payload.requestId === event.payload.requestId &&
131
+ e.type === 'NETWORK_BEFORE_REQUEST'
132
+ );
133
+ if (startedEvent) newEvents.push(event);
134
+ break;
135
+
136
+ case 'NETWORK_RESPONSE_BODY':
137
+ const completedEvent = newEvents.find(
138
+ (e) =>
139
+ e.payload.requestId === event.payload.requestId &&
140
+ e.type === 'NETWORK_COMPLETED_REQUEST'
141
+ );
142
+ if (completedEvent) newEvents.push(event);
143
+ break;
144
+
145
+ case 'LOG_ERROR':
146
+ case 'LOG_EVENT':
147
+ case 'SPA_NAVIGATION':
148
+ tracked = patterns.some((pattern) =>
149
+ verifyPattern(pattern, map[event.payload.tabId])
150
+ );
151
+ if (tracked) newEvents.push(event);
152
+ break;
153
+
154
+ case 'TAB_ACTIVATED':
155
+ tracked = patterns.some((pattern) =>
156
+ verifyPattern(pattern, event.payload.url)
157
+ );
158
+ if (tracked) {
159
+ map[event.payload.tabId] = event.payload.url;
160
+ newEvents.push(event);
161
+ }
162
+ break;
163
+
164
+ case 'INITIAL_TABS':
165
+ newEvents.push(event);
166
+ break;
167
+ }
168
+ } catch (err) {
169
+ console.error(err);
170
+ }
171
+ }
172
+ return newEvents;
173
+ }
174
+
175
+ export {
176
+ verifyPattern,
177
+ updateTabsState,
178
+ filterWebEvents,
179
+ shouldCountEvent,
180
+ eventTypeToStatType,
181
+ sanitizeWebLogEventPayload,
182
+ };