К основному содержанию
скилл CX Codex

k6-manage — скилл для Claude Code: что делает и как установить

Управляет нагрузочными тестами, логами и метриками в Grafana Cloud k6 через утилиту gcx или прямые запросы curl.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add grafana/skills --skill k6-manage -a claude-code
Открыть на GitHub

Что делает k6-manage

Скилл даёт инструкции для работы с Grafana Cloud k6 (GCk6). Он помогает запускать локальные и облачные тесты производительности, останавливать прогоны, обновлять скрипты, менять расписания и лимиты проектов. Агент обращается к подкомандам gcx k6 или вызывает служебные маршруты через gcx api.

Когда утилита gcx не установлена, скилл переключается на прямой curl к https://api.k6.io и сервису логов https://cloudlogs.k6.io. В режиме curl авторизация требует обязательной передачи токена и ID стека в каждом запросе, иначе API отклонит вызов. Для выборки логов Loki и трейсов Tempo агент дополнительно передаёт заголовок X-K6TestRun-Id.

Какие задачи решает

  • проверить логи и причины падения тестового прогона k6 в Loki
  • запустить нагрузочный тест локально или в облаке Grafana Cloud k6
  • получить метрики и трассировки выполненного теста через API
  • изменить лимиты VUh или расписание тестов в проекте

Примеры запросов агенту

  • Покажи логи тестового прогона 12345
  • Запусти сценарий load_test.js через k6 cloud run
  • Почему упал последний тест k6 и какие ошибки в метриках?

Как установить k6-manage

1Через Skills CLI (Vercel)

npx skills add grafana/skills --skill k6-manage -a claude-code

2Вручную: положить файлы

~/.claude/skills/k6-manage/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/k6-manage/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о k6-manage

Как установить k6-manage в Claude Code?

Выполните команду npx skills add grafana/skills --skill k6-manage -a claude-code. Файл навыка сохранится по пути ~/.claude/skills/k6-manage/SKILL.md.

Что делает скилл k6-manage?

Он обучает агента управлять ресурсами Grafana Cloud k6. Ассистент сможет запускать тесты, извлекать логи из Loki, получать метрики прогонов и настраивать проекты.

Обязательно ли устанавливать утилиту gcx?

Утилита рекомендуется, но не обязательна. Без gcx скилл переходит на работу через обычный curl к https://api.k6.io, используя API-токен и Stack ID.

Исходный текст

SKILL.md · Apache-2.0
---
name: k6-manage
description: Interact with Grafana Cloud k6 (GCk6) — manage load tests, test runs, scripts, projects, schedules, env vars, fetch metrics or logs, and run scripts locally — using the `gcx` CLI (or direct curl when gcx is unavailable). Use this skill whenever the user mentions a k6 cloud test or run, asks to list/edit/create/start/abort k6 load tests, wants to fetch logs or metrics for a test run, manage k6 project limits or schedules, run a k6 script locally or via `k6 cloud run`, or needs to call any `/cloud/v6/`, `/cloud/v5/`, or k6-app Loki endpoint against a Grafana Cloud stack. Trigger even when the user doesn't explicitly say "gcx" or "API" — phrases like "why did my k6 test fail", "show me logs for run X", "bump VUh limit on project Y", or "update my k6 script" all qualify. 
---

# Grafana Cloud k6 — interaction reference

The default path is the `gcx` CLI. When gcx isn't installed, every
endpoint here is still reachable via direct curl against k6's public
hosts — see §1.2 for the auth-header and host-translation rules. Two
principles shape the rest:

- **gcx owns Grafana-side auth (when present).** It injects the right
  headers on every call, so you should not set auth headers yourself.
  The only header you ever set by hand is `X-K6TestRun-Id`, on Loki
  log queries (§4), browser screenshot/file fetches (§6), and Tempo
  trace queries (§7) — anything else gets overwritten or causes
  conflicts. In curl mode the auth headers are manual; see §1.2.
- **Reach for `gcx k6 ...` subcommands first.** They wrap the common
  paths with friendlier ergonomics and handle pagination. Discover
  what's available with `gcx help-tree k6` (and drill in further with
  `gcx help-tree k6 <subcommand>`); fall back to `gcx api` only when
  no subcommand exists for what you need.

---

## 1. Authentication

### 1.1 With gcx (default)

```bash
gcx login --context <ctx>                # one-time OAuth, browser flow
gcx --context <ctx> config check         # expect "✔ Connectivity: online"
```

