1246 lines
35 KiB
Markdown
1246 lines
35 KiB
Markdown
# 前端 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 插件系统
|
||
|
||
每个插件实现标准接口:
|
||
|
||
```typescript
|
||
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 - 配置管理
|
||
|
||
#### 配置项
|
||
|
||
```typescript
|
||
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 - 事件总线
|
||
|
||
简单的发布订阅模式,用于插件间通信:
|
||
|
||
```typescript
|
||
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 - 事件队列
|
||
|
||
#### 队列策略
|
||
|
||
```
|
||
新事件 → 入队 → 检查是否触发上报 → 是 → 上报
|
||
│
|
||
└─ 否 → 等待
|
||
```
|
||
|
||
触发上报的条件:
|
||
1. **队列满**:达到 `maxQueueSize` 阈值
|
||
2. **定时**:每 `flushInterval` 毫秒
|
||
3. **页面隐藏**:`visibilitychange` → hidden
|
||
4. **页面卸载**:`beforeunload` / `pagehide`
|
||
5. **手动调用**:`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":[...]}
|
||
```
|
||
|
||
轻量模式下可用简化格式:
|
||
```json
|
||
{
|
||
"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) | 需手动接入 |
|
||
|
||
#### 错误事件格式
|
||
|
||
```typescript
|
||
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 |
|
||
|
||
#### 性能事件格式
|
||
|
||
```typescript
|
||
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`
|
||
|
||
#### 采集字段
|
||
|
||
```typescript
|
||
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 事件 + 防抖 | 页面滚动百分比 |
|
||
|
||
#### 行为事件格式
|
||
|
||
```typescript
|
||
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%
|
||
- **错误突增降级**:错误率过高时自动降采样
|
||
|
||
#### 采样算法
|
||
|
||
```javascript
|
||
// 基于用户 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 插件(面包屑)
|
||
|
||
记录用户操作轨迹,错误发生时一并上报:
|
||
|
||
```typescript
|
||
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 初始化
|
||
|
||
```javascript
|
||
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
|
||
|
||
```javascript
|
||
// 捕获错误
|
||
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
|
||
|
||
```javascript
|
||
import LightSDK from 'light-sentry';
|
||
import { VueIntegration } from 'light-sentry/vue';
|
||
|
||
LightSDK.init({
|
||
dsn: '...',
|
||
plugins: [new VueIntegration(Vue)],
|
||
});
|
||
```
|
||
|
||
#### Vue 3
|
||
|
||
```javascript
|
||
app.use(LightSDK.vuePlugin, { dsn: '...' });
|
||
```
|
||
|
||
#### React
|
||
|
||
```javascript
|
||
// 错误边界
|
||
import { ErrorBoundary } from 'light-sentry/react';
|
||
|
||
<ErrorBoundary fallback={<p>出错了</p>}>
|
||
<App />
|
||
</ErrorBoundary>
|
||
```
|
||
|
||
---
|
||
|
||
## 六、性能优化
|
||
|
||
### 6.1 不影响页面性能的设计
|
||
|
||
1. **全部异步**:SDK 初始化、事件处理、上报全异步
|
||
2. **requestIdleCallback**:非紧急操作放在空闲时间
|
||
3. **防抖节流**:scroll、resize 等高频事件防抖
|
||
4. **不修改原型**:用包装方式(fetch/xhr),不污染原型
|
||
5. **微任务批量**:同一批事件合并处理,减少事件循环开销
|
||
6. **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 帧就能定位问题
|
||
- 真正需要深度堆栈的错误很少,遇到了再开完整模式
|
||
- 业务代码的调用链通常不深
|
||
|
||
**配置方式**:
|
||
```javascript
|
||
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 映射成短编码,减少传输体积。
|
||
|
||
```javascript
|
||
// 内置字典
|
||
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 | 无 | 无 | 无 | 无 | 无 |
|
||
|
||
**配置方式**:
|
||
```javascript
|
||
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: {...}, ... }
|
||
```
|
||
|
||
**轻量方式(共享头)**:
|
||
```json
|
||
{
|
||
"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、表单校验失败这类「预期内的业务错误」,不需要完整上报:
|
||
|
||
```javascript
|
||
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。
|
||
|
||
**问题**:
|
||
```json
|
||
// 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 都有价值,默认只上报对排查问题有用的几个:
|
||
|
||
```javascript
|
||
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 名称映射成短编码,进一步减少体积。
|
||
|
||
```javascript
|
||
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',
|
||
// ...
|
||
};
|
||
```
|
||
|
||
**编码前后对比**:
|
||
```json
|
||
// 编码前(~150B)
|
||
{ "headers": { "content-type": "application/json", "x-request-id": "abc123" } }
|
||
|
||
// 编码后(~70B)
|
||
{ "h": { "ct": "application/json", "rid": "abc123" } }
|
||
```
|
||
|
||
##### 策略 3:Header 值字典化
|
||
|
||
对于值相对固定的 header,也可以做字典映射:
|
||
|
||
```javascript
|
||
// 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
|
||
|
||
```javascript
|
||
network: {
|
||
// ❌ 默认不上报请求/响应体
|
||
captureRequestBody: false,
|
||
captureResponseBody: false,
|
||
|
||
// (可选)只对特定接口上报 body,且做脱敏
|
||
captureBodyForUrls: [
|
||
'/api/debug/*',
|
||
],
|
||
|
||
// body 大小限制(即使开启也截断)
|
||
maxBodySize: 2048, // 最多 2KB
|
||
}
|
||
```
|
||
|
||
#### 6.2.7 进一步精简:协议层与传输层
|
||
|
||
前面的优化都是「内容层面」的精简,协议和传输层面还有很大空间。
|
||
|
||
##### 优化 1:全量字段短编码
|
||
|
||
不只是 header,**所有字段名**都可以用短编码,减少 JSON key 的重复开销。
|
||
|
||
```javascript
|
||
// 字段名映射表
|
||
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',
|
||
};
|
||
```
|
||
|
||
**编码前后对比**:
|
||
```json
|
||
// 编码前(~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:时间戳相对化
|
||
|
||
同一批事件的时间戳都很接近,用「相对于批次起始时间」的差值表示,数字更小、字符更少。
|
||
|
||
```json
|
||
// 传统方式(每条都是完整时间戳)
|
||
{
|
||
"events": [
|
||
{ "ts": 1704067200123, ... },
|
||
{ "ts": 1704067200456, ... },
|
||
{ "ts": 1704067200789, ... }
|
||
]
|
||
}
|
||
|
||
// 相对时间戳(批次头存基准时间,每条存差值)
|
||
{
|
||
"base_ts": 1704067200000,
|
||
"events": [
|
||
{ "ts": 123, ... },
|
||
{ "ts": 456, ... },
|
||
{ "ts": 789, ... }
|
||
]
|
||
}
|
||
```
|
||
|
||
**效果**:每条事件时间戳从 13 位数字 → 2-4 位,减少 60-80%。
|
||
|
||
##### 优化 3:批量内字符串去重
|
||
|
||
同一批事件里,很多字符串是重复的(同一页面、同一浏览器、同一版本),用「字符串表 + 索引」的方式。
|
||
|
||
```json
|
||
// 传统方式(每条都带完整字符串)
|
||
{
|
||
"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%,而是动态调整)
|
||
|
||
不是所有事件都值得同样的采样率,按「价值」动态调整:
|
||
|
||
```javascript
|
||
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 次:只上报计数 + 时间戳(几十字节)
|
||
```
|
||
|
||
```json
|
||
{
|
||
"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 已经精简过了,服务端入库前还可以再来一刀:
|
||
|
||
1. **字段过滤**:再次检查,去掉不需要的字段
|
||
2. **堆栈再裁剪**:生产环境再裁掉几帧
|
||
3. **敏感信息二次脱敏**:防止 SDK 端漏网之鱼
|
||
4. **采样降频**:超过配额的项目,服务端再降采样
|
||
5. **冷热分层**:热数据(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:
|
||
|
||
```diff
|
||
- 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' });
|
||
```
|