ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

puppeteer ConsoleMessage.stackTrace() 详解:从 console 事件到跨浏览器调用栈定位

puppeteer ConsoleMessage.stackTrace() 详解:从 console 事件到跨浏览器调用栈定位 puppeteer ConsoleMessage.stackTrace() 详解从 console 事件到跨浏览器调用栈定位【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer在 puppeteer 中通过page.on(console)捕获到的ConsoleMessage对象不仅是日志文本的载体还携带了消息产生位置的完整调用栈信息。本篇以ConsoleMessage.stackTrace()方法为核心结合 packages/puppeteer-core/src/common/ConsoleMessage.ts 的源码实现、CDP 与 BiDi 两条协议链路的解析逻辑以及 test/src/console.test.ts 中的真实测试用例说明该方法返回什么、数据从何而来、以及如何在自动化脚本中用它精准定位页面上的报错来源。方法签名与返回类型官方文档对该方法的定义非常简洁class ConsoleMessage { stackTrace(): ConsoleMessageLocation[]; }返回值为 ConsoleMessageLocation 类型的数组即console 消息调用栈上的位置数组The array of locations on the stack of the console message。每个ConsoleMessageLocation是一个纯数据接口三个字段全部可选因为浏览器并不总是能提供完整的位置信息属性类型说明urlstring \| undefined资源 URL未知时为undefinedlineNumbernumber \| undefined资源中的0 起始行号未知时为undefinedcolumnNumbernumber \| undefined资源中的0 起始列号未知时为undefined一个典型的返回值如下行号、列号均为 0 起始const locations message.stackTrace(); // [ // { url: http://localhost:8907/consoletrace.html, lineNumber: 8, columnNumber: 16 }, // { url: http://localhost:8907/consoletrace.html, lineNumber: 11, columnNumber: 8 }, // { url: http://localhost:8907/consoletrace.html, lineNumber: 13, columnNumber: 6 }, // ]注意行号与列号都是 0 起始的直接展示给用户或写入报告时通常需要 1换算为编辑器里的行列号。源码实现私有字段与 location() 的分工在 packages/puppeteer-core/src/common/ConsoleMessage.ts 中ConsoleMessage的构造器被标记为internal第三方代码不应直接实例化该类所有实例都由 puppeteer 内部在收到协议事件后构造并随console事件派发。类内通过私有字段保存数据export class ConsoleMessage { #type: ConsoleMessageType; #text: string; #args: JSHandle[]; #stackTraceLocations: ConsoleMessageLocation[]; #frame?: Frame; #rawStackTrace?: Protocol.Runtime.StackTrace; #targetId?: string; constructor( type: ConsoleMessageType, text: string, args: JSHandle[], stackTraceLocations: ConsoleMessageLocation[], frame?: Frame, rawStackTrace?: Protocol.Runtime.StackTrace, targetId?: string, ) { this.#type type; this.#text text; this.#args args; this.#stackTraceLocations stackTraceLocations; this.#frame frame; this.#rawStackTrace rawStackTrace; this.#targetId targetId; }对外暴露的方法中与位置信息相关的是location()和stackTrace()两个/** * The location of the console message. */ location(): ConsoleMessageLocation { return ( this.#stackTraceLocations[0] ?? (this.#frame ? {url: this.#frame.url()} : {}) ); } /** * The array of locations on the stack of the console message. */ stackTrace(): ConsoleMessageLocation[] { return this.#stackTraceLocations; }从源码结构可以看出两者明确的分工location()只返回栈顶第一层位置并且带有一个兜底逻辑——当调用栈为空但消息所属Frame已知时退化为{url: frame.url()}即只告诉你消息来自哪个页面 URLstackTrace()则返回完整的调用栈位置数组无兜底可能为空数组例如协议层没有携带stackTrace时。因此当只需要消息来自哪个文件时用location()即可当需要还原哪一层函数调用了 console时必须用stackTrace()。此外类内部还保留了一份协议原始对象#rawStackTraceProtocol.Runtime.StackTrace通过internal的_rawStackTrace()暴露给内部模块使用公开 API 层面拿到的就是已经清洗过的ConsoleMessageLocation[]。数据来源CDP 与 BiDi 两条链路的解析stackTrace()里的数据并不是凭空生成的它由协议事件中的调用栈字段解析而来。puppeteer 当前同时支持 CDPChrome DevTools Protocol与 WebDriver BiDi 两种底层协议两条链路各自有一套调用栈 → 位置数组的转换逻辑最终汇聚到同一个ConsoleMessage构造函数。CDP 链路在 CDP 模式下console.trace()等 console API 调用会触发Runtime.consoleAPICalled事件其event.stackTrace携带callFrames数组。packages/puppeteer-core/src/cdp/utils.ts 中的createConsoleMessage()负责把它展开为位置数组const stackTraceLocations []; if (event.stackTrace) { for (const callFrame of event.stackTrace.callFrames) { stackTraceLocations.push({ url: callFrame.url, lineNumber: callFrame.lineNumber, columnNumber: callFrame.columnNumber, }); } } return new ConsoleMessage( convertConsoleMessageLevel(event.type), textTokens.join( ), values, stackTraceLocations, undefined, event.stackTrace, // 原始协议对象被一并保留为 #rawStackTrace targetId, );可以看到转换是逐字段直取的url、lineNumber、columnNumber与ConsoleMessageLocation三个可选字段一一对应0 起始的行/列号原样保留不做 1 修正。此外还有一条特殊路径来自Log 域的日志例如fetch失败产生的网络错误由 packages/puppeteer-core/src/cdp/Page.ts 的#onLogEntryAdded()处理。这类消息的Log.entryAdded事件只提供单条url/lineNumber没有完整调用栈所以构造时传入的是长度为 1 的位置数组#onLogEntryAdded(event: Protocol.Log.EntryAddedEvent): void { const {level, text, args, source, url, lineNumber, stackTrace} event.entry; // ... new ConsoleMessage( // ... [], [{url, lineNumber}], // 仅单条位置无调用栈 undefined, stackTrace, // 原始协议栈另行保留 this.#primaryTarget._targetId, );这解释了为什么文档将ConsoleMessageLocation的三个字段都声明为可选网络错误类消息通常只有url没有行列号。BiDi 链路在 WebDriver BiDi 模式下console 条目以Bidi.Log.entryAdded事件到达其中同样携带entry.stackTrace。packages/puppeteer-core/src/bidi/util.ts 提供了对应的转换函数getStackTraceLocations()export function getStackTraceLocations( stackTrace?: Bidi.Script.StackTrace, ): ConsoleMessageLocation[] { const stackTraceLocations: ConsoleMessageLocation[] []; if (stackTrace) { for (const callFrame of stackTrace.callFrames) { stackTraceLocations.push({ url: callFrame.url, lineNumber: callFrame.lineNumber, columnNumber: callFrame.columnNumber, }); } } return stackTraceLocations; }两条链路最终都调用new ConsoleMessage(type, text, args, stackTraceLocations, frame, ...)因此无论底层是 CDP 还是 BiDi用户代码中message.stackTrace()的行为是一致的——这正是 puppeteer 跨 Chrome / Firefox 抽象层的设计意图之一。结合测试用例验证真实行为test/src/console.test.ts 中的用例should have location and stack trace for console API calls是对该方法最直接的行为验证。测试加载的页面资源 test/assets/consoletrace.html 构造了一个三层调用script function foo() { console.trace(yellow) // 第 9 行0 起始为 8 } function bar() { foo(); // 第 12 行0 起始为 11 } bar(); // 第 14 行0 起始为 13 /script测试断言了完整的返回结构expect(message.text()).toBe(yellow); expect(message.type()).toBe(trace); expect(message.location()).toEqual({ url: server.PREFIX /consoletrace.html, lineNumber: 8, columnNumber: 16, }); expect(message.stackTrace()).toEqual([ {url: server.PREFIX /consoletrace.html, lineNumber: 8, columnNumber: 16}, {url: server.PREFIX /consoletrace.html, lineNumber: 11, columnNumber: 8}, {url: server.PREFIX /consoletrace.html, lineNumber: 13, columnNumber: 6}, ]);从断言可以读出三个关键事实console.trace()的消息type()为trace是 ConsoleMessageType 支持类型之一location()恰好等于stackTrace()[0]即栈顶console.trace所在行数组按调用顺序排列从console.trace所在位置foo内第 9 行→ 调用foo()的位置bar内第 12 行→ 调用bar()的位置第 14 行全部以 0 起始行列号呈现。同样的能力在 Web Worker 场景也得到验证test/src/worker.test.ts 断言 worker 中产生的trace消息stackTrace().length 0说明调用栈采集不限于页面主线程。实战用法把调用栈写进自动化日志结合前面的源码与测试事实一个把stackTrace()用起来的最小完整示例如下import puppeteer from puppeteer; const browser await puppeteer.launch(); const page await browser.newPage(); page.on(console, (message) { console.log([${message.type()}] ${message.text()}); // 打印完整调用栈注意行/列号是 0 起始展示时 1 for (const frame of message.stackTrace()) { const line frame.lineNumber ! undefined ? frame.lineNumber 1 : ?; const col frame.columnNumber ! undefined ? frame.columnNumber 1 : ?; console.log( at ${frame.url}:${line}:${col}); } // 栈为空但想知道来自哪个页面时退回 location() 兜底 if (message.stackTrace().length 0) { const loc message.location(); console.log( (no stack) from ${loc.url ?? unknown} ${loc.lineNumber ! undefined ? line (loc.lineNumber 1) : }); } }); // 触发带调用栈的 console 消息 await page.setContent(script function foo() { console.trace(yellow); } function bar() { foo(); } bar(); /script, {waitUntil: domcontentloaded}); await browser.close();使用时需要注意的边界条件空数组是合法的没有调用栈信息时部分 Log 域消息、某些浏览器实现stackTrace()返回空数组脚本应像示例中那样用location()做兜底而location()自身在栈和 frame 都缺失时会返回空对象{}三个字段都可能读到undefined行/列号是 0 起始与编辑器显示的行列号相差 1puppeteer 在内部构造异常栈字符串时自己会做1修正见 packages/puppeteer-core/src/cdp/utils.ts 中createClientError()对lineNumber 1的处理但stackTrace()返回的原始数据不替你换算不要手动构造ConsoleMessage其构造函数为内部 API实例只能来自 puppeteer 派发的事件。小结ConsoleMessage.stackTrace()是 puppeteer console 事件体系里把消息内容升级为消息取证的关键方法它以ConsoleMessageLocation[]的形式返回从 console 调用点到最外层调用的完整位置链行/列号为 0 起始且各字段可选。其数据在 CDP 模式下由Runtime.consoleAPICalled的callFrames在 packages/puppeteer-core/src/cdp/utils.ts 中解析在 BiDi 模式下由 packages/puppeteer-core/src/bidi/util.ts 的getStackTraceLocations()解析最终统一封装在 packages/puppeteer-core/src/common/ConsoleMessage.ts 的私有字段中对外只读暴露。与location()仅栈顶 frame URL 兜底配合使用可以在 E2E 测试失败、页面 JS 报错等场景下精确定位到出错文件的行与列是编写可靠自动化诊断逻辑的常用手段。【免费下载链接】puppeteerJavaScript API for Chrome and Firefox项目地址: https://gitcode.com/GitHub_Trending/puppeteer1/puppeteer创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表