35 KiB
前端 SDK 设计文档
一、设计原则
1.1 核心原则
- 极致轻量:核心 < 5KB gzip,完整功能 < 15KB gzip
- 零侵入:不影响业务代码,不修改原生对象原型
- 高性能:所有耗时操作异步化,不阻塞主线程
- 插件化:功能模块化,按需加载,可扩展
- 容错性:SDK 自身错误不影响页面运行
- 兼容 Sentry:DSN 格式、事件格式、上报协议兼容
1.2 体积预算
| 模块 | 体积 (gzip) | 说明 |
|---|---|---|
| Core 核心 | ~3KB | 事件总线、队列、上报、工具函数 |
| Error 插件 | ~3KB | JS 错误、Promise、资源错误 |
| Performance 插件 | ~4KB | Web Vitals、长任务、资源时序 |
| Network 插件 | ~2KB | fetch / xhr 监控 |
| Behavior 插件 | ~2KB | PV、点击、路由 |
| 完整 SDK | ~12KB | 核心 + 所有插件 |
二、核心架构
2.1 整体结构
┌─────────────────────────────────────────────────┐
│ LightSDK │
│ ┌───────────────────────────────────────────┐ │
│ │ Core (核心) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌─────────┐ │ │
│ │ │ Config │ │ EventBus │ │ Queue │ │ │
│ │ │ 配置管理 │ │ 事件总线 │ │ 事件队列 │ │ │
│ │ └──────────┘ └──────────┘ └────┬────┘ │ │
│ │ │ │ │
│ │ ┌──────────┐ ┌──────────┐ │ │ │
│ │ │ Reporter │ │ Plugins │◄──────┘ │ │
│ │ │ 上报器 │ │ 插件系统 │ │ │
│ │ └──────────┘ └──────────┘ │ │
│ └───────────────────────────────────────────┘ │
│ ▲ ▲ │
└─────────┼───────────────────┼───────────────────┘
│ │
┌─────┴─────┐ ┌──────┴──────┐
│ Plugins │ │ Hooks │
│ (插件) │ │ (生命周期) │
└───────────┘ └─────────────┘
2.2 插件系统
每个插件实现标准接口:
interface LightPlugin {
name: string; // 插件唯一标识
version: string; // 插件版本
setup(client: LightClient): void; // 安装时调用
destroy?(): void; // 销毁时调用
// 可选钩子
beforeReport?(event: Event): Event | null; // 上报前拦截
afterReport?(event: Event): void; // 上报后回调
}
内置插件列表:
| 插件名 | 功能 | 体积 | 默认开启 |
|---|---|---|---|
error |
JS 错误、Promise 异常、资源加载错误 | ~3KB | 是 |
performance |
Web Vitals、长任务、性能指标 | ~4KB | 否 |
network |
fetch / xhr 请求监控 | ~2KB | 否 |
behavior |
PV、点击、路由、用户行为 | ~2KB | 否 |
sampling |
采样控制、流量控制 | ~1KB | 是 |
offline |
离线缓存、网络恢复后补发 | ~2KB | 否 |
breadcrumbs |
用户行为面包屑 | ~2KB | 否 |
三、核心模块详细设计
3.1 Config - 配置管理
配置项
interface LightConfig {
// ===== 基础配置 =====
dsn: string; // DSN: https://{publicKey}@{host}/{projectId}
release?: string; // 版本号,如 1.0.0
environment?: string; // 环境: production / development / staging
enabled?: boolean; // 是否启用,默认 true
// ===== 上报配置 =====
sampleRate?: number; // 全局采样率 0-1,默认 1
maxQueueSize?: number; // 队列最大长度,默认 100
flushInterval?: number; // 定时上报间隔(ms),默认 5000
maxRetries?: number; // 失败重试次数,默认 3
retryDelay?: number; // 重试基础延迟(ms),默认 1000
// ===== 数据配置 =====
ignoreErrors?: (string | RegExp)[]; // 忽略的错误
ignoreUrls?: (string | RegExp)[]; // 忽略的 URL
includePaths?: (string | RegExp)[]; // 只收集的路径
beforeSend?: (event: Event) => Event | null; // 发送前回调
// ===== 用户配置 =====
user?: {
id?: string;
username?: string;
email?: string;
[key: string]: any;
};
// ===== 插件配置 =====
plugins?: (LightPlugin | string)[]; // 插件列表
[pluginName: string]: any; // 各插件自定义配置
}
DSN 解析
格式:{protocol}://{publicKey}@{host}/{projectId}
https://proj_abc123@log.example.com/1001
│ │ │ └── projectId: 1001
│ │ └────────────────── host: log.example.com
│ └────────────────────────────── publicKey: proj_abc123
└──────────────────────────────────────── protocol: https
解析后:
projectId: 项目 ID(纯数字,兼容 Sentry)publicKey: 认证公钥(项目内部 ID)host: 上报域名protocol: http / https
3.2 EventBus - 事件总线
简单的发布订阅模式,用于插件间通信:
class EventBus {
private handlers: Map<string, Function[]> = new Map();
on(event: string, handler: Function): void; // 订阅
off(event: string, handler: Function): void; // 取消订阅
emit(event: string, data?: any): void; // 发布
once(event: string, handler: Function): void; // 一次性订阅
}
内置事件:
| 事件名 | 触发时机 | 数据 |
|---|---|---|
event |
新事件产生 | Event 对象 |
report |
事件上报前 | Event[] 数组 |
reported |
上报成功 | Event[] 数组 |
error |
SDK 自身错误 | Error |
ready |
SDK 初始化完成 | - |
3.3 Queue - 事件队列
队列策略
新事件 → 入队 → 检查是否触发上报 → 是 → 上报
│
└─ 否 → 等待
触发上报的条件:
- 队列满:达到
maxQueueSize阈值 - 定时:每
flushInterval毫秒 - 页面隐藏:
visibilitychange→ hidden - 页面卸载:
beforeunload/pagehide - 手动调用:
client.flush()
事件去重
- 同一错误(相同指纹)1 分钟内最多上报 3 次
- 防止死循环导致的事件风暴
3.4 Reporter - 上报器
上报策略(优先级从高到低)
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
navigator.sendBeacon |
页面卸载、隐藏 | 异步、不阻塞、可靠 | 仅 POST、大小限制 64KB |
fetch + keepalive |
现代浏览器 | 灵活、支持自定义 header | 不支持跨域某些场景 |
XMLHttpRequest |
兼容旧浏览器 | 兼容性好 | 同步会阻塞 |
Image 标签 |
极端兜底 | 兼容性最好 | GET 请求、大小有限 |
上报重试
- 失败后指数退避重试:1s → 2s → 4s
- 最多重试
maxRetries次 - 4xx 错误不重试(客户端错误)
- 5xx / 网络错误才重试
上报格式(兼容 Sentry Envelope)
{"event_id":"abc123","sent_at":"2024-01-01T00:00:00Z"}
{"type":"event","length":123}
{"level":"error","message":"...","exception":{...}}
{"type":"transaction","length":456}
{"transaction":"...","spans":[...]}
轻量模式下可用简化格式:
{
"events": [
{ "type": "error", "data": {...} },
{ "type": "performance", "data": {...} }
]
}
四、内置插件详细设计
4.1 Error 插件(错误采集)
采集内容
| 错误类型 | 监听方式 | 说明 |
|---|---|---|
| JS 运行时错误 | window.onerror |
普通 JS 错误 |
| Promise 异常 | unhandledrejection |
Promise 未捕获 |
| 资源加载错误 | error 事件捕获阶段 |
img、script、link 等 |
| iframe 错误 | 冒泡 + postMessage | 同域 iframe |
| 框架错误 | 钩子函数(Vue/React) | 需手动接入 |
错误事件格式
interface ErrorEvent {
type: 'error';
level: 'fatal' | 'error' | 'warning' | 'info' | 'debug';
message: string; // 错误消息
exception?: {
type: string; // 错误类型: TypeError, ReferenceError...
value: string; // 错误消息
stacktrace?: {
frames: StackFrame[]; // 堆栈帧,倒序(最近的调用在前)
};
};
timestamp: number; // 时间戳
release?: string; // 版本
environment?: string; // 环境
user?: UserInfo; // 用户信息
tags?: Record<string, string>; // 标签
extra?: Record<string, any>; // 额外数据
breadcrumbs?: Breadcrumb[]; // 面包屑
request?: { // 请求信息
url: string;
method?: string;
headers?: Record<string, string>;
};
}
interface StackFrame {
filename: string; // 文件名
function?: string; // 函数名
lineno?: number; // 行号
colno?: number; // 列号
in_app?: boolean; // 是否业务代码
}
错误指纹
用于错误去重和聚合:
指纹 = md5(错误类型 + 错误消息(脱敏后) + 堆栈前3帧的文件名+行号)
4.2 Performance 插件(性能采集)
采集指标
| 指标 | 全称 | 说明 | 采集方式 |
|---|---|---|---|
| FP | First Paint | 首次绘制 | PerformanceObserver |
| FCP | First Contentful Paint | 首次内容绘制 | PerformanceObserver |
| LCP | Largest Contentful Paint | 最大内容绘制 | PerformanceObserver |
| CLS | Cumulative Layout Shift | 累计布局偏移 | PerformanceObserver |
| FID | First Input Delay | 首次输入延迟 | PerformanceObserver |
| TTI | Time to Interactive | 可交互时间 | 长任务 + 网络静默 |
| TBT | Total Blocking Time | 总阻塞时间 | 长任务累加 |
| Long Tasks | - | 长任务列表 | PerformanceObserver |
| Navigation Timing | - | 页面加载各阶段 | performance.timing |
| Resource Timing | - | 资源加载时序 | performance.getEntriesByType |
性能事件格式
interface PerformanceEvent {
type: 'performance';
metric: string; // 指标名: lcp, fcp, cls...
value: number; // 指标值
unit: string; // 单位: ms, score...
rating: 'good' | 'needs-improvement' | 'poor'; // 评级
timestamp: number;
release?: string;
environment?: string;
tags?: {
page_url: string; // 页面 URL
route?: string; // 路由路径
[key: string]: string;
};
extra?: Record<string, any>;
}
性能评级标准(Google Web Vitals)
| 指标 | Good | Needs Improvement | Poor |
|---|---|---|---|
| LCP | < 2.5s | 2.5s - 4.0s | > 4.0s |
| FID | < 100ms | 100ms - 300ms | > 300ms |
| CLS | < 0.1 | 0.1 - 0.25 | > 0.25 |
采样策略
- 错误事件:100% 采样
- 性能指标:默认 10% 采样,可配置
- 长任务:默认 5% 采样
- 资源时序:默认 1% 采样
4.3 Network 插件(网络监控)
采集内容
- fetch:通过 monkey patch 包装
window.fetch - XMLHttpRequest:通过 monkey patch 包装
XHR.prototype.open/send
采集字段
interface NetworkEvent {
type: 'network';
sub_type: 'fetch' | 'xhr';
method: string; // 请求方法
url: string; // 请求 URL
status_code?: number; // 状态码
duration?: number; // 耗时(ms)
request_size?: number; // 请求大小
response_size?: number; // 响应大小
success: boolean; // 是否成功
error?: string; // 错误信息
timestamp: number;
release?: string;
tags?: {
route?: string;
[key: string]: string;
};
}
脱敏处理
- URL 中的敏感参数自动脱敏(password、token、key 等)
- 请求/响应 body 默认不上报(避免数据泄漏)
- 可配置白名单上报特定接口的 body
4.4 Behavior 插件(行为采集)
采集内容
| 行为 | 采集方式 | 说明 |
|---|---|---|
| PV / UV | 页面加载 + 路由变化 | 页面浏览量 |
| 点击事件 | click 事件委托 |
按钮、链接点击 |
| 路由变化 | History / Hash 变化 | SPA 路由跳转 |
| 停留时长 | visibilitychange + 计时 | 页面停留时间 |
| 滚动深度 | scroll 事件 + 防抖 | 页面滚动百分比 |
行为事件格式
interface BehaviorEvent {
type: 'behavior';
sub_type: 'pv' | 'click' | 'route' | 'scroll' | 'duration';
page_url: string;
referrer?: string;
timestamp: number;
properties?: Record<string, any>; // 各子类型的属性
}
点击事件属性
- element: 元素标签名 + id + class(如 button#submit.btn-primary)
- text: 元素文本(截断前 50 字符)
- xpath: 元素 XPath(可选,默认关)
4.5 Sampling 插件(采样控制)
采样维度
- 全局采样率:所有事件的总采样率
- 按事件类型采样:错误 100%,性能 10%,行为 1%
- 按用户采样:按 user.id 哈希,稳定采样同一用户
- 按页面采样:重要页面 100%,普通页面 10%
- 错误突增降级:错误率过高时自动降采样
采样算法
// 基于用户 ID 的稳定采样
function shouldSample(userId, rate) {
const hash = murmurhash(userId || 'anonymous');
return (hash % 10000) < (rate * 10000);
}
4.6 Offline 插件(离线缓存)
- 离线时事件存入 IndexedDB
- 网络恢复后自动补发
- 最多缓存 1000 条,超出丢弃最旧的
- 补发时添加
offline: true标记
4.7 Breadcrumbs 插件(面包屑)
记录用户操作轨迹,错误发生时一并上报:
interface Breadcrumb {
type: 'navigation' | 'click' | 'http' | 'console' | 'user';
message: string;
timestamp: number;
category?: string;
data?: Record<string, any>;
level: 'info' | 'warning' | 'error';
}
默认保留最近 20 条面包屑。
五、SDK API
5.1 初始化
import LightSDK from 'light-sentry';
LightSDK.init({
dsn: 'https://proj_abc123@log.example.com/1001',
release: '1.0.0',
environment: 'production',
sampleRate: 1,
// 插件配置
plugins: ['error', 'performance', 'network'],
// 性能插件配置
performance: {
sampleRate: 0.1,
captureLongTasks: true,
},
// 网络插件配置
network: {
ignoreUrls: ['/health', '/metrics'],
captureBody: false,
},
// 发送前回调
beforeSend(event) {
// 可修改或过滤事件
if (event.message.includes('ignore')) return null;
return event;
},
});
5.2 手动 API
// 捕获错误
LightSDK.captureException(error);
LightSDK.captureMessage('Something happened', 'warning');
// 设置用户
LightSDK.setUser({ id: '123', username: 'test' });
// 设置标签
LightSDK.setTag('page', 'home');
LightSDK.setTags({ platform: 'ios', version: '2.0' });
// 添加上下文
LightSDK.setExtra('order_id', '12345');
// 添加面包屑
LightSDK.addBreadcrumb({
type: 'user',
message: 'User clicked button',
category: 'action',
});
// 手动上报
LightSDK.captureEvent({
type: 'custom',
message: 'custom event',
level: 'info',
extra: { foo: 'bar' },
});
// 强制刷新队列
LightSDK.flush();
// 禁用 / 启用
LightSDK.disable();
LightSDK.enable();
5.3 框架集成
Vue 2
import LightSDK from 'light-sentry';
import { VueIntegration } from 'light-sentry/vue';
LightSDK.init({
dsn: '...',
plugins: [new VueIntegration(Vue)],
});
Vue 3
app.use(LightSDK.vuePlugin, { dsn: '...' });
React
// 错误边界
import { ErrorBoundary } from 'light-sentry/react';
<ErrorBoundary fallback={<p>出错了</p>}>
<App />
</ErrorBoundary>
六、性能优化
6.1 不影响页面性能的设计
- 全部异步:SDK 初始化、事件处理、上报全异步
- requestIdleCallback:非紧急操作放在空闲时间
- 防抖节流:scroll、resize 等高频事件防抖
- 不修改原型:用包装方式(fetch/xhr),不污染原型
- 微任务批量:同一批事件合并处理,减少事件循环开销
- Web Worker:可选,重计算(指纹、聚合)放到 Worker
6.2 数据体积优化
这是轻量 SDK 与 Sentry 的核心区别之一——通过策略大幅减少上报数据量。
6.2.1 错误堆栈精简
| 项目 | Sentry 默认 | 轻量方案 | 节省 |
|---|---|---|---|
| 堆栈深度 | 50 帧 | 3-5 帧(in_app) | ~80% |
| node_modules 帧 | 保留 | 过滤掉 | ~50% |
| 列号 | 保留 | 可选,默认开 | - |
| 完整文件名 | 完整 URL | 只留相对路径 | ~30% |
为什么 3-5 帧就够了?
- 90% 的错误,看前 3 帧就能定位问题
- 真正需要深度堆栈的错误很少,遇到了再开完整模式
- 业务代码的调用链通常不深
配置方式:
LightSDK.init({
error: {
maxStackFrames: 5, // 最多 5 帧
captureNodeModules: false, // 不上报 node_modules 里的帧
captureColumn: true, // 保留列号
relativePathOnly: true, // 只保留相对路径,去掉域名
}
});
堆栈对比:
Sentry(2KB+):
at XMLHttpRequest.onloadend (http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:1845:7)
at settle (http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:1441:12)
at http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:1245:12
at wrapFn (http://localhost:5173/node_modules/.vite/deps/axios.js?v=6b332a51:2312:45)
at fetchUser (http://localhost:5173/src/api/user.js:23:15)
at UserList.vue:45:8
...(还有 40 多帧)
轻量版(~200B):
at fetchUser (src/api/user.js:23)
at UserList.vue:45
6.2.2 环境信息结构化(browser / os / device)
问题:每条事件都带完整的环境信息,数据冗余严重。
// 每条都带:~200 bytes
"browser": { "name": "Chrome", "version": "120.0.0.0" },
"os": { "name": "Mac OS X", "version": "10.15.7" },
"device": { "family": "MacBook Pro", "model": "MacBook Pro" }
优化策略:三级方案
方案 1:字典编码(SDK 端,最简单)
把常用的浏览器/OS 映射成短编码,减少传输体积。
// 内置字典
const BROWSER_MAP = {
'Chrome': 'c',
'Safari': 's',
'Firefox': 'f',
'Edge': 'e',
'Mobile Chrome': 'cm',
'Mobile Safari': 'sm',
// ...
};
const OS_MAP = {
'Windows': 'w',
'Mac OS X': 'm',
'Linux': 'l',
'iOS': 'i',
'Android': 'a',
// ...
};
// 上报时用编码
{
"env": {
"b": "c", // browser = Chrome
"bv": "120", // 只留主版本号
"os": "m", // os = Mac OS X
"osv": "15", // 只留大版本
"dv": "mbp" // device = MacBook Pro
}
}
效果:环境信息从 ~200B → ~50B,减少 75%。
方案 2:会话级共享(进一步优化)
同一个用户同一次会话里,环境信息是不变的,不需要每条都带。
┌─────────────────────────────────────────┐
│ 第一次上报:带完整环境信息 + session_id │
│ 后续上报:只带 session_id │
└─────────────────────────────────────────┘
服务端根据 session_id 关联环境信息。
效果:每条事件节省 50-100B,百万级事件能省几十 GB。
方案 3:服务端 UA 解析(最极致)
SDK 完全不解析 UA,只上报原始 User-Agent 字符串,服务端统一解析后写入维度表。
SDK 上报:只带 1 个 ua 字段(原始 UA 字符串)
服务端:解析后存入维度表,日志里只存维度 ID
| 维度 | 说明 |
|---|---|
| 优点 | SDK 更小(不用带 UA 解析库),解析逻辑集中维护 |
| 缺点 | 服务端多了一层解析开销 |
推荐组合:SDK 做字典编码 + 只保留主版本号 → 简单、够用、SDK 体积增加很少。
6.2.3 上下文信息分层策略
不是所有事件都需要完整上下文,按级别区分:
| 事件级别 | 堆栈 | 环境信息 | breadcrumbs | request 信息 | 用户信息 |
|---|---|---|---|---|---|
| fatal | 5 帧 | 完整 | 20 条 | 完整 | 完整 |
| error | 5 帧 | 精简 | 10 条 | URL+状态码 | id 即可 |
| warning | 3 帧 | 精简 | 5 条 | URL 即可 | id 即可 |
| info | 无 | 编码 | 无 | 无 | 无 |
| debug | 无 | 无 | 无 | 无 | 无 |
配置方式:
LightSDK.init({
contextLevel: {
fatal: 'full',
error: 'standard',
warning: 'minimal',
info: 'none',
}
});
6.2.4 批量上报共享元数据
同批次的事件,共享相同的元数据(release、environment、user 等),不用每条都重复带。
Sentry 方式(每条都带):
event1: { release: "1.0.0", environment: "prod", user: {...}, ... }
event2: { release: "1.0.0", environment: "prod", user: {...}, ... }
event3: { release: "1.0.0", environment: "prod", user: {...}, ... }
轻量方式(共享头):
{
"meta": {
"release": "1.0.0",
"environment": "prod",
"user": { "id": "123" },
"env": { "b": "c", "os": "m" }
},
"events": [
{ "type": "error", "message": "...", "stack": [...] },
{ "type": "performance", "metric": "lcp", "value": 2500 },
{ "type": "error", "message": "...", "stack": [...] }
]
}
效果:批量 10 条的话,元数据部分减少约 90%。
6.2.5 业务错误智能过滤
401、403、表单校验失败这类「预期内的业务错误」,不需要完整上报:
network: {
// 忽略的 HTTP 状态码(业务预期错误)
ignoreStatusCodes: [401, 403, 422, 404],
// 错误请求只上报精简信息(URL + 状态码 + 耗时)
errorRequestDetail: 'minimal',
// 成功请求完全不上报
captureSuccess: false,
// 只上报 5xx 和网络错误为「错误事件」
// 4xx 只计数,不上报详情
}
对比:
| 方式 | 一个 401 错误上报体积 |
|---|---|
| Sentry | ~2-3 KB(完整堆栈+上下文) |
| 轻量版(标准) | ~200B(堆栈裁剪+精简) |
| 轻量版(业务错误过滤) | ~50B(只计数)甚至 0B(忽略) |
6.2.6 请求头 / 响应头精简
请求头和响应头是「隐藏的体积大户」——Sentry 默认会带一堆 headers,一对请求响应头就能有 1-2KB。
问题:
// Sentry 默认带上的 headers(节选,完整有 20+ 个)
{
"request": {
"headers": {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...",
"Content-Type": "application/json",
"Accept": "application/json, text/plain, */*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Referer": "https://example.com/page",
"Origin": "https://example.com",
"Connection": "keep-alive",
"X-Request-Id": "abc123...",
"Authorization": "Bearer xxx..." // ⚠️ 敏感信息!
}
}
}
核心策略:白名单 + 编码 + 不上报 body
策略 1:白名单机制(默认只带必要的)
不是所有 header 都有价值,默认只上报对排查问题有用的几个:
network: {
// 请求头白名单(默认)
captureRequestHeaders: [
'content-type',
'user-agent', // 或者干脆不带,环境信息里已有
'x-request-id',
],
// 响应头白名单(默认)
captureResponseHeaders: [
'content-type',
'content-length',
'x-request-id',
'server',
],
// ❌ 绝对禁止上报的 header(黑名单,防止敏感信息泄露)
forbiddenHeaders: [
'authorization',
'cookie',
'set-cookie',
'token',
'api-key',
'x-api-key',
'x-auth-token',
],
}
效果:从 20+ 个 header → 3-5 个,体积减少 80%+。
策略 2:Header 名称短编码
把常见的 header 名称映射成短编码,进一步减少体积。
const HEADER_NAME_MAP = {
'content-type': 'ct',
'user-agent': 'ua',
'x-request-id': 'rid',
'content-length': 'cl',
'server': 'sv',
'accept': 'ac',
'referer': 'ref',
'origin': 'org',
// ...
};
编码前后对比:
// 编码前(~150B)
{ "headers": { "content-type": "application/json", "x-request-id": "abc123" } }
// 编码后(~70B)
{ "h": { "ct": "application/json", "rid": "abc123" } }
策略 3:Header 值字典化
对于值相对固定的 header,也可以做字典映射:
// Content-Type 的常见值
const CONTENT_TYPE_MAP = {
'application/json': '1',
'text/html': '2',
'text/plain': '3',
'application/x-www-form-urlencoded': '4',
'multipart/form-data': '5',
'image/png': '6',
// ...
};
效果:每个 header 值从十几个字符 → 1-2 个字符。
策略 4:body 默认不上报
请求体和响应体是最大的体积来源,而且:
- 可能包含敏感数据(密码、token、用户信息)
- 体积大(几 KB 到几 MB 都有可能)
- 大部分场景下,排查问题不需要看 body
network: {
// ❌ 默认不上报请求/响应体
captureRequestBody: false,
captureResponseBody: false,
// (可选)只对特定接口上报 body,且做脱敏
captureBodyForUrls: [
'/api/debug/*',
],
// body 大小限制(即使开启也截断)
maxBodySize: 2048, // 最多 2KB
}
6.2.7 进一步精简:协议层与传输层
前面的优化都是「内容层面」的精简,协议和传输层面还有很大空间。
优化 1:全量字段短编码
不只是 header,所有字段名都可以用短编码,减少 JSON key 的重复开销。
// 字段名映射表
const FIELD_MAP = {
// 通用字段
'type': 't',
'level': 'l',
'message': 'm',
'timestamp': 'ts',
'release': 'r',
'environment': 'e',
'platform': 'p',
// 错误相关
'exception': 'ex',
'stacktrace': 'st',
'frames': 'fr',
'filename': 'fn',
'function': 'fc',
'lineno': 'ln',
'colno': 'cn',
'in_app': 'ia',
// 用户
'user': 'u',
'user.id': 'uid',
// 标签/扩展
'tags': 'tg',
'extra': 'xt',
// 请求
'request': 'req',
'url': 'u',
'headers': 'h',
// 性能
'metric': 'mt',
'value': 'v',
'unit': 'un',
'rating': 'rt',
};
编码前后对比:
// 编码前(~120B)
{
"type": "error",
"level": "error",
"message": "Something broke",
"timestamp": 1704067200000,
"release": "1.0.0"
}
// 编码后(~65B)
{
"t": "e",
"l": "e",
"m": "Something broke",
"ts": 1704067200000,
"r": "1.0.0"
}
效果:字段名部分减少 50-60%。
优化 2:时间戳相对化
同一批事件的时间戳都很接近,用「相对于批次起始时间」的差值表示,数字更小、字符更少。
// 传统方式(每条都是完整时间戳)
{
"events": [
{ "ts": 1704067200123, ... },
{ "ts": 1704067200456, ... },
{ "ts": 1704067200789, ... }
]
}
// 相对时间戳(批次头存基准时间,每条存差值)
{
"base_ts": 1704067200000,
"events": [
{ "ts": 123, ... },
{ "ts": 456, ... },
{ "ts": 789, ... }
]
}
效果:每条事件时间戳从 13 位数字 → 2-4 位,减少 60-80%。
优化 3:批量内字符串去重
同一批事件里,很多字符串是重复的(同一页面、同一浏览器、同一版本),用「字符串表 + 索引」的方式。
// 传统方式(每条都带完整字符串)
{
"events": [
{ "page": "/home", "release": "1.0.0", "browser": "Chrome" },
{ "page": "/home", "release": "1.0.0", "browser": "Chrome" },
{ "page": "/about", "release": "1.0.0", "browser": "Safari" }
]
}
// 字符串表方式(共享字符串,只用索引引用)
{
"strings": ["/home", "1.0.0", "Chrome", "/about", "Safari"],
"events": [
{ "page": 0, "release": 1, "browser": 2 },
{ "page": 0, "release": 1, "browser": 2 },
{ "page": 3, "release": 1, "browser": 4 }
]
}
效果:批量越大、重复越多,节省越明显。10 条以上批量时,减少 30-50%。
优化 4:二进制协议替代 JSON
JSON 是文本格式,冗余度很高。用二进制协议可以进一步压缩:
| 协议 | 体积 | 解析速度 | 可读性 |
|---|---|---|---|
| JSON | 100%(基准) | 慢 | 好 |
| MessagePack | 约 60-70% | 快 | 差(需工具解码) |
| Protobuf | 约 40-50% | 最快 | 最差 |
| 自定义二进制 | 约 30-40% | 快 | 最差 |
推荐策略:
- 默认用 JSON(简单、调试方便)
- 高性能模式下可选 MessagePack
- 一般项目 JSON 足够,二进制增加复杂度收益不一定高
优化 5:智能采样(不是全量 10%,而是动态调整)
不是所有事件都值得同样的采样率,按「价值」动态调整:
sampling: {
// 错误事件:100% 采样
error: 1.0,
// 性能指标:默认 10%,但 LCP > 4s 的 100% 采样(慢的全要)
performance: {
default: 0.1,
poor: 1.0, // 评级为 poor 的全采
needsImprovement: 0.5,
good: 0.05,
},
// 网络请求:错误 100%,成功 1%
network: {
error: 1.0,
success: 0.01,
// 慢请求也全采
slowThreshold: 3000,
slow: 1.0,
},
// 同一个用户全采或全不采(保证会话完整)
userBased: true,
}
为什么智能采样更好?
- 「好的」性能数据 99% 都是正常的,采多了没用
- 「差的」性能数据才是我们真正想看的,要全采
- 同样,成功的请求采 1% 就够统计了,失败的才需要详细排查
效果:总体数据量再减 50-70%,同时保证「有价值的数据」不丢。
优化 6:相同错误聚合上报(最狠的一招)
同一个错误 1 分钟内出现 100 次,没必要上报 100 次完整堆栈。
第 1 次:上报完整信息(堆栈 + 上下文)
第 2~N 次:只上报计数 + 时间戳(几十字节)
{
"events": [
// 完整错误(首次出现)
{
"t": "e",
"m": "Cannot read property 'foo' of undefined",
"st": [...],
"full": true
},
// 聚合计数(后续同指纹错误)
{
"fp": "a1b2c3d4", // 错误指纹
"cnt": 15, // 15 次
"ts_start": 123, // 起始时间偏移
"ts_end": 456 // 结束结束
}
]
}
效果:对于高频错误,减少 90%+ 的上报量。
6.2.8 传输层优化
数据体积之外,传输效率也很重要:
优化 1:gzip 压缩
所有上报请求开启 gzip 压缩:
- JSON 数据压缩率通常有 60-80%
- 体积从 300B → 60-120B
- Nginx 层自动解压或直接透传
优化 2:HTTP 连接复用
- 开启
keep-alive,复用 TCP 连接 - 减少 TCP 握手开销
- 用同一个连接发多批数据
优化 3:HTTP/2
- 多路复用,多个请求并行
- 头部压缩(HPACK)
- 服务端支持的话自动启用
优化 4:上报时机优化
- 尽量在网络空闲时上报(
navigator.onLine+requestIdleCallback) - 页面加载高峰期不上报,避免影响页面性能
- 网络从离线变在线时,补发离线缓存的数据
6.2.9 服务端二次精简
SDK 已经精简过了,服务端入库前还可以再来一刀:
- 字段过滤:再次检查,去掉不需要的字段
- 堆栈再裁剪:生产环境再裁掉几帧
- 敏感信息二次脱敏:防止 SDK 端漏网之鱼
- 采样降频:超过配额的项目,服务端再降采样
- 冷热分层:热数据(7天)存完整信息,冷数据(30天+)只存聚合统计
6.2.10 体积优化总览
| 优化项 | Sentry 基线 | 优化后 | 减少比例 |
|---|---|---|---|
| 堆栈深度 | 50 帧 | 5 帧 | 80% |
| node_modules 帧 | 保留 | 过滤 | 50% |
| 环境信息 | 完整明细 | 字典编码 + 维度表 | 75-95% |
| 请求头/响应头 | 20+ 个全量 | 白名单 + 短编码 | 80-90% |
| 请求/响应 body | 默认带上 | 默认不上报 | 100% |
| 字段名 | 完整名称 | 短编码(t/l/m/ts...) | 50-60% |
| 时间戳 | 完整 13 位 | 相对时间戳 | 60-80% |
| 批量元数据 | 每条都带 | 共享头 + 字符串表 | 70-90% |
| 业务错误 | 完整上报 | 只计数/忽略 | 80-100% |
| 相同错误 | 每条都完整上报 | 第1条完整 + 其余计数 | 90%+ |
| 采样策略 | 固定采样率 | 智能采样(按价值) | 50-70% |
| 传输压缩 | 无 | gzip 压缩 | 60-80% |
| 综合(普通错误,gzip前) | ~3KB/条 | ~100-200B/条 | 93-97% |
| 综合(普通错误,gzip后) | ~1KB/条 | ~20-50B/条 | 95-98% |
6.3 错误安全
- SDK 自身 try-catch 包裹,错误不抛出
- 无限循环检测:相同错误 1 秒内 > 10 次自动暂停
- 递归保护:防止 SDK 错误触发新的 SDK 错误
七、与 Sentry SDK 的兼容性
7.1 兼容的部分
- DSN 格式完全一致
- 事件数据结构兼容(exception、stacktrace、user、tags 等)
- envelope 上报格式兼容
- API 风格类似(captureException、captureMessage、setUser 等)
7.2 不兼容的部分
- 没有 Scope 概念(简化设计)
- 没有 Hub 概念(单实例设计)
- 没有 Integration 概念(用 Plugin 替代)
- 不支持完整的分布式追踪(轻量版)
- 不支持 Session/Replay(可插件扩展)
7.3 迁移指南
从 Sentry 迁移到 Light-Sentry:
- import * as Sentry from '@sentry/browser';
+ import LightSDK from 'light-sentry';
- Sentry.init({ dsn: '...', ... });
+ LightSDK.init({ dsn: '...', ... });
// API 基本一致
- Sentry.captureException(err);
+ LightSDK.captureException(err);
- Sentry.setUser({ id: '1' });
+ LightSDK.setUser({ id: '1' });