[干货]实现一个无埋点和可视化埋点sdk

序言

本文结合自身项目中的一些实践,将无埋点及可视化埋点的实现原理部分抽象整理出了一个sdk。同时也查阅了许多相关资料,发现它们在无埋点的实现原理上其实大同小异。html

sdk仅介绍和实现了点击事件的无埋点,其余用户行为的埋点也相相似。 sdk github地址 github.com/mfaying/web…node

无埋点

无埋点实际是全埋点,只要嵌入sdk,就能够自动收集数据。因为再也不须要额外的埋点代码,因此也能够称为无埋点。git

演示

首先,让咱们先来看下sdk的演示效果体验网址 (www.readingblog.cn/#/tutorials…) github

父页面(埋点管理页面)嵌入了一个iframe,指向了一个子页面(嵌入sdk的埋点页面),sdk能够自动计算点击元素的惟一标识(这里命名为"domPath"),以及元素大小、位置等相关信息,将数据发送给后端。同时,也会将这个数据跨域发送给埋点管理页面,管理页面依据这些数据作可视化埋点工做。图中,管理页面能够获取到了元素的信息(包括大小、位置、domPath等)。

如何使用

sdk的使用方式很是简单 首先,在head标签中引入sdk代码web

<script src="https://www.readingblog.cn/web-log-sdk-1.0.0.min.js"></script>
复制代码

而后,初始化sdk,在初始化时你能够传入一些自定义参数。初始化完毕后,sdk就已经在你的页面中工做了,是否是很方便!json

new WebLogger.AutoLogger({
  debug: true,
});
复制代码

这里是一个简单demo页面,在浏览器打开这个页面。随意点击,每次点击能够在控制台中看到自动打印出的埋点数据。后端

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>web-log-sdk</title>
  <script src="https://www.readingblog.cn/web-log-sdk-1.0.0.min.js"></script>
</head>
<body>
  <div>
    1
    <div id='1'>
      2
      <div id="1">3</div>
      <div>4</div>
    </div>
  </div>
  <div>5</div>
  <script> new WebLogger.AutoLogger({ debug: true, }); </script>
</body>
</html>
复制代码

无埋点的原理

无埋点其实监听了document.body上的点击事件(sdk在高版本浏览器中改成了监听事件捕获)。因此页面上的全部点击操做都会发送埋点数据。api

_autoClickCollection = () => {
  event.on(doc.body, 'click', this._autoClickHandle);
}
复制代码

这里就出现了一个问题,虽然这样点击操做可以触发埋点数据发送,可是咱们必须确保发送的数据是有价值的。 这里最关键的是咱们须要知道是页面中的哪一个元素触发了用户的点击操做。因为是自动埋点,咱们必须思考一种页面元素的标记方式。虽然元素有class、nodeName等标识,但这对于整个页面来讲是没法惟必定位一个元素的。元素的id虽然按照规范是惟一的,但也只有个别元素会标记上id属性。 因此咱们想了一种方式,因为整个html的dom结构像一棵树,对于任意元素(节点),咱们先找到它的父节点,父节点再找它的父节点,这样一直回溯,就会到html(根节点元素),这样就组成了一条路径,咱们将这条路径做为元素的惟一标识。固然了,若是的“domPath”反转一下,由“从父到子”的顺序排列,例如html>body>#app。这样咱们经过document.querySelector就能够惟一选中这个被点击的元素了。 具体实现以下:跨域

const _getLocalNamePath = (elm) => {
  const domPath = [];
  let preCount = 0;
  for (let sib = elm.previousSibling; sib; sib = sib.previousSibling) {
    if (sib.localName == elm.localName) preCount ++;
  }
  if (preCount === 0) {
    domPath.unshift(elm.localName);
  } else {
    domPath.unshift(`${elm.localName}:nth-of-type(${preCount + 1})`);
  }
  return domPath;
}

const getDomPath = (elm) => {
  try {
    const allNodes = document.getElementsByTagName('*');
    let domPath = [];
    for (; elm && elm.nodeType == 1; elm = elm.parentNode) {
      if (elm.hasAttribute('id')) {
        let uniqueIdCount = 0
        for (var n = 0; n < allNodes.length; n++) {
          if (allNodes[n].hasAttribute('id') && allNodes[n].id == elm.id) uniqueIdCount++;
          if (uniqueIdCount > 1) break;
        }
        if (uniqueIdCount == 1) {
          domPath.unshift(`#${elm.getAttribute('id')}`);
        } else {
          domPath.unshift(..._getLocalNamePath(elm));
        }
      } else {
        domPath.unshift(..._getLocalNamePath(elm));
      }
    }
    return domPath.length ? domPath.join('>') : null
  } catch (err) {
    console.log(err)
    return null;
  }
}

export default getDomPath;
复制代码

代码中咱们还作一些处理,好比当有多个localName相同的兄弟节点时,常见的例如数组

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
</ul>
复制代码

咱们经过:nth-of-type选择器来区分。

若是有id属性,为了确保id是惟一的(规范要求必须惟一,但开发者也有可能会在无心间赋上重复的id属性),咱们作了检查,若是是惟一的就使用id做为标记,这样能够提升选择器的效率。

肯定了元素的惟一标识,接下来的事情就很简单了。咱们只需获取所须要的埋点数据,将其发送给后端就能够了。

好比获取元素位置信息

const getBoundingClientRect = (elm) => {
  const rect = elm.getBoundingClientRect();
  const width = rect.width || rect.right - rect.left;
  const height = rect.height || rect.bottom - rect.top;
  return {
    width,
    height,
    left: rect.left,
    top: rect.top,
  };
}

export default getBoundingClientRect;
复制代码

获取平台信息

import { ua } from '../common/bom';
import platform from 'platform';

const getPlatform = () => {
  const platformInfo = {};

  platformInfo.os = `${platform.os.family} ${platform.os.version}` || '';
  platformInfo.bn = platform.name || '';
  platformInfo.bv = platform.version || '';
  platformInfo.bl = platform.layout || '';
  platformInfo.bd = platform.description || '';

  const wechatInfo = ua.match(/MicroMessenger\/([\d\.]+)/i);
  const wechatNetType = ua.match(/NetType\/([\w\.]+)/i);
  if (wechatInfo) {
    platformInfo.mmv = wechatInfo[1] || '';
  }
  if (wechatNetType) {
    platformInfo.net = wechatNetType[1] || '';
  }

  return platformInfo;
}

export default getPlatform;
复制代码

当前url、引用url、title、事件的触发时刻等等信息均可以补充进去。这是个人sdk发送的一个埋点数据

{
	"eventData": {
		"et": "click",
		"ed": "auto_click",
		"text": "参考: Elasticsear...icsearch 2.x 版本",
		"nodeName": "p",
		"domPath": "html>body>#app>section>section>main>div:nth-of-type(5)>div>p>p",
		"offsetX": "0.768987",
		"offsetY": "0.333333",
		"pageX": 263,
		"pageY": 167,
		"scrollX": 0,
		"scrollY": 0,
		"left": 20,
		"top": 153,
		"width": 316,
		"height": 42,
		"rUrl": "http://localhost:8080/",
		"docTitle": "blog",
		"cUrl": "http://localhost:8080/#/blog/article/74",
		"t": 1573987603156
	},
	"optParams": {},
	"platform": {
		"os": "Android 6.0",
		"bn": "Chrome Mobile",
		"bv": "77.0.3865.120",
		"bl": "Blink",
		"bd": "Chrome Mobile 77.0.3865.120 on Google Nexus 5 (Android 6.0)"
	},
	"appID": "",
	"sdk": {
		"type": "js",
		"version": "1.0.0"
	}
}
复制代码

实现可视化圈选埋点

可视化埋点通常会使用iframe将埋点页面嵌入。这时子页面是埋点页面(由iframe引入)、父页面是管理页面。因为iframe的src属性是支持跨域加载资源的,因此任何埋点页面都是能够嵌入的。

可是要实现圈选功能,必须实现埋点页面和管理页面的通讯,由于管理页面是不知道埋点信息的。并且因为埋点页面是跨域的,管理页面根本没法操做埋点页面。

这里咱们就须要sdk实现一种通讯机制了,咱们采用通用的跨域通讯方案postMessage。 在sdk的配置项中增长一个postMsgOpts字段用来配置postMessage参数,postMsgOpts的默认值是一个空数组,也就是说它能够容许埋点页面向多个源发送数据,而它的默认配置是不会经过postMessage发送数据的。 postMsgOpts字段配置示例以下:

new AutoLogger({
  debug: true,
  postMsgOpts: [{
    targetWindow: window.parent,
    targetOrigin,
  }, {
    targetWindow: window,
    targetOrigin: curOrigin,
  }],
});
复制代码

这样将要发送的埋点数据也会调用postMessage api发送一份。

postMsgOpts.forEach((opt) => {
  const { targetWindow, targetOrigin } = opt;
  targetWindow.postMessage({ logData: JSON.stringify(logData) }, targetOrigin)
});
复制代码

咱们回过头来分析演示是如何实现可视化埋点的。首先管理页面的iframe加载了埋点页面,因为埋点页面引入了sdk,因此点击页面中任何元素,都会将埋点数据经过postMessage发送一份给管理页面。这里的数据包括了元素的大小和位置、domPath等等。管理页面只要监听了"message"事件,就能够拿到从子页面(埋点页面)传出来的数据了。为了交互友好,根据这些信息管理页面能够圈出iframe中选中的元素。固然了,只要管理页面拿到了埋点数据,就能够在这基础上和使用管理页面的用户交互,作一些自主配置同时将附加信息及选中元素的信息传递给后端,这样后端就能够对选中元素作处理了,从而实现可视化埋点。

配置项

最后介绍一下个人sdk的配置项,先参考一下默认配置

import getPlatform from '../../utils/getPlatform';

const platform = getPlatform();

export default {
  appID: '',
  // 是否自动收集点击事件
  autoClick: true,
  debug: false,
  logUrl: '',
  sdk: {
    // 类型
    type: 'js',
    // 版本
    version: SDK_VERSION,
  },
  // 平台参数
  platform,
  optParams: {},
  postMsgOpts: [],
};
复制代码
  1. appID 你能够在初始化时注册一个appID,因此相关的埋点都会带上这个标记,至关于对埋点数据作了一层app维度上的管理。
  2. autoClick 默认为true,开启会自动收集点击事件(即点击无埋点)。固然你能够实现页面登陆、登出、浏览时间的埋点功能,同时能够在配置中加开关控制,让用户能够有选择地启用这些功能。
  3. debug 默认不开启,开启会将埋点数据打印到控制台,便于调试。
  4. logUrl 接收日志的后端地址
  5. sdk sdk自身信息一些说明
  6. platform 默认会自动获取一些平台参数,你也能够经过配置这个字段覆盖它
  7. optParams 自定义数据

相关文章
相关标签/搜索