Once a context is logged in, every `gcx api ...` and `gcx k6 ...` call
inherits its auth state. If a call returns *"Invalid or expired token —
run gcx login to refresh"*, the OAuth session has lapsed — re-run
`gcx login --context <ctx>`.

### 1.2 Without gcx — direct curl

Check `command -v gcx` first. If it's missing, every endpoint in this
skill is still reachable directly against k6's public hosts — three
things change versus the gcx examples elsewhere:

- **Auth headers are manual.** Set both on every call:
  - `Authorization: Bearer <k6_token>`
  - `X-Stack-ID: <int>`
- **Hosts replace the plugin proxy.**
  - REST (`/cloud/v6/...`, `/cloud/v5/...`, `/cloud-resources/v1/...`,
    `/insights/...`) → `https://api.k6.io`
  - Logs (Loki) and traces (Tempo) → `https://cloudlogs.k6.io`
- **No `/api/plugins/k6-app/resources/{cloud,logs,insights}` prefix.**
  Drop it; everything after that prefix in the gcx examples is the
  real k6 path. The doubled `cloud/cloud/` quirk from §2 collapses to
  a single `/cloud/` — the first one was just the proxy route.

#### Obtaining the credentials

Don't guess these — prompt the user once per session for:

1. **k6 API token** — long-lived bearer; the same value
   `gcx k6 auth token` would print when gcx is configured.
2. **Stack** — either the integer **stack ID** (used directly in
   `X-Stack-ID`) or a Grafana **stack URL** (e.g.
   `https://myorg.grafana.net`). If the user supplies a URL, resolve
   it to an ID once with `GET /cloud/v6/auth`, which takes the URL in
   the `X-Stack-Url` header and returns `{stack_id, default_project_id}`:

   ```bash
   STACK_ID=$(curl -sS https://api.k6.io/cloud/v6/auth \
     -H "Authorization: Bearer $K6_TOKEN" \
     -H "X-Stack-Url: $STACK_URL" \
     | jq -r '.stack_id')
   ```

   Cache the resolved ID for the session — every subsequent call
   needs it in `X-Stack-ID`. (Note: `/cloud/v6/auth` is the *only*
   endpoint that takes `X-Stack-Url` instead of `X-Stack-ID` — it's
   how you cross the gap from "user-known URL" to "API-required ID".)

#### Translation cheat-sheet

| gcx form (plugin proxy)                                                                  | curl form (direct)                                                       |
|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| `gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/123`                     | `curl https://api.k6.io/cloud/v6/test_runs/123 -H ...`                   |
| `gcx api /api/plugins/k6-app/resources/cloud/cloud/v5/test_runs/<id>/metrics`            | `curl https://api.k6.io/cloud/v5/test_runs/<id>/metrics -H ...`          |
| `gcx api /api/plugins/k6-app/resources/cloud/cloud-resources/v1/files/index`             | `curl https://api.k6.io/cloud-resources/v1/files/index -H ...`           |
| `gcx api /api/plugins/k6-app/resources/insights/insights/api/v1/testrun/<id>/executions` | `curl https://api.k6.io/insights/api/v1/testrun/<id>/executions -H ...`  |
| `gcx api /api/plugins/k6-app/resources/logs/api/v1/query_range?...`                      | `curl https://cloudlogs.k6.io/api/v1/query_range?... -H ...`             |
| `gcx api /api/plugins/k6-app/resources/logs/api/v1/tempo/api/search?...`                 | `curl https://cloudlogs.k6.io/api/v1/tempo/api/search?... -H ...`        |

In every `-H ...` slot above, send both auth headers:
`-H "Authorization: Bearer $K6_TOKEN" -H "X-Stack-ID: $STACK_ID"`.

Notes:
- Endpoint-specific headers gcx leaves to you — `X-K6TestRun-Id` on
  log, trace, and files endpoints (§4, §6, §7) — are still required
  *in addition* to the auth pair.
- The `gcx api` flag quirks in §2 (spill envelope, `--json field`
  filtering, `-o` for output format) don't apply to curl. Use plain
  curl flags: `-o file` to save body, `--data-binary @file` for PUT
  payloads, `-w '%{http_code}'` for status code, etc.
- Pagination semantics (`$orderby`, `$top`, `$skip`, `@nextLink`
  from §3) are properties of the v6 endpoints themselves and work
  identically over curl. The `@nextLink` URL returned by the server
  is already an absolute `https://api.k6.io/...` URL — pass it back
  to curl unchanged; the plugin-proxy reshape in §3 isn't needed.

---

## 2. How `gcx api` paths are shaped

