chenfei-hnu / Blog

个人整理的跟前端相关的文档 ( This is some front-end development related documentation )
9 stars 2 forks source link

前端监控 #61

Open chenfei-hnu opened 2 years ago

chenfei-hnu commented 2 years ago

1.为什么需要前端监控

当需求方案存疑时,结合数据给需求提出建议 当发生线上问题时,快速找到关键信息以定位问题 当想提升前端体验时,怎主动发现优化空间

2.基础需求

从业务理解来说,我们需要知道用户的使用情况,包括PV、UV、访问时段、访问时长等。 从问题排障来说,我们需要知道用户的使用历史,包括用户发出的接口请求、页面报错等。 从体验优化来说,我们需要知道真实的性能数据,包括页面加载和资源加载的耗时。

可选方案

1.Sentry 不实用 2.阿里云ARMS 太贵 3.岳鹰 不能私有化部署

自研方案

1.定位

前端工程师观察业务效果(页面访问PV,自定义上报)、分析线上问题(页面访问,接口请求,前端报错)、发现优化空间(页面性能,资源加载)的监控工具

2.模块设计

整体 数据采集 -日志上报 -日志查询

细化 收集 - 处理日志 - 存储 - UI界面
JS SDK - 上报日志 - 处理日志 - HTTP Server - 日志落库 - ElasticSearch - 查询日志 - Kibana

再细化 JS-SDK web/小程序(页面访问日志,资源加载日志,页面性能日志,接口请求日志,前端报错日志,自定义日志)- 上报日志 - HTTP Server - 日志落库 - ElasticSearch,时序数据库 - Kibana(看具体日志,下钻分析),Grafana(整体分析,曲线)

SDK配置

debug 调试模式,将收集到的日志数规打到console(可用于开发阶段的险证) silence 静音模式,会执行湿辑,但不会真正上报日志(可用于测试环境不上报日志) sendPv 是否上报页面PV sendApi 是否上报API接口请求 sendResource 是否上报资源请求 sendError 是否上报js error sendPerf 是否上报页面性能 spa 是否SPA单页应用页面 bizUserld 绑定业务中的用户ID bizUserType 绑定业务中的用户类型 apilgnoreList 需要忽略的api请求列表,要包含protocol协议头, path部分可填可不填['https://restapi.amap.com','https://miao.baidu.com/abdr'] apiBizCodeParser 解析HTTP响应中的业务状态码(非HTTP状态码)

请求方式

new Image().src 优点:简单,兼容性好,没有跨域问题 缺点:URL有长度限制,不利于日志聚合上报

POST 优点:请求的body参数没有长度限制,可以以日志聚合上报 缺点:需要跨域,且页面退出时POST请求发不出去

WebSocket 优点:只建立一次链接,后续日志上报的性能较好 缺点:对上报的服务器压力大,容易连接数溢出

最终使用POST:延迟上报 + 聚合上报 + 重试机制 + 退出兜底 sendBeacon

步骤 1.main 2.initSDK 3.network,error,resource,pageview,pert,custom 4.sender 先定义一些全局变量 const win = window; const siliceFn = [].slice.call;

接口监控

watchApi

export default function(config: SdkConfig) {
    const proto = win.XMLHttpRequest.prorotype;
    const originalOpen = proto.open;
    const originalSend = proto.send;

    proto.open = function (mthod: string, url: string){
        //可以加定制逻辑 省略...
        originalOpen.apply(this,sliceFn.call(arguments) as any);
    }

    proto.send= function(){
    const that = this;
    function handler() {
      if (that.readyState === 4) {
        // 上报 that.responseURL 省略...
      }
    }
    const originalFn = that.onreadystatechange;
    that.onreadystatechange = function () {
       handler.apply(this,sliceFn.call(arguments) as any);
       originalFn && originalFn.apply(this, sliceFn.call(arguments) as any);
    };
    return originalSend.apply(this, sliceFn.call(arguments) as any);
  }
}
JS报错监控
export default function(config: SdkConfig) {
    const originalOnError = win.onerror;
  function errorHandler(message: string, source?: string, lineno?: number,colno?: number, error?: Error) {
        if(originalOnError){
            try {
                originalOnError.call(win, message,source,lineno,colno,error);
            } catch (err) {}
        }
    if(error !== null){
            // 上报错误日志 error.stack 省略...
        }
    }
  // 重写onerror
  window.onerror = function(message,source,lineno,colno,error) {
        errorHandler(message as string,source,lineno,colno,error);
    }
  // 收集未处理的Promise reject
  addEventListener(win,'unhandledrejection',function(evtL PromiseRejectionEvent) {
        const message = evt.reason?.message || evt.reason || evt.type;
    errorHandler(message,undefined,undefined,evt.reason || evt.type);
    })
}
页面PV监控

