synthetic-monitoring-checks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Synthetic Monitoring Check Authoring

Synthetic Monitoring检查编写指南

Docs: https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/ Broad Grafana Cloud Testing entry point (SM + k6 Cloud + Faro):
testing
skill.
文档https://grafana.com/docs/grafana-cloud/testing/synthetic-monitoring/ Grafana Cloud Testing总入口(SM + k6 Cloud + Faro):
testing
技能。

Reliability monitoring, not load testing

可靠性监控,而非负载测试

Synthetic Monitoring (SM) runs k6 as a reliability/availability engine: every check execution runs one iteration with one VU from each selected probe location on a fixed schedule. Success means "the user journey works right now, from this region" — detect outages before your customers do.
Do not apply load-testing idioms. There are no VUs to ramp, no
stages
, no load profiles, no soak/stress/spike phases, and no
thresholds
over aggregated traffic. Vocabulary: check, probe, execution, uptime, reachability, user journey validation — never "load test", "ramping", or "VUs".
If the user actually wants load or performance testing (throughput, latency under load, breakpoints), stop: that is Grafana Cloud k6 / the
grafana-k6
plugin's
k6
skill, not Synthetic Monitoring. A script can be shared between both products, but the goals, options, and pricing are different.
Synthetic Monitoring (SM) 将k6用作可靠性/可用性引擎:每次检查执行都会从每个选定的探测位置按固定调度运行一次迭代、一个VU。成功意味着“当前从该区域来看,用户旅程可正常运行”——在客户发现之前检测到故障。
请勿套用负载测试的惯用方法。这里无需设置VU递增、
stages
阶段、负载模型、浸泡/压力/尖峰测试阶段,也没有针对聚合流量的
thresholds
阈值。相关术语:check(检查)probe(探测)execution(执行)uptime(可用性)reachability(可达性)user journey validation(用户旅程验证)——绝对不要使用“负载测试”“递增”“VU”这类词汇。
如果用户实际需要负载或性能测试(吞吐量、负载下的延迟、断点),请停止:这属于Grafana Cloud k6 /
grafana-k6
插件的
k6
技能范畴,而非Synthetic Monitoring。脚本可在两款产品间共享,但目标、配置选项和定价模式均不同。

Execution model and constraints (verify against these before writing)

执行模型与约束条件(编写前请确认)

ConstraintValue
WorkloadOne iteration per probe execution. Scripted and MultiHTTP run with forced
--vus 1 --iterations 1
; browser checks rely on the script's required single scenario. Either way
vus
,
duration
,
stages
,
iterations
are ignored — never write a load shape
thresholds
Not supported
Frequencyk6-class checks (scripted, MultiHTTP, browser): 60–3600s. Protocol checks (HTTP/ping/DNS/TCP/gRPC): 1–3600s. Traceroute: 120–3600s
TimeoutMust be ≤ frequency. k6-class checks: 1–180s. Protocol checks: 1–60s. Traceroute: fixed 30s
k6 versionChecks run on a k6 version channel (new checks default to the latest stable channel;
v1.x
is deprecated as of July 2026). Pin per check via the UI dropdown or
channels
in API/Terraform
Local files
open()
,
fs
,
grpc.load()
unsupported. Bundle local modules into the script; remote
https://jslib.k6.io/...
imports work
HTTP request errorsSM runs k6 with
--throw
: network-level request failures throw an exception and fail the execution
Script options SM honorsSM sets its own CLI flags, which take precedence over the script's
options
object; the options that still take effect include
batch
,
batch-per-host
,
discardResponseBodies
,
httpDebug
,
insecureSkipTLSVerify
,
maxRedirects
,
noConnectionReuse
,
setupTimeout
,
systemTags
,
tags
,
teardownTimeout
,
throw
,
tlsAuth
,
tlsCipherSuites
,
tlsVersion
,
userAgent
Browser memory1GB RAM per browser on public probes — huge pages fail with
Target has crashed
Browser script formatThe UI rejects bundled/minified browser scripts (import validation) — deploy those via API or Terraform
约束条件取值
工作负载每次探测执行运行一次迭代。脚本化检查和MultiHTTP检查会强制使用
--vus 1 --iterations 1
运行;浏览器检查依赖脚本中要求的单一场景。无论哪种情况,
vus
duration
stages
iterations
都会被忽略——切勿编写负载模型
thresholds
不支持
执行频率k6类检查(脚本化、MultiHTTP、浏览器):60–3600秒。协议类检查(HTTP/ping/DNS/TCP/gRPC):1–3600秒。 traceroute:120–3600秒
超时时间必须≤执行频率。k6类检查:1–180秒。协议类检查:1–60秒。traceroute:固定30秒
k6版本检查运行在k6版本通道上(新检查默认使用最新稳定通道;截至2026年7月,
v1.x
已被弃用)。可通过UI下拉菜单或API/Terraform中的
channels
参数为每个检查固定版本
本地文件不支持
open()
fs
grpc.load()
。需将本地模块打包到脚本中;支持远程导入
https://jslib.k6.io/...
HTTP请求错误SM运行k6时启用
--throw
:网络级请求失败会抛出异常并导致执行失败
SM认可的脚本选项SM会设置自己的CLI标志,优先级高于脚本的
options
对象;仍生效的选项包括
batch
batch-per-host
discardResponseBodies
httpDebug
insecureSkipTLSVerify
maxRedirects
noConnectionReuse
setupTimeout
systemTags
tags
teardownTimeout
throw
tlsAuth
tlsCipherSuites
tlsVersion
userAgent
浏览器内存公共探测上每个浏览器分配1GB内存——页面过大时会因
Target has crashed
报错
浏览器脚本格式UI会拒绝打包/压缩后的浏览器脚本(导入验证)——需通过API或Terraform部署此类脚本

How an execution fails (this is what agents get wrong)

执行失败的判定逻辑(这是常见误区)

probe_success
(1/0) is the uptime signal. An execution is marked failed when the script throws an uncaught exception, calls
fail()
, a k6-testing
expect()
assertion fails (it calls k6's
test.abort()
under the hood), an HTTP request errors at the network level (SM's
--throw
), or the timeout is hit.
A bare failed
check()
does NOT fail the execution
— it only records the
probe_checks_total
/
probe_check_success_rate
metrics. Checks don't affect k6's exit status without thresholds, and thresholds are disabled in SM.
Assertion patterns, in order of preference:
javascript
import { expect } from 'https://jslib.k6.io/k6-testing/0.6.1/index.js';
import { check, fail } from 'k6';

// 1. PREFERRED — assertions module. Throws on failure => execution fails,
//    with a descriptive error in the check logs.
expect(res.status, 'login should succeed').toEqual(200);
expect(res.json('token')).toBeDefined();

// 2. Soft assertions — run all of them, still fail the execution at the end.
expect.soft(res.headers['Content-Type']).toContain('application/json');

// 3. check() when you also want per-assertion metrics — but pair it with
//    fail() or the failure won't affect probe_success/uptime:
check(res, { 'status 200': (r) => r.status === 200 }) ||
  fail(`login failed with status ${res.status}`);
Name every assertion (the message argument / check name): the name is what you see in check logs and in the
check
label of
probe_checks_total
when diagnosing a failure at 3am.
probe_success
(取值1/0)是可用性信号。当脚本抛出未捕获的异常、调用
fail()
、k6-testing的
expect()
断言失败(底层调用k6的
test.abort()
)、HTTP请求出现网络级错误(SM的
--throw
机制)或触发超时时间时,执行会被标记为失败
原生
check()
失败不会导致执行失败
——仅会记录
probe_checks_total
/
probe_check_success_rate
指标。没有阈值的情况下,check不会影响k6的退出状态,而SM中阈值是禁用的。
断言模式优先级如下:
javascript
import { expect } from 'https://jslib.k6.io/k6-testing/0.6.1/index.js';
import { check, fail } from 'k6';

// 1. 首选——断言模块。失败时抛出异常 => 执行失败,
//    并在检查日志中显示描述性错误信息。
expect(res.status, 'login should succeed').toEqual(200);
expect(res.json('token')).toBeDefined();

// 2. 软断言——执行所有断言,最终仍会标记执行失败。
expect.soft(res.headers['Content-Type']).toContain('application/json');

// 3. 需要按断言统计指标时使用check()——但需搭配fail(),否则失败不会影响probe_success/可用性:
check(res, { 'status 200': (r) => r.status === 200 }) ||
  fail(`login failed with status ${res.status}`);
为每个断言命名(消息参数/check名称):凌晨3点排查故障时,你在检查日志和
probe_checks_total
check
标签中看到的就是这个名称。

Choose the simplest sufficient check type first

优先选择最简单且足够的检查类型

Cheaper for the customer, easier to maintain. Work down this list and stop at the first match:
  1. HTTP / ping / DNS / TCP / traceroute / gRPC — a single static endpoint (uptime, status code, body regex, TLS cert expiry, record resolution, port reachability). No script to maintain — these run on the blackbox-exporter probe engine, and Terraform examples with per-type
    target
    formats are in
    references/api-and-terraform.md
    .
  2. MultiHTTP — a sequence of HTTP requests with value-passing between them (
    ${variable}
    capture), but no custom logic. Caution: MultiHTTP does not auto-validate status codes — define assertions per request or failures won't affect uptime.
  3. k6 scripted — an API flow needing real logic: crypto/signing, conditional branching, generated test data, WebSockets, response-driven chaining.
  4. k6 browser — only when you need a real browser: JS-rendered user journeys, forms/clicks, Core Web Vitals.
