优秀的编程知识分享平台

网站首页 > 技术文章 正文

如何设计前端监控sdk,实现前端项目全链路监控

nanyue 2025-09-29 09:06:03 技术文章 1 ℃

一、埋点系统设计与实现(文章最后有如何回答)

1. 埋点分类

1.1 手动埋点(代码埋点)

// 业务代码中主动调用
tracker.track('button_click', {
  button_id: 'submit_btn',
  page: 'checkout',
  timestamp: Date.now()
});

// 封装更友好的API
tracker.clickTracker('submit_btn', 'checkout');

1.2 自动埋点(无痕埋点)

// 全局点击监听
document.addEventListener('click', (e) => {
  const target = e.target;
  const xpath = getXPath(target); // 生成元素XPath
  const data = {
    event: 'click',
    xpath,
    text: target.innerText?.slice(0, 20),
    href: target.href,
    page: location.pathname,
    timestamp: Date.now()
  };
  tracker.track(data);
}, true);

// 获取元素XPath
function getXPath(element) {
  if (element.id) return `//*[@id="${element.id}"]`;
  if (element === document.body) return '/html/body';

  let ix = 0;
  const siblings = element.parentNode.childNodes;
  for (let i = 0; i < siblings.length; i++) {
    const sibling = siblings[i];
    if (sibling === element) {
      return `${getXPath(element.parentNode)}/${element.tagName.toLowerCase()}[${ix + 1}]`;
    }
    if (sibling.nodeType === 1 && sibling.tagName === element.tagName) {
      ix++;
    }
  }
}

1.3 可视化埋点

通过管理后台圈选页面元素

生成对应配置规则

SDK根据配置自动监听指定元素

2. 埋点优化策略

2.1 防抖节流

const throttle = (fn, delay) => {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last > delay) {
      fn.apply(this, args);
      last = now;
    }
  };
};

// 滚动事件节流处理
window.addEventListener('scroll', throttle(() => {
  tracker.track('page_scroll', {
    scrollY: window.scrollY,
    scrollPercent: (window.scrollY / (document.body.scrollHeight - window.innerHeight))
    * 100
  });
}, 1000));

2.2 数据压缩

// 使用JSON简写格式
const compressData = (data) => {
  return {
    e: data.event,       // event
    t: data.timestamp,   // timestamp
    p: data.page,        // page
    d: data.data         // data
  };
};

2.3 本地缓存

class CacheManager {
  constructor(maxSize = 50) {
    this.maxSize = maxSize;
    this.cacheKey = 'monitor_cache';
  }

  add(data) {
    let cache = this.getAll();
    cache.push(data);

    // 超出限制移除最早的数据
    if (cache.length > this.maxSize) {
      cache = cache.slice(cache.length - this.maxSize);
    }

    localStorage.setItem(this.cacheKey, JSON.stringify(cache));
  }

  getAll() {
    const data = localStorage.getItem(this.cacheKey);
    return data ? JSON.parse(data) : [];
  }

  clear() {
    localStorage.removeItem(this.cacheKey);
  }
}

二、错误监控深度实现

1. JavaScript错误捕获

1.1 同步错误捕获

window.onerror = (message, source, lineno, colno, error) => {
  tracker.trackError({
    type: 'js_error',
    message,
    source,
    lineno,
    colno,
    stack: error?.stack,
    timestamp: Date.now()
  });
};

1.2 异步错误捕获

window.addEventListener('error', (event) => {
  // 过滤掉非JS错误(如资源加载错误)
  if (event.error) {
    tracker.trackError({
      type: 'js_error',
      message: event.message,
      stack: event.error.stack,
      timestamp: Date.now()
    });
  }
}, true);

// Promise未捕获异常
window.addEventListener('unhandledrejection', (event) => {
  tracker.trackError({
    type: 'promise_error',
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
    timestamp: Date.now()
  });
});

2. 资源加载错误监控

// 方式1:通过error事件捕获
window.addEventListener('error', (e) => {
  const target = e.target;
  if (target && (target.tagName === 'LINK' || target.tagName === 'SCRIPT' || target.tagName
  === 'IMG')) {
    tracker.trackError({
      type: 'resource_error',
      tag: target.tagName,
      url: target.src || target.href,
      timestamp: Date.now()
    });
  }
}, true);

