✨
blog
  • Blog
  • Element-UI
    • 2019-09-04
  • JS
    • ES6 之 Set 和 Map
    • let 和 const 声明常见概念
    • 元编程
    • ES6之字符串的扩展
    • ES6 之异步流程的前世今生(上)
    • ES6之异步流程的前世今生(下)
    • ES6 之模块你知吗
    • ES6 之解构赋值与箭头函数的妙用
    • 迭代器
    • ES5 之原型(一)
    • ES6之类(二)
    • es7之装饰器
    • es6之数组详解
    • js之this指向
    • 对象
    • vue项目配合使用canvas联动
    • 本文解决痛点:对象里面是否有值
  • MAC
    • vue源码之method
    • Mac的使用技巧
    • 前文
    • Mac常用软件(二)
    • 如何查看 Mac 端口号以及占用情况
  • Node
    • Node之Buffer详解
    • 浏览器与 node 的事件循环(event loop)有何区别
    • Node之多线程
    • node之模块解析(一)
    • 错误捕获与内存告警
  • TS
    • Record
    • 使用方法
    • 工具泛型
    • 类型体操
    • 泛型
  • chrome
    • v8 引擎
    • v8 垃圾回收机制
    • 浏览器的知识
  • flutter
    • 路由
    • 页面布局
  • go
    • index
  • html&css
    • 两栏布局
    • ES5和ES6的区别
    • ES5 和 ES6 的区别
    • HTTP详解
    • TCP 与 UDP 的区别
    • MDN
    • css modules 使用教程
    • css 居中
    • 拖拽
    • flex布局
    • h5 新增特性 html5
    • history 与 hash 路由策略
    • position 定位方式
    • rem布局
    • svg
    • web性能优化
    • 事件循环
    • 从输入网址后发生了什么
    • 前端状态管理
    • 圣杯布局与双飞翼
    • 性能优化 页面的性能统计指标
    • 本地存储的几种对比
    • 浏览器的渲染进程
    • 浏览器缓存策略详解
    • 盒模型
    • 为什么要移动端适配
    • 跨域的 N 种实现方式
  • web3
    • 常见概念
    • vue项目配合使用canvas联动
  • webgl
    • Mac使用技巧(二)
    • Node之模块解析path
  • 代码库
    • documeng的一些常见操作
    • eventBus事件
    • jquery提交
    • jquery的一些常见操作
    • 常见操作
    • 数组polyfill
    • TS代码片段
      • 面试官眼中的test unit
  • 全年安排
    • AfterShip
    • 大企业
  • 函数编程题
    • Promise问题
    • 继承
  • 前端早早聊
    • vue生态
    • 开发一款VScode语言插件
    • 简历回顾和进行复盘
    • 重新认知性能优化及其度量方法
    • 2022-09-17-音视频专场.md
      • 2022-09-17-音视频专场
    • 前端晋升专场
      • 成长的诀窍是靠自己
      • 销销帮
    • 前端监控专场
      • 字节前端监控实践
      • 李港:大前端-从无到有搭建自研前端监控系统
    • 前端跳槽
      • 50个面试官在线招聘
      • 如何识别优秀的前端猎头来跳槽大厂
      • 面试套路
    • 支付宝
      • 面试
    • 管理专场
      • 芋头:管理者眼中的web技术发展前沿
    • 组件专场
      • 基于webCompents的跨技术组件库实践
    • 面试
      • 面试辅导问题
      • 早早聊面试
      • 前端沙箱是什么? 怎么实现沙箱环境?
  • 常见总结
    • 2018年终总结-年底了,你总结了吗?我先来
    • 在逆境中成长
    • 2021年终总结
    • 2024年全年总结
    • 项目
    • Tell2.0 前端复盘
    • 复盘
    • 前端工程师素养
    • 学习方法论
    • 希望与破晓| 2022 年终总结
    • 新起点, 新征途 | 掘金年度征文
    • 稳定| 2023 年终总结
    • 趁着有风快飞翔 | 2019 年终总结
    • AfterShip
      • Emotion:用 JavaScript 编写 CSS 的强大工具
      • 个人中长期目标
      • 事故复盘
      • 时间解析
      • 国内外区别
      • 独立站建设
    • MEIZU
      • NativeApp与H5通信原理
      • SSR 原理
      • SSR的常见问题
      • CLI
      • electron 应用发布流程
      • electron
      • electron 面试
      • 数据结构与算法之美
      • mgc 一期复盘
      • 架构原理
      • 喵币管理
      • 三期复盘总结
      • 异常监控之 sentry 实践
      • 微前端
      • qiankun 原理解析
      • 快游戏一期
      • 游戏中心复盘
    • 个人准则
      • index
    • 编程猫
      • pc 接入 micro bit 方案
      • prompt engineer
      • web work 跨域解析与解决方式
      • web 中的 ai
      • 低版本 node 环境下 ffmpeg 的使用
      • 关于 taobao 源 https 过期
      • 加密 json
      • 安卓 5 和 6 的白屏解决
      • 性能排查与优化实践
      • 探月接入
      • 接入硬件
      • 新生态下的state
      • monorepo 包管理方式
      • 自修复 npm 库
      • 音频的绘制
    • 谨启
      • 音视频
      • 小程序
        • taro 规范
        • 结合 mobx 在跳转前预请求
        • Taro 浅析用法与原理
        • 前文
        • 小程序优化指南
        • 小程序内部实现原理
        • 支付相关
    • tencent
      • TAPD
        • MathJax的食用
        • canvas渲染优化策略
        • 为什么 JavaScript 是单线程的呢?
        • svg 总是不对
        • 前端库
        • 原生端和js端如何通信
        • 在旧项目中复用vue代码
        • 提升自我
        • 批量编辑优化
        • 插入业务对象
        • 编辑器
        • 挂载点
        • 性能优化对比
        • 遇到的问题
        • 项目迁移公告
        • 领导力
      • 行家
        • 实战篇
        • 职业发展、领导力、个人成长
        • 高质量沟通
  • 慕课网
    • react-native原理
    • react-native学习
  • 杂文
    • Dom 节点变动检测并录制的简单实现
    • 错误监控&错误捕获
    • NextJS与NuxtJS
    • 负载均衡的几种常用方式
    • PM2
    • service worker 控制网络请求?
    • SSL 和 TLS 的区别
    • Babel 你太美
    • echart踩坑经验
    • keyup、keydown你都知道有什么区别吗
    • 常见概念
    • 首屏加载优化与性能指标分析
    • preload 和 prefetch 的详解
    • 在项目中配置这几个关系
    • roullp 解析
    • tinymce原理浅析
    • wasm 在前端的应用
    • websocket
    • webworker
    • 项目
    • 从 ajax 到 axios
    • 从postcss 到自己开发一款插件
    • 从输入浏览器到页面展示涉及的缓存机制
    • 代码整洁之道
    • 你知道什么是aop吗
    • 函数式编程
    • 函数式编程指南
    • 前端input框文字最大值
    • 攻坚战
    • 前端书写 sdk
    • 前端文字转语音播放
    • 前端领域的 Docker 和 Kubernetes
    • 前端安全
    • 前端进阶之内存空间
    • 前端音频浅析
    • 十分钟搞定多人协作开发
    • 字符串的比较
    • 尾递归
    • 前文
    • 常见的算法可以分为以下三类
    • 手机调试--mac篇
    • 数组的原生系列
    • COOP 和 COEP - 新的跨域策略
    • 浅谈react组件书写
    • 浏览器与 Node.js 事件循环的区别
    • 由三道题引伸出来的思考
    • 移动端300ms点击延迟
    • 移动端和pc端事件
    • Git 常见疑惑
    • 我们离发 npm 包还有多远
    • 重绘和重排
    • AI 时代下的前端编程范式
    • 音频可视化实战
  • 极客时间
    • Serverless入门课
    • 二分查找
    • 二叉树
    • 全栈工程师
    • 动态规划面试宝典
    • 前端与rust
    • 散列表
    • 前端方面的Docker和Kubernetes
    • 栈
    • 深入浅出区块链
    • 玩转 vue 全家桶
    • 玩转 webpack
    • 程序员的个人财富课
    • 算法
    • 说透元宇宙
    • 跳表
    • 链表
    • 10x 程序员工作法
      • index
    • Node开发实战
      • HTTP服务的性能测试
      • JavaScript语言精髓与编程实战
      • 什么是node。js
      • svg精髓
    • ReactHooks核心原理与实战
      • ReactHooks核心原理与实战
    • Rust
      • Rust编程第一课
      • 前置篇
      • 深度思维
      • 重构
      • 类型体操
      • 基础知识
    • WebAssembly入门课.md
      • 基础篇
      • SSR的注水和脱水
      • jsBriage通信原理
      • 基础知识篇
    • 互联网的英语私教课
      • 互联网人的英语私教课
    • 代码之丑
      • 代码之丑
    • 前端全链路优化实战课
      • 网页指标
    • 图解 Google V8
      • 图解 Google V8
    • 浏览器工作原理与实践
      • 浏览器工作原理与实践
    • 算法面试通关 40 讲
      • 算法面试通关40讲
    • 跟月影学可视化
      • index
    • 软件设计之美
      • 软件设计之美
    • 重学前端
      • js
  • 后续的文件增加都会增加到上面并以编号对应
    • 1029. 两地调度
    • 151.翻转字符串里的单词
    • 2022.3.15
    • 前端数据结构
    • 前端常见算法
    • 前端常见排序
    • 恢复一棵树
  • 设计模式
    • 前端常见设计模式之MVC与MVVM
    • 前端之代理模式
    • 前端常见设计模式之单例模式
    • 前端常见设计模式之发布订阅模式
    • 前端之工厂模式
    • 观察者模式
    • 前端常见设计模式之适配器模式
  • 译文
    • [译] 如何使用CircleCI for GitHub Pages持续部署
    • 您是否优化了 API 的性能
    • [译][官方] Google 正式发布 Flutter 1.2 版本
    • 什么是 Deno ,它将取代 NodeJS ?
  • 读后感
    • JavaScript二十年
    • 1368个单词就够了
    • js编程精解
    • labuladong 的算法小抄
    • lodash常用方法
    • vue的设计与实现
    • 所有的静态资源都是get请求
    • 人生
    • 人生护城河
    • 你不知道的JavaScript
    • 前端核心知识进阶
    • 华为工作法
    • 反脆弱
    • 好好学习
    • 左耳听风
    • 摩托车维修之道
    • 数学之美
    • 深入理解svg
    • 浏览器的ESM到底是啥
    • 经济学原理
    • 编程珠玑
    • 防御式 css 精讲
    • 韭菜的自我修养
  • 雪狼
    • 2022-07-17
    • 基础知识
    • 阶一课程
      • 实战辅导一
      • 实战辅导二
  • 嵌入式
    • 树莓派
      • 排序
  • 源码
    • React
      • 核心知识点
      • errorBoundaries
      • immutable.js 的实现原理
      • React.Suspense
      • react源码分析之Fiber
      • batchedUpdate
      • Component
      • Context
      • react 源码分析之 diff 算法
      • React 中的 key 属性:原理、使用场景与注意事项
      • 使用方式
      • react源码分析之memo
      • react 源码分析之mixin
      • 实战篇
      • react源码分析之react-dom
      • 使用方式
      • scheduleWork
      • useImperativeHandle的使用与原理
      • React 书写小技巧
      • 入口和优化
      • 合成事件和原生事件的区别
      • react 性能优化
      • 构建一个 hooks
      • 浅析 styled-components
      • 生命周期
      • 组合 vs 继承
      • 通信机制
      • 高阶组件
      • 慕课网
        • 应用篇
        • 课程导学
    • ReactHook
      • useCallback
      • useContext
      • useEffect 与 useLayoutEffect
      • useHook
      • useMemo
      • useReducer
      • 原理
      • useState
      • 总结
    • Redux
      • mobx 原理解析
      • redux-saga
      • redux-thunk
      • Mobx 和 Redux 对比
      • 使用方法
      • redux 原理
    • Vite
      • Vite原理
      • Vite配置
      • 热更新原理
      • vite 为什么生产环境用 Rollup
    • Webpack
      • PostCSS
      • Webpack5 核心原理与应用实践-loader
      • Webpack5 核心原理与应用实践-plugin
      • Webpack5 核心原理与应用实践
      • 区分
      • 升级详情
      • treeShaking(树摇Tree Shaking)
      • 编写一个自己的webpack插件plugin
      • 代码分离(code-splitting)
      • webpack 打包优化
      • 基础配置
      • webpack 打包优化
      • webpack 工作原理
      • webpack 按需加载原理
      • webpack 热更新 HMR(Hot Module Replacement)
      • 缓存
      • webpack 自定义 plugin
    • next
      • tailwind
      • 什么是水合
    • sveltejs
      • index
    • tinymce
      • 并发篇
    • 源码手写系列
      • create
      • call
      • bind
      • call
      • es6 单例
      • forEach vs Map
      • instanceOf
      • new
      • reduce
      • 取两个重复数组的交集
      • 函数柯理化
      • 动态规划
      • 基于Generator函数实现async
      • 新建 js 文件
      • 手写一个 slice 方法
      • 手写一个 webpack loader
      • Plugin
      • 手写一个寄生组合式继承
      • 二叉树
      • 链表相关的操作
      • 手动实现发布订阅
      • 数组去重
      • 数组扁平化
      • 数组
      • 构造大顶堆和小顶堆
      • 深浅拷贝 深拷贝
      • 两者对比
    • vue
      • vue2
        • vm.attrs与$listeners
        • vue 和 react 的 diff 算法比较
        • vue 源码分析
        • vue 优化的 diff 策略
        • extends
        • 核心原理篇
        • keep-alive
        • vue 源码分析之 mixins
        • vue 源码分析之 nextTick
        • vue之slot
        • vnode
        • vue 源码分析之 watch
        • 原理
        • vue 源码分析之transition
        • vue 源码分析之异步组件
        • 调用的是 watch
        • 安装
        • react源码分析之portals
        • event 的实现原理(事件的实现原理)
        • 什么是h
        • 分析provide 和 inject
        • vue 源码分析之 use
        • v-model
        • vue源码分析之vuex
        • 响应式原理
        • 初始化的流程
        • 组件更新
        • 编译
        • 父子组件生命周期
        • 原理
        • 多实例
        • Vue 面试
        • 源码研读一
        • 响应式原理
        • 常见问题
        • 数组的劫持
        • vue之自定义指令
        • 运行机制全局概览
      • vue3相比vue2的提升点
        • vue composition api
        • vue3的虚拟dom优化
        • vue3层面的双向数据绑定
        • 预处理优化
  • 重构
    • notification
      • 讲解
  • 面试
    • AfterShip经历
      • JS对URL进行编码和解码
      • ShippingLabelTemplate
      • 接入keycloak详解
      • reCAPTCHA接入
      • yalc与动态解决升级的依赖包
      • RBAC 简介
      • 多语言计划
      • 接入Google登录及其主动弹出快捷登录方式
      • 读书计划
        • 传染
        • 这就是OKR
    • 编程猫经历
      • 2024.1.16
      • 2025.2.20
      • 2025.2.21
      • 2025.2.26
      • 2025.3.28
      • 2025.3.3
      • 2025.3.7
      • 行动轨迹
      • 面试主观题
    • 腾讯经历
      • 2022.02.21
      • 2022.03.30
      • 2022.04.24
      • 2022.04.25
      • 2022.04.27
      • 2022.04.28
      • 2022.04.29
      • 2022.05.05
      • 不同公司的面试关注点不同
      • 2022.05.07
      • 2022.05.09
      • 2022.05.10
      • 2022.05.11
      • 2022.05.12
      • 2022.05.13
      • 2022.05.16
      • 2022.05.17
      • 2022.05.19
      • 2022.05.27
      • 面试
      • 行动轨迹
      • 面试主观题
    • 针对字节
      • 2022.05.14
      • 2022.05.17
      • HR面试准备
      • Promise的相关题目
      • React 进阶实践指南(二)
      • React 面试准备
      • vue 与 react 有什么不同 (react 和 vue 有什么区别)
      • TypeScript 全面进阶指南
      • cookie和session区别
      • express 面试准备 koa 中间件原理
      • next面试准备
      • requestCallBack
      • interface 与 type 异同点
      • 取消 promise
      • 如何设计一个前端项目
      • 进阶篇
      • 早早聊面试准备
      • 自动化部署
      • 挖掘项目的深度
      • 面试
      • 出题指数
    • 魅族经历
      • 2020.09.11
      • 一灯
      • 一灯
      • 一灯
      • 2020.09.20
      • 2020.09.21
      • 网易二面
      • 2020.09.23
      • 头条
      • 360 金融面试题
      • 富途一面
      • 算法
      • 字节
      • 2020.11.04
      • baidu 一面
      • meta 标签的作用
      • 字节
      • 2020.11.22
      • 2020.11.25
      • 微前端接入笔记
      • 面试的基本原则
由 GitBook 提供支持
在本页
  • 全链路
  • 全链路指标
  • 06 | 指纹 id:关联起全链路日志
  • 网页指标
  • 目前,网页指标分为两部分,一部分是核心网页指标,一部分是其它网页指标。
  • 09 | 接口指标: 监听接口状态,收集页面接口异常数据

这有帮助吗?

  1. 极客时间
  2. 前端全链路优化实战课

网页指标

上一页前端全链路优化实战课下一页图解 Google V8

最后更新于2个月前

这有帮助吗?

全链路

前端全链路也是通过一定的机制记录前端交互的过程。在前端领域里面,“链路”代表的是页面与页面之间的访问关系,前端与后端的调用关系,用户在页面的交互过程等一系列的动作。“全”是指完整的、可溯源的、可记录的用户路径。

前端全链路是指从用户在前端页面发起操作开始,经过前端应用的各个组件和逻辑,最终与后端服务器进行数据通讯并完成用户期望功能的整个过程。

前端全链路的核心是监控与优化。

但也正是在这个时候,动态语言开始发展。此时 Web 页面的全链路问题排查、调试和优化,基本上都以解决浏览器页面兼容问题、处理脚本错误、结合动态语言调试等页面优化为主。

兼容、数据、交互、性能。这 4 种类型基本上形成了前端全链路最核心的问题维度。

用户视角的全链路:各个操作

--- 各个操作,包括交互,例如点击、手势、发起请求,接收请求、键盘输入等。

工程师视角的全链路: 用户失败、性能、执行报错等

--- 从用户发起请求,到用户完成所有产品功能的全路径,包括浏览器请求、服务器运行、浏览器返回、浏览器渲染、页面性能、脚本执行过程(包括失败、脚本错误)

03 |全链路流程:前端全链路的关键路径有哪些?

前端处理问题的四个维度,分别是数据维度、交互维度、性能维度、兼容维度。

前端全链路的实现方案中有三个关键要素:数据指标、监控、决策。这三个要素之间相辅相成,形成了一套完整的前端解决方案。

数据指标和监控的关系,更像是前后端互相配合的关系。

在前端技术领域,数据指标非常广泛,但不外乎 3 个维度:交互、性能、数据。7 种类型数据:PVUV 指标、基础指标、性能指标、行为动作指标、脚本异常日志、接口状态日志、资源状态日志。其中,基础数据是一套前端通用数据,用来记录其关联的数据,例如 UserAgent、URL、UserId 等等。

至于监控的核心职责就是发现问题并通知决策者,同时还能让我们通过日志库快速定位问题。

这个要素中,存在 3 个主要路径:日志分析、设置阈值监控、通知。如果问题跟产品功能相关,那么最佳的决策者就是产品。假如是性能问题,那么前端同学就是最合适的决策角色。

04 | 数据结构:如何设计全链路数据模型

如何定义链路日志数据结构?

基础数据指标是全链路日志的通用数据,也是其他 6 种指标数据类型必备的字段

基础数据类型中一共有 17 个属性字段,又细分为 5 种类别:基本日志信息、浏览器信息、页面信息、用户信息、业务信息。

  • 基础日志信息

  1. 基础日志数据。顾名思义,这是链路日志的必要数据,包含了唯一键值、时间、类型以及最后更新时间。

首先,我们定义四个必要的字段,并将类型的名称命名为 BaseTrace。

// ./typing.d.ts

// 全链路日志基类
type BaseTrace = {
  // 唯一ID,用户侧生成
  traceId: string;
  // 日志类型
  type: TraceTypes;
  // 日志产生时间
  createdAt: string;
  // 日志最后更新时间
  updatedAt: string;
};

其中类型

// ./typing.d.ts

enum TraceTypes {
  // PVUV
  PAGE_VIEW = "PageView",
  // Event
  EVENT = "EVENT",
  // 性能
  PERF = "Perf",
  // 资源
  RESOURCE = "Resource",
  // 动作、行为类型
  ACTION = "Action",
  // 请求类型
  FETCH = "Fetch",
  // 代码错误
  CODE_ERROR = "CodeError",
  // 日志
  CONSOLE = "Console",
  // 其它
  CUSTOMER = "Customer",
}

其中,最核心的类型是 EVENT、PERF、ACTION、FETCH 和 CODE_ERROR,分别对应的是事件、性能、行为、接口和代码异常五大类。Action 类型在这里主要是记录用户的交互行为,作为我们分析用户行为的参考。

  • 浏览器信息

分别是浏览器的 UserAgent 以及预定义的 BrowserType 枚举类型。

// ./typing.d.ts