Cost model (execution-based billing): an execution is one check run on one probe, metered per minute of runtime rounded up. Per month:
probes × duration_minutes × (43200 / frequency_minutes)
. API test executions (HTTP, ping, DNS, TCP, traceroute, MultiHTTP, scripted) and browser test executions are billed separately — browser checks are the expensive tier. A browser check on 3 probes every minute is ~129,600 browser executions/month; the same check every 5 minutes is ~25,920. Pick the longest frequency that still meets your detection-time goal, and 2–3 probes near your users (multiple probes reduce alert flapping; more isn't better).
对客户来说成本更低,也更易维护。按以下顺序选择,找到第一个匹配项即可停止:
  1. HTTP / ping / DNS / TCP / traceroute / gRPC——针对单个静态端点(可用性、状态码、Body正则匹配、TLS证书过期、记录解析、端口可达性)。无需维护脚本——这些检查运行在blackbox-exporter探测引擎上,各类型
    target
    格式的Terraform示例可参考
    references/api-and-terraform.md
  2. MultiHTTP——一系列可传递值的HTTP请求(使用
    ${variable}
    捕获),但无自定义逻辑。注意:MultiHTTP不会自动验证状态码——需为每个请求定义断言,否则失败不会影响可用性。
  3. k6脚本化——需要实际逻辑的API流程:加密/签名、条件分支、生成测试数据、WebSocket、响应驱动的链式调用。
  4. k6浏览器——仅在需要真实浏览器时使用:JS渲染的用户旅程、表单/点击操作、Core Web Vitals指标。
成本模型(按执行次数计费):一次执行指一个检查在一个探测节点上运行一次,按运行时长向上取整到分钟计费。每月费用计算公式:
探测节点数 × 单次执行时长(分钟) × (43200 / 执行频率(分钟))
。API测试执行(HTTP、ping、DNS、TCP、traceroute、MultiHTTP、脚本化)和浏览器测试执行分开计费——浏览器检查属于高成本层级。一个浏览器检查在3个探测节点上每分钟运行一次,每月约129600次浏览器执行;每5分钟运行一次则约25920次。选择能满足故障检测时间目标的最长执行频率,且仅选择2–3个靠近用户的探测节点(多个节点可减少告警抖动,但并非越多越好)。

Scripted check authoring

脚本化检查编写

Start every script you generate (scripted and browser alike) with a line-1 attribution comment, as shown in the skeletons below. It tells whoever reads the check later how it was authored (and where to find the skill), and the fixed prefix makes skill-authored checks queryable. Keep
Generated by synthetic-monitoring-checks
verbatim — vary only the timestamp (
date -u +%Y-%m-%dT%H:%M:%SZ
).
Skeleton — a login + API action journey with secrets and hard-failing assertions:
javascript
// Generated by synthetic-monitoring-checks (https://github.com/grafana/skills) on 2026-07-31T12:00:00Z
import http from 'k6/http';
import { expect } from 'https://jslib.k6.io/k6-testing/0.6.1/index.js';
import secrets from 'k6/secrets';

const BASE = 'https://api.example.com';

export default async function () {
  // Secrets are managed in Synthetics > Config > Secrets — never hardcode credentials.
  const password = await secrets.get('checkout-monitor-password');

  // Step 1: authenticate with a dedicated monitoring account
  const login = http.post(
    `${BASE}/auth/login`,
    JSON.stringify({ user: 'sm-checkout-monitor', password }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  expect(login.status, 'login should return 200').toEqual(200);
  const token = login.json('token');
  expect(token, 'auth token should be present').toBeDefined();

  // Step 2: exercise the journey and assert the OUTCOME, not just the status
  const order = http.post(`${BASE}/orders`, JSON.stringify({ sku: 'TEST-SKU-1', qty: 1 }), {
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
  });
  expect(order.status, 'order should be created').toEqual(201);
  const orderId = order.json('id');
  expect(orderId, 'order id should be returned').toBeDefined();

  // Step 3: clean up so the check is idempotent against production
  // http.url groups metrics for URLs containing unique IDs — without it, every
  // execution creates new time series (cardinality + active-series cost).
  const del = http.del(http.url`${BASE}/orders/${orderId}`, null, {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(del.status, 'test order should be cleaned up').toEqual(204);
}
Rules that make a scripted check a good monitor (vs a good test):
  • Deterministic: fixed test data (or generated-then-deleted, as above), no time-of-day or ordering dependence. Every execution must be able to pass at any hour from any probe.
  • Idempotent against production: create-then-delete, or use read-only endpoints. The check runs forever — leaked state accumulates forever.
  • Dedicated test account: never a real user's credentials; scope it minimally, store the password as an SM secret, and exclude the account from analytics/billing.
  • Assert every step — an unasserted step that breaks shows up as a later step's confusing failure.
  • Stable URL cardinality:
    http.url
    template literal for any URL containing an ID.
  • Keep runtime well under the timeout, and the timeout under the frequency.
生成的所有脚本(脚本化和浏览器检查)都要在第一行添加归属注释,如下列模板所示。这能让后续查看检查的人知道脚本的生成方式(以及技能的位置),固定前缀也便于查询技能生成的检查。请保留
Generated by synthetic-monitoring-checks
原文——仅修改时间戳(格式为
date -u +%Y-%m-%dT%H:%M:%SZ
)。
模板——包含密钥管理和强失败断言的登录+API操作旅程:
javascript
// Generated by synthetic-monitoring-checks (https://github.com/grafana/skills) on 2026-07-31T12:00:00Z
import http from 'k6/http';
import { expect } from 'https://jslib.k6.io/k6-testing/0.6.1/index.js';
import secrets from 'k6/secrets';

const BASE = 'https://api.example.com';

export default async function () {
  // 密钥在Synthetics > Config > Secrets中管理——切勿硬编码凭证。
  const password = await secrets.get('checkout-monitor-password');

  // 步骤1:使用专用监控账号认证
  const login = http.post(
    `${BASE}/auth/login`,
    JSON.stringify({ user: 'sm-checkout-monitor', password }),
    { headers: { 'Content-Type': 'application/json' } }
  );
  expect(login.status, 'login should return 200').toEqual(200);
  const token = login.json('token');
  expect(token, 'auth token should be present').toBeDefined();

  // 步骤2:执行旅程并断言结果,而非仅断言状态码
  const order = http.post(`${BASE}/orders`, JSON.stringify({ sku: 'TEST-SKU-1', qty: 1 }), {
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
  });
  expect(order.status, 'order should be created').toEqual(201);
  const orderId = order.json('id');
  expect(orderId, 'order id should be returned').toBeDefined();

  // 步骤3:清理资源,确保检查对生产环境是幂等的
  // http.url会对包含唯一ID的URL分组指标——如果不使用,每次执行都会创建新的时间序列(基数+活跃序列成本)。
  const del = http.del(http.url`${BASE}/orders/${orderId}`, null, {
    headers: { Authorization: `Bearer ${token}` },
  });
  expect(del.status, 'test order should be cleaned up').toEqual(204);
}
让脚本化检查成为优秀监控工具(而非测试工具)的规则:
  • 确定性:使用固定测试数据(或生成后立即删除,如上例),不依赖时间或执行顺序。每次执行必须能在任意时间、任意探测节点通过。
  • 对生产环境幂等:创建后删除,或使用只读端点。检查会持续运行——泄露的状态会不断累积。
  • 专用测试账号:切勿使用真实用户的凭证;最小化权限范围,将密码存储为SM密钥,并将该账号排除在分析/计费统计之外。
  • 每个步骤都要断言——未断言的步骤出现故障时,会表现为后续步骤的异常失败,难以排查。
  • 稳定的URL基数:对包含ID的URL使用
    http.url
    模板字符串。
  • 确保运行时长远低于超时时间,且超时时间小于执行频率。

Generating a check from an OpenAPI spec (or similar)

从OpenAPI规范(或类似文档)生成检查

Given an API description — an OpenAPI/Swagger spec, GraphQL schema, or Postman collection — the mechanical conversion to k6 calls is easy. What matters is what you choose to convert:
  1. Journeys, not endpoints. Do NOT generate one check per path, or one check that sweeps every path — that monitors the spec, not the service, and every extra check multiplies execution cost. Identify the 1–3 flows whose failure means "customers are impacted" (auth → core action → result) and write one scripted check per flow.
  2. Filter for safety. Only include mutating operations (
    POST
    /
    PUT
    /
    DELETE
    ) when the flow cleans up after itself (create-then-delete, as above) or targets dedicated test resources. A spec lists destructive operations right next to health endpoints — never exercise them against production just because they're documented.
  3. Assert from the response schema. The spec tells you exactly what a healthy response contains — assert required fields, not just the status code:
    expect(order.json('id'), 'id required by OrdersResponse schema').toBeDefined()
    .
  4. Verify the target URL.
    servers:
    blocks (and Postman environments) often list localhost or staging first — confirm the production base URL with the user, and map
    securitySchemes
    credentials to SM secrets, never to values inlined from the spec. (Secrets in plain HTTP/protocol checks are a recent, feature-flagged rollout — check current docs; the scripted
    secrets.get()
    path always works.)
  5. Treat
    format: int64
    ids as strings.
    res.json('id')
    parses into a JS number and silently corrupts values past 2^53 (snowflake-style ids), so the readback URL 404s on every execution while the create looks fine. Extract from the raw body instead:
    const id = (/"id":\s*(\d+)/.exec(res.body) || [])[1];
    — and never do arithmetic on it.
No API spec at all? Probe the frontend: open the web app with browser devtools (or
curl
likely routes) and capture the
/api/*
XHR calls it makes — that's a monitorable HTTP surface even when the documented backend services are gRPC-only or internal.
给定API描述——OpenAPI/Swagger规范、GraphQL schema或Postman集合——转换为k6调用的机械操作很简单。关键在于选择转换哪些内容:
  1. 旅程而非端点。不要为每个路径生成一个检查,也不要生成遍历所有路径的检查——这是监控规范而非服务,额外的检查会成倍增加执行成本。找出1–3个故障会直接影响客户的流程(认证→核心操作→结果),为每个流程编写一个脚本化检查。
  2. 筛选安全操作。仅当流程会自行清理(如上例的创建后删除)或针对专用测试资源时,才包含修改类操作(
    POST
    /
    PUT
    /
    DELETE
    )。规范中会将破坏性操作与健康检查端点并列——切勿仅因为文档中有就直接在生产环境执行。
  3. 根据响应Schema断言。规范明确说明了健康响应应包含的内容——断言必填字段,而非仅断言状态码:
    expect(order.json('id'), 'id required by OrdersResponse schema').toBeDefined()
  4. 验证目标URL
    servers:
    块(以及Postman环境)通常会先列出localhost或预发布环境——请与用户确认生产环境的基础URL,并将
    securitySchemes
    凭证映射到SM密钥,切勿直接使用规范中的内嵌值。(普通HTTP/协议检查的密钥功能是近期推出的,处于功能标志阶段——请查阅最新文档;脚本化检查的
    secrets.get()
    方式始终有效。)
  5. format: int64
    类型的ID视为字符串
    res.json('id')
    会解析为JS数字,当数值超过2^53(雪花ID等)时会被静默损坏,导致读取URL每次执行都返回404,但创建操作看起来正常。应从原始Body中提取:
    const id = (/"id":\s*(\d+)/.exec(res.body) || [])[1];
    ——切勿对其进行算术操作。
没有API规范?探测前端:使用浏览器开发者工具(或
curl
模拟路由)打开Web应用,捕获其发起的
/api/*
XHR请求——即使文档化的后端服务是gRPC-only或内部服务,这也是可监控的HTTP接口。

Browser check authoring

浏览器检查编写

Required scaffold: import
k6/browser
and declare the
chromium
browser type. The UI validates both.
javascript
// Generated by synthetic-monitoring-checks (https://github.com/grafana/skills) on 2026-07-31T12:00:00Z
import { browser } from 'k6/browser';
import { expect } from 'https://jslib.k6.io/k6-testing/0.6.1/index.js';
import secrets from 'k6/secrets';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: { browser: { type: 'chromium' } },
    },
  },
};

export default async function () {
  const page = await browser.newPage();
  try {
    await page.goto('https://shop.example.com/login');

    // Prefer role/label/test-id locators over CSS chains — they survive redesigns.
    const password = await secrets.get('shop-monitor-password');
    await page.getByLabel('Email').fill('sm-monitor@example.com');
    await page.getByLabel('Password').fill(password);
    await page.getByRole('button', { name: 'Sign in' }).click();

    // Assert the JOURNEY OUTCOME with auto-retrying assertions — never sleep().
    await expect(page.getByRole('heading', { name: 'Your account' })).toBeVisible();

    await page.getByRole('link', { name: 'Orders' }).click();
    await expect(page.getByTestId('order-list')).toBeVisible();
  } finally {
    await page.close();
  }
}
Browser-specific rules:
  • Locators:
    getByRole
    /
    getByLabel
    /
    getByTestId
    (ask the app team to add
    data-testid
    where needed) > text > CSS. Never XPath or generated class names.
    getByTestId
    assumes
    data-testid
    — apps instrumented for Cypress often use
    data-cy
    instead; fall back to
    page.locator('[data-cy="..."]')
    .
  • No manual waits before interactions: locator actions auto-wait for visibility and enabled state. Don't call
    waitFor()
    before
    click()
    /
    fill()
    , don't use
    waitForLoadState()
    , never
    sleep()
    .
  • Auto-retrying
    expect()
    (
    toBeVisible
    ,
    toBeEnabled
    , ...) is the wait mechanism for asserting state you don't interact with. Caveat: despite being listed as retrying, the text matchers (
    toHaveText
    /
    toContainText
    ) hard-fail on the first mismatched read — e.g. an empty string mid-hydration on a client-rendered app. Assert dynamic text by locating it and asserting visibility instead:
    await expect(page.getByText('Order confirmed')).toBeVisible()
    .
  • Assertion timeout defaults to 5s — client-side-rendered apps routinely take longer to first meaningful render. Raise it once and use the configured instance:
    const expectUi = expect.configure({ timeout: 20000 });
    .
  • Assert the outcome (logged-in heading, order list, confirmation text) — a page can load fine while the journey is broken.
  • try/finally
    with
    page.close()
    so the browser is released even when an assertion throws.
  • Screenshot artifacts aren't a documented SM feature — don't build failure handling around
    page.screenshot()
    ; rely on assertion messages and the check's logs (SM stores per-execution logs in Loki).
  • Web Vitals (
    probe_browser_web_vital_lcp|cls|fcp|inp|ttfb
    ) are collected automatically — no extra code needed.
必填框架:导入
k6/browser
并声明
chromium
浏览器类型。UI会验证这两项。
javascript
// Generated by synthetic-monitoring-checks (https://github.com/grafana/skills) on 2026-07-31T12:00:00Z
import { browser } from 'k6/browser';
import { expect } from 'https://jslib.k6.io/k6-testing/0.6.1/index.js';
import secrets from 'k6/secrets';

export const options = {
  scenarios: {
    ui: {
      executor: 'shared-iterations',
      options: { browser: { type: 'chromium' } },
    },
  },
};

export default async function () {
  const page = await browser.newPage();
  try {
    await page.goto('https://shop.example.com/login');

    // 优先使用role/label/test-id定位器,而非CSS链式选择——它们能在页面重构后仍正常工作。
    const password = await secrets.get('shop-monitor-password');
    await page.getByLabel('Email').fill('sm-monitor@example.com');
    await page.getByLabel('Password').fill(password);
    await page.getByRole('button', { name: 'Sign in' }).click();

    // 使用自动重试的断言验证旅程结果——切勿使用sleep()。
    await expect(page.getByRole('heading', { name: 'Your account' })).toBeVisible();

    await page.getByRole('link', { name: 'Orders' }).click();
    await expect(page.getByTestId('order-list')).toBeVisible();
  } finally {
    await page.close();
  }
}
浏览器检查专属规则:
  • 定位器
    getByRole
    /
    getByLabel
    /
    getByTestId
    (请应用团队在需要的地方添加
    data-testid
    )> 文本 > CSS。切勿使用XPath或自动生成的类名。
    getByTestId
    默认对应
    data-testid
    ——使用Cypress做过 instrumentation的应用通常使用
    data-cy
    ,此时可回退到
    page.locator('[data-cy="..."]')
  • 交互前无需手动等待:定位器操作会自动等待元素可见且可用。不要在
    click()
    /
    fill()
    前调用
    waitFor()
    ,不要使用
    waitForLoadState()
    ,绝对不要使用
    sleep()
  • 自动重试的
    expect()
    toBeVisible
    toBeEnabled
    等)是验证非交互状态的等待机制。注意:尽管文本匹配器(
    toHaveText
    /
    toContainText
    )被标注为可重试,但第一次读取到不匹配内容时就会直接失败——例如客户端渲染应用在 hydration 过程中出现空字符串。验证动态文本时,应先定位元素再断言可见性:
    await expect(page.getByText('Order confirmed')).toBeVisible()
  • 断言超时默认5秒——客户端渲染应用首次有意义渲染通常需要更长时间。请设置一次超时时间并使用配置后的实例:
    const expectUi = expect.configure({ timeout: 20000 });
  • 断言结果(登录后的标题、订单列表、确认文本)——页面加载正常不代表旅程可正常完成。
  • 使用
    try/finally
    包裹
    page.close()
    ——即使断言抛出异常,也能释放浏览器资源。
  • 截图并非SM的文档化功能——不要围绕
    page.screenshot()
    构建故障处理逻辑;请依赖断言消息和检查日志(SM将每次执行的日志存储在Loki中)。
  • Web Vitals指标(
    probe_browser_web_vital_lcp|cls|fcp|inp|ttfb
    )会自动收集——无需额外代码。

Validate locally, then deploy

本地验证,再部署

SM scripts are plain k6 scripts — always run them locally first:
bash
k6 run script.js                                              # scripted check
K6_BROWSER_HEADLESS=true k6 run browser-check.js              # browser check
k6 run --secret-source=mock=checkout-monitor-password=example-password script.js   # with secrets
SM脚本是标准的k6脚本——务必先在本地运行:
bash
k6 run script.js                                              # 脚本化检查
K6_BROWSER_HEADLESS=true k6 run browser-check.js              # 浏览器检查
k6 run --secret-source=mock=checkout-monitor-password=example-password script.js   # 带密钥的检查

Many/large secrets: k6 run --secret-source=file=secrets.txt script.js

大量密钥:k6 run --secret-source=file=secrets.txt script.js


Pass = exit code 0, one iteration, no failed assertions in the summary. Run it 3–5 times;
a script that is 90% reliable locally will page you nightly from 3 probes.

Then create the check (pick one):

- **UI**: Testing & synthetics → Synthetics → Add new check → *k6 scripted* / *k6
  browser* → paste script → select probes + frequency → **Test** (runs once without
  saving) → Save.
- **API or Terraform**: see [`references/api-and-terraform.md`](references/api-and-terraform.md).
  Key gotchas: API `frequency`/`timeout` are **milliseconds** and `settings.scripted.script`
  / `settings.browser.script` are **base64-encoded**; Terraform takes the plain script
  via `file()`.

通过标准:退出码为0,运行一次迭代,摘要中无失败断言。请运行3–5次;本地可靠性为90%的脚本,在3个探测节点上会每晚触发告警。

然后创建检查(选择一种方式):

- **UI**:Testing & synthetics → Synthetics → 添加新检查 → *k6 scripted* / *k6 browser* → 粘贴脚本 → 选择探测节点+执行频率 → **测试**(不保存运行一次)→ 保存。
- **API或Terraform**:参考[`references/api-and-terraform.md`](references/api-and-terraform.md)。关键注意事项:API中的`frequency`/`timeout`单位是**毫秒**,`settings.scripted.script` / `settings.browser.script`需要**base64编码**;Terraform通过`file()`读取原始脚本。

Verify it works, and rollback

验证有效性并回滚

Wait one frequency interval, then in Explore against the Synthetic Monitoring metrics (Prometheus) datasource:
promql
undefined
等待一个执行周期后,在Explore中针对Synthetic Monitoring指标(Prometheus)数据源运行以下查询:
promql
undefined

1 from every selected probe = healthy

所有选定探测节点返回1即为健康

probe_success{job="checkout-flow"}
probe_success{job="checkout-flow"}

Assertion pass rate per named assertion (scripted/browser)

每个命名断言的通过率(脚本化/浏览器检查)

probe_check_success_rate{job="checkout-flow"}
probe_check_success_rate{job="checkout-flow"}

Journey duration per probe — confirm it's comfortably under the timeout

每个探测节点的旅程时长——确认远低于超时时间

probe_script_duration_seconds{job="checkout-flow"}
probe_script_duration_seconds{job="checkout-flow"}

Uptime over time (how the SM app computes it)

历史可用性(SM应用的计算方式)

max by () (max_over_time(probe_success{job="checkout-flow"}[5m]))

A healthy first execution: `probe_success == 1` from every probe, all
`probe_check_success_rate` series at 1, duration stable across probes, and the check's
prebuilt dashboard (Synthetics → check → View dashboard) showing logs for each execution.
Browser checks should additionally show `probe_browser_web_vital_*` series.

**Rollback**: set the check's `enabled: false` (UI toggle, API update, or Terraform) to
stop executions without losing history; delete the check only when you no longer need
its configuration. Alerting: start with `alertSensitivity` / the default alert rules on
`probe_success` — see the [`testing`](../testing/SKILL.md) skill for alert rule examples.
max by () (max_over_time(probe_success{job="checkout-flow"}[5m]))

首次执行健康的标志:所有探测节点的`probe_success == 1`,所有`probe_check_success_rate`序列为1,各探测节点的时长稳定,且检查的预构建仪表盘(Synthetics → 检查 → 查看仪表盘)显示每次执行的日志。浏览器检查还应显示`probe_browser_web_vital_*`序列。

**回滚**:将检查的`enabled: false`(UI开关、API更新或Terraform配置)以停止执行但保留历史记录;仅当不再需要配置时才删除检查。告警:从`alertSensitivity`或`probe_success`的默认告警规则开始——告警规则示例请参考[`testing`](../testing/SKILL.md)技能。

Common failure modes

常见故障模式

SymptomCause → fix
Passes locally, fails on all probesTarget not reachable from the public internet (internal DNS, VPN, IP allowlist). Use private probes for internal targets, or allowlist probe egress
Passes locally, fails on some probesGeo-blocking, regional CDN/WAF rules, or bot protection challenging datacenter IPs. Check
probe
label on failures; exempt the SM
userAgent
or those regions in the WAF
Check "fails" in your eyes but
probe_success
stays 1
Bare
check()
without
fail()
/
expect()
— failures are recorded as metrics only. Convert to
expect()
or
check(...) || fail(...)
Browser check flaps with locator timeoutsBrittle selectors or animation timing. Switch to
getByRole
/
getByTestId
, assert with auto-retrying
expect()
, remove manual waits
toBeVisible
reports
Expected: visible / Received: hidden
but the element is clearly visible
The locator matches multiple elements (strict mode) — the error message is misleading. Tighten the selector or use
.first()
Create succeeds but readback 404s on every executionThe id exceeds
Number.MAX_SAFE_INTEGER
(2^53) and
res.json()
silently rounded it — extract int64 ids from the raw body as strings (see the OpenAPI section)
secrets.get()
fails
Secret name mismatch (names are exact, ≤253 chars, letters/numbers/
-
/
_
), secret deleted (checks fail until recreated), or the editing user lacks the Admin/Editor role or "Checks writer" permission
Executions time out but the journey is fineTimeout too low for the journey (max 180s) — raise it; or the script does unbounded work per iteration. Also confirm timeout < frequency
Target has crashed
in browser check logs
Page exceeds the 1GB probe browser memory — trim the journey, block heavy third-party resources, or use a private probe with more memory
UI rejects a browser scriptBundled/minified script fails the UI's import validation — create it via API or Terraform instead
Metrics/billing explosion after adding a checkUnique IDs in URLs creating per-execution time series — use
http.url
, and check frequency × probe count against the cost formula above
症状原因 → 修复方案
本地通过,所有探测节点失败目标无法从公网访问(内部DNS、VPN、IP白名单)。针对内部目标使用私有探测节点,或将探测节点出口IP加入白名单
本地通过,部分探测节点失败地域封禁、区域CDN/WAF规则,或机器人防护机制针对数据中心IP发起挑战。查看失败记录的
probe
标签;在WAF中豁免SM的
userAgent
或对应区域
你认为检查“失败”但
probe_success
始终为1
使用了原生
check()
但未搭配
fail()
/
expect()
——失败仅会被记录为指标。转换为
expect()
或`check(...)
浏览器检查因定位器超时出现抖动选择器不够健壮或动画时序问题。切换为
getByRole
/
getByTestId
,使用自动重试的
expect()
断言,移除手动等待
toBeVisible
报告
Expected: visible / Received: hidden
但元素明显可见
定位器匹配到多个元素(严格模式)——错误信息具有误导性。收紧选择器或使用
.first()
创建成功但每次执行读取都返回404ID超过
Number.MAX_SAFE_INTEGER
(2^53),
res.json()
将其静默取整——按字符串从原始Body中提取int64类型ID(参考OpenAPI部分)
secrets.get()
失败
密钥名称不匹配(名称需完全一致,≤253字符,仅包含字母/数字/
-
/
_
)、密钥已删除(检查会失败直到重新创建),或编辑用户缺少Admin/Editor角色或“Checks writer”权限
执行超时但旅程本身正常超时时间设置过低(最大180秒)——调高超时时间;或脚本每次迭代执行无边界操作。同时确认超时时间<执行频率
浏览器检查日志中出现
Target has crashed
页面超过探测节点浏览器的1GB内存限制——精简旅程、阻止第三方重资源,或使用内存更高的私有探测节点
UI拒绝浏览器脚本打包/压缩后的脚本未通过UI的导入验证——改为通过API或Terraform创建
添加检查后指标/账单激增URL中的唯一ID导致每次执行创建新的时间序列——使用
http.url
,并根据上述成本公式确认执行频率×探测节点数

References

参考资料

  • references/api-and-terraform.md
    — SM API auth + check CRUD payloads (scripted, browser, MultiHTTP) and Terraform examples for every check type, including the protocol checks (HTTP, ping, DNS, TCP, traceroute, gRPC)
  • references/api-and-terraform.md
    ——SM API认证 + 各类型检查(脚本化、浏览器、MultiHTTP)的CRUD请求体,以及所有检查类型(包括HTTP、ping、DNS、TCP、traceroute、gRPC等协议类检查)的Terraform示例

Resources

资源