略干,写一套小程序无埋点SDK

lxf2023-03-08 18:21:01

略干,写一套小程序无埋点SDK

首先,先看看埋点能做什么?

  1. 采集用户行为

    • 页面的pv/uv
    • 用户在页面的停留时长
    • 用户的访问链路
    • 用户的交互行为
    • 营销模块的曝光数据
    • 用户的设备信息等
  2. 性能监控和异常监控

    • 首屏加载时长
    • 白屏时间
    • 小程序运行异常
    • http接口异常
    • 脚本异常1. 采集用户行为

埋点分类及对比

略干,写一套小程序无埋点SDK

言归正传

本期主要内容:在微信小程序中通过无埋点的方式收集上报用户行为;

一、通过拦截小程序app和page的生命周期来采集用户的pv、uv、停留时长、及访问链路;

拦截的目的是什么?

答:进行拦截之后,小程序生命周期在执行时,可以顺带执行我们的埋点收集和上报的程序。

因此,

我们不能干扰开发者定义的生命周期方法,要让我们的埋点程序在开发者无感知的情况下执行。

怎么实现?

1.对App,Page,Component三个方法进行重写。

cnost init = () => {
  const oldApp = App;
  const oldPage = Page;
  const oldComponent = Component;
  
  /**
  *options 是开发者对App方法的传参
  *options={
  *			onLaunch(){ .... }
  * }
  **/
  App = function(options){
    
    //拦截options中的生命周期方法,以拦截onLaunch方法为例,
    // options['onLaunch'] = function(){ ...此处是拦截方法... }
    
    //执行开发者传入的逻辑
    oldApp(options)
  }
  
  // ....同理, Page 和 Component 也是同样操作....
}

2.拦截的核心逻辑

/**
 * 页面生命周期拦截
 * @param {*} options 
 * @param {*} key 
 * @param {*} callback 
 * @param {*} isIntercepted 
 * @returns 
 */
const interceptor = function(options, key, callback, isIntercepted){
    if(!isIntercepted){
        // 是否进行拦截,不拦截直接return
        return;
    }
    if(options&&options[key]&&typeof options[key] === 'function'){
        const fn = options[key];
        options[key] = function(args){
            // 执行拦截逻辑
            callback.call(this, args);
            // 执行开发者自定义的生命周期方法
            return fn.call(this, args);
        }
    }else{
        options[key] = function(args){
            return callback.call(this, args);
        }
    }
}

3.在init中使用拦截器

   init = ()=>{
        const oldApp = App;
        const oldPage = Page;
        const oldComponent = Component;

        App = function(options){
          interceptor(options, 'onLaunch', callback, _config['appOnLaunch']);
         oldApp(options);
        }
      ......
   }

4.定义拦截逻辑

import Report from "./report.js"
// 上报
let report = null;
//保存配置
let _config = {};

class Tracker {
    constructor(config){
        const defConfig = {
            'appName': '',
            'url': '',
            'appOnLaunch': true,
            'appOnShow': true,
            'appOnHide': true,
            'pageOnLoad': true,
            'pageOnShow': true,
            'pageOnHide': true,
            'pageOnUnload': true,
            'reportTime': 2000, //上报时间间隔,
            'pageConfig':{},
            'componentConfig':{},
        }
        _config = Object.assign(defConfig, config);
        // 初始化上报方法
        report = new Report(_config.reportTime);
        this.report = report;
        this.init();
    }

    // APP生命周期拦截回调
    app = {
        onLaunch(args){
            console.log('#tracker: app launch', args)
          //上报数据
            report.push({ name: 'app', event: 'APP_ON_LAUNCH', view:'app', type: 'appload' })
        },
        ......
    }
    // Page生命周期拦截
    page = {
        onLoad(args){
            console.log('#tracker: page load', args)
          //上报数据
            report.push({ name: 'page', event: 'PAGE_ON_LOAD', view: 'page', type: 'load' })
        },
        ......
    }
    // 组件生命周期拦截
    component = {
        attached(){
            handleComponentConfig.call(this);
        }
    }
    init = ()=>{
        const oldApp = App;
        const oldPage = Page;
        const oldComponent = Component;

        const _app = this.app;
        const _page = this.page;
        const _component = this.component;
        App = function(options){
            interceptor(options, 'onLaunch', _app.onLaunch, _config['appOnLaunch']);
        }

        Page = function(options){
            interceptor(options, 'onShow', _page.onShow, _config['pageOnShow']);
            oldPage(options)

        }

        Component = function(options){
          //组件的生命周期在lifetimes中,拦截器略有不同
            compLifetimeInterceptor(options, 'attached', _component['attached']);
            oldComponent(options)
        }

    }

}

小结:

通过对App,Page的生命周期进行拦截,可以采集pv,uv, 用户停留时长,页面加载时长等,也可以捕获用户的页面访问链路。

二、通过拦截事件行为来采集用户行为,以点击事件为例

常用的实现方法:在页面的最外层元素上添加代理事件,对所有点击事件进行拦截。

缺点:

1.无法捕获到catch掉的事件,也无法捕获到组件中的点击事件;

2.会捕获到大量无用事件,造成大量无用数据的上报;

我的思路:使用配置文件的方式在需要埋点的页面,配置需要拦截的事件即可。

配置文件结构

export const pageConfig = {
    //页面路径作为配置项的key
    'pages/index/index':{
      name: '首页', //页面名称
      elements:[ //配置项元素,是一个数组
        {
          method: 'bindViewTap', //需要拦截的事件
          type:'click', //事件类型,click代表点击事件
          dataset:['nickname'], //需要采集的数据
        },
      ]
    },
  } 
//组件的事件配置
  export const componentConfig = {
      'components/myTest/myTest':{
          name: '测试组件',
          elements: [
              {
                method: 'btnTap',
                type: 'click',
                dataset:[ 'test' ]
              }
          ]
      }
  }

核心拦截方法

// 点击事件拦截处理
const clickMethodProxy = (_this, el, action)=>{
    // 点击事件
    const methodName = el.method; //需要拦截的事件名称
    const methodFn = _this[methodName]; //开发者自定义的事件回调
    // 点击事件拦截
    _this[methodName] = function(e){
        //执行开发者自定义的拦截方法
        methodFn.call(_this, e);
        //采集需要上传的数据
        const dataset = e.currentTarget.dataset;
        const extraData = mapExtraData(dataset, el.dataset||[])
        //数据上报
        report.push({ page: action.path, pageName: action.name, event: methodName, view: 'element', type: 'click',extraData } )
    }
}

小结:

通过配置文件的方式对指定的事件进行拦截来实现埋点上报,方便统一管理和维护,且对业务代码基本没有干扰。

三、借用interscetionObserver实现对曝光数据的采集

什么是intersectionObserver?传送门: developer.mozilla.org/zh-CN/docs/… 下面是简单的示意图:

略干,写一套小程序无埋点SDK

曝光的实现逻辑相对简单,读者可以自行思考实现。^-^ ^-^ 这里还有一个坑,小心动态渲染的曝光元素,不做特殊处理是监听不到的