Skip to content

Add extension usage telemetry - #6126

Merged
inancgumus merged 16 commits into
masterfrom
add/ext-telemetry
Jul 30, 2026
Merged

inancgumus merged 16 commits into
masterfrom
add/ext-telemetry

Conversation

@inancgumus

@inancgumus inancgumus commented Jul 3, 2026 •

Copy link
Copy Markdown
Contributor

What?

Adds anonymous extension usage to the k6 usage report, covering the three user-facing surfaces:

  • An extension (a k6/x/ module) is reported when a run resolves it, not when it is merely compiled into the binary.
  • An output extension is reported when a run selects it with --out; built-in outputs are unaffected.
  • A subcommand extension is reported when invoked via k6 x, whether it exits zero or non-zero.

Each entry carries the extension's Go module path, version, and kind. Only extensions listed in the public registry catalog are reported: matching is by module path, so a private fork reusing a public import name is dropped, and an unreachable catalog reports nothing rather than falling back to unfiltered data. When no cataloged extension was used, the report omits the field entirely. The existing --no-usage-report / K6_NO_USAGE_REPORT opt-out gates all of it; no new opt-out, no new endpoint, and the send stays bounded and non-fatal: it never fails a command, and a hanging endpoint delays exit by at most 3 seconds, the same bound the k6 run report has always had.

Why?

Extension adoption is a blind spot: the usage report tracks built-in modules but deliberately drops every extension, so decisions about which extensions to promote, maintain, or retire run on guesswork. Cloud-executed runs are partly tracked, while local execution, most OSS usage, stays invisible. Reporting catalog extensions through the same anonymous report k6 already sends closes that gap without a new data category: private and unlisted extensions never appear, xk6-built and provisioned binaries report identically, and a newly cataloged extension becomes reportable as soon as the catalog cache refreshes, with no new k6 release.

Note

  • Spec: Extension telemetry #6110 holds the requirements, edge cases, and test vectors.
  • The output surface ships now but reports nothing in production until the catalog lists output extensions.
  • Distinguishing xk6 builds from provisioned binaries is out of scope.
  • One new env var, K6_USAGE_REPORT_URL, makes the report endpoint overridable so the integration tests can assert on the actually-sent report; catalog membership reuses the existing K6_PROVISION_CATALOG_URL override.

Checklist

  • I have performed a self-review of my code.
  • I have commented on my code, particularly in hard-to-understand areas.
  • I have added tests for my changes.
  • I have run linter and tests locally (make check) and all pass.

Related PR(s)/Issue(s)

@inancgumus
inancgumus requested a review from a team as a code owner July 3, 2026 19:02
@inancgumus
inancgumus requested review from ankur22, mstoykov and szkiba and removed request for a team and mstoykov July 3, 2026 19:02
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 3, 2026 19:08 — with GitHub Actions Inactive
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 3, 2026 19:10 — with GitHub Actions Inactive
@inancgumus inancgumus self-assigned this Jul 3, 2026
szkiba
szkiba previously approved these changes Jul 7, 2026

@ankur22 ankur22 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work!

A few questions inline, none blocking. Two small things here:

  • The description says a slow endpoint "never delays or fails a command", but k6 x sends its report before exiting, so a hanging endpoint can add up to 3 seconds to the exit. Probably fine, just worth softening the wording.
  • The usage-collection docs will need an update — k6 x now sends a report, and the run report has a new extensions field. Is that tracked somewhere?

Comment thread internal/cmd/report.go
Comment thread internal/cmd/run.go
Comment thread internal/cmd/subcommand.go
Comment thread internal/cmd/subcommand.go
@inancgumus

Copy link
Copy Markdown
Contributor Author

Great review, @ankur22! Yes, Task #19 tracks the docs update, which I'll do once this PR merges 🤝 I've also reworded the PR description. For the review requests, I've created a stacked follow-up PR, which we can merge after this one.

