前端用户行为监控

前端用户行为监控是收集用户在网站或应用上的交互数据,以便分析用户行为、优化产品体验、发现潜在问题的重要手段。以下是一些常见的前端用户行为监控类型及其详细代码讲解。

在开始之前,我们先定义一个通用的数据上报函数 reportData,用于将收集到的数据发送到后端服务器。为了避免影响页面性能和用户体验,通常推荐使用 navigator.sendBeacon 或在页面卸载时使用 fetchkeepalive 选项。

// 定义一个通用的数据上报函数
function reportData(eventType, data) {
  const payload = {
    eventType: eventType,
    timestamp: Date.now(),
    url: window.location.href,
    userAgent: navigator.userAgent,
    ...data,
  };

  // 假设你的后端上报接口是 /api/monitor
  const url = "/api/monitor";

  // 优先使用 sendBeacon,因为它在页面卸载时也能可靠地发送数据
  if (navigator.sendBeacon) {
    navigator.sendBeacon(url, JSON.stringify(payload));
    console.log(`Data reported via sendBeacon: ${eventType}`, payload);
  } else {
    // 降级使用 fetch,并设置 keepalive 以便在页面卸载时也能发送
    fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(payload),
      keepalive: true, // 确保在页面卸载时请求也能完成
    })
      .then((response) => {
        if (!response.ok) {
          console.error(
            `Failed to report data via fetch: ${eventType}`,
            response.statusText
          );
        } else {
          console.log(`Data reported via fetch: ${eventType}`, payload);
        }
      })
      .catch((error) => {
        console.error(`Error reporting data via fetch: ${eventType}`, error);
      });
  }
}

1. 页面访问量 (PV) 监控

页面访问量是最基础的监控指标,用于统计有多少次页面被加载。

实现原理:

在页面加载完成后立即发送一个请求。

代码示例:

// 页面访问量 (PV) 监控
function setupPageViewMonitoring() {
  // 确保在 DOM 完全加载后执行,避免阻塞页面渲染
  window.addEventListener("load", () => {
    reportData("page_view", {
      title: document.title,
      referrer: document.referrer,
    });
  });

  // 对于单页应用 (SPA),需要监听路由变化
  // 这里以 history API 为例,如果是 hash 路由,监听 'hashchange'
  if (window.history && typeof window.history.pushState === "function") {
    const originalPushState = window.history.pushState;
    window.history.pushState = function () {
      originalPushState.apply(this, arguments);
      reportData("page_view", {
        title: document.title,
        referrer: document.referrer,
        isSPA: true, // 标记为 SPA 路由变化
      });
    };

    const originalReplaceState = window.history.replaceState;
    window.history.replaceState = function () {
      originalReplaceState.apply(this, arguments);
      reportData("page_view", {
        title: document.title,
        referrer: document.referrer,
        isSPA: true, // 标记为 SPA 路由变化
      });
    };

    window.addEventListener("popstate", () => {
      reportData("page_view", {
        title: document.title,
        referrer: document.referrer,
        isSPA: true, // 标记为 SPA 路由变化
      });
    });
  }
}

// 调用函数启动监控
setupPageViewMonitoring();

代码讲解:

  • window.addEventListener('load', ...): 监听 load 事件,确保整个页面(包括所有资源)都已加载完毕后发送数据。
  • 对于单页应用 (SPA),仅监听 load 事件不足以统计 PV。需要劫持 history.pushState, history.replaceState 方法以及监听 popstate 事件来捕获路由变化,从而模拟页面切换。
  • reportData('page_view', { ... }): 调用上报函数,事件类型为 page_view,并附带页面标题和来源页信息。

2. 点击事件 (Click) 监控

点击事件监控用于统计用户点击了哪些元素,常用于按钮、链接等交互元素的统计。

实现原理:

利用事件委托(Event Delegation)在文档根部监听 click 事件,根据点击的元素或其父元素上的特定属性(如 data-track-id)来判断是否需要上报。

代码示例:

// 点击事件 (Click) 监控
function setupClickMonitoring() {
  document.addEventListener("click", (event) => {
    let target = event.target;
    // 向上冒泡查找带有 data-track-id 属性的元素
    while (target && target !== document.body) {
      if (target.dataset && target.dataset.trackId) {
        reportData("click", {
          elementId: target.id,
          trackId: target.dataset.trackId,
          tagName: target.tagName,
          innerText: target.innerText.substring(0, 50), // 截取部分文本
        });
        break; // 找到即停止
      }
      target = target.parentNode;
    }
  });
}

// 调用函数启动监控
setupClickMonitoring();

HTML 示例:

<button id="submitBtn" data-track-id="submit_form_button">提交</button>
<a href="/products" data-track-id="nav_products_link">查看产品</a>

代码讲解:

  • document.addEventListener('click', ...): 在 document 上监听点击事件,利用事件冒泡机制。
  • while (target && target !== document.body): 循环向上查找,直到找到带有 data-track-id 属性的元素或者到达 body
  • target.dataset.trackId: 获取自定义的 data-track-id 属性值,用于标识点击的元素。
  • reportData('click', { ... }): 上报事件类型为 click,并附带元素 ID、跟踪 ID、标签名和部分文本内容。

3. 元素曝光 (Impression) 监控

元素曝光监控用于统计特定元素何时进入用户的可视区域,常用于广告、商品卡片、文章列表等。

实现原理:

使用 Intersection Observer API,这是一个异步观察目标元素与祖先元素或顶级文档视窗交叉状态的 API。

代码示例:

// 元素曝光 (Impression) 监控
function setupImpressionMonitoring() {
  if (!("IntersectionObserver" in window)) {
    console.warn("IntersectionObserver API is not supported in this browser.");
    return;
  }

  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          // 元素进入可视区域
          const element = entry.target;
          if (element.dataset && element.dataset.trackImpressionId) {
            reportData("impression", {
              trackImpressionId: element.dataset.trackImpressionId,
              elementId: element.id,
              tagName: element.tagName,
              // 可以添加更多元素相关信息,如位置、尺寸等
              boundingClientRect: element.getBoundingClientRect(),
            });
            // 一旦曝光,停止观察,避免重复上报
            observer.unobserve(element);
          }
        }
      });
    },
    {
      root: null, // 根元素,默认为浏览器视窗
      rootMargin: "0px", // 根元素的边距
      threshold: 0.1, // 目标元素与根元素交叉的比例达到 10% 时触发回调
    }
  );

  // 查找所有需要监控曝光的元素
  document.querySelectorAll("[data-track-impression-id]").forEach((element) => {
    observer.observe(element);
  });
}

// 调用函数启动监控
setupImpressionMonitoring();

HTML 示例:

<div
  id="adBanner1"
  data-track-impression-id="homepage_top_banner"
  style="height: 200px; background: lightblue; margin-top: 1000px;"
>
  这是一个广告横幅
</div>
<div
  id="productCard1"
  data-track-impression-id="product_list_item_1"
  style="height: 300px; background: lightgreen; margin-top: 500px;"
>
  商品卡片 1
</div>

代码讲解:

  • IntersectionObserver: 实例化一个观察器。
    • entries: 回调函数参数,包含所有被观察元素的交叉状态信息。
    • entry.isIntersecting: 如果为 true,表示元素进入了可视区域。
    • threshold: 0.1: 表示当目标元素有 10% 的面积进入可视区域时触发回调。可以根据需求调整,例如 0 表示只要有一个像素进入就触发,1 表示整个元素完全进入才触发。
  • observer.observe(element): 开始观察指定元素。
  • observer.unobserve(element): 在元素曝光并上报后,停止观察该元素,避免重复上报。
  • document.querySelectorAll('[data-track-impression-id]'): 查找所有带有 data-track-impression-id 属性的元素进行观察。

4. 性能指标 (Performance) 监控

性能监控是前端监控的重要组成部分,可以帮助我们了解用户实际体验到的页面加载速度和流畅度。主要关注 Core Web Vitals (LCP, FID/INP, CLS) 以及其他关键指标 (TTFB, FCP, DOMContentLoaded, Load)。

实现原理:

使用 Performance API (如 PerformanceObserver, performance.getEntriesByType) 来获取各种性能数据。

代码示例:

// 性能指标 (Performance) 监控
function setupPerformanceMonitoring() {
  if (!("PerformanceObserver" in window)) {
    console.warn("PerformanceObserver API is not supported in this browser.");
    return;
  }

  // 1. 监控 Largest Contentful Paint (LCP)
  const lcpObserver = new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries();
    const lastEntry = entries[entries.length - 1]; // LCP 通常是最后一个
    if (lastEntry) {
      reportData("performance_lcp", {
        lcp: lastEntry.renderTime || lastEntry.loadTime,
        element: lastEntry.element ? lastEntry.element.tagName : "N/A",
        url: lastEntry.url,
      });
      lcpObserver.disconnect(); // 仅上报一次
    }
  });
  lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });

  // 2. 监控 Cumulative Layout Shift (CLS)
  const clsObserver = new PerformanceObserver((entryList) => {
    let cls = 0;
    for (const entry of entryList.getEntries()) {
      if (!entry.hadRecentInput) {
        // 排除用户输入导致的布局变化
        cls += entry.value;
      }
    }
    // CLS 可能会持续累积,这里可以在页面卸载时上报最终值
    // 或者每隔一段时间上报当前累积值
    // 为了简化,这里在页面卸载时上报
    window.addEventListener("beforeunload", () => {
      reportData("performance_cls", { cls: cls });
    });
  });
  clsObserver.observe({ type: "layout-shift", buffered: true });

  // 3. 监控 First Input Delay (FID) / Interaction to Next Paint (INP)
  // FID 已被 INP 取代,INP 更能反映用户交互的响应性
  // INP 测量的是从用户第一次交互到浏览器绘制下一帧之间的时间。
  const inpObserver = new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries();
    const lastEntry = entries[entries.length - 1]; // INP 是所有交互中最大的那个
    if (lastEntry) {
      reportData("performance_inp", {
        inp: lastEntry.duration,
        name: lastEntry.name,
        entryType: lastEntry.entryType,
      });
      // INP 可能会持续更新,这里可以考虑在页面卸载时上报最终值
      // 或者定期上报当前最大值
    }
  });
  inpObserver.observe({ type: "event", buffered: true });

  // 4. 监控 First Contentful Paint (FCP) 和 Time to First Byte (TTFB)
  // FCP 和 TTFB 可以通过 performance.getEntriesByType('paint') 和 Navigation Timing API 获取
  window.addEventListener("load", () => {
    setTimeout(() => {
      // 确保所有 paint 事件都已记录
      const paintEntries = performance.getEntriesByType("paint");
      const fcpEntry = paintEntries.find(
        (entry) => entry.name === "first-contentful-paint"
      );
      if (fcpEntry) {
        reportData("performance_fcp", { fcp: fcpEntry.startTime });
      }

      const navTiming = performance.getEntriesByType("navigation")[0];
      if (navTiming) {
        reportData("performance_ttfb", {
          ttfb: navTiming.responseStart - navTiming.requestStart,
        });
        reportData("performance_dom_ready", {
          domReady:
            navTiming.domContentLoadedEventEnd -
            navTiming.domContentLoadedEventStart,
        });
        reportData("performance_load", {
          loadTime: navTiming.loadEventEnd - navTiming.loadEventStart,
        });
        reportData("performance_full_load", {
          fullLoadTime: navTiming.loadEventEnd - navTiming.fetchStart,
        });
      }
    }, 0); // 异步执行,确保所有性能条目都已准备好
  });
}

// 调用函数启动监控
setupPerformanceMonitoring();

代码讲解:

  • PerformanceObserver: 用于监听性能事件,如 LCP, CLS, INP。
    • observe({ type: '...', buffered: true }): buffered: true 允许访问观察器创建之前发生的条目。
  • LCP (Largest Contentful Paint) : 测量视口中最大的内容元素(图片或文本块)渲染完成所需的时间。
  • CLS (Cumulative Layout Shift) : 测量页面整个生命周期中所有意外布局偏移的累积得分。
  • INP (Interaction to Next Paint) : 测量从用户交互(如点击、轻触、键盘输入)到浏览器绘制下一帧所需的时间。
  • FCP (First Contentful Paint) : 测量浏览器首次渲染任何文本、图像、非白色 canvas 或 SVG 的时间。
  • TTFB (Time to First Byte) : 测量从请求页面到接收到响应的第一个字节所需的时间。
  • DOMContentLoaded: DOMContentLoaded 事件触发时,表示文档已被完全加载和解析,无需等待样式表、图像和子帧的加载。
  • Load: load 事件触发时,表示整个页面,包括所有依赖资源(如样式表和图像),都已完全加载。

5. 错误日志 (Error) 监控

错误日志监控用于捕获 JavaScript 运行时错误和资源加载错误,帮助开发者及时发现并修复问题。

实现原理:

监听 window.onerrorwindow.addEventListener('error', true) 事件。

代码示例:

// 错误日志 (Error) 监控
function setupErrorMonitoring() {
  // 1. 捕获 JavaScript 运行时错误 (onerror)
  window.onerror = function (message, source, lineno, colno, error) {
    reportData("js_error", {
      message: message,
      source: source,
      lineno: lineno,
      colno: colno,
      stack: error ? error.stack : "N/A",
    });
    // 返回 true 表示错误已处理,阻止浏览器默认的错误提示
    return false;
  };

  // 2. 捕获资源加载错误 (error event on window)
  // 注意:第三个参数 true 表示在捕获阶段捕获事件
  window.addEventListener(
    "error",
    (event) => {
      const target = event.target;
      if (
        target &&
        (target.tagName === "IMG" ||
          target.tagName === "SCRIPT" ||
          target.tagName === "LINK")
      ) {
        reportData("resource_error", {
          tagName: target.tagName,
          src: target.src || target.href,
          outerHTML: target.outerHTML,
          message: `Resource failed to load: ${target.src || target.href}`,
        });
      }
    },
    true
  ); // 注意这里的 true,表示在捕获阶段监听

  // 3. 捕获 Promise 错误 (unhandledrejection)
  window.addEventListener("unhandledrejection", (event) => {
    reportData("promise_error", {
      reason: event.reason ? event.reason.message || event.reason : "N/A",
      stack: event.reason && event.reason.stack ? event.reason.stack : "N/A",
    });
    // 阻止浏览器默认的 Promise 错误提示
    event.preventDefault();
  });
}

// 调用函数启动监控
setupErrorMonitoring();

代码讲解:

  • window.onerror: 用于捕获全局的 JavaScript 运行时错误。
    • message: 错误信息。
    • source: 发生错误的脚本 URL。
    • lineno, colno: 错误发生的行号和列号。
    • error: 错误对象,包含 stack 属性,提供调用栈信息。
  • window.addEventListener('error', ..., true): 用于捕获资源加载错误(如图片、脚本、样式表加载失败)。true 表示在捕获阶段监听,这样可以捕获到冒泡阶段无法捕获的错误。
  • window.addEventListener('unhandledrejection', ...): 用于捕获未被 catch 处理的 Promise 拒绝错误。

6. 用户停留时间 (Dwell Time) 监控

用户停留时间用于衡量用户在某个页面上花费的时间。

实现原理:

在页面加载时记录一个时间戳,在页面卸载或离开时计算时间差并上报。

代码示例:

// 用户停留时间 (Dwell Time) 监控
function setupDwellTimeMonitoring() {
  let pageEnterTime = 0;

  // 页面进入时记录时间
  window.addEventListener("pageshow", (event) => {
    // pageshow 事件在页面从 bfcache 恢复时也会触发
    // event.persisted 为 true 表示从 bfcache 恢复
    if (event.persisted) {
      // 如果是从 bfcache 恢复,不重新计算停留时间,或者根据需求处理
      return;
    }
    pageEnterTime = Date.now();
  });

  // 页面离开或卸载时计算并上报停留时间
  window.addEventListener("beforeunload", () => {
    if (pageEnterTime > 0) {
      const dwellTime = Date.now() - pageEnterTime;
      reportData("dwell_time", {
        duration: dwellTime, // 毫秒
        pageTitle: document.title,
      });
      pageEnterTime = 0; // 重置
    }
  });

  // 对于单页应用 (SPA),需要在路由切换时计算停留时间
  // 假设在 SPA 路由切换时会触发一个自定义事件或有统一的路由监听器
  // 这里以 history API 变化为例
  if (window.history && typeof window.history.pushState === "function") {
    const originalPushState = window.history.pushState;
    window.history.pushState = function () {
      if (pageEnterTime > 0) {
        const dwellTime = Date.now() - pageEnterTime;
        reportData("dwell_time", {
          duration: dwellTime, // 毫秒
          pageTitle: document.title, // 上一个页面的标题
          isSPA: true,
        });
      }
      pageEnterTime = Date.now(); // 记录新页面的进入时间
      originalPushState.apply(this, arguments);
    };

    const originalReplaceState = window.history.replaceState;
    window.history.replaceState = function () {
      if (pageEnterTime > 0) {
        const dwellTime = Date.now() - pageEnterTime;
        reportData("dwell_time", {
          duration: dwellTime, // 毫秒
          pageTitle: document.title, // 上一个页面的标题
          isSPA: true,
        });
      }
      pageEnterTime = Date.now(); // 记录新页面的进入时间
      originalReplaceState.apply(this, arguments);
    };

    window.addEventListener("popstate", () => {
      if (pageEnterTime > 0) {
        const dwellTime = Date.now() - pageEnterTime;
        reportData("dwell_time", {
          duration: dwellTime, // 毫秒
          pageTitle: document.title, // 上一个页面的标题
          isSPA: true,
        });
      }
      pageEnterTime = Date.now(); // 记录新页面的进入时间
    });
  }
}

// 调用函数启动监控
setupDwellTimeMonitoring();

代码讲解:

  • pageEnterTime: 记录用户进入页面的时间戳。
  • window.addEventListener('pageshow', ...): 监听页面显示事件,用于记录进入时间。event.persisted 用于判断是否从浏览器缓存(bfcache)中恢复。
  • window.addEventListener('beforeunload', ...): 监听页面卸载事件,计算并上报停留时间。sendBeacon 在此时非常有用。
  • 对于 SPA,同样需要劫持 history API 或监听路由变化事件来在每次路由切换时计算并上报前一个页面的停留时间,并重置当前页面的进入时间。

7. 表单交互 (Form Interaction) 监控

表单交互监控可以帮助我们了解用户在表单中的行为,例如填写了哪些字段、是否提交成功、耗时等。

实现原理:

监听表单的 submit 事件,以及表单字段的 changeblur 事件。

代码示例:

// 表单交互 (Form Interaction) 监控
function setupFormMonitoring() {
  document.querySelectorAll("form[data-track-form-id]").forEach((form) => {
    const formId = form.dataset.trackFormId;
    const formStartTime = Date.now(); // 记录表单开始填写时间

    // 监听表单提交事件
    form.addEventListener("submit", (event) => {
      const formEndTime = Date.now();
      const duration = formEndTime - formStartTime;
      const formData = {};

      // 收集表单数据 (注意:敏感信息不要上报)
      new FormData(form).forEach((value, key) => {
        // 过滤掉密码等敏感信息
        if (!key.toLowerCase().includes("password")) {
          formData[key] = value;
        }
      });

      reportData("form_submit", {
        formId: formId,
        duration: duration,
        // formData: formData // 根据需求决定是否上报表单内容
        // 可以只上报表单字段的名称,而不上报具体值
        fields: Object.keys(formData),
      });
    });

    // 监听表单字段的 change/blur 事件(可选,用于更细粒度的监控)
    form.querySelectorAll("input, select, textarea").forEach((field) => {
      if (field.type === "password") return; // 敏感信息不监控
      field.addEventListener("change", (event) => {
        reportData("form_field_change", {
          formId: formId,
          fieldId: field.id || field.name,
          fieldType: field.type,
          // value: field.value // 谨慎上报具体值
          changed: true,
        });
      });
      field.addEventListener("focus", (event) => {
        reportData("form_field_focus", {
          formId: formId,
          fieldId: field.id || field.name,
          fieldType: field.type,
        });
      });
    });
  });
}

// 调用函数启动监控
setupFormMonitoring();

HTML 示例:

<form id="loginForm" data-track-form-id="user_login_form">
  <input type="text" name="username" placeholder="用户名" />
  <input type="password" name="password" placeholder="密码" />
  <button type="submit">登录</button>
</form>

代码讲解:

  • document.querySelectorAll('form[data-track-form-id]'): 查找所有带有 data-track-form-id 属性的表单。
  • form.addEventListener('submit', ...): 监听表单提交事件,计算填写耗时,并收集表单字段信息。
  • new FormData(form): 方便地获取表单数据。注意: 务必过滤掉敏感信息(如密码、银行卡号等),避免数据泄露。通常只上报字段名或字段是否被修改。
  • field.addEventListener('change', ...) / field.addEventListener('focus', ...): 可选的细粒度监控,用于了解用户与每个表单字段的交互情况。

8. 滚动行为 (Scroll Behavior) 监控

滚动行为监控可以帮助我们了解用户是否滚动到了页面的底部,或者某个关键区域是否被用户看到。

实现原理:

监听 scroll 事件,计算滚动百分比或结合 Intersection Observer 监控特定区域的可见性。

代码示例:

// 滚动行为 (Scroll Behavior) 监控
function setupScrollMonitoring() {
  let lastReportedScrollDepth = 0; // 上次上报的滚动深度百分比

  const reportScrollDepth = () => {
    const docHeight = document.documentElement.scrollHeight;
    const viewportHeight = window.innerHeight;
    const scrollTop = window.scrollY || document.documentElement.scrollTop;

    // 计算滚动百分比
    // 确保 docHeight - viewportHeight 不为 0,避免除以零
    const scrollPercentage =
      docHeight - viewportHeight > 0
        ? Math.min(
            100,
            Math.round((scrollTop / (docHeight - viewportHeight)) * 100)
          )
        : 100; // 如果页面内容不足一屏,则视为 100% 滚动

    // 每当滚动深度达到新的 25% 增量时上报 (25%, 50%, 75%, 100%)
    if (scrollPercentage >= 25 && lastReportedScrollDepth < 25) {
      reportData("scroll_depth", { depth: 25 });
      lastReportedScrollDepth = 25;
    }
    if (scrollPercentage >= 50 && lastReportedScrollDepth < 50) {
      reportData("scroll_depth", { depth: 50 });
      lastReportedScrollDepth = 50;
    }
    if (scrollPercentage >= 75 && lastReportedScrollDepth < 75) {
      reportData("scroll_depth", { depth: 75 });
      lastReportedScrollDepth = 75;
    }
    if (scrollPercentage >= 90 && lastReportedScrollDepth < 90) {
      // 接近底部
      reportData("scroll_depth", { depth: 90 });
      lastReportedScrollDepth = 90;
    }
    if (scrollPercentage >= 99 && lastReportedScrollDepth < 99) {
      // 几乎到底
      reportData("scroll_depth", { depth: 100 });
      lastReportedScrollDepth = 100;
    }
  };

  // 使用节流 (throttle) 优化滚动事件的性能
  let throttleTimer = null;
  window.addEventListener("scroll", () => {
    if (throttleTimer) return;
    throttleTimer = setTimeout(() => {
      reportScrollDepth();
      throttleTimer = null;
    }, 200); // 每 200 毫秒检查一次
  });

  // 页面加载时也检查一次,以防页面本身就很短
  window.addEventListener("load", reportScrollDepth);
}

// 调用函数启动监控
setupScrollMonitoring();

代码讲解:

  • document.documentElement.scrollHeight: 整个文档的高度。
  • window.innerHeight: 浏览器视窗的高度。
  • window.scrollY / document.documentElement.scrollTop: 页面垂直滚动的距离。
  • scrollPercentage: 计算当前滚动到的百分比。
  • 节流 (Throttle) : 滚动事件触发非常频繁,直接监听会造成性能问题。使用节流函数可以限制事件处理函数的执行频率。
  • lastReportedScrollDepth: 确保只在达到新的深度阈值时才上报,避免重复上报相同深度的事件。

总结与注意事项

  1. 数据上报策略:

    • navigator.sendBeacon: 推荐用于页面卸载时的数据上报(如停留时间、最终 CLS),因为它不会阻塞页面卸载,且能保证数据发送成功。
    • fetch + keepalive: 也是页面卸载时的一个可靠选择,兼容性比 sendBeacon 稍差。
    • fetch / XMLHttpRequest: 对于非关键、实时性要求不高的事件,可以直接使用。
    • 图片打点 (Image Beacon) : 最古老的方式,通过创建一个 <img> 标签并设置其 src 为上报接口,利用浏览器对图片的请求来发送数据。简单但无法获取响应。
  2. 数据批处理 (Batching):

    对于高频事件(如滚动、鼠标移动),不应每次都立即上报。可以将数据缓存在一个数组中,达到一定数量或每隔一段时间(如 5-10 秒)进行批量上报,减少网络请求次数。

  3. 性能优化:

    • 事件节流 (Throttle) / 防抖 (Debounce) : 对于高频触发的事件(如 scroll, resize, mousemove),务必使用节流或防抖来限制事件处理函数的执行频率,避免性能问题。
    • 异步执行: 将数据收集和上报逻辑放在异步任务中(如 setTimeout(..., 0)),避免阻塞主线程。
    • 懒加载: 对于曝光监控,只在元素进入可视区域时才进行观察和上报。
  4. 隐私保护:

    • 敏感信息过滤: 绝对不要收集和上报用户的敏感信息,如密码、身份证号、银行卡号等。
    • 数据脱敏: 如果需要上报用户输入内容,务必进行脱敏处理。
    • 遵守法规: 遵循 GDPR、CCPA 等数据隐私法规。
  5. 单页应用 (SPA) 适配:

    SPA 的页面切换不触发传统的 loadunload 事件。需要监听路由变化(如 history.pushState, popstate 或框架的路由事件)来模拟页面 PV、停留时间等。

  6. 错误处理与降级:

    在代码中加入对浏览器 API 的兼容性检查(如 if ('IntersectionObserver' in window)),并提供降级方案或警告。

  7. 数据结构设计:

    设计合理的数据结构,包含事件类型、时间戳、页面 URL、用户 ID(如果可用且允许)、事件详情等通用字段,方便后端分析。

通过上述代码和讲解,你可以构建一个相对完善的前端用户行为监控系统。实际项目中,你可能还会用到一些成熟的监控 SDK(如 Google Analytics, Matomo, Sentry, 或者自研的 SDK),它们通常会封装这些底层逻辑并提供更强大的功能。