by-lenovointer

Front-end

Website

Monitoring

1.为什么需要前端监控

If you cannot measure it, you cannot improve it —— William Thomson

1.为什么需要前端监控

2.前端监控流程

采集

上报

处理

存储

统计

分析

报警

可视化

采集

上报

采集

上报

采集

上报

采集

上报

采集

上报

3.前端监控采集分类

用户行为监控

性能监控

异常监控

3.前端监控采集分类

一、用户行为监控

PV

UV

 useragent

IP

退出率

跳出率

访问时长

用户网络

访问入口

用户行为

点击、拖动

跳转、滚屏

自定义事件

页面访问流程

 操作系统

分辨率

 颜色深度

 客户端语言

 wifi\4G\3G

鼠标位子xy轴

3.前端监控采集分类

一、用户行为监控

跳出率与退出率

3.前端监控采集分类

var $petal;
if(!$petal) $petal = {};

(function ($petal) {

(function() {
    var _id = 1;
    Object.defineProperty(Object.prototype, "__oid_w__", {
      writable: true
    });
    Object.defineProperty(Object.prototype, "__oid__", {
      get: function() {
        if (!this.__oid_w__)
          this.__oid_w__ = _id++;
        return this.__oid_w__;
      },
    });
})();

var original_addEventListener = EventTarget.prototype.addEventListener;
var map_listener = {
  config: {
    count: true
  }
};

function new_addEventListener(type, listener, options) {
  return original_addEventListener.call(
    this,
    type,
    wrapper(this, listener, type, map_listener),
    options
  );
}

function mapid(obj, listener) {
  return obj.__oid__ + '_' + listener.__oid__;
}

function wrapper(target, listener, type, __map__) {
  var __id__ = mapid(target, listener);
  var __item__ = __map__[__id__];
  if (!__item__) {
    __item__ = {};
    __map__[__id__] = __item__;
  }
  __item__.count = 0;
  __item__.type = type;
  return function (evt) {
    if (__item__.callback) __item__.callback(evt);
    if (__map__.config.count) __item__.count ++;
    listener(evt);
  };
}

function switch_off() {
  EventTarget.prototype.addEventListener = original_addEventListener;
}

function switch_on() {
  EventTarget.prototype.addEventListener = new_addEventListener;
}

$petal.evtmonitor = {
  config: function () {
    return map_listener.config;
  },
  count: function (target, listener) {
    var item = map_listener[mapid(target, listener)];
    if (!item) return -1;
    return item.count;
  },
  type: function (target, listener) {
    var item = map_listener[mapid(target, listener)];
    if (!item) return null;
    return item.type;
  },
  addCleanEventListener: function (target, type, listener, options) {
    return original_addEventListener.call(
      target,
      type,
      listener,
      options
    );
  },
  switchOn: switch_on,
  switchOff: switch_off
};

})($petal);

重写addEventListener

3.前端监控采集分类


// 方案一 重写   
    proxyAjax.send = XMLHttpRequest.prototype.send;
    proxyAjax.open = XMLHttpRequest.prototype.open;
    // 重写 open
    XMLHttpRequest.prototype.open = function(){
        // 先在此处取得请求的url、method等信息并记录等处理
        // 调用原生 open 实现重写
        proxyAjax.open.apply(this, arguments);
    }
    // 重写 send
    XMLHttpRequest.prototype.send = function () {
        // 调用原生send
        proxyAjax.send.apply(this, arguments);
        // 在onleadend ontimeout等事件中上报,上报处理函数 handleMonitor
        this.onloadend = function() {
            handleMonitor(someParams)
        }
    }
    // 上报函数
    handleMonitor = function(params) {
        this.send(params)
   }
  

// 方案二 拦截
function ajaxEventTrigger (event) {
    var ajaxEvent = new CustomEvent(event, { detail: this })
    window.dispatchEvent(ajaxEvent)
}

var oldXHR = window.XMLHttpRequest
function newXHR () {
    var realXHR = new oldXHR()
    realXHR.addEventListener('readystatechange', function () { ajaxEventTrigger.call(this, 'ajaxReadyStateChange') }, false)
    return realXHR
}
window.XMLHttpRequest = newXHR
var startTime = 0
var gapTime = 0 // 计算请求延时
window.addEventListener('ajaxReadyStateChange', function (e) {
    var xhr = e.detail
    var status = xhr.status
    var readyState = xhr.readyState
    /**
     * 计算请求延时
     */
    if (readyState === 1) {
        startTime = (new Date()).getTime()
    }
    if (readyState === 4) {
        gapTime = (new Date()).getTime() - startTime
    }
    /**
     * 上报请求信息
     */
     if (readyState === 4) {
        if(status === 200){
            // 接口正常响应时捕获接口响应耗时
            console.log('接口',xhr.responseURL,'耗时',gapTime)
         }else{
            // 接口异常时捕获异常接口及状态码
            console.log('异常接口',xhr.responseURL,'状态码',status)
        }
     }
})

重写XMLHttpRequest

//fetch可定义成widow方法全局覆盖 也可export输出新的fetch模块这是就需要业务方使用新的fetch请求

3.前端监控采集分类

客满部门、营销服务、用户体验

3.前端监控采集分类

二、性能监控​

1、合成监控(Synthetic Monitoring,SYN)

3.前端监控采集分类

优点:

无侵入性。

简单快捷。

缺点:

不是真实的用户访问情况,只是模拟的。

1、合成监控(Synthetic Monitoring,SYN)

3.前端监控采集分类

2、真实用户监控(Real User Monitoring,RUM)

* 使用标准的 API;

* 定义合适的指标;

* 采集正确的数据;

* 上报关联的维度; 

3.前端监控采集分类

performance.timing

2、真实用户监控(Real User Monitoring,RUM)

3.前端监控采集分类

2、真实用户监控(Real User Monitoring,RUM)

// 1秒 = 1000 毫秒 = 1000000微秒 = 1000000000纳秒
Date.now()
performance.timeOrigin
performance.now()

3.前端监控采集分类

2、真实用户监控(Real User Monitoring,RUM)

performance.getEntries()

/**
 * mark、measure、navigation、resource、paint、frame
 */
performance.getEntriesByType()


performance.getEntriesByName()



const observer = new PerformanceObserver(function (list) {
  // 当记录一个新的性能指标时执行
})
// 注册长任务观察者
observer.observe({entryTypes: ['longtask']})

3.前端监控采集分类

2、真实用户监控(Real User Monitoring,RUM)

performance.getEntriesByType('navigation')

3.前端监控采集分类

2、真实用户监控(Real User Monitoring,RUM)

基于 performance 我们可以测量如下几个方面:

mark、measure、navigation、resource、paint、frame。
performance.getEntriesByType('navigation')


重定向次数:performance.navigation.redirectCount
进入形式:performance.navigation.type

let p = performance.getEntries();
JS 资源数量: p.filter(ele => ele.initiatorType === "script").length
CSS 资源数量:p.filter(ele => ele.initiatorType === "css").length
AJAX 请求数量:p.filter(ele => ele.initiatorType === "xmlhttprequest").length
IMG 资源数量:p.filter(ele => ele.initiatorType === "img").length

总资源数量: window.performance.getEntriesByType("resource").length

内存使用:performance.memory

不重复的耗时时段区分:

重定向耗时: redirectEnd - redirectStart

DNS 解析耗时: domainLookupEnd - domainLookupStart

TCP 连接耗时: connectEnd - connectStart

SSL 安全连接耗时: connectEnd - secureConnectionStart

网络请求耗时 (TTFB): responseStart - requestStart

HTML 下载耗时:responseEnd - responseStart

DOM 解析耗时: domInteractive - responseEnd

资源加载耗时: loadEventStart - domContentLoadedEventEnd


其他组合分析:

白屏时间: domLoading - fetchStart

粗略首屏时间: loadEventEnd - fetchStart 或者 domInteractive - fetchStart

DOM Ready 时间: domContentLoadEventEnd - fetchStart

页面完全加载时间: loadEventStart - fetchStart


JS 总加载耗时:

const p = window.performance.getEntries();

let jsR = p.filter(ele => ele.initiatorType === "script");

Math.max(...jsR.map((ele) => ele.responseEnd)) - Math.min(...jsR.map((ele) => ele.startTime));


CSS 总加载耗时:

const p = window.performance.getEntries();

let cssR = p.filter(ele => ele.initiatorType === "css");

Math.max(...cssR.map((ele) => ele.responseEnd)) - Math.min(...cssR.map((ele) => ele.startTime));


const observer = new PerformanceObserver(function (list) {
  // 当记录一个新的性能指标时执行
})
// 注册长任务观察者
observer.observe({entryTypes: ['longtask']})

3.前端监控采集分类

优点:

是真实用户访问情况。

可以观察历史性能趋势。

有一些额外的功能:报表推送、监控告警等等。

缺点:

有侵入性,会一定程度上响应 web 性能。 

2、真实用户监控(Real User Monitoring,RUM)

3.前端监控采集分类

三、异常监控​

数据类型错误

逻辑错误

语法句法错误

网络错误

系统错误

3.前端监控采集分类

1、逻辑错误、数据类型

    * try...catch                                              catch error的监听

    * window.onerror                                     js error监听

    * window.onunhandledrejection              promise reject 的异常监听

// 相关的js文件上加上Access-Control-Allow-Origin:*的response header
// 引用相关的js文件时加上crossorigin属性
window.onerror = function(message, source, lineno, colno, error) { 
	console.log('捕获到异常:', { message, source, lineno, colno,error });
}
Vue.config.errorHandler = function (err, vm, info) {
    let msg = `错误发生在:${info}中,具体信息:${err.stack}`
    console.log(msg)
}

3.前端监控采集分类

2、网络错误

*  window.addEventListener('error',function(e){},true)

* performance.getEntries()

* object.onerror: dom对象的onerror事件

* Error事件捕获

* 重写window.XMLHttpRequest 和 window.fetch 捕获请求错误

<script src="//mockuai.com/test.js"></script>
window.addEventListener('error',function(e){
    const err = e.target.src || e.target.href
    if(err){
        console.log('捕获到资源加载异常',err)
    }
},true)
//最后allIms和loadedImgs对比即可找出图片资源未加载项目 

 var allImgs = document.getElementsByTagName('script')
 var loadedImgs = performance.getEntries().filter(i => i.initiatorType == 'script')
<img src="image.gif" onerror="alert('图片找不到,无法加载!!')" />

4.前端埋点方案

1、埋点方案:代码手动埋点

优点:

* 可以在任意时刻,精确的发送或保存所需要的数据信息

缺点:

*监控埋点逻辑嵌入业务

* 工作量较大,每一个埋点的新增和变更都需要添加相应的代码

  // 页面加载时发送埋点请求
$(document).ready(function(){
 // ... 这里存在一些业务逻辑
 sendRequest(params);
});
// 按钮点击时发送埋点请求
$('button').click(function(){
   //  这里存在一些业务逻辑
   sendRequest(params);
});

4.前端埋点方案

2、埋点方案:声明式埋点

优点:减小代码冗余解决代码耦合

缺点:需要手动在需要埋点的节点中添加指令

// key表示埋点的唯一标识;act表示埋点方式
<button data-stat="{key:'111', act: 'click'}">埋点</button>
// 自定义指令
<div v-track:2001.click="goOrderList" class="order-list">

4.前端埋点方案

3、埋点方案:可视化埋点

优点:

* 方便产品等非技术人员元配置使用获得所需要的用户行为信息

缺点:

* 可视化埋点可以埋点的控件有限,不能手动定制

4.前端埋点方案

4、埋点方案:无痕埋点

优点:

* 采集的是全量数据,所以产品迭代过程中是不需要关注埋点逻辑的,也不会出现漏埋、误埋等现象

缺点:

* 无埋点采集全量数据,给数据传输和服务器增加压力

* 无法灵活的定制各个事件所需要上传的数据

5.上报方案

1、上报周期

 

  • 基于时间间隔:每隔 n秒(时间间隔可以根据业务情况自定义)
  • 基于数据条数:每累积 n条数据(条数可以自定义)
  • 基于数据量大小
  • 命中概率上报
  • 不间断实时上报,如果是低频率,数据量小,实时性要求高的数据可以不设限制
  • 重试机制上报

5.上报方案

2、上报形式

navigator.sendBeacon(url, data);

5.上报方案

2、上报形式

.button-1:active::after {
    content: url(./pixel.gif?action=click&id=button1);
    display: none;
}

.any-element {
   background: url(./pixel.gif?css=2);
   background-image: url(./pixel.gif?css=3), none;
}

css 上报

6.参考

thanks!

Made with Slides.com