沐游虞笔记
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 前端面试题

    • HTML面试题汇总(无答案)
    • HTML面试题汇总
    • CSS 面试题汇总(无答案)
    • CSS 面试题汇总
    • javascript 面试题汇总(无答案)
    • javascript 面试题汇总
    • promise 面试题(无答案)
    • promise 面试题
    • 浏览器面试题汇总(无答案)
    • 浏览器面试题汇总
    • 网络面试题汇总(无答案)
    • 网络面试题汇总
    • 工程化面试题汇总(无答案)
    • 工程化面试题汇总
    • VUE面试题汇总(无答案)
    • VUE面试题汇总
  • 直播课文件

    • 静态页面学习指导
    • 属性的计算过程
    • 层叠继承规则总结
    • BFC
    • JS基础知识回顾
    • DOM 事件的传播机制
    • DOM 事件的注册和移除
    • 阻止事件默认行为
    • 基础领航考试题
    • 基础领航考试题(答案)
    • 2024前端发展
    • JS核心概念学习指导
    • 第三方库与工程化学习指导
    • Vue入门学习指导
    • vue进阶学习指导
    • 前端性能优化
  • 笔面试环节知识讲解

    • 目录
    • 图像处理
    • 图像处理(面试)
    • Webpack构建优化
    • Webpack构建优化(面试)
    • TTS性能优化
    • TTS性能优化(面试)
    • 实时协作
    • 实时协作(面试)
    • 网页复制图片到剪贴板
    • 网页复制图片到剪贴板(面试)
    • vite插件
    • vite插件(面试)
    • 表单数据同步与保持
    • 表单数据同步与保持(面试)
    • 优化虚拟列表
    • 优化虚拟列表(面试)
    • 微前端解决巨石应用
    • 微前端解决巨石应用(面试)
    • DNS解析与优化
    • DNS解析与优化(面试)
    • 前端监控
    • 前端监控(面试)
    • 12.跨标签页通信
    • 12.跨标签页通信(面试)
    • 13.Vite相关优化
    • 13.Vite相关优化(面试)
    • 14.计时器节流问题
    • 14.计时器节流问题(面试)
    • 15.多文件预览支持
    • 15.多文件预览支持(面试)
    • 16.defer优化白屏时间
    • 16.defer优化白屏时间(面试)
  • Vue3整体变化
  • Vue2响应式回顾
  • Vue3响应式变化
  • nextTick实现原理
  • 两道代码题
  • Vue运行机制
  • 渲染器核心功能
  • 事件绑定与更新
  • computed面试题
  • watch面试题
  • 图解双端diff
  • 图解快速dff
  • 最长递增子序列
  • 模板编译器
  • 模板编译提升
  • 组件name作用
  • 路由传参方式
  • 基础篇

    • 序章React介绍
    • JSX基础语法
    • React基本介绍
    • 表单
    • 生命周期
    • 组件与事件绑定
    • 组件状态与数据传递
    • Hooks
    • React--redux介绍
    • React-router介绍
  • 就业篇

    • 属性默认值和类型验证
    • 高阶组件
    • Ref
    • Context
    • Render Props
    • Portals
    • 错误边界
    • 组件渲染性能优化
    • 前端框架的理解
    • Reacti和Vue描述页面的区别
    • 前端框架的分类
    • 虚拟DoM
    • React整体架构
    • React渲染流程
    • Fiber双缓冲
    • MessageChannel
    • Scheduleri调度普通任务
    • Scheduleri调度延时任务
    • 最小堆
    • React中的位运算
    • beginWork工作流程
    • completeWork工作流程
    • 图解diff算法
    • commit工作流程
    • lane模型
    • React中的事件
    • Hooks原理
    • useStateuseReducer.
    • effect相关hook
    • useCallbackuseMemo
    • useRef
    • Update
    • 性能优化策略之eagerState
    • 性能优化策略之bailout
    • bailoutContextAPl
    • 性能优化对日常开发启示
  • 前端监控概述
  • 错误监控
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • CSS3手册
  • HTML5手册
  • JavaScript语言提升

    • es补充
    • 事件循环
    • promise基础
    • Promise的链式调用
    • Promise的静态方法
    • async和await
    • Promise相关面试题
  • 网络

    • 客户端与服务器
    • 关于 Apifox 的使用
  • git文档
  • 工程化

    • CommonJS
    • ES module
    • npm文档(包管理)
    • Lass笔记
    • webpack工具
  • canvas详解
  • uinapp笔记
  • 自动化测试
  • oauth2令牌

    • 认识Oauth2
    • 三方应用实现github授权
    • 微信三方应用登录实现
    • 支付宝沙箱支付功能
  • 前端服务监控概述
  • 错误监控
    • JS运行异常
      • 1、window.onerror
      • 2、window.addEventListener('error')
      • 两者的区别和选用
    • 静态资源加载异常
    • Promise异常
    • Vue2、Vue3 错误捕获
    • HTTP请求异常
    • 跨域脚本错误
    • React 错误捕获
    • 项目代码实战
      • 1、创建全局配置
      • 2、error
      • 3、事件处理
      • 4、路径处理
      • 5、index.js
  • 数据上报
  • 页面性能监控
  • 用户行为收集与埋点
  • 前端服务监控
luzhichang
2024-10-15
目录

错误监控

# 错误监控

虽然在我们开发完成之后,会经历多轮的单元测试、集成测试、人工测试,但是难免漏掉一些边缘的测试场景,甚至还有一些奇奇怪怪的玄学故障出现;而出现报错后,轻则某些数据页面无法访问,重则导致客户数据出错;

因此,我们的前端监控,需要对前端页面的错误进行监控,一个强大完整的错误监控系统,可以帮我们做以下的事情:

  • 应用报错时,及时知晓线上应用出现了错误,及时安排修复止损;
  • 应用报错后,根据上报的用户行为追踪记录数据,迅速进行bug复现;
  • 应用报错后,通过上报的错误行列以及错误信息,找到报错源码并快速修正;
  • 数据采集后,进行分析提供宏观的 错误数、错误率、影响用户数等关键指标;

# JS运行异常

当 JavaScript运行时产生的错误 就属于 JS运行异常,比如我们常见的:

TypeError: Cannot read properties of null
TypeError: xxx is not a function
ReferenceError: xxx is not defined

像这种运行时异常,我们很少手动去捕获它,当它发生异常之后,js有两种情况都会触发它

这里有一个点需要特别注意, SyntaxError 语法错误,除了用 eval() 执行的脚本以外,一般是不可以被捕获到的。

其实原因很简单, 语法错误,在编译解析阶段就已经报错了,而拥有语法错误的脚本不会放入任务队列进行执行,自然也就不会有错误冒泡到我们的捕获代码。

当然,现在代码检查这么好用,早在编写代码时这种语法错误就被避免掉了,一般我们碰不上语法错误的~

# 1、window.onerror

window.onerror 是一个全局变量,默认值为null。当有js运行时错误触发时,window会触发error事件,并执行 window.onerror(),借助这个特性,我们对 window.onerror 进行重写就可以捕获到代码中的异常

const rawOnError = window.onerror;
// 监听 js 错误
window.onerror = (msg, url, line, column, error) => {
  //处理原有的onerror
  if (rawOnError) {
    rawOnError.call(window, msg, url, line, column, error);
  }

  console.log("监控中......");
  console.log(msg, url, line, column, error);
}

# 2、window.addEventListener('error')

window.addEventListener('error') 来捕获 JS运行异常;它会比 window.onerror 先触发;

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

# 两者的区别和选用

  • 它们两者均可以捕获到 JS运行异常,但是 方法二除了可以监听 JS运行异常 之外,还可以同时捕获到 静态资源加载异常
  • onerror 可以接受多个参数。而 addEventListener('error') 只有一个保存所有错误信息的参数

更加建议使用第二种 addEventListener('error') 的方式;原因很简单:不像方法一可以被 window.onerror 重新覆盖;而且可以同时处理静态资源错误

# 静态资源加载异常

界面上的link的css、script的js资源、img图片、CDN资源 打不开了,其实都会触发window.addEventListener('error')事件

使用 addEventListener 捕获资源错误时,一定要将 第三个选项设为 true,因为资源错误没有冒泡,所以只能在捕获阶段捕获。

我们只需要再事件中加入简单的判断,就可以区分是资源加载错误,还是js错误

window.addEventListener('error', e => {
    const target = e.target;
    
    //资源加载错误
    if (target && (target.src || target.href)) {
      
    }
    else { //js错误
      
    }
    
  }, true)

# Promise异常

什么叫 Promise异常 呢?其实就是我们使用 Promise 的过程中,当 Promise 被 reject 且没有被 catch 处理的时候,就会抛出 Promise异常;同样的,如果我们在使用 Promise 的过程中,报了JS的错误,同样也被以 Promise异常 的形式抛出:

Promise.resolve().then(() => console.log(c));
Promise.reject(Error('promise'))

而当抛出 Promise异常 时,会触发 unhandledrejection 事件,所以我们只需要去监听它就可以进行 Promise 异常 的捕获了,不过值得注意的一点是:相比与上面所述的直接获取报错的行号、列号等信息,Promise异常 我们只能捕获到一个 报错原因 而已

window.addEventListener('unhandledrejection', e => {
    console.log("---promiseErr监控中---");
    console.error(e)    
})

# Vue2、Vue3 错误捕获

  • Vue2 如果在组件渲染时出现运行错误,错误将会被传递至全局 Vue.config.errorHandler 配置函数;
  • Vue3 同 Vue2,如果在组件渲染时出现运行错误,错误将会被传递至全局的 app.config.errorHandler 配置函数;

我们可以利用这两个钩子函数来进行错误捕获,由于是依赖于 Vue配置函数 的错误捕获,所以我们在初始化时,需要用户将 Vue实例 传进来;

if (config.vue?.Vue) {
    config.vue.Vue.config.errorHandler = (e, vm, info) => {
      console.log("---vue---")
      console.log(e);
    }
  }

注意:由于内容实在太多,下面的内容在视频中不做演示,感兴趣的同学可以自己继续深入研究

# HTTP请求异常

所谓 Http请求异常 也就是异步请求 HTTP 接口时的异常,比如我调用了一个登录接口,但是我的传参不对,登录接口给我返回了 500 错误码,其实这个时候就已经产生了异常了.

看到这里,其实有的同学可能会疑惑,我们现在的调用 HTTP 接口,一般也就是通过 async/await 这种基于Promise的解决异步的最终方案;那么,假如说请求了一个接口地址报了500,因为是基于 Promise 调用的接口,我们能够在上文的 Promise异常 捕获中,获取到一个错误信息(如下图);

但是有一个问题别忘记了,Promise异常捕获没办法获取报错的行列,我们只知道 Promise 报错了,报错的信息是 接口请求500;但是我们根本不知道是哪个接口报错了;

所以说,我们对于 Http请求异常 的捕获需求就是:全局统一监控、报错的具体接口、请求状态码、请求耗时以及请求参数等等;

而为了实现上述的监控需求,我们需要了解到:现在异步请求的底层原理都是调用的 XMLHttpRequest 或者 Fetch,我们只需要对这两个方法都进行 劫持 ,就可以往接口请求的过程中加入我们所需要的一些参数捕获;

# 跨域脚本错误

还有一种错误,平常我们较难遇到,那就是 跨域脚本错误,简单来说,就是你跨域调用的内容出现的错误。

当跨域加载的脚本中发生语法错误时,浏览器出于安全考虑,不会报告错误的细节,而只报告简单的 Script error。浏览器只允许同域下的脚本捕获具体错误信息,而其他脚本只知道发生了一个错误,但无法获知错误的具体内容(控制台仍然可以看到,JS脚本无法捕获)

其实对于三方脚本的错误,我们是否捕获都可以,不过我们需要一点处理,如果不需要捕获的话,就不进行上报,如果需要捕获的话,只上报类型;

# React 错误捕获

React 一样也有官方提供的错误捕获,见文档:zh-hans.reactjs.org/docs/react-…

和 Vue 不同的是,我们需要自己定义一个类组件暴露给项目使用,我这里就不具体详写了,感兴趣的同学可以自己进行补全:

import React from 'react';
export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
  componentDidCatch(error, info) {
    this.setState({ hasError: true });
    // 将component中的报错发送到后台
    // monitor为监控sdk导出的对象
    monitor && monitor.reactError(error, info);
  }
  render() {
    if (this.state.hasError) {
      return null
      // 也可以在出错的component处展示出错信息
      // return <h1>出错了!</h1>;
    }
    return this.props.children;
  }
}

其中 reactError() 方法在组装错误信息。:

monitor.reactError = function (err, info) {
  report({
    type: ERROR_REACT,
    desc: err.toString(),
    stack: info.componentStack
  });
};

# 项目代码实战

# 1、创建全局配置

// config/index.js
const config = {
  appId: 'monitor-demo',
  userId: 'ys',
  reportUrl: 'http://127.0.0.1:3001/report/actions',
  vue: {
      Vue: null,
      router: null,
  },
  ua:navigator.userAgent,
}

export default config

export function setConfig(options) {
  for (const key in config) {
    if (options[key]) {
      config[key] = options[key]
    }
  }
}

# 2、error

// error/index.js
import config from '../config'
import lastCaptureEvent from '../utils/captureEvent'
import {getPaths} from "../utils/"

/**
 * 这个正则表达式用于匹配 JavaScript 错误栈中的堆栈跟踪信息中的单个条目,其中包含文件名、行号和列号等信息。
 * 具体来说,它匹配以下格式的文本:
 * at functionName (filename:lineNumber:columnNumber)
 * at filename:lineNumber:columnNumber
 * at http://example.com/filename:lineNumber:columnNumber
 * at https://example.com/filename:lineNumber:columnNumber
 */
const FULL_MATCH =
  /^\s*at (?:(.*?) ?\()?((?:file|https?|blob|chrome-extension|address|native|eval|webpack|<anonymous>|[-a-z]+:|.*bundle|\/).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i;

// 限制只追溯10个
const STACKTRACE_LIMIT = 10;

export default function error() {
  /*
  const rawOnError = window.onerror;
  // 监听 js 错误
  window.onerror = (msg, url, line, column, error) => {
    //处理原有的onerror
    if (rawOnError) {
      rawOnError.call(window, msg, url, line, column, error);
    }

    console.log("监控中......");
    console.log(msg, url, line, column);
    console.log(error);
  }
  */

  // 监听 promise 错误 缺点是获取不到列数据
  window.addEventListener('unhandledrejection', e => {
    console.log("---promiseErr监控中---");
    console.error(e)

    const lastEvent = lastCaptureEvent();
    let data = {};
    const reason = e.reason;

    if (typeof reason === 'string') {
      data.message = reason
    }
    else if (typeof reason === 'object') {
      const paths = getPaths(lastEvent);
      data.message = reason.message
      if (reason.stack) { 
        const errs = parseStackFrames(reason);
        const currentError = errs[0];
        data.filename = currentError.filename;
        data.functionName = currentError.functionName;
        data.lineno = currentError.lineno;
        data.colno = currentError.colno;
        data.stack = reason.stack;
        data.paths = paths;
        data.type = 'error';
        data.errorType = "promiseError";
      }
    }

    console.log(data);
  })

  // 捕获资源加载失败错误 js css img...
  //window.addEventListener('error',fn) 能捕获js错误,也能捕获资源加载失败错误
  //使用 addEventListener 捕获资源错误时,一定要将 第三个选项设为 true,
  //因为资源错误没有冒泡,所以只能在捕获阶段捕获。
  //同理,由于 window.onerror 是通过在冒泡阶段捕获错误,所以无法捕获资源错误。
  window.addEventListener('error', e => {
    const target = e.target;
    //注意当前并不是事件对象本身,而是error事件,因此获取不了当前点击的对象
    //我们可以利用事件传递的机制,获取最后一个捕获的对象
    const lastEvent = lastCaptureEvent();
    
    //资源加载错误
    if (target && (target.src || target.href)) {
      const paths = getPaths(target);
      const data = {
        type:'error',
        errorType: "resourceError",
        filename: target.src || target.href,
        tagName: target.tagName,
        message:`加载${target.tagName}资源失败`,
        paths:paths ? paths : 'Window',
      }
      console.log(data);
    }
    else { //js错误
      const errs = parseStackFrames(e.error);
      const currentError = errs[0];
      const paths = getPaths(lastEvent);
      const data = {
        type:'error',
        errorType: "jsError",
        filename: currentError.filename,
        functionName: currentError.functionName,
        lineno: currentError.lineno,
        colno: currentError.colno,
        message: e.message,
        stack: e.error.stack,
        paths:paths ? paths : 'Window'
      }
      console.log(data);
    }
    
  }, true)

  if (config.vue?.Vue) {
    config.vue.Vue.config.errorHandler = (e, vm, info) => {
      console.log("---vue---")

      const lastEvent = lastCaptureEvent();
      
      const paths = getPaths(lastEvent);

      const errs = parseStackFrames(e);
      const {
        filename,
        functionName,
        lineno,
        colno
      } = errs[0];

      const data = {
        type: 'error',
        errorType: "vueError",
        filename,
        functionName,
        lineno,
        colno,
        message: e.message,
        stack: e.stack,
        paths: paths ? paths : 'Window'
      }

      console.log(data);
    }
  }
}

function parseStackLine(line) {
  const lineMatch = line.match(FULL_MATCH);
  if (!lineMatch) return {};
  const filename = lineMatch[2];
  const functionName = lineMatch[1] || '';
  const lineno = parseInt(lineMatch[3], 10) || undefined;
  const colno = parseInt(lineMatch[4], 10) || undefined;
  return {
    filename,
    functionName,
    lineno,
    colno,
  };
}

// 解析错误堆栈
function parseStackFrames(error) {
  const { stack } = error;
  // 无 stack 时直接返回
  if (!stack) return [];
  const frames = [];
  for (const line of stack.split('\n').slice(1)) {
    const frame = parseStackLine(line);
    if (frame) {
      frames.push(frame);
    }
  }
  return frames.slice(0, STACKTRACE_LIMIT);
}
/**
 * 手动捕获错误
 * @param {*} error 
 * @param {*} msg 
 */
export function errorCaptured(error, msg){
  console.log(error);
  console.log(msg);
}

# 3、事件处理

// utils/captureEvent.js
let lastCaptureEvent;
['click', 'mousedown', 'keydown', 'scroll', 'mousewheel', 'mouseover'].forEach(eventType => {
  document.addEventListener(
    eventType,
    event => {
      lastCaptureEvent = event;
    },
    { capture: true, passive: true }
  )
});
export default () => { 
  return lastCaptureEvent;
}

# 4、路径处理

export const getComposePathEle = (e) => { 
  //如果存在path属性,直接返回path属性
  //e.composedPath()也能返回事件路径,但是还是有兼容性问题
  //https://developer.mozilla.org/zh-CN/docs/Web/API/Event/composedPath
  if(!e) return [];
  let pathArr = e.path || (e.composedPath && e.composedPath());
  if ((pathArr||[]).length) { 
    return pathArr;
  }

  //如果不存在,就向上遍历节点
  let target = e.target;
  const composedPath = [];

  while(target && target.parentNode) { 
    composedPath.push(target);
    target = target.parentNode;
  }
  //最后push进去document和window
  composedPath.push(document, window);

  return composedPath;
}

export const getComposePath = (e) => {
  if (!e) return [];
  const composedPathEle = getComposePathEle(e);
  const composedPath = composedPathEle.reverse().slice(2).map(ele => { 
    
    let selector = ele.tagName.toLowerCase();
    if(ele.id) { 
      selector += `#${ele.id}`;
    }
    if(ele.className) { 
      selector += `.${ele.className}`;
    }
    return selector;
  })

  return composedPath;
}

export const getPaths = (e) => {
  if (!e) return '';
  const composedPath = getComposePath(e);
  const selector = composedPath.join(' > ');
  return selector;
}

# 5、index.js

import { setConfig } from "./config"
import error, { errorCaptured } from "./error"
const monitor = {
  init(options = {}) { 
    setConfig(options);
    error();
  },
  errorCaptured
}

export default monitor
前端服务监控概述
数据上报

← 前端服务监控概述 数据上报→

Theme by Vdoing | Copyright © 2021-2024 蜀ICP备2024068710号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式