44 * SPDX-License-Identifier: Apache-2.0
55 */
66
7+ import type { AggregatedIssue } from '../node_modules/chrome-devtools-frontend/mcp/mcp.js' ;
78import {
8- type AggregatedIssue ,
99 IssueAggregatorEvents ,
1010 IssuesManagerEvents ,
1111 createIssuesFromProtocolIssue ,
@@ -14,7 +14,11 @@ import {
1414
1515import { FakeIssuesManager } from './DevtoolsUtils.js' ;
1616import { logger } from './logger.js' ;
17- import type { CDPSession , ConsoleMessage } from './third_party/index.js' ;
17+ import type {
18+ CDPSession ,
19+ ConsoleMessage ,
20+ Protocol ,
21+ } from './third_party/index.js' ;
1822import {
1923 type Browser ,
2024 type Frame ,
@@ -210,68 +214,92 @@ export class PageCollector<T> {
210214export class ConsoleCollector extends PageCollector <
211215 ConsoleMessage | Error | AggregatedIssue
212216> {
217+ #subscribedPages = new WeakSet < Page > ( ) ;
218+ #issueManager = new FakeIssuesManager ( ) ;
219+ #seenKeys = new Set < string > ( ) ;
220+ #seenIssues = new Set < AggregatedIssue > ( ) ;
221+
213222 override addPage ( page : Page ) : void {
214- const subscribed = this . storage . has ( page ) ;
215223 super . addPage ( page ) ;
216- if ( ! subscribed ) {
217- void this . subscribeForIssues ( page ) ;
218- }
224+ void this . subscribeForIssues ( page ) ;
219225 }
220226
221- async subscribeForIssues ( page : Page ) {
222- const seenKeys = new Set < string > ( ) ;
223- const mockManager = new FakeIssuesManager ( ) ;
224- const aggregator = new IssueAggregator ( mockManager ) ;
225- aggregator . addEventListener (
227+ #resetIssueAggregator( page : Page ) {
228+ this . #issueManager = new FakeIssuesManager ( ) ;
229+ new IssueAggregator ( this . #issueManager) . addEventListener (
226230 IssueAggregatorEvents . AGGREGATED_ISSUE_UPDATED ,
227231 event => {
228- const withId = event . data as WithSymbolId < AggregatedIssue > ;
229- // Emit aggregated issue only if it's a new one
230- if ( withId [ stableIdSymbol ] ) {
232+ if ( this . #seenIssues. has ( event . data ) ) {
231233 return ;
232234 }
235+ this . #seenIssues. add ( event . data ) ;
233236 page . emit ( 'issue' , event . data ) ;
234237 } ,
235238 ) ;
239+ }
236240
241+ async subscribeForIssues ( page : Page ) {
242+ if ( this . #subscribedPages. has ( page ) ) {
243+ return ;
244+ }
245+ this . #subscribedPages. add ( page ) ;
246+ this . #resetIssueAggregator( page ) ;
237247 try {
238248 // @ts -expect-error use existing CDP client (internal Puppeteer API).
239249 const session = page . _client ( ) as CDPSession ;
240- session . on ( 'Audits.issueAdded' , data => {
241- try {
242- const inspectorIssue = data . issue ;
243- // @ts -expect-error Types of protocol from Puppeteer and CDP are
244- // incomparable for InspectorIssueCode, one is union, other is enum.
245- const issue = createIssuesFromProtocolIssue ( null , inspectorIssue ) [ 0 ] ;
246- if ( ! issue ) {
247- logger ( 'No issue mapping for for the issue: ' , inspectorIssue . code ) ;
248- return ;
249- }
250-
251- const primaryKey = issue . primaryKey ( ) ;
252- if ( seenKeys . has ( primaryKey ) ) {
253- return ;
254- }
255- seenKeys . add ( primaryKey ) ;
256-
257- mockManager . dispatchEventToListeners (
258- IssuesManagerEvents . ISSUE_ADDED ,
259- {
260- issue,
261- // @ts -expect-error We don't care that issues model is null
262- issuesModel : null ,
263- } ,
264- ) ;
265- } catch ( error ) {
266- logger ( 'Error creating a new issue' , error ) ;
267- }
268- } ) ;
269-
250+ page . on ( 'framenavigated' , this . #onFrameNavigated) ;
251+ session . on ( 'Audits.issueAdded' , this . #onIssueAdded) ;
270252 await session . send ( 'Audits.enable' ) ;
271253 } catch ( error ) {
272254 logger ( 'Error subscribing to issues' , error ) ;
273255 }
274256 }
257+
258+ // On navigation, we reset issue aggregation.
259+ #onFrameNavigated = ( frame : Frame ) => {
260+ // Only split the storage on main frame navigation
261+ if ( frame !== frame . page ( ) . mainFrame ( ) ) {
262+ return ;
263+ }
264+ this . #seenKeys. clear ( ) ;
265+ this . #seenIssues. clear ( ) ;
266+ this . #resetIssueAggregator( frame . page ( ) ) ;
267+ } ;
268+
269+ #onIssueAdded = ( data : Protocol . Audits . IssueAddedEvent ) => {
270+ try {
271+ const inspectorIssue = data . issue ;
272+ // @ts -expect-error Types of protocol from Puppeteer and CDP are
273+ // incomparable for InspectorIssueCode, one is union, other is enum.
274+ const issue = createIssuesFromProtocolIssue ( null , inspectorIssue ) [ 0 ] ;
275+ if ( ! issue ) {
276+ logger ( 'No issue mapping for for the issue: ' , inspectorIssue . code ) ;
277+ return ;
278+ }
279+
280+ const primaryKey = issue . primaryKey ( ) ;
281+ if ( this . #seenKeys. has ( primaryKey ) ) {
282+ return ;
283+ }
284+ this . #seenKeys. add ( primaryKey ) ;
285+ this . #issueManager. dispatchEventToListeners (
286+ IssuesManagerEvents . ISSUE_ADDED ,
287+ {
288+ issue,
289+ // @ts -expect-error We don't care that issues model is null
290+ issuesModel : null ,
291+ } ,
292+ ) ;
293+ } catch ( error ) {
294+ logger ( 'Error creating a new issue' , error ) ;
295+ }
296+ } ;
297+
298+ protected override cleanupPageDestroyed ( page : Page ) : void {
299+ super . cleanupPageDestroyed ( page ) ;
300+ this . #subscribedPages. delete ( page ) ;
301+ page . off ( 'framenavigated' , this . #onFrameNavigated) ;
302+ }
275303}
276304
277305export class NetworkCollector extends PageCollector < HTTPRequest > {
0 commit comments