监听hashchange + 劫持 history

export default function(config: SdkConfig) {
  let lastVisit = '';

  functin onLoad() {
        sender.reportPV();
    lastVisit = location.href;
    }
  function onHashChange() {
        sender.reportPV({
            spa: config.spa,
      from: lastVisit,
        });
    lastVisit = location.href;
    }
  // 监听hash路由
  addEventListener(win, 'hashchange', onHashChange);
  addEventListener(win, 'load', onLoad);

  function hijackHistory(fnName: string) {
        const hostory: any = win.history || {};
    const originnalFn = history[fnName];

    history[fnName] = function(state: object, title: string, newUrl: string) {
            const curUrl = location.href;
      originalFn && originalFn.call(thistory,state,title,newUrl);
      if(newUrl !== curUrl) {
                dispatchCustomEvent('historystatechange', newUrl);
            }
        }
    }
  hijackHistory('pushState');
  hijackHistory('repalceState');
  // 监听history 路由
  addEventListener(win,'historystatechange',onHistoryStateChange);
}
资源加载监控

PerformaceEntry performace.getEntriesByType('resource') 通过initiatorType字段过滤资源类型 压缩体积 压缩变量名,提取公共变量 使用自执行函数的方式打包 life风格 如何支持多端小程序

function getPlatformInfo() {
        // 微信小程序
    if(typeof wx !== 'undefined' && wx.getSystemInfo && !isEmptyObj(wx)) {
        return {env: Env.wx, global: wx};
    }
    // 百度小程序
    if(typeof swan !== 'undefined' && swan.getSystemInfo && !isEmptyObj(swan)) {
      return {env: Env.swan, global: swan};
    }}
    // 支付宝小程序
    if(typeof my !== 'undefined' && my.getSystemInfo && !isEmptyObj(my)) {
      return {env: Env.my, global: my};
    }}
    // 头条小程序
    if(typeof tt !== 'undefined' && my.getSystemInfo && !isEmptyObj(tt)) {
      return {env: Env.tt, global: tt};
    }}
    // 京东小程序
    if(typeof jd !== 'undefined' && my.getSystemInfo && !isEmptyObj(jd)) {
      return {env: Env.jd, global: jd};
    }}
    return {env: '', global: {}};
}
小程序接口监控
function hijackRequest() {
    const originRequest = global.request;
  const newRequest = (options: any) => {
        const originComplete = options.complate;
    const startTime = Date.now();
    // 请求响应劫持
    options.complete = (response: any) => {
            emitter.emit('onRequest',{options,response,startTime});
      originComplete && originComplete(response);
        }
    return originRequest.call(global,options);
    }

  Object.defineProperty(global,'request',{
        value: newRequest,
    configurable: true,
    enumerable: true,
    writable: true
    })
}
小程序页面监控
function hijackOnLoad() {
    const originPage = Page;
  Page = (options: any) => {
        const originPageOnLoad = options.onLoad;
    options.onLoad = function(options: Object) {
            emitter.emit('onLoad');
      originPageOnLoad && originPageOnLoad.call(this,options);
        };
    originPage(options);
    }
}
function hijackOnError() {
    const originApp = App;
  App = (options: any) => {
        const originOnErrror = options.onError;
    options.onError = function(error: any) {
            emitter.emit('onError',error);
      originOnErrror && originOnErrror.call(this,error);
        };
    originApp(options);
    }
}

最佳实践

通过用户ID -》 前端访问日志 -》 命令行工具 -》 页面复现 -》 接口请求 trace 通过下面链路排查who - when - where - what 查看业务状态码成功率的折线图 页面QPS查看页面访问成功,重复访问情况 近期有无新发布 找到对应请求日志,有无报错 筛选一分钟内日志 查看日志参数细节 分析结论 前端监控与告警 稳定情况 10分钟内页面访问QPS增幅大于100%,或降幅大于50%触发告警(针对公司上班人员,在工作时间内进行监控)

总结

为什么需要前端监控

理解业务最快的办法是数据,解决问题的依据也是数据 借助日志和数据,主动争取话语权 选中现有的监控方案 需求:观察上线效果,分析线上问题,发现优化空间 功能组织: 页面访问,接口请求,JS报错,资源加载 其他考虑:费用,私有化部署

前端监控自研

整体设计:数据采集,日志上报,日志查询(ElasticSearch + Kibana + Grafana) SDK的设计:使用方式的定义,日志上报的设计 监控项的采集原理(WEB/小程序):接口监控,JS报错,页面PV,资源加载

如何利用好前端监控

最佳实践:从整体到局部 who - when - where - what 排障案例分析:多看多用,在排查问题的过程中锻炼自己 从前端监控到告警规则:普适规则,因地制宜