[乾貨]實現一個無埋點和可視化埋點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 自定義數據

相關文章
相關標籤/搜索