前端性能优化之埋点监控实战
·
一、为什么要做埋点(本质)
埋点 ≠ 统计用户行为
埋点 = 线上“黑盒调试能力”
当线上出 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?: Record<string, 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.ts
1、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);
}
);
更多推荐
所有评论(0)