技术调研
实现应用监控项目,需要使用 node框架 和 数据库
Node.js 框架
| 框架名称 | Express.js | Koa.js | Egg.js | MidwayJS | Nest.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平台、管理系统等。 |
数据库选择
| 数据库名称 | MongoDB | PostgreSQL | MySQL | Redis | Elasticsearch |
|---|---|---|---|---|---|
| 特点 | 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 包调研
| 打包方式 | Webpack | Rollup | Parcel |
|---|---|---|---|
| 特点 | 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);
};
-
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路由的实现主要依赖的就是 pushState 和 replaceState 来实现的,但是这两种方法不能被 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);
}