chrometools-mcp 1.9.1 → 2.3.2

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,298 @@
1
+ /**
2
+ * browser/page-manager.js
3
+ *
4
+ * Page lifecycle and monitoring management
5
+ */
6
+
7
+ import { getBrowser } from './browser-manager.js';
8
+ import { injectRecorder } from '../recorder/recorder-script.js';
9
+
10
+ /**
11
+ * Debug log helper (only logs to stderr when DEBUG=1)
12
+ * @param {...any} args - Arguments to log
13
+ */
14
+ function debugLog(...args) {
15
+ if (process.env.DEBUG === '1') {
16
+ console.error('[page-manager]', ...args);
17
+ }
18
+ }
19
+
20
+ // Global state for pages and monitoring
21
+ export const openPages = new Map();
22
+ export let lastPage = null;
23
+
24
+ // Console logs storage
25
+ export const consoleLogs = [];
26
+
27
+ // Network requests storage
28
+ export const networkRequests = [];
29
+
30
+ // Page analysis cache
31
+ export const pageAnalysisCache = new Map();
32
+
33
+ // Track pages with recorder injected
34
+ export const pagesWithRecorder = new WeakSet();
35
+
36
+ // Track pages with network monitoring to prevent duplicate setup
37
+ const pagesWithNetworkMonitoring = new WeakSet();
38
+
39
+ /**
40
+ * Setup network monitoring with auto-reinitialization on navigation
41
+ * @param {Page} page - Puppeteer page instance
42
+ */
43
+ export async function setupNetworkMonitoring(page) {
44
+ // Prevent duplicate setup on the same page
45
+ if (pagesWithNetworkMonitoring.has(page)) {
46
+ return;
47
+ }
48
+ pagesWithNetworkMonitoring.add(page);
49
+
50
+ const client = await page.target().createCDPSession();
51
+ await client.send('Network.enable');
52
+
53
+ client.on('Network.requestWillBeSent', (event) => {
54
+ const timestamp = new Date().toISOString();
55
+ networkRequests.push({
56
+ requestId: event.requestId,
57
+ url: event.request.url,
58
+ method: event.request.method,
59
+ headers: event.request.headers,
60
+ postData: event.request.postData,
61
+ timestamp,
62
+ type: event.type, // Document, Stylesheet, Image, Media, Font, Script, XHR, Fetch, etc.
63
+ initiator: event.initiator.type, // parser, script, other
64
+ status: 'pending',
65
+ documentURL: event.documentURL
66
+ });
67
+ });
68
+
69
+ client.on('Network.responseReceived', (event) => {
70
+ const req = networkRequests.find(r => r.requestId === event.requestId);
71
+ if (req) {
72
+ req.status = event.response.status;
73
+ req.statusText = event.response.statusText;
74
+ req.responseHeaders = event.response.headers;
75
+ req.mimeType = event.response.mimeType;
76
+ req.fromCache = event.response.fromDiskCache || event.response.fromServiceWorker;
77
+ req.timing = event.response.timing;
78
+ }
79
+ });
80
+
81
+ client.on('Network.loadingFinished', (event) => {
82
+ const req = networkRequests.find(r => r.requestId === event.requestId);
83
+ if (req && req.status === 'pending') {
84
+ req.status = 'completed';
85
+ }
86
+ if (req) {
87
+ req.encodedDataLength = event.encodedDataLength;
88
+ req.finishedTimestamp = new Date().toISOString();
89
+ }
90
+ });
91
+
92
+ client.on('Network.loadingFailed', (event) => {
93
+ const req = networkRequests.find(r => r.requestId === event.requestId);
94
+ if (req) {
95
+ req.status = 'failed';
96
+ req.errorText = event.errorText;
97
+ req.canceled = event.canceled;
98
+ req.finishedTimestamp = new Date().toISOString();
99
+ }
100
+ });
101
+
102
+ // Auto-reinitialize on navigation (CDP session is reset on navigation)
103
+ let lastUrl = page.url();
104
+
105
+ page.on('framenavigated', async (frame) => {
106
+ // Only handle main frame navigation
107
+ if (frame !== page.mainFrame()) return;
108
+
109
+ const currentUrl = frame.url();
110
+
111
+ // Skip if URL hasn't changed
112
+ if (currentUrl === lastUrl) return;
113
+ lastUrl = currentUrl;
114
+
115
+ // Remove from tracking set to allow re-setup
116
+ pagesWithNetworkMonitoring.delete(page);
117
+
118
+ // Small delay to let navigation settle
119
+ setTimeout(async () => {
120
+ try {
121
+ await setupNetworkMonitoring(page);
122
+ } catch (error) {
123
+ debugLog('Failed to reinitialize network monitoring:', error.message);
124
+ }
125
+ }, 100);
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Setup recorder auto-reinjection on navigation
131
+ * @param {Page} page - Puppeteer page instance
132
+ */
133
+ export async function setupRecorderAutoReinjection(page) {
134
+ let reinjectionTimeout = null;
135
+ let lastUrl = null;
136
+
137
+ // Handle navigation events (form submits, link clicks, history API)
138
+ page.on('framenavigated', async (frame) => {
139
+ // Only handle main frame navigation
140
+ if (frame !== page.mainFrame()) return;
141
+
142
+ // Get current URL
143
+ const currentUrl = frame.url();
144
+
145
+ // Skip if URL hasn't changed (prevents duplicate injections on same page)
146
+ if (currentUrl === lastUrl) {
147
+ return;
148
+ }
149
+ lastUrl = currentUrl;
150
+
151
+ // Clear any pending reinjection
152
+ if (reinjectionTimeout) {
153
+ clearTimeout(reinjectionTimeout);
154
+ }
155
+
156
+ // Debounce reinjection (wait 100ms for navigation to settle)
157
+ reinjectionTimeout = setTimeout(async () => {
158
+ // Check if this page had recorder before
159
+ if (pagesWithRecorder.has(page)) {
160
+ try {
161
+ // Project ID will be determined from URL in browser context
162
+ await injectRecorder(page);
163
+ } catch (error) {
164
+ debugLog('Failed to re-inject recorder:', error.message);
165
+ }
166
+ }
167
+ }, 100);
168
+ });
169
+
170
+ // Handle page reloads (F5, Ctrl+R) - use 'load' event
171
+ page.on('load', async () => {
172
+ // Check if this page had recorder before
173
+ if (pagesWithRecorder.has(page)) {
174
+ try {
175
+ // Project ID will be determined from URL in browser context
176
+ await injectRecorder(page);
177
+ } catch (error) {
178
+ debugLog('Failed to re-inject recorder after reload:', error.message);
179
+ }
180
+ }
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Get or create page for URL
186
+ * @param {string} url - URL to navigate to
187
+ * @returns {Promise<Page>} - Puppeteer page instance
188
+ */
189
+ export async function getOrCreatePage(url) {
190
+ const browser = await getBrowser();
191
+
192
+ // Check if page for this URL already exists
193
+ if (openPages.has(url)) {
194
+ const existingPage = openPages.get(url);
195
+ if (!existingPage.isClosed()) {
196
+ lastPage = existingPage;
197
+ return existingPage;
198
+ }
199
+ openPages.delete(url);
200
+ }
201
+
202
+ // Create new page
203
+ const page = await browser.newPage();
204
+
205
+ // Set up console log capture
206
+ const client = await page.target().createCDPSession();
207
+ await client.send('Runtime.enable');
208
+ await client.send('Log.enable');
209
+
210
+ client.on('Runtime.consoleAPICalled', (event) => {
211
+ const timestamp = new Date().toISOString();
212
+ const args = event.args.map(arg => {
213
+ if (arg.value !== undefined) return arg.value;
214
+ if (arg.description) return arg.description;
215
+ return String(arg);
216
+ });
217
+
218
+ consoleLogs.push({
219
+ type: event.type, // log, warn, error, info, debug
220
+ timestamp,
221
+ message: args.join(' '),
222
+ stackTrace: event.stackTrace
223
+ });
224
+ });
225
+
226
+ client.on('Log.entryAdded', (event) => {
227
+ const entry = event.entry;
228
+ consoleLogs.push({
229
+ type: entry.level, // verbose, info, warning, error
230
+ timestamp: new Date(entry.timestamp).toISOString(),
231
+ message: entry.text,
232
+ source: entry.source,
233
+ url: entry.url,
234
+ lineNumber: entry.lineNumber
235
+ });
236
+ });
237
+
238
+ // Setup network monitoring with auto-reinitialization on navigation
239
+ await setupNetworkMonitoring(page);
240
+
241
+ // Setup recorder auto-reinjection on navigation
242
+ setupRecorderAutoReinjection(page);
243
+
244
+ await page.goto(url, { waitUntil: 'networkidle2' });
245
+ openPages.set(url, page);
246
+ lastPage = page;
247
+
248
+ return page;
249
+ }
250
+
251
+ /**
252
+ * Get last opened page (for tools that don't need URL)
253
+ * @returns {Promise<Page>} - Puppeteer page instance
254
+ */
255
+ export async function getLastOpenPage() {
256
+ // Check if we have lastPage reference
257
+ if (!lastPage) {
258
+ throw new Error('No page is currently open. Use openBrowser first to open a page.');
259
+ }
260
+
261
+ // Check if page is closed with timeout to prevent hanging
262
+ let isClosed = false;
263
+ try {
264
+ // Race between isClosed check and timeout
265
+ isClosed = await Promise.race([
266
+ lastPage.isClosed(),
267
+ new Promise((_, reject) =>
268
+ setTimeout(() => reject(new Error('Page check timeout')), 1000)
269
+ )
270
+ ]);
271
+ } catch (error) {
272
+ // If timeout or error, assume page is not available
273
+ lastPage = null;
274
+ throw new Error('No page is currently open. Use openBrowser first to open a page.');
275
+ }
276
+
277
+ if (isClosed) {
278
+ lastPage = null;
279
+ throw new Error('No page is currently open. Use openBrowser first to open a page.');
280
+ }
281
+
282
+ // Setup recorder auto-reinjection if not already set up
283
+ // Check if page already has navigation listener
284
+ const listenerCount = lastPage.listenerCount('framenavigated');
285
+ if (listenerCount === 0) {
286
+ setupRecorderAutoReinjection(lastPage);
287
+ }
288
+
289
+ return lastPage;
290
+ }
291
+
292
+ /**
293
+ * Update lastPage reference (for external updates)
294
+ * @param {Page} page - New last page
295
+ */
296
+ export function setLastPage(page) {
297
+ lastPage = page;
298
+ }