如何搭建应用监控项目

183 阅读13分钟

技术调研

实现应用监控项目,需要使用 node框架 和 数据库

Node.js 框架

框架名称Express.jsKoa.jsEgg.jsMidwayJSNest.js
特点灵活、简单,适合快速搭建Web应用程序和API简洁、高效,提供强大的中间件支持基于Koa.js,提供了丰富的插件生态系统和企业级特性。基于Egg.js,提供了企业级工具和插件,适用于构建中大型的Node.js应用程序基于TypeScript,模块化、可扩展、易于维护,强调依赖注入和模块化架构
优点灵活性高,中间件丰富,适用于各种规模的项目简洁、高效,适用于构建高性能的Web应用程序稳定的基础架构、丰富的插件生态系统,适用于中大型项目。TypeScript和IoC概念、提高开发效率,适用于构建中大型的Node.js应用程序强大的依赖注入系统、模块化架构设计,适合构建复杂、可扩展的应用程序
缺点灵活性带来的选择困难,需要开发者自行选择和配置中间件,可能导致项目的复杂性增加。相对较新的框架,可能缺乏一些成熟的插件和解决方案,有时候需要开发者自行解决问题。配置较复杂,有时候需要花费一些额外的时间来配置插件和理解其运行机制。作为基于Egg.js的框架,可能会继承Egg.js的一些复杂性,需要更多的学习和适应时间。学习曲线较陡,需要时间来熟悉其概念和实践,可能会增加团队的学习成本。
使用场景快速原型开发、小型到中型的Web应用程序、API开发需要高性能、简洁的项目、异步操作和错误处理较多的场景。1. 中大型的Web应用程序、需要多进程管理、插件扩展的项目。1. 企业级Node.js应用程序、希望利用TypeScript和IoC概念的团队。面向企业级应用程序、复杂、可扩展的应用程序。
应用案例Twitter、Uber、MySpace等均使用Express.js构建部分服务。Alibaba、Baidu等在部分项目中使用Koa.js阿里巴巴的前端监控系统、知乎的后端服务等均使用Egg.js构建。MidwayJS广泛应用于企业级应用的开发中,如金融系统、大型电商平台等。Nest.js适用于企业级应用程序的开发,如SaaS平台、管理系统等。

数据库选择

数据库名称MongoDBPostgreSQLMySQLRedisElasticsearch
特点NoSQL数据库,JavaScript和Node.js的配合非常良好,使用BSON格式存储数据。关系型数据库,提供了丰富的功能和数据完整性的支持关系型数据库,具有良好的性能和稳定性内存数据库,用作缓存和快速数据访问的存储搜索引擎和分布式文档存储,适合全文搜索和日志分析
优点灵活的模式设计、高可扩展性、适用于文档型数据。稳定、成熟的数据库系统、丰富的数据类型和扩展性。成熟的数据库系统、广泛的支持和社区高速的数据访问、支持丰富的数据结构和数据类型。强大的全文搜索功能、分布式的存储和搜索引擎
缺点不支持复杂事务内存占用较大较高的维护成本、不易扩展配置和管理相对复杂一些。较高的维护成本、不易扩展不适合大规模数据数据量受限于内存大小、持久性较差(Redis默认不支持持久化,即数据在断电时会丢失)牺牲了一致性和复杂查询能力不适合复杂的事务处理。
使用场景社交网络应用:存储用户信息、帖子、评论等动态数据。实时分析:处理大规模实时数据,进行统计和分析。日志存储:存储大量日志数据,并支持快速检索和分析。需要严格数据完整性和一致性的应用,如金融系统、电子商务平台等。复杂查询和事务处理:适用于需要复杂查询和事务处理逻辑的应用。数据分析和报表生成:PostgreSQL提供丰富的数据分析功能,适合用于生成报表和进行数据分析。适合需要高性能和稳定性的应用程序Web应用程序:MySQL常用于存储用户数据、文章信息等Web应用程序的数据。小型至中型规模的系统:对于小型至中型规模的系统,MySQL提供了稳定的数据存储和处理能力。适合需要快速数据访问和缓存的应用程序。缓存:Redis可以用作高速缓存,提高Web应用程序和数据库之间的性能。消息队列:Redis可以用作高性能的消息队列,实现实时处理和数据传输。适合需要全文搜索和大规模文档存储的应用程序。全文搜索应用:适用于需要强大全文搜索功能的应用,如文档管理、电子商务搜索等。实时日志分析:用于实时索引和分析大量日志数据。监控和指标分析:适用于实时监控系统性能指标、用户行为分析等。
应用案例eBay:使用MongoDB存储产品目录、用户信息等数据。The Weather Channel:利用MongoDB存储实时天气数据和用户偏好信息。Instagram:使用PostgreSQL存储用户数据、图片信息等。Uber:PostgreSQL用于存储用户行程数据、支付信息等。WordPress:MySQL作为其默认数据库,用于存储博客内容、用户信息等。Facebook:虽然Facebook已经迁移到自己的存储解决方案,但在早期阶段使用了MySQL作为其数据库存储引擎。Wikipedia:使用Elasticsearch实现全文搜索和查询功能。Stack Overflow:使用Elasticsearch支持站内搜索功能。

SDK 包调研

打包方式WebpackRollupParcel
特点Webpack 是一个功能强大的模块打包工具,它可以将多个 JavaScript 文件和其他资源(如 CSS、图片等)打包成一个或多个输出文件Rollup 是另一个流行的 JavaScript 打包工具是一个轻量级的打包工具,它专注于快速开发和简化配置
优点它支持代码拆分、按需加载、代码压缩等功能生态丰富,大部分功能可以使用插件实现它也可以将多个模块打包成一个或多个输出文件,支持代码拆分它可以自动处理依赖关系,并支持代码拆分、热重载和自动刷新等功能,完全零配置
使用场景可以满足复杂的打包需求Rollup 更适合构建库和工具,它可以生成适用于浏览器和 Node.js 环境的输出文件适合小型项目或快速原型开发
调研文档 webpack 中文文档 Rollup 快速入门,JDK 打包必备Parcel中文网

前端监控内容梳理

错误监控

错误类型

语法错误

语法错误一般在可发阶段就可以发现,比如常见的单词拼写错误,中英文符号错误等。注意:语法错误是无法被try catch捕获的,因为在开发阶段就能发现,所以一般不会发布到线上环境。

 try {
     let name = 'heima; // 少一个单引号
     console.log(name);
   } catch (error) {
     console.log('----捕获到了语法错误-----');
  }

同步错误

同步错误指的是在js同步执行过程中的错误,比如变量未定义,是可以被try catch给捕获到的

   try {
     const name = 'heima';
     console.log(nam);
   } catch (error) {
     console.log('------同步错误-------')
   }

异步错误

  • 异步错误指的是在setTimeout等函数中发生的错误,是无法被try catch捕获到的
 try {
     setTimeout(() => {
       undefined.map();
     }, 0);
   } catch (error) {
     console.log('-----异步错误-----')
   }
  • 异步错误的话我们可以用window.onerror来进行处理,这个方法比try catch要强大很多
 /**
    * @param {String}  msg    错误描述
    * @param {String}  url    报错文件
    * @param {Number}  row    行号
    * @param {Number}  col    列号
    * @param {Object}  error  错误Error对象
    */
    window.onerror = function (msg, url, row, col, error) {
      console.log('出错了!!!');
      console.log(msg);
      console.log(url);
      console.log(row);
      console.log(col);
      console.log(error);
   };

promise错误

promise 中使用 catch 可以捕获到异步的错误,但是如果没有写 catch 去捕获错误的话 window.onerror 也捕获不到的,所以写 promise 的时候最好要写上 catch ,或者可以在全局加上 unhandledrejection 的监听,用来监听没有被捕获的promise错误。

window.addEventListener("unhandledrejection", function(error){
     console.log('捕获到异常:', error);
}, true);

资源加载错误

资源加载错误指的是比如一些资源文件获取失败,可能是服务器挂掉了等原因造成的,出现这种情况就比较严重了,所以需要能够及时的处理,网路错误一般用 window.addEventListener 来捕获。

 window.addEventListener('error', (error) => {
     console.log(error);
  }, true);

SDK错误监控的实现,就是围绕这几种错误实现的

  • try-catch 用来在可预见情况下监控特定的错误;

  • window.onerror 主要是来捕获预料之外的错误,比如异步错误

  • unhandledrejection 监听来捕获promise错误

  • error 监听捕获资源加载的错误。

用户埋点统计

监控用户在应用的动作以及表现,分析用户行为,并记录页面访问时长,且制定之后产品的迭代优化等,对于产品后续的发展起着重要作用。埋点又分为手动埋点和无痕埋点。

手动埋点

手动埋点就是手动的在代码里面添加相关的埋点代码,比如用户点击某个按钮,就在这个按钮的点击事件中加入相关的埋点代码,或者提交了一个表单,就在这个提交事件中加入埋点代码。

  • 优点:可控性强,可以自定义上报具体的数据。
  • 缺点:对业务代码侵入性强,如果有很多地方需要埋点就得一个一个手动的去添加埋点代码。
// 方式1
<button
  onClick={() => {
    // 业务代码
          tracker('click', '用户去支付');
    // tracker('visit', '访问新页面');
    // tracker('submit', '提交表单');
  }}
>手动埋点</button>
<button
        data-target="支付按钮"
        onClick={() => {
    // 业务代码
  }}
>手动上报</button>

无痕埋点

无痕埋点是为了解决手动埋点的缺点

实现一种不用侵入业务代码就能在应用中添加埋点监控的埋点方式。

  • 优点:不用侵入务代码就能实现全局的埋点。
  • 缺点:只能上报基本的行为交互信息,无法上报自定义的数据;上报次数多,服务器性能压力大。
<button onClick={() => {
  // 业务代码
}}>自动埋点</button>
  • 通过监听 click 事件
// 自动埋点实现
function autoTracker () {
  // 添加全局click监听
  document.body.addEventListener('click', function (e) {
    const clickedDom = e.target;
    // 获取data-target属性值
    let target = clickedDom?.getAttribute('data-target');
    if (target) {
      // 如果设置data-target属性就上报对应的值--手动埋点
      tracker('click', target);
    } else {
      // 如果没有设置data-target属性就上报被点击元素的html路径
      const path = getPathTo(clickedDom);
      tracker('click', path);
    }
  }, false);
};
  1. PV统计

PV即页面浏览量,用来表示该页面的访问数量

在SPA应用之前只需要监听 onload 事件即可统计页面的PV。 在SPA应用中,页面路由的切换完全由前端实现。 主流的react和vue框架都有自己的路由管理库,而单页路由又区分为 hash 路由和 history 路由,

history路由

history路由依赖全局对象 history 实现的,包含以下方法

  • history.back(); // 返回上一页,和浏览器回退功能一样
  • history.forward(); // 前进一页,和浏览器前进功能一样
  • history.go(); // 跳转到历史记录中的某一页, eg: history.go(-1); history.go(1)
  • history.pushState(); // 添加新的历史记录
  • history.replaceState(); // 修改当前的记录项

history路由的实现主要依赖的就是 pushStatereplaceState 来实现的,但是这两种方法不能被 popstate 监听到,所以需要对这两种方法进行重写来实现数据的采集。

/**
 * 重写pushState和replaceState方法
 * @param {*} name
 * @returns
 */
const createHistoryEvent = function (name) {
  // 拿到原来的处理方法
  const origin = window.history[name];
  return function(event) {
    if (name === 'replaceState') {
      const { current } = event;
      const pathName = location.pathname;
      if (current === pathName) {
        let res = origin.apply(this, arguments);
        return res;
      }
    }

    let res = origin.apply(this, arguments);
    let e = new Event(name);
    e.arguments = arguments;
    window.dispatchEvent(e);
    return res;
  };
};

window.history.pushState = createHistoryEvent('pushState');
window.history.replaceState = createHistoryEvent('replaceState');

function listener() {
  const stayTime = getStayTime(); // 停留时间
  const currentPage = window.location.href; // 页面路径
  lazyReport('visit', {
    stayTime,
    page: beforePage,
  })
  beforePage = currentPage;
}

// history.go()、history.back()、history.forward() 监听
window.addEventListener('popstate', function () {
  listener()
});

// history.pushState
window.addEventListener('pushState', function () {
  listener()
});

// history.replaceState
window.addEventListener('replaceState', function () {
  listener()
});

hash路由

url上hash的改变会出发 hashchange 的监听, 所以我们只需要在全局加上一个监听函数,在监听函数中实现采集并上报就可以了。 但是在react和vue中,对于hash路由的跳转并不是通过 hashchange 的监听实现的, 而是通过 pushState 实现,所以,还需要加上对 pushState 的监听才可以。

export function hashPageTrackerReport() {
  let beforeTime = Date.now(); // 进入页面的时间
  let beforePage = ''; // 上一个页面
  // 上报
  function listener() {
    const stayTime = getStayTime();
    const currentPage = window.location.href;
      lazyReport('visit', {
      stayTime,
      page: beforePage,
    })
    beforePage = currentPage;
  }

  // hash路由监听
  window.addEventListener('hashchange', function () {
    listener()
  });
}

UV统计

UV统计的是一天内访问该网站的用户数,uv统计比较简单,就只需要在SDK初始化的时候上报一条消息就可以了

/**
 * 初始化配置
 * @param {*} options
 */
function init(options) {
  ... // 加载配置
  report('user', '加载应用'); // uv统计
}

数据上报

上报方式xhr接口请求img标签sendBeacon
特点跟请求其他业务接口一样img标签的方式是通过将埋点数据伪装成图片URL的请求方式sendBeacon 方法会异步地将少量数据发送到服务器,即使用户正在离开页面或关闭浏览器,也会尽力完成发送。由于 sendBeacon 不会阻塞页面卸载过程,因此适合用于发送一些监测数据或日志,而不会影响用户体验。
优点最简单的避免了跨域的问题可靠性高:即使在页面卸载时也会尽力发送数据。不会阻塞页面卸载:不会影响用户体验。适合发送少量数据:适合用于发送少量数据,比如监测数据或日志。
缺点存在跨域的问题刷新或者重新打开新页面,可能会造成埋点数据的缺失浏览器对url的长度会有限制,不适合大数据量上报存在刷新或者打开页面的时候上报的数据丢失在某些浏览器上存在兼容性的问题- Chrome: 从 Chrome 13 版本开始支持 sendBeacon 方法。
  • Firefox: 从 Firefox 18 版本开始支持 sendBeacon 方法。
  • Safari: 从 Safari 10.1 版本开始支持 sendBeacon 方法。
  • Edge: 从 Edge 79 版本开始支持 sendBeacon 方法。
  • Internet Explorer: sendBeacon 方法在 Internet Explorer 中不受支持。 |

在日常的开发场景中,通常采用sendBeacon上报和img标签上报结合的方式

* 上报
 * @param { } type
* * @param {*} params
 */
export function report(type, params) {
  const appId = window['_monitor_app_id_'];
  const userId = window['_monitor_user_id_'];
  const url = window['_monitor_report_url_'];

  const logParams = {
    appId, // 项目的appId
    userId,
    type, // error/action/visit/user
    data: params, // 上报的数据
    currentTime: new Date().getTime(), // 时间戳
    currentPage: window.location.href, // 当前页面
    ua: navigator.userAgent, // ua信息
  };

  let logParamsString = JSON.stringify(logParams);

  if (navigator.sendBeacon) { // 支持sendBeacon的浏览器
    navigator.sendBeacon(url, logParamsString);
  } else { // 不支持sendBeacon的浏览器
    let oImage = new Image();
    oImage.src = `${url}?logs=${logParamsString}`;
  }
}

合并上报

对于无痕埋点来说,一次点击就进行一次上报对服务器来说压力有点大,所以最好是能进行一个合并上报。

// cache.js
const cache = [];

export function getCache() {
  return cache;
}

export function addCache(data) {
  cache.push(data);
}

// lazyReport.js
export function lazyReport(type, params) {
  // ....
  const data = getCache();

  if (delay === 0) { // delay=0相当于不做延迟上报
    report(data);
    return;
  }

  if (data.length > 10) { // 数据达到10条上报
    report(data);
    clearTimeout(timer);
    return;
  }

  clearTimeout(timer);
  timer = setTimeout(() => { // 合并上报
    report(data);
  }, delay);
}