一、为什么要做埋点本质埋点 ≠ 统计用户行为埋点 线上“黑盒调试能力”当线上出 bug 时你要能回答用户点了什么请求发了没参数是什么页面状态是什么报错发生在哪一步 本质就是还原用户现场二、埋点体系的完整结构一个成熟埋点系统 5部分事件埋点用户行为错误监控JS / Promise / 资源加载接口监控请求 响应性能监控上下文环境用户 页面 设备三、核心设计思路非常重要 不要乱埋点必须结构化统一格式四、第一步封装埋点SDK核心// // Enterprise Monitoring SDK // React TypeScript // Features: // - Event tracking // - Error monitoring // - API monitoring // - Breadcrumb // - Queue batch upload // - Offline cache (localStorage) // // ---------- types.ts ---------- export type LogLevel info | warn | error; export interface TrackEvent { event: string; level?: LogLevel; page?: string; timestamp?: number; userId?: string; extra?: Recordstring, any; breadcrumb?: Breadcrumb[]; } export interface Breadcrumb { event: string; data?: any; time: number; } // ---------- breadcrumb.ts ---------- class BreadcrumbManager { private list: Breadcrumb[] []; private max 20; add(event: string, data?: any) { this.list.push({ event, data, time: Date.now() }); if (this.list.length this.max) this.list.shift(); } get() { return [...this.list]; } } export const breadcrumb new BreadcrumbManager(); // ---------- queue.ts ---------- class Queue { private queue: TrackEvent[] []; private timer: any null; private maxSize 10; constructor(private flushFn: (logs: TrackEvent[]) void) {} push(log: TrackEvent) { this.queue.push(log); if (this.queue.length this.maxSize) { this.flush(); } else { this.schedule(); } } private schedule() { if (this.timer) return; this.timer setTimeout(() this.flush(), 3000); } flush() { if (!this.queue.length) return; const logs [...this.queue]; this.queue []; clearTimeout(this.timer); this.timer null; this.flushFn(logs); } } // ---------- storage.ts ---------- const STORAGE_KEY TRACK_OFFLINE_CACHE; export const storage { save(logs: TrackEvent[]) { const old this.get(); localStorage.setItem(STORAGE_KEY, JSON.stringify([...old, ...logs])); }, get(): TrackEvent[] { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || []); } catch { return []; } }, clear() { localStorage.removeItem(STORAGE_KEY); }, }; // ---------- transport.ts ---------- export async function sendLogs(logs: TrackEvent[]) { try { const res await fetch(/api/log, { method: POST, body: JSON.stringify(logs), headers: { Content-Type: application/json }, }); if (!res.ok) throw new Error(upload failed); } catch (err) { storage.save(logs); } } // ---------- tracker.ts ---------- import { breadcrumb } from ./breadcrumb; import { Queue } from ./queue; import { sendLogs } from ./transport; import { storage } from ./storage; class Tracker { private queue new Queue(sendLogs); constructor() { this.init(); } private init() { this.replayOffline(); this.bindError(); this.patchFetch(); } private base() { return { // userId: localStorage.getItem(userId) || anonymous, page: location.pathname, timestamp: Date.now(), }; } track(event: string, extra?: any, level: LogLevel info) { const log: TrackEvent { ...this.base(), event, level, extra, breadcrumb: breadcrumb.get(), }; this.queue.push(log); } private replayOffline() { const logs storage.get(); if (logs.length) { sendLogs(logs); storage.clear(); } } private bindError() { window.onerror (msg, url, line, col, error) { this.track(js_error, { msg, url, line, col, stack: error?.stack }, error); }; window.addEventListener(unhandledrejection, (e) { this.track(promise_error, { reason: e.reason }, error); }); } // private patchFetch() { // const originFetch window.fetch; // window.fetch async (...args) { // const start Date.now(); // try { // const res await originFetch(...args); // this.track(api_success, { // url: args[0], // status: res.status, // duration: Date.now() - start, // }); // return res; // } catch (err) { // this.track( // api_error, // { // url: args[0], // error: err, // }, // error, // ); // throw err; // } // }; // } } export const tracker new Tracker(); // ---------- react-hook.ts ---------- import { tracker } from ./tracker; export function useTrack() { return (event: string, extra?: any) { tracker.track(event, extra); }; } // ---------- auto-track.ts ---------- import { tracker } from ./tracker; export function initAutoTrack() { document.addEventListener(click, (e: any) { const el e.target; const name el?.dataset?.track; if (name) { tracker.track(name, { text: el.innerText, }); } }); } // ---------- usage ---------- /* import { tracker } from ./tracker; import { initAutoTrack } from ./auto-track; initAutoTrack(); tracker.track(page_view); */BreadcrumbManager这个实例记录用户行为Queue这个实例定时发送或者批量发送数据storage.ts用来记录离线数据Tracker.ts1、replayOffline()检查本地有没有离线数据如果有立刻尝试补发。这叫“断点续传”。2、bindError()监听js、promise报错有网上报日志反之离线缓存transport.ts sendLogs()上报日志或离线缓存autoTrack.ts 自动化埋点实际项目中 升级1实际项目中配置 axios 拦截器axios.interceptors.request.use((config) { config.metadata { start: Date.now() }; return config; }); axios.interceptors.response.use( (res) { tracker.track(api_success, { url: res.config.url, duration: Date.now() - res.config.metadata.start, }); return res; }, (err) { tracker.track(api_error, { url: err.config?.url, message: err.message, }, error); return Promise.reject(err); } );