react错误收集和定位

376 阅读3分钟

开发中,难免有些错误逻辑没有想到,线上各种复杂的环境,可能导致程序错误,或者报错,但是打包后的代码都是混淆压缩的,很难定位到发生错误的位置,如何根据线上的错误代码快速定位到源呢。

比如下面这段错误代码

import React, { useEffect } from 'react';
import styles from './index.less';
import ErrorBoundary from '@/components/ErrorBoundary';
function IndexPage() {
  useEffect(() => {
    setTimeout(() => {
      console.log(a) // 这儿错误代码
    }, 2000)
  }, [])
  return (
    <div>
      <h1 className={styles.title}>Page index</h1>
    </div>
  );
}

export default function App() {

  return (
    <ErrorBoundary>
      <IndexPage />
    </ErrorBoundary>
  )
}

打包后线上的报错位置很难定位到源码是哪儿有问题,我平时在项目中经常遇到这样的问题,每次排查起来都比较困难,要如何解决这个问题呢

webpack打包的时候的可以开启source-map,打包出来会有一个.js.map文件,该文件可以映射我们打包后的文件

  1. 使用ErrorBoundary收集react错误
  2. 调用TraceKit获取发生错误的行数、调用栈级错误信息
  3. TraceKit获取的错误信息传递给服务端
  4. 服务端根据上传的错误信息调用source-map插件,获取源码报错的位置
使用ErrorBoundary收集react错误
// ErrorBoundary组件
class ErrorBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.state = { hasError: false };
    }
    static getDerivedStateFromError(error) {
        return { hasError: true };
    }
    componentDidCatch(error, errorInfo) {
    }
    render() {
        if (this.state.hasError) {
            return <h1>出错了!</h1>;
        }
        return this.props.children;
    }
}
export default ErrorBoundary
// 
import React, { useEffect } from 'react';
import styles from './index.less';
import ErrorBoundary from '@/components/ErrorBoundary';
function IndexPage() {
  useEffect(() => {
    setTimeout(() => {
      console.log(a)
    }, 2000)
  }, [])
  return (
    <div>
      <h1 className={styles.title}>Page index</h1>
    </div>
  );
}

export default function App() {

  return (
    <ErrorBoundary>
      <IndexPage />
    </ErrorBoundary>
  )
}

如果组件发生错误,会除非这个getDerivedStateFromError这个函数,componentDidCatch收集错误信息和调用栈等,文字功底不哈,详情可看官网这两个函数的解释。

TraceKit获取发生错误的行数、调用栈级错误信息,上报错误
import React from "react";
import TraceKit from 'tracekit'; // 引入tracekit
TraceKit.report.subscribe((error) => {
  const { message = "", stack } = error || {};
  console.log("stack", stack[0])
  const obj = {
    message,
    stack: {
      column: stack[0].column,
      line: stack[0].line,
      func: stack[0].func,
      url: stack[0].url
    }
  };
  console.log("调用栈信息", obj)
  // 上传错误信息
  const response = fetch("http://localhost:3001/errorUp", {
    method: "POST",
    body: JSON.stringify(obj),
    headers: {
      "Content-Type": "application/json",
    },
  }).then(res => {
    console.log("上传成功")
  })
})
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  componentDidCatch(error, errorInfo) {
    // 调用TraceKit收集错误的的信息
    TraceKit.report(error)
  }
  render() {
    if (this.state.hasError) {
      return <h1>出错了!</h1>;
    }
    return this.props.children;
  }
}
export default ErrorBoundary

在componentDidCatch中调用TraceKit.report(),把报错信息传给TraceKit解析,解析出报错的行数、报错的文件路径、报错了的列、级报错的函数,,并把报错信息传给服务器

服务端解析

服务端根据上传的错误信息,调用source-map插件解析map文件,找到源码中对应的错误信息位置

const express = require('express')
const cors = require('cors')
const SourceMap = require('source-map');
const multiparty = require("multiparty") // 处理form-data请求
const path = require('path')
const fse = require("fs-extra")
const fs  =require('fs')


const app = express()

// 解析 url-encoded格式的表单数据
app.use(express.urlencoded({ extended: false }));
 
// 解析json格式的表单数据
app.use(express.json());


app.use(cors())

app.post('/errorUp', async (req, res) => {
    const urlParams = req.body;
    console.log('urlParams', req.body);
    const stack = urlParams.stack;
    // 获取文件名
    const fileName = path.basename(stack.url);
    // 获取对应的map文件
    const filePath = path.join(__dirname, fileName + '.map');
    console.log("filePath",filePath)
    const readFile = function (filePath) {
        return new Promise((resolve, reject) => {
            fs.readFile(filePath, { encoding: 'utf-8' }, (err, data) => {
                if (err) {
                    console.log('readFileErr', err)
                    return reject(err);
                }
                resolve(JSON.parse(data));
            })
        })
    }

    async function searchSource({ filePath, line, column }) {
        const rawSourceMap = await readFile(filePath);
        const consumer = await new SourceMap.SourceMapConsumer(rawSourceMap);
        const res = consumer.originalPositionFor({ line, column })

        consumer.destroy();
        return res;
    }

    let sourceMapParseResult = '';

    try {
        // 解析sourceMap结果
        sourceMapParseResult = await searchSource({ filePath, line: stack.line, column: stack.column });
    } catch (err) {
        sourceMapParseResult = err;
    }
    console.log('解析结果', sourceMapParseResult)

    res.send("ok")

})
app.listen(3001, () => {
    console.log("serve3001 服务端启动")
})

找到对应源码中的报错的位置,在/src/pages/index.tsx文件中第7行处18的一列,刚好于源码对应

总结

主要用到了tracekit解析错误的调用栈信息,在使用source-map插件根据.map文件和tracekit解析的错误信息找到源码中发生错误的位置,关键用到tracekit、source-map插件和.map文件