# 前端监控
# 介绍
为什么要进行前端监控:
- 页面的访问行为,PV、UV、IP、PV 点击率、UV 点击率、停留时长
- 用户的操作行为,模块曝光、
- 模块点击
- 页面的性能,首屏渲染时间、API 请求时间
- 异常的监控,JS Error、API 异常、业务异常
- 业务的监控,成交金额、每日消息数
常见的应用场景:
- 流量分析
常见的前端监控平台:
- 百度统计
- 阿里云 ARMS
- 友盟
前端监控的数据有什么作用
- 流量数据监控,如:PV、UV、点击率、页面停留时长等
- 自定义事件监控,如:曝光事件、滑动事件、请求事件等
- 交易指标监控,如:成交额、转化率等
- 其他综合分析,如:用户画像分析、流量漏斗、销量预测等
前端监控的三个阶段
- 初阶:使用第三方平台,百度、友盟、阿里云 ARMS 等
- 中阶:自研前端监控库,缺乏完整的监控体系
- 高阶:自研前端监控平台,具备完整的监控体系
# 百度统计接入
流量分析免费,行为分析需要付费。
- 官网创建网站,设置域名和首页
- 保证域名公共可读
- 复制统计代码,添加到要跟踪的网页中(加入到 head 标签之前)
阿里云 ARM 和友盟分析方案较全,接入方案大致相同,但是当数据量较大时,有一定的成本,所以考虑自建前端统计系统。
什么时候需要考虑自建前端监控系统?
- 不仅仅需要流量分析,还需要做行为分析
- 自建成本小于或等于平台付费
- 希望网站监控数据能存到自己数据库中,并且数据隐私化。
# 监控平台架构
前端监控平台的分层:
前端监控 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 架构提供快速、全托管的在线数据仓库服务,消除了传统数据平台在资源扩展性和弹性方面的限制,最小化用户运维投入。
使用:
使用 MaxCompute 创建数据库,在数据开发页面创建表进行记录前端监控数据
使用 py 脚本对接(暂时没有 js 包),需要安装 python 和 pip(包管理工具)
安装 pyodps:
pip install pyodps
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)
# .... 等等后续操作
前端监控平台可视化架构图: