JuniorTour / blog

not only front-end
8 stars 5 forks source link

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

Open JuniorTour opened 1 year ago

JuniorTour commented 1 year ago

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

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

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

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

1. Prometheus 及 Grafana 简介

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

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

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

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

2. 接入演示

我们将基于:

演示如何从本地环境收集 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 项目:

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

《feat: 引入express && prom-client;初始化服务》commit:https://github.com/JuniorTour/node-prometheus-grafana-demo/commit/7ba9148a70164c944cd6d474251c5bef2507273c

运行 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:https://github.com/JuniorTour/node-prometheus-grafana-demo/commit/6c7c57abcf4c94685c45cbef3a0715f8a41e4fab


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

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

接下来我们基于之前的《获取web-vitals数据 DEMO》: https://output.jsbin.com/bizanep 增加上报数据到 Node.js 后端的逻辑:
``` js
// 《发送 web-vitals 数据 DEMO》:https://output.jsbin.com/xifudez
async function sendDataToBackend(data) {
  if (!data) {
    return
  }
  await fetch('http://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);

我们的改动主要是:

完整示例请参考 《发送 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模式,可以看到包含以下各选项:

分别对应我们调用 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. 分别保存图表和仪表盘

先后点击:

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

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

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

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

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

例如这条从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. 一错不再错-用自动化测试避免功能衰退

小册海报