# 前端监控

# 介绍

为什么要进行前端监控:

  • 页面的访问行为,PV、UV、IP、PV 点击率、UV 点击率、停留时长
  • 用户的操作行为,模块曝光、
  • 模块点击
  • 页面的性能,首屏渲染时间、API 请求时间
  • 异常的监控,JS Error、API 异常、业务异常
  • 业务的监控,成交金额、每日消息数

常见的应用场景:

  • 流量分析

常见的前端监控平台:

  • 百度统计
  • 阿里云 ARMS
  • 友盟

前端监控的数据有什么作用

  • 流量数据监控,如:PV、UV、点击率、页面停留时长等
  • 自定义事件监控,如:曝光事件、滑动事件、请求事件等
  • 交易指标监控,如:成交额、转化率等
  • 其他综合分析,如:用户画像分析、流量漏斗、销量预测等

前端监控的三个阶段

  • 初阶:使用第三方平台,百度、友盟、阿里云 ARMS 等
  • 中阶:自研前端监控库,缺乏完整的监控体系
  • 高阶:自研前端监控平台,具备完整的监控体系

# 百度统计接入

流量分析免费,行为分析需要付费。

  1. 官网创建网站,设置域名和首页
  2. 保证域名公共可读
  3. 复制统计代码,添加到要跟踪的网页中(加入到 head 标签之前)

阿里云 ARM 和友盟分析方案较全,接入方案大致相同,但是当数据量较大时,有一定的成本,所以考虑自建前端统计系统。

什么时候需要考虑自建前端监控系统?

  1. 不仅仅需要流量分析,还需要做行为分析
  2. 自建成本小于或等于平台付费
  3. 希望网站监控数据能存到自己数据库中,并且数据隐私化。

# 监控平台架构

前端监控平台的分层:

  • 前端监控 JSSDK

    • 采集
    • 上报
      • 默认上报:页面 PV、性能等
      • 手动上报:页面操作行为
  • 前端监控 API 和大数据仓库

    • 接收上报的数据
    • 数据仓库:MaxCompute
      • 数据查询
      • 数据存储
  • 前端监控数据可视化

    • 日志大数据清洗
    • 大数据回流 RDS(非结构化数据 => 结构化数据)

监控平台架构说明:

监控平台架构

# JS 库的开发

# JS SDK

monitor.js:

function collect() {
  console.log("collect");
}
function upload() {
  console.log("upload");
}
window.testMonitor = {
  collect,
  upload,
};

上传到服务器,在需要监控的页面引入脚本。

一、直接引入脚本:

直接使用 script 标签引入在线地址。

二、异步加载(确保脚本加载完成后再使用 api):

<script>
  (function()
      const script = document.createElement('script');
      script.src = 'https://imooc.youbaobao.xyz/imooc-cli-monitor.js'
      const body = document.body;
      body.insertBefore(script,body.firstChild);
      script.onload = function(){
      var event = new CustomEvent('onMonitorScriptLoad');
      window.dispatchEvent(event);
  )()
</script>
<script>
      window.addEventListener('onMonitorScriptLoad',function(){
      window.testMonitor.collect();
      window.testMonitor.upload();
  }):
</script>

# PV 埋点

一、分包便于代码书写和维护

假设将项目分为:index.js、collect.js 和 upload.js 三个 js 文件,分别用于整合、收集和上报。

二、设置页面基本信息

在 meta 标签中设置变量,假设为:test-app-id,在 body 标签中设置 test-page-id,此变量用于区分不同的站点。

三、collect.js

import { upload } from "./upload";
// 自定义一些钩子函数
let beforeCreateParams;
let beforeUpload;
let afterUpload;
let onError = (err) => {
  console.error(err);
};
export function collect() {
  console.log("收集开始...");
}
// 采集信息
function collection(customData, eventType) {
  let appId, pageId, timeStamp, ua;
  beforeCreateParams && beforeCreateParams();
  const metaList = document.getElementsByTagName("meta");
  for (let i = 0; i < metaList.length; i++) {
    const meta = metaList[i];
    console.log(meta.getAttribute("test-app-id"));
    if (meta.getAttribute("test-app-id")) {
      appId = meta.getAttribute("test-app-id");
    }
  }
  const body = document.body;
  pageId = body.getAttribute("test-page-id");
  if (!appId || !pageId) return;
  timeStamp = new Date().getTime();
  ua = window.navigator.userAgent;
  console.log(appId, pageId, timeStamp, ua);
  let data = `appId=${appId}&pageId=${pageId}&timeStamp=${timeStamp}&ua=${ua}`;
  if (beforeUpload) {
    data = beforeUpload(data); // 允许定制数据
  }
  // 日志上报
  //upload ({appId,pageId,timeStamp,ua})  不常用
  let url, uploadData;
  try {
    data = { ...customData, ...data };
    const ret = upload(data, { eventType });
    url = ret.url;
    uploadData = ret.data;
  } catch (e) {
    onError(e);
  } finally {
    afterUpload && afterUpload(url, uploadData);
  }
}
// 发送 PV 日志
export function sendPV() {
  collection({}, "PV");
}
// 上报曝光埋点
export function sendExp(data = {}) {
  collection(data, "EXP");
}
// 注册钩子函数
export function registerBeforeCreateParams(fn) {
  beforeCreateParams = fn;
}
export function registerBeforeUpload(fn) {
  beforeUpload = fn;
}
export function registerAfterUpload(fn) {
  afterUpload = fn;
}
export function registerOnError(fn) {
  onError = fn;
}
export default {};

四、upload.js

export function upload(data) {
  const img = new Image(); // 利用 image 标签跨域特性
  const { eventType = "PV" } = options;
  const params = encodeURIComponent(data) + "&eventType=" + eventType;
  const src = "http://dmqtest.com?data=" + params;
  console.log(params, src, eventType);
  img.src = src;
  img = null; // 注意内存释放
  return {
    url: src,
    data: {
      params,
    },
  };
}
export default upload;

五、index.js

import { sendPV , registerBeforeCreateParams,registerBeforeUpload,registerAfterUpload } from './collect';
import { upload } from './upload';
window.testMonitor = {
	upload,
	sendPV,
	registerBeforeCreateParams,
	registerBeforeUpload,
	registerAfterUpload,
	registerOnError
}

六、index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" test-app-id="app123456" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <script src="https://dmqtest.com/index.js"></script>
  <body test-page-id="page123456">
    <script>
      window.onload = function () {
        window.testMonitor.registerBeforeCreateParams(() => {
          console.log("创建之前");
        });
        window.testMonitor.registerBrforeUpload((params) => {
          return params + "&custom=1"; // 添加自定义数据
        });
        window.testMonitor.registerAfterUpload((url, data) => {});
        window.testMonitor.sendPV();
      };
    </script>
  </body>
</html>

# 曝光埋点

曝光埋点记录元素由不可变到可变的过程,需要浏览器 IntersectionObserver 这个 API 的支持。

浏览器 5 种 Observer:

  • MutationObserver(用于监听 DOM 树的变化,一般为属性、子节点的增删改)
  • IntersectionObserver(用于监听一个元素和可视区域相交部分的比列,然后在可视比列到达某一阈值的时候触发回调)
  • PerformanceObserver(用于检测性能度量事件,在浏览器的性能事件轴记录下一个新的 performance entries 的时候将会被通知)
  • ResizeObserver(用于监听 DOM 的变化,一般为节点的出现和隐藏,节点大小的变化)
  • ReportingObserver(用于监听过时的 api、浏览器的一些干预行为的预告)

IntersectionObserver:

方法:

  • observe:开始监听一个目标元素

    语法:IntersectionObserver.disconnect ();

  • disconnect:停止监听

    语法:IntersectionObserver.observe (targetElement);

  • takeRecords: 返回所有观察目标的 IntersectionObserverEntry 对象数组。

    语法:intersectionObserverEntries = intersectionObserver.takeRecords ();

  • unobserve: 使 IntersectionObserver 停止监听特定目标元素

    语法:IntersectionObserver.unobserve (targetElement);

配置项:

  • targetElement:目标 DOM
  • root:指定根目录,也就就是当目标元素显示在这个元素中时会触发监控回调
  • rootMargin:类似于 css 的 margin,设定 root 元素的边框区域。
  • threhold:阈值,决定了什么时候触发回调函数。

返回参数:

  • tIme: 可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • rootBounds: 是在根元素矩形区域的信息
  • intersectionRatio: 目标元素的可见比例
  • intersectionRect: 目标元素与根元素交叉区域的信息
  • isIntersecting: 判断元素是否符合 options 中的可见条件
  • boundingClientRect: 目标元素的矩形区域的信息
  • target: 被观察的目标元素

upload.js 文件中添加:

//.... 省略
// 上报曝光埋点
export function sendExp(data = {}) {
  collection(data, "EXP");
}
// 最后在 index.js 中暴露出去。

collect.js 文件中添加:

//.... 省略
export function collectAppear() {
  const appearEvent = new CustomEvent("onAppear");
  const disappearEvent = new CustomEvent("onDisappear");
  let ob;
  if (window.testMonitorObserver) {
    ob = window.testMonitorObserver;
  } else {
    ob = new IntersectionObserver(function (e) {
      e.forEach((item) => {
        if (item.intersectionRatio > 0) {
          console.log(item.target.className + "appear");
          item.target.dispatchEvent(appearEvent);
        } else {
          console.log(item.target.className + "disappear");
          item.target.dispatchEvent(disappearEvent);
        }
      });
    });
  }
  let obList = [];
  const appear = document.querySelectorAll("[appear]");
  for (let i = 0; i < appear.length; i++) {
    if (obList.includes(appear[i])) {
      ob.observe(appear[i]);
      obList.push(appear[i]);
    }
  }
  window.testMonitorObserver = ob; // 存起来防止重复
  window.monitorObserverList = obList;
}

index.js 中使用:

//... 省略
//import 引入 collectAppear 函数。
window.onload = function () {
  collectAppear();
};

# 点击埋点

collect.js 中添加:

//... 省略
// 上报点击埋点
export function sendClick(data = {}) {
  collection(data, "CLICK");
}
// 暴露出去,index.js 导入该方法并暴露出去。

# 自定义埋点

collect.js 中添加:

自定义埋点行为就直接在内部添加 CUSTOM 对应的处理逻辑,可以使用其他的 Observer 实现更多功能。

//... 省略
// 上报自定义埋点
export function sendCustom(data = {}) {
  collection(data, "CUSTOM");
}
// 暴露出去,index.js 导入该方法并暴露出去。

# 大数据平台开发

MaxCompute 阿里云原生大数据计算服务

MaxCompute 是基于数据分析场景的企业级 SaaS 模式云数据仓库,以 Serverless 架构提供快速、全托管的在线数据仓库服务,消除了传统数据平台在资源扩展性和弹性方面的限制,最小化用户运维投入。

使用:

  1. 使用 MaxCompute 创建数据库,在数据开发页面创建表进行记录前端监控数据

  2. 使用 py 脚本对接(暂时没有 js 包),需要安装 python 和 pip(包管理工具)

  3. 安装 pyodps: pip install pyodps

  4. connect.py 文件中写入示例:

    from odps import ODPS;
    odps = ODPS('LTAI5tBDj3HajwRVhc6me5KR','DJqWAI1IWUBZnZGE#FKDSFJDEJLet','test_monitor',endpoint='https://service-cn-hangzhou.odps.aliyun-inc.com')
    result = odps.executexecute_sql('select * from test_monitor where datetime="20240325"')
    with result.open_reader() as reader:
        for record in reader:
            print(record[0],record[1])
    # 打印表名
    for table in odps.list_tables():
        print(table)
    data = [
        ['appid123','pageid123','123456','ua123','http://www.baidu.com','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','20240325','202403']
    ]
    # 写入数据
    odps.write_table('test_table', data)
    # .... 等等后续操作

前端监控平台可视化架构图:

前端监控流程架构

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

dmq 微信支付

微信支付

dmq 支付宝

支付宝