代码埋点
代码埋点是最灵活,同时也是最耗时的一种方式。
一般会封装自己的一套埋点上报的npm包, 提供给各业务线使用。
一般我们需要上报什么信息呢?
- 埋点的标识信息, 比如eventId, eventType
- 业务自定义的信息, 比如电商, 点击一个按钮, 我们要上报用户点击的是哪个商品
- 通用的设备信息, 比如用户的userId, useragent, deviceId, timestamp, locationUrl等等
一般怎么上报?
- 实时上报, 业务方调用发送埋点的api后, 立即发出上报请求
- 延时上报, sdk内部收集业务方要上报的信息, 在浏览器空闲时间或者页面卸载前统一上报,上报失败会做补偿措施。
实现
1// async-task-queue.ts2import { debounce } from 'lodash';34interface RequiredData {5 timestamp: number | string;6}78class TaskQueueStorableHelper<T extends RequiredData = any> {9 public static getInstance<T extends RequiredData = any>() {10 if (!this.instance) {11 this.instance = new TaskQueueStorableHelper<T>();12 }13 return this.instance;14 }1516 private static instance: TaskQueueStorableHelper | null = null;1718 protected store: any = null;19 private STORAGE_KEY = 'my_store';2021 constructor() {22 const localStorageValue = localStorage.getItem(this.STORAGE_KEY);23 if (localStorageValue) {24 this.store = JSON.parse(localStorageValue);25 }26 }2728 get queueData() {29 return this.store?.queueData || [];30 }3132 set queueData(queueData: T[]) {33 this.store = {34 ...this.store,35 queueData: queueData.sort((a, b) => Number(a.timestamp) - Number(b.timestamp)),36 };37 localStorage.setItem(this.STORAGE_KEY, JSON.stringify(this.store));38 }3940}4142export abstract class AsyncTaskQueue<T extends RequiredData = any> {43 private get storableService() {44 return TaskQueueStorableHelper.getInstance<T>();45 }4647 private get queueData() {48 return this.storableService.queueData;49 }5051 private set queueData(value: T[]) {52 this.storableService.queueData = value;53 if (value.length) {54 this.debounceRun();55 }56 }5758 protected debounceRun = debounce(this.run.bind(this), 500);5960 protected abstract consumeTaskQueue(data: T[]): Promise<any>;6162 protected addTask(data: T | T[]) {63 this.queueData = this.queueData.concat(data);64 }656667 private run() {68 const currentDataList = this.queueData;6970 if (currentDataList.length) {71 this.queueData = [];72 this.consumeTaskQueue(currentDataList); // .catch(() => this.addTask(currentDataList))73 }74 }75}
1// track.ts2import axios from "axios";3import { AsyncTaskQueue } from "./async-task-queue";4import queryString from "query-string";5import { v4 as uuid } from "uuid";67interface TrackData {8 seqId: number;9 id: string;10 timestamp: number;11}1213interface UserTrackData {14 msg?: string;15}1617export class BaseTrack extends AsyncTaskQueue<TrackData> {18 private seq: number = 0;19 public track(data: UserTrackData) {20 this.addTask({21 id: uuid(),22 seqId: this.seq++,23 timestamp: Date.now(),24 ...data,25 });26 }27 public consumeTaskQueue(data: any) {28 return axios.post(`https://xxx.com`, { data });29 }30}
无埋点
概念
无埋点并不是真正的字面意思,其真实含义其实是,不需要研发去手动埋点。
一般会有一个 sdk 封装好各种逻辑, 然后业务方直接引用即可。
sdk中做的事情一般是监听所有页面事件, 上报所有点击事件以及对应的事件所在的元素,然后通过后台去分析这些数据。
业界有GrowingIO, 神策, 诸葛IO, Heap, Mixpanel等等商业产品
实现
- 监听window元素
1window.addEventListener("click", function(event){2 let e = window.event || event;3 let target = e.srcElement || e.target;4}, false);
- 获取元素唯一标识 xPath
1function getXPath(element) {2 // 如果元素有id属性,直接返回//*[@id="xPath"]3 if (element.id) {4 return '//*[@id=\"' + element.id + '\"]';5 }6 // 向上查找到body,结束查找, 返回结果7 if (element == document.body) {8 return '/html/' + element.tagName.toLowerCase();9 }10 let currentIndex = 1, // 默认第一个元素的索引为111 siblings = element.parentNode.childNodes;121314 for (let sibling of siblings) {15 if (sibling == element) {16 // 确定了当前元素在兄弟节点中的索引后, 向上查找17 return getXPath(element.parentNode) + '/' + element.tagName.toLowerCase() + '[' + (currentIndex) +18 ']';19 } else if (sibling.nodeType == 1 && sibling.tagName == element.tagName) {20 // 继续寻找当前元素在兄弟节点中的索引21 currentIndex++;22 }23 }24};
获取元素的位置
1function getOffset(event) {2 const rect = getBoundingClientRect(event.target);3 if (rect.width == 0 || rect.height == 0) {4 return;5 }6 let doc = document.documentElement || document.body.parentNode;7 const scrollX = doc.scrollLeft;8 const scrollY = doc.scrollTop;9 const pageX = event.pageX || event.clientX + scrollX;10 const pageY = event.pageY || event.clientY + scrollY;1112 const data = {13 offsetX: ((pageX - rect.left - scrollX) / rect.width).toFixed(4),14 offsetY: ((pageY - rect.top - scrollY) / rect.height).toFixed(4),15 };1617 return data;18}
上报
1window.addEventListener("click", function(event){2 const e = window.event || event;3 const target = e.srcElement || e.target;4 const xPath = getXPath(target);5 const offsetData = getOffset(event);67 report({ xPath, ...offsetData});8}, false);
性能监控
1// performance.ts2import { VueRouter } from 'vue-router/types/router';3import { BaseTrack } from './track';45export class Performance {6 // TODO 注意上报的单位 现在是毫秒7 public static readonly timing = window.performance && window.performance.timing;89 public static init() {10 if (!this.timing) {11 console.warn('当前浏览器不支持performance API');12 return;13 }1415 window.addEventListener('load', () => {16 new BaseTrack().track(this.getTimings());17 });18 }1920 public static record(router?: VueRouter) {21 const setFPT = () => {22 if (window.performance && window.performance.now) {23 this.customFPT = window.performance.now();24 }25 };26 return {27 created: () => {28 if (router) {29 router.onReady(() => {30 setFPT();31 });32 } else {33 setFPT();34 }35 },36 };37 }383940 public static getTimings(): { [key in string]: number } {41 if (!this.timing) {42 console.warn('当前浏览器不支持performance API');43 return {};44 }4546 return {47 redirect: this.getRedirectTime(),48 dns: this.getDnsTime(),49 tcp: this.getTcpTime(),50 ttfb: this.getTimeOfFirstByte(),51 req: this.getReqTime(),52 ppdt: this.getParsePureDomTime(),53 dclt: this.getDomContentLoadTime(),54 fpt: this.getFirstPaintTime(),55 load: this.getLoadTime(),56 };57 }5859 private static customFPT: number = 0;6061 private static getRedirectTime() {62 // 重定向耗时63 return Performance.timing.redirectEnd - Performance.timing.redirectStart;64 }6566 private static getDnsTime() {67 // dns查询耗时68 return Performance.timing.domainLookupEnd - Performance.timing.domainLookupStart;69 }7071 private static getTcpTime() {72 // tcp连接耗时73 return Performance.timing.connectEnd - Performance.timing.connectStart;74 }7576 private static getTimeOfFirstByte() {77 // 读取页面第一个字节耗时78 return Performance.timing.responseStart - Performance.timing.navigationStart;79 }8081 private static getReqTime() {82 // request请求耗时83 return Performance.timing.responseEnd - Performance.timing.responseStart;84 }8586 private static getParsePureDomTime() {87 // 解析纯DOM树耗时, 不包含js css等资源的加载和执行88 return Performance.timing.domInteractive - Performance.timing.domLoading;89 }9091 private static getDomContentLoadTime() {92 // 页面资源加载耗时, 包含vue, js css等资源的加载和执行93 return Performance.timing.domComplete - Performance.timing.domInteractive;94 }9596 private static getFirstPaintTime() {97 // first paint time, 首次渲染时间, 即白屏时间98 return Math.round(99 (window.performance.getEntriesByName &&100 window.performance.getEntriesByName('first-paint') &&101 window.performance.getEntriesByName('first-paint')[0] &&102 window.performance.getEntriesByName('first-paint')[0].startTime) ||103 this.customFPT,104 );105 }106107 private static getLoadTime() {108 // 页面load总耗时109 return Performance.timing.loadEventStart - Performance.timing.navigationStart;110 }111112 private static toSeconds(time: number) {}113}