日志埋点 SDK
背景
- 其他部门的方案不合适、特殊参数上传、耗时埋点等
- 流量监测(按时间空间维度分析,留存分析,转化分析)
- 构建行为路径, 获取用户的整条行为链路,实现用户分群、人群洞察、行为细查等,构建用户画像
- 通过对埋点数据的处理、分析、建模,判断产品的效果和未来走向
- 热力分析,帮助判断访客热衷的区域,评估网页设计是否合理等
内容总结
- pv【PageView】上报(包括history上报、hash上报)、 hashChange
- uv【UserView】上报(客户端 IP 字段、userId)
- dom 事件上报、click、data-xxx、keydown
- js 报错上报(包括常规错误上报、Promise报错上报)
难点
保证在文档卸载期间发送数据一直是一个困难 一个理论上的统计代码——在卸载事件处理器中尝试通过一个同步的 XMLHttpRequest 向服务器发送数据,导致了页面卸载被延迟。
使用 sendBeacon()
方法会使用户代理在有机会时异步地向服务器发送数据,同时不会延迟页面的卸载或影响下一导航的载入性能,这意味着:
- 数据发送是可靠的
- 数据异步传输
- 不影响下一导航的载入
发送数据的时机 网站通常希望在用户完成页面浏览后向服务器发送分析或诊断数据,最可靠的方法是在 visibilitychange
事件发生时发送数据:
document.addEventListener("visibilitychange", function logData() {
if (document.visibilityState === "hidden") {
navigator.sendBeacon("/log", analyticsData);
}
});
批量上报和延迟上报
- 从数量维度上,将单条上报聚合成多条上报,大大减少了数量的请求
- 从时间维度上,先本地化存储数据,将上报请求延后,优先处理业务逻辑请求,在程序空闲时进行上报
- 一般采用组合方式,根据数据量,选择 Image 或者 Beacon 的方式,
- 若检测不支持 Beacon, 在大数据量时回退到传统的 XHR 请求
成果
- 成果 10个 项目接入
具体细节
PV
History API
可以让我们更精细地控制页面的导航和状态
history.pushState(state, title, url)
向浏览器的会话历史栈增加了一个条目、异步、history.replaceState(stateObj, title[, url])
修改当前历史记录实体popstate
事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JS 中调用 history.back() 方法)
dom 事件上报
private domReport(targetKey: string) {
mouseEventList.forEach((ev) => {
window.addEventListener(ev, (e) => {
// console.log(e.target);
const target = e.target as HTMLElement;
if (target.getAttribute("target-key")) {
console.log("监听到带有target-key属性元素的dom事件");
this.reportTracker({
event: ev,
targetKey,
});
}
console.log("未监听到带有target-key属性元素的dom事件");
// let activeElement = document.activeElement;
// if (activeElement?.getAttribute("target-key")) {
// console.log("监听到dom事件");
// }
});
});
}
错误上报
// 常规报错上报
private errorEvent() {
window.addEventListener("error", (event) => {
console.log(event.message, "常规报错");
this.reportTracker({
event: "error",
targetKey: "message",
message: event.message,
});
});
}
// Promise报错上报
private promiseReject() {
window.addEventListener("unhandledrejection", (event) => {
event.promise.catch((error) => {
console.log(error, "promise报错");
this.reportTracker({
event: "unhandledrejection",
targetKey: "message",
reason: error,
});
});
});
}
// vue 报错上报
app.config.errorHandler = (err) => {
navigator.sendBeacon(url, {error: error.message, text: 'vue运行异常' })
}
日志上报
- gif(跨域、远离dom,不阻塞、体积小,节约流量)1×1 透明像素的 GIF
navigator.sendBeacon(url, data)
用于将数据以非阻塞(后台)方式发送到服务器、即使页面卸载(关闭)也会发送请求- 当用户代理成功把数据加入传输队列时,
sendBeacon()
方法将会返回true
,否则返回false
- 相较于
img
标签,使用navigator.sendBeacon
会更规范,数据传输上可传输资源类型会更多。 sendBeacon
是异步的,不会影响当前页到下一个页面的跳转速度,且不受同域限制。- 这个方法还是异步发出请求,但是请求与当前页面脱离关联,作为浏览器的任务,因此可以保证会把数据发出去,不拖延卸载流程。
- 当用户代理成功把数据加入传输队列时,
性能日志计算公式
PV:是用 SDK 采集的性能日志(Performace-OL)进行计算
UV:使用客户端采集的性能日志(Performace-OL)中的
客户端 IP 字段
,进行去重计算first-paint(首屏有效绘制时间)
:直接获取的 window.performance.getEntries().first-paint
以下字段均来自于 performance.timing 下:
ttfb(发起文档请求到首字节返回的时间)
: responseStart - requestStart- responseStart:当浏览器从服务器,缓存或本地资源接收到响应的第一个字节后, 立即返回时间戳
- requestStart:开始请求文档时间
FirstByte(从 DNS 解析到首字节返回的时间)
: responseStart - domainLookupStart- responseStart:当浏览器从服务器,缓存或本地资源接收到响应的第一个字节后,responseStart 立即返回时间戳
- domainLookupStart:DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
DNS(DNS 解析时间)
:domainLookupEnd - domainLookupStart- domainLookupEnd:DNS 域名查询完成的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
- domainLookupStart:DNS 域名查询开始的时间,如果使用了本地缓存(即无 DNS 查询)或持久连接,则与 fetchStart 值相等
TCP(TCP 链接建立时间)
:connectEnd - connectStart- connectStart 和 connectEnd:分别代表TCP建立连接和连接成功的时间节点。如果浏览器没有进行TCP连接(比如使用持久化连接 webscoket、使用缓存或本地资源),则两者都等于 domainLookupEnd
- domainLookupEnd:在浏览器完成资源的域名查找所需时间,如果有缓存则表示缓存查找时间
SSL(建立 ssl 链接所需要的时间)
:connectEnd - secureConnectionStart- connectEnd:TCP 连接成功的时间节点
- secureConnectionStart: HTTPS 连接开始的时间,如果不是安全连接,则值为 0
TTL(可交互时间)
:domInteractive - fetchStart- domInteractive:完成解析 DOM 树的时间,Document.readyState 变为 interactive,并将抛出 readystatechange 相关事件
- fetchStart:表示浏览器即将开始获取资源之前的时间戳。
Ready(基本 DOM 加载完毕的时间)
: domContentLoadedEventEnd - fetchStart- domContentLoadedEventEnd:DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕),文档的DOMContentLoaded 事件的结束时间
- fetchStart:表示浏览器即将开始获取资源之前的时间戳。
Load(所有资源加载完毕的时间)
: loadEventStart - fetchStart- loadEventStart:DOM 解析完成后,网页内资源加载完成的时间(如 JS 脚本加载执行完毕),文档的DOMContentLoaded 事件的结束时间
- fetchStart:表示浏览器即将开始获取资源之前的时间戳。
其他一些指标:
LCP(Largest Contentful Paint):最大内容绘制
,用于记录视窗内最大的元素绘制的时间。FID(First Input Delay)
:首次输入延迟,FID指的是用户首次与产品进行交互时,我们产品可以在多长时间给出反馈。CLS(Cumulative Layout Shift)
:累计位移偏移,记录了页面上非预期的位移波动。TBT(Total Blocking Time)
:阻塞总时间,记录在 FCP 到 TTI 之间所有长任务的阻塞时间总和。
通过谷歌官方库 web-vitals
可以获取到这些指标
埋点功能的意义
数据采集
:埋点是数据采集领域(尤其是用户行为数据采集领域)的术语,它针对特定用户行为或事件进行捕获、处理和发送的相关技术及其实施过程。通过埋点,可以收集到用户在应用中的所有行为数据,例如页面浏览、按钮点击、表单提交等。数据分析
:采集的数据可以帮助业务人员分析网站或者App的使用情况、用户行为习惯等,是后续建立用户画像、用户行为路径等数据产品的基础。通过数据分析,企业可以更好地了解用户需求,优化产品和服务。改进决策
:通过对埋点数据的分析,企业可以了解用户的真实需求和行为习惯,从而做出更符合市场和用户需求的决策,提高产品和服务的质量和竞争力。优化运营
:通过埋点数据,企业可以了解用户的兴趣和行为,从而更好地定位目标用户群体,优化运营策略,提高运营效率和收益。预测趋势
:通过对埋点数据的分析,企业可以预测市场和用户的未来趋势,从而提前做好准备,把握市场机遇,赢得竞争优势。
SDK 打包
主流模块
CommonJS
规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD和CMD解决方案AMD
规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅。CMD
规范整合了CommonJS和AMD规范的特点, CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM打包,模块的加载逻辑偏重UMD
是AMD和CommonJS两者的结合,这个模式中加入了当前存在哪种规范的判断,所以能够“通用”,它兼容了AMD和CommonJS,同时还支持老式的“全局”变量规范ESM
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
rollup 和 webpack 的区别
- webpack 由于年代相对久远,在 commonjs 后且 esMoudles 之前,所以通过 webpack 通过自己来实现 commonjs 等语法,rollup 则可以通过配置打包成想要的语法,比如 esm
- 所以说 rollup 很适合打包成 库,而 webpack 比较适合用来做来打包应用
- 由于 rollup 不能够直接读取 node_modules 中的依赖项,需要引入加载 npm 模块的插件:rollup-plugin-node-resolve
- 由于 rollup 默认只支持 esm 模块打包,所以需要引入插件来支持 cjs 模块:rollup-plugin-commonjs
- 由于 rollup 通过可以 esm 模块开发和打包,所以支持 tree-shaking 模式
- vite 就是 rollup 开发而来的