dashcam 0.8.4 → 1.0.1-beta.11
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/.dashcam/cli-config.json +3 -0
- package/.dashcam/recording.log +135 -0
- package/.dashcam/web-config.json +11 -0
- package/.github/RELEASE.md +59 -0
- package/.github/workflows/publish.yml +43 -0
- package/BACKWARD_COMPATIBILITY.md +177 -0
- package/LOG_TRACKING_GUIDE.md +225 -0
- package/README.md +709 -155
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.cjs +8 -0
- package/bin/dashcam.js +703 -0
- package/bin/index.js +63 -0
- package/examples/execute-script.js +152 -0
- package/examples/simple-test.js +37 -0
- package/lib/applicationTracker.js +311 -0
- package/lib/auth.js +222 -0
- package/lib/binaries.js +21 -0
- package/lib/config.js +34 -0
- package/lib/extension-logs/helpers.js +182 -0
- package/lib/extension-logs/index.js +347 -0
- package/lib/extension-logs/manager.js +344 -0
- package/lib/ffmpeg.js +155 -0
- package/lib/logTracker.js +23 -0
- package/lib/logger.js +118 -0
- package/lib/logs/index.js +488 -0
- package/lib/permissions.js +85 -0
- package/lib/processManager.js +317 -0
- package/lib/recorder.js +690 -0
- package/lib/store.js +58 -0
- package/lib/tracking/FileTracker.js +105 -0
- package/lib/tracking/FileTrackerManager.js +62 -0
- package/lib/tracking/LogsTracker.js +161 -0
- package/lib/tracking/active-win.js +212 -0
- package/lib/tracking/icons/darwin.js +39 -0
- package/lib/tracking/icons/index.js +167 -0
- package/lib/tracking/icons/windows.js +27 -0
- package/lib/tracking/idle.js +82 -0
- package/lib/tracking.js +23 -0
- package/lib/uploader.js +456 -0
- package/lib/utilities/jsonl.js +77 -0
- package/lib/webLogsDaemon.js +234 -0
- package/lib/websocket/server.js +223 -0
- package/package.json +53 -21
- package/recording.log +814 -0
- package/sea-bundle.mjs +34595 -0
- package/test-page.html +15 -0
- package/test.log +1 -0
- package/test_run.log +48 -0
- package/test_workflow.sh +154 -0
- package/examples/crash-test.js +0 -11
- package/examples/github-issue.sh +0 -1
- package/examples/protocol.html +0 -22
- package/index.js +0 -158
- package/lib.js +0 -199
- package/recorder.js +0 -85
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { logger } from '../logger.js';
|
|
3
|
+
import { jsonl } from '../utilities/jsonl.js';
|
|
4
|
+
import {
|
|
5
|
+
verifyPattern,
|
|
6
|
+
updateTabsState,
|
|
7
|
+
shouldCountEvent,
|
|
8
|
+
eventTypeToStatType,
|
|
9
|
+
sanitizeWebLogEventPayload,
|
|
10
|
+
} from './helpers.js';
|
|
11
|
+
|
|
12
|
+
// Simple ref implementation for reactivity (adapted from Vue's ref)
|
|
13
|
+
function ref(value) {
|
|
14
|
+
return {
|
|
15
|
+
value,
|
|
16
|
+
_isRef: true
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function computed(fn) {
|
|
21
|
+
// Don't cache - always recompute to ensure reactivity
|
|
22
|
+
return {
|
|
23
|
+
get value() {
|
|
24
|
+
return fn();
|
|
25
|
+
},
|
|
26
|
+
_isComputed: true
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function effect(fn) {
|
|
31
|
+
fn();
|
|
32
|
+
return () => {}; // Simplified cleanup
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Simple getStats function (same as used in FileTracker)
|
|
36
|
+
function getStats(eventTimes = []) {
|
|
37
|
+
const endTime = Date.now();
|
|
38
|
+
const startTime = Date.now() - 60000;
|
|
39
|
+
|
|
40
|
+
let startIndex = 0;
|
|
41
|
+
let count = 0;
|
|
42
|
+
|
|
43
|
+
for (const time of eventTimes) {
|
|
44
|
+
if (time < startTime) startIndex++;
|
|
45
|
+
else if (time <= endTime) {
|
|
46
|
+
count++;
|
|
47
|
+
} else break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
eventTimes: eventTimes.slice(startIndex),
|
|
52
|
+
count,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const filename = 'dashcam_logs_web_events.jsonl';
|
|
57
|
+
|
|
58
|
+
class WebLogsTracker {
|
|
59
|
+
// Omitting a directory puts it in a watch only mode where it
|
|
60
|
+
// only collect per minute stats
|
|
61
|
+
constructor({ config, webTrackerManager, directory }) {
|
|
62
|
+
logger.debug('WebLogsTracker: Initializing', {
|
|
63
|
+
isWatchOnly: !directory,
|
|
64
|
+
configKeys: Object.keys(config),
|
|
65
|
+
configCount: Object.keys(config).length
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// if watchOnly, the configs in the argument will be a ref
|
|
69
|
+
// but if tracked for a recording the configs will be static
|
|
70
|
+
// we wrap it in a ref just for a consistent way to access it
|
|
71
|
+
this.tabs = {};
|
|
72
|
+
this.events = new Set();
|
|
73
|
+
this.config = ref(config);
|
|
74
|
+
this.trackedPatterns = {};
|
|
75
|
+
this.eventTimesByUrl = {};
|
|
76
|
+
this.lastActiveTabId = null;
|
|
77
|
+
this.isWatchOnly = !directory;
|
|
78
|
+
this.webTrackerManager = webTrackerManager;
|
|
79
|
+
this.fullLogs = { tabs: {}, activeTabs: [] };
|
|
80
|
+
this.fileLocation = this.isWatchOnly ? '' : path.join(directory, filename);
|
|
81
|
+
|
|
82
|
+
this.patterns = computed(
|
|
83
|
+
() =>
|
|
84
|
+
new Set(
|
|
85
|
+
Object.values(this.config.value)
|
|
86
|
+
.map((config) => config.patterns)
|
|
87
|
+
.flat()
|
|
88
|
+
)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
logger.debug('WebLogsTracker: Subscribing to global tabs and navigation events');
|
|
92
|
+
this.unsubscribeFromGlobalTabsAndNavigation =
|
|
93
|
+
this.webTrackerManager.subscribeToGlobalTabsAndNavigation((event) =>
|
|
94
|
+
this.#handleEvent(event)
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
this.watchPatternsEffect = effect(() => {
|
|
98
|
+
const patternsToRemove = Object.keys(this.trackedPatterns).filter(
|
|
99
|
+
(pattern) => !this.patterns.value.has(pattern)
|
|
100
|
+
);
|
|
101
|
+
const newPatterns = [...this.patterns.value].filter(
|
|
102
|
+
(pattern) => !this.trackedPatterns[pattern]
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
logger.debug('WebLogsTracker: Updating pattern subscriptions', {
|
|
106
|
+
patternsToRemove: patternsToRemove.length,
|
|
107
|
+
newPatterns: newPatterns.length,
|
|
108
|
+
totalPatterns: this.patterns.value.size
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
newPatterns.forEach((pattern) => this.#startWatchingPattern(pattern));
|
|
112
|
+
patternsToRemove.forEach((pattern) => this.#stopWatchingPattern(pattern));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
updateConfig(config) {
|
|
117
|
+
this.config.value = config;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#startWatchingPattern(pattern) {
|
|
121
|
+
if (this.trackedPatterns[pattern]) {
|
|
122
|
+
logger.debug('WebLogsTracker: Pattern already being watched', { pattern });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
logger.debug('WebLogsTracker: Starting to watch pattern', { pattern });
|
|
127
|
+
this.trackedPatterns[pattern] = this.webTrackerManager.subscribe(
|
|
128
|
+
pattern,
|
|
129
|
+
(event) => this.#handleEvent(event)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#stopWatchingPattern(pattern) {
|
|
134
|
+
const unsubscribe = this.trackedPatterns[pattern];
|
|
135
|
+
if (unsubscribe) {
|
|
136
|
+
logger.debug('WebLogsTracker: Stopping watch for pattern', { pattern });
|
|
137
|
+
unsubscribe();
|
|
138
|
+
Object.keys(this.eventTimesByUrl)
|
|
139
|
+
.filter((shortUrl) => verifyPattern(pattern, shortUrl))
|
|
140
|
+
.forEach((shortUrl) => {
|
|
141
|
+
logger.debug('WebLogsTracker: Removing event times for URL', { shortUrl, pattern });
|
|
142
|
+
delete this.eventTimesByUrl[shortUrl];
|
|
143
|
+
});
|
|
144
|
+
delete this.trackedPatterns[pattern];
|
|
145
|
+
} else {
|
|
146
|
+
logger.debug('WebLogsTracker: Pattern not being watched', { pattern });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getStatus() {
|
|
151
|
+
const statuses = Object.values(this.config.value).map(
|
|
152
|
+
({ id, type, name, patterns }) => {
|
|
153
|
+
const items = Object.values(this.eventTimesByUrl)
|
|
154
|
+
.filter(({ shortUrl }) =>
|
|
155
|
+
patterns.some((pattern) => verifyPattern(pattern, shortUrl))
|
|
156
|
+
)
|
|
157
|
+
.map(({ stats, shortUrl, logsCount, errorsCount, networkCount }) => {
|
|
158
|
+
if (this.isWatchOnly) {
|
|
159
|
+
const { eventTimes: l, count: lCount } = getStats(stats.logs);
|
|
160
|
+
const { eventTimes: e, count: eCount } = getStats(stats.errors);
|
|
161
|
+
const { eventTimes: n, count: nCount } = getStats(stats.network);
|
|
162
|
+
|
|
163
|
+
stats.logs = l;
|
|
164
|
+
stats.errors = e;
|
|
165
|
+
stats.network = n;
|
|
166
|
+
logsCount = lCount;
|
|
167
|
+
errorsCount = eCount;
|
|
168
|
+
networkCount = nCount;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
item: shortUrl,
|
|
173
|
+
count: logsCount + errorsCount + networkCount,
|
|
174
|
+
counts: {
|
|
175
|
+
logs: logsCount,
|
|
176
|
+
errors: errorsCount,
|
|
177
|
+
network: networkCount,
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
type,
|
|
185
|
+
name,
|
|
186
|
+
items,
|
|
187
|
+
fileLocation: this.fileLocation,
|
|
188
|
+
count: items.reduce((sum, item) => sum + item.count, 0),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return statuses;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#handleEvent(event) {
|
|
197
|
+
if (this.events.has(event)) {
|
|
198
|
+
logger.debug('WebLogsTracker: Ignoring duplicate event', { type: event.type });
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.events.add(event);
|
|
202
|
+
|
|
203
|
+
logger.debug('WebLogsTracker: Processing event', {
|
|
204
|
+
type: event.type,
|
|
205
|
+
time: event.time,
|
|
206
|
+
tabId: event.payload?.tabId,
|
|
207
|
+
url: event.payload?.url?.substring(0, 100)
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
this.tabs = updateTabsState(event, this.tabs);
|
|
211
|
+
|
|
212
|
+
const { type, time, payload } = event;
|
|
213
|
+
const patterns = [...this.patterns.value];
|
|
214
|
+
|
|
215
|
+
let newEvent = { ...event };
|
|
216
|
+
switch (type) {
|
|
217
|
+
case 'INITIAL_TABS':
|
|
218
|
+
newEvent.payload = event.payload.filter((tab) =>
|
|
219
|
+
patterns.some((pattern) => verifyPattern(pattern, tab.url))
|
|
220
|
+
);
|
|
221
|
+
logger.debug('WebLogsTracker: Filtered initial tabs', {
|
|
222
|
+
originalCount: event.payload.length,
|
|
223
|
+
filteredCount: newEvent.payload.length
|
|
224
|
+
});
|
|
225
|
+
break;
|
|
226
|
+
|
|
227
|
+
case 'TAB_REMOVED':
|
|
228
|
+
logger.debug('WebLogsTracker: Tab removed event, marking as null');
|
|
229
|
+
newEvent = null;
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case 'TAB_ACTIVATED':
|
|
233
|
+
const tabId = patterns.some((pattern) =>
|
|
234
|
+
verifyPattern(pattern, this.tabs[payload.tabId]?.url)
|
|
235
|
+
)
|
|
236
|
+
? payload.tabId
|
|
237
|
+
: null;
|
|
238
|
+
|
|
239
|
+
if (tabId !== this.lastActiveTabId) {
|
|
240
|
+
logger.debug('WebLogsTracker: Active tab changed', {
|
|
241
|
+
from: this.lastActiveTabId,
|
|
242
|
+
to: tabId,
|
|
243
|
+
url: this.tabs[tabId]?.url?.substring(0, 100)
|
|
244
|
+
});
|
|
245
|
+
this.lastActiveTabId = tabId;
|
|
246
|
+
if (!tabId) {
|
|
247
|
+
newEvent.payload = { tabId: null, windowId: null };
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
logger.debug('WebLogsTracker: Active tab unchanged, ignoring event');
|
|
251
|
+
newEvent = null;
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
|
|
255
|
+
case 'NAVIGATION_STARTED':
|
|
256
|
+
case 'NAVIGATION_COMPLETED':
|
|
257
|
+
if (
|
|
258
|
+
!patterns.some((ptrn) =>
|
|
259
|
+
verifyPattern(ptrn, this.tabs[payload.tabId]?.url)
|
|
260
|
+
)
|
|
261
|
+
) {
|
|
262
|
+
logger.debug('WebLogsTracker: Navigation event does not match patterns, ignoring', {
|
|
263
|
+
type,
|
|
264
|
+
url: this.tabs[payload.tabId]?.url?.substring(0, 100)
|
|
265
|
+
});
|
|
266
|
+
newEvent = null;
|
|
267
|
+
} else {
|
|
268
|
+
logger.debug('WebLogsTracker: Navigation event matches pattern', {
|
|
269
|
+
type,
|
|
270
|
+
url: this.tabs[payload.tabId]?.url?.substring(0, 100)
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
break;
|
|
274
|
+
|
|
275
|
+
case 'LOG_EVENT':
|
|
276
|
+
case 'LOG_ERROR':
|
|
277
|
+
case 'NETWORK_BEFORE_REQUEST':
|
|
278
|
+
case 'NETWORK_COMPLETED_REQUEST':
|
|
279
|
+
case 'NETWORK_RESPONSE':
|
|
280
|
+
case 'NETWORK_RESPONSE_BODY':
|
|
281
|
+
// These events are already filtered by pattern in WebTrackerManager
|
|
282
|
+
// They should be written as-is
|
|
283
|
+
logger.debug('WebLogsTracker: Processing web event', {
|
|
284
|
+
type,
|
|
285
|
+
tabId: payload?.tabId,
|
|
286
|
+
url: this.tabs[payload.tabId]?.url?.substring(0, 100)
|
|
287
|
+
});
|
|
288
|
+
break;
|
|
289
|
+
|
|
290
|
+
default:
|
|
291
|
+
logger.debug('WebLogsTracker: Unknown event type, passing through', { type });
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!newEvent) {
|
|
296
|
+
logger.debug('WebLogsTracker: Event filtered out, not processing further');
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!this.isWatchOnly) {
|
|
301
|
+
logger.debug('WebLogsTracker: Writing event to file', { fileLocation: this.fileLocation });
|
|
302
|
+
jsonl.append(this.fileLocation, {
|
|
303
|
+
...newEvent,
|
|
304
|
+
payload: sanitizeWebLogEventPayload(newEvent.payload),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this.tabs[payload.tabId] && shouldCountEvent(type)) {
|
|
309
|
+
const shortUrl = this.tabs[payload.tabId].url.split('?')[0];
|
|
310
|
+
this.eventTimesByUrl[shortUrl] ??= {
|
|
311
|
+
shortUrl,
|
|
312
|
+
stats: {
|
|
313
|
+
logs: [],
|
|
314
|
+
errors: [],
|
|
315
|
+
network: [],
|
|
316
|
+
},
|
|
317
|
+
logsCount: 0,
|
|
318
|
+
errorsCount: 0,
|
|
319
|
+
networkCount: 0,
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const statType = eventTypeToStatType[type];
|
|
323
|
+
if (statType) {
|
|
324
|
+
this.eventTimesByUrl[shortUrl][`${statType}Count`]++;
|
|
325
|
+
this.eventTimesByUrl[shortUrl].stats[statType].push(time);
|
|
326
|
+
logger.debug('WebLogsTracker: Updated stats for URL', {
|
|
327
|
+
shortUrl,
|
|
328
|
+
statType,
|
|
329
|
+
newCount: this.eventTimesByUrl[shortUrl][`${statType}Count`]
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
destroy() {
|
|
336
|
+
const status = this.getStatus();
|
|
337
|
+
// stop(this.watchPatternsEffect); // Simplified for CLI
|
|
338
|
+
this.events.clear();
|
|
339
|
+
this.unsubscribeFromGlobalTabsAndNavigation();
|
|
340
|
+
Object.keys(this.trackedPatterns).forEach((pattern) => {
|
|
341
|
+
this.#stopWatchingPattern(pattern);
|
|
342
|
+
});
|
|
343
|
+
return status;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export { WebLogsTracker };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { ref } from '../websocket/server.js';
|
|
2
|
+
import { verifyPattern, updateTabsState } from './helpers.js';
|
|
3
|
+
import { logger } from '../logger.js';
|
|
4
|
+
|
|
5
|
+
const endMessage = {
|
|
6
|
+
type: 'STOP_RECORDING',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// Simple reactive implementations (simplified versions for CLI)
|
|
10
|
+
function reactive(obj) {
|
|
11
|
+
return obj;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function computed(fn) {
|
|
15
|
+
return { get value() { return fn(); } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function effect(fn) {
|
|
19
|
+
fn();
|
|
20
|
+
return fn;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class WebTrackerManager {
|
|
24
|
+
constructor(server) {
|
|
25
|
+
this.tabs = {};
|
|
26
|
+
this.cleanups = [];
|
|
27
|
+
this.server = server;
|
|
28
|
+
this.isListening = ref(server.isListening?.value || false);
|
|
29
|
+
this.patternsByCallback = reactive(new Map());
|
|
30
|
+
this.globalTabsAndNavigationCallbacks = reactive(new Set());
|
|
31
|
+
this.eventCallbacks = computed(
|
|
32
|
+
() => new Set(this.patternsByCallback.keys())
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
this.watchCallbacksSizeEffect = effect(() => {
|
|
36
|
+
const size =
|
|
37
|
+
this.eventCallbacks.value.size +
|
|
38
|
+
this.globalTabsAndNavigationCallbacks.size;
|
|
39
|
+
const isListening = this.isListening.value;
|
|
40
|
+
logger.debug('WebTrackerManager: Effect triggered', { size, isListening, shouldStart: isListening && size > 0 });
|
|
41
|
+
if (!isListening || size === 0) {
|
|
42
|
+
this.#stop();
|
|
43
|
+
} else this.#start();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get isStarted() {
|
|
48
|
+
return this.cleanups.length > 0 && this.isListening.value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#start() {
|
|
52
|
+
if (!this.isListening.value) {
|
|
53
|
+
logger.debug('WebTrackerManager: Not starting because WebSocket server is not listening');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (this.cleanups.length > 0) {
|
|
58
|
+
logger.debug('WebTrackerManager: Already started, skipping duplicate start');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Collect all patterns from the patternsByCallback map
|
|
63
|
+
const patterns = Array.from(new Set(this.patternsByCallback.values()));
|
|
64
|
+
logger.info('WebTrackerManager: Starting tracking...', { patterns, callbackCount: this.patternsByCallback.size });
|
|
65
|
+
|
|
66
|
+
const startMessage = {
|
|
67
|
+
type: 'START_RECORDING',
|
|
68
|
+
payload: patterns.length > 0 ? patterns : ['*'], // Default to all if no patterns
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
this.server.broadcast(startMessage);
|
|
72
|
+
logger.info('WebTrackerManager: Broadcasted start message to all clients', { patterns });
|
|
73
|
+
|
|
74
|
+
const messageCleanup = this.server.on('message', (event) => {
|
|
75
|
+
logger.debug('WebTrackerManager: Received message from server', { eventType: event.type, hasPayload: !!event.payload });
|
|
76
|
+
this.#handleEvent(event);
|
|
77
|
+
});
|
|
78
|
+
const connectionCleanup = this.server.on('connection', (client) => {
|
|
79
|
+
// Send current patterns to the new client
|
|
80
|
+
const patterns = Array.from(new Set(this.patternsByCallback.values()));
|
|
81
|
+
logger.info('WebTrackerManager: New client connected, sending start message', {
|
|
82
|
+
patterns,
|
|
83
|
+
callbackCount: this.patternsByCallback.size
|
|
84
|
+
});
|
|
85
|
+
const startMessage = {
|
|
86
|
+
type: 'START_RECORDING',
|
|
87
|
+
payload: patterns.length > 0 ? patterns : ['*'],
|
|
88
|
+
};
|
|
89
|
+
this.server.send(client, startMessage);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.cleanups.push(messageCleanup, connectionCleanup);
|
|
93
|
+
logger.debug('WebTrackerManager: Started successfully, registered event handlers');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#stop() {
|
|
97
|
+
logger.debug('WebTrackerManager: Stopping tracking...');
|
|
98
|
+
if (this.isListening.value) {
|
|
99
|
+
this.server.broadcast(endMessage);
|
|
100
|
+
logger.debug('WebTrackerManager: Broadcasted stop message to all clients');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.cleanups.forEach((cleanupFn) => cleanupFn());
|
|
104
|
+
this.cleanups = [];
|
|
105
|
+
logger.debug('WebTrackerManager: Stopped and cleaned up event handlers');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
subscribeToGlobalTabsAndNavigation(callback) {
|
|
109
|
+
logger.debug('WebTrackerManager: Subscribing to global tabs/navigation events');
|
|
110
|
+
this.globalTabsAndNavigationCallbacks.add(callback);
|
|
111
|
+
this.#tryCallback(callback, this.#getInitialTabs());
|
|
112
|
+
// Manually trigger effect since we're using a simplified effect implementation
|
|
113
|
+
const size =
|
|
114
|
+
this.eventCallbacks.value.size +
|
|
115
|
+
this.globalTabsAndNavigationCallbacks.size;
|
|
116
|
+
if (this.isListening.value && size > 0) {
|
|
117
|
+
this.#start();
|
|
118
|
+
}
|
|
119
|
+
return () => this.unsubscribeFromGlobalTabsAndNavigation(callback);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
unsubscribeFromGlobalTabsAndNavigation(callback) {
|
|
123
|
+
logger.debug('WebTrackerManager: Unsubscribing from global tabs/navigation events');
|
|
124
|
+
this.globalTabsAndNavigationCallbacks.delete(callback);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
subscribe(pattern, callback) {
|
|
128
|
+
logger.info('WebTrackerManager: Subscribing to pattern', { pattern, isStarted: this.isStarted });
|
|
129
|
+
this.patternsByCallback.set(callback, pattern);
|
|
130
|
+
|
|
131
|
+
// If already started, send updated patterns to all clients
|
|
132
|
+
if (this.cleanups.length > 0 && this.isListening.value) {
|
|
133
|
+
const patterns = Array.from(new Set(this.patternsByCallback.values()));
|
|
134
|
+
logger.info('WebTrackerManager: Sending updated patterns to clients', { patterns, callbackCount: this.patternsByCallback.size });
|
|
135
|
+
const startMessage = {
|
|
136
|
+
type: 'START_RECORDING',
|
|
137
|
+
payload: patterns,
|
|
138
|
+
};
|
|
139
|
+
this.server.broadcast(startMessage);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Manually trigger effect since we're using a simplified effect implementation
|
|
143
|
+
const size =
|
|
144
|
+
this.eventCallbacks.value.size +
|
|
145
|
+
this.globalTabsAndNavigationCallbacks.size;
|
|
146
|
+
if (this.isListening.value && size > 0) {
|
|
147
|
+
this.#start();
|
|
148
|
+
}
|
|
149
|
+
return () => this.unsubscribe(callback);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
unsubscribe(callback) {
|
|
153
|
+
const pattern = this.patternsByCallback.get(callback);
|
|
154
|
+
logger.debug('WebTrackerManager: Unsubscribing from pattern', { pattern });
|
|
155
|
+
this.patternsByCallback.delete(callback);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get the DOM from a webpage through the Chrome extension
|
|
160
|
+
* @param {Object} options - Execution options
|
|
161
|
+
* @param {number} [options.tabId] - Specific tab ID (optional, uses active tab if not provided)
|
|
162
|
+
* @param {string} [options.url] - URL pattern to match (optional)
|
|
163
|
+
* @param {boolean} [options.allFrames=false] - Get from all frames
|
|
164
|
+
* @returns {Promise<Object>} - Resolves with {html, title, url} from the page
|
|
165
|
+
*/
|
|
166
|
+
getPageDOM({ tabId, url, allFrames = false } = {}) {
|
|
167
|
+
return new Promise((resolve, reject) => {
|
|
168
|
+
if (!this.isListening.value) {
|
|
169
|
+
logger.error('WebTrackerManager: Cannot execute script, WebSocket server is not listening');
|
|
170
|
+
return reject(new Error('WebSocket server is not listening'));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const requestId = `getdom_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
174
|
+
const message = {
|
|
175
|
+
type: 'GET_PAGE_DOM',
|
|
176
|
+
requestId,
|
|
177
|
+
payload: {
|
|
178
|
+
tabId,
|
|
179
|
+
url,
|
|
180
|
+
allFrames,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
logger.info('WebTrackerManager: Sending get page DOM request', {
|
|
185
|
+
requestId,
|
|
186
|
+
tabId,
|
|
187
|
+
url
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Set up one-time listener for the response
|
|
191
|
+
const timeout = setTimeout(() => {
|
|
192
|
+
logger.error('WebTrackerManager: Script execution timed out', { requestId });
|
|
193
|
+
cleanup();
|
|
194
|
+
reject(new Error('Script execution timed out'));
|
|
195
|
+
}, 30000); // 30 second timeout
|
|
196
|
+
|
|
197
|
+
const responseHandler = (event) => {
|
|
198
|
+
logger.debug('WebTrackerManager: Received message in responseHandler', {
|
|
199
|
+
type: event.type,
|
|
200
|
+
requestId: event.requestId,
|
|
201
|
+
matchesOurRequest: event.requestId === requestId
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (event.type === 'DOM_RESULT' && event.requestId === requestId) {
|
|
205
|
+
clearTimeout(timeout);
|
|
206
|
+
cleanup();
|
|
207
|
+
|
|
208
|
+
if (event.error) {
|
|
209
|
+
logger.error('WebTrackerManager: Failed to get DOM', {
|
|
210
|
+
requestId,
|
|
211
|
+
error: event.error
|
|
212
|
+
});
|
|
213
|
+
reject(new Error(event.error));
|
|
214
|
+
} else {
|
|
215
|
+
logger.info('WebTrackerManager: Got DOM successfully', { requestId });
|
|
216
|
+
resolve(event.result);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const cleanup = this.server.on('message', responseHandler);
|
|
222
|
+
this.server.broadcast(message);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#getInitialTabs(time = Date.now()) {
|
|
227
|
+
return {
|
|
228
|
+
time,
|
|
229
|
+
type: 'INITIAL_TABS',
|
|
230
|
+
payload: Object.values(this.tabs).map(({ previousUrl, ...tab }) => tab),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#shouldSendEvent(pattern, event, cache = {}) {
|
|
235
|
+
if (cache[pattern] !== undefined) return cache[pattern];
|
|
236
|
+
|
|
237
|
+
const tab = this.tabs[event.payload.tabId];
|
|
238
|
+
if (tab) {
|
|
239
|
+
const urlMatches = verifyPattern(pattern, tab.url);
|
|
240
|
+
const previousUrlMatches = verifyPattern(pattern, tab.previousUrl);
|
|
241
|
+
|
|
242
|
+
logger.info('WebTrackerManager: Checking pattern match', {
|
|
243
|
+
pattern,
|
|
244
|
+
tabUrl: tab.url,
|
|
245
|
+
previousUrl: tab.previousUrl,
|
|
246
|
+
urlMatches,
|
|
247
|
+
previousUrlMatches,
|
|
248
|
+
willMatch: urlMatches || previousUrlMatches
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (urlMatches || previousUrlMatches) {
|
|
252
|
+
cache[pattern] = true;
|
|
253
|
+
} else {
|
|
254
|
+
cache[pattern] = false;
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
cache[pattern] = false;
|
|
258
|
+
logger.debug('WebTrackerManager: Tab not found in tabs map', {
|
|
259
|
+
tabId: event.payload.tabId,
|
|
260
|
+
availableTabs: Object.keys(this.tabs).length
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return cache[pattern];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#handleEvent(event) {
|
|
268
|
+
logger.info('WebTrackerManager: Received event', {
|
|
269
|
+
type: event.type,
|
|
270
|
+
tabId: event.payload?.tabId,
|
|
271
|
+
url: event.payload?.url?.substring(0, 100),
|
|
272
|
+
requestUrl: event.payload?.requestUrl?.substring(0, 100)
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
this.tabs = updateTabsState(event, this.tabs);
|
|
276
|
+
const { payload, type } = event;
|
|
277
|
+
switch (type) {
|
|
278
|
+
case 'INITIAL_TABS':
|
|
279
|
+
case 'TAB_REMOVED':
|
|
280
|
+
case 'TAB_ACTIVATED':
|
|
281
|
+
case 'NAVIGATION_STARTED':
|
|
282
|
+
case 'NAVIGATION_COMPLETED':
|
|
283
|
+
logger.debug('WebTrackerManager: Broadcasting global tabs/navigation event', { type, tabCount: event.payload?.length || 1 });
|
|
284
|
+
this.globalTabsAndNavigationCallbacks.forEach((callback) => {
|
|
285
|
+
this.#tryCallback(callback, event);
|
|
286
|
+
});
|
|
287
|
+
break;
|
|
288
|
+
|
|
289
|
+
default:
|
|
290
|
+
const cache = {};
|
|
291
|
+
let matchedCallbacks = 0;
|
|
292
|
+
|
|
293
|
+
logger.info('WebTrackerManager: Processing non-nav event', {
|
|
294
|
+
type,
|
|
295
|
+
callbackCount: this.eventCallbacks.value.size,
|
|
296
|
+
patternCount: this.patternsByCallback.size
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
this.eventCallbacks.value.forEach((callback) => {
|
|
300
|
+
const pattern = this.patternsByCallback.get(callback);
|
|
301
|
+
logger.info('WebTrackerManager: Checking callback', { pattern, hasPattern: !!pattern });
|
|
302
|
+
if (pattern && this.#shouldSendEvent(pattern, event, cache)) {
|
|
303
|
+
logger.debug('WebTrackerManager: Event matches pattern, sending to callback', {
|
|
304
|
+
type,
|
|
305
|
+
pattern,
|
|
306
|
+
tabUrl: this.tabs[event.payload?.tabId]?.url?.substring(0, 100)
|
|
307
|
+
});
|
|
308
|
+
this.#tryCallback(callback, event);
|
|
309
|
+
matchedCallbacks++;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
if (matchedCallbacks === 0) {
|
|
314
|
+
const availablePatterns = Array.from(this.patternsByCallback.values());
|
|
315
|
+
logger.debug('WebTrackerManager: Event did not match any patterns', {
|
|
316
|
+
type,
|
|
317
|
+
tabId: event.payload?.tabId,
|
|
318
|
+
tabUrl: this.tabs[event.payload?.tabId]?.url,
|
|
319
|
+
availablePatterns,
|
|
320
|
+
totalTabs: Object.keys(this.tabs).length
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#tryCallback(callback, event) {
|
|
327
|
+
try {
|
|
328
|
+
callback(event);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
logger.error(
|
|
331
|
+
'Failed sending ExtensionTracker event',
|
|
332
|
+
{ event, error }
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
destroy() {
|
|
338
|
+
this.#stop();
|
|
339
|
+
this.patternsByCallback.clear();
|
|
340
|
+
this.globalTabsAndNavigationCallbacks.clear();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export { WebTrackerManager, reactive, computed, effect };
|