前言

置信不少人因为我的项目中没有接触过数据埋点相干的内容,而没有花工夫去理解它,总感觉这又是一个本人还不能波及的方面,然而数据埋点自身并不难理解,只是很难做得好,本文会从 意识数据埋点 SDK设计前端数据埋点 SDK 两个外围方面来开展,聊聊前端数据埋点的那些事。

意识数据埋点 SDK

SDK 全称是 Software Development Kit 即 软件开发工具包,个别都是一些软件工程师为特定的软件包、软件框架、硬件平台、操作系统等建设应用软件时的开发工具的汇合。

为什么须要前端数据埋点?

对产品自身而言,咱们须要关注内容包含如下几个方面:

  • 用户在产品里 次要做什么操作、停留多久、拜访几次
  • 用户点击率占比如何,会不会呈现某些功能设计对于用户而言是有效的
  • 用户在外围应用流程上是否顺畅,页面反馈是否失常敌对
  • 可能有哪些潜在的用户的性能须要更新

总的来说,数据埋点 外围是为了 收集数据有了数据就能够随心所欲),只有通过剖析数据,能力更好的评估出整个我的项目的品质和重要性(数据为王),并且可能为产品优化指明方向(数据驱动产品)。

前端数据埋点要思考哪些方面?

数据埋点的外围是数据收集,而与数据相干的内容不外乎如下几个内容:

  • 数据又是基于利用产生的,因为没有利用就不会有相干的数据
  • 利用自身要提供展现、收集、操作内容,而这是基于平台的,比方网站就是基于浏览器平台
  • 有利用、有平台就得有用户,因为利用自身就是为了给用户提供好用的性能去解决某些存在的问题
  • 针对开发者而言,利用就是代码,代码运行的品质也能决定利用的品质,而显式品质体现在谬误或正告上

总结下来,数据埋点其实要思考的就是 用户行为、谬误正告、页面性能 三个外围方面。

用户行为

用户行为就是在网页利用中进行的一系列操作,但用户的操作有很多种,都须要记录下来是不可能的,个别须要记录用户的以下几种行为:

  • 用户浏览页面次数,PV(Page View)

    • 用户每次拜访网站中的一个页面就被记录为 1PV,屡次拜访同一个页面,访问量就会累计
  • 页面浏览用户数,UV(Unique visitor)

    • 通过网络失常拜访页面的使用者,通常一台电脑客户端或一个用户账号为一个访客,个别同一个客户端或用户账号在 24h 内屡次拜访只会被记录为 1UV,计算策略视具体情况而定
  • 用户点击按钮次数

    • 以上两种能够认为是 自动式触发埋点,而点击按钮次数就属于是 互动式触发埋点,便于去理解这个性能按钮的应用状况

    谬误正告

    页面中代码运行产生的谬误,可能会导致用户外围操作流程被中断,为了防止大量用户受到影响,咱们须要获取 生产环境的谬误数据,这样能力便于开发者及时进行修复。

通常来讲代码中的谬误会蕴含以下几大类:

  • 全局谬误,即未被捕捉的谬误
  • 部分谬误,即通过 try...catch、promise.then、promise.catch 等捕捉的谬误
  • 接口申请谬误,即在二次封装申请 API 中进行申请和接管响应时的谬误
  • 组件级谬误,即应用 Vue/React 组件时产生的谬误

页面性能

页面性能其实也是前端性能优化中一个须要思考和优化的点,毕竟如果一个网站老是产生 白屏、交互卡顿、页面资源加载工夫长 等问题,必定是没方法留住用户的,特地是用户的实在环境各不相同,如 Windows x、MACOS、Android、iOS 等,更加须要统计和收集相干数据,便于进行集中优化解决,晋升用户体验。

与页面性能指标相干的内容,在之前的 前端性能优化到底该怎么做(上)— 单刀直入 一文中有提到,这里大抵总结下:

  • 首次绘制(First Paint,FP

    • 在渲染过程确认要渲染以后响应资源后,渲染过程会先创立一个空白页面,通常把创立空白页面的这个工夫点称为 First Paint,简称 FP
    • 所谓的 白屏工夫 其实指的就是创立这个空白页面到浏览器开始渲染非空白内容的工夫,比方页面背景发生变化等
  • 首次内容绘制(First Contentful Paint,FCP

    • 当用户看见一些 "内容" 元素被绘制在页面上的工夫点,和白屏是不一样,它能够是 文本 首次绘制,或 SVG 首次呈现,或 Canvas 首次绘制等,即当页面中绘制了第一个 像素 时,这个工夫点称为 First Content Paint,简称 FCP
  • 首屏工夫 / 最大内容绘制(Largest Contentful Paint, LCP

    • LCP 是一种新的性能度量规范,LCP 侧重于用户体验的性能度量规范,与现有度量规范相比,更容易了解与推理,当首屏内容齐全绘制实现时,这个工夫点称为 Largest Content Paint,简称 LCP
    • 最大内容绘制应在 2.5s 内实现
  • 首次输出提早(First Input Delay, FID

    • FID 测量的是当用户第一次在页面上交互的时候(点击链接点击按钮自定义基于 js 的事件),到浏览器理论开始解决这个事件的工夫
    • 首次输出提早应在 100ms 内实现
  • 累积布局偏移(Cumulative Layout Shift, CLS)

    • CLS 是为了测量 视觉稳定性,以便提供良好的用户体验
    • 累积布局偏移应放弃在 0.1 或更少
  • 首字节达到工夫(Time to First Byte,TTFB

    • 指的是浏览器开始收到服务器响应数据的工夫(后盾解决工夫 + 重定向工夫),是反映服务端响应速度的重要指标
    • TTFB 工夫如果超过 500ms,用户在关上网页的时就会感觉到显著的期待

了解了 为什么要做前端数据埋点 和 前端数据埋点所须要统计数据的方方面面,接下来咱们就须要设计一个本人的 前端数据埋点 SDK 了。

设计前端数据埋点 SDK

这里只咱们思考数据埋点的核心内容,因而不会波及得必定没有那么全面,而一开始也不可能设计得全面,只有保障外围性能,那么在基于外围进行扩大即可。

确定 options 和 data 内容

利用的惟一标识 — options.AppId

数据埋点 SDK 作为一个通用的工具集,是可供多个零碎进行应用的,而这就意味着须要去保障每个利用的唯一性,一般来讲,在初始化 SDK 的时候是须要接入方提供的以后利用的 ID

那这个 ID 从何而来?轻易生成吗?一般来说须要通过如下步骤:

  • 在对应监控零碎上为以后利用生成惟一的 AppId
  • 在对应利用接入 SDK 时作为配置项之一传入

其实还会波及到申请 url 内容,次要用于发送给对应的监控零碎,因而 options 核心内容简略设计如下:

{  appId: '', // 以后利用惟一标识  baseUrl: '', // 数据发送的地址}

数据发送格局 — data

因为须要收集的数据类型蕴含多种,最好可能定义一种比拟通用的数据格式,便于更敌对地进行数据收集。

这里简略定义一下数据格式,大抵如下,格局随需要场景产生差别:

{  appId: '', // 以后利用惟一标识  type: 'action' | 'performance'| 'network' | 'error', // 不同数据类型  pageUrl: '', // 页面地址  apiUrl: '', // 接口地址  userId: '', // 以后用户 id  userName: '', // 以后用户 name  time: '',// 触发记录的工夫  data: {}, // 接口响应后果 | 性能指标 | 谬误对象 | 用户操作相干信息}

确定数据发送形式

如果要问前端埋点最根本要实现的性能是什么,那必然是 数据发送 的能力,否则即使有利用、有用户、有数据也只能保留在本地没法发送给相应的监控零碎,象征就没法进行收集和统计(数据等于白给)。

那么数据发送都有什么形式呢?针对这个问题把 数据发送 翻译成 申请发送 就容易多了,转而问题就变成了 申请发送形式都有哪些?

个别会包含如下几种(包含但不限于):

  • XMLHttpRequest
  • fetch
  • form 表单的 action
  • 基于元素 src 属性的申请

    • img 标签的 src
    • script 标签的 src
    • Navigator.sendBeacon()

这里抉择的是最初一种,因为 Navigator.sendBeacon() 就是专门用于通过 HTTP POST 将统计数据 异步 发送到 Web 服务器上,同时能防止传统技术发送剖析数据的一些问题。

传统技术发送统计数据的一些问题,能够间接通过 传送门 查看,因为文章篇幅无限不在额定解释。

SDK 外围代码

这里咱们只思考极简状况,设计好的 SDK 代码内容比较简单,间接上代码:

let SDK = null // EasyAgentSDK 实例对象const QUEUE = [] // 工作队列cosnt NOOP = (v) => v// 通过 web-vitals 页面性能指标const reportWebVitals = (onPerfEntry) => {  if (onPerfEntry && onPerfEntry instanceof Function) {    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {      getCLS(onPerfEntry) // 布局偏移量      getFID(onPerfEntry) // 首次输出延迟时间      getFCP(onPerfEntry) // 首次内容渲染工夫      getLCP(onPerfEntry) // 首次最大内容渲染工夫      getTTFB(onPerfEntry) // 首个字节达到工夫    })  }}export default class EasyAgentSDK {  appId = ''  baseUrl = ''  timeOnPage = 0  config = {}  onPageShow = null  onPagesHide = null    constructor(options = {}) {    if (SDK) return    SDK = this    this.appId = options.appId    this.baseUrl = options.baseUrl || window.location.origin    this.onPageShow = options.onPageShow || NOOP    this.onPagesHide = options.onPagesHide || NOOP    // 初始化监听页面变动    this.listenPage()  }    // 设置 config  setConfig(congfig){    this.config = congfig  }  // 刷新工作队列  flushQueue() {    Promise.resolve().then(() => {      QUEUE.forEach((fn) => fn())      QUEUE.length = 0;    })  }  // 监听页面变动  listenPage() {    let pageShowTime = 0    window.addEventListener('pageshow', () => {      pageShowTime = performance.now()             // 页面性能指标上报      reportWebVitals((data) => {        this.performanceReport({ data })      })            // 执行 onPageShow      this.onPageShow();    })    window.addEventListener('pagehide', () => {      // 记录用户在页面停留时间      this.timeOnPage = performance.now() - pageShowTime            // 刷新队列前执行 onPageShow      this.onPageShow();      // 刷新工作队列      this.flushQueue()    })  }  // Json 转 FormData  json2FormData(data){    const formData = new FormData()    Object.keys(data).forEach(key => {      formData.append(key, data[key])    });    return formData  }  // 自定义上报类型  report(config) {    QUEUE.push(() => {      const formData = json2FormData({        ...this.config,        ...config,        time: new Date().toLocaleString(),        appId: this.appId,        pageUrl: window.location.href,      });      navigator.sendBeacon(`${this.baseUrl}${config.url || ''}`, formData)    })  }  // 用户行为上报  actionReport(config) {    this.report({      ...config,      type: 'action',    })  }  // 网络情况上报  networkReport(config) {    this.report({      ...config,      type: 'network',    })  }  // 页面性能指标上报  performanceReport(config) {    this.report({      ...config,      type: 'performance',    })  }  // 谬误正告上报  errorReport(config) {    this.report({      ...config,      type: 'error',    })  }}

上报用户行为

统计 PV 和 UV — 主动触发埋点

对于 PVUV 在上述曾经做过介绍了,实质上这两个数据统计都可在一个上报类型为 action 数据发送中取得,次要看监控零碎是依照怎么的规定对数据进行剖析和统计,这里在 SDK 外部监听了页面的 pageshow / pagehide 两个事件:

  • pageshow 中能够上报与 PV / UV 相干的数据 和 页面性能相干的数据

    window.SDK = new EasyAgentSDK({    appId: 'application_id',    baseUrl: '//aegis.example.com/collect',    onPageShow() {        window.SDK.actionReport({            data: {} // 其余必要传递的信息        })    }});window.SDK.setConfig({    userId: UserInfo.userId, // 以后用户 id    userName: UserInfo.userName, // 以后用户 name});
  • pagehide 中次要用于计算用户停留在页面上的工夫 timeOnPage 和 刷新工作队列

统计用户点击按钮 — 交互式触发埋点

假如咱们心愿记录某些按钮的应用次数的数据,能够在 document 上监听 click 事件,目标利用事件冒泡以便于不须要侵入不同按钮的 click 事件,比方:

const TargetElementFilter = ['export_btn']const findTarget = (filters) => { return filters.find((filter) => TargetElementFilter.find((v) => filter === v)));}document.addEventListener('click', (e) => {  const { id, className, outerHTML } = e.target  const isTarget = findTarget([id, className])  if (isTarget) {    SDK.actionReport({      data: {        id,         className,        outerHTML      }, // 其余必要传递的信息    })  }})

上报页面性能

和页面性能相干的内容,属于 SDK 主动触发埋点,不应该让使用者在手动接入,在下面的实现中,咱们在 pageshow 事件中通 reportWebVitalsperformanceReport 进行数据上报,并且这里抉择了 Google 推出的 web-vitals 来获取和页面性能指标相干的具体数据,对应代码为:

// 通过 web-vitals 页面性能指标const reportWebVitals = (onPerfEntry) => {  if (onPerfEntry && onPerfEntry instanceof Function) {    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {      getCLS(onPerfEntry) // 布局偏移量      getFID(onPerfEntry) // 首次输出延迟时间      getFCP(onPerfEntry) // 首次内容渲染工夫      getLCP(onPerfEntry) // 首次最大内容渲染工夫      getTTFB(onPerfEntry) // 首个字节达到工夫    })  }}

获取失去的数据大抵如下:

上报谬误正告

全局谬误

全局谬误,即未被捕捉的谬误,能够通过 window.onerror 事件来捕捉,而后进行谬误数据上报,大抵如下:

window.addEventListener('error', (reason) => {    const { filename, message, error } = reason;    window.SDK.errorReport({        data: {            filename,             message,             error        }    });})

部分谬误

部分谬误,即通过 try...catch、promise.then、promise.catch 等捕捉的谬误,大抵应用如下:

 try {    throw new Error('error for test')  } catch(error) {    window.SDK.errorReport({      data: {        error,      },    })  }  Promise.reject(new Error('Promise reject for test'))  .then(    () => {},    (reason) => {      window.SDK.errorReport({        data: {            error: reason        }    });    },  )    Promise.reject(new Error('Promise reject for test'))  .catch(    (reason) => {      window.SDK.errorReport({        data: {            error: reason        }    });    },  )

接口申请谬误

接口申请谬误,即在二次封装申请 API 中进行申请和接管响应时的谬误,为了不便这里以 axios 来举例子,咱们能够在它的 申请拦挡响应拦挡 的第二个回调参数中去上报对应的谬误数据信息,大抵如下:

// 创立axios实例const service = axios.create({  baseURL, // api 的 base_url  timeout: 60000, // 申请超时工夫  responseType: reqConf.responseType,});// 申请拦挡service.interceptors.request.use(  (config) => {    ...    return config;  },  (error) => {    window.SDK.errorReport({      apiUrl: config.url,      data: {        error,      },    })  },);// 响应拦挡service.interceptors.response.use(  (config: any) => {    ...    return config;  },  (error: any) => {    window.SDK.errorReport({      apiUrl: config.url,      data: {        error,      },    })    return error.response.data;  },);

组件级谬误

组件级谬误,即应用 Vue / React 框架组件时产生的谬误,齐全能够应用它们在官网文档中提到的谬误捕捉形式来捕捉并上报谬误。

  • Vue 中的 errorHandler 就是用于为利用内抛出的未捕捉谬误指定一个全局解决函:

    // App.vueonMounted(()=>{  throw new Error('error in onMounted')});// main.tsconst app = createApp(App)app.config.errorHandler = (error, instance, info) => {    window.SDK.errorReport({        data: {            instance,            info,            error        }    });}
  • React 中的 ErrorBoundary 谬误边界相干的 getDerivedStateFromErrorcomponentDidCatch 钩子

    // 定义谬误边界组件class ErrorBoundary extends React.Component {  constructor(props) {    super(props);    this.state = { hasError: false };  }  static getDerivedStateFromError(error) {          // 更新 state 使下一次渲染可能显示降级后的 UI          return { hasError: true };    }  componentDidCatch(error, info) {          // 能够将谬误日志上报给服务器          window.SDK.errorReport({        data: {            info,            error        }    });  }  render() {    if (this.state.hasError) {              // 自定义降级后的 UI 并渲染      、        return <h1>Something went wrong.</h1>;        }    return this.props.children;   }}// 应用谬误边界组件<ErrorBoundary>  <MyWidget /></ErrorBoundary>

    最初

    当初咱们理解了 前端数据埋点 SDK 的二三事,通过下面的例子可能让你感觉看起来比较简单,然而真的要做好数据埋点也必然没有那么容易,比方好须要思考你的 SDK 数据发送的工夫、发送的次数、需不需要将某些数据信息整合在一起只发送一次、怎么防止网络拥塞等等问题。