enum BrowserType {
  // 手机端浏览器
  MOBILE = "mobile",
  // PC浏览器
  PC = "pc",
  // webview
  WEBVIEW = "webview",
  // 小程序
  MINI_PROGRAM = "miniProgram",
}

// 浏览器相关字段基类
type BaseBrowserTrace = {
  // 当前浏览器的UserAgent
  ua: string;
  // 浏览器类型
  bt: BrowserType;
};
  • 页面信息

页面类型记录的是用户访问当前页面的一些基本信息,例如页面 URL、标题等。

页面类型属性的其中一个功能是帮助我们快速判断哪些页面出现了严重 BUG。同时,在监控中通过 URL 筛选,也可以判断是通用性问题还是个例问题。

// ./typing.d.ts

// 页面相关字段基类
type BasePageTrace = {
  // 页面ID
  pid: string;
  // 页面标题
  title?: string;
  // 当前页面URL
  url: string;
};
  • 用户信息

用户信息记录的是用户的一些基本信息,例如用户 ID、用户名、用户类型等。

// 用户相关字段基类
type BaseUserTrace = {
  // 指纹ID,fingerprintId
  fpId: string;
  // 用户ID
  uid?: string | number;
  // 用户名称
  userName?: string;
  // 用户邮箱
  email?: string;
};
  • 业务信息

业务信息记录的是用户在页面中的一些业务信息,例如页面 ID、页面标题等。

enum TraceLevelType {
  // 告警级别
  error = "error",
  // 预警级别
  warn = "warn",
  // 普通日志
  info = "info",
  // 调试日志
  debug = "debug",
}

enum TraceClientTypes {
  // 安卓
  ANDROID_H5 = "android",
  // iOS
  IOS_H5 = "ios",
  // PC端
  PC_H5 = "pc",
  // 浏览器
  BROWSER_H5 = "browser",
}

// 业务相关字段基类
type BaseAppTrace = {
  // 业务ID
  appId: string;
  // 业务名称
  appName?: string;
  // 客户端类型
  clientType: TraceClientTypes;
  // 日志级别
  level: TraceLevelType;
};

一条链路日志,不应该被过度设计

type BaseTraceInfo = BaseTrace &
  BaseBrowserTrace &
  BaseUserTrace &
  BaseAppTrace &
  BasePageTrace;

下面是上报的完整类型

const exampleBaseData: BaseTraceInfo = {
  traceId: "0bdf6c8e-25c8-427d-847a-9950318a2e14",
  level: TraceLevelType.warn,
  type: TraceTypes.ACTION,
  ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
  bt: BrowserType.MOBILE,
  fpId: "c77a37f4",
  uid: 1002,
  // 例如:极客邦App的命名
  appId: "kn-app",
  clientType: TraceClientTypes.iOS_H5,
  pid: "088c8a92-5a24-4144-9c37-310848c397e1",
  url: "https://time.kn.org/",
  createdAt: "",
  updatedAt: "",
};

全链路指标

代码维度把日志分成了异常信息、性能数据、操作行为三套数据模型

// 完整的全链路日志
type TraceData = BaseTraceInfo & {
  // 记录错误信息
  data?: TraceTypeData;
  // 记录操作行为
  breadcrumbs?: TraceBreadcrumbs;
  // 记录性能信息
  perf?: TracePerf[];
};

异常信息类型

代码异常。不仅包括脚本失败,还应该包含 trycatch 中的 catch 异常。

Promise 异常。这主要是由异步代码引起的逻辑问题。

网络请求。通常来说,发起 http 请求都无法保证 100% 的成功率。

资源异常信息。和网络请求同理,但这里的重点在于监控图片和一些外部资源的请求状态。

PV/UV 日志。用于记录用户访问的次数和频率,它也是唯一的非异常日志。

自定义日志。目的是在一些特殊场景下记录日志,以便快速定位问题。

通用数据字段

// 通用数据字段
type TraceBaseData = {
  // id
  dataId: number;
  // 日志信息名称
  name: string;
  // 问题级别
  level: TraceDataSeverity;
  // 异常信息
  message: string;
  // 发生时间
  time: number;
  // 问题类型
  type: TraceDataTypes;
};

dataId。这个字段不是指唯一键值,而是对异常信息进行哈希后得出的字符串。如果有大量相同字符串,那就说明存在通用异常问题。name。这是为异常日志提供的一个名称字段。message。这个字段的核心作用是记录异常信息的详情或者描述。time。顾名思义,就是时间。指事件触发的时刻。

网络请求类型

// 请求类信息
type TraceDataFetch = TraceBaseData & {
  // 执行时间,用于统计耗时
  elapsedTime: number;
  // 请求方法
  method: "POST" | "GET";
  // 请求类型
  httpType: "fetch" | "xhr";
  // 请求地址
  url: string;
  // 请求参数
  body: string;
  // 响应状态
  status: number;
};

其余类型字段(Promise、资源、自定义、PV/UV)剩余的其它数据类型,包括 Promise 类型、资源类型以及自定义日志类型。由于公共字段已经满足全链路概念的能力,所以,基于最少字段原则,对于 Promise 和 Resource,我不会再扩展过多的字段。至于自定义日志类型,我会增加一个 tag 字段作为对上层业务内容的补充,既可以当成标签使用,也可以当作一段需要标记的字符串。还有 PV/UV 统计类日志,也只补充一个用于 SPA 的静态路由字段,记录用户当前所在的页面。

// Promise类型
type TraceDataPromise = TraceBaseData;

// 资源类型
type TraceDataResource = TraceBaseData;

// 普通日志
type TraceDataLog = TraceBaseData & {
  tag: string;
};

// PV/UV
type TraceDataPageView = TraceBaseData & {
  route: string;
};

操作行为类型

// 基类行为日志类型
type TraceBaseAction = {
  // 动作名称
  name: string;
  // 动作参数
  level: TraceDataSeverity;
  // 动作时间
  time: string;
  // 日志类型
  type: BreadcrumbTypes;
  // 行为分类
  category: BreadcrumbsCategorys;
};

// 行为日志
type TraceAction = TraceBaseAction & {
  // 行为动作相关的信息,可以是DOM,可以是错误信息,可以是自定义信息
  message?: string;
  // 请求参数
  request?: any;
  // 请求结果内容
  response?: any;
  // 错误堆栈信息
  stack?: string;
};

完整的链路日志

// 一份错误信息的类型集合
type TraceTypeData =
  | TraceDataFetch
  | TractDataCodeError
  | TraceDataPromise
  | TraceDataResource
  | TraceDataLog
  | TraceDataPageView;

// 操作行为日志
type TraceBreadcrumbs = TraceAction[];

// 完整的全链路日志
type TraceData = BaseTraceInfo & {
  // 记录错误信息
  data?: TraceTypeData;
  // 记录操作行为
  breadcrumbs?: TraceBreadcrumbs;
  // 记录性能信息
  perf?: TracePerf[];
};

demo

// trace data json
{
  "traceId": "0bdf6c8e-25c8-427d-847a-9950318a2e14",
  "level": "warn",
  "type": "Action",
  "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
  "bt": "mobile",
  "fpId": "c77a37f4",
  "uid": 1002,
  "appId": "geekbang-h5",
  "clientType": "browser",
  "pid": "088c8a92-5a24-4144-9c37-310848c397e1",
  "url": "https://time.geekbang.org/",
  "createdAt": "2024-03-13T15:35:30.292Z",
  "updatedAt": "2024-03-13T15:35:30.292Z",
  "data": {
    "dataId": 2384780,
    "name": "fetch-api",
    "level": "info",
    "message": "success",
    "time": 1710345961943,
    "type": "HTTP",
    "elapsedTime": 166.34,
    "method": "POST",
    "httpType": "fetch",
    "url": "https://time.geekbang.org/serv/v3/product/infos",
    "body": "{\"ids\":[100035801,100002401,100024001,100007001,100003901,100029601,100027801,100034101,100042501,100023701]}",
    "status": 0
  },
  "breadcrumbs": [
    {
      "name": "fetch-api",
      "level": "info",
      "time": "string",
      "type": "Fetch",
      "category": "hhtp"
    }
  ]
}

06 | 指纹 id:关联起全链路日志

所以,我们只要收集这些具有较高辨析度的信息,并进行一定的计算处理,就能生成一个能唯一标识当前浏览器的值,也就是我们所说的指纹 ID。

但在这里,我们的目的是追踪前端应用的用户行为,是为了帮助用户提升体验,快速解决问题。一定要记住,我们的出发点并不是获取用户的隐私。

指纹生成方案的选择

  1. fingerprintjs

  2. 使用 cryptojs 生成指纹 ID

  3. 使用 canvas 生成指纹 ID

轻量级的这个方案就是帆布指纹识别技术。它是一种利用帆布纹理特征进行身份验证的方法。具体实现的思路,就是利用 HTML5 的画布的 Canvas 特性,通过 Canvas 生成的图片,然后转换成哈希码,从而形成用户指纹。

const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const txt = "kn-editor";
ctx.textBaseline = "top";
ctx.font = "14px 'Arial'";

ctx.fillStyle = "#f60";
// 先画一个60x20矩形内容
ctx.fillRect(125, 1, 60, 20);
// 把字填充到矩形内
ctx.fillStyle = "#069";
ctx.fillText(txt, 2, 15);

然后,我们采用社区提供的转换方案,将填充的矩形和文字的画布转换成 Base64 字符串。接下来,我们使用 atob 函数对 Base64 字符串进行编码,最后截取一部分字符,将其转换成十六进制字符串。

const b64 = canvas.toDataURL().replace("data:image/png;base64,", "");
const bin = atob(b64);
const crc = bin2hex(bin.slice(-16, -12));

定义了以下四个指纹参数类型。

type FingerprintOptions = {
  font?: string
  reactStyle?: string | CanvasGradient | CanvasPattern
  contentStyle?: string | CanvasGradient | CanvasPattern
  textBaseline?: CanvasTextBaseline;
};

网页指标

目前,网页指标分为两部分,一部分是核心网页指标,一部分是其它网页指标。

核心网页指标,是围绕用户体验的三个方面去衡量,包括加载响应速度(LCP)、互动交互(FID)以及视觉稳定性(CLS)。

其它网页指标,包括首次内容绘制(FCP)、互动延迟(INP)、可交互时间(TTI)、总阻塞时间(TBT)、首字节时间(TTFB)等等。

  1. 网页加载速度指标(LCP)

这个指标实际上是记录页面首屏可见区域中最大元素的呈现时间。而最大元素可能是一张图片,可能是一个视频,也可能是一段文本内容块。具体记录哪一种元素,与页面首屏所呈现的内容有关。

  1. 网页可交互指标(FID)

第二个核心网页指标就是 FID,全称为 First Input Delay,其含义就是从用户首次和 Web 应用互动到浏览器实际开始处理事件或者处理脚本,响应用户互动的这段时间。

如果说,LCP 是衡量感受 Web 应用加载速度的第一印象的话,那么 FID 就是衡量你和 Web 应用互动的第一印象。

一个侧重加载,一个侧重互动,也即响应能力。

特别是移动端 Web 应用,由于用户环境复杂,我们经常会遇到,当在手机上加载一个 Web 页面时,看到页面内容那一刻(此时可能页面还没完全渲染完成),就会很自觉地快速找到我们想要的操作位置(例如表单、按钮等),并尝试和页面进行互动(例如弹出键盘并输入、点击播放按钮等),但此时,页面仍然没有任何反应。

  1. 网页视觉稳定性指标(CLS)

第三个核心指标就是视觉稳定性指标(CLS),英文全称 Cumulative Layout Shift。这个指标更多的是判断用户的视觉上的体验,也就是在浏览器可视区内现有元素发生位置的改变,触发布局偏移,是否影响用户的使用体验。

  1. 首次内容绘制(FCP)

首次内容绘制也是一个非常重要的指标。我认为如果三大核心网页指标作为排名前三重要的话,那排在第四位的就是 FCP。

从这样的概念理解,FCP 的时间必定小于 LCP,在 FCP 和 LCP 之间的时间差里,就是浏览器持续性地渲染加载页面内容,又或者在执行脚本的过程。

  1. 可交互时间(TTI)

可交互时间(TTI),顾名思义,就是衡量从网页开始加载到其主要子资源加载完成的时间。从定义上看,这个时间比 FCP 更靠前。

  1. 总阻塞时间(TBT)

首次内容绘制 FCP 之后还存在如 LCP 等其它指标。在基于 FCP 时间之后,页面会发生很多不同场景的事件,比如渲染内容、请求接口、主线程脚本执行等。而在这个时间范围内,可以定义一个用于衡量事件或任务的总阻塞时间,我们称为 TBT,它能帮助我们快速判断在这个时间范围内,哪些事件耗时较长、哪些接口请求时间长等等。

  1. 首字节时间(TTFB)

首字节时间 TTFB,就是资源请求与响应的第一个字节开始到达之间的时间。TTFB 就是衡量 Performance.startTime 和 Performance.responseStart 之间的间隔时间。

虽然 TTFB 并不是核心网页指标,但它是衡量 Web 应用可用性的 最重要的参考指标。它能帮助我们分析和评估在到达用户设备之前的网络状况。

09 | 接口指标: 监听接口状态,收集页面接口异常数据

监听请求目的是什么?

  1. 监控前端发起请求的稳定性

  2. 关注用户的网络情况

  3. 现在许多接口功能具备登录鉴权和用户画像等特性,几乎每个用户都有其独特的数据或者分组,所以我们还需要判断接口返回的信息是否合理。

export const getFetchStatusLevel = (status: number): TraceDataSeverity => {
  if (status >= 500) {
    return TraceDataSeverity.Critical;
  } else if (status >= 400) {
    return TraceDataSeverity.Error;
  } else if (status >= 300) {
    return TraceDataSeverity.Warning;
  } else if (status >= 200) {
    return TraceDataSeverity.Info;
  } else {
    return TraceDataSeverity.Else;
  }
};

请求中断

try {
  res = await fetch(url, options);
} catch (err) {
  if (err.name == "AbortError") {
    // 发现被中止请求后,处理的逻辑
  } else {
    throw err;
  }
}

请求超时

const controller = new AbortController()
const { signal } = controller

const handleTimeout = (delay = 6000) => {
  return new Promise(_, reject) => {
    setTimeout(() => {
      // 触发中止
      controller.abort()
      reject(new Error("TimeoutAbortError"));
    }, delay);
  });
};

// 处理请求超时
Promise.race([
  handleTimeout(),
  fetch(url, {
    signal
  })
]).catch(err => {
    if (err.name === 'TimeoutAbortError') {
      // 请求超时
    }
})

由于我们需要在不耦合代码的前提下自动捕获 HTTP 状态码,所以,基于现有代码,结合前端全链路的数据指标设计方案,我们将增加三个逻辑。首先,由于我们只关心非正常状态,所以我们在拦截器里只关注非 2xx 的状态码。其次,我们将在 interceptFetch 函数中新增 onerror 函数参数,以便在请求异常时为外层提供自定义逻辑参数。最后,在拦截器内增加请求耗时,同时以 elapsedTime 参数传入 onerror 函数。具体的代码实现逻辑,参考如下。

import { getTimestamp } from "./util";
const { fetch: originFetch } = window;

export type OnFetchError = {
  url: string;
  status: number;
  statusText: string;
  method: "POST" | "GET";
  body: any;
  elapsedTime: number;
};

export type InterceptFetchType = {
  onError: (error: OnFetchError) => void;
};

// 拦截fetch
const interceptFetch = ({ onError }: InterceptFetchType) => {
  return async (...args: any) => {
    const [url, options] = args;
    const startTime = getTimestamp();
    let res;
    try {
      res = await originFetch(url, options);
    } catch (err) {}
    if (!(res.ok && res.status >= 200 && res.status < 300)) {
      onError({
        url,
        status: res.status,
        statusText: res.statusText,
        method: options.method,
        body: options.body,
        elapsedTime: getTimestamp() - startTime,
      });
    }

    return res;
  };
};

export default interceptFetch;

10 |资源和脚本异常指标:监听资源和脚本状态,收集异常数据

怎么全局监听前端资源状态

window.addEventListener('error', (event) => {
  let target = event.target || event.srcElement;
  let isResTarget = isResourceTarget(target as HTMLElement);
  if (isResTarget) {
  // 处理全链路关注的前端资源
    saveResourceError(event)
  }
})

isResourceTarget 函数, 他是一个元素过滤器,可以筛选前端页面重点关注的资源文件。

export const isResourceTarget = (target: HTMLElement) =>
  target instanceof HTMLScriptElement ||
  target instanceof HTMLLinkElement ||
  target instanceof HTMLImageElement ||
  target instanceof HTMLVideoElement ||
  target instanceof HTMLAudioElement;

isResourceTarget 函数只判断匹配了脚本、链接、图片、音频和视频这 5 种常见的元素资源。如果需要监控更多的前端资源,你可以根据项目的实际情况扩展该函数。

第二个函数是 saveResourceError 函数,它负责记录资源错误。

public saveError(event: ErrorEvent) {
  const target = event.target || event.srcElement;

  const nodeName = (target as HTMLElement).nodeName

  const url = (target as HTMLElement).getAttribute('src') || (target as HTMLElement).getAttribute('href')

  const dataId = hashCode(`${nodeName}${event.message}${url}`)

  const traceDataResource: TraceDataResource = {
    dataId,
    name: 'resource-load-error',
    level: TraceDataSeverity.Error,
    message: event.message,
    time: getTimestamp(),
    type: TraceDataTypes.RESOURCE
  }
  this.resources.push(traceDataResource)
}

是否可以发现加载慢的资源

export class BaseTrace implements BaseTraceInterface {

  public observer = null

  public constructor(options: TraceOptions) {
    // 忽略其它业务逻辑

    this.observer = new PerformanceObserver((list, observer) => {
      list.getEntries().forEach((entry) => {
        if (entry.entryType === 'resource') {
          this.handleObserverResource(entry as PerformanceResourceTiming)
        }
      });
    });
  }
}

如果当前实体资源是 resource 类型,那就要进入 handleObserverResource 函数处理逻辑。

handleObserverResource 函数内部。首先是通过实体资源对象的 duration 属性值判断耗时时长。我们设定了每个资源加载时长超过 1 秒时,就会上报全链路监控。具体的实现逻辑如下代码。

export class BaseTrace implements BaseTraceInterface {

  public resources: TraceDataResource[] = []
  public observer = null

  public handleObserverResource(entry: PerformanceResourceTiming) {
    if (entry.entryType === 'resource') {
      let level = TraceDataSeverity.Info
      if (entry.duration > 1000 && entry.duration < 1500) {
        level = TraceDataSeverity.Warning
      } else  if (entry.duration > 1500) {
        level = TraceDataSeverity.Error
      }
      entry.duration > 1000 && this.resources.push({
        url: entry.name,
        name: `${entry.entryType}-duration-${entry.initiatorType}`,
        type: TraceDataTypes.PERF,
        level,
        message: `duration:${Math.round(entry.duration)}`,
        time: getTimestamp(),
        dataId: hashCode(`${entry.entryType}-${entry.name}`),
      })
    }
  }
}

11 | 用户行为指标:如何有效监听用户交互行为?

我总结出一共有 5 种行为类型,作为全链路里的用户行为日志,它们分别是操作类型、请求类型、全局错误、框架内置错误以及自定义上报错误。

操作类型,也就是用户的操作行为。现在的前端页面的输入方式种类非常多,不仅有点击、触屏、键盘等传统的输入方式,还有手势、人脸识别、AI 这种非接触式的输入方式。操作类型指的就是这些输入方式。

请求类型,也就是记录使用 Fetch 这类发起接口请求的状态。我们之所以要强调记录请求类型,是因为随着业务的深入发展,前端依赖接口以及画像数据的情况会越来越多,记录它是为了追踪接口的输入输出,以便辅助定位问题。另外,请求接口是一个输入输出的异步过程,所以一次请求,一定包含请求前和请求后两条行为记录。

全局错误。我们上节课学习了捕获全局 Error 事件的方法,它能捕捉脚本错误和资源错误。除了全局 Error 事件,前端还存在一种异步异常事件,它就是基于 Promise 的异步事件,其中 reject 回调事件通常都会被作为异常处理。

框架内置错误。实际上就是以 React 或 Vue 技术框架实现的前端项目。通常这类框架都有内置错误捕获事件机制,对于我们监听基于框架实现的业务逻辑异常错误有很大的帮助。

自定义上报类型。这种类型是为了业务异常而存在。有时候有些业务逻辑场景就是不能往下走的,例如本节课的例子,我们可以在不满足用户群组的条件下,上报一条自定义日志,这样就能减少排查问题所耗费的时间了。

// src/baseTrace.ts

export class BaseTrace implements BaseTraceInterface {
  // 记录用户行为
  public breadcrumb: TraceBreadcrumbs = []

  // 最大存储用户行为
  public maxBreadcrumb = 10
}

操作类型

  1. 以点击事件为例来记录点击交互行为

public onGlobalClick() {
  const _t = this
  window.addEventListener('click', (event) => {
    const target = event.target as HTMLElement
    const innerHTML = target.innerHTML
    const bc: TraceAction = {
      name: 'click',
      level: TraceDataSeverity.Normal,
      type: BreadcrumbTypes.CLICK,
      category: BreadcrumbsCategorys.User,
      message: innerHTML,
      time: getTimestamp()
     }
    _t.saveBreadcrumb(bc)
  })
}
image.png
前端解决方案
前端链路三要素
基础类型
image.png
image-5.png
解决问题的等级
交互行为日志类型