1. 测试提 Bug 的隐藏成本,真的比你想的高
干测试这么多年,我一直觉得最浪费时间的环节不是执行用例,而是:Bug 已经在你眼前了,证据却还没到手。你刚在一个页面上点出来一个 500,顺手按 F12 准备导出 HAR,一抬头发现接口响应已经刷过去了,再一看 Network 面板里什么都没留下——因为你还没勾上 Preserve log。于是只能复现一次,可复现偏偏又不稳定,第二次怎么点都不报错了。这种场景,凡是做过 Web 测试的应该都懂。
传统提 Bug 的完整链路是:发现问题 → 打开开发者工具 → 勾选 Preserve log → 复现问题 → 右键导出 HAR → 登录禅道 → 新建 Bug → 填标题、模块、严重程度、影响版本 → 上传 HAR 附件 → 写复现步骤 → 提交。顺利的话三到五分钟,不顺利的话,光“影响版本”这个下拉框就能卡你十分钟——版本列表里没有当前跑的版本,你选谁都不对,选“其他”又会被开发随手打回。更糟的是环境不稳定,开发那边已经把旧包覆盖了,你想补抓一个旧版本的 HAR 都无路可走。
我这次想聊的,就是我们团队内部打磨的一个小工具:HAR 一键生成 Skill。它本质上是一个跑在浏览器里的扩展,核心动作是:捕获当前页面所有网络请求,拼装成标准 HAR 文件;同时自动读取当前页面的版本号,在禅道里新建用例或提交 Bug 时,能跳过版本下拉框的等待和匹配,直接把证据和现场信息一次性带过去。适合所有天天和 Web 项目打交道的测试工程师,项目越复杂、版本迭代越快,它的价值越明显。
1.1 传统流程到底浪费在哪
先掰开算一笔账。假设每天处理 5 个 Bug,每个 Bug 光是“抓包 + 填单”就要花 10 分钟,那一天就是 50 分钟,一个月按 20 个工作日算,整整浪费 1000 分钟。这还没算“复现不出来被迫重新跑一遍用例”的时间。很多团队对 Bug 单的要求是“必须附带 HAR”,因为开发需要看请求参数、响应头和耗时数据。可如果你恰好在发现问题之后没有第一时间抓包,后面补起来就是地狱难度。
另一个浪费点在于“版本信息靠人肉记忆”。禅道里的“影响版本”字段,理论上应该填当前被测版本。可实际执行测试时,你经常同时开着两三个环境,一个联调环境、一个测试环境、一个预发布环境,页面长得一模一样,只是 URL 带的后缀不同。等你切到禅道去填单时,早就忘了刚才点的是哪个版本。Bug 单上的版本信息一错,开发就得重新确认,一来一回又是一轮沟通成本。
1.2 Skill 到底解决什么问题
这个 Skill 的本质不是“自动帮你点按钮”,而是把“发现 Bug → 留下证据 → 关联版本 → 提交禅道”这条链路压缩成一次点击。它的工作方式大致这样:你在被测页面上看到一个异常,点一下悬浮按钮,扩展会从浏览器后台拿回这个页面近期所有请求数据、拼成 HAR,再自动打开禅道的新建 Bug 页面,把标题、复现步骤、影响版本、HAR 附件全部填好。你只需要检查一遍、点提交。
“不再受版本限制”这句话,我理解包含两层意思。第一,HAR 是现场抓的,跟你跑的是哪个分支、哪个镜像、哪次构建都无关,只要页面还开着,你就能拿到当下的完整请求证据,不需要为了复现而去部署旧版本;第二,版本字段不再要求下拉框里刚好有选项,工具会自动从页面 URL、meta 标签或页脚文字里提取当前版本号,能匹配上就自动选,匹配不上也会写进 Bug 描述里,确保信息不丢失、提交不阻塞。
2. 整体设计:这枚 Skill 是怎么搭出来的
工具做出来看着简单,但设计过程中我踩了不少坑。先说结论:整体架构分三层——浏览器扩展负责采集和注入,中间的逻辑层负责 HAR 构建和版本识别,最后是禅道动作层,负责建用例和提 Bug。每一层都要考虑浏览器的限制和禅道页面本身的交互特点。
2.1 为什么非要做成浏览器扩展
很多同事第一反应是:抓包工具不是现成的吗?独立抓包工具也能导出 HAR,再写个脚本上传到禅道不就行了?理论上可行,但实际用起来很别扭。独立抓包工具需要额外的代理配置,手机和电脑混测时尤其麻烦;而且它读不到页面上下文里的版本号、登录态、当前用户信息,这些数据在浏览器页面 DOM 里才有。
浏览器扩展的优势是“贴地飞行”。它可以注入脚本到页面里,读取 URL、meta 标签、全局变量;可以通过 chrome.webRequest 系列接口监听网络请求;还可以主动打开禅道页面,模拟用户交互去填表单。更重要的是,它能把“记录请求”和“禅道提交”这两件事真正串成一个动作,而不是让你在多个工具之间来回切换。这也是我们内部管它叫 Skill 而不是“插件”的原因——插件强调功能,Skill 强调场景动作,抓包、转用例、提 Bug 是一条完整的技能链。
2.2 三个核心模块的分工
模块划分是我觉得这个项目里最值得参考的部分。不需要一开始就做一个大而全的框架,只需要把三个能力边界理清楚:
| 模块 | 职责 | 关键技术点 |
|---|---|---|
| 网络请求采集器 | 收集页面所有 HTTP 请求的信息 | chrome.webRequest 监听、performance 兜底、请求去重 |
| HAR 构建器 | 把采集到的数据转成标准 HAR JSON | 字段映射、时间戳格式化、响应体截断 |
| 禅道动作器 | 打开禅道并自动填表提交 | DOM 自动填充、受控组件事件触发、版本模糊匹配 |
三个模块之间通过浏览器扩展的消息机制串起来。采集器把原始请求数据发给构建器,构建器生成 HAR 后存到扩展的本地存储里,禅道动作器再从存储里取数据去填表。这样做的好处是每个环节都可以单独调试——比如 HAR 构建器只负责生成文件,即使禅道页面改版了,也不用动采集逻辑。
2.3 版本字段解绑的设计思路
版本解绑是这个小工具的灵魂。最开始我们想的方案是直接读出页面 URL 里的版本号,比如https://app.example.com/v2.3.1/#/order就提取v2.3.1。但实际项目里,版本号经常只出现在登录后的页脚里,比如“版本号:2024.06.18-release”,甚至藏在打包时注入的全局变量里。后来我们定了三条提取路径,按优先级依次尝试:URL 正则 →meta[name=version]→ 页脚可见文本。提取到纯文本版本号之后,再对禅道版本下拉框里的选项做模糊匹配,把带相似前缀的选项自动选中。
如果最后还是匹配不上,就退而求其次:把识别到的版本号写进 Bug 复现步骤里,并给“影响版本”字段填一个默认值。这样 Bug 单不会被卡在必填校验上,开发拿到单还能从描述里看到准确版本。这一步很重要,别为了追求“全自动选中”而把提交动作卡住,目标是信息不丢,而不是 UI 完美。
3. HAR 生成核心拆解:数据从哪来、怎么转
HAR 这个东西,看着是一个 JSON 文件,里面有名堂。如果你只是手动从 DevTools 导过一次,可能觉得它长得挺标准;但等你要自己生成一份能让禅道附件正常展示、开发能打开分析的 HAR,就会遇到一堆细节问题。
3.1 HAR 结构速览与最小可用模型
HAR 的整体结构是一个log对象,里面包含version、creator和entries数组。每个entry对应一次完整的 HTTP 请求,包含startedDateTime、time、request、response、timings等字段。
{ "log": { "version": "1.2", "creator": { "name": "TestHAR-Skill", "version": "1.0.0" }, "entries": [ { "startedDateTime": "2025-01-12T14:23:11.000Z", "time": 234.5, "request": { "method": "GET", "url": "https://api.example.com/v1/order/list", "headers": [], "cookies": [], "queryString": [], "postData": { "mimeType": "application/json", "text": "{}" } }, "response": { "status": 500, "statusText": "Internal Server Error", "headers": [], "content": { "mimeType": "application/json", "size": 128, "text": "" }, "redirectURL": "" }, "timings": { "blocked": 10, "dns": 5, "connect": 30, "send": 5, "wait": 180, "receive": 5 }, "cache": {} } ] } }很多开发同学拿到 HAR 后,第一件事就是看请求参数、响应状态和耗时。所以我在构建的时候坚持三个原则:请求按时间排序、状态码必须真实、响应体尽量截断保存。真实环境里一个响应体动辄几百 KB,全塞进 HAR 会导致附件体积爆炸,后面在禅道上传时非常痛苦。最小可用模型的意思是:字段可以缺,但 URL、状态码、请求头、耗时这几个核心字段必须完整。
3.2 用 webRequest 捕获请求的几个关键坑
在 Chromium 系的扩展里,监听网络请求最直接的 API 是chrome.webRequest。但用起来有几个容易踩的坑。
第一个坑是 Manifest V3 里webRequest不再支持同步修改请求内容的blocking权限(除非企业策略),所以别想着在请求发出前改参数,我们只需要“看”,用onCompleted和onErrorOccurred就够了。
第二个坑是监听回调里拿不到响应体。chrome.webRequest只给你响应头、状态码,不给你响应内容。想拿响应体,得上chrome.debuggerAPI,但那玩意儿权限更大、用户会有“调试器已附加”的提示条,体验很差。我们的折中方案是:默认只记录响应头和大小,响应体通过页面的performance接口兜底获取,拿不到就留空。开发要看具体返回内容时,再配合控制台操作。
第三个坑是 Service Worker 生命周期。Manifest V3 的扩展后台是 sw 进程,一段时间不活动会休眠,可能错过正在发起的请求。这问题没有银弹,只能靠“在页面打开时 keepalive”或者把采集逻辑部分放到 content script 里,用页面自身的生命周期来兜底。我最终用的是 content script 里的 performance 兜底方案,后面细说。
3.3 用 Performance API 做数据补全
performance.getEntriesByType("resource")这个方法,能取到当前页面加载以来所有资源请求的元数据。它和webRequest的区别在于:它拿不到所有响应头,但能拿到 Duration、TransferSize、RequestStart、ResponseStart 这些时间戳;而且它在扩展 content script 里也能访问,不受后台休眠影响。
实际实现的时候,我做了两层融合。以webRequest为主,因为它能拿到完整的请求头和部分响应头,适合做 HAR 的骨架;再用 performance 条目补全startedDateTime、耗时等数据,顺便给那些被缓存命中、没触发网络事件的请求补一条记录。两条数据源靠 URL 和时间戳做关联,以 URL 去重,这样生成出来的 HAR 不会漏掉页面的静态资源请求,也不会重复记录同一个接口。
这里有个小技巧:performance条目里的startTime是相对页面加载开始的时间,不是绝对时间。要转成 HAR 里的startedDateTime,需要先performance.timeOrigin拿到页面加载的绝对基准时间,再相加。踩坑踩过一次之后,我对这种“相对时间”的细节格外敏感。
3.4 版本号识别的实现策略
版本识别这件事,我写了一个独立的detectVersion()函数,按顺序尝试:
- 从
location.href里匹配常见版本号模式,比如v?[0-9]+\.[0-9]+(\.[0-9]+)?,以及日期版本号2024\.\d+\.\d+; - 查
document.querySelector('meta[name="version"]')和meta[name="app-version"]; - 如果上面都没有,就扫描页脚区域
footer、.version、.release等常见选择器,取第一个符合版本号格式的文本。
识别出的版本号会先展示在悬浮按钮旁边,方便测试人员肉眼确认。识别错了还可以手动改。这个交互细节很重要——自动化识别再准,也得留人工确认的口子,否则识别错误会把 Bug 带到错误版本上,比不识别还坑。
4. 禅道自动化:建用例、填 Bug、解绑版本
工具要落到禅道上,才算真正闭环。禅道的表单不算复杂,但有几个交互细节非常磨人:富文本编辑器、异步加载的下拉框、受控组件。我分别说一下我是怎么处理的。
4.1 从 HAR 自动生成 Bug 标题
标题是 Bug 单的第一印象,不能随便起。自动生成标题的规则,我参考了团队历史 Bug 单的高质量样本,总结出一个简单模板:
[页面/模块] 接口 X 返回 YYY,导致 Z 问题
具体做法是,先筛选出 HAR 里状态码非 2xx 的请求,按严重程度排序(5xx > 4xx > 超时)。取最严重的一个请求,从 URL 里提取接口路径片段,比如/order/detail;状态码直接取;Z 问题则从请求 URL 的 query 参数里的message、err_msg关键词去猜,猜不到就写“异常”,留给你自己改。
举个例子:如果 HAR 里有一条POST /api/payment/confirm返回 500,那么生成的标题就是“【订单模块】接口 /api/payment/confirm 返回 500,导致支付确认失败”。如果禅道里已经存在相同标题的 Bug,我会在标题尾部加“(已有相似单,建议核实是否重复)”,防止测试同事因为自动化而疯狂提单,污染 Bug 库。
4.2 复现步骤和用例草稿怎么自动生成
这是我觉得最有价值的部分,也是标题里“建用例”的来源。HAR 本身就是一段真实操作的行为记录,把它变成用例步骤并不难。我的做法是:首先按时间排序所有请求,找出状态码异常的那个请求,把它之前的三个相邻请求视为“前置操作链路”。然后对每一条请求生成一个步骤描述,规则是“操作”加“断言”。
比如前面三条请求是:打开首页 → 点击支付按钮 → 提交支付表单,那么生成的用例步骤就是:
- 进入订单确认页;
- 点击“确认支付”按钮;
- 观察接口
/api/payment/confirm的返回值; - 预期:返回 200,页面展示支付成功;
- 实际:接口返回 500,支付失败。
生成好的用例草稿会渲染在一个中间预览面板里,你可以在提交前直接编辑,也可以一键“创建到禅道用例模块”。禅道用例模块的字段是标题、前置条件、步骤、预期,这几个字段在预览面板里都能改。这里我强烈建议保留人工编辑,因为纯自动生成的步骤描述难免粗糙,测试人员的判断力不能省。
4.3 填表单遇到的受控组件与异步下拉框问题
禅道是一个存在了很久的开源项目,前端历史包袱不少。有些页面用的是老式 jQuery 表单,有些模块已经改成了现代框架,这就导致“填值”不能只用input.value = xxx这条路,否则输入框的值看着变了,但提交的时候表单拿不到。
解决方案是:对 input 和 textarea 用setNativeValue方式设置值,并派发input和change事件。setNativeValue的核心就是利用原生 setter 绕过框架的取值缓存:
function setNativeValue(element, value) { const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; valueSetter.call(element, value); element.dispatchEvent(new Event("input", { bubbles: true })); element.dispatchEvent(new Event("change", { bubbles: true })); }版本下拉框的问题则是“异步加载”。禅道的版本选项是打开页面后通过接口加载的,如果你刚打开页面就去选,下拉框里还是空的。解决方案是轮询等待:先记录当前下拉框选项数量,每 300ms 检查一次,直到选项数稳定后再执行版本匹配。千万别一上来就 sleep 两秒,禅道接口快的时候一半时间都用不上,慢的时候 5 秒都等不完。
5. 从 0 到 1 完成这枚 Skill 的实操步骤
现在进入最实操的部分。我完整记录了一遍搭建流程,照抄即可跑通一个最小可用版本。
5.1 扩展骨架与权限配置
首先创建一个目录,里面放manifest.json,这是扩展的入口。核心权限如下:
{ "manifest_version": 3, "name": "HAR-Skill", "version": "1.0.0", "permissions": ["webRequest", "activeTab", "scripting", "storage"], "host_permissions": ["<all_urls>"], "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"], "run_at": "document_idle" } ], "action": { "default_popup": "popup.html", "default_title": "HAR-Skill" } }说明一下:webRequest权限是采集流量的基础;host_permissions用<all_urls>是为了在任意被测站点上都能工作——如果你只测固定域名,建议收紧到具体域名,减少权限风险;storage用来在扩展不同页面之间传递 HAR 数据。
5.2 注入悬浮按钮与捕获入口
content.js负责在页面上注入一个悬浮按钮。代码很简单,但有一个要点:按钮要挂在document.documentElement下,并且要设置很高的z-index,否则容易被页面自己的导航条或弹层盖住。
const floatingBtn = document.createElement("div"); floatingBtn.textContent = "🎯"; floatingBtn.style.cssText = `position:fixed;right:24px;bottom:120px;z-index:2147483647;...`; document.documentElement.appendChild(floatingBtn); floatingBtn.addEventListener("click", () => { chrome.runtime.sendMessage({ type: "CAPTURE_HAR_REQUEST" }); });按钮点击之后,只是向后台发一条消息。真正抓包和构建 HAR 的逻辑放在background.js里,因为chrome.webRequest接口只有扩展后台才能用。
5.3 background 拼接 HAR 的核心逻辑
background.js里维护一个数组requestLog,每次onCompleted或onErrorOccurred触发时,把请求信息 push 进去。收到CAPTURE_HAR_REQUEST消息时,再统一拼装成 HAR。
const requestLog = []; chrome.webRequest.onCompleted.addListener( (details) => { requestLog.push({ url: details.url, method: details.method, statusCode: details.statusCode, statusLine: details.statusLine, requestHeaders: details.requestHeaders, responseHeaders: details.responseHeaders, time: details.timeStamp }); }, { urls: ["<all_urls>"], types: ["xmlhttprequest", "script", "stylesheet", "image"] } ); chrome.webRequest.onErrorOccurred.addListener((details) => { requestLog.push({ url: details.url, method: details.method, statusCode: 0, error: details.error, time: details.timeStamp }); }, { urls: ["<all_urls>"] });这里我故意用timeStamp来排顺序。details.timeStamp是事件发生时刻戳,能精确反映请求发生顺序。之后在生成 HAR 时,再把这个时间戳格式化成startedDateTime。
5.4 打开禅道自动填表
生成 HAR 之后,数据会存在chrome.storage.local里,然后通过chrome.tabs.create打开禅道新建 Bug 页面。填表逻辑复用另一段 content script,从 storage 读数据,按上一章的setNativeValue思路逐字段填充。
这里要提醒的是,不要在 popup 关闭后还在后台用storage.local存超大 HAR。如果 HAR 超过 5MB,建议只存精简字段,附件单独通过dataUrl或者临时文件的方式在真正提交时才组装。我们吃过亏,存了个 12MB 的 HAR 到 storage,结果扩展直接白屏。
5.5 安全与合规提醒
工具虽好,但使用边界要明确。HAR 文件里包含了请求 Cookie、Token、Authorization 头等敏感信息,生成后不要随意外发。扩展应限制在团队内部域名使用,不要发布到公共扩展商店;如果内部有安全审计要求,还需要对 HAR 中的敏感字段做脱敏处理。我在实际工件里加了一个开关,默认打开“脱敏模式”,把 Authorization 头替换成***。
6.1 请求列表为空,点按钮生成的 HAR 没有任何条目
这个问题第一次跑通时十有八九会碰到。排查顺序如下:
- 确认扩展权限里包含了
webRequest,并且host_permissions覆盖了被测页面域名; - 打开扩展后台 Service Worker 的日志,看监听回调里有没有数据进入
requestLog; - 注意:安装扩展之前已经加载完成的页面,历史请求是拿不到的,刷新页面后再测试;
- 如果页面有 Service Worker 缓存或者 HTTP 缓存,某些请求可能根本不经过网络层,需要用 performance 补全,确认
performance.getEntriesByType("resource")里有数据。
最后一个原因最隐蔽。webRequest在有些私有网络请求、WebSocket 升级请求上触发方式不一样,所以永远要用 performance 数据做兜底。
6.2 版本下拉框匹配不中
版本匹配是重灾区。原因通常是禅道的版本下拉框选项加载慢,或者是版本格式不一致。我的建议是先在 console 里把所有候选版本打出来看一眼,然后对“识别到的版本号”和“候选版本”做格式化统一:去掉v前缀、去掉空格、统一大小写,再做includes判断。如果还是匹配不到,就不要强选,把版本号写进描述字段,同时给“影响版本”选一个兜底值,保证能提交。
6.3 富文本描述塞不进去
禅道的富文本编辑器在现代版本里是基于编辑器封装的,直接用innerHTML赋值会被框架覆盖。可靠的做法是:先定位到textarea或contenteditable元素,用原生的 Document.execCommand 插入文本,或者直接调用编辑器的公开 API(如果有暴露全局对象的话)。如果没有暴露,就在有焦点时模拟粘贴事件,把文本通过ClipboardEvent传进去。这个是最坑的一环,我从“手动触发 input 事件”一直试到“模拟粘贴”,才找到稳定方案。
6.4 HAR 附件体积过大上传失败
HAR 的体积问题几乎无法避免,因为content.text会包含响应体全文。我的处理是三层降级:默认不保存响应体内容,只记录content.size;如果遇到状态码异常,保存响应体但截断到前 2KB;再提供一个“完整保存”开关,仅在必要的时候开启。同时生成 HAR 后,用gzip压缩一下文本,50% 的压缩率基本都能达到。禅道上传的是压缩后的.har.gz,开发那边也能正常解压查看。
7. 我的几点使用体会
7.1 它如何改变了我的测试习惯
工具用了一个月后,我发现自己最大的变化不是“提单变快”,而是开始习惯盯着请求链路去做判断。以前看到一个页面报错,我的第一反应是“界面显示不对”;现在第一反应是“哪个接口挂的、参数是什么、耗时是多少、是不是依赖的服务有问题”。有了自动生成的 HAR,我对每个 Bug 都能拿出比“肉眼可见”更硬核的证据。团队里的开发反馈,Bug 单被打回的现象少了很多,因为“HAR 就在附件里,自己拉下来看请求参数和返回码,基本能定位问题”。
7.2 后续还可以怎么玩
这个 Skill 的框架并不局限于禅道。把禅道动作器换成其他缺陷管理系统,就是一套通用的“网页抓包 + 缺陷自动化”底座。我接下来准备做两件事:一是把生成 HAR 的过程接上 CI,让自动化测试跑完直接产出 HAR 附件;二是把“用例草稿”升级成“接口变更对比”,统计同一个接口在两个版本之间的响应差异。到时候再写一篇单独的实战记录分享出来。
最后分享一个我踩过的小坑:扩展版本更新时,如果用户还停留在旧页面上,content script 不会自动重新注入,一定要让用户刷新页面再用。这个看似不起眼的问题,曾经让我们在演示时翻车,不得不重启浏览器。工具写得好不好是一回事,什么时候触发加载、什么时候重新注入,这些细节决定了它在真实工作流里到底顺不顺手。