Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

前端优化数据量化必备神器:Grafana《现代前端工程体验优化》第一章 数据驱动 第二节 用户体验数据收集与可视化 #13

Open
JuniorTour opened this issue May 9, 2023 · 0 comments
Labels

Comments

@JuniorTour
Copy link
Owner

JuniorTour commented May 9, 2023

上一节:《你做的前端优化都错了!《现代前端工程体验优化》前言 && 第一章 数据驱动》

3. 用户体验数据收集与可视化:基于 Prometheus 及 Grafana 的实现简介

有了获取用户体验指标数据的工具,还需要进一步大量收集、细致分析这些数据,以便了解现状、确定优化方向。

笔者推荐一套经过实践检验、开发体验较好的开源工具:Prometheus 和 Grafana。

1. Prometheus 及 Grafana 简介

Prometheus 是一款开源的数据监控解决方案,包括针对各种编程语言的数据采集SDK(例如面向 Node.js 的NPM包客户端:prom-client)、接收数据上报的后端服务器应用、基于时间序的数据库、以及基础的数据可视化前端应用,具有强大的拓展能力,可以方便快速地融合进已有的项目中,作为数据监控的中台工具。

Grafana 是一款开源的数据可视化工具,主要包括兼容 Prometheus 在内各种数据库的数据查询工具、内置各种图表模板的数据可视化前端应用,并且支持免费的私有化部署。

将 Prometheus 与 Grafana 整合进我们的项目中就能方便地实现强大的数据收集、可视化能力。

下面我们以Node.js为例,演示如何接入这2款工具。

2. 接入演示

我们将基于:

  • Grafana 官方的云端应用
  • 本地环境自建的 Node.js 服务器应用
  • Node.js 的 Prometheus 数据收集SDK:prom-client

演示如何从本地环境收集 web-vitals 数据,并上传到 Grafana,最终创建出数据可视化图表。

首先我们注册并登录 Grafana 官方的云端应用:https://grafana.com/get/?pg=graf&plcmt=hero-btn-1 ,每个账户都有足够的免费试用额度供我们测试。

完成注册后,我们就会进入 Grafana 的看板页面,这里是我们管理数据可视化图表、接入数据源的主要工作区域。

但因为此时我们还没有接入数据,所以内容还是一片空白。接下来我们启动一个基于 prom-client 的 Node.js 服务器作为我们的数据收集应用。

首先我们从左侧侧边栏访问 Connection > Connect data 目录,搜索 node.js 即可找到官方推荐的 Node.js 应用接入基础设施:

不同的数据源有不同的特性,例如:

  • 查询语句的格式
  • 是否支持日志全文查询

Grafana 支持众多数据源,如果 Prometheus 不适合你的前后端架构,可以参考官方文档选择ElasticsearchGraphite等其他数据源:https://grafana.com/docs/grafana/latest/datasources/

如果只需要指标收集和存储,建议选择 Prometheus 或 Graphite;
如果需要进行日志收集和分析,对日志全文做可视化,建议选择 Elasticsearch;

点击进入 Node.js Infrastructure 后,接下来我们按照官方文档首先检查必要准备:

安装 Grafana Agent 应用用于收集、转发本地的Prometheus数据:

选择对应系统,输入 API key 的备注名,点击 Generate API token 生成接口令牌,随后会提示生成已完成:Your API token has been generated below.

接下来我们要运行官方提供的命令行从而下载、安装并配置 Grafana Agent 应用,下载和安装可能需要一定时间并克服网络限制:

注意:需要以管理员身份运行 PowerShell,运行方式如下图:

命令执行完成后,点击左下角的 Test agent connection 即可验证安装是否完成。

验证通过、安装完成后,就可以在本地通过Node.js应用收集并上报数据了,让我们新建一个空白 Node.js 项目:

  • 创建新目录:grafana-node-demo
  • 执行 npm init 初始化生成 package.json,并添加启动脚本:"start": "node app.js"
  • 安装必要NPM包依赖:npm install express prom-client
  • 新建 app.js ,将官方的代码示例复制粘贴下来

官方示例需要Node.js环境支持 ES module 语法,你也可以参考笔者提供的示例项目,直接使用更方便的 CommonJS module 语法:

《feat: 引入express && prom-client;初始化服务》commit:JuniorTour/node-prometheus-grafana-demo@7ba9148

运行 npm run start 后,prom-client就会自动开始采集一批默认的 Node.js 应用数据。

最后,让我们把预置的可视化图表,添加到我们的看板中,点击下图中的安装 Install 按钮:

安装后,一套监控 Node.js 应用内存用量、CPU使用率等指标的可视化看板就被添加到我们的看板中了:

我们的目标不只是监控这些基础指标,下面让我们试试如何增加自定义指标来。

Grafana 官方 Node.js 集成文档:https://grafana.com/docs/grafana-cloud/data-configuration/integrations/integration-reference/integration-nodejs/

各大云服务供应商也有集成 Grafana:

3. 增加自定义指标上报并创建可视化图表

为了将前端利用 web-vitals 收集的数据,发送到 Grafana,后端服务需要基于 prom-client 的能力,增加一批自定义指标。

让我继续在 app.js 中添加以下代码:

这部分可以参考笔者提供示例中的《feat: 增加自定义计数指标 webVitalsRatingCounter》commit:JuniorTour/node-prometheus-grafana-demo@6c7c57a

// 3. 对所有接口增加 跨域资源共享(CORS)配置
// Enable CORS for all routes
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*"); // Allow any origin
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); // Allow these headers
  if (req.method === 'OPTIONS') {
    res.header("Access-Control-Allow-Methods", "POST"); // Allow POST method
    return res.sendStatus(200);
  }
  next();
});

// 2. 新建一个自定义计数指标webVitalsRatingCounter
// https://www.npmjs.com/package/prom-client#:~:text=()%3B-,Custom%20Metrics,-All%20metric%20types
const webVitalsRatingCounter = new client.Counter({
  name: 'webVitalsRatingCounter',
  help: 'counter to store web-vitals rating data',
  registers: [register],
  labelNames: ['name', 'rating'],
});

// 1. 增加一个POST方法的接口
app.post('/post-web-vitals', function(req, res) {
  console.log(`req.body=${req.body}`)
  const labels = req.body.labels

  webVitalsRatingCounter.inc(labels)

  const message = `Get web-vitals: labels=${JSON.stringify(labels)}`
  console.log(message)
  res.status(200).json({ message });
});

通过这段代码,我们:

  1. 增加了一个POST方法的接口/post-web-vitals,用于接收前端发送来的 web-vitals 数据;
  2. 新建了一个自定义计数指标webVitalsRatingCounter,用于收到 web-vitals 数据计数、统计数据名称(name)、评分(rating);
  3. 对所有接口增加 跨域资源共享(CORS)配置,以便我们稍后从DEMO中获取的数据可以跨域发送给我们本地的https://localhost:4001/post-web-vitals接口;

基于这段代码我们就可以从前端应用中通过HTTP 异步请求发送 web-vitals 数据到这个后端服务,并借助 prom-client 的自定义计数指标webVitalsRatingCounter将数据上报到 Grafana 供我们查询、创建可视化图表。

接下来我们基于之前的《获取web-vitals数据 DEMO》: https://output.jsbin.com/bizanep 增加上报数据到 Node.js 后端的逻辑:

// 《发送 web-vitals 数据 DEMO》:https://output.jsbin.com/xifudez
async function sendDataToBackend(data) {
  if (!data) {
    return
  }
  await fetch('https://localhost:4001/post-web-vitals', {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        labels: {
          name: data?.name,
          rating: data?.rating
        }
      }),
  })
}

function onGetWebVitalsData(data) {
  if (!data?.name) {
    return
  }
  const name = data.name
  const value = data.value
  const rating = data.rating
  sendDataToBackend(data)
  const msg = (`(已发送到后端)${name}: value=${value}, rating=${rating}`)
  console.log(msg)
  setInnerHtml(name?.toLowerCase(), msg)
}

onFCP(onGetWebVitalsData);

我们的改动主要是:

  • 增加sendDataToBackend方法,用于接受到web-vitals库的数据后发送HTTP请求到后端接口,并把name, rating这2个字段作为请求体;
  • 在之前已有的onGetWebVitalsData方法中增加sendDataToBackend(data)方法的调用,将指标数据作为参数传入;

完整示例请参考 《发送 web-vitals 数据 DEMO》:https://output.jsbin.com/xifudez

运行这些新增改动后,我们将能在开发中工具的Network面板中看发送数据的HTTP请求,以及后端的响应信息:

有了这样一套前后端逻辑,我们的web-vitals数据就成功的发送到了 Grafana,接下来我们试着基于这些数据创建几张可视化图表。

4. 创建 Grafana 可视化图表指南

下面我们一起来试着创建一张 Grafana 的可视化图表,主要有以下几步:

1. 访问仪表盘(Dashboard)页面,点击新建仪表盘(New Dashboard)按钮

2. 点击增加可视化(Add visualization)

3. 选择(或确认)数据源

如果按照上文的接入演示一路操作过来,会有默认的数据源grafanaclound-yourName-prom

4. 输入查询语录

Grafana 中不同的数据源对应不同的查询器面板及语法,我们以前文示例的 Prometheus 数据源为例,其主要有 交互界面选择查询语句(Builder)和 直接输入查询语句文本(Code)2种模式。

推荐使用对新用户更为友好的Builder模式,可以看到包含以下各选项:

  • 指标(Metric)
  • 标签过滤(Label filters)

分别对应我们调用 prom-client 的 new Counter API 时传入的name,labelNames字段:

const webVitalsRatingCounter = new client.Counter({
  name: 'webVitalsRatingCounter',
  help: 'counter to store web-vitals rating data',
  registers: [register],
  labelNames: ['name', 'rating'],
});

我们分别输入并选择 webVitalsRatingCountername=FCPrating=good 三项后,

点击刷新仪表盘(Refresh Dashboard)即可看到我们刚刚上传的数据,将对应时段有多少次name=FCPrating=good 在可视化图表中展示出来:

不同的数据源有各自不同的查询语法

5. 调整样式、配置等细节

有了数据后,我们就可以进一步完善可视化图表的细节,常用的功能主要有:

  1. 图表类型:常用的有折线图、柱状图,纯文本;

  2. 标题和描述

  3. 提示(Tooltip)和图例(Legend):设置何时显示提示、图例的样式和内容;

  4. 度量选项(Standard Options):设置当前查询的值是什么单位,常用的有:

    1. 其他-短数值(Misc / short)
    2. 其他-百分比(Misc / Percent(0.0-0.1))
    3. 时间-毫秒(Time/ millisecond)
  5. 时间间隔:设置将多久的时间间隔内的所有数据,聚合数据为可视化图中的一个数据点。例如截图中5m即表示将5分钟内的数据统一计入可视化图中的一个点。例如下图中时间范围(Time Range)选择最近15分钟(Last 15 minutes),对应折线图就有3个点。

  6. 覆盖配置:设置查询语句显示名称、颜色等特殊配置

以上各项配置是个人总结较为常用的配置项,可以随意尝试调整,观察效果。

6. 分别保存图表和仪表盘

先后点击:

  • 图表编辑界面的右上角的应用(Apply)按钮,保存图表;
  • 仪表盘页面的保存仪表盘按钮,保存整体仪表盘,在此处可以输入仪表盘的名称及所属目录;

至此,我们就创建了第一张可视化图表,后续可以在实践中逐步熟悉 Grafana 的强大功能。这只是我们的第一步,接下来我会分享实践中遇到的问题和解决方案。

将数据记录并可视化,非常有成就感,可以让我们明确的感知到工作的产出。

当我们的数据和图表越来越多,我们对维护项目的了解也会越来越深入。

5. 堆叠百分比图优点及示例说明

从 web-vitals 获得的数据中比较有统计意义的是:

  • 各指标的数值(value),例如累计布局变化CLS的 0.2;
  • 评分(ratings):按官方标准对值进行划分得到的字符串值,共有优('good'),待提升('needs-improvement'),差('poor')三类值,可用于对数据进行标准化处理;
  • 指标的统计来源(sources):记录了计算各指标的来源,例如累计布局变化CLS的 sources 字段记录的就是每次意外布局变化对应的DOM元素,及其变化前后的位置尺寸数据;

例如这条从onFCPAPI获取的LCP数据:

{
    delta: 382.80000019073486
    entries: [
        {
            duration: 0
            element: p
            entryType: "largest-contentful-paint"
            id: ""
            loadTime: 0
            name: ""
            renderTime: 382.8
            size: 8985
            startTime: 382.80000019073486
            url: ""
        }
    ]
    id: "v3-1683034382854-2926018174544"
    name: "LCP"
    navigationType: "reload"
    rating: "good"
    value: 382.80000019073486
}

笔者更推荐使用评分字段作为可视化图表的主要指标。

原因是直接使用值经常会有异常波动,经常在前端项目没有任何变更的情况下,观察到值产生了显著的变化。

使用评分作为指标,相当于对观测的值做了一次标准化处理,将一定范围内的值处理成统一的评分,有助于规避个别极端值导致的异常波动。

如下图,4/22 前后的 LCP 平均值在我们的前端项目没有变更的情况下就出现了减少11%的变化,这样的异常波动显然不利于评估我们优化的效果:

笔者最初基于 web-vitals 制作数据可视化图时,用的就是值字段,计算了FCP等指标的平均值,观察一段时间后发现即使前后端项目没有上线变更,各指标的平均值也会有10%以上的波动,这显然是不符合预期的,这种波动将会降低我们评估优化效果的准确性。

后来改成基于评分字段制作各评分占比变化图后,数据波动问题就不再出现了,各评分的占比平均值在长时间内都能保持不超过5%的波动。当我们主动进行一些用户优化后就能观察到更客观,更有说服力的指标变化。

下面分享一套基于模拟数据的“堆叠百分比图”示例及配置源码,各位读者可以根据需要复制后粘贴到自己的仪表盘内,并替换为真实数据,从而得到客观稳定的优化效果评估数据。

堆叠百分比图配置源码

{
  "datasource": {
    "type": "testdata",
    "uid": "a95c7111-f01f-4e29-b3a6-9b0ac81d9094"
  },
  "description": "“堆叠百分比图”配置源码,作者:https://github.com/JuniorTour",
  "fieldConfig": {
    "defaults": {
      "custom": {
        "drawStyle": "line",
        "lineInterpolation": "linear",
        "barAlignment": 0,
        "lineWidth": 1,
        "fillOpacity": 70,
        "gradientMode": "none",
        "spanNulls": false,
        "showPoints": "auto",
        "pointSize": 1,
        "stacking": {
          "mode": "normal",
          "group": "A"
        },
        "axisPlacement": "auto",
        "axisLabel": "",
        "axisColorMode": "text",
        "scaleDistribution": {
          "type": "linear"
        },
        "axisCenteredZero": false,
        "hideFrom": {
          "tooltip": false,
          "viz": false,
          "legend": false
        },
        "thresholdsStyle": {
          "mode": "off"
        }
      },
      "color": {
        "mode": "palette-classic"
      },
      "mappings": [],
      "thresholds": {
        "mode": "absolute",
        "steps": [
          {
            "color": "green",
            "value": null
          },
          {
            "color": "red",
            "value": 80
          }
        ]
      },
      "max": 1,
      "min": 0,
      "unit": "percentunit"
    },
    "overrides": [
      {
        "matcher": {
          "id": "byName",
          "options": "G"
        },
        "properties": [
          {
            "id": "color",
            "value": {
              "fixedColor": "red",
              "mode": "fixed"
            }
          },
          {
            "id": "displayName",
            "value": ""
          }
        ]
      },
      {
        "matcher": {
          "id": "byName",
          "options": "E"
        },
        "properties": [
          {
            "id": "displayName",
            "value": ""
          }
        ]
      },
      {
        "matcher": {
          "id": "byName",
          "options": "F"
        },
        "properties": [
          {
            "id": "displayName",
            "value": "待改进"
          }
        ]
      }
    ]
  },
  "gridPos": {
    "h": 8,
    "w": 12,
    "x": 0,
    "y": 16
  },
  "id": 4,
  "options": {
    "tooltip": {
      "mode": "multi",
      "sort": "none"
    },
    "legend": {
      "showLegend": true,
      "displayMode": "table",
      "placement": "right",
      "calcs": [
        "min",
        "max",
        "mean"
      ]
    }
  },
  "targets": [
    {
      "alias": "good",
      "datasource": {
        "type": "testdata",
        "uid": "a95c7111-f01f-4e29-b3a6-9b0ac81d9094"
      },
      "hide": true,
      "refId": "A",
      "scenarioId": "csv_metric_values",
      "stringInput": "47,57,46,54,54,57,47,46,54,54,55,52,46,53,46"
    },
    {
      "alias": "needsImprove",
      "datasource": {
        "type": "testdata",
        "uid": "a95c7111-f01f-4e29-b3a6-9b0ac81d9094"
      },
      "hide": true,
      "refId": "B",
      "scenarioId": "csv_metric_values",
      "stringInput": "10,14,11,11,19,13,15,19,15,13,16,17,12,9,19"
    },
    {
      "alias": "poor",
      "datasource": {
        "type": "testdata",
        "uid": "a95c7111-f01f-4e29-b3a6-9b0ac81d9094"
      },
      "hide": true,
      "refId": "C",
      "scenarioId": "csv_metric_values",
      "stringInput": "15,18,6,12,9,17,10,16,5,8,15,12,11,12,16"
    },
    {
      "datasource": {
        "name": "Expression",
        "type": "__expr__",
        "uid": "__expr__"
      },
      "expression": "$A+$B+$C",
      "hide": true,
      "refId": "D",
      "type": "math"
    },
    {
      "datasource": {
        "name": "Expression",
        "type": "__expr__",
        "uid": "__expr__"
      },
      "expression": "$A/$D",
      "hide": false,
      "refId": "E",
      "type": "math"
    },
    {
      "datasource": {
        "name": "Expression",
        "type": "__expr__",
        "uid": "__expr__"
      },
      "expression": "$B/$D",
      "hide": false,
      "refId": "F",
      "type": "math"
    },
    {
      "datasource": {
        "name": "Expression",
        "type": "__expr__",
        "uid": "__expr__"
      },
      "expression": "$C/$D",
      "hide": false,
      "refId": "G",
      "type": "math"
    }
  ],
  "title": "堆叠百分比统计 WebVitals CLS 指标示例",
  "type": "timeseries"
}

粘贴图表方式:
创建一张空图表后,点击更多选项-Inspect-Panel JSON,粘贴上述配置源码,点击应用(Apply)后即可立即生效

通过创建多张针对不同指标的堆叠百分比图,我们就能对生产环境的web-vitals各项指标有稳定、客观、可量化的监控,从而:

  • 找到明确的优化目标:根据可视化图表快速定位用户体验相关的不足之处;
  • 建立量化指标:对比优化前后客观、可量化的指标,评估优化效果;
  • 判断是否有实质性的用户体验改善
  • 建立长效化的机制:通过长期观测、对比可视化图表,确保用户体验不出现衰退;

《前端工程体验优化》全书现已发布到掘金小册,欢迎各位朋友交流学习

  1. 你做的前端优化都错了-数据驱动、指标先行
  2. 前端优化数据量化必备神器-用户体验数据收集与可视化
  3. 光速入门Performance API
  4. 2行代码让JS加载耗时减少67%-资源优先级提示
  5. CDN最佳实践:让CDN流量节省10%
  6. CDN最佳实践:验证,量化与评估
  7. 超简单的代码模块懒加载:让JS加载体积减少13%
  8. 超简单的代码模块懒加载:懒加载常见问题解决方案
  9. 代码分割最佳实践:细粒度代码分割(Granular Code Split)
  10. 代码分割最佳实践:应用改造示例
  11. 前端渲染进化史:用SSR让首次内容绘制耗时(FCP)降低72%
  12. 前端渲染进化史:SSR进阶优化
  13. 图片加载体积减少20%-自适应选择最优图片格式
  14. GIF体积减少80%-GIF图片优化
  15. 万物皆可懒加载-3类通用资源懒加载实现方案
  16. CLS 和 LCP 专项优化
  17. 打包耗时减少43%-现代构建工具的魔力
  18. 重构CSS-用CSS In JS解决CSS的诸多痛点
  19. 打造技术改进文化-征求意见稿制度RFC
  20. 一错不再错-用自动化测试避免功能衰退

小册海报

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant