Skip to content

Fix extension usage reporting gaps - #6224

Merged
inancgumus merged 10 commits into
masterfrom
fix/ext-telemetry-reporting-gaps
Jul 30, 2026
Merged

inancgumus merged 10 commits into
masterfrom
fix/ext-telemetry-reporting-gaps

Conversation

@inancgumus

@inancgumus inancgumus commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

What?

Fixes three reporting gaps in extension usage telemetry:

  • A run that used no extensions no longer contacts the catalog; the report goes out without the needless fetch.
  • A nested subcommand (k6 x docs search) is reported. A builtin sharing an extension's name is not.
  • noUsageReport suppresses the subcommand report, matching the run report; the env var keeps precedence.

Why?

Extension telemetry should count what users actually run and respect opt-outs.

@inancgumus
inancgumus requested a review from a team as a code owner July 28, 2026 17:07
@inancgumus
inancgumus requested review from ankur22 and janHildebrandt98 and removed request for a team July 28, 2026 17:07
@inancgumus inancgumus changed the title fix/ext telemetry reporting gaps Fix extension usage reporting gaps Jul 28, 2026
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 17:13 — with GitHub Actions Inactive
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 17:17 — with GitHub Actions Inactive
@inancgumus inancgumus mentioned this pull request Jul 28, 2026
4 tasks done
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 22:11 — with GitHub Actions Inactive
@inancgumus
inancgumus force-pushed the fix/ext-telemetry-reporting-gaps branch from 02a493c to 5916301 Compare July 28, 2026 22:13
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 22:19 — with GitHub Actions Inactive
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 22:21 — with GitHub Actions Inactive
Every reported run consulted the extension catalog, fetching it on a
cold or stale cache even when the script used no extensions, which is
most runs. The fetch also spent part of the bounded report window with
nothing to filter.

The report step now resolves the catalog lazily, only after finding
recorded extension usage, so an extension-less run sends its report
without touching the registry.
The subcommand report matched the executed command's bare name against
the extension registry. cobra returns the deepest command it ran, so an
extension invoked through a nested subcommand (k6 x docs search) was
never reported, while a builtin sharing a registered extension's name
(k6 version) was reported without the extension running. Two of the
four catalogued subcommand extensions already nest, so their real usage
went uncounted.

The report now resolves the extension from the direct child of the
root-level x command on the executed command's parent chain: nested
invocations count toward their extension, builtins stay unreported, and
the catch-all x help and provisioning stubs stay excluded.
The subcommand report checked only K6_NO_USAGE_REPORT, while the run
report also honors the config file's noUsageReport option. The public
usage-collection docs list the config file as a supported opt-out, so a
user who disabled reporting there still sent a report on every k6 x
extension run.

The subcommand gate now reads the config file too, with the env var
winning like on the run path, and fails closed by skipping the send
when either source is unreadable, logging the skip at debug. Ordinary
commands never pay for the file read: the extension match returns
first.
createReport's catalog parameter became a func, and the unit tests
passed a bare nil. That was safe only because no case records an
extension: the old nil map filtered everything out when consulted, but
calling a nil func panics.

The tests now pass a func returning a nil catalog, so a future case
that records an extension filters against an empty catalog instead of
panicking.
K6_NO_USAGE_REPORT is documented to win over the config file's
noUsageReport, but no test set both sources at once, so inverting the
Apply order in reportSubcommandUsage passed the whole telemetry suite.

The config-file opt-out test now tables the env variant: with
K6_NO_USAGE_REPORT=false against the file's noUsageReport: true, one
report must go out on both the run and subcommand paths. The
inverted-precedence mutant now fails it.
The walk's root-level check in subcommandExtension had no covering
test: simplifying its condition to a bare name match passed the whole
suite while an invocation under an extension's own x-named subcommand
went unreported. Rows for a grandchild and an x-named nested
subcommand now pin the attribution; the weakened guard fails them.
Only the negative half of the builtin-name collision was pinned:
running the builtin sends nothing. A walk that additionally
blacklisted builtin names would have passed the suite while a
builtin-named extension invoked under x silently went unreported. The
positive case pins that k6 x version reports the extension.
reportSubcommandUsage skips the send when the config file is
unreadable, but no test covered that branch: swallowing the read error
and reporting anyway would have passed the suite. The new case pins
that a malformed config file suppresses the report even when the env
re-enables it, while the subcommand still runs.
The hardcoded module path never matched the registered extension: a
func module resolves to its constructor's symbol name, so the catalog
fixture filtered every extension out and the count-only assertions hid
it. Deriving the path via ext.Get keeps the fixture real, like the
sibling report tests.
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 23:34 — with GitHub Actions Inactive
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 28, 2026 23:36 — with GitHub Actions Inactive
@inancgumus
inancgumus force-pushed the fix/ext-telemetry-reporting-gaps branch from 79e80f4 to 2b1399e Compare July 28, 2026 23:40
@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:48 — with GitHub Actions Inactive
k6 x <name> --help reported the extension, but k6 help x <name>,
which displays the same help, did not, undercounting extension
usage. Resolve the root help command to the command whose help it
displayed before matching it to an extension, so both help forms
count. k6 help x and k6 x help <name> describe the x namespace
rather than an extension and stay unreported.
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 29, 2026 01:20 — with GitHub Actions Inactive
@inancgumus
inancgumus temporarily deployed to azure-trusted-signing July 29, 2026 01:22 — 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 🚀

@inancgumus
inancgumus requested a review from szkiba July 29, 2026 21:11

@szkiba szkiba 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

stacked auto-merge was automatically disabled July 30, 2026 13:51

Pull Request is not mergeable

stacked auto-merge was automatically disabled July 30, 2026 13:55

Pull Request is not mergeable

Base automatically changed from add/ext-telemetry to master July 30, 2026 14:03
@inancgumus
inancgumus merged commit e574be4 into master Jul 30, 2026
65 of 66 checks passed
@inancgumus
inancgumus deleted the fix/ext-telemetry-reporting-gaps branch July 30, 2026 14:03
@inancgumus inancgumus added this to the v2.2.0 milestone Jul 30, 2026
@inancgumus inancgumus self-assigned this Jul 30, 2026
@inancgumus inancgumus mentioned this pull request Aug 6, 2026
1 of 7 tasks
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 — 3374facb Deployed Jul 29, 2026 by inancgumus via sign-packages #10167
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.

4 participants