When no subcommand exists, fall back to `gcx api` against the Grafana
plugin-proxy routes:

- **REST API** (`/cloud/v6/`, `/cloud/v5/`) — prefix with
  `/api/plugins/k6-app/resources/cloud/<k6-path>`.
- **Logs (Loki)** — prefix with
  `/api/plugins/k6-app/resources/logs/<loki-path>`.

| k6 path                                   | gcx invocation                                                                    |
|-------------------------------------------|-----------------------------------------------------------------------------------|
| `/cloud/v6/test_runs/{id}`                | `gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/{id}`             |
| `/cloud/v5/test_runs/{id}/metrics`        | `gcx api /api/plugins/k6-app/resources/cloud/cloud/v5/test_runs/{id}/metrics`     |
| Loki `/api/v1/query_range?...`            | `gcx api /api/plugins/k6-app/resources/logs/api/v1/query_range?...`               |

Note the doubled `cloud/cloud/` in every REST path — the first `cloud`
is the proxy route, the second is k6's `/cloud/v{N}/` namespace.

### `gcx api` flag quirks

`gcx api` is not a curl clone — a few flags differ from what curl muscle
memory suggests:

- **Response body** is written to stdout. There is no `-o <file>` flag
  for saving the body; `-o` selects output *format* (`json`, `yaml`,
  `agents`). Use shell redirection (`> file`) or `$(...)` capture
  instead.
- **Request body** uses `-d <string>`, `-d @file`, or `-d @-` (stdin).
  There is no `--data-binary`; `-d @file` already preserves bytes.
- **Field selection without jq**: `--json field1,field2,...` returns
  only the listed fields, and `--json list` (or `--json '?'`)
  discovers what's available. Often cleaner than piping into jq for
  shallow extractions.
- **Stderr noise**: gcx prints a one-line `hint:` to stderr on most
  invocations. Pipelines into `jq` should redirect with `2>/dev/null`
  to avoid surprises.
- **Response headers** are not directly exposed by `gcx api`. When a
  branch in the workflow hinges on `Content-Type` (e.g. script GET in
  §5), inspect the downloaded body with `file <path>` instead.
- **Large responses spill to a temp file**, with stdout reduced to a
  wrapper envelope. The threshold is a few tens of KB, so even
  moderately-sized list endpoints trigger it. The envelope looks like
  this:
  ```json
  {"spilled_to":"/var/folders/.../gcx-results-<n>.json",
   "bytes":2365037,
   "preview_sample":["@nextLink","value"],
   "message":"Response too large for stdout..."}
  ```
  Naive `json.loads(stdout).get('value', [])` patterns silently return
  empty in this case — the envelope has no `value` key. Two reliable
  fixes:
  - **Pass `-o json`** to force inline output regardless of size
    (recommended for scripts that parse the body). The agent-mode
    default formatting is what triggers the spill; `-o json` opts out.
  - **Or detect the envelope and re-read from the spilled file**:
    ```python
    d = json.loads(stdout)
    if 'spilled_to' in d:
        d = json.load(open(d['spilled_to']))
    ```
  Either works; `-o json` is fewer lines.

- **The Grafana plugin proxy rewrites `Content-Type: multipart/...`
  to `application/json`.** `gcx api` itself forwards your
  `-H "Content-Type: ..."` header correctly (visible in
  `--log-http-payload` traces), but the upstream plugin proxy
  rewrites multipart Content-Types to JSON before they reach k6's
  API. The net effect: endpoints that require multipart bodies —
  notably `POST /cloud/v6/projects/{id}/load_tests` for test
  creation, which takes `name` + `script` as form parts — return
  `HTTP 415 "Unsupported media type \"application/json\""` no
  matter what header you set on the gcx side. Fall back to direct
  curl against `api.k6.io` (§1.2) for these endpoints —
  `curl -F name=... -F script=@...` builds the multipart body for
  you and bypasses the plugin proxy. Other Content-Type values
  (e.g. `application/octet-stream` for the script-update PUT in §5)
  are forwarded through the proxy unchanged.

---

## 3. Discovering and calling endpoints

`gcx k6` subcommands (see `gcx help-tree k6`) cover the common reads.
For everything else — mutations, niche reads, endpoints not surfaced as
subcommands — the k6 Cloud API surface is large and changes over time.
Rather than rely on a cheat-sheet that goes stale, discover the
operation you need from the OpenAPI spec at request time.

### Workflow

