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.
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 +703 -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
@@ -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 };