The usage report endpoint was a hardcoded const posting to stats.grafana.org, so no test could observe the anonymous usage report. Reading the endpoint from K6_USAGE_REPORT_URL (defaulting to the production endpoint) lets integration tests point it at a local server and assert the report is sent.
The usage report tracked only built-in modules and dropped every k6/x/
import, so extension adoption was invisible. Resolved Go extensions are
now recorded in a dedicated extensions bucket at the cached resolution
site, identified by registry membership rather than the k6/x/ name, and
reported with their Go module path, version, and kind, de-duplicated
per module and kind.

Only extensions whose module path appears in the public registry
catalog are reported, so private or unlisted extensions never leak. The
catalog is read through the same cache file, TTL, and
K6_PROVISION_CATALOG_URL override the k6 x subcommand path uses,
fetched inside the bounded report send off the run's hot path, and an
unreachable catalog fail-closed reports nothing. When nothing
catalogued was used, the extensions key is omitted rather than sent as
an empty array, consistent with how modules behaves.
The usage report must not list the same extension multiple times when many VUs import it. This adds a --vus 5 --iterations 5 acceptance row asserting a module imported across VUs yields exactly one js entry, locking in the per-(module,kind) de-duplication done at report assembly.
The usage report must list every imported extension, not collapse
distinct modules into one slot. This guards run's reporting so that
two extensions in separate Go packages, which resolve to distinct
module paths, each surface as their own js entry and survive
de-duplication.
An empty or fully filtered extensions result omits the key entirely,
consistent with how modules behaves, so key presence always means at
least one catalogued extension was used. Nothing pinned that. This adds
a row asserting a run that uses no extension produces no extensions key
rather than an empty array.
The report filter keys on Go module path, but nothing pinned that a private fork reusing a public import name resolves to its own path and is dropped. A guard row now imports an in-tree fork registered under a catalogued import name while the catalog advertises only the real extension's path; the fork's distinct module path keeps it out of the report, so the row fails if matching ever regresses to import names.
catalogModulePaths returns nothing when the catalog cannot be fetched
and resolveExtensions drops the raw bucket before consulting it, so an
unreachable catalog with no cache reports no extensions instead of
leaking the unfiltered list. Nothing pinned that. This adds a row
importing a used extension against an unreachable catalog, asserting
the report carries no extensions and the run still exits 0.
The usage report gained a catalog fetch and extension enrichment. This guards that the existing K6_NO_USAGE_REPORT opt-out still gates all of it: with the opt-out set, neither the report endpoint nor the catalog server is contacted, so no new opt-out is needed and none of the added work runs ahead of the gate.
Output extensions chosen via --out were tracked only in the built-in
outputs list, so catalogued xk6 output extensions never appeared in the
usage report. A selected output that is a member of the output registry
is now recorded as a resolved registry entry in the shared extensions
bucket, so the report filters it by module path like every other
surface. Built-in outputs stay out of the bucket.
The output extension bucket shared the same catalog filter as JS imports, but nothing pinned that behavior. This adds a test row proving a used output extension absent from the catalog is dropped from the usage report, so a future change that routes outputs around the filter fails loudly.
Extension usage recording keys the extensions bucket on output-registry membership, so built-in outputs like json were never reported as extensions. Without a test, a future change could broaden recording to all outputs and start leaking built-ins. This guards that json stays listed under outputs and never produces an output-kind extension entry.
Baked-in subcommands invoked via `k6 x` sent no usage telemetry, so the
extension registry had no signal about their use. The root execute seam
now reports once after the command runs, using cobra's ExecuteC to
recover the command that ran and its error, so a registered subcommand
extension is reported whether it succeeds or fails, through the shared
usage-report transport. It carries only the run-independent identifying
fields and the catalogued extension entry, bounded and non-fatal so it
never fails or delays the command, and the command's own hook is left
untouched.
createSubcommandReport keeps a subcommand's entry only when its module
path appears in the public catalog, matching the run path's filter.
Nothing pinned that. This adds a row running a baked-in subcommand
whose module path the catalog omits, asserting the report carries no
entry for it.
Before, nothing guarded against a usage report firing for a subcommand
name the binary does not provide. This adds a guard: running k6 x with
an unregistered name against an unreachable catalog builds no command
and sends no report, so an early or stray report call would now be
caught.
The subcommand report rides the shared bounded, non-fatal send, so a
slow or unreachable endpoint cannot stall or fail k6 x. Nothing pinned
that. This adds a row proving the send is abandoned within the bounded
run-report timeout, the command still exits 0 with no surfaced error,
and the failure surfaces only as a debug-level log.
Nothing exercised the K6_NO_USAGE_REPORT gate on the k6 x report path, so a regression could re-enable telemetry unnoticed. This adds a table row that runs k6 x under the opt-out and asserts neither the report endpoint nor the catalog is consulted.
@inancgumus
inancgumus changed the base branch from add/ext-telemetry-spec to master July 28, 2026 23:39
@inancgumus
inancgumus dismissed szkiba’s stale review July 28, 2026 23:39

