有关性能监控系统中的source-map | 青训营笔记

260 阅读3分钟

这是我参与「第四届青训营 」笔记创作活动的第1天

本章主要记录本次训练营,团队在实现前端性能监控时,为了解决用户线上应用js错误时,sdk捕获错误后如何定位错误的真实位置进行的探索。

source-map概述

通俗的来说,Source Map 就是一个信息文件,里面存储了代码打包转换后的位置信息,实质是一个 json 描述文件,维护了打包前后的代码映射关系。 在本地开发时,如果编写的js代码出现了bug,可以在开发者工具的console中看见报错信息,其中很关键的信息就是错误发生的位置,它能够帮助我们快速定位bug,做出修改。 但是在生产环境中,往往会对代码进行打包压缩,减小代码体积,这会使得当发生bug时,定位的位置是bundle中的代码位置不是实际的位置,当打包工具配置了source-map,他就会在打包时同时生成一个source-map文件,以此映射真实错误位置。

性能监控中的jsError监控

在性能监控sdk中,在发生js错误时将js错误的信息发送至服务器进行保存,可以想象对于监控系统来说,我们肯定是希望保存js错误的各种信息,真实的位置信息必不可少 但第三方用户在使用sdk时,他们自己的应用必然是要打包后进行部署的,不要希望与用户将source-map一起部署,因为打包生成的source-map文件体积一般很大,而且如果将source-map部署,会造成代码泄露,带来安全风险。 那监控系统要如何实现对生产环境的js error定位呢? 据我的了解,像sentry这样的开源性能监控系统,它提供了两种方式:

  • 在用户手动将build生成的source-map上传到自己应用的中台系统中
  • 使用打包工具的plugin,在build时自动将生成的source-map上传到服务端 当服务端拥有了source-map,当sdk传递来js error日志时,就可以使用source-map对日志进行解析得到真实的错误位置

训练营-前端监控系统中的应用

自动上传清理source-map的webpack插件

在本次训练营我们团队的监控系统中,编写了一个简单的webpack插件用于在测试项目build时,自动上传source-map并清理source-map,无痕操作

const { sources } = require("webpack");
const axios = require("axios");
const ora = require("ora");

class UploadSourceMapPlugin {
    constructor(params) {
        // 用于在dev时关闭插件
        this.enable = typeof params?.enable === "boolean" ? params?.enable : true;
        // 目标url
        this.uploadURL = params?.uploadURL || "http://127.0.0.1:7001/logstore/uploadMap";
    }

    apply(compiler) {
        // webpack的hooks
        compiler.hooks.emit.tapAsync(
            "UploadSourceMapPlugin",
            (compilation, callback) => {
                if (this.enable) {
                    for (let key in compilation.assets) {
                        // 去除js尾部指向source-map的链接,避免报警
                        if (key.endsWith(".js")) {
                            console.log(key);
                            const asset = compilation.getAsset(key);
                            const contents = asset.source.source();

                            let newContent = contents.split("\n");
                            newContent.pop();
                            newContent = newContent.join("\n");

                            compilation.updateAsset(key, new sources.RawSource(newContent));
                        }
                        // 上传source-map,然后清理
                        if (key.endsWith(".js.map")) {
                            console.log(key);
                            const spinner = ora(`UpLoading ${key} \n`).start();
                            const asset = compilation.getAsset(key);
                            const body = {};
                            body[key] = asset.source.source();

                            axios({
                                method: "post",
                                url: this.uploadURL,
                                data: body,
                            })
                            .then(() => {
                                    spinner.color = "green";
                                    spinner.succeed(` ${key} uploaded \n`);
                            })
                            .catch((e) => {
                                    console.log(e);
                                    spinner.color = "red";
                                    spinner.fail(` ${key} upload failed \n`);
                            });

                            // 清除source-map
                            compilation.deleteAsset(key);
                        }
                    }
                }
                callback();
            }
        );
    }
}

module.exports = UploadSourceMapPlugin;

该插件主要实现:

1、将.js文件的尾部的指向source-map文件的链接清理掉,可以避免浏览器报警

2、上传source-map,然后清理

服务端解析上报日志

服务端实现也很简单主要是利用source-map包加载map文件并进行解析

const SourceMap = require('source-map')
const { SourceMapConsumer } = SourceMap
...
// 针对js错误需要使用sourceMap进行position定位
const rawSourceMap = JSON.parse(fs.readFileSync(path.join(__dirname, `../../assert/sourceMap/${data.filename}.map`)))
SourceMapConsumer.with(rawSourceMap, null, consumer => {
    let position = data.position.split(':').map(item => parseInt(item)) // 获取浏览检测的错误位置

    const pos = consumer.originalPositionFor({
        line: position[0],
        column: position[1],
    })

    data.filename = pos.source
    data.position = `${pos.line}:${pos.column}`
})

经过这样的处理,存入数据库的日志就是真实的js错误位置了,可视化展示时也能看到真实有价值的错误信息,因为采用的webpack插件的形式,只需要简单的配置,之后的各种操作都是无痕的,用户可以像正常使用一样进行服务的发布,至于另一种手动上传至中台,因为团队成员时间精力有限没有实现,以后自己做的时候再去尝试一波吧。 以上。