Grafana Labs

How to monitor Cypress tests with Grafana Cloud

8.5内容质量
How to monitor Cypress tests with Grafana Cloud

TL;DR · AI 摘要

通过Prometheus指标与Grafana Cloud集成,可实现Cypress测试的长期趋势监控,免费层级即可完成部署。

核心要点

  • 使用Cypress插件钩子生成Prometheus指标并推送到Pushgateway
  • Alloy工具负责从Pushgateway采集数据并写入Grafana Cloud Metrics
  • 无需付费即可利用Grafana Cloud免费层级实现完整监控流水线

结构提纲

按章节快速跳转。

  1. 说明Cypress测试监控的挑战及现有解决方案的局限性。

  2. 介绍通过Prometheus指标和Pushgateway实现数据持久化的机制。

  3. 分步讲解如何配置Cypress插件生成指标并推送到监控系统。

  4. 详细说明Alloy在数据采集和转发中的核心作用。

  5. 展示如何利用Grafana Cloud免费层级完成完整监控流水线。

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • Cypress测试监控方案
    • 数据采集
      • Cypress插件钩子生成指标
      • Prometheus Pushgateway存储
    • 数据传输
      • Alloy采集器转发
      • 远程写入Grafana Cloud
    • 可视化
      • Grafana Cloud Metrics仪表盘

金句 / Highlights

值得收藏与分享的关键句。

#Cypress#Grafana Cloud#Prometheus#测试监控
打开原文

如果您的 Cypress 测试套件中存在频繁失败或运行缓慢的测试,您会知道从单个作业中很难发现其中的规律。可能是某个测试用例变慢了,或者某个测试失败了,又或者整个套件整体变慢了。根本原因可能是应用程序中的缺陷、测试不稳定,或者其他因素。终端输出和 CI 日志可以告诉您单次运行发生了什么,但这无法帮助您发现更宏观的趋势——尤其是作业完成后这些数据就会丢失。

幸运的是,Cypress 是一个专为 Web 应用程序构建的前端自动化测试框架,它已经通过插件钩子暴露了您需要的所有信息。每个测试用例执行完成后,Cypress 会提供一个包含通过/失败数量、单个测试耗时和状态的结果对象。您只需将这些数据转换为指标并发送到持久化存储即可。

在本文中,您将学习如何通过在 Cypress 钩子中将这些结果转换为 Prometheus 指标,推送到 Prometheus Pushgateway,然后让 Alloy 从网关抓取数据并转发到 Grafana Cloud Metrics ——全程仅使用免费层级即可实现。

最终您将拥有一个按照以下架构运行的监控流水线:

Image 1: 一张流程图,展示从测试环境到 Grafana Cloud 的远程写入过程%3Aquality(90)%2F&w=3840&q=75)

所需准备

本教程将与您现有的 Cypress 项目一起运行。开始之前,请确保您具备以下条件:

  • 一个 Cypress 项目(本示例使用 Cypress 14.x)以及一个可编辑的 cypress.config.js 文件
  • 一个 Prometheus Pushgateway。由于 Cypress 运行是短暂的批处理作业,无法直接被抓取——Pushgateway 会在运行之间保存指标,以便抓取器获取。在您的基础设施中设置 Prometheus Pushgateway
  • Alloy,我们的开源采集器,用于抓取 Pushgateway 并远程写入到 Grafana Cloud
  • 一个 Grafana Cloud 账户。免费层级包含 Grafana Cloud Metrics 和 Prometheus 远程写入端点。如果没有账户,可以在此处 here 注册
  • 您的 Grafana Cloud 远程写入 URL、数字用户 ID 以及具有 metrics:write 权限范围的访问策略令牌

从 Cypress 钩子中发出指标

1a. 实现功能的关键钩子