// 方式2:通过PerformanceObserver
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.initiatorType !== 'navigation' && entry.duration === 0) {
      tracker.trackError({
        type: 'resource_error',
        url: entry.name,
        initiatorType: entry.initiatorType,
        timestamp: Date.now()
      });
    }
  });
});
observer.observe({ entryTypes: ['resource'] });

3. API请求错误监控

3.1 Fetch拦截

const originalFetch = window.fetch;
window.fetch = async (...args) => {
  const start = Date.now();
  try {
    const response = await originalFetch(...args);
    if (!response.ok) {
      tracker.trackError({
        type: 'api_error',
        url: args[0],
        status: response.status,
        duration: Date.now() - start,
        timestamp: Date.now()
      });
    }
    return response;
  } catch (error) {
    tracker.trackError({
      type: 'api_error',
      url: args[0],
      status: 0,
      message: error.message,
      duration: Date.now() - start,
      timestamp: Date.now()
    });
    throw error;
  }
};

3.2 XMLHttpRequest拦截

const originalXHROpen = XMLHttpRequest.prototype.open;
const originalXHRSend = XMLHttpRequest.prototype.send;

XMLHttpRequest.prototype.open = function(method, url) {
  this._startTime = Date.now();
  this._method = method;
  this._url = url;
  return originalXHROpen.apply(this, arguments);
};

XMLHttpRequest.prototype.send = function(body) {
  this.addEventListener('loadend', () => {
    if (this.status >= 400) {
      tracker.trackError({
        type: 'api_error',
        url: this._url,
        method: this._method,
        status: this.status,
        duration: Date.now() - this._startTime,
        timestamp: Date.now()
      });
    }
  });
  return originalXHRSend.apply(this, arguments);
};

4. 错误聚合与指纹生成

function generateErrorFingerprint(error) {
  // 基础信息
  const { message, stack } = error;

  // 提取堆栈关键信息
  let stackTrace = '';
  if (stack) {
    const stackLines = stack.split('\n');
    // 取前3个堆栈帧
    for (let i = 0; i < Math.min(3, stackLines.length); i++) {
      const line = stackLines[i];
      // 提取文件名和行号
      const match = line.match(/\(?(.+):(\d+):(\d+)\)?/);
      if (match) {
        const file = match[1].split('/').pop();
        stackTrace += `${file}:${match[2]}|`;
      }
    }
  }

  // 生成指纹
  return md5(`${message}|${stackTrace}`);
}

三、性能指标深度采集

1. 关键性能指标采集

1.1 使用Performance API

// 获取Navigation Timing数据
const getNavigationTiming = () => {
  const [entry] = performance.getEntriesByType('navigation');
  return {
    dns: entry.domainLookupEnd - entry.domainLookupStart,
    tcp: entry.connectEnd - entry.connectStart,
    ssl: entry.secureConnectionStart > 0 ? entry.connectEnd - entry.secureConnectionStart
    : 0,
    ttfb: entry.responseStart - entry.requestStart,
    transfer: entry.responseEnd - entry.responseStart,
    domReady: entry.domComplete - entry.domInteractive,
    load: entry.loadEventEnd - entry.loadEventStart
  };
};

1.2 使用PerformanceObserver

// 监听LCP (Largest Contentful Paint)
const lcpObserver = new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];
  tracker.trackPerformance({
    type: 'LCP',
    value: lastEntry.renderTime || lastEntry.loadTime,
    timestamp: Date.now()
  });
});
lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true });

// 监听CLS (Cumulative Layout Shift)
let clsValue = 0;
let clsEntries = [];
const clsObserver = new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      clsEntries.push(entry);
      clsValue += entry.value;
    }
  }
});
clsObserver.observe({ type: 'layout-shift', buffered: true });

// 页面卸载前上报CLS
window.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    tracker.trackPerformance({
      type: 'CLS',
      value: clsValue,
      entries: clsEntries,
      timestamp: Date.now()
    });
    clsValue = 0;
    clsEntries = [];
  }
});

2. 自定义性能指标

2.1 首屏时间计算

