當神策js被廣告攔截外掛攔截後-關於神策埋點的思考

語言: CN / TW / HK

前言

最近公司的資料採集由谷歌分析轉為神策埋點,提前一天完成後,本著“知其然也知其所以然”的精神,開始研究神策埋點的實現,正巧,出現採集不到的情況,研究發現是因為瀏覽器裝了攔截外掛(uBlock origin),sensorsdata.min.js被攔截了,自然無法採集,問題得解;這麼簡單嗎?請看下文。(關鍵詞:訂閱釋出)

閱讀本文期待收貨

  • 如果你公司要進行神策埋點,將帶你理解神策的資料採集到底是怎麼實現的?
  • 如果不,通過神策資料流轉理解訂閱釋出設計模式

問題

關鍵是,js都無法引入,為什麼採集方法觸發時不報錯呢?關鍵點:

  • js都無法引入,為什麼採集方法觸發時不報錯
  • 全域性上依然有sensors屬性,且依然可以觸發track方法,但神策資料後臺無資料

接入神策操作

其實接入過程挺簡單的,分全埋點和程式碼埋點兩部分,全埋點不需要開發人員關心,程式碼埋點只要在特定位置觸發封裝的方法,傳入資料即可;那關鍵自然就是頁面載入時初始化神策的邏輯。(文末附上全部初始化程式碼,其實大部分是神策文件提供的);分步分析:(建議將全部程式碼複製出來對照著看)

一上來就是關鍵的自執行函式,傳入神策使用者的配置引數,進入函式內容分析

  1. 初始化sensors物件

    此處重點注意_q ,其實就是 訂閱釋出 中儲存的佇列,用於儲存事件的資料

// n = 'sensors'  w = window
w[n] = w[n] || function(a) { return function() {
            (w[n]._q = w[n]._q || []).push([a, arguments]);
         } 
        };
// 此時window上存在屬性sensors 是一個高階函式       
function(a) { return function() {
            (window['sensors']._q = window['sensors']._q || []).push([a, arguments]);
} 
// 此函式執行時會在sensors上掛_q屬性 陣列  且會將呼叫引數和預設陣列傳進去
複製程式碼

  1. 初始化屬性

    相當於掛載方法,觸發方法時就是將採集事件和資料存入_q中(佇列),即為訂閱

var ifs = ['track', 'quick', 'register', 'registerPage', 'registerOnce', 'clearAllRegister', 'trackSignup', 'trackAbtest', 'setProfile', 'setOnceProfile', 'appendProfile', 'incrementProfile', 'deleteProfile', 'unsetProfile', 'identify', 'login', 'logout', 'trackLink', 'clearAllRegister'];
// 遍歷掛載 
    for (var i = 0; i < ifs.length; i++) {
        w[n][ifs[i]] = w[n].call(null, ifs[i]);
    }
複製程式碼

以track為例

w[n][ifs[i]] = w[n].call(null, ifs[i]);
// 相當於
sensors['track'] = sensors.call(null,'sensors') = function() {
            (window['sensors']._q = window['sensors']._q || []).push(['sensors', arguments]);
} 
// 此時如果觸發方法,sensors上將有一個屬性名為_q 的陣列 用於儲存採集事件 格式為 [method,[event,data]]
複製程式碼

觸發的話 就會存入_q

  1. 引入sensorsdata.min.js 並插入檔案(順帶將使用者定義引數儲存在sensors物件的para屬性上)
 x = d.createElement(s), y = d.getElementsByTagName(s)[0];
        x.async = 1;
        x.src = p;
        w[n].para = para;
        y.parentNode.insertBefore(x, y);
複製程式碼

到此為止,我們已經瞭解了(實現了)程式碼埋點觸發時將事件和需要的資料的採集,存入到_q中;個人理解其實這就是神策埋點的資料採集邏輯,採集好了再將這些傳入神策的分析後臺生成視覺化表格就成了;

而且我們也能夠解釋我們最開始的疑問了

解答

  • js都無法引入,為什麼採集方法觸發時不報錯

    因為本來就是呼叫我們封裝的物件,函式也有,自然不會報錯
    複製程式碼
  • 全域性上依然有sensors屬性,且依然可以觸發track方法,但神策資料後臺無資料

    sensorsdata.min.js被block了(攔截),神策沒辦法“釋出”,自然沒辦法採集資料,所有資料還保留在sensors._q中
    複製程式碼
驗證
  1. 開啟uBlock情況,觸發feedback_click事件,資料將存入_q中且不會被消費,後臺無法採集資料

  1. 關閉uBlock情況,觸發feedback_click事件,資料將存入_q中隨即被消費,_q中找不到資料,後臺採集到資料

到此為止?不

延展:本地的sensor和神策的sensorsdata.min.js是怎麼聯絡起來的呢?換句話說,神策是如何獲得sensor._q的?(獲取到了自然執行匯入等操作不是問題)

下載被攔截的神策js檔案放在本地開始研究,(涉及到公司名稱,直接公開又擔心涉及神策隱私,就沒法提供地址了,很抱歉,會提供我研究的部分程式碼)

  1. 首先,在個人專案中,會有一個掛載’sensorsDataAnalytic201505‘屬性在window上的操作,屬性值是定義物件名(我這是sensors)

    // n = 'sensors'  w = window
    w['sensorsDataAnalytic201505'] = n;
    複製程式碼
  2. 神策js中,會進行取值操作,獲取到使用者定義的字串,然後進行初始化操作(其實就是將本地的sensor物件的資料取出來,釋出,然後將自己定義的物件替換上去)

    1. 判斷,若存在呼叫sd.setPreConfig
    "string" != typeof window.sensorsDataAnalytic201505) return "undefined" == typeof window.sensorsDataAnalytic201505 ? (window.sensorsDataAnalytic201505 = sd, sd) : window.sensorsDataAnalytic201505;
            sd.setPreConfig(window[sensorsDataAnalytic201505]),
    複製程式碼
    1. setPreConfig方法很簡單,取出使用者傳遞配置引數及事件佇列
    sd.setPreConfig = function(e) {
                sd.para = e.para,
                sd._q = e._q
            },
    複製程式碼
    1. 替換物件
      window[sensorsDataAnalytic201505] = sd,
      sd.init(),
      window.sensorsDataAnalytic201505 = sd
    複製程式碼