The base branch was changed.

@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 23:45 — with GitHub Actions Inactive
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 23:47 — with GitHub Actions Inactive

@ankur22 ankur22 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🚀

Comment thread internal/cmd/report.go
@inancgumus
inancgumus requested a review from szkiba July 30, 2026 13:24
@inancgumus
inancgumus merged commit 5407282 into master Jul 30, 2026
59 checks passed
@inancgumus
inancgumus deleted the add/ext-telemetry branch July 30, 2026 14:03
@inancgumus inancgumus added this to the v2.2.0 milestone Jul 30, 2026
inancgumus added a commit to grafana/k6-docs that referenced this pull request Jul 31, 2026
The usage report now lists used extensions from the public extension
catalog, and extension subcommands send a report of their own. Updates
the usage collection page with the extensions entry, the catalog-only
scope, the k6 x report, and which opt-outs cover it.

Related: grafana/k6#6126
Related: grafana/k6#6229
inancgumus added a commit to grafana/k6-docs that referenced this pull request Aug 4, 2026
* docs: document extension usage collection

The usage report now lists used extensions from the public extension
catalog, and extension subcommands send a report of their own. Updates
the usage collection page with the extensions entry, the catalog-only
scope, the k6 x report, and which opt-outs cover it.

Related: grafana/k6#6126
Related: grafana/k6#6229

* docs: extension telemetry use admonition

Co-authored-by: Heitor Tashiro Sergent <heitortsergent@gmail.com>

---------

Co-authored-by: Heitor Tashiro Sergent <heitortsergent@gmail.com>
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
k6 2.2.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>k6 `v2.2.0` is here 🎉! This release includes:

- `k6 cloud run --local-execution` now streams k6's logs to Grafana Cloud, so the test run's log view works for local execution too.
- `chromium.connectOverCDP()`, which connects browser tests to an already-running Chromium instance.
- `TextEncoder` and `TextDecoder` available as globals, and `WritableStream` support in `k6/experimental/streams`.
- A `k6 cloud load-zone list` command.
- Two new experimental feature flags: `merge-run-tags` and `freeze-env`.

## Breaking changes

There are no breaking changes in this release.

## New features

### `k6 cloud run --local-execution` streams logs to Grafana Cloud [#6171](grafana/k6#6171)

When a cloud test runs locally with `k6 cloud run --local-execution`, k6's logs now stream to the Grafana Cloud test run, so the run's log view is populated the same way it is for cloud execution. Previously, local-execution logs stayed on the machine running k6 and never reached the cloud. Work with Grafana's secrets management to safely work with secrets and redact them if they're accidentally leaked into logs are pushed. Use the new `--no-cloud-logs` flag opts out to opt out of streaming of logs when working with `--local-execution`:

```shell
k6 cloud run --local-execution script.js
k6 cloud run --local-execution --no-cloud-logs script.js
```

### Connect to a running browser with `chromium.connectOverCDP()` [#6165](grafana/k6#6165)