Cypress 插件在 Node.js 中运行,可以在 setupNodeEvents 中订阅生命周期事件。两个钩子为我们提供了所有必要的信息:

  • before:run 在任何测试用例运行前触发一次。我们使用它为整个测试套件生成一个唯一的 run_id,并清除上一次运行留下的状态。在这个示例中,run_id 是测试套件启动时间的 Unix 时间戳。
  • after:spec 在每个测试文件运行结束后触发,并接收该测试文件的结果对象。测试的数量、状态和持续时间都存储在这里,因此我们在此处构建并推送指标。
code
// cypress.config.js
module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on) {
      on("before:run", () => {
        runEpoch = String(Date.now());
        specMetrics.clear();
      });

      on("after:spec", async (spec, results) => {
        try {
          await pushSpecToPrometheus(spec, results);
        } catch (err) {
          console.error(
            `Prometheus push failed for ${spec.relative || spec.name}:`,
            err.message
          );
        }
      });
    },
  },
});

推送操作被包裹在 try/catch 中,这样监控服务的中断不会导致原本通过的测试变成失败——遥测数据是副作用,不应成为测试通过的条件。其次,run_idbefore:run 中仅设置一次,所有测试用例都会复用这个 ID,因此同一套件执行的所有测试用例共享一个标识符,后续可以按运行批次进行分组。

1b. 将 Cypress 结果对象转换为 Prometheus 指标

Cypress 传递给 after:spec 的结果对象包含一个统计块(通过数、失败数、待定数、跳过数、总测试数和持续时间毫秒数),以及一个测试数组,每个测试包含标题、状态和持续时间。我们将这些信息映射到一组指标中,使用标签按测试用例、运行批次和单个测试进行分类:

code
function metricLinesForSpec(specName, runId, githubRunId, results) {
  const stats = results.stats || {};
  const passed = stats.passes ?? 0;
  const failed = stats.failures ?? 0;
  const pending = stats.pending ?? 0;
  const skipped = stats.skipped ?? 0;
  const total = stats.tests ?? passed + failed + pending + skipped;
  const durationSec = (stats.duration ?? 0) / 1000;
  const labels = [
    `spec="${escapeLabelValue(specName)}"`,
    `run_id="${escapeLabelValue(runId)}"`,
    `github_run_id="${escapeLabelValue(githubRunId)}"`,
  ].join(",");

  const lines = [
    `cypress_tests_total{${labels},result="passed"} ${passed}`,
    `cypress_tests_total{${labels},result="failed"} ${failed}`,
    `cypress_tests_total{${labels},result="pending"} ${pending}`,
    `cypress_tests_total{${labels},result="skipped"} ${skipped}`,
    `cypress_tests_run_total{${labels}} ${total}`,
    `cypress_spec_duration_seconds{${labels}} ${durationSec}`,
    `cypress_spec_success{${labels}} ${failed === 0 ? 1 : 0}`,
  ];

  for (const test of results.tests || []) {
    const testName = (test.title || []).join(" -- ") || "unknown";
    const testLabels = `${labels},test="${escapeLabelValue(testName)}"`;
    const state = test.state || "unknown";
    const testDurationSec = (test.duration ?? 0) / 1000;

    lines.push(
      `cypress_test_success{${testLabels}} ${state === "passed" ? 1 : 0}`,
      `cypress_test_duration_seconds{${testLabels},result="${escapeLabelValue(state)}"} ${testDurationSec}`
    );
  }

  return lines.join("\n");
}

每个系列都携带一组通用的标签 spec(哪个文件)、run_id(在 before:run 中生成的纪元时间戳)和 ci_run_id(当存在时的 CI 运行标识)——这样你可以将仪表板筛选到某个特定的 spec 或某个 CI 运行。最终会得到一组紧凑的指标:

  • cypress_tests_total:按 spec 统计测试结果(通过、失败、待定、跳过)的数量
  • cypress_tests_run_total:每个 spec 执行的总测试数
  • cypress_spec_duration_seconds:每个 spec 执行耗时
  • cypress_spec_success:如果 spec 没有失败则为 1,否则为 0
  • cypress_test_successcypress_test_duration_seconds:在单个测试级别实现相同功能,使你能够跟踪某个易变测试随时间的变化趋势

