@@ -9,7 +9,9 @@ import {Mutex} from './Mutex.js';
99import { DevTools } from './third_party/index.js' ;
1010import type {
1111 Browser ,
12+ ConsoleMessage ,
1213 Page ,
14+ Protocol ,
1315 Target as PuppeteerTarget ,
1416} from './third_party/index.js' ;
1517
@@ -224,3 +226,94 @@ const SKIP_ALL_PAUSES = {
224226 // Do nothing.
225227 } ,
226228} ;
229+
230+ export async function createStackTraceForConsoleMessage (
231+ devTools : TargetUniverse ,
232+ consoleMessage : ConsoleMessage ,
233+ ) : Promise < DevTools . StackTrace . StackTrace . StackTrace | undefined > {
234+ const message = consoleMessage as ConsoleMessage & {
235+ _rawStackTrace ( ) : Protocol . Runtime . StackTrace | undefined ;
236+ _targetId ( ) : string | undefined ;
237+ } ;
238+ const rawStackTrace = message . _rawStackTrace ( ) ;
239+ if ( ! rawStackTrace ) {
240+ return undefined ;
241+ }
242+
243+ const targetManager = devTools . universe . context . get ( DevTools . TargetManager ) ;
244+ const messageTargetId = message . _targetId ( ) ;
245+ const target = messageTargetId
246+ ? targetManager . targetById ( messageTargetId ) || devTools . target
247+ : devTools . target ;
248+ const model = target . model ( DevTools . DebuggerModel ) as DevTools . DebuggerModel ;
249+
250+ // DevTools doesn't wait for source maps to attach before building a stack trace, rather it'll send
251+ // an update event once a source map was attached and the stack trace retranslated. This doesn't
252+ // work in the MCP case, so we'll collect all script IDs upfront and wait for any pending source map
253+ // loads before creating the stack trace. We might also have to wait for Debugger.ScriptParsed events if
254+ // the stack trace is created particularly early.
255+ const scriptIds = new Set < Protocol . Runtime . ScriptId > ( ) ;
256+ for ( const frame of rawStackTrace . callFrames ) {
257+ scriptIds . add ( frame . scriptId ) ;
258+ }
259+ for (
260+ let asyncStack = rawStackTrace . parent ;
261+ asyncStack ;
262+ asyncStack = asyncStack . parent
263+ ) {
264+ for ( const frame of asyncStack . callFrames ) {
265+ scriptIds . add ( frame . scriptId ) ;
266+ }
267+ }
268+
269+ const signal = AbortSignal . timeout ( 1_000 ) ;
270+ await Promise . all (
271+ [ ...scriptIds ] . map ( id =>
272+ waitForScript ( model , id , signal )
273+ . then ( script =>
274+ model . sourceMapManager ( ) . sourceMapForClientPromise ( script ) ,
275+ )
276+ . catch ( ) ,
277+ ) ,
278+ ) ;
279+
280+ const binding = devTools . universe . context . get (
281+ DevTools . DebuggerWorkspaceBinding ,
282+ ) ;
283+ // DevTools uses branded types for ScriptId and others. Casting the puppeteer protocol type to the DevTools protocol type is safe.
284+ return binding . createStackTraceFromProtocolRuntime (
285+ rawStackTrace as Parameters <
286+ DevTools . DebuggerWorkspaceBinding [ 'createStackTraceFromProtocolRuntime' ]
287+ > [ 0 ] ,
288+ target ,
289+ ) ;
290+ }
291+
292+ // Waits indefinitely for the script so pair it with Promise.race.
293+ async function waitForScript (
294+ model : DevTools . DebuggerModel ,
295+ scriptId : Protocol . Runtime . ScriptId ,
296+ signal : AbortSignal ,
297+ ) {
298+ while ( true ) {
299+ if ( signal . aborted ) {
300+ throw signal . reason ;
301+ }
302+
303+ const script = model . scriptForId ( scriptId ) ;
304+ if ( script ) {
305+ return script ;
306+ }
307+
308+ await new Promise ( ( resolve , reject ) => {
309+ signal . addEventListener ( 'abort' , ( ) => reject ( signal . reason ) , {
310+ once : true ,
311+ } ) ;
312+ void model
313+ . once (
314+ 'ParsedScriptSource' as Parameters < DevTools . DebuggerModel [ 'once' ] > [ 0 ] ,
315+ )
316+ . then ( resolve ) ;
317+ } ) ;
318+ }
319+ }
0 commit comments