一、为什么要做埋点(本质)

埋点 ≠ 统计用户行为
埋点 = 线上“黑盒调试能力”

当线上出 bug 时,你要能回答:

  • 用户点了什么?
  • 请求发了没?
  • 参数是什么?
  • 页面状态是什么?
  • 报错发生在哪一步?

👉 本质就是:还原用户现场

二、埋点体系的完整结构

一个成熟埋点系统 = 5部分

  1. 事件埋点(用户行为)
  2. 错误监控(JS / Promise / 资源加载)
  3. 接口监控(请求 + 响应)
  4. 性能监控
  5. 上下文环境(用户 + 页面 +设备)
三、核心设计思路(非常重要)

👉 不要乱埋点,必须结构化

统一格式:

四、第一步:封装埋点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);
  }
);

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