1c. 推送到 Pushgateway

标准的 Prometheus 设置会定期抓取长期运行的服务。而 Cypress 运行正好相反。它是一个短暂运行的批处理任务,在任何抓取器到达之前就会终止。Prometheus Pushgateway 正是为此场景而设计:你的任务将指标推送到网关,网关保存这些指标,Prometheus(或 Alloy)按照自己的计划从网关抓取数据。

推送本身只是将 Prometheus 文本格式的指标以普通 HTTP POST 方式发送:

code
// cypress.config.js — 向 Pushgateway 推送一个累积的指标体async function pushSpecToPrometheus(spec, results) {
  const baseUrl = process.env.PROMETHEUS_PUSHGATEWAY_URL;
  if (!baseUrl) {
    console.log(
      "跳过 Prometheus 推送(设置 PROMETHEUS_PUSHGATEWAY_URL 以启用)。"
    );
    return;
  }

  const job = process.env.PROMETHEUS_JOB || "cypress";
  const instance = process.env.PROMETHEUS_INSTANCE || "local";
  const runId = runEpoch || (runEpoch = String(Date.now()));
  const githubRunId = process.env.GITHUB_RUN_ID || "localrun";
  const specName = spec.relative || spec.name || "unknown";

  specMetrics.set(
    specName,
    metricLinesForSpec(specName, runId, githubRunId, results)
  );

  const url = new URL(
    `/metrics/job/${encodeURIComponent(job)}/instance/${encodeURIComponent(instance)}`,
    baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`
  );

  const res = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "ngrok-skip-browser-warning": "true",
    },
    body: buildBody(),
  });

  if (!res.ok) {
    const detail = await res.text().catch(() => "");
    throw new Error(
      `Pushgateway 返回 ${res.status} ${res.statusText} for ${url.href}${detail ? `: ${detail}` : ""}`
    );
  }

  console.log(
    `已将 spec "${specName}" 的指标(run_id="${runId}", github_run_id="${githubRunId}")推送到 Pushgateway`
  );
}

路径中的 job 和 instance 构成了 Pushgateway 的分组键(此处为 job="cypress" 和 instance="local";可以根据运行套件的项目或模块分配任意值)。

由于我们把每个 spec 的指标累积到 specMetrics 映射中,并在每个 spec 之后重新 POST 完整的指标体,网关始终保存着套件的完整最新快照。而通过在 before:run 中清空该映射,可以确保每次新运行都从干净状态开始,而不是携带旧的 spec 数据。

整个集成都在 cypress.config.js 中实现,仅当设置了 PROMETHEUS_PUSHGATEWAY_URL 时才会激活,因此本地 Cypress 运行不会受到影响,除非你主动启用该功能:

# package.json script PROMETHEUS_PUSHGATEWAY_URL=http://localhost:9091 cypress run

Ship to Grafana Cloud with Alloy

现在指标已存储到 Pushgateway,但它是本地缓冲区,并非长期存储。Alloy 桥接了这两者:它从网关抓取数据,并远程写入到 Grafana Cloud Metrics。

// config.alloy — 抓取 Pushgateway 并发送到 Grafana Cloud Metrics prometheus.scrape "pushgateway" { targets = [ { "__address__" = coalesce(sys.env("PUSHGATEWAY_ADDRESS"), "localhost:9091") }, ] honor_labels = true metrics_path = "/metrics" scrape_interval = "60s" forward_to = [prometheus.remote_write.grafana_cloud.receiver] } prometheus.remote_write "grafana_cloud" { endpoint { url = sys.env("GRAFANA_CLOUD_RW_URL") basic_auth { username = sys.env("GRAFANA_CLOUD_USER") password = sys.env("GRAFANA_CLOUD_TOKEN") } } external_labels = { source = "cypress-pushgateway", } }

将 Grafana Cloud 凭据作为环境变量提供并启动 Alloy:

export GRAFANA_CLOUD_RW_URL=https://prometheus-prod-XX-XXX.grafana.net/api/prom/push export GRAFANA_CLOUD_USER=<your-user-id> export GRAFANA_CLOUD_TOKEN=<your-access-policy-token> alloy run config.alloy

Run tests and verify

启动 Pushgateway 和 Alloy,然后运行指向网关的测试套件:

# 1. Pushgateway(在运行之间保存指标) docker run -d -p 9091:9091 prom/pushgateway # 2. Alloy(抓取网关,发送到 Grafana Cloud)——参见第 2 部分 alloy run config.alloy # 3. 启用推送的 Cypress npm run test:prometheus

每个测试规范完成后,Cypress 会向 Pushgateway 记录类似 Pushed metrics for spec "login.cy.js" (run_id="1753100000000") 的日志。在抓取间隔内,这些时间序列会出现在 Grafana Cloud 中。

Image 2: 时间序列出现在 Grafana Cloud%3Aquality(90)%2F&w=3840&q=75)

从这里你可以构建一个仪表板,用于识别通过和失败次数的趋势、规范持续时间以及每个测试的成功率。你还可以设置 Grafana Alerting,当套件成功率下降或关键规范开始失败时通知你。

Image 3: Grafana 仪表板面板显示按运行划分的测试结果趋势,11:30 出现峰值%3Aquality(90)%2F&w=3840&q=75)

基于收集的指标,我们可以可视化每个规范的执行时间以及单个测试的执行时间。

Image 4: Grafana 仪表板面板显示规范执行时间和最慢的 10 个测试%3Aquality(90)%2F&w=3840&q=75)

在 CI 中运行

在 CI 环境中运行时,确保将环境变量注入到构建流程中,并验证 Docker 容器和 Alloy 的启动是否符合预期。对于持续集成系统(如 GitHub Actions、GitLab CI 或 Jenkins),可以将环境变量定义在配置文件中,并确保 Alloy 配置文件路径正确无误。

该机制在 CI 环境中同样适用,唯一不同的是指标的来源。

例如,在 GitHub Actions 工作流中,将 PROMETHEUS_PUSHGATEWAY_URL 设置为 runner 可访问的 Pushgateway,其余工作由 Cypress 完成:

yml
# .github/workflows/main.yml (节选)
env:
  PROMETHEUS_PUSHGATEWAY_URL: ${{ secrets.PROMETHEUS_PUSHGATEWAY_URL }}
  # GITHUB_RUN_ID 由系统自动提供,并用作 ci_run_id 标签

由于代码在检测到 GITHUB_RUN_ID 时会自动读取,每个 CI 运行都会被标记上对应的 ci_run_id,因此你可以直接从指标突变跳转到产生该指标的工作流运行记录。

图 5:Grafana 仪表板面板显示每次运行的通过和失败情况%3Aquality(90)%2F&w=3840&q=75)

以下建议可帮助你节省时间:

  • 使用 Pushgateway 而非抓取目标。Cypress 运行在几秒内就会完成;抓取工具永远无法捕获到它们。Pushgateway 是使短期任务可观测的缓冲区。
  • 永远不要让遥测数据导致运行失败。用 try/catch 包裹推送操作。监控系统故障不应让原本通过的测试套件变成红色。
  • 转义标签值。测试标题是自由文本,可能包含引号和换行符,会破坏 Prometheus 的文本格式。在写入指标前请先进行转义。
  • 为每个测试套件设置唯一的run_id 在 before:run 阶段设置该值并在所有测试用例中复用,这是后续按运行批次进行分组和对比的基础。

Grafana Assistant 是在 Grafana Cloud 上开始使用指标、日志、追踪、仪表板等功能最简单的方式。我们提供慷慨的永久免费层级,并为所有使用场景准备了相应的方案。立即免费注册!

标签