至此,本地的自定義物件(其實是函式)sensors和神策就關聯起來了,簡言之

神策只關心神策使用者存入佇列的事件的“釋出”,而不關心如何存入;完美收尾

全部初始化程式碼

var currentHost = window.location.host;
var sensor_server_url = '測試環境資料採集後臺地址';
// 環境判斷
if (currentHost == "生產環境域名") {
    sensor_server_url = '生產環境資料採集後臺地址';
};
(function(para) {
    var p = para.sdk_url,
        n = para.name,
        w = window,
        d = document,
        s = 'script',
        x = null,
        y = null;
    w['sensorsDataAnalytic201505'] = n;
    w[n] = w[n] || function(a) { return function() {
            (w[n]._q = w[n]._q || []).push([a, arguments]); } };
    var ifs = ['track', 'quick', 'register', 'registerPage', 'registerOnce', 'clearAllRegister', 'trackSignup', 'trackAbtest', 'setProfile', 'setOnceProfile', 'appendProfile', 'incrementProfile', 'deleteProfile', 'unsetProfile', 'identify', 'login', 'logout', 'trackLink', 'clearAllRegister'];
    for (var i = 0; i < ifs.length; i++) {
        w[n][ifs[i]] = w[n].call(null, ifs[i]);
    }
    if (!w[n]._t) {
        x = d.createElement(s), y = d.getElementsByTagName(s)[0];
        x.async = 1;
        x.src = p;
        w[n].para = para;
        y.parentNode.insertBefore(x, y);
    }
})({
    sdk_url: 'https://assets-cdn.lanqb.com/js/sensors/sensorsdata.min.js',
    name: 'sensors',
    server_url: sensor_server_url,
    heatmap: {
        clickmap: 'default',
        scroll_notice_map: 'default',
        collect_element: function(element_target) {
            // 如果這個元素有屬性sensors-disable=true時候,不採集
            return element_target.getAttribute('sensors-disable') === 'true' ? false : true
        }
    },
    send_type: 'ajax',
    show_log: true,
});
var anonymous_id = null;
sensors.quick('isReady', function() {
    var user_type = null,
        roleArr = [];
    anonymous_id = sensors.quick('getAnonymousID');
    if (window.auth_user && window.auth_user.id) {
        sensors.login(window.auth_user.id);
        // 獲取使用者身份
        var roles = window.auth_user.roles;
        if (typeof roles != "object") return;
        for (var k in roles) {
            user_type = '註冊使用者'
        }
    }
    sensors.registerPage({ //設定公共屬性
        platform_type: window.form_type,
        user_type: function() {
            return user_type ? user_type : '訪客';
        },
    })

    sensors.quick('autoTrack'); //頁面瀏覽事件($pageview)
});


function ssCustomTrack(ele_t, ele_type, class_name, content) {
    sensors.quick('trackHeatMap', ele_t, { //非a,input,button元素的點選採集
        '$element_type': ele_type,
        '$element_class_name': class_name,
        '$element_content': content
    });
}

function sensorTrack(event_name, prop_obj) {
    sensors.track(event_name, prop_obj)
}

複製程式碼