The browser module can now attach to an existing Chromium-based browser over the Chrome DevTools Protocol, mirroring Playwright's `browserType.connectOverCDP()`. Pass the browser's WebSocket endpoint and k6 manages the returned browser's connection — it's auto-closed at the end of the iteration, though you can call `close()` earlier to release the connection on demand.

```js
import { chromium } from 'k6/browser';

export default async function () {
  const browser = await chromium.connectOverCDP('ws://localhost:9222/devtools/browser/<id>');
  const page = await browser.newPage();

  try {
    await page.goto('https://quickpizza.grafana.com/');
  } finally {
    await page.close();
    await browser.close();
  }
}
```

Unlike the `K6_BROWSER_WS_URL` environment variable, the endpoint is a runtime value — you can, for example, request a fresh session URL from a browser provider's API in `setup()` and connect to it from the iterations.

### `TextEncoder` and `TextDecoder` globals [#6182](grafana/k6#6182)

`TextEncoder` and `TextDecoder` are now available as standard globals in both the init and VU contexts, no import required — matching how they are exposed in browsers and other JavaScript runtimes.

```js
const encoded = new TextEncoder().encode('Hello, world!');
const decoded = new TextDecoder().decode(encoded);
```

### `WritableStream` in `k6/experimental/streams` [#6132](grafana/k6#6132)

The experimental streams module now implements `WritableStream` and `WritableStreamDefaultWriter` following the WHATWG Streams specification, complementing the existing `ReadableStream` and paving the way for a future `TransformStream` implementation.

```js
import { WritableStream } from 'k6/experimental/streams';

export default async function () {
  const stream = new WritableStream({
    write(chunk) {
      console.log(`wrote ${chunk}`);
    },
  });

  const writer = stream.getWriter();
  await writer.write('hello');
  await writer.close();
}
```

### `k6 cloud load-zone list` command [#6142](grafana/k6#6142)

A new `k6 cloud load-zone list` subcommand lists the load zones — public and private — available in the configured Grafana Cloud k6 stack, mirroring the existing `k6 cloud project list` command. Output defaults to a human-readable table; pass `--json` to emit a JSON array instead.

```console
$ k6 cloud load-zone list
Load zones for https://example.grafana.net:

ID                     NAME                     TYPE      AVAILABLE
amazon:us:ashburn      Ashburn, US (Amazon)     public    yes
amazon:sa:cape town    Cape Town, SA (Amazon)   public    yes
```

### Configurable `handleSummary()` timeout [#5854](grafana/k6#5854)

The time budget for the `handleSummary()` callback — previously hardcoded to 120 seconds — is now configurable through the `handleSummaryTimeout` option or the `K6_HANDLE_SUMMARY_TIMEOUT` environment variable, so long-running tests with heavy summaries no longer fail with `handleSummary() execution timed out`. Thanks, @LBaronceli!

```js
export const options = {
  handleSummaryTimeout: '5m',
};
```

### New experimental feature flags: `merge-run-tags` and `freeze-env`

Two new experimental flags join the feature-flag system introduced in v2.1.0:

- [#5714](grafana/k6#5714) `merge-run-tags` merges run tags per key across config layers, so `options.tags` in a script is no longer silently discarded when `--tag` or `K6_TAGS` is also used — higher-priority layers win on conflicting keys instead of replacing the whole map. Thanks, @yordis!
- [#6032](grafana/k6#6032) `freeze-env` freezes the `__ENV` object, so modifications from script code throw a `TypeError` (in strict mode) instead of silently persisting across iterations and scenarios. Thanks, @lohitkolluri!

```shell
k6 run --features merge-run-tags,freeze-env script.js
```

## UX improvements and enhancements

- [#5631](grafana/k6#5631) Makes the browser module's header accessors — `response.allHeaders()`, `headerValue()`, `headerValues()`, and `headersArray()` — return the raw wire headers (including `Set-Cookie` and security-related headers), correctly paired with each hop of a redirect chain instead of Chrome's provisional headers. As part of this, `headerValues()` now matches header names case-insensitively and splits repeated values on newlines rather than commas, and the `browser_data_sent`/`browser_data_received` metrics now include the raw header bytes and no longer vary run-to-run with CDP event ordering.
- [#6208](grafana/k6#6208) Makes `k6 cloud` reject the run flags (for example, `--vus`) with an `unknown flag` error and a non-zero exit code. Previously `k6 cloud --vus 10 script.js` accepted the flags, printed the help text, and exited 0 — running tests with `k6 cloud` directly was deprecated in v2.0.0 in favor of `k6 cloud run`.
- [#6096](grafana/k6#6096) Points the cloud secrets error at `K6_CLOUD_SECRETS_TOKEN` and `K6_CLOUD_SECRETS_ENDPOINT` when a test run is reused via `K6_CLOUD_PUSH_REF_ID` under `--local-execution`, instead of suggesting the `--local-execution` flag the user is already using.
- [#6196](grafana/k6#6196) Adds `catch` blocks to the browser examples so a failing iteration reports the original error instead of a subsequent `page.close()` failure. Thanks, @locker95!

## Bug fixes

- [#6234](grafana/k6#6234) Classifies HTTP/2 errors by message so the `error_code` metric tag stays correct when k6 is built with Go 1.27 (whose `x/net/http2` delegates to the standard library), and explicitly enables HTTP/2 negotiation on VU transports.
- [#6232](grafana/k6#6232) Drains queued log entries in the Loki hook at shutdown so `--out loki` and cloud log streaming no longer lose the final batch, and emits a `k6 dropped N log messages` warning when the cloud log buffer overflows instead of dropping logs silently.
- [#6125](grafana/k6#6125) Serializes the first concurrent open of a file in the caching filesystem so parallel `fs.open()` calls on the same file no longer read zero or truncated bytes.
- [#6147](grafana/k6#6147) Fixes a data race and inconsistent request-interception state when browser routes are added or removed concurrently. Thanks, @somak2kai!
- [#6070](grafana/k6#6070) Flushes buffered file log output once per second so recent logs aren't lost when k6 is killed before shutdown. Thanks, @rohan-patnaik!
- [#6205](grafana/k6#6205) Stops sending an invalid `Sec-WebSocket-Protocol` header when tailing Grafana Cloud logs; spec-strict servers rejected the handshake with `websocket: bad handshake`.
- [#6200](grafana/k6#6200) Leaves a counter's `rate` unset when the observed duration is zero, instead of computing `+Inf` and spuriously failing `rate` thresholds. Thanks, @samarth70!
- [#6195](grafana/k6#6195) Initializes a gauge's maximum from the first sample so all-negative gauge series no longer report `max=0`. Thanks, @Solaris-star!
- [#6145](grafana/k6#6145) Prevents the OpenTelemetry output from panicking at startup when basic auth is configured without `K6_OTEL_HEADERS`. Thanks, @lukdz!
- [#6140](grafana/k6#6140) Stops `SharedArray` deep-freezing JS primitives, which needlessly wrapped large strings in `String` objects — cutting memory usage in the reported reproduction from roughly 1 GB to 100 MB.

## Maintenance and internal improvements

- [#6126](grafana/k6#6126), [#6224](grafana/k6#6224), [#6229](grafana/k6#6229) Adds anonymous extension usage to the k6 usage report: a run reports the Go module path, version, and type of registry-cataloged extensions it actually uses (imported `k6/x/` modules, output extensions selected with `--out`, and `k6 x` subcommands). Private and unlisted extensions are never reported, and the existing `--no-usage-report` opt-out covers it.
- [#6183](grafana/k6#6183), [#6218](grafana/k6#6218) Updates Sobek and regexp2, making `W

See merge request: Harmonybrew/homebrew-core!16299

This branch was previously deployed

1 inactive deployment
azure-trusted-signing — b23fe9f2 Deployed Jul 28, 2026 by inancgumus via sign-packages #10165
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add extension usage telemetry

4 participants