@@ -11,7 +11,9 @@ import {Mutex} from './Mutex.js';
1111import { DevTools } from './third_party/index.js' ;
1212import type {
1313 Browser ,
14+ ConsoleMessage ,
1415 Page ,
16+ Protocol ,
1517 Target as PuppeteerTarget ,
1618} from './third_party/index.js' ;
1719
@@ -270,3 +272,77 @@ const SKIP_ALL_PAUSES = {
270272 // Do nothing.
271273 } ,
272274} ;
275+
276+ export async function createStackTraceForConsoleMessage (
277+ devTools : TargetUniverse ,
278+ consoleMessage : ConsoleMessage ,
279+ ) : Promise < DevTools . StackTrace . StackTrace . StackTrace | undefined > {
280+ const message = consoleMessage as ConsoleMessage & {
281+ _rawStackTrace ( ) : Protocol . Runtime . StackTrace | undefined ;
282+ _targetId ( ) : string | undefined ;
283+ } ;
284+ const rawStackTrace = message . _rawStackTrace ( ) ;
285+ if ( ! rawStackTrace ) {
286+ return undefined ;
287+ }
288+
289+ const targetManager = devTools . universe . context . get ( DevTools . TargetManager ) ;
290+ const messageTargetId = message . _targetId ( ) ;
291+ const target = messageTargetId
292+ ? targetManager . targetById ( messageTargetId ) || devTools . target
293+ : devTools . target ;
294+ const model = target . model ( DevTools . DebuggerModel ) as DevTools . DebuggerModel ;
295+
296+ // DevTools doesn't wait for source maps to attach before building a stack trace, rather it'll send
297+ // an update event once a source map was attached and the stack trace retranslated. This doesn't
298+ // work in the MCP case, so we'll collect all script IDs upfront and wait for any pending source map
299+ // loads before creating the stack trace. We might also have to wait for Debugger.ScriptParsed events if
300+ // the stack trace is created particularly early.
301+ const scriptIds = new Set < Protocol . Runtime . ScriptId > ( ) ;
302+ rawStackTrace . callFrames . forEach ( frame => scriptIds . add ( frame . scriptId ) ) ;
303+ for (
304+ let asyncStack = rawStackTrace . parent ;
305+ asyncStack ;
306+ asyncStack = asyncStack . parent
307+ ) {
308+ asyncStack . callFrames . forEach ( frame => scriptIds . add ( frame . scriptId ) ) ;
309+ }
310+
311+ await Promise . all (
312+ [ ...scriptIds ] . map ( id =>
313+ Promise . race ( [
314+ waitForScript ( model , id ) . then ( script =>
315+ model . sourceMapManager ( ) . sourceMapForClientPromise ( script ) ,
316+ ) ,
317+ new Promise ( r => setTimeout ( r , 1_000 ) ) ,
318+ ] ) ,
319+ ) ,
320+ ) ;
321+
322+ const binding = devTools . universe . context . get (
323+ DevTools . DebuggerWorkspaceBinding ,
324+ ) ;
325+ // DevTools uses branded types for ScriptId and others. Casting the puppeteer protocol type to the DevTools protocol type is safe.
326+ return binding . createStackTraceFromProtocolRuntime (
327+ rawStackTrace as Parameters <
328+ DevTools . DebuggerWorkspaceBinding [ 'createStackTraceFromProtocolRuntime' ]
329+ > [ 0 ] ,
330+ target ,
331+ ) ;
332+ }
333+
334+ // Waits indefinitely for the script so pair it with Promise.race.
335+ async function waitForScript (
336+ model : DevTools . DebuggerModel ,
337+ scriptId : Protocol . Runtime . ScriptId ,
338+ ) {
339+ while ( true ) {
340+ const script = model . scriptForId ( scriptId ) ;
341+ if ( script ) {
342+ return script ;
343+ }
344+
345+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
346+ await model . once ( 'ParsedScriptSource' as any ) ;
347+ }
348+ }
0 commit comments