Xây dựng một hệ thống tracking hành vi người dùng (phần 2)

Các bạn có thể xem lại phần 1 tại đây.
Sau khi đã có được các tag tiến hành xây dựng các Helper để hỗ trợ việc render ra file tracking.js
Helper này có nhiệm vụ xử lý các logic của file tracking.js nhưng nếu xử lý trực tiếp trên file js thì nó quá dài dòng và dễ bị lộ logic của file.
JS helper

 module JsHelper  
  def renderBindClicks(tags)  
   click_tags = tags.inject([]) do |click_tags, tag|  
    if tag&.trigger[:value] == 'click'  
     event_type = tag&.selector[:dom]  
     selector = if event_type == "classes element"  
      ".#{tag&.selector[:value]}"  
     elsif event_type == "id element"  
      "\##{tag&.selector[:value]}"  
     end  
     if selector.present?  
      click_tags << "'#{selector}'"  
     end  
    end  
    click_tags  
   end  
   if click_tags.present?  
    return "[#{click_tags.uniq.join(', ')}].forEach(bindClick);".html_safe  
   end  
  end  
 def renderBindClickExtend(tags)  
  selector = nil  
  attribute = nil  
  click_tags = []  
  tags.each do |tag|  
   obj_temp = {}  
   if tag&.trigger[:value] == 'click'  
    if tag&.handle[:value] == 'other' && !tag&.handle[:other_handle].blank?  
     attribute = tag&.handle[:other_handle].chomp  
     event_type = tag&.selector[:dom]  
     if event_type == "classes element"  
      selector = ".#{tag&.selector[:value]}"  
     elsif event_type == "id element"  
      selector = "\##{tag&.selector[:value]}"  
     end  
     obj_temp["attr"] = attribute  
     obj_temp["selector"] = selector  
     click_tags.push(obj_temp)  
    else  
     event_type = tag&.selector[:dom]  
     if event_type == "classes element"  
      selector = ".#{tag&.selector[:value]}"  
     elsif event_type == "id element"  
      selector = "\##{tag&.selector[:value]}"  
     end  
     obj_temp["selector"] = selector  
     click_tags.push(obj_temp)  
    end  
   end  
  end  
  # binding.pry  
  return "#{click_tags.to_json}.forEach(bindClickExtend);".html_safe  
 end  
  def renderImport(tags)  
   import_tag = tags.inject([]) do |import_tags, tag|  
    trigger_other_value = if tag&.trigger[:value] == 'other'  
      "#{tag&.trigger[:other_value]}"  
    end  
    if trigger_other_value.present?  
     import_tags.push(trigger_other_value)  
    end  
    import_tags  
   end  
   if import_tag.present?  
    return "#{import_tag.uniq.join("\n")}".html_safe  
   end  
  end  
  def renderBindFocus(tags)  
   focus_tags = tags.inject([]) do |focus_tags, tag|  
    if tag&.trigger[:value] == 'focus'  
     event_type = tag&.selector[:dom]  
     selector = if event_type == "classes element"  
      ".#{tag&.selector[:value]}"  
     elsif event_type == "id element"  
      "\##{tag&.selector[:value]}"  
     end  
     if selector.present?  
      focus_tags << "'#{selector}'"  
     end  
    end  
    focus_tags  
   end  
   if focus_tags.present?  
    return "bindFocus([#{focus_tags.uniq.join(', ')}]);".html_safe  
   end  
  end  
  # Generate debug command for javascript  
  def debug(msg, param = nil)  
   if Rails.env.development?  
    if param && msg  
     return "debug(\"#{msg}\" + #{param});"  
    elsif msg  
     return "debug(\"#{msg}\");"  
    elsif param  
     return "debug(#{param});"  
    end  
   end  
   return nil  
  end  
  # Generate debug function for javascript  
  def debug_js_function  
   Rails.env.development? ?  
   "var debug = function(obj){\n\  
    if (typeof(obj) == 'string')\n\  
     console.debug('Spymaster: ' + obj);\n\  
    else\n\  
     console.debug(obj);\n\  
   };" : nil  
  end  
  # Render a javascript file  
  def render_part(name, assigns = {})  
   # fpath = Dir.glob(Rails.root.join("client-api/js/components/_#{name}.*")).first  
   # return nil if not fpath  
   # template = File.new(fpath).read  
   # result = ERB.new(template).result(OpenStruct.new(assigns).instance_eval { binding })  
   # return result  
   render partial: name, locals: assigns  
  end  
  def domain  
   Rails.env.development? ? 'http://localhost:3000' : 'https://tracking.pedia.vn'  
  end  
 end  

Để render ra file javascript sử dụng file .erb.File .erb trong ruby cũng tương tự như các .jsp trong java .php nó cho phép nhúng code ruby vào html(trong trường hợp này là nhúng code ruby vào javascript.Để tránh làm file này bị phình quá to nên split nhỏ file này ra thành các file khác và file này chỉ viết những logic quan trọng và include các file js khác vào.

RadaTracking


 <%= render partial: 'tagmanager/components/rada_tracking' %>  
 <%= render partial: 'tagmanager/components/scroll_tracking' %>  
 <%= render partial: 'tagmanager/components/action_cookie' %>  
 var RadaTracking = (function() {  
  var rada_info = {  
   <% if Rails.env.development? %>  
    url_api:"//localhost:3000/api/tracking/rada_track",  
    request_cid:"//localhost:3000/api/tracking/generate_client_id",  
   <% elsif Rails.env.staging? %>  
    url_api: "//tcs-rada-reporter-staging.ingress.v2.cloud.edumall.io/api/tracking/rada_track",  
    request_cid: "//tcs-rada-reporter-staging.ingress.v2.cloud.edumall.io/api/tracking/generate_client_id",  
   <% elsif Rails.env.production? %>  
    url_api:"//rada-reporter.edumall.io/api/tracking/rada_track",  
    request_cid:"//rada-reporter.edumall.io/api/tracking/generate_client_id",  
   <% end %>  
    method: "post"  
  }  
  var app_info = {  
   name: <%= "'#{@app_name}'".html_safe %>,  
   version: "beta",  
   // user: $('.data_user').val(),  
  }  
  //set expires for cookie is 10h  
  var cookie_info = {  
   name: "rada_tracking",  
   expires: 10  
  }  
  //init data for track  
  function build_data_for_track(){  
   var time_start = new Date().getTime();  
   var storage_key = "rada" + time_start;  
   var dataTemp = {  
    storage_key: storage_key,  
    time_start: time_start,  
    app_name: app_info.name,  
    app_version: app_info.version,  
    referer:document.referrer,  
    url: window.location.href,  
    client_os: RadaReporter.get_device_info().os_name,  
    client_browser: RadaReporter.get_browser_info().name,  
    type_track:"",  
    user: app_info.user,  
    extras: {},  
   }  
   return dataTemp;  
  }  
  //push data to localStorage  
  function push_data_to_lc(name,data){  
   data_json = JSON.stringify(data);  
   localStorage.setItem(name,data_json);  
  }  
  //transformation data before send data to server  
  function tranform_data(params){  
   var data_tranform = [];  
   var substring = "rada";  
   for (i = 0; i < params.length; i++)  {  
    var obj_temp = {};  
    if(params.key(i).indexOf(substring) !== -1){  
     obj_temp.key = params.key(i);  
     obj_temp.data = JSON.parse(params.getItem(params.key(i)));  
     data_tranform.push(obj_temp)  
    }  
   }  
   return data_tranform;  
  }  
  //after send data to server then delete old data  
  function delete_old_localStorage(params){  
   for (i = 0; i < params.length; i++) {  
    localStorage.removeItem(params[i]["key"]);  
   }  
  }  
  function send_data_to_server (){  
   var data_send_server = {};  
   var data_local_storage = tranform_data(localStorage);  
   if(getValueCookie("rada_tracking")){  
    data_send_server.client_id = getValueCookie("rada_tracking");  
   }else{  
    generate_client_id();  
    data_send_server.client_id = "Anonymous";  
   }  
   data_send_server.source = JSON.stringify(data_local_storage);  
   // data_send_server.time_send = new Date().getTime();  
   $.ajax({  
    url: rada_info.url_api,  
    type: rada_info.method,  
    data: data_send_server,  
    success: function(response) {  
     delete_old_localStorage(data_local_storage);  
     push_data_to_lc("time_send",new Date().getTime());  
     console.log('save data to server success');  
     //time_send_succes = Time.now  
     //save_time_now_to_local  
    }  
   });  
  }  
  function getValueCookie(name){  
   if(name === undefined){  
    return undefined;  
   }else{  
    var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));  
    if (match) return match[2];  
   }  
  }  
  function generate_client_id(){  
   var check_cookie = getValueCookie("rada_tracking");  
   if(!check_cookie){  
    $.ajax({  
     url: rada_info.request_cid,  
     type: "POST",  
     data: {action:"request_cid"},  
     success: function(response) {  
      var client_id = response.client_id;  
      Action_cookie.set_cookie(cookie_info.name,client_id,cookie_info.expires);  
     }  
    });  
   }  
  }  
  return {  
   generate_cid: generate_client_id,  
   build_data: build_data_for_track,  
   push_data: push_data_to_lc,  
   tranform_data: tranform_data,  
   delete_data: delete_old_localStorage,  
   send_data: send_data_to_server  
  }  
 })();  
 var Focus_tracker = (function() {  
  var checks ={};  
  var check = {};  
  return function(params,callback){  
   $(window).scroll(function() {  
    clearTimeout($.data(this, 'scrollTimer'));  
    $.data(this, 'scrollTimer', setTimeout(function() {  
     for(var i=0;i<params.length;i++){  
      var selectors = $(params[i]);  
      if(selectors.length > 1){  
       for(var x=0;x<selectors.length;x++){  
        var a = selectors[x].getBoundingClientRect().top;  
        if(a >= 0 && a <= window.innerHeight && !checks[x]){  
         callback(params[i]);  
         checks[x] = true;  
        }  
       }  
      }if(selectors.length == 1){  
       var b = selectors[0].getBoundingClientRect().top;  
       if(b >= 0 && b <= window.innerHeight && !check[i]){  
        callback(params[i]);  
        check[i] = true;  
       }  
      }  
     }  
    }, 5000));  
   });  
  }  
 })();  
 var TagConfig = {  
  "SendIntervalInSecs": <%= Rails.env.development? ? 40 : 120 %>,  
  "BeatIntervalInSecs": 5  
 };  
 var RadarTag = (function() {  
  function debug_log(msg) {  
   <% if Rails.env.development? %>  
    console.log(msg);  
   <% end %>  
  }  
  function handleViewPage() {  
   var data = RadaTracking.build_data();  
   data["type_track"] = "page_view";  
   RadaTracking.push_data(data["storage_key"],data);  
   // debug_log(data);  
  }  
  function handleTimeSpent(){  
   var data = RadaTracking.build_data();  
   data["type_track"] = "time_in_page";  
   data["value_track"] = 0;  
   const beatInterval = TagConfig.BeatIntervalInSecs;  
    setInterval(function(){  
    data["value_track"] += beatInterval;  
    RadaTracking.push_data(data["storage_key"],data);  
    // debug_log(data["value_track"]);  
   }, beatInterval * 1000);  
  }  
  //this function is not working  
  function handleClick(params, event) {  
   var data = RadaTracking.build_data();  
   data["type_track"] = "click_in_page";  
   data["value_track"] = params.selector;  
   data["extras"]["btn_name"] = event.target.textContent;  
   RadaTracking.push_data(data["storage_key"],data);  
  }  
  function handleClickExtend(params,event){  
   var data = RadaTracking.build_data();  
   var obj_temp = params.selector;  
   data["type_track"] = "click_in_page";  
   data["value_track"] = obj_temp.selector;  
   data["extras"]["bnt_name"] = event.target.textContent;  
   if(Object.prototype.hasOwnProperty.call(obj_temp,'attr')){  
    data["extras"]["other_value"] = event.target.getAttribute(obj_temp.attr);  
   }  
   RadaTracking.push_data(data["storage_key"],data);  
  }  
  function handleFocus(params){  
   var data = RadaTracking.build_data();  
   data["type_track"] = "focus_in_page";  
   var callback = function(params){  
    data["time_start"] = new Date().getTime();  
    data["value_track"] = params;  
    // debug_log(data);  
    RadaTracking.push_data(data["storage_key"],data);  
   }  
   Focus_tracker(params.selector,callback);  
  }  
  function triggerViewPage(){  
   handleViewPage();  
  }  
  function triggerTimeSpent(){  
   handleTimeSpent();  
  }  
  //this function is not working  
  function triggerClick(params) {  
   try {  
    $(document).on("click", params.selector, function(event) {  
       params.handle(params, event);  
    })  
   } catch (e) {  
    debug_log("some thing when wrong,in triggerClick");  
   }  
  }  
  function triggerClickExtend(params) {  
   try {  
    $(document).on("click", params.selector.selector, function(event) {  
      params.handle(params, event);  
    })  
   } catch (e) {  
    debug_log("some thing when wrong,in triggerClickExtend")  
   }  
  }  
  function triggerFocus(params) {  
   try {  
    params.handle(params);  
   }catch (e) {  
    debug_log("some thing when wrong,in triggerFocus")  
   }  
  }  
 //this function is not working  
  function bindClick(selector) {  
   var params = {};  
   params.selector = selector;  
   params.trigger = triggerClick;  
   params.handle = handleClick;  
   params.trigger(params);  
  }  
  function bindClickExtend(selector){  
   var params = {};  
   params.selector = selector;  
   params.trigger = triggerClickExtend;  
   params.handle = handleClickExtend;  
   params.trigger(params);  
  }  
  function bindFocus(selector) {  
   var params = {};  
   params.selector = selector;  
   params.trigger = triggerFocus;  
   params.handle = handleFocus;  
   params.trigger(params);  
  }  
  function handleScroll(){  
   window.tracker_scroll = window.ScrollTracker();  
   window.tracker_scroll.on({  
    percentages: {  
     every: [50]  
    }  
   }, function(evt) {  
    var data = RadaTracking.build_data();  
    data["type_track"] = "scroll_track";  
    data["value_track"] = evt.data.label;  
    data["extras"] = window.tracker_scroll._show_info();  
    data["extras"]["value_depth"] = evt.data.depth;  
    RadaTracking.push_data(data["storage_key"],data);  
    // debug_log(data);  
   });  
  }  
  function triggerScroll(){  
   handleScroll();  
  }  
  function set_cookie_after_load(){  
   RadaTracking.generate_cid();  
  }  
  function set_current_time(){  
   return new Date().getTime();  
  }  
  return {  
   onReady: function() {  
    set_cookie_after_load();  
    triggerViewPage();  
    triggerTimeSpent();  
    triggerScroll();  
    loopsend = setInterval(function(){  
     var time_send = localStorage.getItem("time_send");  
     var current_time = set_current_time();  
     if(time_send){  
      time_send = JSON.parse(time_send);  
      // console.log((current_time - time_send));  
      if ((current_time - time_send) >= (TagConfig.SendIntervalInSecs * 1000)){  
        RadaTracking.send_data();  
      }  
     }else{  
      RadaTracking.push_data("time_send",current_time);  
     }  
    }, TagConfig.SendIntervalInSecs * 250);  
    // <%= renderBindClicks(@tags) %>  
    <%= renderBindClickExtend(@tags) %>  
    <%= renderImport(@tags) %>  
    <%= renderBindFocus(@tags) %>  
   }  
  }  
 })();  
 $(document).ready(function() {  
  RadarTag.onReady();  
 });  

Trên đây là bài viết tổng quan về xây dựng hệ thống rada tracking,một số vấn đề chuyên sâu về hệ thống này sẽ được viết ở một bài viết khác.

Nhận xét

Bài đăng phổ biến từ blog này

Cài đặt SSL cho website sử dụng certbot

Xây dựng một hệ thống comment real-time hoặc chat đơn giản sử dụng Pusher

CÁC BÀI TẬP SQL CƠ BẢN - PART 1

Xây dựng một hệ thống tracking hành vi người dùng (phần 1)

Enterprise architecture trên 1 tờ A4

Web caching (P2)

Bàn về async/await trong vòng lặp javascript

Web caching (P1)

Cài đặt môi trường để code website Rails