# 前端 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 = 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; // 标签 extra?: Record; // 额外数据 breadcrumbs?: Breadcrumb[]; // 面包屑 request?: { // 请求信息 url: string; method?: string; headers?: Record; }; } 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; } ``` #### 性能评级标准(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; // 各子类型的属性 } ``` #### 点击事件属性 ``` - 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; 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'; 出错了

}>
``` --- ## 六、性能优化 ### 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' }); ```