Vue3埋点实践

866 阅读2分钟

背景

pc + elementplus + Vue3 自用系统埋点的实践

用法

  1. v-trace
<el-button
  v-trace="'登录按钮'"
  type="default"
  plain
  >登录</el-button
>
<el-button
  v-trace="{ evName: evName, evType: TrackerType.nav }"
  type="default"
  plain
  >登录</el-button
       >
<script setup>
  // 上报存在变量的动态数据
  const evName = ref("登录按钮")
</script>
  1. api调用
<script setup>
import { ref, getCurrentInstance } from "vue";

  // 读取挂载到app上的全局对象
const { proxy } = getCurrentInstance();
  // 具体上报
const reportHandler = () => {
  proxy.$tracker.report({
    evName: '列表-删除按钮',
    evInfo: `行号: ${row.index}; 输入内容: ${row.searchInput}; 输出内容: ${row.searchOutput}`
  });
}
</script>
  1. 组合api上报页面访问
<script setup>
import { ref, usePageTracker } from "@/hooks/index";

const formData = ref({
  name: '',
  type: 1
});
  
// 入参为当前页面的标题
// 上报元素埋点时会带上当前的页面信息
// 返回埋点实例,方便元素层级的埋点,不用再读取一下全局对象了
const { $trace } = usePageTracker("测试列表页");

const reportEle = () => {
  $trace({
    evName: '查询按钮',
    evInfo: `输入内容:${JSON.stringify(formData.value)}`,
  });
};
</script>

具体实现

  1. v-trace 指令实现

首先实现一个指令,然后将指令挂到app全局上,便于在项目中使用。

import { TrackerType } from "@/libs/tracker";

/** 元素埋点指令 */
export default function VTrace(app) {
  return {
    mounted(el, binding) {
      el.addEventListener(
        "click",
        (e) => {
          e.stopPropagation();
          const value = binding.value;
          // 具体事件名(操作的元素名)
          let evName = value;
          // 当前事件类型
          let evType = TrackerType.btn;
          let evInfo = "";
          if (typeof value === "object") {
            evName = value.evName;
            evType = value.evType || TrackerType.btn;
            evInfo = value.evInfo;
          }
          // 调用全局上报方法
          app.config.globalProperties.$tracker.report(
            {
              evName,
              evInfo,
            },
            evType
          );
        },
        false
      );
    },
  };
}

在项目入口文件中,初始化埋点实例并将实例挂载到全局对象上,将全局指令也挂载到全局对象上。

import { createApp } from "vue";
import ElementPlus from "element-plus";
import MyTracker from "@/libs/tracker";
import directives from "@/directives";


const app = createApp(App)
  .use(ElementPlus)
  .use(Router);

app.config.globalProperties.$tracker = new MyTracker();

Object.keys(directives).forEach((k) => {
  app.directive(
    k,
    typeof directives[k] === "function" ? directives[k](app) : directives[k]
  );
});

app.mount("#app");

初始化埋点信息以及在埋点信息中添加登录后的用户信息

import { onMounted,getCurrentInstance } from "vue";
import { useStore } from "vuex";

const store = useStore();

const { proxy } = getCurrentInstance();
/** 应用初始化
 * 1. 埋点初始化
 * 2. 获取用户信息并更新给埋点系统
 */
const init = async () => {
  proxy.$tracker.init();
  const query = getParams(location.href);
  const { data } = await store.dispatch("getUserInfo");
  proxy.$tracker.addUserInfo({
    userId: data.id,
    userName: data.username,
  });
};

onMounted(() => {
  init();
});
  1. tracker类的实现
import { getDuration, getCurrentTimeStamp } from "@/libs/date";
import { decrypt, encrypt, localSave } from "@/libs/util";
import pkg from "../../package.json";

/** 埋点类型 */
export const TrackerType = {
  page: "pv",
  btn: "btn-clk",
  nav: "nav",
  short: "short-key",
  area: "area-clk",
};

const Storage_Key = "t_data";

/** 埋点 */
class MyTracker {
  constructor() {
    this.base = {};
    this.user = {};
    this.page = {};
    this.timeInterval = null;
  }

  /** 获取浏览器 */
  getSystemInfo() {
    const _navigator =
      typeof navigator !== "undefined" ? navigator : window.navigator;
    this.base = {
      app_version: pkg.version,
      ua: _navigator.userAgent,
      domain: window.location.origin,
    };
  }

  /** 注入登录用户信息到埋点数据中 */
  addUserInfo(user) {
    this.user = user;
  }

  /** 注入当前页面信息 */
  addPageInfo(params = {}) {
    const _location = window.location;
    this.page = {
      url: _location.href,
      sTimeStamp: getCurrentTimeStamp(),
      ...params,
    };
    this.pageStorage();
  }

  /** 定期缓存页面信息,在上报时清除掉 */
  pageStorage() {
    // 延迟缓存防止在补报前缓存就被清掉
    setTimeout(() => {
      this.clearPageStorage();
      this.timeInterval = setInterval(() => {
        // 记录时间点
        this.page.eTimeStamp = getCurrentTimeStamp();
        localSave(
          Storage_Key,
          JSON.stringify({ page: { ...this.page }, user: { ...this.user } })
        );
      }, 1000);
    }, 5000);
  }

  /** 清除页面缓存 */
  clearPageStorage() {
    localStorage.removeItem(Storage_Key);
    clearInterval(this.timeInterval);
    this.timeInterval = null;
  }

  /** 补报之前的页面浏览信息
  * 应用场景: 页面pv是在访问页面后跳转其他路由时上报的,所以这里是用户访问页面后直接关闭浏览器导致当前
  * 浏览记录没能上报,在下次访问时进行补报
  */
  fixReportPage() {
    const tData = JSON.parse(localStorage.getItem(Storage_Key) || "{}");
    if (tData.page?.url) {
      const _page = tData.page;
      _page.duration = getDuration(_page.sTimeStamp, _page.eTimeStamp);
      console.log("tracker log=> 页面信息补报");
      this.report({ evName: "页面浏览" }, TrackerType.page, {
        page: _page,
        ...tData.user,
      });
      this.clearPageStorage();
    }
  }

  /** 上报pv */
  reportPage() {
    const _page = this.page;
    if (!_page.url) return;
    // 记录停留时间
    _page.duration = getDuration(_page.sTimeStamp, _page.eTimeStamp);
    this.report({ evName: "页面浏览" }, TrackerType.page);
  }

  /** 上报数据到服务端 */
  report(eventParams = {}, type = TrackerType.btn, otherParams = {}) {
    const page =
      type === TrackerType.page
        ? this.page
        : { pName: this.page.pName, url: this.page.url };
    const params = {
      ...this.base,
      ...this.user,
      ...page,
      e: {
        ...eventParams,
      },
      trackType: type,
      t: getCurrentTimeStamp(),
      ...otherParams,
    };
    console.log("tracker log=> ", params);
  }

  /** 初始化 */
  init() {
    this.getSystemInfo();
    this.fixReportPage();
  }
}

export default MyTracker;

关于页面pv上报这里,是在浏览当前路由后跳转其他路由时上报当前路由访问信息,并带上停留时长。如果不需要停留时长,可以在页面访问时直接上报。关于如何在浏览器关闭时调接口上报数据,还有待进一步优化。

欢迎评论区交流讨论更多关于vue埋点的信息~