k6 1.5.0 is here 🎉! This release includes:

- Changes in the browser module:
  - `page.waitForEvent()` for event-based synchronization with page events.
  - `locator.pressSequentially()` for character-by-character typing simulation.
- Improved debugging with deep object logging in `console.log()`.
- Extended WebSocket support with close code and reason information.
- Enhanced extension ecosystem with custom subcommands and DNS resolver access.
- URL-based secret management for external secret services.
- New machine-readable summary format for test results.

## Breaking changes

As per our [stability guarantees](https://grafana.com/docs/k6/latest/reference/versioning-and-stability-guarantees/), breaking changes across minor releases are allowed only for experimental features.

### Breaking changes for experimental modules

- [#5237](https://github.com/grafana/k6/pull/5237) Deprecates the `experimental/redis` module. The module will be removed in a future release. Users should migrate to alternative solutions, such as [the official k6 Redis extension](https://github.com/grafana/xk6-redis).

## New features

### `page.waitForEvent()` [#5478](https://github.com/grafana/k6/pull/5478)

The browser module now supports [`page.waitForEvent()`](https://grafana.com/docs/k6/latest/javascript-api/k6-browser/page/waitforevent/), which blocks the caller until a specified event is captured. If a predicate is provided, it waits for an event that satisfies the predicate. This method is particularly valuable for testing scenarios where you need to synchronize your test flow with specific browser or page events before proceeding with the next action.

Event-driven synchronization is vital for test reliability, especially when dealing with asynchronous operations where timing is unpredictable. This is more robust than using fixed delays and helps avoid flaky tests.

```javascript
import { browser } from 'k6/browser';

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

export default async function () {
  const page = await browser.newPage();

  // Wait for a console message containing specific text
  const msgPromise = page.waitForEvent('console', msg => msg.text().includes('hello'));
  await page.evaluate(() => console.log('hello world'));
  const msg = await msgPromise;
  console.log(msg.text());
  // Output: hello world

  // Wait for a response from a specific URL with timeout
  const resPromise = page.waitForEvent('response', {
    predicate: res => res.url().includes('/api/data'),
    timeout: 5000,
  });
  await page.click('button#fetch-data');
  const res = await resPromise;

  await page.close();
}
```

### `locator.pressSequentially()` [#5457](https://github.com/grafana/k6/pull/5457)

The browser module now supports [`locator.pressSequentially()`](https://grafana.com/docs/k6/latest/javascript-api/k6-browser/locator/presssequentially/), which types text character by character, firing keyboard events (`keydown`, `keypress`, `keyup`) for each character. This method is essential for testing features that depend on gradual typing to trigger specific behaviors, such as autocomplete suggestions, real-time input validation per character, or dynamic character counters. Thank you, @rajan2345, for the contribution 🎉

The method supports a configurable delay between keystrokes, enabling you to simulate realistic typing speeds and test time-dependent input handlers:

```javascript
import { browser } from 'k6/browser';

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://test.k6.io/browser.php');

    // Type text character by character
    const searchInput = page.locator('#text1');
    await searchInput.pressSequentially('Hello World');

    // Type with delay to simulate realistic typing speed
    await searchInput.clear();
    await searchInput.pressSequentially('test query', { delay: 100 });
  } finally {
    await page.close();
  }
}
```

This complements existing text input methods: [`locator.fill()`](https://grafana.com/docs/k6/latest/javascript-api/k6-browser/locator/fill/) for simple form filling, [`locator.type()`](https://grafana.com/docs/k6/latest/javascript-api/k6-browser/locator/type/) for gradual typing without keyboard events, and now `pressSequentially` for character-by-character typing with full keyboard event firing. Thank you, @rajan2345, for the contribution 🎉

### `console.log()` Deep Object Logging [#5460](https://github.com/grafana/k6/pull/5460)

`console.log()` now properly traverses and displays complex JavaScript structures, including functions, classes, and circular references. Previously, Sobek's JSON marshaling would lose nested functions, classes, and other non-serializable types, making debugging painful.

Objects with mixed function and class properties are now properly displayed:

```javascript
console.log({
  one: class {},
  two: function() {}
});
// Before: {}
// After:  {"one":"[object Function]","two":"[object Function]"}
```

Nested arrays and objects with functions are now fully traversed:

```javascript
console.log([
  { handler: class {} },
  { data: [1, 2, class {}] }
]);
// Before: [{},{"data":[1,2,null]}]
// After:  [{"handler":"[object Function]"},{"data":[1,2,"[object Function]"]}]
```

Complex objects with multiple property types are properly preserved:

```javascript
console.log({
  a: [1, 2, 3],
  b: class {},
  c: () => {},
  d: function() {},
  e: [1, () => {}, function() {}, class {}, 2]
});
// Before: {"a":[1,2,3],"e":[1,null,null,null,2]}
// After:  {
//   "a":[1,2,3],
//   "b":"[object Function]",
//   "c":"[object Function]",
//   "d":"[object Function]",
//   "e":[1,"[object Function]","[object Function]","[object Function]",2]
// }
```

Circular references are now properly detected and marked:

```javascript
const obj = {
  fn: function() {},
  foo: {}
};
obj.foo = obj;

console.log(obj);
// Before: [object Object]
// After:  {"fn":"[object Function]","foo":"[Circular]"}
```

This improvement makes debugging k6 test scripts significantly easier when working with API responses, event handlers, and complex state objects.

### `experimental/websockets` - Close Code and Reason Support [#5376](https://github.com/grafana/k6/pull/5376)

The experimental WebSockets module now supports sending close codes and reasons when closing connections, and properly captures close event information. This is essential for testing WebSocket
implementations that rely on specific close codes to determine whether a connection was closed normally or due to an error.

```javascript
import ws from 'k6/experimental/websockets';

export default function () {
  const socket = ws.connect('ws://example.com', (socket) => {
    socket.on('close', (data) => {
      console.log(`Connection closed with code: ${data.code}, reason: ${data.reason}`);
      // Output: Connection closed with code: 1000, reason: Normal closure
    });
  });

  // Close with code and reason
  socket.close(1000, 'Normal closure');
}
```

Thanks, @etodanik, for the contribution 🎉

### Subcommand Extension Support [#5399](https://github.com/grafana/k6/pull/5399)

Extensions can now register custom subcommands under the `k6 x` namespace, enabling custom command-line tools that integrate seamlessly with k6. This provides a consistent and discoverable way for extensions to offer specialized CLI utilities while maintaining k6's familiar command structure.

Extensions can now define custom commands like:

```bash
k6 x my-tool --help
k6 x debug --inspect
```

This integration pattern allows extension authors to provide powerful tooling that feels native to the k6 ecosystem.

### DNS Resolver Access [#5421](https://github.com/grafana/k6/pull/5421)

Extensions can now access k6's DNS resolver for custom DNS handling and networking extensions. The resolver respects k6's configuration including `hosts` overrides, custom DNS servers, and DNS caching settings. This enables extensions to use it directly instead of having to reproduce the functionality. Which also makes them work the same way as native modules.

### Machine-Readable Summary Format [#5338](https://github.com/grafana/k6/pull/5338)

A new machine-readable summary format for the end-of-test summary is now available, providing structured, programmatic shapes via `--summary-export` and `handleSummary()`. This format is designed for easier integration with external systems and analytics pipelines.

The new format is currently opt-in via the `--new-machine-readable-summary` flag or `K6_NEW_MACHINE_READABLE_SUMMARY` environment variable, and will become the default in k6 v2:

```bash
k6 run script.js --new-machine-readable-summary --summary-export=summary.json
```

This makes it easier to integrate k6 results into CI/CD pipelines, dashboards, and custom analysis tools.

### URL-Based Secret Management [#5413](https://github.com/grafana/k6/pull/5413)

The secret management system now supports URL-based secret sources, allowing k6 to fetch secrets from HTTP endpoints. This let users implement a simple HTTP API to provide secrets to a test. There is mock implementation, but no particular production ready implementation is provided at this time. In the future there is potential for proxies to other systems including HashiCorp Vault, AWS Secrets Manager or Azure Key Vault.

## UX improvements and enhancements

- [#5458](https://github.com/grafana/k6/pull/5458) Adds link to k6 extensions list in README for better discoverability.
- [#5366](https://github.com/grafana/k6/pull/5366) Adds multi-source secret example for better documentation of secret management patterns.

## Bug fixes

- [#5374](https://github.com/grafana/k6/pull/5374) Fixes `getBy*` selectors when using quotes inside element names.
- [#5477](https://github.com/grafana/k6/pull/5477) Fixes retry mechanism when frame has been detached.
- [#5461](https://github.com/grafana/k6/pull/5461) Fixes panic when using nil `page.on` handlers.
- [#5401](https://github.com/grafana/k6/pull/5401) Fixes panic when assigning to nil headers and cookies when host is not found. Thanks, @chojs23, for the contribution 🎉
- [#5379](https://github.com/grafana/k6/pull/5379) Fixes browsers not being stopped in tests due to `EndIteration`.
- [#5439](https://github.com/grafana/k6/pull/5439) Fixes loading files with spaces.
- [#5406](https://github.com/grafana/k6/pull/5406) Fixes error messages after Sobek/goja changes.
- [#5381](https://github.com/grafana/k6/pull/5381) Fixes version command JSON output for output extensions.
- [#5358](https://github.com/grafana/k6/pull/5358) Fixes sending correct data when using `ArrayViews` in experimental/websockets.
- [#5488](https://github.com/grafana/k6/pull/5488) Fixes a goroutine leak when performing CDP requests.

## Maintenance and internal improvements

- [#5464](https://github.com/grafana/k6/pull/5464) Adds `ExecutionStatusMarkedAsFailed` status for improved test execution tracking.
- [#5467](https://github.com/grafana/k6/pull/5467) Refactors span error recording to avoid boilerplate code.
- [#5438](https://github.com/grafana/k6/pull/5438) Unblocks the release workflow for package publishing.
- [#5436](https://github.com/grafana/k6/pull/5436) Optimizes browser module allocations and improves CI stability.
- [#5411](https://github.com/grafana/k6/pull/5411) Extends base config and stops updating go.mod toolchain.
- [#5408](https://github.com/grafana/k6/pull/5408) Removes redundant GitHub Actions rule.
- [#5415](https://github.com/grafana/k6/pull/5415) Updates Sobek dependency to latest version.
- [#5392](https://github.com/grafana/k6/pull/5392) Updates OpenTelemetry proto module to v1.9.0.
- [#5357](https://github.com/grafana/k6/pull/5357) Refactors browser module task queue usage.
- [#5378](https://github.com/grafana/k6/pull/5378) Fixes `TestNavigationSpanCreation` test in the browser module.
- [#5482](https://github.com/grafana/k6/pull/5482) Fixes tests and subcommand handling in version command.
- [#5255](https://github.com/grafana/k6/pull/5255) Updates gRPC module to v1.77.0.
- [#5506](https://github.com/grafana/k6/pull/5506) Updates xk6-redis to v0.3.6.
- [#5473](https://github.com/grafana/k6/pull/5473) Updates compression library to v1.18.2.
- [#5505](https://github.com/grafana/k6/pull/5505) Removes `close` call in integration tests.
- [#5517](https://github.com/grafana/k6/pull/5517) Removes `SECURITY.md` to sync with Grafana's org-wide security policy documentation.
- [#5528](https://github.com/grafana/k6/pull/5528) Resolves CVE-2025-61729. Thanks, @SimKev2, for the contribution 🎉

## Roadmap

### Deprecation of First Input Delay (FID) Web Vital

Following the official [web vitals guidance](https://web.dev/blog/fid), First Input Delay (FID) is no longer a Core Web Vital as of September 9, 2024, having been replaced by Interaction to Next Paint (INP). The k6 browser module already emits INP metrics, and we're planning to deprecate FID support to align with industry standards.

FID only measures the delay before the browser runs your event handler, so it ignores the time your code takes and the delay to paint the UI—often underestimating how slow an interaction feels. INP captures the full interaction latency (input delay + processing + next paint) across a page’s interactions, so it better reflects real user-perceived responsiveness and is replacing FID.

#### Action required

If you're currently using FID in your test scripts for thresholds or relying on it in external integrations, you should migrate to using INP as soon as possible.

```javascript
// Instead of relying on FID
export const options = {
  thresholds: {
    // 'browser_web_vital_fid': ['p(95)<100'], // Deprecated
    'browser_web_vital_inp': ['p(95)<200'], // Use INP instead
  },
};
```

This change ensures k6 browser testing stays aligned with modern web performance best practices and Core Web Vitals standards.
