TL;DR: Network Error Reporter 是一个 Chrome DevTools 扩展,把失败的网络请求一键转成结构化 Markdown 报告和可分享的 PNG 图片。零依赖,纯原生 JavaScript,两个文件搞定全部逻辑。本文拆解它的架构决策、Canvas 图片导出方案和敏感数据检测设计。
本文假设你了解 Chrome 扩展的基础概念(manifest.json、DevTools panel),但不需要有扩展开发经验。
问题:截图 + 手写说明 = 低效沟通
前端遇到接口报错时,团队的沟通路径通常是这样的:
- 打开 DevTools,找到那条红色请求
- 截图 Network 面板
- 再截一张 Headers 面板
- 打开 IM,贴图,手写一段"我在某某页面点了保存,接口 502 了"
- 后端回复:"Request ID 呢?""Trace ID 发一下?""Request Body 是什么?"
- 再来一轮截图
这个来回的核心问题不是技术,而是信息结构------截图是非结构化的,每次都要人肉提取和补充关键字段。
方案:把 HAR 数据转成结构化报告
思路很直接:Chrome DevTools 扩展能拿到 HAR 格式的请求数据(chrome.devtools.network.getHAR()),把它结构化整理成 Markdown 和图片就行了。
最终的产品形态是 DevTools 里的一个三栏面板:
scss
┌──────────────┬──────────────────┬──────────────────┐
│ 请求列表 │ 补充上下文 │ 报告预览 │
│ (Rail) │ (Compose) │ (Preview) │
│ │ │ │
│ ● 500 POST │ 影响范围: [多用户] │ 环境信息 │
│ /api/save │ 频率: [持续性] │ 错误详情 │
│ │ 复现步骤: ... │ 请求与响应明细 │
│ ○ 200 GET │ 预期结果: ... │ 影响评估 │
│ /api/list │ 实际结果: ... │ 复现说明 │
└──────────────┴──────────────────┴──────────────────┘
选一条请求 → 自动生成报告 → 复制 Markdown 或导出图片。
架构:两个文件,职责分明
整个项目只有两个核心 JS 文件,加起来 ~1300 行:
| 文件 | 职责 | 行数 |
|---|---|---|
panel/report.js |
数据层:HAR 归一化、Markdown 生成、敏感检测 | ~450 |
panel/main.js |
视图层:请求列表、表单交互、DOM 预览、Canvas 导出 | ~850 |
这个分层不是随意的。report.js 是纯函数------输入 HAR entry 和表单状态,输出 Markdown 和预览数据结构,不碰 DOM,不碰 Chrome API。main.js 负责所有副作用:Chrome DevTools API 调用、DOM 渲染、剪贴板操作。
scss
chrome.devtools.network ──→ main.js ──→ report.js
│ │
│ normalizeEntry()
│ buildMarkdownReport()
│ │
│ ▼
│ { markdown, preview,
│ sensitiveHits }
│ │
▼ ▼
DOM 渲染 ←──── preview 数据
Canvas 导出
剪贴板写入
为什么不用框架? 两个原因。第一,DevTools panel 的生命周期很简单------打开面板就加载,关闭就销毁,没有路由、没有复杂状态同步,原生 DOM 操作完全够用。第二,零构建依赖意味着 Load unpacked 就能跑,改一行代码刷新面板就生效,开发体验反而比加了打包工具更快。
技术决策一:请求归一化
Chrome 的 HAR entry 格式冗长且不稳定------不同版本的 Chrome 可能在 _resourceType、resourceType 字段上有差异。normalizeEntry() 把它收敛成一个干净的内部结构:
javascript
// panel/report.js
function normalizeEntry(entry) {
const request = entry.request ?? {};
const response = entry.response ?? {};
const status = Number(response.status ?? 0);
return {
id: [method, url, startedAt, entry.time ?? 0].join("::"),
method,
url,
path: safePathname(url),
status,
statusText: response.statusText || (status === 0 ? "Network Error" : ""),
timeMs: Number(entry.time ?? 0),
requestHeaders: normalizeHeaders(request.headers),
responseHeaders: normalizeHeaders(response.headers),
resourceType: normalizeResourceType(entry, mimeType, url),
isFailure: status >= 400 || status === 0
};
}
几个值得注意的点:
请求去重用的是复合 ID :[method, url, timestamp, duration].join("::")。HAR 里同一个请求可能因为重定向或重试出现多次,用这四个字段组合能有效区分。
资源类型有两层回退 :先读 _resourceType(Chrome 私有字段),拿不到就通过 MIME type 和 URL 模式推断。这样即使 HAR 格式变动,分类功能也不会挂。
javascript
function normalizeResourceType(entry, mimeType, url) {
// 第一优先级:Chrome 的私有字段
const rawType = String(entry._resourceType || entry.resourceType || "").toLowerCase();
if (rawType === "fetch" || rawType === "xhr") return "fetch-xhr";
if (rawType === "document") return "document";
// ...
// 第二优先级:MIME type + URL 推断
if (lowerMime.includes("json") || /\/api\//.test(lowerUrl)) return "fetch-xhr";
if (lowerMime.includes("text/html")) return "document";
// ...
}
内存上限 200 条,按时间倒序排列。DevTools 面板不适合持有太多数据------它本身就在一个受限的执行上下文里。
技术决策二:Markdown 报告生成
buildMarkdownReport() 做两件事:生成 Markdown 文本和一个结构化预览对象。
报告不是把 HAR 原样搬过来,而是做了信息提炼:
Header 分组 :自动把 headers 分成「鉴权/链路头」和「业务自定义头」两组。标准 HTTP 头(Accept、Content-Type、User-Agent 等)直接过滤掉------后端不需要看这些。
javascript
function splitHeaderGroups(headers, side) {
const excluded = new Set([
"accept", "accept-encoding", "accept-language", "cache-control",
"content-type", "cookie", "host", "origin", "user-agent", "vary"
// ...
]);
const filtered = headers.filter(h => {
const name = h.name.toLowerCase();
if (excluded.has(name)) return false;
return name.startsWith("x-") || name.startsWith("trace")
|| name.startsWith("auth") || name.startsWith("gateway")
// ...
});
return {
trace: filtered.filter(h => isTraceHeader(h.name)),
business: filtered.filter(h => !isTraceHeader(h.name))
};
}
Trace ID 自动提取 :支持 7 种常见 trace header(x-request-id、traceparent、x-b3-traceid 等),优先从 response headers 取------因为很多网关是在响应时才注入 trace ID。
JSON 响应智能摘要 :不是把完整 response body 贴上去,而是优先提取 code、message、error、requestId 等关键字段。大数组只展示第一个元素和总长度。
javascript
function summarizeJson(value) {
if (Array.isArray(value)) {
const sample = value.length > 0 ? JSON.stringify(value[0], null, 2) : "[]";
return `type: array\nlength: ${value.length}\nsample: ${truncateLargeBlock(sample)}`;
}
const importantKeys = [
"code", "status", "success", "message", "msg",
"error", "errorCode", "errorMessage", "requestId", "traceId", "timestamp"
];
// 优先提取这些字段,没有的话取前 8 个
}
技术决策三:Canvas 图片导出
这是整个项目最复杂的部分。为什么不用 html2canvas 或者截图 API?因为 DevTools panel 的安全策略限制了大部分 DOM-to-image 方案,而且引入第三方库违背了零依赖原则。
方案是手动测量文本布局,然后用 Canvas 2D 绘制。
流程分两步:
第一步:测量布局
用一个离屏 canvas 的 measureText() 来计算每个文本块换行后的实际高度,得出整张图片的像素尺寸。
javascript
function measurePreviewLayout(preview) {
const width = 1080; // 固定宽度,适配即时通讯工具
const measureCanvas = document.createElement("canvas");
const measureCtx = measureCanvas.getContext("2d");
// 逐字符测量换行
function measureWrappedLines(ctx, text, font, maxWidth) {
const paragraphs = text.split("\n");
const lines = [];
ctx.font = font;
paragraphs.forEach(paragraph => {
let current = "";
for (const char of paragraph) {
const candidate = current + char;
if (!current || ctx.measureText(candidate).width <= maxWidth) {
current = candidate;
continue;
}
lines.push(current);
current = char;
}
if (current) lines.push(current);
});
return lines.length ? lines : ["无"];
}
// ...
}
为什么逐字符测量? 因为报告里有大量中英文混排和 URL,按单词分割不适用。逐字符虽然慢一点,但对混合文本最准确。
第二步:绘制
按照测量结果,用 Canvas 2D 的基础 API 绘制圆角矩形、分割线、表格和文本。调色板用的是 Material Design 3 的色值。
javascript
function drawPreviewCanvas(ctx, metrics) {
const palette = {
background: "#f7f2fa",
section: "#fffafb",
sectionBorder: "rgba(121, 116, 126, 0.16)",
text: "#1d1b20",
muted: "#625b66",
monoBg: "#f3edf7"
};
// 逐 section 绘制:环境信息、错误详情、请求明细...
metrics.blocks.forEach((block) => {
drawSectionFrame(ctx, ...);
if (block.type === "list") drawListSection(ctx, ...);
else if (block.type === "table") drawTableSection(ctx, ...);
else if (block.type === "blocks") drawBlocksSection(ctx, ...);
// ...
});
}
导出策略也有回退机制:
lua
尝试 navigator.clipboard.write(ClipboardItem) → 成功:图片到剪贴板
→ 失败:自动下载 .png 文件
图片固定宽度 1080px、2x 设备像素比,在微信、飞书等 IM 工具里显示效果清晰。
技术决策四:敏感数据检测
在把报告发出去之前,自动扫描是否包含敏感信息:
javascript
const SENSITIVE_PATTERNS = [
/authorization/i, // 认证头
/cookie/i, // Cookie
/set-cookie/i, // Set-Cookie
/token/i, // Token 相关头
/bearer\s+[a-z0-9\-_.]+/i, // Bearer 令牌值
/\b1[3-9]\d{9}\b/, // 中国大陆手机号
/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i // 邮箱地址
];
检测到后不自动脱敏 ------只弹出确认提示,告诉用户发现了几类敏感字段。自动脱敏听起来更安全,但实际上会导致信息丢失,有时候 Authorization 头本身就是排查问题的关键。这里把决策权留给用户。
没有自动化测试?
是的。当前只有 node --check 语法检查------连单元测试都没有。
这是一个有意识的取舍。report.js 的函数都是纯函数,后续加测试非常容易。但项目早期,手动验证(在 DevTools 里实际点一遍)反而能覆盖更多 Chrome API 的集成场景。自动化测试的优先级会在功能稳定后提上来。
你不需要框架、不需要依赖
这个项目刻意保持极简:
- 零 npm 依赖 :没有
package.json,没有node_modules - 零构建步骤 :
Load unpacked直接加载项目目录 - 两个核心文件:数据层 + 视图层,职责清晰
对于一个 DevTools 工具来说,这是正确的复杂度。不是每个项目都需要 React + Webpack + TypeScript。当你的状态管理就是一个 plain object、UI 更新就是重新渲染一个列表,原生 JS 完全够了。
后续方向
当前版本是 0.2.0,scope 故意收窄到单请求报告。后续可能的方向:
- 多请求聚合(同一个接口连续 5xx 的趋势)
- 自定义报告模板
- 与团队工具集成(直接创建 issue)
但这些都不会改变核心架构------report.js 负责数据,main.js 负责视图,保持这个边界比任何新功能都重要。
Network Error Reporter 是一个 MIT 协议的开源项目。如果你的团队也被接口报错的截图沟通困扰,试试看。