开发者有时会面临上线的生产环境包出现了异常 ,在长期生产bug并修复bug的循环中总结出一下几个痛点:
source map
(开源贡献者除外)错误埋点追踪系统的出现就是为了应对上述问题的解决方案,笔者正好最近接触了不少前端埋点与错误处理的博客内容,按例阶段性产出博客总结一下。
还不了解的同学可以阅读以下文章:
前端-埋点-理念-通识-浅谈
大数据时代数据的重要性不言而喻,而其中最重要的就是用户信息的采集。埋点,无论是项目后期的复盘,还是明确业务价值,还是产品价值的挖掘,都具备很重要的意义。
在ES3之前js代码执行的过程中,一旦出现错误,整个js代码都会停止执行,这样就显的代码非常的不健壮。从ES3开始,js也提供了类似的异常处理机制,从而让js代码变的更健壮,程序执行的过程中出现了异常,也可以让程序具有了一部分的异常恢复能力。js异常的特点是,出现不会导致JS引擎崩溃,最多只会终止当前执行的任务。
回归正题,我们该如何在程序异常发生时捕捉并进行对应的处理呢?在Javascript中,我们通常有以下两种异常捕获机制。
function errFunc() {
// eslint-disable-next-line no-undef
error;
}
function catchError() {
try {
this.errFunc();
} catch (error) {
console.log(error);
}
}
catchError()
复制代码
能捕捉到的异常,必须是线程执行已经进入 try catch 但 try catch 未执行完的时候抛出来的,以下都是无法被捕获到的情形。
try ... catch
包裹,影响代码可读性。面试官:请用一句话描述 try catch 能捕获到哪些 JS 异常
window.onerror
最大的好处就是同步任务、异步任务都可捕获,可以得到具体的异常信息、异常文件的URL、异常的行号与列号及异常的堆栈信息,再捕获异常后,统一上报至我们的日志服务器,而且可以全局监听,代码看起来也简洁很多。
window.onerror
捕获不到正确的异常信息,而是统一返回一个Script error
,可通过在<script>
使用crossorigin
属性来规避这个问题image.png
window.addEventListener('error', function() {
console.log(error);
// ...
// 异常上报
});
throw new Error('这是一个错误');
复制代码
前文已经提到,onerror
以及 try-catch
也无法捕获Promise实例抛出的异常,只能最后在 catch 函数上处理,但是代码写多了就容易糊涂,忘记写 catch。
如果你的应用用到很多的 Promise 实例的话,特别是在一些基于 promise 的异步库比如 axios 等一定要小心,因为你不知道什么时候这些异步请求会抛出异常而你并没有处理它,所以最好添加一个 Promise 全局异常捕获事件 unhandledrejection
。
window.addEventListener("unhandledrejection", e => {
console.log('unhandledrejection',e)
});
复制代码
window.onerror
并不能捕获.vue文件发生的获取,Vue 2.2.0以上的版本中增加了一个errorHandle
,使用Vue.config.errorHandler
这样的Vue全局配置,可以在Vue指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和Vue 实例。
//main.js
import { createApp } from "vue";
import App from "./App.vue";
let app = createApp(App);
app.config.errorHandler = function(e) {
console.log(e);
//错误上报...
};
app.mount("#app");
复制代码
Vue项目JS脚本错误捕获
综上,可以将几种方式有效结合起来,笔者这里是在vue-cli框架中做的处理,其余类似:
import { createApp } from "vue";
import App from "./App.vue";
let app = createApp(App);
window.addEventListener(
"error",
(e) => {
console.log(e);
//TODO:上报逻辑
return true;
},
true
);
// 处理未捕获的异常,主要是promise内部异常,统一抛给 onerror
window.addEventListener("unhandledrejection", (e) => {
throw e.reason;
});
// 框架异常统一捕获
app.config.errorHandler = function(err, vm, info) {
//TODO:上报逻辑
console.log(err, vm, info);
};
app.mount("#app");
复制代码
生产环境下所有的报错的代码行数都在第一行了,为什么呢?
通常在该环境下的代码是经过webpack打包后压缩混淆的代码,否则源代码泄漏易造成安全问题,在生产环境下,我们的代码被压缩成了一行。而保留了sourcemap文件就可以利用webpack打包后的生成的一份.map的脚本文件就可以让浏览器对错误位置进行追踪了,但这种做法并不可取,更为推荐的是在服务端使用Node.js对接收到的日志信息时使用source-map解析,以避免源代码的泄露造成风险
image.png
vue.config.js
配置里通过属性productionSourceMap: true
可以控制webpack是否生成map文件
为了我们每一次构建服务端能拿到最新的map文件,我们编写一个插件让webpack在打包完成后触发一个钩子实现文件上传,在vue.config.js
中进行配置
调整 webpack 配置
//vue.config.js
let SourceMapUploader = require("./source-map-upload");
module.exports = {
configureWebpack: {
resolve: {
alias: {
"@": resolve("src"),
},
},
plugins: [
new SourceMapUploader({url: "http://localhost:3000/upload"})
],
}
// chainWebpack: (config) => {},
}
复制代码
//source-map-upload.js
const fs = require("fs");
const http = require("http");
const path = require("path");
class SourceMapUploader {
constructor(options) {
this.options = options;
}
/**
* 用到了hooks,done表示在打包完成之后
* status.compilation.outputOptions就是打包的dist文件
*/
apply(compiler) {
if (process.env.NODE_ENV == "production") {
compiler.hooks.done.tap("sourcemap-uploader", async (status) => {
// console.log(status.compilation.outputOptions.path);
// 读取目录下的map后缀的文件
let dir = path.join(status.compilation.outputOptions.path, "/js/");
let chunks = fs.readdirSync(dir);
let map_file = chunks.filter((item) => {
return item.match(/\.js\.map$/) !== null;
});
// 上传sourcemap
while (map_file.length > 0) {
let file = map_file.shift();
await this.upload(this.options.url, path.join(dir, file));
}
});
}
}
//调用upload接口,上传文件
upload(url, file) {
return new Promise((resolve) => {
let req = http.request(`${url}?name=${path.basename(file)}`, {
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
Connection: "keep-alive",
},
});
let fileStream = fs.createReadStream(file);
fileStream.pipe(req, { end: false });
fileStream.on("end", function() {
req.end();
resolve();
});
});
}
}
module.exports = SourceMapUploader;
复制代码
两种方式:
这里采用第一种,通过动态创建一个img,浏览器就会向服务器发送get请求。将需要上报的错误数据放在url中,利用这种方式就可以将错误上报到服务器了。
确定上报的内容,应该包含异常位置(行号,列号),异常信息,在错误堆栈中包含了绝大多数调试有关的信息,我们通讯的时候只能以字符串方式传输,我们需要将对象进行序列化处理。
后端接收到信息后进行对应的反向操作,就可以在日志中记录。
1621581164(1).png
function uploadErr({ lineno, colno, error: { stack }, message, filename }) {
let str = window.btoa(
JSON.stringify({
lineno,
colno,
error: { stack },
message,
filename,
})
);
let front_ip = "http://localhost:3000/error";
new Image().src = `${front_ip}?info=${str}`;
}
复制代码
用koa搭一个简单后台服务,代码比较简单,按功能拆开来讲
文件流写入:
router.post("/upload", async (ctx) => {
const stream = ctx.req;
const filename = ctx.query.name;
let dir = path.join(__dirname, "source-map");
//判断source文件夹是否存在
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
let target = path.join(dir, filename);
const ws = fs.createWriteStream(target);
stream.pipe(ws);
});
复制代码
使用log4js
记录我们的错误日志,这个也是非常流行的日志插件了,直接贴代码。
log4js-node
const path = require('path')
const log4js = require('log4js');
log4js.configure({
appenders: {
info: {
type: "dateFile",
filename: path.join(__dirname, 'logs', 'info', 'info'),
pattern: "yyyy-MM-dd.log",
encoding: 'utf-8',
alwaysIncludePattern: true,
},
error: {// 错误日志
type: 'dateFile',
filename: path.join(__dirname, 'logs', 'error', 'error'),
pattern: 'yyyy-MM-dd.log',
encoding: 'utf-8',
alwaysIncludePattern: true
}
},
categories: {
default: { appenders: ['info'], level: 'info' },
info: { appenders: ['info'], level: 'info' },
error: { appenders: ['error'], level: 'error' }
}
});
/**
* 错误日志记录方式
* @param {*} content 日志输出内容
*/
function logError(content) {
const log = log4js.getLogger("error");
log.error(content)
}
/**
* 日志记录方式
* @param {*} content 日志输出内容
*/
function logInfo(content) {
const log = log4js.getLogger("info");
log.info(content)
}
module.exports = {
logError,
logInfo
}
复制代码
这个接口就是对上报的错误信息进行解析,得到错误堆栈对象 上面我们已经拿到colno为2319,lineno为1,接下来需要安装一个插件帮助我们找到对应压缩前的代码位置。
npm install source-map -S
复制代码
先读取对应的map文件(按filename对应),然后只需传入压缩后的报错行号列号即可,就会返回压缩前的错误信息。打个比喻:简单地说相当于一本书的目录,我们根据目录可以快速找到某一部分内容的页数
router.get("/error", async (ctx) => {
const errInfo = ctx.query.info;
// 转码 反序列化
let obj = JSON.parse(Buffer.from(errInfo, "base64").toString("utf-8"));
let fileUrl = obj.filename.split("/").pop() + ".map"; // map文件路径
// 解析sourceMap
// 1.sourcemap文件的文件流,我们已经上传
// 2.文件编码格式
let consumer = await new sourceMap.SourceMapConsumer(
fs.readFileSync(path.join(__dirname, "source-map/" + fileUrl), "utf8")
);
// 解析原始报错数据
let result = consumer.originalPositionFor({
line: obj.lineno, // 压缩后的行号
column: obj.colno, // 压缩后的列号
});
// 写入到日志中
obj.lineno = result.line;
obj.colno = result.column;
log4js.logError(JSON.stringify(obj));
ctx.body = "";
});
复制代码
image.png
ELK前端日志分析
www.cnblogs.com/xiao9873341…
看了一下许多平台对错误日志的分析和可视化都使用了ELK,ELK在服务器运维界应该是运用的非常成熟了,很多成熟的大型项目都使用ELK来作为前端日志监控、分析的工具。我对运维这一块兴趣不大,有兴趣的可以自行搭建,整出来界面还是挺炫酷的。
而我又不想每一次都跑去服务器查看日志,于是想到了可以建个表来把错误信息给存起来。用起老三样koa+mongodb+vue,我们这项目就算是齐活了。(mongodb,yyds,省去了建表许多功夫)
npm install mongodb --save
复制代码
新建一个文件db.js封装一下mongo连接,方便复用:
// db.js
const MongoClient = require("mongodb").MongoClient;
const url = "mongodb://localhost:27017/";
const dbName = "err_db";
const collectionName = "errList";
class Db {
// 单例模式,解决多次实例化时候每次创建连接对象不共享的问题,实现共享连接数据库状态
static getInstance() {
if (!Db.instance) {
Db.instance = new Db();
}
return Db.instance;
}
constructor() {
// 属性 存放db对象
this.dbClient = "";
// 实例化的时候就连接数据库,增加连接数据库速度
this.connect();
}
// 连接数据库
connect() {
return new Promise((resolve, reject) => {
// 解决数据库多次连接的问题,要不然每次操作数据都会进行一次连接数据库的操作,比较慢
if (!this.dbClient) {
// 第一次的时候连接数据库
MongoClient.connect(
url,
{ useNewUrlParser: true, useUnifiedTopology: true },
(err, client) => {
if (err) {
reject(err);
} else {
// 将连接数据库的状态赋值给属性,保持长连接状态
this.dbClient = client.db(dbName);
resolve(this.dbClient);
}
}
);
} else {
// 第二次之后直接返回dbClient
resolve(this.dbClient);
}
});
}
// 增加一条数据
insert(json) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
db.collection(collectionName).insertOne(json, (err, result) => {
if (err) {
reject(err);
} else {
resolve(result);
}
});
});
});
}
//查询 --
find(query = {}) {
return new Promise((resolve, reject) => {
this.connect().then((db) => {
let res = db.collection(collectionName).find(query);
res.toArray((e, docs) => {
if (e) {
reject(e);
return;
}
resolve(docs);
});
});
});
}
}
module.exports = Db.getInstance();
复制代码
然后就可以在项目中愉快使用
let db = require("./db");
...
log4js.logError(JSON.stringify(obj));
//插入数据
await db.insert(obj);
ctx.body = "";
复制代码
数据插入成功
增加一个查询接口:
router.get("/errlist", async (ctx) => {
let res = await db.find({});
ctx.body = {
data: res,
};
});
复制代码
为了丰富错误信息,我们还可以在上报的时候增加报错时间,用户浏览器信息,自定义错误类型统计,引入图表可视化展示,更加直观地追踪
image.png
至此,我们总结了几种异常捕获的做法,并完成了对前端程序异常的上报功能,这对开发和测试人员都有较大的意义,用一句或说便是,要对产品保持敬畏之心,时刻关注存在的缺陷问题。代码中有疑问或者不对的地方欢迎各位批评指正,共同进步。
本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/ithQ2DDFvTfpuaixH8pt2A
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。