什么是無痕埋點(diǎn)(smart-tracker)
傳統(tǒng)的埋點(diǎn)形式备埃,都是手動(dòng)埋點(diǎn),在指定的元素上綁定事件,將用戶行為信息發(fā)送到服務(wù)端進(jìn)行統(tǒng)計(jì)鸭限。但是當(dāng)引入無痕埋點(diǎn)的庫以后,用戶在瀏覽器里所有行為和操作都會(huì)被自動(dòng)記錄下來两踏,并將信息發(fā)送到后端進(jìn)行統(tǒng)計(jì)和分析败京。
我們?yōu)槭裁匆鰺o痕埋點(diǎn)
提高工作效率,解放雙手
無痕埋點(diǎn)原理
這里只講click的無痕埋點(diǎn)原理梦染。
當(dāng)用戶點(diǎn)擊了頁面上某一個(gè)元素赡麦,我們要把當(dāng)前元素到body之間整個(gè)dom的路徑記錄下來,作為這個(gè)元素的唯一標(biāo)識(shí)帕识,我們稱之為domPath泛粹,這個(gè)domPath不僅是這個(gè)元素唯一標(biāo)識(shí),還可以通過document.querySelector(domPath)去唯一選擇和定位到這個(gè)元素肮疗,當(dāng)用戶點(diǎn)擊一次這個(gè)元素戚扳,就會(huì)將埋點(diǎn)數(shù)據(jù)上傳到服務(wù)器,服務(wù)器上這個(gè)domPath對(duì)應(yīng)的統(tǒng)計(jì)數(shù)據(jù)加一族吻。
無痕埋點(diǎn)代碼實(shí)現(xiàn)
document.body.addEventListener('click', (event) => {
const eventFix = getEvent(event);
if (!eventFix) {
return;
}
this._handleEvent(eventFix);
}, false)
首先在document的body上監(jiān)聽和綁定全局click事件帽借,捕獲用戶所有的點(diǎn)擊事件。
// 關(guān)鍵代碼
const getDomPath = (element, useClass = false) => {
if (!(element instanceof HTMLElement)) {
console.warn('input is not a HTML element!');
return '';
}
let domPath = [];
let elem = element;
while (elem) {
let domDesc = getDomDesc(elem, useClass);
if (!domDesc) {
break;
}
domPath.unshift(domDesc);
if (querySelector(domPath.join('>')) === element || domDesc.indexOf('body') >= 0) {
break;
}
domPath.shift();
const children = elem.parentNode.children;
if (children.length > 1) {
for (let i = 0; i < children.length; i++) {
if (children[i] === elem) {
domDesc += `:nth-child(${i + 1})`;
break;
}
}
}
domPath.unshift(domDesc);
if (querySelector(domPath.join('>')) === element) {
break;
}
elem = elem.parentNode;
}
return domPath.join('>');
}
獲取元素唯一標(biāo)識(shí)domPath這段代碼是關(guān)鍵超歌。getDomPath函數(shù)傳入的是用戶點(diǎn)擊事件的target對(duì)象: getDomPath(event.target)砍艾。
主要思路是找到當(dāng)前元素event.target,然后不斷的去循環(huán)找它的父節(jié)點(diǎn)parentNode巍举,將父節(jié)點(diǎn)的tagName當(dāng)做domPath路徑上的節(jié)點(diǎn)脆荷,如果當(dāng)前元素有id,那就取消所有路徑的循環(huán),直接講id賦值給domPath蜓谋。
const children = elem.parentNode.children;
if (children.length > 1) {
for (let i = 0; i < children.length; i++) {
if (children[i] === elem) {
domDesc += `:nth-child(${i + 1})`;
break;
}
}
}
domPath.unshift(domDesc);
getDomPath函數(shù)中的這段代碼意思是在同一級(jí)上出現(xiàn)了多個(gè)相同tagName元素梦皮,那我們要定位到這個(gè)event.target這個(gè)元素在這一級(jí)里的第幾個(gè),假設(shè)這個(gè)div是同一級(jí)的第三個(gè)桃焕,那返回的就是div:nth-child(3)剑肯,這樣就可以在document.querySelector(domPath)里唯一定位到這個(gè)元素。
_handleEvent(event) {
const domPath = getDomPath(event.target);
const rect = getBoundingClientRect(event.target);
if (rect.width == 0 || rect.height == 0) {
return;
}
let t = document.documentElement || document.body.parentNode;
const scrollX = (t && typeof t.scrollLeft == 'number' ? t : document.body).scrollLeft;
const scrollY = (t && typeof t.scrollTop == 'number' ? t : document.body).scrollTop;
const pageX = event.pageX || event.clientX + scrollX;
const pageY = event.pageY || event.clientY + scrollY;
const data = {
domPath: encodeURIComponent(domPath),
trackingType: event.type,
offsetX: ((pageX - rect.left - scrollX) / rect.width).toFixed(6),
offsetY: ((pageY - rect.top - scrollY) / rect.height).toFixed(6),
};
this.send(data);
}
這段代碼就是得到用戶點(diǎn)擊某個(gè)元素的相對(duì)位置的橫向位置和豎向位置比例观堂,得到這個(gè)位置的值让网,就可以反向從埋點(diǎn)數(shù)據(jù)中得到用戶點(diǎn)擊元素的具體位置,因?yàn)槭莻€(gè)比例值师痕,所以在反向推導(dǎo)中還能自適應(yīng)頁面大小的改變溃睹。
send(data = {}) {
const image = new Image(1, 1);
image.onload = function () {
image = null;
};
image.src = `/?${stringify(data)}`;
}
得到了用戶點(diǎn)擊的位置信息和唯一標(biāo)識(shí)domPath,就可以將數(shù)據(jù)發(fā)送到服務(wù)端進(jìn)行統(tǒng)計(jì)了
用image的src胰坟,將數(shù)據(jù)進(jìn)行傳輸因篇。
用image的src有個(gè)好處就是輕量,并且還支持跨域笔横,打點(diǎn)基本上都用的這個(gè)方法進(jìn)行發(fā)送數(shù)據(jù)竞滓。
結(jié)尾
這里講的僅僅只是無痕埋點(diǎn)的一個(gè)簡(jiǎn)單實(shí)現(xiàn),對(duì)整個(gè)無痕埋點(diǎn)體系來說狠裹,這些只是冰山一角虽界。
真正的無痕埋點(diǎn),還需要做統(tǒng)計(jì)涛菠、分析莉御、差量預(yù)測(cè)、標(biāo)記策略俗冻、智能降噪礁叔、可視化無痕、無痕分桶迄薄、反向推導(dǎo)熱力圖琅关、大數(shù)據(jù)中臺(tái)等等,涉及到前端讥蔽、后端涣易、運(yùn)維、DBA和算法冶伞。