1. **Fetch the spec once per session** (it's large; cache to `/tmp`):

   ```bash
   gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/openapi > /tmp/k6-openapi.json
   ```

2. **Index by `operationId` + `description` first** — this keeps the
   working payload small while you search:

   ```bash
   jq '[.paths | to_entries[] | .key as $p | .value | to_entries[]
        | {path: $p, method: .key,
           operationId: .value.operationId,
           description: .value.description}]' /tmp/k6-openapi.json
   ```

   Grep this for the action you need ("abort", "limits", "schedule", …).

3. **Pull the chosen operation's full schema** — parameters, request
   body, response — resolving any `$ref` indirections:

   ```bash
   jq '.paths["<path>"]["<method>"]' /tmp/k6-openapi.json
   # Then for each "$ref": "#/components/schemas/Foo":
   jq '.components.schemas.Foo' /tmp/k6-openapi.json
   ```

4. **Build the `gcx api` call** against the same path, prefixed per §2.
   Authorization is already injected by gcx — do not add it yourself.

### List requests

When the operation you're calling is a list endpoint, default to
**ordering by `created` descending** (newest first) and **paginating
by 20** — both via whatever query parameter names the OpenAPI schema
for that operation exposes. Names vary (`ordering=-created`,
`order_by=created.desc`, `sort=-created`, `limit=20`, `page_size=20`),
which is exactly why discovering them from the spec in step 3 matters.
Newest-first means the entries the user usually cares about land in
the first page; a page size of 20 keeps the response small enough to
summarise without burning context.

#### Full enumeration: paginating with `@nextLink`

When you need to enumerate *all* rows (not just the newest page), the
v6 list endpoints cap each response at **1000 rows** (the default
`$top`) and return an `@nextLink` field pointing at the next page.
**`gcx k6 runs list --limit 0` does NOT auto-follow** `@nextLink` and
defaults to ascending order — for tests with >1000 historical runs it
silently returns the **oldest** 1000, not the newest. The
practical effect: `(first/last_run)` summarised from this output can
be wildly stale (e.g. a daily-scheduled test that's been running for
years will show a `last_run` from ~3 years ago). Use `gcx api`
against the v6 endpoint with `$orderby=created desc` for the
newest-first slice, or loop on `@nextLink` until it's absent for full
enumeration.

The v6 list endpoints expose OData-style query parameters:

| Param      | Purpose                                                              |
|------------|----------------------------------------------------------------------|
| `$orderby` | Ordering — value must be `<field> <direction>` (e.g. `created desc`). The allowed fields are operation-specific; `created` is the common one. Default is ascending. |
| `$top`     | Page size, default 1000 (also the cap)                              |
| `$skip`    | Offset (`@nextLink` already encodes this for you)                   |
| `$count`   | Include total count in response                                     |

Two practical defaults for the loop: `$orderby=created desc` so the
first page contains the newest runs (most of the time the agent only
cares about those), and `-o json` to opt out of the spill envelope
(see §2) so the parsing is uniform across pages.

```python
import json, subprocess, urllib.parse

CTX = "<stack>"
TEST_ID = "<test_id>"
PROXY = "/api/plugins/k6-app/resources/cloud"

all_runs = []
# Start with newest first; -o json keeps the body inline for clean json.loads().
path = (
    f"{PROXY}/cloud/v6/load_tests/{TEST_ID}/test_runs"
    "?%24orderby=created%20desc&%24top=1000"
)
while path:
    r = subprocess.run(["gcx", "--context", CTX, "api", path, "-o", "json"],
                       capture_output=True, text=True, timeout=60)
    d = json.loads(r.stdout)
    all_runs.extend(d.get("value", []))
    nxt = d.get("@nextLink")
    if not nxt:
        path = None
        continue
    # @nextLink comes back as an absolute http://api.k6.io/... URL with
    # OData params already encoded ($skip, $top, $orderby). Reshape its
    # path+query into the plugin-proxy prefix:
    p = urllib.parse.urlparse(nxt)
    path = f"{PROXY}{p.path}" + (f"?{p.query}" if p.query else "")
```

Same pattern works for any v6 list endpoint that returns `@nextLink`,
not just `/test_runs`. If you only need the few most-recent items,
stop the loop after the first page — with `$orderby=created desc` that
page is already the newest 1000.

### Metrics

Time-series and aggregate metrics live on the **v5** API
(`/cloud/v5/...`) — Prometheus-like query semantics inside OData-style
URL function-calls (`query_range_k6(query='...',metric='...')`), not
captured by the v6 OpenAPI. The full endpoint reference, selector
syntax, query methods per metric type, and worked examples are inlined
in [`references/metrics.md`](references/metrics.md) — read that before
constructing a metrics query.

### Logs

See §4.

---

## 4. Logs (Loki via `gcx api`)

The plugin proxies Loki under `/api/plugins/k6-app/resources/logs/`. The
only header you supply by hand is `X-K6TestRun-Id`; gcx handles
authorization.

```bash
RUN_ID="<run_id>"

# Scope the log window to the run itself: `created` for start, `ended`
# for end. If `ended` is null the run is still in progress — fall back
# to "now". Use a wider hard-coded window only when the run object's
# timestamps don't fit what you're investigating.
RUN=$(gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID 2>/dev/null)
# k6 emits sub-second precision (e.g. "...:58.984041Z"); jq's
# fromdateiso8601 only accepts whole seconds, so strip the fraction.
START=$(echo "$RUN" | jq -r '.created | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601')
END=$(echo "$RUN"   | jq -r 'if .ended then (.ended | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) else (now | floor) end')

QUERY=$(printf '{test_run_id="%s"}' "$RUN_ID" | jq -sRr @uri)

# Save the raw response to /tmp/ — log payloads can be large; streaming
# them inline burns context. Summarise from the file, then jq into
# specific entries only when needed.
gcx api "/api/plugins/k6-app/resources/logs/api/v1/query_range?query=${QUERY}&direction=backward&start=${START}&end=${END}&limit=1000" \
  -H "X-K6TestRun-Id: $RUN_ID" 2>/dev/null > /tmp/run_${RUN_ID}_logs.json

# Tight summary — counts and stream labels only:
jq '{status, streams: (.data.result | length),
     total_entries: ([.data.result[].values | length] | add),
     stream_labels: [.data.result[].stream]}' /tmp/run_${RUN_ID}_logs.json
```

Pull individual entries from `/tmp/run_${RUN_ID}_logs.json` as needed
(e.g. `jq '.data.result[].values[]' …`) rather than re-running the
query.

Every LogQL query **must** include the `{test_run_id="<run_id>"}` stream
selector — the plugin proxy partitions logs by run and rejects (or
returns nothing for) queries without it. Layer additional filters on
top of that selector:

- `{test_run_id="<run_id>"}` — everything
- `{test_run_id="<run_id>"} | level=~"(error|warn)"` — errors and warnings only
- `{test_run_id="<run_id>"} |= "specific text"` — substring filter

Direction & window tips:

- **Scope `start`/`end` to the run's `created`/`ended` fields**, not to
  wall-clock "last hour" — otherwise queries silently miss anything
  older than the window. The only reason to deviate is when you
  specifically want a different interval (e.g. surrounding context).
- `direction=forward` with `start = run.created` to find the *first* errors.
- `direction=backward` with `end = run.ended` (or now, if still running)
  to find the *most recent* output.
- Loki retention typically outlives k6's cascade-delete, so logs of a
  deleted child run may still be queryable for a while.

---

## 5. Editing a test script safely

### Two distinct script endpoints

There are two GET endpoints that return a k6 script body, and they are
not interchangeable:

| Endpoint                                    | What it returns                           | When to use                                            |
|---------------------------------------------|-------------------------------------------|--------------------------------------------------------|
| `/cloud/v6/load_tests/<test_id>/script`     | The **current** load-test script (mutable; supports PUT) | Editing the script; reading current state              |
| `/cloud/v6/test_runs/<run_id>/script`       | The script **snapshot bundled into a specific run** (read-only) | Investigating what a past run actually executed; diffing across versions |

The two can drift apart: if the load-test script is edited after a
run completes, that run's bundled snapshot stays frozen at what it
executed, while the load-test endpoint serves the new current version.
The bundled-snapshot endpoint is GET-only — you cannot mutate
historical bytes.

The safe-edit recipe below uses the load-test endpoint (the editable
one). For run-vs-run script diffs (e.g. "did the script change between
the last passing and first failing run?"), GET both runs'
`/test_runs/<id>/script` and diff them locally.

### Script body format

The script GET endpoint returns one of two shapes — always detect which
before assuming the format:

- **Single js/ts file**. The recipe below handles this case directly.
- **k6 tar archive** (plain tar or gzipped). A multi-file project
  bundle (entry script + imported modules + assets). To edit: extract
  the JS file, modify it, then use `k6 archive` to rebuild the
  archive from the modified JS. Do **not** manually repack with
  `tar` — the archive contains a `metadata.json` with parsed options
  (projectID, thresholds, scenarios) that `k6 archive` regenerates
  correctly from the script source. Manual repacking preserves stale
  metadata and causes runtime errors (e.g. projectID mismatch).

  ```bash
  # Edit a k6 tar archive
  tar -xf /tmp/script_body -C /tmp/extracted/   # extract
  # ... edit the JS file ...
  k6 archive /tmp/edited_script.js -O /tmp/new_archive.tar  # rebuild
  ```

`gcx api` doesn't expose response headers (see §2), so detect the shape
by inspecting the downloaded body with `file(1)`:

```bash
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
  > /tmp/script_body
file /tmp/script_body
# Expected output (examples):
#   ASCII text          → js/ts source
#   POSIX tar archive   → k6 tar bundle
#   gzip compressed data → gzipped tar bundle
```

The PUT body must be the raw script (or archive) bytes
(`application/octet-stream`). gcx forwards the request to k6 unchanged.

```bash
ID="<load_test_id>"

# 1. Pull current script (single-file js/ts case)
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
  > /tmp/script_current.js

# 2. Backup
cp /tmp/script_current.js /tmp/script_backup_$(date +%s).js

# 3. Edit → /tmp/script_new.js

# 4. Local parse-only sanity check
k6 inspect /tmp/script_new.js | head -20

# 5. 1-iteration smoke (edit IN-FILE; --iterations CLI flag breaks browser scenarios)
sed 's/iterations: [0-9]\+/iterations: 1/' /tmp/script_new.js > /tmp/script_1iter.js
k6 run --quiet /tmp/script_1iter.js   # exit 0 = pass, 99 = threshold fail

# 6. PUT — Content-Type must be application/octet-stream
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
  -X PUT \
  -H "Content-Type: application/octet-stream" \
  -d "@/tmp/script_new.js"

# 7. Verify by re-fetching and comparing sha256 — the two hashes MUST match.
#    Do not rely on the `updated` timestamp; it does not bump on script PUT.
gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/load_tests/$ID/script \
  > /tmp/script_verify.js
shasum -a 256 /tmp/script_new.js /tmp/script_verify.js
```

If the PUT returns 415, double-check that `-H "Content-Type:
application/octet-stream"` made it through (some shell quoting mistakes
can drop it). Pass `-vvv` to `gcx api` for the request/response trace
when debugging.

---

## 6. Browser screenshots

Browser-module runs write screenshot PNGs to per-run S3 storage —
that's the only artifact type the files API surfaces today. They're
not exposed via `/cloud/v6/`; retrieval goes through a separate
`cloud-resources/v1/files/` plugin route in a two-step flow. Both
calls require the same `X-K6TestRun-Id` header used for log queries
(§4) — gcx still handles auth, but the run ID must be supplied by
hand or the endpoint rejects with HTTP 422.

```bash
RUN_ID="<run_id>"

# 1. Index — JSON array of screenshot paths owned by this run. Empty
#    array if the run took none (e.g. non-browser tests, or browser
#    runs that didn't call page.screenshot()).
gcx api /api/plugins/k6-app/resources/cloud/cloud-resources/v1/files/index \
  -H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/files_${RUN_ID}.json

# Each entry looks like:
#   "7542817/files/screenshots/screenshots/login-success.png"
# The doubled "screenshots/screenshots/" segment is real, not a typo —
# pass the path back unchanged to step 2.

# 2. Request pre-signed download URLs for the files you want. The
#    request body must be JSON; Content-Type matters.
FILES=$(jq -c '[.[] | {name: .}]' /tmp/files_${RUN_ID}.json)
PAYLOAD=$(jq -nc --argjson files "$FILES" \
  '{service:"aws_s3", operation:"download", files:$files}')

gcx api /api/plugins/k6-app/resources/cloud/cloud-resources/v1/files/generate-pre-signed-url \
  -X POST \
  -H "X-K6TestRun-Id: $RUN_ID" \
  -H "Content-Type: application/json" \
  -d "$PAYLOAD" -o json 2>/dev/null > /tmp/presigned_${RUN_ID}.json

# 3. Download each pre-signed URL directly — they're plain S3 GETs, no
#    gcx and no auth header (the signature is in the URL). The URLs
#    are valid for 24h (X-Amz-Expires=86400); re-run step 2 if they
#    expire.
mkdir -p /tmp/run_${RUN_ID}_files
jq -r '.urls[] | [.name, .pre_signed_url] | @tsv' /tmp/presigned_${RUN_ID}.json |
  while IFS=$'\t' read -r NAME URL; do
    curl -sS -o "/tmp/run_${RUN_ID}_files/$(basename "$NAME")" "$URL"
  done
```

### Notes worth knowing

- **Index before signing.** `generate-pre-signed-url` does *not*
  validate that the file exists — it will happily mint a URL for any
  key, and the 404 only surfaces when you try to download (S3 returns
  `<Error><Code>NoSuchKey</Code>...`). Always derive the file list
  from `files/index`, not from a guess at the path shape.
- **Batch the sign request.** `files` is an array, so request all the
  URLs you need in one POST rather than one call per file — same
  24h expiry covers the whole batch.
- **Browser tests vs. protocol tests.** Only runs that called
  `page.screenshot()` have entries in the index; a pure protocol/HTTP
  run will return `[]`. Don't assume the index is non-empty.

---

## 7. Browser traces (Tempo via `gcx api`)

Browser-module runs emit OTel spans for every iteration, navigation,
locator click, screenshot, web-vital observation, etc. The plugin
proxies a Tempo backend under `/api/plugins/k6-app/resources/logs/`
(same prefix as Loki — see §4), and the same `X-K6TestRun-Id` header
is required on every call. Without it, both search and fetch return
HTTP 401 `"Test run ID missing"`.

Retrieval is a two-step flow: TraceQL search → fetch full trace by
ID. The full trace is large (≈100 KB on disk for a 120-span iteration
because `-o json` pretty-prints; ~50 KB compact), so the
join+summarise step is what keeps agent context lean.

```bash
RUN_ID="<run_id>"

# 1. Find the run's start/end window and pick the scenario name.
#    Same caveat as §4: strip the sub-second fraction before
#    fromdateiso8601, and fall back to "now" if the run is still
#    running. Add a small buffer because spans can land slightly
#    after the run object reports `ended`.
RUN=$(gcx api /api/plugins/k6-app/resources/cloud/cloud/v6/test_runs/$RUN_ID 2>/dev/null)
START=$(echo "$RUN" | jq -r '.created | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601')
END=$(  echo "$RUN" | jq -r 'if .ended then (.ended | sub("\\.[0-9]+Z$"; "Z") | fromdateiso8601) else (now|floor) end')
START=$((START - 60))
END=$((END + 60))

SCENARIO="ui"  # whatever your scenario is named; check run.ui.exec / k6 script

# 2. Search for iteration root spans. TraceQL goes in `q`; URL-encode
#    it. `start`/`end` must be unix seconds — RFC3339 returns
#    HTTP 400 `invalid start: strconv.ParseUint`. limit=500 caps the
#    result, enough to cover several thousand iterations.
Q=$(printf '{ name = "iteration" && span.test.iteration.number >= 0 && span.test.vu >= 0 && span.test.scenario = "%s" }' "$SCENARIO")
QE=$(printf '%s' "$Q" | jq -sRr @uri)

gcx api "/api/plugins/k6-app/resources/logs/api/v1/tempo/api/search?start=${START}&end=${END}&q=${QE}&limit=500" \
  -H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/traces_${RUN_ID}.json

# Each entry has: traceID (32-char hex), rootServiceName ("k6"),
# rootTraceName ("iteration"), durationMs, startTimeUnixNano, and a
# spanSet preview of the iteration root span's attributes
# (test.iteration.number, test.vu, test.scenario).

# 3. Fetch a full trace. The response is OTLP-shaped:
#    { "batches": [ { "resource": {...}, "scopeSpans": [ { "spans": [...] } ] } ] }
TRACE_ID=$(jq -r '.traces[0].traceID' /tmp/traces_${RUN_ID}.json)
gcx api "/api/plugins/k6-app/resources/logs/api/v1/tempo/api/traces/$TRACE_ID" \
  -H "X-K6TestRun-Id: $RUN_ID" -o json 2>/dev/null > /tmp/trace_${TRACE_ID}.json
```

### Compact summary for an agent

The OTLP body for a single iteration can run ~100 KB pretty-printed.
Dumping it verbatim into context is wasteful — most of the useful
signal lives in the span-name distribution, the slowest individual
spans, and the web-vital ratings. The pipeline below collapses a
trace into ~1–2 KB of plain text:

```bash
jq -r '
  # OTel value envelope: pick by key presence, not //-chaining — jq's //
  # treats `false` as missing and would drop legitimate boolValue:false.
  def attrval:
    if   has("stringValue") then .stringValue
    elif has("intValue")    then .intValue
    elif has("boolValue")   then .boolValue
    elif has("doubleValue") then .doubleValue
    else null end;
  def attrs: [.attributes[] | {(.key): (.value | attrval)}] | add // {};
  def dur:   (((.endTimeUnixNano|tonumber) - (.startTimeUnixNano|tonumber)) / 1000000 | floor);

  [.batches[].scopeSpans[].spans[] | {name, dur: dur, a: attrs, status: (.status.code // "OK")}] as $spans

  | "── span-name rollup (count · total ms · max ms) ──",
    ([$spans[] | {name, dur}]
       | group_by(.name) | map({name: .[0].name, n: length,
                                t: (map(.dur)|add), m: (map(.dur)|max)})
       | sort_by(-.t)[]
       | "  \(.n|tostring|.+"      "|.[0:4])  \(.t|tostring|.+"        "|.[0:7])  \(.m|tostring|.+"      "|.[0:6])  \(.name)"),

    "\n── slowest spans (ms · name · navigation.url / page.goto.url) ──",
    ([$spans[] | select(.name != "iteration")] | sort_by(-.dur) | .[0:8][]
       | "  \(.dur|tostring|.+"        "|.[0:7])  \(.name)\(if .a["navigation.url"] // .a["page.goto.url"] then "  " + (.a["navigation.url"] // .a["page.goto.url"]) else "" end)"),

    "\n── web-vital ratings ──",
    ([$spans[] | select(.name == "web_vital")...

© grafana · Apache-2.0 · grafana/skills

Похожие скиллы

Всё по теме «Тесты и QA» →
improve-codebase-architecture Matt Pocock Скилл ищет архитектурные узкие места в кодовой базе и показывает варианты углубления модулей в HTML-отчёте. Подойдёт для ревью перед рефакторингом. 1 163 035установок design-taste-frontend Leonxlnx Скилл задаёт арт-дирекцию для публичных страниц и рабочих экранов, а затем проверяет результат в браузере по скриншотам. Нужен, когда интерфейс выглядит шаблонным или недоработанным. CX Codex +1 678 700установок tdd Matt Pocock Скилл управляет циклом красный-зелёный в TDD: проверяет, что тесты пишутся до кода, держит их на уровне публичных интерфейсов и не завязывает на внутреннюю структуру. 1 141 062установок skill-creator официальный anthropics Помогает создать новый скилл, доработать существующий и проверить результат через тестовые промпты и оценки. 414 180установок agent-browser vercel-labs Скилл управляет браузером через CLI: открывает страницы, заполняет формы, нажимает кнопки и проверяет веб-приложения. Подходит для задач, где агенту нужно действовать в интерфейсе. CX Codex +1 1 200 000установок prototype Matt Pocock Создаёт временный прототип, чтобы проверить логику, модель состояния или вид интерфейса до работы над основным кодом. 986 201установок

Из того же набора47

skill-authoring Помогает писать и проверять SKILL.md по рубрике из четырёх критериев, которую использует CI-гейт grafana/skills. Пригодится, когда скилл не срабатывает или не проходит ревью. CX Codex +1 3 800установок prometheus-label-strategy Помогает спроектировать схему меток Prometheus, оценить кардинальность и убрать лишние лейблы в коде приложения или таргетах сбора до отправки в хранилище. CX Codex +1 3 800установок k6-perf-test-website Разворачивает гибридные нагрузочные тесты сайта на k6, связывает протокольные сценарии с проверками в браузере и следит за нагрузкой генератора. CX Codex +1 3 300установок datasources-provisioning Генерирует готовые файлы провижининга источников данных Grafana в формате YAML или Terraform по официальной схеме плагина из CDN. CX Codex +1 3 000установок check-npm Проверяет конфигурацию пакетного менеджера в JS/TS-репозитории: версии инструментов, lifecycle-скрипты, опасные протоколы зависимостей и минимальный возраст релиза. CX Codex +1 2 800установок audit-and-reduce-dependencies Очищает дерево зависимостей JavaScript в проектах с pnpm и сокращает размер lockfile без изменения структуры воркспейса. Пригодится при оптимизации плагинов Grafana и монорепозиториев. CX Codex +1 2 800установок k6-cloud-investigate-test Ведёт агента по девяти шагам разбора теста или прогона Grafana Cloud k6: скрипт, история запусков, статус, метрики и логи. CX Codex +1 2 600установок k6-trend-analysis Анализирует динамику прогонов тестов Grafana Cloud k6 и выявляет медленную деградацию метрик до срабатывания порогов. Пригодится для оценки запаса производительности и аудита тестов. CX Codex +1 2 600установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой