内忧外患:埋点的优化

2018-09-19  本文已影响94人  凌霄光

为了感知用户的行为,并基于用户使用产品的各种行为,进行产品的迭代优化等。项目代码中有大量的埋点,主要是页面浏览元素点击两种。

按理说,这也是一个业务无关的逻辑,不应该出现在业务代码中,就算必须嵌入业务代码,也应该占据很少的一部分,但事实上,埋点不但侵入了业务代码,而且占据了非常多的代码量。

现场是这样的,感受一下。

点击事件埋点:

   timer(x = this) {
      this.$logger.log({
        type: this.$logger.eventType.action,
        event_id: Date.now().toString(),
        payload: [
          {
            key: 'type',
            value: 'time'
          }
        ],
        event: 'quick_tool_click',
        event_name: this.$logger.getEventName('quick_tool_click')
      })
      //使用默认值,把this方便传给vuex,方便埋点
      this.showTimer(x)
    },
    redflag() {
      this.$logger.log({
        type: this.$logger.eventType.action,
        event_id: Date.now().toString(),
        payload: [
          {
            key: 'type',
            value: 'redflag'
          }
        ],
        event: 'quick_tool_click',
        event_name: this.$logger.getEventName('quick_tool_click')
      })

      if (this.selectedStudentList.length == 0) {
        return this.toast2()
      } else {
        this.$router.push({
          name: coverPageNameToLowerCase(PageName.GROUPING_GRANT.ROUTE),
          query: {
            from: PageName.LECTURE.ROUTE
          }
        })
      }
    },

页面浏览埋点:

  beforeRouteEnter(to, from, next) {
    next(vm => {
      vm.pv_id = Date.now().toString()
      vm.$logger.log({
        type: vm.$logger.eventType.pv,
        event_id: vm.pv_id,
        page_name: PageName.CLASS_ENJOY.LOG,
        event: 'page_entry',
        event_name: vm.$logger.getEventName('page_entry')
      })
    })
  },
  beforeRouteLeave(to, from, next) {
    this.$logger.log({
      type: this.$logger.eventType.pv,
      event_id: this.pv_id,
      page_name: PageName.CLASS_ENJOY.LOG,
      event: 'page_leave',
      event_name: this.$logger.getEventName('page_leave')
    })
    next()
  }

这两段代码,第一段是点击事件埋点,第二段是页面浏览埋点。很常见的一个需求,却也很常见的大段代码,这显然是不合理的。

经过分析,我发现了几个问题:

  1. 很多参数是没必要传的,完全可以封装到log内部,比如自动生成的event_id,比如从map中取的event_name。
  2. 像页面浏览,每个页面都要加一个beforeRouteEnter,beforeRouteLeave,太繁琐了,而且一旦有改动,那需要改动的文件数量也特多。

这两点问题是两个方面,一个是内部的封装(内忧),一个是外部的使用(外患),一个是部分参数应该封装到内部,一个是外部的使用方式应该简化。

我分别对这两点进行了改进,第一点可以把内部暴露出的方法简化,第二点可以用mixin或指令来解决,这里指令的方式更加优雅。

之前暴露出的接口:

const _logger = function (Vue, options) {
  if (_logger.installed) {
    return;
  }

  const tracker = logger.getLogger('tracker');

  Object.defineProperties(Vue.prototype, {
    $logger: {
      get() {
        return {
          log: function (data) {
            var _log = configContext(tracker, options);
            _log.info(data)
          },
          getEventName: function (key) {
            return _eventmap.get(key);
          },
          eventType: {
            pv: 'page_view',
            action: 'action',
            profile: 'performance'
          }
        }
      }
    }
  })
}

我把log改成了私有方法,暴露出了trackPv,trackAction和trackPerformance三个方法。参数也精简成了:pageName、eventName、trackData,简洁却又足够。

  const tracker= logger.getLogger('tracker');

  const eventType = {
    pv: 'page_view',
    action: 'action',
    performance: 'performance'
  }
  //这里叫eventName是不对的,应该是eventDesc,是描述信息
  const getEventDesc = function (key) {
    return _eventmap.get(key);
  }
  const log = function (data) {
    var _log = configContext(tracker, options);
    _log.info(data)
  }

  const idGenerator = {
    timeId() {
      return Date.now().toString()
    }
  }

  const track = (eventType, pageName, eventName, eventData) => {
    log({
      event_id: idGenerator.timeId(),
      type: eventType,
      page_name: pageName,
      event: eventName,
      event_name: getEventDesc(eventName),
      payload: eventData
    });
  }

  const eventTracker = {
    trackPv(pageName, eventName, eventData) {
      track(eventType.pv, pageName, eventName, eventData);
    },
    trackAction(pageName, eventName, eventData) {
      track(eventType.action, pageName, eventName, eventData);
    },
    trackPerformance(pageName, eventName, eventData) {
      track(eventType.performance, pageName, eventName, eventData);
    }
  };

  Object.defineProperties(Vue.prototype, {
    $tracker: {
      get() {
        return eventTracker;
      }
    }
  })

现在组件内的调用方式:

this.$tracker.trackPv('login', 'login_btn_click', {
    a: 'aaa',
    b: 'bbb'
});

这样还不够,比如页面浏览,埋点还是得写在组件里的路由切换的钩子里。于是我又封装了v-trakPv,v-trackClickAction两个指令,前者加在页面组件上,后者用在具体元素上。

trackPv:

/**
 * trackPv指令
 * 绑定元素在组件创建销毁时会进行 page_entry和page_leave埋点
 *
 */
export default (tracker) => {

    const handleTrackInfo = (value) => {
        const trackInfo = value || {};
        if(!trackInfo.pageName) {
            throw new Error('trackPv指令: pv埋点必须传入pageName');
        }
        return trackInfo;
    }

    return {
        bind(el, binding) {
            const {pageName, eventData} = handleTrackInfo(binding.value);
            tracker.trackPv(pageName, 'page_entry', eventData);
        },
        unbind(el, binding) {
            const {pageName, eventData} = handleTrackInfo(binding.value);
            tracker.trackPv(pageName, 'page_leave', eventData);
        }
    }
}

trackClickAction:

/**
 * trackClickAction指令
 * 绑定元素点击时会埋点
 * 
 */
export default (tracker) =>{

    let clickHandler = () => {}

    return {
        bind(el, binding) {
            const {pageName, eventName, eventData} = binding.value || {};
            clickHandler = () => {
                tracker.trackAction(pageName, eventName, eventData);
            }
            el.addEventListener('click', clickHandler);
        },
        unbind(el, binding) {
            el.removeEventListener('click', clickHandler);
        }
    }
}

这样,就把pageview埋点的所有代码都封装到了指令内部,组件里只需要:

  <div v-trackPv="{pageName: 'aaa', eventData: {}}"></div>

而元素点击的埋点也可以简化成:

 <button  v-trackClickAction="{pageName:'login', eventName: 'login_btn_click',eventData:{}}">登陆</button>

这两种情况之外,与业务有关的埋点,或者其他事件的埋点,可以手动调用api

this.$tracker.trackAction('pageName', 'event_name ', {data: 'data'});

组件中的业务无关代码量,得到了非常大的减少。都转移到了模板中的指令里。

总结

埋点是很常见的需求,有些是业务无关、有的是业务相关。业务无关的埋点不应该出现在组件代码里,业务相关的埋点调用方式也应该简单。

所以,我首先部分解决了内忧,把暴露的api进行了精简,同时也针对外患,也就是调用方式的繁杂,通过指令进行了封装。

上一篇下一篇

猜你喜欢

热点阅读