function getFirstScreenTime() {
  // 方法1:通过MutationObserver监听DOM变化
  return new Promise((resolve) => {
    const ignoreTags = ['SCRIPT', 'STYLE', 'LINK', 'META'];
    const observer = new MutationObserver(() => {
      const viewportHeight = window.innerHeight;
      const images = Array.from(document.images);

      // 检查首屏内图片是否加载完成
      const loaded = images.filter(img => {
        const rect = img.getBoundingClientRect();
        return rect.top < viewportHeight && img.complete;
      });

      if (loaded.length === images.length) {
        observer.disconnect();
        resolve(Date.now() - performance.timing.navigationStart);
      }
    });

    observer.observe(document, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['src']
    });

    // 超时处理
    setTimeout(() => {
      observer.disconnect();
      resolve(Date.now() - performance.timing.navigationStart);
    }, 10000);
  });
}

2.2 卡顿检测

// 通过长任务API检测卡顿
const longTaskObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 100) { // 超过100ms视为长任务
      tracker.trackPerformance({
        type: 'long_task',
        duration: entry.duration,
        startTime: entry.startTime,
        timestamp: Date.now()
      });
    }
  }
});
longTaskObserver.observe({ entryTypes: ['longtask'] });

// 通过FPS检测卡顿
let lastTime = performance.now();
let frameCount = 0;
let fps = 60;
const checkFPS = () => {
  frameCount++;
  const now = performance.now();
  if (now - lastTime >= 1000) {
    fps = Math.round((frameCount * 1000) / (now - lastTime));
    frameCount = 0;
    lastTime = now;

    if (fps < 30) { // FPS低于30视为卡顿
      tracker.trackPerformance({
        type: 'low_fps',
        value: fps,
        timestamp: Date.now()
      });
    }
  }
  requestAnimationFrame(checkFPS);
};
checkFPS();

3. 资源性能分析

// 获取所有资源加载性能数据
const getResourceTiming = () => {
  return performance.getEntriesByType('resource').map(resource => ({
    name: resource.name,
    type: resource.initiatorType,
    duration: resource.duration,
    size: resource.transferSize,
    dns: resource.domainLookupEnd - resource.domainLookupStart,
    tcp: resource.connectEnd - resource.connectStart,
    ttfb: resource.responseStart - resource.requestStart,
    transfer: resource.responseEnd - resource.responseStart
  }));
};

// 监听新增资源加载
const resourceObserver = new PerformanceObserver((list) => {
  list.getEntries().forEach(resource => {
    tracker.trackPerformance({
      type: 'resource_load',
      name: resource.name,
      initiatorType: resource.initiatorType,
      duration: resource.duration,
      timestamp: Date.now()
    });
  });
});
resourceObserver.observe({ type: 'resource', buffered: true });

四、高级优化技巧

1. 数据上报优化

1.1 优先级队列

class ReportQueue {
  constructor() {
    this.highPriority = []; // 错误、关键性能数据
    this.lowPriority = [];  // 行为数据、非关键指标
    this.isSending = false;
  }

  add(data, priority = 'low') {
    if (priority === 'high') {
      this.highPriority.push(data);
    } else {
      this.lowPriority.push(data);
    }
    this.send();
  }

  send() {
    if (this.isSending) return;
    this.isSending = true;

    // 优先发送高优先级数据
    const data = this.highPriority.length > 0 
      ? this.highPriority.shift()
      : this.lowPriority.shift();

    if (!data) {
      this.isSending = false;
      return;
    }

    fetch('/report', {
      method: 'POST',
      body: JSON.stringify(data),
      keepalive: true
    }).finally(() => {
      this.isSending = false;
      if (this.highPriority.length > 0 || this.lowPriority.length > 0) {
        setTimeout(() => this.send(), 0);
      }
    });
  }
}

1.2 Web Worker处理

// 主线程
const worker = new Worker('monitor.worker.js');
worker.postMessage({
  type: 'track',
  data: {
    event: 'click',
    // ...其他数据
  }
});

// monitor.worker.js
self.onmessage = (e) => {
  const { type, data } = e.data;
  if (type === 'track') {
    // 处理数据并存储
    const compressed = compressData(data);
    storeData(compressed);

    // 批量上报
    if (shouldReport()) {
      const batch = getBatchData();
      sendBatch(batch);
    }
  }
};

function sendBatch(batch) {
  fetch('/report', {
    method: 'POST',
    body: JSON.stringify(batch)
  }).catch(() => {
    // 失败后重新放回队列
    storeBatch(batch);
  });
}

2. 数据采样策略

2.1 分层采样

function getSampleRate(eventType) {
  const rates = {
    error: 1.0,       // 错误全采集
    performance: 0.3,  // 性能数据30%
    click: 0.1,       // 点击事件10%
    scroll: 0.05      // 滚动事件5%
  };
  return rates[eventType] || 0.1;
}

function shouldTrack(eventType) {
  const rate = getSampleRate(eventType);
  return Math.random() < rate;
}

2.2 用户ID哈希采样

function getUserSampleRate(userId) {
  // 将用户ID哈希后取模
  const hash = md5(userId).substring(0, 8);
  const intVal = parseInt(hash, 16);
  return intVal % 100 / 100; // 返回0-1之间的值
}

function shouldTrackUser(userId, sampleRate) {
  return getUserSampleRate(userId) < sampleRate;
}

3. 数据增强

3.1 设备信息收集

function getDeviceInfo() {
  const ua = navigator.userAgent;
  const screen = window.screen;
  return {
    ua,
    platform: navigator.platform,
    screenWidth: screen.width,
    screenHeight: screen.height,
    colorDepth: screen.colorDepth,
    devicePixelRatio: window.devicePixelRatio || 1,
    cpuCores: navigator.hardwareConcurrency || 'unknown',
    memory: navigator.deviceMemory || 'unknown',
    connection: navigator.connection ? {
      effectiveType: navigator.connection.effectiveType,
      rtt: navigator.connection.rtt,
      downlink: navigator.connection.downlink,
      saveData: navigator.connection.saveData
    } : 'unknown'
  };
}

3.2 会话信息

class Session {
  constructor() {
    this.sessionId = generateUUID();
    this.startTime = Date.now();
    this.pageViewCount = 0;
    this.lastActivity = Date.now();
  }

  trackPageView() {
    this.pageViewCount++;
    this.lastActivity = Date.now();
  }

  isExpired() {
    // 30分钟无活动视为会话结束
    return Date.now() - this.lastActivity > 30 * 60 * 1000;
  }

  renew() {
    if (this.isExpired()) {
      this.sessionId = generateUUID();
      this.startTime = Date.now();
      this.pageViewCount = 0;
    }
    this.lastActivity = Date.now();
  }
}

五、实际案例分析

1. SPA应用监控方案

1.1 路由变化监听

// Vue Router示例
router.afterEach((to, from) => {
  tracker.trackPageView({
    from: from.fullPath,
    to: to.fullPath,
    duration: Date.now() - tracker.currentPageStartTime
  });
  tracker.currentPageStartTime = Date.now();
});

// 手动监听history变化
const originalPushState = history.pushState;
history.pushState = function(state, title, url) {
  originalPushState.apply(this, arguments);
  tracker.trackPageView(url);
};

window.addEventListener('popstate', () => {
  tracker.trackPageView(location.pathname);
});

1.2 组件级性能监控

// Vue组件生命周期监控
const componentTracker = {
  install(Vue) {
    Vue.mixin({
      beforeCreate() {
        this.$_startTime = Date.now();
      },
      mounted() {
        const duration = Date.now() - this.$_startTime;
        tracker.trackPerformance({
          type: 'component_mount',
          name: this.$options.name || 'anonymous',
          duration,
          timestamp: Date.now()
        });
      }
    });
  }
};

2. 错误定位优化

2.1 SourceMap解析

async function parseErrorStack(error) {
  if (!error.stack) return error.stack;

  const stackLines = error.stack.split('\n');
  const parsedStack = [];

  for (const line of stackLines) {
    const match = line.match(/at (.+) \((.+):(\d+):(\d+)\)/);
    if (match) {
      const [, method, file, lineNo, columnNo] = match;
      const sourcePos = await sourceMapService.lookup(file, lineNo, columnNo);
      parsedStack.push({
        method,
        file,
        line: lineNo,
        column: columnNo,
        sourceFile: sourcePos?.source,
        sourceLine: sourcePos?.line,
        sourceColumn: sourcePos?.column
      });
    } else {
      parsedStack.push({ raw: line });
    }
  }

  return parsedStack;
}

2.2 错误上下文收集

function collectErrorContext() {
  return {
    url: location.href,
    route: window.currentRoute, // SPA应用当前路由
    userAgent: navigator.userAgent,
    localStorageKeys: Object.keys(localStorage),
    cookies: document.cookie,
    screen: `${window.screen.width}x${window.screen.height}`,
    viewport: `${window.innerWidth}x${window.innerHeight}`,
    network: navigator.connection?.effectiveType,
    memory: navigator.deviceMemory,
    // 最近3个用户操作
    userActions: tracker.getRecentActions(3),
    // 当前页面DOM节点数
    domCount: document.getElementsByTagName('*').length
  };
}

六、总结与最佳实践

1. 设计原则总结

性能优先:监控系统本身不能成为性能瓶颈

数据准确:确保采集的数据真实可靠

渐进增强:根据用户设备能力动态调整采集策略

故障隔离:监控系统异常不能影响主业务

可扩展性:方便添加新的监控维度

2. 推荐配置示例

const defaultConfig = {
  // 错误监控
  error: {
    enable: true,
    sampleRate: 1.0, // 错误全采集
    ignore: [/Script error/i], // 忽略某些错误
    maxStackDepth: 10 // 堆栈最大深度
  },

  // 性能监控
  performance: {
    enable: true,
    sampleRate: 0.3,
    metrics: ['LCP', 'FID', 'CLS', 'FCP', 'TTFB'],
    resourceTiming: true,
    longTaskThreshold: 100 // 长任务阈值(ms)
  },

  // 行为监控
  behavior: {
    enable: true,
    sampleRate: 0.1,
    events: ['click', 'scroll', 'input'],
    scrollThreshold: 50 // 滚动超过50px才记录
  },

  // 上报配置
  report: {
    url: 'https://api.monitor.com/report',
    batchSize: 5, // 批量上报条数
    interval: 10000, // 上报间隔(ms)
    retryTimes: 3, // 失败重试次数
    useBeacon: true // 是否使用sendBeacon
  },

  // 调试模式
  debug: false
};

3. 持续优化方向

智能采样:基于用户价值动态调整采样率

异常预测:通过机器学习识别潜在问题

前端追踪:实现完整的分布式追踪

性能洞察:提供可操作的性能优化建议

隐私保护:加强数据脱敏和合规处理

通过以上详细实现方案,可以构建一个功能完善、性能优异的前端监控SDK,满足各种复杂场景下的监控需求。在面试中,可以根据面试官的问题选择适当的深度进行讲解,同时结合自己的实际项目经验,展示解决具体问题的能力。


面试怎么说?

一、核心要点概述(1分钟)

"我设计的前端监控SDK主要解决三个核心问题:

全面错误监控:覆盖JS运行时、资源加载、API请求等各类异常

性能指标采集:实现Web Vitals标准指标和业务自定义指标

用户行为追踪:通过手动/自动埋点记录关键交互路径

整体设计遵循轻量级、低侵入原则,SDK体积控制在15KB以内,通过智能采样和分级上报保证系统稳定性。"

二、关键模块精讲(3分钟)

1. 错误监控

"错误系统实现了:

同步错误通过window.onerror捕获

异步错误通过unhandledrejection处理

资源错误监听元素error事件

API错误拦截fetch/XHR

为每个错误生成唯一指纹,附加设备、路由等上下文"

2. 性能监控

"性能模块重点采集:

加载指标:FP/FCP/LCP,交互指标:FID,视觉稳定性:CLS

通过PerformanceObserver监听长任务

针对SPA应用优化路由切换时的测量"

3. 埋点设计

"埋点系统采用分层策略:关键业务点手动埋点,通用交互自动采集,通过防抖节流优化性能,上报采用优先级队列,错误数据优先发送"

三、技术亮点(2分钟)

"解决的主要技术难点包括:

SPA监控:重写路由API确保页面跳转准确追踪

数据可靠性:本地缓存+失败重试机制

性能平衡:动态采样和懒加载机制

错误分析:SourceMap解析和错误聚合"

四、实践案例(1分钟)

"在实际项目中,通过这套系统:

发现并解决了第三方SDK导致的长任务卡顿

将LCP指标优化了30%

错误排查时间缩短50%"

五、总结(30秒)

"这套监控系统帮助我们建立了完善的前端可观测体系,后续计划加入智能告警和全链路追踪功能。"

最近发表
标签列表