diff --git a/docs/sources/.eslintrc.js b/docs/sources/.eslintrc.js index a5ab855e0..332400d63 100644 --- a/docs/sources/.eslintrc.js +++ b/docs/sources/.eslintrc.js @@ -40,5 +40,6 @@ module.exports = { __ENV: 'readonly', __ITER: 'readonly', open: 'readonly', + window: 'readonly', }, }; diff --git a/docs/sources/v0.50.x/_index.md b/docs/sources/v0.50.x/_index.md new file mode 100644 index 000000000..77c60159d --- /dev/null +++ b/docs/sources/v0.50.x/_index.md @@ -0,0 +1,105 @@ +--- +aliases: + - /docs/k6/ +description: 'The k6 documentation covers everything you need to know about k6 OSS, load testing, and performance testing.' +menuTitle: Grafana k6 +title: Grafana k6 documentation +weight: -10 +--- + +# Grafana k6 documentation + +This documentation will help you go from a total beginner to a seasoned k6 expert! + +## Get started + + + +## What is k6? + +Grafana k6 is an open-source load testing tool that makes performance testing easy and productive for engineering teams. +k6 is free, developer-centric, and extensible. + +Using k6, you can test the reliability and performance of your systems and catch performance regressions and problems earlier. +k6 will help you to build resilient and performant applications that scale. + +k6 is developed by [Grafana Labs](https://grafana.com/) and the community. + +Watch the video below to learn more about k6 and why it could be the missing puzzle in your Grafana stack. + +{{< youtube id="1mtYVDA2_iQ" >}} + +## Key features + +k6 is packed with features, which you can learn all about in the documentation. +Key features include: + +- [CLI tool](https://grafana.com/docs/k6//using-k6/k6-options/how-to) with developer-friendly APIs. +- Scripting in JavaScript ES2015/ES6 - with support for [local and remote modules](https://grafana.com/docs/k6//using-k6/modules) +- [Checks](https://grafana.com/docs/k6//using-k6/checks) and [Thresholds](https://grafana.com/docs/k6//using-k6/thresholds) - for goal-oriented, automation-friendly load testing + +## Use cases + +k6 users are typically Developers, QA Engineers, SDETs, and SREs. +They use k6 for testing the performance and reliability of APIs, microservices, and websites. +Common k6 use cases are: + +- **Load testing** + + k6 is optimized for minimal resource consumption and designed for running high load tests + ([spike](https://grafana.com/docs/k6//testing-guides/test-types/spike-testing), [stress](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing), [soak tests](https://grafana.com/docs/k6//testing-guides/test-types/soak-testing)). + +- **Browser testing** + + Through [k6 browser](https://grafana.com/docs/k6//using-k6-browser), you can run browser-based performance testing and catch issues related to browsers only which can be skipped entirely from the protocol level. + +- **Chaos and resilience testing** + + You can use k6 to simulate traffic as part of your chaos experiments, trigger them from your k6 tests or inject different types of faults in Kubernetes with [xk6-disruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor). + +- **Performance and synthetic monitoring** + + With k6, you can automate and schedule to trigger tests very frequently with a small load to continuously validate the performance and availability of your production environment. + +## Load Testing Manifesto + +Our load testing manifesto is the result of having spent years hip deep in the trenches, doing performance- and load testing. +We’ve created it to be used as guidance, helping you in getting your performance testing on the right track! + +- [Simple testing is better than no testing](https://k6.io/our-beliefs/#simple-testing-is-better-than-no-testing) +- [Load testing should be goal oriented](https://k6.io/our-beliefs/#load-testing-should-be-goal-oriented) +- [Load testing by developers](https://k6.io/our-beliefs/#load-testing-by-developers) +- [Developer experience is super important](https://k6.io/our-beliefs/#developer-experience-is-super-important) +- [Load test in a pre-production environment](https://k6.io/our-beliefs/#load-test-in-a-pre-production-environment) + +## What k6 does not + +k6 is a high-performing load testing tool, scriptable in JavaScript. The architectural design to have these capabilities brings some trade-offs: + +- **Does not run natively in a browser** + + By default, k6 does not render web pages the same way a browser does. + Browsers can consume significant system resources. + Skipping the browser allows running more load within a single machine. + + However, with [k6 browser](https://grafana.com/docs/k6//using-k6-browser), you can interact with real browsers and collect frontend metrics as part of your k6 tests. + +- **Does not run in NodeJS** + + JavaScript is not generally well suited for high performance. + To achieve maximum performance, the tool itself is written in Go, embedding a JavaScript runtime allowing for easy test scripting. + + If you want to import npm modules or libraries using NodeJS APIs, you can [bundle npm modules with webpack](https://grafana.com/docs/k6//using-k6/modules#bundling-node-modules) and import them in your tests. diff --git a/docs/sources/v0.50.x/examples/_index.md b/docs/sources/v0.50.x/examples/_index.md new file mode 100644 index 000000000..3b126c0c8 --- /dev/null +++ b/docs/sources/v0.50.x/examples/_index.md @@ -0,0 +1,10 @@ +--- +title: Examples +weight: 11 +--- + +# Examples + + + +{{< section >}} diff --git a/docs/sources/v0.50.x/examples/api-crud-operations.md b/docs/sources/v0.50.x/examples/api-crud-operations.md new file mode 100644 index 000000000..2339ad529 --- /dev/null +++ b/docs/sources/v0.50.x/examples/api-crud-operations.md @@ -0,0 +1,261 @@ +--- +title: 'API CRUD Operations' +description: 'This example covers the usage of k6 to test a REST API CRUD operations.' +weight: 10 +--- + +# API CRUD Operations + +The examples showcase the testing of CRUD operations on a REST API. + +CRUD refers to the basic operations in a database: Create, Read, Update, and Delete. We can map these operations to HTTP methods in REST APIs: + +- _Create_: HTTP `POST` operation to create a new resource. +- _Read_: HTTP `GET` to retrieve a resource. +- _Update_: HTTP `PUT`or `PATCH` to change an existing resource. +- _Delete_: HTTP `DELETE` to remove a resource. + +This document has two examples, one that uses the core k6 APIs (`k6/http` and `checks`) and another to show the more recent APIs [`httpx`](https://grafana.com/docs/k6//javascript-api/jslib/httpx) and [`k6chaijs`](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs)). + +## Test steps + +In the [setup() stage](https://grafana.com/docs/k6//using-k6/test-lifecycle#setup-and-teardown-stages) we create a user for the [k6 HTTP REST API](https://test-api.k6.io/). We then retrieve and return a bearer token to authenticate the next CRUD requests. + +The steps implemented in the [VU stage](https://grafana.com/docs/k6//using-k6/test-lifecycle#the-vu-stage) are as follows: + +1. _Create_ a new resource, a "croc". +2. _Read_ the list of "crocs". +3. _Update_ the name of the "croc" and _read_ the "croc" to confirm the update operation. +4. _Delete_ the "croc" resource. + +## Core k6 APIs example + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, group, fail } from 'k6'; + +export const options = { + vus: 1, + iterations: 1, +}; + +// Create a random string of given length +function randomString(length, charset = '') { + if (!charset) charset = 'abcdefghijklmnopqrstuvwxyz'; + let res = ''; + while (length--) res += charset[(Math.random() * charset.length) | 0]; + return res; +} + +const USERNAME = `${randomString(10)}@example.com`; // Set your own email or `${randomString(10)}@example.com`; +const PASSWORD = 'superCroc2019'; + +const BASE_URL = 'https://test-api.k6.io'; + +// Register a new user and retrieve authentication token for subsequent API requests +export function setup() { + const res = http.post(`${BASE_URL}/user/register/`, { + first_name: 'Crocodile', + last_name: 'Owner', + username: USERNAME, + password: PASSWORD, + }); + + check(res, { 'created user': (r) => r.status === 201 }); + + const loginRes = http.post(`${BASE_URL}/auth/token/login/`, { + username: USERNAME, + password: PASSWORD, + }); + + const authToken = loginRes.json('access'); + check(authToken, { 'logged in successfully': () => authToken !== '' }); + + return authToken; +} + +export default (authToken) => { + // set the authorization header on the session for the subsequent requests + const requestConfigWithTag = (tag) => ({ + headers: { + Authorization: `Bearer ${authToken}`, + }, + tags: Object.assign( + {}, + { + name: 'PrivateCrocs', + }, + tag + ), + }); + + let URL = `${BASE_URL}/my/crocodiles/`; + + group('01. Create a new crocodile', () => { + const payload = { + name: `Name ${randomString(10)}`, + sex: 'F', + date_of_birth: '2023-05-11', + }; + + const res = http.post(URL, payload, requestConfigWithTag({ name: 'Create' })); + + if (check(res, { 'Croc created correctly': (r) => r.status === 201 })) { + URL = `${URL}${res.json('id')}/`; + } else { + console.log(`Unable to create a Croc ${res.status} ${res.body}`); + return; + } + }); + + group('02. Fetch private crocs', () => { + const res = http.get(`${BASE_URL}/my/crocodiles/`, requestConfigWithTag({ name: 'Fetch' })); + check(res, { 'retrieved crocs status': (r) => r.status === 200 }); + check(res.json(), { 'retrieved crocs list': (r) => r.length > 0 }); + }); + + group('03. Update the croc', () => { + const payload = { name: 'New name' }; + const res = http.patch(URL, payload, requestConfigWithTag({ name: 'Update' })); + const isSuccessfulUpdate = check(res, { + 'Update worked': () => res.status === 200, + 'Updated name is correct': () => res.json('name') === 'New name', + }); + + if (!isSuccessfulUpdate) { + console.log(`Unable to update the croc ${res.status} ${res.body}`); + return; + } + }); + + group('04. Delete the croc', () => { + const delRes = http.del(URL, null, requestConfigWithTag({ name: 'Delete' })); + + const isSuccessfulDelete = check(null, { + 'Croc was deleted correctly': () => delRes.status === 204, + }); + + if (!isSuccessfulDelete) { + console.log(`Croc was not deleted properly`); + return; + } + }); +}; +``` + +{{< /code >}} + +## httpx and k6chaijs example + +{{< code >}} + +```javascript +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; +import { + randomIntBetween, + randomItem, + randomString, +} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export const options = { + // for the example, let's run only 1 VU with 1 iteration + vus: 1, + iterations: 1, +}; + +const USERNAME = `user${randomIntBetween(1, 100000)}@example.com`; // Set your own email; +const PASSWORD = 'superCroc2019'; + +const session = new Httpx({ baseURL: 'https://test-api.k6.io' }); + +// Register a new user and retrieve authentication token for subsequent API requests +export function setup() { + let authToken = null; + + describe(`setup - create a test user ${USERNAME}`, () => { + const resp = session.post(`/user/register/`, { + first_name: 'Crocodile', + last_name: 'Owner', + username: USERNAME, + password: PASSWORD, + }); + + expect(resp.status, 'User create status').to.equal(201); + expect(resp, 'User create valid json response').to.have.validJsonBody(); + }); + + describe(`setup - Authenticate the new user ${USERNAME}`, () => { + const resp = session.post(`/auth/token/login/`, { + username: USERNAME, + password: PASSWORD, + }); + + expect(resp.status, 'Authenticate status').to.equal(200); + expect(resp, 'Authenticate valid json response').to.have.validJsonBody(); + authToken = resp.json('access'); + expect(authToken, 'Authentication token').to.be.a('string'); + }); + + return authToken; +} + +export default function (authToken) { + // set the authorization header on the session for the subsequent requests + session.addHeader('Authorization', `Bearer ${authToken}`); + + describe('01. Create a new crocodile', (t) => { + const payload = { + name: `Croc name ${randomString(10)}`, + sex: randomItem(['M', 'F']), + date_of_birth: '2023-05-11', + }; + + session.addTag('name', 'Create'); + const resp = session.post(`/my/crocodiles/`, payload); + + expect(resp.status, 'Croc creation status').to.equal(201); + expect(resp, 'Croc creation valid json response').to.have.validJsonBody(); + + session.newCrocId = resp.json('id'); + }); + + describe('02. Fetch private crocs', (t) => { + session.clearTag('name'); + const resp = session.get('/my/crocodiles/'); + + expect(resp.status, 'Fetch croc status').to.equal(200); + expect(resp, 'Fetch croc valid json response').to.have.validJsonBody(); + expect(resp.json().length, 'Number of crocs').to.be.above(0); + }); + + describe('03. Update the croc', (t) => { + const payload = { + name: `New croc name ${randomString(10)}`, + }; + + const resp = session.patch(`/my/crocodiles/${session.newCrocId}/`, payload); + + expect(resp.status, 'Croc patch status').to.equal(200); + expect(resp, 'Fetch croc valid json response').to.have.validJsonBody(); + expect(resp.json('name'), 'Croc name').to.equal(payload.name); + + // read "croc" again to verify the update worked + const resp1 = session.get(`/my/crocodiles/${session.newCrocId}/`); + + expect(resp1.status, 'Croc fetch status').to.equal(200); + expect(resp1, 'Fetch croc valid json response').to.have.validJsonBody(); + expect(resp1.json('name'), 'Croc name').to.equal(payload.name); + }); + + describe('04. Delete the croc', (t) => { + const resp = session.delete(`/my/crocodiles/${session.newCrocId}/`); + + expect(resp.status, 'Croc delete status').to.equal(204); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/bundling-and-transpilation.md b/docs/sources/v0.50.x/examples/bundling-and-transpilation.md new file mode 100644 index 000000000..dac710512 --- /dev/null +++ b/docs/sources/v0.50.x/examples/bundling-and-transpilation.md @@ -0,0 +1,10 @@ +--- +title: 'Bundling and transpilation' +redirect: 'https://github.com/k6io/k6-es6/' +description: | + Reference project demonstrating how to use webpack and babel to bundle + node modules or transpile code to ES5.1+ for usage in k6 tests. +weight: 18 +--- + +# Bundling and transpilation diff --git a/docs/sources/v0.50.x/examples/cookies-example.md b/docs/sources/v0.50.x/examples/cookies-example.md new file mode 100644 index 000000000..81d65d9c1 --- /dev/null +++ b/docs/sources/v0.50.x/examples/cookies-example.md @@ -0,0 +1,198 @@ +--- +title: 'Cookies Example' +description: 'Scripting examples on how you can interact with cookies during your load test if required.' +weight: 08 +--- + +# Cookies Example + +Scripting examples on how you can interact with cookies during your load test if required. + +## Cookies + +As HTTP is a stateless protocol, cookies are used by server-side applications to persist data +on client machines. This is used more or less everywhere on the web, commonly for user session +tracking purposes. In k6 cookies are managed automatically by default, however, there are use +cases where access to read and manipulate cookies are required. + +## From the response headers + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, group } from 'k6'; + +export default function () { + // Since this request redirects the `res.cookies` property won't contain the cookies + const res = http.get('https://httpbin.test.k6.io/cookies/set?name1=value1&name2=value2'); + check(res, { + 'status is 200': (r) => r.status === 200, + }); + + // Make sure cookies have been added to VU cookie jar + const vuJar = http.cookieJar(); + const cookiesForURL = vuJar.cookiesForURL(res.url); + check(null, { + "vu jar has cookie 'name1'": () => cookiesForURL.name1.length > 0, + "vu jar has cookie 'name2'": () => cookiesForURL.name2.length > 0, + }); +} +``` + +{{< /code >}} + +## Log all the cookies in a response + +> ### ⚠️ Note that this only works when using k6 locally +> +> The `console.log()` family of APIs are currently only usable when running k6 locally. +> When running k6 tests with LoadImpact Cloud Execution the logs will be discarded. + +{{< code >}} + +```javascript +// Example showing two methods how to log all cookies (with attributes) from a HTTP response. + +import http from 'k6/http'; + +function logCookie(cookie) { + // Here we log the name and value of the cookie along with additional attributes. + // For full list of attributes see: https:///grafana.com/docs/k6/using-k6/cookies#properties-of-a-response-cookie-object + console.log( + `${cookie.name}: ${cookie.value}\n\tdomain: ${cookie.domain}\n\tpath: ${cookie.path}\n\texpires: ${cookie.expires}\n\thttpOnly: ${cookie.http_only}` + ); +} + +export default function () { + const res = http.get('https://www.google.com/'); + + // Method 1: Use for-loop and check for non-inherited properties + for (const name in res.cookies) { + if (res.cookies.hasOwnProperty(name) !== undefined) { + logCookie(res.cookies[name][0]); + } + } + + // Method 2: Use ES6 Map to loop over Object entries + new Map(Object.entries(res.cookies)).forEach((v, k) => logCookie(v[0])); +} +``` + +{{< /code >}} + +## Setting a cookie in the VU cookie jar + +To set a cookie that should be sent with every request matching a particular domain, path, etc. +you'd do something like this: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + // Get VU cookie jar and add a cookie to it providing the parameters + // that a request must match (domain, path, HTTPS or not etc.) + // to have the cookie attached to it when sent to the server. + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie', 'hello world', { + domain: 'httpbin.test.k6.io', + path: '/cookies', + secure: true, + max_age: 600, + }); + + // As the following request is matching the above cookie in terms of domain, + // path, HTTPS (secure) and will happen within the specified "age" limit, the + // cookie will be attached to this request. + const res = http.get('https://httpbin.test.k6.io/cookies'); + check(res, { + 'has status 200': (r) => r.status === 200, + "has cookie 'my_cookie'": (r) => r.json().cookies.my_cookie !== null, + 'cookie has correct value': (r) => r.json().cookies.my_cookie == 'hello world', + }); +} +``` + +{{< /code >}} + +## Delete a cookie in the VU cookie jar + +To delete a cookie in the jar for a specific URL and name, use the `delete` method. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie_1', 'hello world_1'); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie_2', 'hello world_2'); + + const res1 = http.get('https://httpbin.test.k6.io/cookies'); + check(res1, { + 'res1 has status 200': (r) => r.status === 200, + "res1 has cookie 'my_cookie_1'": (r) => r.json().cookies.my_cookie_1 !== null, + 'res1 cookie has correct value_1': (r) => r.json().cookies.my_cookie_1 == 'hello world_1', + "res1 has cookie 'my_cookie_2'": (r) => r.json().cookies.my_cookie_2 !== null, + 'res1 cookie has correct value_2': (r) => r.json().cookies.my_cookie_2 == 'hello world_2', + }); + + jar.delete('https://httpbin.test.k6.io/cookies', 'my_cookie_1'); + + const res2 = http.get('https://httpbin.test.k6.io/cookies'); + check(res2, { + 'res2 has status 200': (r) => r.status === 200, + "res2 hasn't cookie 'my_cookie_1'": (r) => r.json().cookies.my_cookie_1 == null, + "res2 has cookie 'my_cookie_2'": (r) => r.json().cookies.my_cookie_2 !== null, + 'res2 cookie has correct value_2': (r) => r.json().cookies.my_cookie_2 == 'hello world_2', + }); +} +``` + +{{< /code >}} + +## Clear all cookies in the VU cookie jar + +To clear all cookies in the jar by specifying url, use the `clear` method. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie', 'hello world'); + const res1 = http.get('https://httpbin.test.k6.io/cookies'); + check(res1, { + 'has status 200': (r) => r.status === 200, + "has cookie 'my_cookie'": (r) => r.json().cookies.my_cookie !== null, + 'cookie has correct value': (r) => r.json().cookies.my_cookie == 'hello world', + }); + + jar.clear('https://httpbin.test.k6.io/cookies'); + + const res2 = http.get('https://httpbin.test.k6.io/cookies'); + check(res2, { + 'has status 200': (r) => r.status === 200, + "hasn't cookie 'my_cookie'": (r) => r.json().cookies.my_cookie == null, + }); +} +``` + +{{< /code >}} + +**Relevant k6 APIs**: + +- [http.cookieJar()](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar-method) +- [http.CookieJar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) + +- [set(url, name, value, [options])](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar/cookiejar-set) +- [delete(url, name)](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar/cookiejar-delete) +- [clear(url)](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar/cookiejar-clear) diff --git a/docs/sources/v0.50.x/examples/correlation-and-dynamic-data.md b/docs/sources/v0.50.x/examples/correlation-and-dynamic-data.md new file mode 100644 index 000000000..d5cbe1dd9 --- /dev/null +++ b/docs/sources/v0.50.x/examples/correlation-and-dynamic-data.md @@ -0,0 +1,149 @@ +--- +title: 'Correlation and Dynamic Data' +slug: '/correlation-and-dynamic-data' +description: | + Scripting examples on how to correlate dynamic data in your test script. Correlation is + often required when using the Chrome Extension or HAR converter to generate your test script. + This is due to the fact that those tools will capture session IDs, CSRF tokens, VIEWSTATE, + wpnonce, and other dynamic values from your specific session. +weight: 04 +--- + +# Correlation and Dynamic Data + +Scripting examples on how to correlate dynamic data in your test script. Correlation is often +required when using the Chrome Extension or HAR converter to generate your test script. This +is because those tools will capture session IDs, CSRF tokens, VIEWSTATE, wpnonce, and other +dynamic values from your specific session. These tokens typically expire very quickly. This +is one of the most common things that users will script for when testing user journeys across +websites or web apps. + +### Correlation + +In a load testing scenario, correlation means extracting one or more values from the response +of one request and then reusing them in subsequent requests. Often, this could be getting +a token or some sort of ID necessary to fulfill a sequence of steps in a user journey. + +A [recording](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings) will capture session data such as CSRF tokens, +VIEWSTATES, nonce, etc. This type of data is unlikely to be valid when +you run your test, meaning you'll need to handle the extraction of this data from the HTML/form +to include it in subsequent requests. This issue is fairly common with any site that has forms +and can be handled with a little bit of scripting. + +### Extracting values/tokens from a JSON response + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + // Make a request that returns some JSON data + const res = http.get('https://httpbin.test.k6.io/json'); + + // Extract data from that JSON data by first parsing it + // using a call to "json()" and then accessing properties by + // navigating the JSON data as a JS object with dot notation. + const slide1 = res.json().slideshow.slides[0]; + check(slide1, { + 'slide 1 has correct title': (s) => s.title === 'Wake up to WonderWidgets!', + 'slide 1 has correct type': (s) => s.type === 'all', + }); + + // Now we could use the "slide1" variable in subsequent requests... +} +``` + +{{< /code >}} + +**Relevant k6 APIs**: + +- [Response.json()](https://grafana.com/docs/k6//javascript-api/k6-http/response) + +- [JSON.parse()](https://developer.mozilla.org/en-US/Web/JavaScript/Reference/Global_Objects/JSON/parse) + (An alternative API that can be used for parsing JSON data) + +### Extracting values/tokens from form fields + +You can choose from two different approaches when deciding how to handle form submissions. +Either you use the higher-level [Response.submitForm([params])](https://grafana.com/docs/k6//javascript-api/k6-http/response/response-submitform) API +or you extract necessary hidden fields etc. and build a request yourself and then send it using the +appropriate `http.*` family of APIs, like [http.post(url, [body], [params])](https://grafana.com/docs/k6//javascript-api/k6-http/post). + +#### Extracting .NET ViewStates, CSRF tokens and other hidden input fields + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + // Request the page containing a form and save the response. This gives you access + //to the response object, `res`. + const res = http.get('https://test.k6.io/my_messages.php', { responseType: 'text' }); + + // Query the HTML for an input field named "redir". We want the value or "redir" + const elem = res.html().find('input[name=redir]'); + + // Get the value of the attribute "value" and save it to a variable + const val = elem.attr('value'); + + // Now you can concatenate this extracted value in subsequent requests that require it. + // ... + // console.log() works when executing k6 scripts locally and is handy for debugging purposes + console.log('The value of the hidden field redir is: ' + val); + + sleep(1); +} +``` + +{{< /code >}} + +> ### ⚠️ Did you know? +> +> Take note if `discardResponseBodies` is set to true in the options +> section of your script. If it is, you can either make it `false` or save the response per +> request with `{"responseType": "text"}` as shown in the example. + +**Relevant k6 APIs**: + +- [Selection.find(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-find) (the [jQuery Selector API](http://api.jquery.com/category/selectors/) + docs are also a good resource on what possible selector queries can be made) +- [Selection.attr(name)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-attr) + +### Generic extraction of values/tokens + +Sometimes, responses may be neither JSON nor HTML, in which case the above extraction methods would not apply. In these situations, you would likely want to operate directly on the `Response.body` string using a simple function capable of extracting a string at some known location. This is typically achieved by looking for the string "boundaries" immediately before (left) and after (right) the value needing extraction. + +The [jslib utils](https://grafana.com/docs/k6//javascript-api/jslib/utils) library contains an example of this kind of function,[findBetween](https://grafana.com/docs/k6//javascript-api/jslib/utils/findbetween). The function uses the JavaScript built-in [String.indexOf](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf) and therefore doesn't depend on potentially expensive regular-expression operations. + +#### Extracting a value/token using findBetween + +{{< code >}} + +```javascript +import { findBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + // This request returns XML: + const res = http.get('https://httpbin.test.k6.io/xml'); + + // Use findBetween to extract the first title encountered: + const title = findBetween(res.body, '', ''); + + check(title, { + 'title is correct': (t) => t === 'Wake up to WonderWidgets!', + }); +} +``` + +{{< /code >}} + +**Relevant k6 APIs**: + +- [Response.body](https://grafana.com/docs/k6//javascript-api/k6-http/response) +- [findBetween(content, left, right)](https://grafana.com/docs/k6//javascript-api/jslib/utils/findbetween) diff --git a/docs/sources/v0.50.x/examples/crawl-webpage.md b/docs/sources/v0.50.x/examples/crawl-webpage.md new file mode 100644 index 000000000..e1ac98849 --- /dev/null +++ b/docs/sources/v0.50.x/examples/crawl-webpage.md @@ -0,0 +1,9 @@ +--- +title: 'Crawl a web page' +redirect: 'https://stackoverflow.com/questions/60927653/downloading-whole-websites-with-k6/' +description: | + Stack overflow answer demonstrating how to crawl a web page +weight: 17 +--- + +# Crawl a web page diff --git a/docs/sources/v0.50.x/examples/data-generation.md b/docs/sources/v0.50.x/examples/data-generation.md new file mode 100644 index 000000000..411f6c140 --- /dev/null +++ b/docs/sources/v0.50.x/examples/data-generation.md @@ -0,0 +1,9 @@ +--- +title: 'Generating realistic data' +redirect: 'https://github.com/k6io/example-data-generation/' +description: | + Reference project demonstrating how to generate data with realistic traits at runtime using faker.js +weight: 16 +--- + +# Generating realistic data diff --git a/docs/sources/v0.50.x/examples/data-parameterization.md b/docs/sources/v0.50.x/examples/data-parameterization.md new file mode 100644 index 000000000..f084b2ee6 --- /dev/null +++ b/docs/sources/v0.50.x/examples/data-parameterization.md @@ -0,0 +1,256 @@ +--- +title: 'Data Parameterization' +description: | + Scripting examples on how to parameterize data in a test script. Parameterization is typically + necessary when Virtual Users(VUs) will make a POST, PUT, or PATCH request in a test. + + Parameterization helps to prevent server-side caching from impacting your load test. + This will, in turn, make your test more realistic. +weight: 05 +--- + +# Data Parameterization + +_Data parameterization_ is the process of turning test values into reusable parameters, for example, through variables and shared arrays. + +This page gives some examples of how to parameterize data in a test script. +Parameterization is typically necessary when Virtual Users (VUs) will make a POST, PUT, or PATCH request in a test. +You can also use parameterization when you need to add test data from a separate file. + +Parameterization helps to prevent server-side caching from impacting your load test. +This will, in turn, make your test more realistic. + +## Performance implications of `SharedArray` + +Each VU in k6 is a separate JS VM. To prevent multiple copies of the whole data file, +[SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) was added. It does have some CPU overhead in accessing elements compared to a normal non shared +array, but the difference is negligible compared to the time it takes to make requests. This becomes +even less of an issue compared to not using it with large files, as k6 would otherwise use too much memory to run, which might lead to your script not being able to run at all or aborting in the middle if the system resources are exhausted. + +For example, the Cloud service allocates 8GB of memory for every 300 VUs. So if your files are large +enough and you are not using [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray), that might mean that your script will run out of memory at +some point. Additionally even if there is enough memory, k6 has a garbage collector (as it's written +in golang) and it will walk through all accessible objects (including JS ones) and figure out which +need to be garbage collected. For big JS arrays copied hundreds of times this adds quite a lot of +additional work. + +A note on performance characteristics of `SharedArray` can be found within its [API documentation](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray#performance-characteristics). + +## From a JSON file + +{{< code >}} + +```json +{ + "users": [ + { "username": "test", "password": "qwerty" }, + { "username": "test", "password": "qwerty" } + ] +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { SharedArray } from 'k6/data'; +// not using SharedArray here will mean that the code in the function call (that is what loads and +// parses the json) will be executed per each VU which also means that there will be a complete copy +// per each VU +const data = new SharedArray('some data name', function () { + return JSON.parse(open('./data.json')).users; +}); + +export default function () { + const user = data[0]; + console.log(data[0].username); +} +``` + +{{< /code >}} + +## From a CSV file + +k6 doesn't parse CSV files natively, but you can use an external library, [Papa Parse](https://www.papaparse.com/). + +You can download the library and import it locally like this: + +{{< code >}} + +```javascript +import papaparse from './papaparse.js'; +import { SharedArray } from 'k6/data'; +// not using SharedArray here will mean that the code in the function call (that is what loads and +// parses the csv) will be executed per each VU which also means that there will be a complete copy +// per each VU +const csvData = new SharedArray('another data name', function () { + // Load CSV file and parse it using Papa Parse + return papaparse.parse(open('./data.csv'), { header: true }).data; +}); + +export default function () { + // ... +} +``` + +{{< /code >}} + +Or you can grab it directly from [jslib.k6.io](https://jslib.k6.io/) like this. + +{{< code >}} + +```javascript +import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js'; +import { SharedArray } from 'k6/data'; + +// not using SharedArray here will mean that the code in the function call (that is what loads and +// parses the csv) will be executed per each VU which also means that there will be a complete copy +// per each VU +const csvData = new SharedArray('another data name', function () { + // Load CSV file and parse it using Papa Parse + return papaparse.parse(open('./data.csv'), { header: true }).data; +}); + +export default function () { + // ... +} +``` + +{{< /code >}} + +Here's an example using Papa Parse to parse a CSV file of username/password pairs and using that +data to login to the test.k6.io test site: + +{{< code >}} + +```javascript +/* Where contents of data.csv is: +username,password +admin,123 +test_user,1234 +*/ +import http from 'k6/http'; +import { check, sleep } from 'k6'; +import { SharedArray } from 'k6/data'; +import papaparse from 'https://jslib.k6.io/papaparse/5.1.1/index.js'; + +// not using SharedArray here will mean that the code in the function call (that is what loads and +// parses the csv) will be executed per each VU which also means that there will be a complete copy +// per each VU +const csvData = new SharedArray('another data name', function () { + // Load CSV file and parse it using Papa Parse + return papaparse.parse(open('./data.csv'), { header: true }).data; +}); + +export default function () { + // Now you can use the CSV data in your test logic below. + // Below are some examples of how you can access the CSV data. + + // Loop through all username/password pairs + for (const userPwdPair of csvData) { + console.log(JSON.stringify(userPwdPair)); + } + + // Pick a random username/password pair + const randomUser = csvData[Math.floor(Math.random() * csvData.length)]; + console.log('Random user: ', JSON.stringify(randomUser)); + + const params = { + login: randomUser.username, + password: randomUser.password, + }; + console.log('Random user: ', JSON.stringify(params)); + + const res = http.post('https://test.k6.io/login.php', params); + check(res, { + 'login succeeded': (r) => r.status === 200 && r.body.indexOf('successfully authorized') !== -1, + }); + + sleep(1); +} +``` + +{{< /code >}} + +## Retrieving unique data + +It is often a requirement not to use the same data more than once in a test. With the help of [k6/execution](https://grafana.com/docs/k6//javascript-api/k6-execution), which includes a property `scenario.iterationInTest`, you can retrieve unique rows from your data set. + +> ### ⚠️ Multiple scenarios +> +> `scenario.iterationInTest` property is unique **per scenario**, not the overall test. +> That means if you have multiple scenarios in your test you might need to split your data per scenario. + +{{< code >}} + +```javascript +import { SharedArray } from 'k6/data'; +import { scenario } from 'k6/execution'; + +const data = new SharedArray('users', function () { + return JSON.parse(open('./data.json')).users; +}); + +export const options = { + scenarios: { + 'use-all-the-data': { + executor: 'shared-iterations', + vus: 10, + iterations: data.length, + maxDuration: '1h', + }, + }, +}; + +export default function () { + // this is unique even in the cloud + const user = data[scenario.iterationInTest]; + console.log(`user: ${JSON.stringify(user)}`); +} +``` + +{{< /code >}} + +Alternatively, if your use case requires using a unique data set per VU, you could leverage a property called `vu.idInTest`. + +In the following example we're going to be using `per-vu-iterations` executor to ensure that every VU completes +a fixed amount of iterations. + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import { SharedArray } from 'k6/data'; +import { vu } from 'k6/execution'; + +const users = new SharedArray('users', function () { + return JSON.parse(open('./data.json')).users; +}); + +export const options = { + scenarios: { + login: { + executor: 'per-vu-iterations', + vus: users.length, + iterations: 20, + maxDuration: '1h30m', + }, + }, +}; + +export default function () { + // VU identifiers are one-based and arrays are zero-based, thus we need - 1 + console.log(`Users name: ${users[vu.idInTest - 1].username}`); + sleep(1); +} +``` + +{{< /code >}} + +## Generating data using faker.js + +The following articles show how to use faker.js in k6 to generate realistic data during the test execution: + +- [Performance Testing with Generated Data using k6 and Faker](https://dev.to/k6/performance-testing-with-generated-data-using-k6-and-faker-2e) +- [Load Testing Made Easy with K6: Using Faker Library and CSV Files](https://farhan-labib.medium.com/load-testing-made-easy-with-k6-using-faker-library-and-csv-files-c997d48fb6e2) diff --git a/docs/sources/v0.50.x/examples/data-uploads.md b/docs/sources/v0.50.x/examples/data-uploads.md new file mode 100644 index 000000000..211cb8cc2 --- /dev/null +++ b/docs/sources/v0.50.x/examples/data-uploads.md @@ -0,0 +1,150 @@ +--- +title: 'Data Uploads' +description: 'Scripting examples on how to execute a load test that will upload a file to the System Under Test (SUT).' +weight: 09 +--- + +# Data Uploads + +Example to execute a load test that will upload a file to the System Under Test (SUT). + +## The open() function + +Using the built-in function [`open()`](https://grafana.com/docs/k6//javascript-api/init-context/open), +we can read the contents of a file given a filename or URL. + +Below is a simple example showing how to load the contents of a local file `data.json`. + +{{< code >}} + +```json +{ + "my_key": "has a value" +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +const data = JSON.parse(open('./data.json')); + +export default function () { + console.log(data.my_key); +} +``` + +{{< /code >}} + +If you want to open a binary file you need to pass in `"b"` as the second argument. + +{{< code >}} + +```javascript +const binFile = open('./image.png', 'b'); + +export default function () { + //... +} +``` + +{{< /code >}} + +## Multipart request (uploading a file) + +Now that you know how to load a local file, let's look at a script that creates a POST request +to upload this data to an API endpoint along with a regular text field (`field` in the example +below): + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +const binFile = open('/path/to/file.bin', 'b'); + +export default function () { + const data = { + field: 'this is a standard form field', + file: http.file(binFile, 'test.bin'), + }; + + const res = http.post('https://example.com/upload', data); + sleep(3); +} +``` + +{{< /code >}} + +In the example above we use the [http.file()](https://grafana.com/docs/k6//javascript-api/k6-http/file) +API to wrap the file contents in a [FileData](https://grafana.com/docs/k6//javascript-api/k6-http/filedata) object. +When passing a JS object as the body parameter to [http.post()](https://grafana.com/docs/k6//javascript-api/k6-http/post), +or any of the other HTTP request functions, where one of the property values is a +[FileData](https://grafana.com/docs/k6//javascript-api/k6-http/filedata) a multipart request will be constructed +and sent. + +### Relevant k6 APIs + +- [open(filePath, [mode])](https://grafana.com/docs/k6//javascript-api/init-context/open) +- [http.file(data, [filename], [contentType])](https://grafana.com/docs/k6//javascript-api/k6-http/file) + +## Advanced multipart request + +The previous multipart request example has some limitations: + +- It's not possible to assemble the parts in a specific order, because of the + unordered nature of JS objects when they're converted to Golang maps, which k6 uses internally. + Uploading files in a specific order is a requirement for some APIs (e.g. AWS S3). +- It's not possible to upload multiple files as part of the same form field, because + JS object keys must be unique. + +To address this we suggest using the [`FormData` polyfill for k6](https://jslib.k6.io/formdata/0.0.2/index.js). + +Here's an example of uploading several binary files and a text file using the polyfill: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; +import { FormData } from 'https://jslib.k6.io/formdata/0.0.2/index.js'; + +const img1 = open('/path/to/image1.png', 'b'); +const img2 = open('/path/to/image2.jpg', 'b'); +const txt = open('/path/to/text.txt'); + +export default function () { + const fd = new FormData(); + fd.append('someTextField', 'someValue'); + fd.append('aBinaryFile', { + data: new Uint8Array(img1).buffer, + filename: 'logo.png', + content_type: 'image/png', + }); + fd.append('anotherTextField', 'anotherValue'); + fd.append('images', http.file(img1, 'image1.png', 'image/png')); + fd.append('images', http.file(img2, 'image2.jpg', 'image/jpeg')); + fd.append('text', http.file(txt, 'text.txt', 'text/plain')); + + const res = http.post('https://httpbin.test.k6.io/post', fd.body(), { + headers: { 'Content-Type': 'multipart/form-data; boundary=' + fd.boundary }, + }); + check(res, { + 'is status 200': (r) => r.status === 200, + }); +} +``` + +{{< /code >}} + +Note that: + +- Both binary files will be uploaded under the `images` form field, and the text file + will appear last in the request under the `text` form field. +- It's required to specify the multipart boundary in the `Content-Type` header, + so you must assemble the header manually as shown. +- Blob is not supported or implemented. For the same functionality, use + a simple object with the fields `data`, `content_type` (defaulting to "application/octet-stream") and optionally + `filename` as shown for `aBinaryFile` above. diff --git a/docs/sources/v0.50.x/examples/distribute-workloads.md b/docs/sources/v0.50.x/examples/distribute-workloads.md new file mode 100644 index 000000000..173a9a64e --- /dev/null +++ b/docs/sources/v0.50.x/examples/distribute-workloads.md @@ -0,0 +1,160 @@ +--- +title: Distribute workloads across VUs +description: How to configure different amounts of traffic for different VU behaviors +slug: /distribute-workloads +weight: 24 +--- + +# Distribute workloads across VUs + +k6 can schedule different load patterns for different VU functions. +A test with multiple workloads might better simulate traffic in the real world, where user behavior is rarely uniform. +For example, most traffic to an e-commerce site might come from users who only search for items and read reviews. A small percentage of users might actively shop, performing actions that involve writes to the database and calls to different APIs. + +The following sections provide examples of how to structure k6 scripts to split logic across VUs. +To inspect the results for a certain behavior, you can [create a custom metric](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics) or use [Tags](https://grafana.com/docs/k6//using-k6/tags-and-groups) to filter by scenario, code block, or individual request. + +{{% admonition type="note" %}} + +These techniques can create very complex configurations. +However, more complexity creates more ambiguity in result interpretation + +{{% /admonition %}} + +## Split logic across scenarios + +{{% admonition type="note" %}} + +In this context, _workload_ refers to the traffic pattern simulated by a scenario. + +{{% /admonition %}} + +One way to distribute traffic is to use scenarios to schedule different workloads for different functions. + +1. Define multiple scenarios in your [options](https://grafana.com/docs/k6//using-k6/k6-options). +1. Use the scenario `exec` property to execute different VU functions with a workload. + +For example, imagine a social media site that typically receives 100 concurrent users. +Of those, 80 might visit their contacts page, and 20 might view the news. +To configure such a distribution, make two scenarios with different throughput or VUs: + +```javascript +import http from 'k6/http'; + +export const options = { + //scenario to view contacts + scenarios: { + contacts: { + executor: 'shared-iterations', + exec: 'contacts', + vus: 80, + iterations: 100, + }, + //scenario to view news + news: { + executor: 'shared-iterations', + exec: 'news', + vus: 20, + iterations: 100, + }, + }, +}; + +//use the exec property to run different scenarios for different functions + +export function contacts() { + http.get('https://test.k6.io/contacts.php'); +} + +export function news() { + http.get('https://test.k6.io/news.php'); +} +``` + +To view granular results for a specific scenario, you can filter by the built-in scenario [tag](https://grafana.com/docs/k6//using-k6/tags-and-groups). + +## Distribute logic by VU ID + +In some cases, writing a scenario for each behavior might be inconvenient or impractical. +As an alternative, you can distribute logic across a range of VUs with the [execution context variables](https://grafana.com/docs/k6//using-k6/execution-context-variables) from the [`k6/execution`](https://grafana.com/docs/k6//javascript-api/k6-execution) module. +With the `exec` object, you can scope logic to a specific instance, scenario, or across all VUs. + +For example, this statement assigns behavior to the first 25 VUs in a test. + +```bash +if (exec.vu.idInTest <= 25) { + //do something; + } +``` + +For more flexibility, you can use modulo expressions to distribute VUs according to percentages. +For example, the following script distributes logic according to different user profiles: + +- 40 percent of users check the news. +- 60 percent play a coinflip game. + - Half bet `heads`, and half bet `tails`. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import exec from 'k6/execution'; + +export const options = { + scenarios: { + quickRamp: { + executor: 'ramping-arrival-rate', + startRate: 0, + timeUnit: '1s', + preAllocatedVUs: 100, + stages: [ + { target: 10, duration: '10s' }, + { target: 10, duration: '15s' }, + { target: 0, duration: '5s' }, + ], + }, + }, +}; + +export default function () { + if (exec.vu.idInTest % 10 < 4) { + // 0-3 range, read the news + http.get('http://test.k6.io/news'); + } else if (exec.vu.idInTest % 10 < 7) { + // 4-6 range, bet heads + http.get('http://test.k6.io/flip_coin.php?bet=heads'); + } else { + // 7-9 range, bet tails + http.get('http://test.k6.io/flip_coin.php?bet=tails'); + } +} +``` + +To view results for a specific request or group, you can define [tags](https://grafana.com/docs/k6//using-k6/tags-and-groups). + +{{< /code >}} + +## Randomize behavior + +To add a degree of random behavior, consider one of the randomizing functions from the [k6 utils](https://grafana.com/docs/k6//javascript-api/jslib/utils). + +For example, this script randomly assigns one behavior to happen one-third of the time, and another to happen all other times. + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export default function () { + if (randomIntBetween(1, 3) % 3 > 1) { + console.log('1 in 3 times'); + } else { + console.log('2 in 3 times'); + } +} +``` + +{{< /code >}} + +For a more sophisticated example of randomizing, read this [forum post](https://community.grafana.com/t/how-to-distribute-vus-across-different-scenarios-with-k6/97698/17). diff --git a/docs/sources/v0.50.x/examples/error-handler.md b/docs/sources/v0.50.x/examples/error-handler.md new file mode 100644 index 000000000..9b1a6cfec --- /dev/null +++ b/docs/sources/v0.50.x/examples/error-handler.md @@ -0,0 +1,74 @@ +--- +title: 'Error handler' +description: Using a custom error handler to store errors in custom metrics or logs. +weight: 25 +--- + +# Error handler + +When encountering errors from an application's backend, gathering additional error information, such as error messages, tracing data, or request and response bodies, is often necessary. The initial suggestion is to leverage your observability solution to find these errors. However, capturing error details directly in k6 can also be useful for troubleshooting. + +In k6, there are two common approaches to store additional error information: + +- Console logs and output [k6 logs to Loki](https://k6.io/blog/using-loki-to-store-and-query-k6-logs/), a file, or use [Grafana Cloud k6](https://grafana.com/docs/grafana-cloud/k6/analyze-results/inspect-test-results/inspect-logs/). +- Using [Tags](https://grafana.com/docs/k6//using-k6/tags-and-groups/) in a [custom counter metric](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics) to track error data. + +When deciding what error data to store, consider whether it's a general or specific error, and be aware that high-load tests may generate a substantial volume of error data. + +Below is an example using an `ErrorHandler` class to log error information. It accepts a callback that instructs how to log errors. The example provides instructions for both previously mentioned options: console logs and custom metrics. + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +// General error handler to log error details. +class ErrorHandler { + // Instruct the error handler how to log errors + constructor(logErrorDetails) { + this.logErrorDetails = logErrorDetails; + } + + // Logs response error details if isError is true. + logError(isError, res, tags = {}) { + if (!isError) return; + + // the Traceparent header is a W3C Trace Context + const traceparentHeader = res.request.headers['Traceparent']; + + // Add any other useful information + const errorData = Object.assign( + { + url: res.url, + status: res.status, + error_code: res.error_code, + traceparent: traceparentHeader && traceparentHeader.toString(), + }, + tags + ); + this.logErrorDetails(errorData); + } +} + +// Set up the error handler to log errors to the console +const errorHandler = new ErrorHandler((error) => { + console.error(error); +}); + +/* Alternatively, you may log errors using a custom metric or any other option. +const errors = new CounterMetric('errors'); +const errorHandler = new ErrorHandler((error) => {errors.add(1, error);}); +*/ + +// Basic example to demonstrate the error handler +export default function () { + let res, checkStatus; + + res = http.get('https://httpbin.org/status/200'); + checkStatus = check(res, { 'status is 200': (res) => res.status === 200 }); + errorHandler.logError(!checkStatus, res); + + res = http.get('https://httpbin.org/status/300'); + checkStatus = check(res, { 'status is 200': (res) => res.status === 200 }); + errorHandler.logError(!checkStatus, res); +} +``` diff --git a/docs/sources/v0.50.x/examples/functional-testing.md b/docs/sources/v0.50.x/examples/functional-testing.md new file mode 100644 index 000000000..eb661827c --- /dev/null +++ b/docs/sources/v0.50.x/examples/functional-testing.md @@ -0,0 +1,1650 @@ +--- +title: 'Functional testing' +description: | + Use Chaijs library for functional and integration testing. +weight: 19 +--- + +# Functional testing + +### Most basic integration test + +{{< code >}} + + + +```javascript +import http from 'k6/http'; +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +export const options = { + thresholds: { + checks: ['rate == 1.00'], + }, +}; + +export default function () { + describe('Hello world!', () => { + const response = http.get('https://test-api.k6.io/public/crocodiles/'); + + expect(response.status, 'response status').to.equal(200); + expect(response).to.have.validJsonBody(); + expect(response.json(), 'croc list').to.be.an('array'); + }); +} +``` + +{{< /code >}} + +### Sample integration test + +This test goes through several steps. It creates a new user account, authenticates, and interacts with protected resources. + +{{< code >}} + + + +```javascript +import chai, { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; +import { Httpx, Get } from 'https://jslib.k6.io/httpx/0.0.6/index.js'; +import { randomString } from 'https://jslib.k6.io/k6-utils/1.0.0/index.js'; + +chai.config.logFailures = true; + +export let options = { + thresholds: { + // fail the test if any checks fail or any requests fail + checks: ['rate == 1.00'], + http_req_failed: ['rate == 0.00'], + }, + vus: 1, + iterations: 1, +}; + +let session = new Httpx({ baseURL: 'https://test-api.k6.io' }); + +function retrieveIndividualCrocodilesInABatch() { + describe('[Crocs service] Fetch public crocs one by one', () => { + let responses = session.batch([ + new Get('/public/crocodiles/1/'), + new Get('/public/crocodiles/2/'), + new Get('/public/crocodiles/3/'), + ]); + + expect(responses, 'responses').to.be.an('array'); + + responses.forEach((response) => { + expect(response.status, 'response status').to.equal(200); + expect(response).to.have.validJsonBody(); + expect(response.json(), 'crocodile').to.be.an('object'); + expect(response.json(), 'crocodile').to.include.keys('age', 'name', 'id', 'sex'); + expect(response.json(), 'crocodile').to.not.have.a.property('outfit'); + }); + }); +} + +function retrieveAllPublicCrocodiles() { + describe('[Crocs service] Fetch a list of crocs', () => { + let response = session.get('/public/crocodiles'); + + expect(response.status, 'response status').to.equal(200); + expect(response).to.have.validJsonBody(); + expect(response.json(), 'croc list').to.be.an('array').lengthOf.above(5); + }); +} + +function validateAuthService() { + const USERNAME = `${randomString(10)}@example.com`; + const PASSWORD = 'superCroc2021'; + + describe('[Registration service] user registration', () => { + let sampleUser = { + username: USERNAME, + password: PASSWORD, + email: USERNAME, + first_name: 'John', + last_name: 'Smith', + }; + + let response = session.post(`/user/register/`, sampleUser); + + expect(response.status, 'registration status').to.equal(201); + expect(response).to.have.validJsonBody(); + }); + + describe('[Auth service] user authentication', () => { + let authData = { + username: USERNAME, + password: PASSWORD, + }; + + let resp = session.post(`/auth/token/login/`, authData); + + expect(resp.status, 'Auth status').to.be.within(200, 204); + expect(resp).to.have.validJsonBody(); + expect(resp.json()).to.have.a.property('access'); + expect(resp.json('access'), 'auth token').to.be.a('string'); + + let authToken = resp.json('access'); + // set the authorization header on the session for the subsequent requests. + session.addHeader('Authorization', `Bearer ${authToken}`); + }); +} + +function validateCrocodileCreation() { + // authentication happened before this call. + + describe('[Croc service] Create a new crocodile', () => { + let payload = { + name: `Croc Name`, + sex: 'M', + date_of_birth: '2019-01-01', + }; + + let resp = session.post(`/my/crocodiles/`, payload); + + expect(resp.status, 'Croc creation status').to.equal(201); + expect(resp).to.have.validJsonBody(); + + session.newCrocId = resp.json('id'); // caching croc ID for the future. + }); + + describe('[Croc service] Fetch private crocs', () => { + let response = session.get('/my/crocodiles/'); + + expect(response.status, 'response status').to.equal(200); + expect(response, 'private crocs').to.have.validJsonBody(); + expect(response.json(), 'private crocs').to.not.be.empty; + }); +} + +export default function testSuite() { + retrieveIndividualCrocodilesInABatch(); + retrieveAllPublicCrocodiles(); + validateAuthService(); + validateCrocodileCreation(); +} +``` + +{{< /code >}} + +### Full example showcasing all functionality + +Here's an auto-generated k6 test script showcasing all examples from the [Chaijs API documentation](https://www.chaijs.com/api/bdd/). + +{{< code >}} + + + +```javascript +import { describe, expect, chai } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +chai.config.aggregateChecks = false; +chai.config.logFailures = true; + +export default function testSuite() { + describe('docs example 0', () => { + expect(function () {}).to.not.throw(); + expect({ a: 1 }).to.not.have.property('b'); + expect([1, 2]).to.be.an('array').that.does.not.include(3); + }); + + describe('docs example 1', () => { + expect(2).to.equal(2); // Recommended + expect(2).to.not.equal(1); // Not recommended + }); + + describe('docs example 2', () => { + // Target object deeply (but not strictly) equals `{a: 1}` + expect({ a: 1 }).to.deep.equal({ a: 1 }); + expect({ a: 1 }).to.not.equal({ a: 1 }); + + // Target array deeply (but not strictly) includes `{a: 1}` + expect([{ a: 1 }]).to.deep.include({ a: 1 }); + expect([{ a: 1 }]).to.not.include({ a: 1 }); + + // Target object deeply (but not strictly) includes `x: {a: 1}` + expect({ x: { a: 1 } }).to.deep.include({ x: { a: 1 } }); + expect({ x: { a: 1 } }).to.not.include({ x: { a: 1 } }); + + // Target array deeply (but not strictly) has member `{a: 1}` + expect([{ a: 1 }]).to.have.deep.members([{ a: 1 }]); + expect([{ a: 1 }]).to.not.have.members([{ a: 1 }]); + + // Target set deeply (but not strictly) has key `{a: 1}` + expect(new Set([{ a: 1 }])).to.have.deep.keys([{ a: 1 }]); + expect(new Set([{ a: 1 }])).to.not.have.keys([{ a: 1 }]); + + // Target object deeply (but not strictly) has property `x: {a: 1}` + expect({ x: { a: 1 } }).to.have.deep.property('x', { a: 1 }); + expect({ x: { a: 1 } }).to.not.have.property('x', { a: 1 }); + }); + + describe('docs example 3', () => { + expect({ a: { b: ['x', 'y'] } }).to.have.nested.property('a.b[1]'); + expect({ a: { b: ['x', 'y'] } }).to.nested.include({ 'a.b[1]': 'y' }); + }); + + describe('docs example 4', () => { + expect({ '.a': { '[b]': 'x' } }).to.have.nested.property('\\.a.\\[b\\]'); + expect({ '.a': { '[b]': 'x' } }).to.nested.include({ '\\.a.\\[b\\]': 'x' }); + }); + + describe('docs example 5', () => { + Object.prototype.b = 2; + + expect({ a: 1 }).to.have.own.property('a'); + expect({ a: 1 }).to.have.property('b'); + expect({ a: 1 }).to.not.have.own.property('b'); + + expect({ a: 1 }).to.own.include({ a: 1 }); + expect({ a: 1 }).to.include({ b: 2 }).but.not.own.include({ b: 2 }); + }); + + describe('docs example 6', () => { + expect([1, 2]).to.have.ordered.members([1, 2]).but.not.have.ordered.members([2, 1]); + }); + + describe('docs example 7', () => { + expect([1, 2, 3]).to.include.ordered.members([1, 2]).but.not.include.ordered.members([2, 3]); + }); + + describe('docs example 8', () => { + expect({ a: 1, b: 2 }).to.not.have.any.keys('c', 'd'); + }); + + describe('docs example 9', () => { + expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b'); + }); + + describe('docs example 10', () => { + expect('foo').to.be.a('string'); + expect({ a: 1 }).to.be.an('object'); + expect(null).to.be.a('null'); + expect(undefined).to.be.an('undefined'); + expect(new Error()).to.be.an('error'); + expect(Promise.resolve()).to.be.a('promise'); + expect(new Float32Array()).to.be.a('float32array'); + expect(Symbol()).to.be.a('symbol'); + }); + + describe('docs example 11', () => { + var myObj = { + [Symbol.toStringTag]: 'myCustomType', + }; + + expect(myObj).to.be.a('myCustomType').but.not.an('object'); + }); + + describe('docs example 12', () => { + expect([1, 2, 3]).to.be.an('array').that.includes(2); + expect([]).to.be.an('array').that.is.empty; + }); + + describe('docs example 13', () => { + expect('foo').to.be.a('string'); // Recommended + expect('foo').to.not.be.an('array'); // Not recommended + }); + + describe('docs example 14', () => { + expect(1).to.be.a('string', 'nooo why fail??'); + expect(1, 'nooo why fail??').to.be.a('string'); + }); + + describe('docs example 15', () => { + expect({ b: 2 }).to.have.a.property('b'); + }); + + describe('docs example 16', () => { + expect('foobar').to.include('foo'); + }); + + describe('docs example 17', () => { + expect([1, 2, 3]).to.include(2); + }); + + describe('docs example 18', () => { + expect({ a: 1, b: 2, c: 3 }).to.include({ a: 1, b: 2 }); + }); + + describe('docs example 19', () => { + expect(new Set([1, 2])).to.include(2); + }); + + describe('docs example 20', () => { + expect( + new Map([ + ['a', 1], + ['b', 2], + ]) + ).to.include(2); + }); + + describe('docs example 21', () => { + expect([1, 2, 3]).to.be.an('array').that.includes(2); + }); + + describe('docs example 22', () => { + // Target array deeply (but not strictly) includes `{a: 1}` + expect([{ a: 1 }]).to.deep.include({ a: 1 }); + expect([{ a: 1 }]).to.not.include({ a: 1 }); + + // Target object deeply (but not strictly) includes `x: {a: 1}` + expect({ x: { a: 1 } }).to.deep.include({ x: { a: 1 } }); + expect({ x: { a: 1 } }).to.not.include({ x: { a: 1 } }); + }); + + describe('docs example 23', () => { + Object.prototype.b = 2; + + expect({ a: 1 }).to.own.include({ a: 1 }); + expect({ a: 1 }).to.include({ b: 2 }).but.not.own.include({ b: 2 }); + }); + + describe('docs example 24', () => { + expect({ a: { b: 2 } }).to.deep.own.include({ a: { b: 2 } }); + }); + + describe('docs example 25', () => { + expect({ a: { b: ['x', 'y'] } }).to.nested.include({ 'a.b[1]': 'y' }); + }); + + describe('docs example 26', () => { + expect({ '.a': { '[b]': 2 } }).to.nested.include({ '\\.a.\\[b\\]': 2 }); + }); + + describe('docs example 27', () => { + expect({ a: { b: [{ c: 3 }] } }).to.deep.nested.include({ 'a.b[0]': { c: 3 } }); + }); + + describe('docs example 28', () => { + expect('foobar').to.not.include('taco'); + expect([1, 2, 3]).to.not.include(4); + }); + + describe('docs example 29', () => { + expect({ c: 3 }).to.not.have.any.keys('a', 'b'); // Recommended + expect({ c: 3 }).to.not.include({ a: 1, b: 2 }); // Not recommended + }); + + describe('docs example 30', () => { + expect({ a: 3, b: 4 }).to.include({ a: 3, b: 4 }); // Recommended + expect({ a: 3, b: 4 }).to.not.include({ a: 1, b: 2 }); // Not recommended + }); + + describe('docs example 31', () => { + expect([1, 2, 3]).to.include(4, 'nooo why fail??'); + expect([1, 2, 3], 'nooo why fail??').to.include(4); + }); + + describe('docs example 32', () => { + // Target object's keys are a superset of ['a', 'b'] but not identical + expect({ a: 1, b: 2, c: 3 }).to.include.all.keys('a', 'b'); + expect({ a: 1, b: 2, c: 3 }).to.not.have.all.keys('a', 'b'); + + // Target array is a superset of [1, 2] but not identical + expect([1, 2, 3]).to.include.members([1, 2]); + expect([1, 2, 3]).to.not.have.members([1, 2]); + + // Duplicates in the subset are ignored + expect([1, 2, 3]).to.include.members([1, 2, 2, 2]); + }); + + describe('docs example 33', () => { + // Both assertions are identical + expect({ a: 1 }).to.include.any.keys('a', 'b'); + expect({ a: 1 }).to.have.any.keys('a', 'b'); + }); + + describe('docs example 34', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.be.ok; // Not recommended + + expect(true).to.be.true; // Recommended + expect(true).to.be.ok; // Not recommended + }); + + describe('docs example 35', () => { + expect(0).to.equal(0); // Recommended + expect(0).to.not.be.ok; // Not recommended + + expect(false).to.be.false; // Recommended + expect(false).to.not.be.ok; // Not recommended + + expect(null).to.be.null; // Recommended + expect(null).to.not.be.ok; // Not recommended + + expect(undefined).to.be.undefined; // Recommended + expect(undefined).to.not.be.ok; // Not recommended + }); + + describe('docs example 36', () => { + expect(false, 'nooo why fail??').to.be.ok; + }); + + describe('docs example 37', () => { + expect(true).to.be.true; + }); + + describe('docs example 38', () => { + expect(false).to.be.false; // Recommended + expect(false).to.not.be.true; // Not recommended + + expect(1).to.equal(1); // Recommended + expect(1).to.not.be.true; // Not recommended + }); + + describe('docs example 39', () => { + expect(false, 'nooo why fail??').to.be.true; + }); + + describe('docs example 40', () => { + expect(false).to.be.false; + }); + + describe('docs example 41', () => { + expect(true).to.be.true; // Recommended + expect(true).to.not.be.false; // Not recommended + + expect(1).to.equal(1); // Recommended + expect(1).to.not.be.false; // Not recommended + }); + + describe('docs example 42', () => { + expect(true, 'nooo why fail??').to.be.false; + }); + + describe('docs example 43', () => { + expect(null).to.be.null; + }); + + describe('docs example 44', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.not.be.null; // Not recommended + }); + + describe('docs example 45', () => { + expect(42, 'nooo why fail??').to.be.null; + }); + + describe('docs example 46', () => { + expect(undefined).to.be.undefined; + }); + + describe('docs example 47', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.not.be.undefined; // Not recommended + }); + + describe('docs example 48', () => { + expect(42, 'nooo why fail??').to.be.undefined; + }); + + describe('docs example 49', () => { + expect(NaN).to.be.NaN; + }); + + describe('docs example 50', () => { + expect('foo').to.equal('foo'); // Recommended + expect('foo').to.not.be.NaN; // Not recommended + }); + + describe('docs example 51', () => { + expect(42, 'nooo why fail??').to.be.NaN; + }); + + describe('docs example 52', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.exist; // Not recommended + + expect(0).to.equal(0); // Recommended + expect(0).to.exist; // Not recommended + }); + + describe('docs example 53', () => { + expect(null).to.be.null; // Recommended + expect(null).to.not.exist; // Not recommended + + expect(undefined).to.be.undefined; // Recommended + expect(undefined).to.not.exist; // Not recommended + }); + + describe('docs example 54', () => { + expect(null, 'nooo why fail??').to.exist; + }); + + describe('docs example 55', () => { + expect([]).to.be.empty; + expect('').to.be.empty; + }); + + describe('docs example 56', () => { + expect(new Set()).to.be.empty; + expect(new Map()).to.be.empty; + }); + + describe('docs example 57', () => { + expect({}).to.be.empty; + }); + + describe('docs example 58', () => { + expect([]).to.be.an('array').that.is.empty; + }); + + describe('docs example 59', () => { + expect([1, 2, 3]).to.have.lengthOf(3); // Recommended + expect([1, 2, 3]).to.not.be.empty; // Not recommended + + expect(new Set([1, 2, 3])).to.have.property('size', 3); // Recommended + expect(new Set([1, 2, 3])).to.not.be.empty; // Not recommended + + expect(Object.keys({ a: 1 })).to.have.lengthOf(1); // Recommended + expect({ a: 1 }).to.not.be.empty; // Not recommended + }); + + describe('docs example 60', () => { + expect([1, 2, 3], 'nooo why fail??').to.be.empty; + }); + + describe('docs example 61', () => { + function test() { + expect(arguments).to.be.arguments; + } + + test(); + }); + + describe('docs example 62', () => { + expect('foo').to.be.a('string'); // Recommended + expect('foo').to.not.be.arguments; // Not recommended + }); + + describe('docs example 63', () => { + expect({}, 'nooo why fail??').to.be.arguments; + }); + + describe('docs example 64', () => { + expect(1).to.equal(1); + expect('foo').to.equal('foo'); + }); + + describe('docs example 65', () => { + // Target object deeply (but not strictly) equals `{a: 1}` + expect({ a: 1 }).to.deep.equal({ a: 1 }); + expect({ a: 1 }).to.not.equal({ a: 1 }); + + // Target array deeply (but not strictly) equals `[1, 2]` + expect([1, 2]).to.deep.equal([1, 2]); + expect([1, 2]).to.not.equal([1, 2]); + }); + + describe('docs example 66', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.not.equal(2); // Not recommended + }); + + describe('docs example 67', () => { + expect(1).to.equal(2, 'nooo why fail??'); + expect(1, 'nooo why fail??').to.equal(2); + }); + + describe('docs example 68', () => { + // Target object is deeply (but not strictly) equal to {a: 1} + expect({ a: 1 }).to.eql({ a: 1 }).but.not.equal({ a: 1 }); + + // Target array is deeply (but not strictly) equal to [1, 2] + expect([1, 2]).to.eql([1, 2]).but.not.equal([1, 2]); + }); + + describe('docs example 69', () => { + expect({ a: 1 }).to.eql({ a: 1 }); // Recommended + expect({ a: 1 }).to.not.eql({ b: 2 }); // Not recommended + }); + + describe('docs example 70', () => { + expect({ a: 1 }).to.eql({ b: 2 }, 'nooo why fail??'); + expect({ a: 1 }, 'nooo why fail??').to.eql({ b: 2 }); + }); + + describe('docs example 71', () => { + expect(2).to.equal(2); // Recommended + expect(2).to.be.above(1); // Not recommended + }); + + describe('docs example 72', () => { + expect('foo').to.have.lengthOf(3); // Recommended + expect('foo').to.have.lengthOf.above(2); // Not recommended + + expect([1, 2, 3]).to.have.lengthOf(3); // Recommended + expect([1, 2, 3]).to.have.lengthOf.above(2); // Not recommended + }); + + describe('docs example 73', () => { + expect(2).to.equal(2); // Recommended + expect(1).to.not.be.above(2); // Not recommended + }); + + describe('docs example 74', () => { + expect(1).to.be.above(2, 'nooo why fail??'); + expect(1, 'nooo why fail??').to.be.above(2); + }); + + describe('docs example 75', () => { + expect(2).to.equal(2); // Recommended + expect(2).to.be.at.least(1); // Not recommended + expect(2).to.be.at.least(2); // Not recommended + }); + + describe('docs example 76', () => { + expect('foo').to.have.lengthOf(3); // Recommended + expect('foo').to.have.lengthOf.at.least(2); // Not recommended + + expect([1, 2, 3]).to.have.lengthOf(3); // Recommended + expect([1, 2, 3]).to.have.lengthOf.at.least(2); // Not recommended + }); + + describe('docs example 77', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.not.be.at.least(2); // Not recommended + }); + + describe('docs example 78', () => { + expect(1).to.be.at.least(2, 'nooo why fail??'); + expect(1, 'nooo why fail??').to.be.at.least(2); + }); + + describe('docs example 79', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.be.below(2); // Not recommended + }); + + describe('docs example 80', () => { + expect('foo').to.have.lengthOf(3); // Recommended + expect('foo').to.have.lengthOf.below(4); // Not recommended + + expect([1, 2, 3]).to.have.length(3); // Recommended + expect([1, 2, 3]).to.have.lengthOf.below(4); // Not recommended + }); + + describe('docs example 81', () => { + expect(2).to.equal(2); // Recommended + expect(2).to.not.be.below(1); // Not recommended + }); + + describe('docs example 82', () => { + expect(2).to.be.below(1, 'nooo why fail??'); + expect(2, 'nooo why fail??').to.be.below(1); + }); + + describe('docs example 83', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.be.at.most(2); // Not recommended + expect(1).to.be.at.most(1); // Not recommended + }); + + describe('docs example 84', () => { + expect('foo').to.have.lengthOf(3); // Recommended + expect('foo').to.have.lengthOf.at.most(4); // Not recommended + + expect([1, 2, 3]).to.have.lengthOf(3); // Recommended + expect([1, 2, 3]).to.have.lengthOf.at.most(4); // Not recommended + }); + + describe('docs example 85', () => { + expect(2).to.equal(2); // Recommended + expect(2).to.not.be.at.most(1); // Not recommended + }); + + describe('docs example 86', () => { + expect(2).to.be.at.most(1, 'nooo why fail??'); + expect(2, 'nooo why fail??').to.be.at.most(1); + }); + + describe('docs example 87', () => { + expect(2).to.equal(2); // Recommended + expect(2).to.be.within(1, 3); // Not recommended + expect(2).to.be.within(2, 3); // Not recommended + expect(2).to.be.within(1, 2); // Not recommended + }); + + describe('docs example 88', () => { + expect('foo').to.have.lengthOf.within(2, 4); // Not recommended + + expect([1, 2, 3]).to.have.lengthOf(3); // Recommended + expect([1, 2, 3]).to.have.lengthOf.within(2, 4); // Not recommended + }); + + describe('docs example 89', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.not.be.within(2, 4); // Not recommended + }); + + describe('docs example 90', () => { + expect(4).to.be.within(1, 3, 'nooo why fail??'); + expect(4, 'nooo why fail??').to.be.within(1, 3); + }); + + describe('docs example 91', () => { + function Cat() {} + + expect(new Cat()).to.be.an.instanceof(Cat); + expect([1, 2]).to.be.an.instanceof(Array); + }); + + describe('docs example 92', () => { + expect({ a: 1 }).to.not.be.an.instanceof(Array); + }); + + describe('docs example 93', () => { + expect(1).to.be.an.instanceof(Array, 'nooo why fail??'); + expect(1, 'nooo why fail??').to.be.an.instanceof(Array); + }); + + describe('docs example 94', () => { + expect({ a: 1 }).to.have.property('a'); + }); + + describe('docs example 95', () => { + expect({ a: 1 }).to.have.property('a', 1); + }); + + describe('docs example 96', () => { + // Target object deeply (but not strictly) has property `x: {a: 1}` + expect({ x: { a: 1 } }).to.have.deep.property('x', { a: 1 }); + expect({ x: { a: 1 } }).to.not.have.property('x', { a: 1 }); + }); + + describe('docs example 97', () => { + Object.prototype.b = 2; + + expect({ a: 1 }).to.have.own.property('a'); + expect({ a: 1 }).to.have.own.property('a', 1); + expect({ a: 1 }).to.have.property('b'); + expect({ a: 1 }).to.not.have.own.property('b'); + }); + + describe('docs example 98', () => { + expect({ x: { a: 1 } }).to.have.deep.own.property('x', { a: 1 }); + }); + + describe('docs example 99', () => { + expect({ a: { b: ['x', 'y'] } }).to.have.nested.property('a.b[1]'); + expect({ a: { b: ['x', 'y'] } }).to.have.nested.property('a.b[1]', 'y'); + }); + + describe('docs example 100', () => { + expect({ '.a': { '[b]': 'x' } }).to.have.nested.property('\\.a.\\[b\\]'); + }); + + describe('docs example 101', () => { + expect({ a: { b: [{ c: 3 }] } }).to.have.deep.nested.property('a.b[0]', { c: 3 }); + }); + + describe('docs example 102', () => { + expect({ a: 1 }).to.not.have.property('b'); + }); + + describe('docs example 103', () => { + expect({ b: 2 }).to.not.have.property('a'); // Recommended + expect({ b: 2 }).to.not.have.property('a', 1); // Not recommended + }); + + describe('docs example 104', () => { + expect({ a: 3 }).to.have.property('a', 3); // Recommended + expect({ a: 3 }).to.not.have.property('a', 1); // Not recommended + }); + + describe('docs example 105', () => { + expect({ a: 1 }).to.have.property('a').that.is.a('number'); + }); + + describe('docs example 106', () => { + // Recommended + expect({ a: 1 }).to.have.property('a', 2, 'nooo why fail??'); + expect({ a: 1 }, 'nooo why fail??').to.have.property('a', 2); + expect({ a: 1 }, 'nooo why fail??').to.have.property('b'); + + // Not recommended + expect({ a: 1 }).to.have.property('b', undefined, 'nooo why fail??'); + }); + + describe('docs example 107', () => { + expect({ a: 1 }).to.have.ownPropertyDescriptor('a'); + }); + + describe('docs example 108', () => { + expect({ a: 1 }).to.have.ownPropertyDescriptor('a', { + configurable: true, + enumerable: true, + writable: true, + value: 1, + }); + }); + + describe('docs example 109', () => { + expect({ a: 1 }).to.not.have.ownPropertyDescriptor('b'); + }); + + describe('docs example 110', () => { + // Recommended + expect({ b: 2 }).to.not.have.ownPropertyDescriptor('a'); + + // Not recommended + expect({ b: 2 }).to.not.have.ownPropertyDescriptor('a', { + configurable: true, + enumerable: true, + writable: true, + value: 1, + }); + }); + + describe('docs example 111', () => { + // Recommended + expect({ a: 3 }).to.have.ownPropertyDescriptor('a', { + configurable: true, + enumerable: true, + writable: true, + value: 3, + }); + + // Not recommended + expect({ a: 3 }).to.not.have.ownPropertyDescriptor('a', { + configurable: true, + enumerable: true, + writable: true, + value: 1, + }); + }); + + describe('docs example 112', () => { + expect({ a: 1 }).to.have.ownPropertyDescriptor('a').that.has.property('enumerable', true); + }); + + describe('docs example 113', () => { + // Recommended + expect({ a: 1 }).to.have.ownPropertyDescriptor( + 'a', + { + configurable: true, + enumerable: true, + writable: true, + value: 2, + }, + 'nooo why fail??' + ); + + // Recommended + expect({ a: 1 }, 'nooo why fail??').to.have.ownPropertyDescriptor('a', { + configurable: true, + enumerable: true, + writable: true, + value: 2, + }); + + // Recommended + expect({ a: 1 }, 'nooo why fail??').to.have.ownPropertyDescriptor('b'); + + // Not recommended + expect({ a: 1 }).to.have.ownPropertyDescriptor('b', undefined, 'nooo why fail??'); + }); + + describe('docs example 114', () => { + expect([1, 2, 3]).to.have.lengthOf(3); + expect('foo').to.have.lengthOf(3); + expect(new Set([1, 2, 3])).to.have.lengthOf(3); + expect( + new Map([ + ['a', 1], + ['b', 2], + ['c', 3], + ]) + ).to.have.lengthOf(3); + }); + + describe('docs example 115', () => { + expect('foo').to.have.lengthOf(3); // Recommended + expect('foo').to.not.have.lengthOf(4); // Not recommended + }); + + describe('docs example 116', () => { + expect([1, 2, 3]).to.have.lengthOf(2, 'nooo why fail??'); + expect([1, 2, 3], 'nooo why fail??').to.have.lengthOf(2); + }); + + describe('docs example 117', () => { + // Recommended + expect([1, 2, 3]).to.have.lengthOf(3); + + // Not recommended + expect([1, 2, 3]).to.have.lengthOf.above(2); + expect([1, 2, 3]).to.have.lengthOf.below(4); + expect([1, 2, 3]).to.have.lengthOf.at.least(3); + expect([1, 2, 3]).to.have.lengthOf.at.most(3); + expect([1, 2, 3]).to.have.lengthOf.within(2, 4); + }); + + describe('docs example 118', () => { + expect([1, 2, 3]).to.have.a.length(3); // incompatible; throws error + expect([1, 2, 3]).to.have.a.lengthOf(3); // passes as expected + }); + + describe('docs example 119', () => { + expect('foobar').to.match(/^foo/); + }); + + describe('docs example 120', () => { + expect('foobar').to.not.match(/taco/); + }); + + describe('docs example 121', () => { + expect('foobar').to.match(/taco/, 'nooo why fail??'); + expect('foobar', 'nooo why fail??').to.match(/taco/); + }); + + describe('docs example 122', () => { + expect('foobar').to.have.string('bar'); + }); + + describe('docs example 123', () => { + expect('foobar').to.not.have.string('taco'); + }); + + describe('docs example 124', () => { + expect('foobar').to.have.string('taco', 'nooo why fail??'); + expect('foobar', 'nooo why fail??').to.have.string('taco'); + }); + + describe('docs example 125', () => { + expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b'); + expect(['x', 'y']).to.have.all.keys(0, 1); + + expect({ a: 1, b: 2 }).to.have.all.keys(['a', 'b']); + expect(['x', 'y']).to.have.all.keys([0, 1]); + + expect({ a: 1, b: 2 }).to.have.all.keys({ a: 4, b: 5 }); // ignore 4 and 5 + expect(['x', 'y']).to.have.all.keys({ 0: 4, 1: 5 }); // ignore 4 and 5 + }); + + describe('docs example 126', () => { + expect( + new Map([ + ['a', 1], + ['b', 2], + ]) + ).to.have.all.keys('a', 'b'); + expect(new Set(['a', 'b'])).to.have.all.keys('a', 'b'); + }); + + describe('docs example 127', () => { + expect({ a: 1, b: 2 }).to.be.an('object').that.has.all.keys('a', 'b'); + }); + + describe('docs example 128', () => { + // Target set deeply (but not strictly) has key `{a: 1}` + expect(new Set([{ a: 1 }])).to.have.all.deep.keys([{ a: 1 }]); + expect(new Set([{ a: 1 }])).to.not.have.all.keys([{ a: 1 }]); + }); + + describe('docs example 129', () => { + // Recommended; asserts that target doesn't have any of the given keys + expect({ a: 1, b: 2 }).to.not.have.any.keys('c', 'd'); + + // Not recommended; asserts that target doesn't have all of the given + // keys but may or may not have some of them + expect({ a: 1, b: 2 }).to.not.have.all.keys('c', 'd'); + }); + + describe('docs example 130', () => { + // Recommended; asserts that target has all the given keys + expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b'); + + // Not recommended; asserts that target has at least one of the given + // keys but may or may not have more of them + expect({ a: 1, b: 2 }).to.have.any.keys('a', 'b'); + }); + + describe('docs example 131', () => { + // Both assertions are identical + expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b'); // Recommended + expect({ a: 1, b: 2 }).to.have.keys('a', 'b'); // Not recommended + }); + + describe('docs example 132', () => { + // Target object's keys are a superset of ['a', 'b'] but not identical + expect({ a: 1, b: 2, c: 3 }).to.include.all.keys('a', 'b'); + expect({ a: 1, b: 2, c: 3 }).to.not.have.all.keys('a', 'b'); + }); + + describe('docs example 133', () => { + // Both assertions are identical + expect({ a: 1 }).to.have.any.keys('a', 'b'); + expect({ a: 1 }).to.include.any.keys('a', 'b'); + }); + + describe('docs example 134', () => { + expect({ a: 1 }, 'nooo why fail??').to.have.key('b'); + }); + + describe('docs example 135', () => { + var badFn = function () { + throw new TypeError('Illegal salmon!'); + }; + + expect(badFn).to.throw(); + }); + + describe('docs example 136', () => { + var badFn = function () { + throw new TypeError('Illegal salmon!'); + }; + + expect(badFn).to.throw(TypeError); + }); + + describe('docs example 137', () => { + var err = new TypeError('Illegal salmon!'); + var badFn = function () { + throw err; + }; + + expect(badFn).to.throw(err); + }); + + describe('docs example 138', () => { + var badFn = function () { + throw new TypeError('Illegal salmon!'); + }; + + expect(badFn).to.throw('salmon'); + }); + + describe('docs example 139', () => { + var badFn = function () { + throw new TypeError('Illegal salmon!'); + }; + + expect(badFn).to.throw(/salmon/); + }); + + describe('docs example 140', () => { + var err = new TypeError('Illegal salmon!'); + var badFn = function () { + throw err; + }; + + expect(badFn).to.throw(TypeError, 'salmon'); + expect(badFn).to.throw(TypeError, /salmon/); + expect(badFn).to.throw(err, 'salmon'); + expect(badFn).to.throw(err, /salmon/); + }); + + describe('docs example 141', () => { + var goodFn = function () {}; + + expect(goodFn).to.not.throw(); + }); + + describe('docs example 142', () => { + var goodFn = function () {}; + + expect(goodFn).to.not.throw(); // Recommended + expect(goodFn).to.not.throw(ReferenceError, 'x'); // Not recommended + }); + + describe('docs example 143', () => { + var badFn = function () { + throw new TypeError('Illegal salmon!'); + }; + + expect(badFn).to.throw(TypeError, 'salmon'); // Recommended + expect(badFn).to.not.throw(ReferenceError, 'x'); // Not recommended + }); + + describe('docs example 144', () => { + var err = new TypeError('Illegal salmon!'); + err.code = 42; + var badFn = function () { + throw err; + }; + + expect(badFn).to.throw(TypeError).with.property('code', 42); + }); + + describe('docs example 145', () => { + var goodFn = function () {}; + + expect(goodFn).to.throw(TypeError, 'x', 'nooo why fail??'); + expect(goodFn, 'nooo why fail??').to.throw(); + }); + + describe('docs example 146', () => { + var fn = function () { + throw new TypeError('Illegal salmon!'); + }; + + expect(fn).to.throw(); // Good! Tests `fn` as desired + expect(fn()).to.throw(); // Bad! Tests result of `fn()`, not `fn` + }); + + describe('docs example 147', () => { + expect(function () { + fn(42); + }).to.throw(); // Function expression + expect(() => fn(42)).to.throw(); // ES6 arrow function + }); + + describe('docs example 148', () => { + expect(function () { + cat.meow(); + }).to.throw(); // Function expression + expect(() => cat.meow()).to.throw(); // ES6 arrow function + expect(cat.meow.bind(cat)).to.throw(); // Bind + }); + + describe('docs example 149', () => { + function Cat() {} + Cat.prototype.meow = function () {}; + + expect(new Cat()).to.respondTo('meow'); + }); + + describe('docs example 150', () => { + function Cat() {} + Cat.prototype.meow = function () {}; + + expect(Cat).to.respondTo('meow'); + }); + + describe('docs example 151', () => { + function Cat() {} + Cat.prototype.meow = function () {}; + Cat.hiss = function () {}; + + expect(Cat).itself.to.respondTo('hiss').but.not.respondTo('meow'); + }); + + describe('docs example 152', () => { + function Cat() {} + Cat.prototype.meow = function () {}; + + expect(new Cat()).to.be.an('object').that.respondsTo('meow'); + }); + + describe('docs example 153', () => { + function Dog() {} + Dog.prototype.bark = function () {}; + + expect(new Dog()).to.not.respondTo('meow'); + }); + + describe('docs example 154', () => { + expect({}).to.respondTo('meow', 'nooo why fail??'); + expect({}, 'nooo why fail??').to.respondTo('meow'); + }); + + describe('docs example 155', () => { + function Cat() {} + Cat.prototype.meow = function () {}; + Cat.hiss = function () {}; + + expect(Cat).itself.to.respondTo('hiss').but.not.respondTo('meow'); + }); + + describe('docs example 156', () => { + expect(1).to.satisfy(function (num) { + return num > 0; + }); + }); + + describe('docs example 157', () => { + expect(1).to.not.satisfy(function (num) { + return num > 2; + }); + }); + + describe('docs example 158', () => { + expect(1).to.satisfy(function (num) { + return num > 2; + }, 'nooo why fail??'); + + expect(1, 'nooo why fail??').to.satisfy(function (num) { + return num > 2; + }); + }); + + describe('docs example 159', () => { + // Recommended + expect(1.5).to.equal(1.5); + + // Not recommended + expect(1.5).to.be.closeTo(1, 0.5); + expect(1.5).to.be.closeTo(2, 0.5); + expect(1.5).to.be.closeTo(1, 1); + }); + + describe('docs example 160', () => { + expect(1.5).to.equal(1.5); // Recommended + expect(1.5).to.not.be.closeTo(3, 1); // Not recommended + }); + + describe('docs example 161', () => { + expect(1.5).to.be.closeTo(3, 1, 'nooo why fail??'); + expect(1.5, 'nooo why fail??').to.be.closeTo(3, 1); + }); + + describe('docs example 162', () => { + expect([1, 2, 3]).to.have.members([2, 1, 3]); + expect([1, 2, 2]).to.have.members([2, 1, 2]); + }); + + describe('docs example 163', () => { + // Target array deeply (but not strictly) has member `{a: 1}` + expect([{ a: 1 }]).to.have.deep.members([{ a: 1 }]); + expect([{ a: 1 }]).to.not.have.members([{ a: 1 }]); + }); + + describe('docs example 164', () => { + expect([1, 2, 3]).to.have.ordered.members([1, 2, 3]); + expect([1, 2, 3]).to.have.members([2, 1, 3]).but.not.ordered.members([2, 1, 3]); + }); + + describe('docs example 165', () => { + // Target array is a superset of [1, 2] but not identical + expect([1, 2, 3]).to.include.members([1, 2]); + expect([1, 2, 3]).to.not.have.members([1, 2]); + + // Duplicates in the subset are ignored + expect([1, 2, 3]).to.include.members([1, 2, 2, 2]); + }); + + describe('docs example 166', () => { + expect([{ a: 1 }, { b: 2 }, { c: 3 }]) + .to.include.deep.ordered.members([{ a: 1 }, { b: 2 }]) + .but.not.include.deep.ordered.members([{ b: 2 }, { c: 3 }]); + }); + + describe('docs example 167', () => { + expect([1, 2]).to.not.include(3).and.not.include(4); // Recommended + expect([1, 2]).to.not.have.members([3, 4]); // Not recommended + }); + + describe('docs example 168', () => { + expect([1, 2]).to.have.members([1, 2, 3], 'nooo why fail??'); + expect([1, 2], 'nooo why fail??').to.have.members([1, 2, 3]); + }); + + describe('docs example 169', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.be.oneOf([1, 2, 3]); // Not recommended + }); + + describe('docs example 170', () => { + expect(1).to.equal(1); // Recommended + expect(1).to.not.be.oneOf([2, 3, 4]); // Not recommended + }); + + describe('docs example 171', () => { + expect('Today is sunny').to.contain.oneOf(['sunny', 'cloudy']); + expect('Today is rainy').to.not.contain.oneOf(['sunny', 'cloudy']); + expect([1, 2, 3]).to.contain.oneOf([3, 4, 5]); + expect([1, 2, 3]).to.not.contain.oneOf([4, 5, 6]); + }); + + describe('docs example 172', () => { + expect(1).to.be.oneOf([2, 3, 4], 'nooo why fail??'); + expect(1, 'nooo why fail??').to.be.oneOf([2, 3, 4]); + }); + + describe('docs example 173', () => { + var dots = '', + addDot = function () { + dots += '.'; + }, + getDots = function () { + return dots; + }; + + // Recommended + expect(getDots()).to.equal(''); + addDot(); + expect(getDots()).to.equal('.'); + + // Not recommended + expect(addDot).to.change(getDots); + }); + + describe('docs example 174', () => { + var myObj = { dots: '' }, + addDot = function () { + myObj.dots += '.'; + }; + + // Recommended + expect(myObj).to.have.property('dots', ''); + addDot(); + expect(myObj).to.have.property('dots', '.'); + + // Not recommended + expect(addDot).to.change(myObj, 'dots'); + }); + + describe('docs example 175', () => { + var dots = '', + noop = function () {}, + getDots = function () { + return dots; + }; + + expect(noop).to.not.change(getDots); + + var myObj = { dots: '' }, + noop = function () {}; + + expect(noop).to.not.change(myObj, 'dots'); + }); + + describe('docs example 176', () => { + var myObj = { dots: '' }, + addDot = function () { + myObj.dots += '.'; + }; + + expect(addDot).to.not.change(myObj, 'dots', 'nooo why fail??'); + + var dots = '', + addDot = function () { + dots += '.'; + }, + getDots = function () { + return dots; + }; + + expect(addDot, 'nooo why fail??').to.not.change(getDots); + }); + + describe('docs example 177', () => { + var myObj = { val: 1 }, + addTwo = function () { + myObj.val += 2; + }, + subtractTwo = function () { + myObj.val -= 2; + }; + + expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended + expect(addTwo).to.change(myObj, 'val').by(2); // Not recommended + + expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended + expect(subtractTwo).to.change(myObj, 'val').by(2); // Not recommended + }); + + describe('docs example 178', () => { + var val = 1, + addTwo = function () { + val += 2; + }, + getVal = function () { + return val; + }; + + expect(addTwo).to.increase(getVal).by(2); // Recommended + expect(addTwo).to.increase(getVal); // Not recommended + }); + + describe('docs example 179', () => { + var myObj = { val: 1 }, + addTwo = function () { + myObj.val += 2; + }; + + expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended + expect(addTwo).to.increase(myObj, 'val'); // Not recommended + }); + + describe('docs example 180', () => { + var myObj = { val: 1 }, + subtractTwo = function () { + myObj.val -= 2; + }; + + expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended + expect(subtractTwo).to.not.increase(myObj, 'val'); // Not recommended + }); + + describe('docs example 181', () => { + var myObj = { val: 1 }, + noop = function () {}; + + expect(noop).to.not.change(myObj, 'val'); // Recommended + expect(noop).to.not.increase(myObj, 'val'); // Not recommended + }); + + describe('docs example 182', () => { + var myObj = { val: 1 }, + noop = function () {}; + + expect(noop).to.increase(myObj, 'val', 'nooo why fail??'); + + var val = 1, + noop = function () {}, + getVal = function () { + return val; + }; + + expect(noop, 'nooo why fail??').to.increase(getVal); + }); + + describe('docs example 183', () => { + var val = 1, + subtractTwo = function () { + val -= 2; + }, + getVal = function () { + return val; + }; + + expect(subtractTwo).to.decrease(getVal).by(2); // Recommended + expect(subtractTwo).to.decrease(getVal); // Not recommended + }); + + describe('docs example 184', () => { + var myObj = { val: 1 }, + subtractTwo = function () { + myObj.val -= 2; + }; + + expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended + expect(subtractTwo).to.decrease(myObj, 'val'); // Not recommended + }); + + describe('docs example 185', () => { + var myObj = { val: 1 }, + addTwo = function () { + myObj.val += 2; + }; + + expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended + expect(addTwo).to.not.decrease(myObj, 'val'); // Not recommended + }); + + describe('docs example 186', () => { + var myObj = { val: 1 }, + noop = function () {}; + + expect(noop).to.not.change(myObj, 'val'); // Recommended + expect(noop).to.not.decrease(myObj, 'val'); // Not recommended + }); + + describe('docs example 187', () => { + var myObj = { val: 1 }, + noop = function () {}; + + expect(noop).to.decrease(myObj, 'val', 'nooo why fail??'); + + var val = 1, + noop = function () {}, + getVal = function () { + return val; + }; + + expect(noop, 'nooo why fail??').to.decrease(getVal); + }); + + describe('docs example 188', () => { + var myObj = { val: 1 }, + addTwo = function () { + myObj.val += 2; + }; + + expect(addTwo).to.increase(myObj, 'val').by(2); + }); + + describe('docs example 189', () => { + var myObj = { val: 1 }, + subtractTwo = function () { + myObj.val -= 2; + }; + + expect(subtractTwo).to.decrease(myObj, 'val').by(2); + }); + + describe('docs example 190', () => { + var myObj = { val: 1 }, + addTwo = function () { + myObj.val += 2; + }, + subtractTwo = function () { + myObj.val -= 2; + }; + + expect(addTwo).to.increase(myObj, 'val').by(2); // Recommended + expect(addTwo).to.change(myObj, 'val').by(2); // Not recommended + + expect(subtractTwo).to.decrease(myObj, 'val').by(2); // Recommended + expect(subtractTwo).to.change(myObj, 'val').by(2); // Not recommended + }); + + describe('docs example 191', () => { + var myObj = { val: 1 }, + addTwo = function () { + myObj.val += 2; + }; + + // Recommended + expect(addTwo).to.increase(myObj, 'val').by(2); + + // Not recommended + expect(addTwo).to.increase(myObj, 'val').but.not.by(3); + }); + + describe('docs example 192', () => { + var myObj = { val: 1 }, + addTwo = function () { + myObj.val += 2; + }; + + expect(addTwo).to.increase(myObj, 'val').by(3, 'nooo why fail??'); + expect(addTwo, 'nooo why fail??').to.increase(myObj, 'val').by(3); + }); + + describe('docs example 193', () => { + expect({ a: 1 }).to.be.extensible; + }); + + describe('docs example 194', () => { + var nonExtensibleObject = Object.preventExtensions({}), + sealedObject = Object.seal({}), + frozenObject = Object.freeze({}); + + expect(nonExtensibleObject).to.not.be.extensible; + expect(sealedObject).to.not.be.extensible; + expect(frozenObject).to.not.be.extensible; + expect(1).to.not.be.extensible; + }); + + describe('docs example 195', () => { + expect(1, 'nooo why fail??').to.be.extensible; + }); + + describe('docs example 196', () => { + var sealedObject = Object.seal({}); + var frozenObject = Object.freeze({}); + + expect(sealedObject).to.be.sealed; + expect(frozenObject).to.be.sealed; + expect(1).to.be.sealed; + }); + + describe('docs example 197', () => { + expect({ a: 1 }).to.not.be.sealed; + }); + + describe('docs example 198', () => { + expect({ a: 1 }, 'nooo why fail??').to.be.sealed; + }); + + describe('docs example 199', () => { + var frozenObject = Object.freeze({}); + + expect(frozenObject).to.be.frozen; + expect(1).to.be.frozen; + }); + + describe('docs example 200', () => { + expect({ a: 1 }).to.not.be.frozen; + }); + + describe('docs example 201', () => { + expect({ a: 1 }, 'nooo why fail??').to.be.frozen; + }); + + describe('docs example 202', () => { + expect(1).to.be.finite; + }); + + describe('docs example 203', () => { + expect('foo').to.be.a('string'); // Recommended + expect('foo').to.not.be.finite; // Not recommended + }); + + describe('docs example 204', () => { + expect(NaN).to.be.NaN; // Recommended + expect(NaN).to.not.be.finite; // Not recommended + }); + + describe('docs example 205', () => { + expect(Infinity).to.equal(Infinity); // Recommended + expect(Infinity).to.not.be.finite; // Not recommended + }); + + describe('docs example 206', () => { + expect(-Infinity).to.equal(-Infinity); // Recommended + expect(-Infinity).to.not.be.finite; // Not recommended + }); + + describe('docs example 207', () => { + expect('foo', 'nooo why fail??').to.be.finite; + }); + + describe('docs example 208', () => { + expect.fail(); + expect.fail('custom error message'); + expect.fail(1, 2); + expect.fail(1, 2, 'custom error message'); + expect.fail(1, 2, 'custom error message', '>'); + expect.fail(1, 2, undefined, '>'); + }); + + // describe('docs example 209', () => { + // should.fail(); + // should.fail("custom error message"); + // should.fail(1, 2); + // should.fail(1, 2, "custom error message"); + // should.fail(1, 2, "custom error message", ">"); + // should.fail(1, 2, undefined, ">"); + + // }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/generating-uuids.md b/docs/sources/v0.50.x/examples/generating-uuids.md new file mode 100644 index 000000000..9c736c9de --- /dev/null +++ b/docs/sources/v0.50.x/examples/generating-uuids.md @@ -0,0 +1,82 @@ +--- +title: 'Generating UUIDs' +description: 'Scripting example on how to generate UUIDs in your load test.' +weight: 11 +--- + +# Generating UUIDs + +If you want to make a version 4 UUID, +you can use the [`uuidv4` function](https://grafana.com/docs/k6//javascript-api/jslib/utils/uuidv4) from the [k6 JS lib repository](https://jslib.k6.io/). + +{{< code >}} + +```javascript +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; + +export default function () { + const randomUUID = uuidv4(); + console.log(randomUUID); // 35acae14-f7cb-468a-9866-1fc45713149a +} +``` + +{{< /code >}} + +If you really need other UUID versions, you must rely on an external library. + +## Generate v1 UUIDs + +As k6 doesn't have built-in support +for version 1 UUID, you'll have to use a third-party library. + +This example uses a Node.js library called [uuid](https://www.npmjs.com/package/uuid) +and [Browserify](http://browserify.org/) (to make it work in k6). +For this to work, we first need to go through a few required steps: + +1. Make sure you have the necessary prerequisites installed: + [Node.js](https://nodejs.org/en/download/) and [Browserify](http://browserify.org/) + +2. Install the `uuid` library: + {{< code >}} + + ```bash + $ npm install uuid@3.4.0 + ``` + + {{< /code >}} + +3. Run it through browserify: + {{< code >}} + + ```bash + $ browserify node_modules/uuid/index.js -s uuid > uuid.js + ``` + + {{< /code >}} + +4. Move the `uuid.js` file to the same folder as your script file. Now you can import + it into your test script: + + {{< code >}} + + ```javascript + import uuid from './uuid.js'; + ``` + + {{< /code >}} + +This example generates a v1 UUID: + +{{< code >}} + +```javascript +import uuid from './uuid.js'; + +export default function () { + // Generate a UUID v1 + const uuid1 = uuid.v1(); + console.log(uuid1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/get-started-with-k6/_index.md b/docs/sources/v0.50.x/examples/get-started-with-k6/_index.md new file mode 100644 index 000000000..a99732943 --- /dev/null +++ b/docs/sources/v0.50.x/examples/get-started-with-k6/_index.md @@ -0,0 +1,46 @@ +--- +title: 'Get started with k6' +description: A series of docs to learn how to use the major features of k6 +weight: 01 +weight: -10 +--- + +# Get started with k6 + +This tutorial provides some procedures for common real-life uses of k6. +They assume no prior knowledge of k6 or of JavaScript. + +These tasks are all reproducible, so open up your favorite editor and follow along. +Imagine that you are a developer or tester who is responsible for performance in your development team. + +## Context: test a new endpoint + +Your development team has just developed a new login endpoint. +Before releasing, you must test the endpoint for the following questions: + +- Does it work? +- Does it perform within the service-level objectives under average load? +- At what load does performance degrade beyond objectives? + +After you test the endpoint, your team wants you to compare different components of the user-facing application. + +Finally, after you test the API and web application, break your scripts down into reusable parts. + +## Before you start + +We encourage you to open your terminal and actively experiment with these examples. +The tutorial requires the following: + +- [ ][k6 installed](https://grafana.com/docs/k6//get-started/installation) +- [ ] A clean directory to experiment in. +- [ ] Something to do during the minute or two when k6 runs the longest example tests +- [ ] Optional: [`jq`](https://stedolan.github.io/jq/) to filter some results + +## Tutorials + +| In this tutorial | Learn how to | +| ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [Test for functional behavior](https://grafana.com/docs/k6//examples/get-started-with-k6/test-for-functional-behavior) | Use k6 to script requests and evaluate that performance is correct | +| [Test for performance](https://grafana.com/docs/k6//examples/get-started-with-k6/test-for-performance) | Use k6 to increase load and find faults | +| [Analyze results](https://grafana.com/docs/k6//examples/get-started-with-k6/analyze-results) | Filter results and make custom metrics | +| [Reuse and re-run](https://grafana.com/docs/k6//examples/get-started-with-k6/reuse-and-re-run-tests) | Modularize and re-run tests | diff --git a/docs/sources/v0.50.x/examples/get-started-with-k6/analyze-results.md b/docs/sources/v0.50.x/examples/get-started-with-k6/analyze-results.md new file mode 100644 index 000000000..f447af313 --- /dev/null +++ b/docs/sources/v0.50.x/examples/get-started-with-k6/analyze-results.md @@ -0,0 +1,357 @@ +--- +title: Analyze results +description: Use k6 to write custom metrics and filter results. +weight: 300 +--- + +# Analyze results + +In this tutorial, learn how to: + +- Apply tags to filter specific results +- Learn about k6 metrics +- Use [jq](https://jqlang.github.io/jq/) to filter JSON results +- Define groups to organize the test +- Create custom metrics + +## Context: k6 result outputs + +k6 provides many [result outputs](https://grafana.com/docs/k6//results-output/). +By default, the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test) provides the aggregated results of the test metrics. + +{{< code >}} + +```bash +checks.........................: 50.00% ✓ 45 ✗ 45 +data_received..................: 1.3 MB 31 kB/s +data_sent......................: 81 kB 2.0 kB/s +group_duration.................: avg=6.45s min=4.01s med=6.78s max=10.15s p(90)=9.29s p(95)=9.32s +http_req_blocked...............: avg=57.62ms min=7µs med=12.25µs max=1.35s p(90)=209.41ms p(95)=763.61ms +http_req_connecting............: avg=20.51ms min=0s med=0s max=1.1s p(90)=100.76ms p(95)=173.41ms +http_req_duration..............: avg=144.56ms min=104.11ms med=110.47ms max=1.14s p(90)=203.54ms p(95)=215.95ms + { expected_response:true }...: avg=144.56ms min=104.11ms med=110.47ms max=1.14s p(90)=203.54ms p(95)=215.95ms +http_req_failed................: 0.00% ✓ 0 ✗ 180 +http_req_receiving.............: avg=663.96µs min=128.46µs med=759.82µs max=1.66ms p(90)=1.3ms p(95)=1.46ms +http_req_sending...............: avg=88.01µs min=43.07µs med=78.03µs max=318.81µs p(90)=133.15µs p(95)=158.3µs +http_req_tls_handshaking.......: avg=29.25ms min=0s med=0s max=458.71ms p(90)=108.31ms p(95)=222.46ms +http_req_waiting...............: avg=143.8ms min=103.5ms med=109.5ms max=1.14s p(90)=203.19ms p(95)=215.56ms +http_reqs......................: 180 4.36938/s +iteration_duration.............: avg=12.91s min=12.53s med=12.77s max=14.35s p(90)=13.36s p(95)=13.37s +iterations.....................: 45 1.092345/s +vus............................: 1 min=1 max=19 +vus_max........................: 20 min=20 max=20 +``` + +{{< /code >}} + +For simplicity to learn about [k6 metric results](https://grafana.com/docs/k6//using-k6/metrics/reference), this tutorial uses the [JSON output](https://grafana.com/docs/k6//results-output/real-time/json) and [jq](https://jqlang.github.io/jq/) to filter results. + +For other options to analyze test results such as storage and time-series visualizations in real-time, refer to: + +- [Results output](https://grafana.com/docs/k6//results-output/) + +- [Ways to visualize k6 results](https://k6.io/blog/ways-to-visualize-k6-results/) + +## Write time-series results to a JSON file + +To output results to a JSON file, use the `--out` flag. + +```bash +k6 run --out json=results.json api-test.js +``` + +Then run this `jq` command to filter the latency results; `http_req_duration` metric. + +```bash +jq '. | select(.type == "Point" and .metric == "http_req_duration")' results.json +``` + +k6 results have a number of [built-in tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#system-tags). For example, filter results to only results where the status is 200. + +```bash +jq '. | select(.type == "Point" and .data.tags.status == "200")' results.json +``` + +Or calculate the aggregated value of any metric with any particular tags. + +{{< code >}} + +```average +jq '. | select(.type == "Point" and .metric == "http_req_duration") | .data.value' results.json | jq -s 'add/length' +``` + +```min +jq '. | select(.type == "Point" and .metric == "http_req_duration") | .data.value' results.json | jq -s min +``` + +```max +jq '. | select(.type == "Point" and .metric == "http_req_duration") | .data.value' results.json | jq -s max +``` + +{{< /code >}} + +## Apply custom tags + +You can also apply [_Tags_](https://grafana.com/docs/k6//using-k6/tags-and-groups#tags) to requests or code blocks. For example, this is how you can add a [`tags`](https://grafana.com/docs/k6//using-k6/tags-and-groups#tags) to the [request params](https://grafana.com/docs/k6//javascript-api/k6-http/params). + +```javascript +const params = { + headers: { + 'Content-Type': 'application/json', + }, + tags: { + 'my-custom-tag': 'auth-api', + }, +}; +``` + +Create a new script named "tagged-login.js", and add a custom tag to it. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const url = 'https://test-api.k6.io'; + const payload = JSON.stringify({ + username: 'test_case', + password: '1234', + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + //apply tags + tags: { + 'my-custom-tag': 'auth-api', + }, + }; + + //Login with tags + http.post(`${url}/auth/basic/login`, payload, params); +} +``` + +{{< /code >}} + +Run the test: + +```bash +k6 run --out json=results.json tagged-login.js +``` + +Filter the results for this custom tag: + +```bash +jq '. | select(.type == "Point" and .metric == "http_req_duration" and .data.tags."my-custom-tag" == "auth-api")' results.json +``` + +## Organize requests in groups + +You can also organize your test logic into [Groups](https://grafana.com/docs/k6//using-k6/tags-and-groups#groups). Test logic inside a `group` tags all requests and metrics within its block. +Groups can help you organize the test as a series of logical transactions or blocks. + +### Context: a new test to group test logic + +Results filtering isn't very meaningful in a test that makes one request. +And the API test script is getting long. +To learn more about how to compare results and other k6 APIs, write a test for the following situation: + +> **A dummy example**: your development team wants to evaluate the performance of two user-facing flows. +> +> - visit an endpoint, then another one +> - A GET request to `https://test.k6.io/contacts.php` +> - A GET to `https://test.k6.io/` +> - play the coinflip game: +> - A POST request to `https://test.k6.io/flip_coin.php` with the query param `bet=heads` +> - Another POST to `https://test.k6.io/flip_coin.php` with the query param `bet=tails` + +Can you figure out how to [script the requests](https://grafana.com/docs/k6//using-k6/http-requests)? +If not, use the following script. Since this example simulates a human user rather than an API call, it has a sleep between each request. Run with `k6 run multiple-flows.js`. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { group, sleep } from 'k6'; + +//set URL as variable +const baseUrl = 'https://test.k6.io'; + +export default function () { + // visit contacts + http.get(`${baseUrl}/contacts.php`); + sleep(1); + // return to the home page + http.get(`${baseUrl}/`); + sleep(1); + + //play coinflip game + http.get(`${baseUrl}/flip_coin.php?bet=heads`); + sleep(1); + http.get(`${baseUrl}/flip_coin.php?bet=tails`); + sleep(1); +} +``` + +{{< /code >}} + +### Add Group functions + +Wrap the two endpoints in different groups. +Name one group `Contacts flow` and another `Coinflip game`. + +{{< code >}} + +```javascript +//import necessary modules +import http from 'k6/http'; +import { group, sleep } from 'k6'; + +//set baseURL +const baseUrl = 'https://test.k6.io'; + +export default function () { + // visit some endpoints in one group + group('Contacts flow', function () { + http.get(`${baseUrl}/contacts.php`); + sleep(1); + // return to the home page + http.get(`${baseUrl}/`); + sleep(1); + }); + + // Coinflip players in another group + group('Coinflip game', function () { + http.get(`${baseUrl}/flip_coin.php?bet=heads`); + sleep(1); + http.get(`${baseUrl}/flip_coin.php?bet=tails`); + sleep(1); + }); +} +``` + +{{< /code >}} + +### Run and filter + +Inspect the results for only the `Coinflip game` group. +To do so: + +1. Save the preceding script as `multiple-flows.js`. +1. Run the script with the command: + +```bash +k6 run multiple-flows.js --out json=results.json --iterations 10 +``` + +1. Inspect the results with `jq`. Group names have a `::` prefix. + +```bash +jq '. | select(.data.tags.group == "::Coinflip game")' results.json +``` + +## Add a custom metric + +As you have seen in the output, all k6 tests emit metrics. +However, if the built-in metrics aren't enough, you can [create custom metrics](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics). +A common use case is to collect metrics of a particular scope of your test. + +As an example, create a metric that collects latency results for each group: + +1. Import [`Trend`](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend) from the k6 metrics module. +1. Create two duration trend metric functions. +1. In each group, add the `duration` time to the trend for requests to `contacts` and the `coin_flip` endpoints. + +{{< code >}} + +```javascript +//import necessary modules +import http from 'k6/http'; +import { group, sleep } from 'k6'; +import { Trend } from 'k6/metrics'; + +//set baseURL +const baseUrl = 'https://test.k6.io'; + +// Create custom trend metrics +const contactsLatency = new Trend('contacts_duration'); +const coinflipLatency = new Trend('coinflip_duration'); + +export default function () { + // Put visits to contact page in one group + let res; + group('Contacts flow', function () { + // save response as variable + res = http.get(`${baseUrl}/contacts.php`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + + res = http.get(`${baseUrl}/`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + }); + + // Coinflip players in another group + + group('Coinflip game', function () { + // save response as variable + let res = http.get(`${baseUrl}/flip_coin.php?bet=heads`); + // add duration property to metric + coinflipLatency.add(res.timings.duration); + sleep(1); + + res = http.get(`${baseUrl}/flip_coin.php?bet=tails`); + // add duration property to metric + coinflipLatency.add(res.timings.duration); + sleep(1); + }); +} +``` + +{{< /code >}} + +Run the test with small number of iterations and output the results to `results.json`. + +```bash +k6 run multiple-flows.js --out json=results.json --iterations 10 +``` + +Look for the custom trend metrics in the end-of-test console summary. + +```bash +coinflip_duration..............: avg=119.6438 min=116.481 med=118.4755 max=135.498 p(90)=121.8459 p(95)=123.89565 +contacts_duration..............: avg=125.76985 min=116.973 med=120.6735 max=200.507 p(90)=127.9271 p(95)=153.87245 +``` + +You can also query custom metric results from the JSON results. For example, to get the aggregated results as. + +{{< code >}} + +```avg +jq '. | select(.type == "Point" and .metric == "coinflip_duration") | .data.value' results.json | jq -s 'add/length' +``` + +```min +jq '. | select(.type == "Point" and .metric == "coinflip_duration") | .data.value' results.json | jq -s min +``` + +```max +jq '. | select(.type == "Point" and .metric == "coinflip_duration") | .data.value' results.json | jq -s max +``` + +{{< /code >}} + +## Next steps + +In this tutorial, you looked at granular output and filtered by built-in and custom tags. +Then you made a new script with groups. +Finally, you added a new metric for each group. +A next step would be to create a [Custom end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test/custom-summary) or to [stream the results to a database](https://grafana.com/docs/k6//results-output/real-time#service). + +For ongoing operations, you can modularize your logic and configuration. +That's the subject of the [next step of this tutorial](https://grafana.com/docs/k6//examples/get-started-with-k6/reuse-and-re-run-tests). diff --git a/docs/sources/v0.50.x/examples/get-started-with-k6/reuse-and-re-run-tests.md b/docs/sources/v0.50.x/examples/get-started-with-k6/reuse-and-re-run-tests.md new file mode 100644 index 000000000..2b3b8506b --- /dev/null +++ b/docs/sources/v0.50.x/examples/get-started-with-k6/reuse-and-re-run-tests.md @@ -0,0 +1,418 @@ +--- +title: Reuse and re-run tests +description: Modularize your k6 test logic and workload configuration. +weight: 400 +--- + +# Reuse and re-run tests + +In the previous tutorials, you designed k6 scripts to assert performance and make comparing results easy. + +In this tutorial, learn how to: + +- Modularize test scripts into reusable components +- Dynamically configure scripts with environment variables + +## Example script + +For fun, let's combine the scripts from the previous tutorials. +Use the logic of the `multiple-flows.js` test with the thresholds and scenario of the `api-test.js` (feel free to add more checks, requests, groups, and so on). +Take note of the features of this script: + +- The `default` function has two groups, `Contacts flow`, and `Coinflip game` +- The `options` object has two properties, `thresholds` and `scenarios` + +In the following sections, learn how to split these components into separate files, and combine them dynamically at run time. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { group, sleep } from 'k6'; +import { Trend } from 'k6/metrics'; + +//define configuration +export const options = { + scenarios: { + //arbitrary name of scenario: + breaking: { + executor: 'ramping-vus', + stages: [ + { duration: '10s', target: 20 }, + { duration: '50s', target: 20 }, + { duration: '50s', target: 40 }, + { duration: '50s', target: 60 }, + { duration: '50s', target: 80 }, + { duration: '50s', target: 100 }, + { duration: '50s', target: 120 }, + { duration: '50s', target: 140 }, + //.... + ], + }, + }, + //define thresholds + thresholds: { + http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }], // availability threshold for error rate + http_req_duration: ['p(99)<1000'], // Latency threshold for percentile + }, +}; + +//set baseURL +const baseUrl = 'https://test.k6.io'; + +// Create custom trends +const contactsLatency = new Trend('contacts_duration'); +const coinflipLatency = new Trend('coinflip_duration'); + +export default function () { + // Put visits to contact page in one group + let res; + group('Contacts flow', function () { + // save response as variable + res = http.get(`${baseUrl}/contacts.php`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + + res = http.get(`${baseUrl}/`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + }); + + // Coinflip players in another group + + group('Coinflip game', function () { + // save response as variable + let res = http.get(`${baseUrl}/flip_coin.php?bet=heads`); + // add duration property to metric + coinflipLatency.add(res.timings.duration); + sleep(1); + + res = http.get(`${baseUrl}/flip_coin.php?bet=tails`); + // add duration property to metric + coinflipLatency.add(res.timings.duration); + sleep(1); + }); +} +``` + +{{< /code >}} + +## Modularize logic + +With [modules](https://grafana.com/docs/k6//using-k6/modules), you can use logic and variables from other files. +Use modules to extract the functions to their own files. + +To do so, follow these steps: + +1. Copy the previous script (`whole-tutorial.js`) and save it as `main.js`. +1. Extract the `Contacts flow` group function from `main.js` script file and paste it into a new file called `contacts.js` + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { group, sleep } from 'k6'; + +export function contacts() { + const baseUrl = 'https://test.k6.io'; + const contactsLatency = []; + + group('Contacts flow', function () { + // save response as variable + let res = http.get(`${baseUrl}/contacts.php`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + + res = http.get(`${baseUrl}/`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + }); +} +``` + +{{< /code >}} + +As is, this script won't work, since it has undeclared functions and variables. + +1. Add the necessary imports and variables. This script uses the `group`, `sleep`, and `http` functions or libraries. It also has a custom metric. Since this metric is specific to the group, you can add it `contacts.js`. + +1. Finally, pass `baseUrl` as a parameter of the `contacts` function. + + {{< code >}} + + ```javascript + import http from 'k6/http'; + import { Trend } from 'k6/metrics'; + import { group, sleep } from 'k6'; + + const contactsLatency = new Trend('contact_duration'); + + export function contacts(baseUrl) { + group('Contacts flow', function () { + // save response as variable + let res = http.get(`${baseUrl}/contacts.php`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + + res = http.get(`${baseUrl}/`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + }); + } + ``` + + {{< /code >}} + +1. Repeat the process with the `coinflip` group in a file called `coinflip.js`. + Use the tabs to see the final three files should (`options` moved to the bottom of `main.js` for better readability). + + {{< code >}} + + ```main + import { contacts } from './contacts.js'; + import { coinflip } from './coinflip.js'; + + const baseUrl = 'https://test.k6.io'; + + export default function () { + // Put visits to contact page in one group + contacts(baseUrl); + // Coinflip players in another group + coinflip(baseUrl); + } + + //define configuration + export const options = { + scenarios: { + //arbitrary name of scenario: + breaking: { + executor: 'ramping-vus', + stages: [ + { duration: '10s', target: 20 }, + { duration: '50s', target: 20 }, + { duration: '50s', target: 40 }, + { duration: '50s', target: 60 }, + { duration: '50s', target: 80 }, + { duration: '50s', target: 100 }, + { duration: '50s', target: 120 }, + { duration: '50s', target: 140 }, + //.... + ], + }, + }, + //define thresholds + thresholds: { + http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }], // availability threshold for error rate + http_req_duration: ['p(99)<1000'], // Latency threshold for percentile + }, + }; + ``` + + ```contacts + import http from 'k6/http'; + import { Trend } from 'k6/metrics'; + import { group, sleep } from 'k6'; + + const contactsLatency = new Trend('contact_duration'); + + export function contacts(baseUrl) { + group('Contacts flow', function () { + // save response as variable + let res = http.get(`${baseUrl}/contacts.php`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + + res = http.get(`${baseUrl}/`); + // add duration property to metric + contactsLatency.add(res.timings.duration); + sleep(1); + }); + } + ``` + + ```coinflip + import http from 'k6/http'; + import { Trend } from 'k6/metrics'; + import { group, sleep } from 'k6'; + + const coinflipLatency = new Trend('coinflip_duration'); + + export function coinflip(baseUrl) { + group('Coinflip game', function () { + // save response as variable + let res = http.get(`${baseUrl}/flip_coin.php?bet=heads`); + // add duration property to metric + coinflipLatency.add(res.timings.duration); + sleep(1); + // mutate for new request + res = http.get(`${baseUrl}/flip_coin.php?bet=tails`); + // add duration property to metric + coinflipLatency.add(res.timings.duration); + sleep(1); + }); + } + ``` + + {{< /code >}} + + Run the test: + + ```bash + # setting the workload to 10 iterations to limit run time + k6 run main.js --iterations 10 + ``` + + The results should be very similar to running the script in a combined file, since these are the same test. + +## Modularize workload + +Now that the iteration code is totally modularized, you might modularize your `options`, too. + +The following example creates a module `config.js` to export the threshold and workload settings. + +{{< code >}} + +```main +import { coinflip } from './coinflip.js'; +import { contacts } from './contacts.js'; +import { thresholdsSettings, breakingWorkload } from './config.js'; + +export const options = { + scenarios: { breaking: breakingWorkload }, + thresholds: thresholdsSettings, +}; + +const baseUrl = 'https://test.k6.io'; + +export default function () { + contacts(baseUrl); + coinflip(baseUrl); +} +``` + +```config +export const thresholdsSettings = { + http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }], + http_req_duration: ['p(99)<1000'], +}; + +export const breakingWorkload = { + executor: 'ramping-vus', + stages: [ + { duration: '10s', target: 20 }, + { duration: '50s', target: 20 }, + { duration: '50s', target: 40 }, + { duration: '50s', target: 60 }, + { duration: '50s', target: 80 }, + { duration: '50s', target: 100 }, + { duration: '50s', target: 120 }, + { duration: '50s', target: 140 }, + //.... + ], +}; +``` + +{{< /code >}} + +Notice the length of this final script and compare it with the script at the beginning of this page. +Though the final execution is the same, it's half the size and more readable. + +Besides shortness, this modularity lets you compose scripts from many parts, or dynamically configure scripts at run time. + +## Mix and match logic + +With modularized configuration and logic, you can mix and match logic. +An easy way to configure this is through [environment variables](https://grafana.com/docs/k6//using-k6/environment-variables). + +Change `main.js` and `config.js` so that it: + +- _By default_ runs a smoke test with 5 iterations +- With the right environment variable value, runs a breaking test + +To do this, follow these steps: + +1. Add the workload settings for configuring the smoke test to `config.js`: + + {{< code >}} + + ```javascript + export const smokeWorkload = { + executor: 'shared-iterations', + iterations: 5, + vus: 1, + }; + + export const thresholdsSettings = { + http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }], + http_req_duration: ['p(99)<1000'], + }; + + export const breakingWorkload = { + executor: 'ramping-vus', + stages: [ + { duration: '10s', target: 20 }, + { duration: '50s', target: 20 }, + { duration: '50s', target: 40 }, + { duration: '50s', target: 60 }, + { duration: '50s', target: 80 }, + { duration: '50s', target: 100 }, + { duration: '50s', target: 120 }, + { duration: '50s', target: 140 }, + //.... + ], + }; + ``` + + {{< /code >}} + +1. Edit `main.js` to choose the workload settings depending on the `WORKLOAD` environment variable. For example: + + {{< code >}} + + ```javascript + import { coinflip } from './coinflip.js'; + import { contacts } from './contacts.js'; + import { thresholdsSettings, breakingWorkload, smokeWorkload } from './config.js'; + + export const options = { + scenarios: { + my_scenario: __ENV.WORKLOAD === 'breaking' ? breakingWorkload : smokeWorkload, + }, + thresholds: thresholdsSettings, + }; + + const baseUrl = 'https://test.k6.io'; + + export default function () { + contacts(baseUrl); + coinflip(baseUrl); + } + ``` + + {{< /code >}} + +1. Run the script with and without the `-e` flag. + + - What happens when you run `k6 run main.js`? + - What about `k6 run main.js -e WORKLOAD=breaking`? + +This was a simple example to showcase how you can modularize a test. +As your test suite grows and more people are involved in performance testing, your modularization strategy becomes essential to building and maintaining an efficient testing suite. + +## Next steps + +Now you've seen examples to write tests, assert for performance, filter results, and modularize scripts. +Notice how the tests progress in complexity: from single endpoints to holistic tests, from small to large loads, and from single tests to reusable modules. These progressions are typical in testing, with the next step being to automate. It might be impractical to automate a tutorial, but if you are interested, +read the [Automated performance testing](https://grafana.com/docs/k6//testing-guides/automated-performance-testing) guide. + +More likely, you want to learn more about k6. The [k6-learn repository](https://github.com/grafana/k6-learn) has more details to practice. +Or, you can read and explore the [testing guides](https://grafana.com/docs/k6//testing-guides) and try to build out your testing strategy. + +Happy testing! diff --git a/docs/sources/v0.50.x/examples/get-started-with-k6/test-for-functional-behavior.md b/docs/sources/v0.50.x/examples/get-started-with-k6/test-for-functional-behavior.md new file mode 100644 index 000000000..1968592a5 --- /dev/null +++ b/docs/sources/v0.50.x/examples/get-started-with-k6/test-for-functional-behavior.md @@ -0,0 +1,161 @@ +--- +title: Test for functional behavior +description: Use k6 to write requests and assert that they respond correctly +weight: 100 +--- + +# Test for functional behavior + +In this tutorial, learn how to write a test that does the following: + +- Sends a POST request to the new endpoint +- Creates a check for the response status + +## Script the Request + +The first thing to do is to add logic for the endpoint. +To do that, you need to make an [HTTP request](https://grafana.com/docs/k6//using-k6/http-requests): + +1. Import the HTTP module. +2. Create a payload to authenticate the user. +3. Use the [`http.post`](https://grafana.com/docs/k6//javascript-api/k6-http/post) method to send your request with the payload to an endpoint. + +To test, copy this file and save it as `api-test.js`. + +{{< code >}} + +```javascript +// import necessary module +import http from 'k6/http'; + +export default function () { + // define URL and payload + const url = 'https://test-api.k6.io/auth/basic/login/'; + const payload = JSON.stringify({ + username: 'test_case', + password: '1234', + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + }; + + // send a post request and save response as a variable + const res = http.post(url, payload, params); +} +``` + +{{< /code >}} + +Run the script using the `k6 run` command: + +```bash +k6 run api-test.js +``` + +After the test finishes, k6 reports the [default result summary](https://grafana.com/docs/k6//results-output/end-of-test#the-default-summary). + +```bash + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: api-test.js + output: - + ... +``` + +As an optional step, you can log the response body to the console to make sure you're getting the right response. + +{{< code >}} + +```javascript +import { http } from 'k6/http'; + +export default function () { + // ... + + const res = http.post('https://test-api.k6.io', JSON.stringify({ foo: 'bar' }), { + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Log the request body + console.log(res.body); +} +``` + +{{< /code >}} + +## Add response checks + +Once you're sure the request is well-formed, add a [check](https://grafana.com/docs/k6//using-k6/checks) that validates whether the system responds with the expected status code. + +1. Update your script so it has the following check function. + +{{< code >}} + +```javascript +// Import necessary modules +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + // define URL and request body + const url = 'https://test-api.k6.io/auth/basic/login/'; + const payload = JSON.stringify({ + username: 'test_case', + password: '1234', + }); + const params = { + headers: { + 'Content-Type': 'application/json', + }, + }; + + // send a post request and save response as a variable + const res = http.post(url, payload, params); + + // check that response is 200 + check(res, { + 'response code was 200': (res) => res.status == 200, + }); +} +``` + +{{< /code >}} + +1. Run the script again. + +```bash +k6 run api-test.js +``` + +1. Inspect the result output for your check. + It should look something like this. + + ``` + ✓ response code was 200 + ``` + +{{% admonition type="note" %}} + +Under larger loads, this check will fail in some iterations. +**Failed checks do not stop tests.** + +Rather, k6 tracks the success rate and presents it in your [end of test](https://grafana.com/docs/k6//results-output/end-of-test) summary. + +{{% /admonition %}} + +## Next steps + +In this tutorial, you've used k6 to make a POST request and check that it responds with a `200` status. + +However, these tests make only one request, which doesn't say much about how the system will respond under load. +For that, you need to [test under load](https://grafana.com/docs/k6//examples/get-started-with-k6/test-for-performance). diff --git a/docs/sources/v0.50.x/examples/get-started-with-k6/test-for-performance.md b/docs/sources/v0.50.x/examples/get-started-with-k6/test-for-performance.md new file mode 100644 index 000000000..c4519fea1 --- /dev/null +++ b/docs/sources/v0.50.x/examples/get-started-with-k6/test-for-performance.md @@ -0,0 +1,299 @@ +--- +title: Test for performance +description: Write thresholds to evaluate performance criteria, then increase load to see how the system performs. +weight: 200 +--- + +# Test for performance + +In the previous section, you made a working script to test an endpoint functionality. +The next step is to test how this system responds under load. +This requires setting up a few [`options`](https://grafana.com/docs/k6//using-k6/k6-options) to configure the parts of the test that don't deal with test logic. + +In this tutorial, learn how to: + +- Use [thresholds](https://grafana.com/docs/k6//using-k6/thresholds) to assert for performance criteria +- Configure load increases through [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) + +These examples build on the script from the [previous section](https://grafana.com/docs/k6//examples/get-started-with-k6/test-for-functional-behavior). + +## Context: meet service-level objectives + +To assess the login endpoint's performance, your team may have defined [service level objectives](https://sre.google/sre-book/service-level-objectives/) (SLOs). For example: + +- 99% of requests should be successful +- 99% of requests should have a latency of 1000ms or less + +The service must meet these SLOs under different types of usual traffic. + +## Assert for performance with thresholds + +To codify the SLOs, add [_thresholds_](https://grafana.com/docs/k6//using-k6/thresholds) to test that your system performs to its goal criteria. + +Thresholds are set in the [`options`](https://grafana.com/docs/k6//using-k6/k6-options) object. + +```javascript +export const options = { + // define thresholds + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + http_req_duration: ['p(99)<1000'], // 99% of requests should be below 1s + }, +}; +``` + +Add this `options` object with thresholds to your script `api-test.js`. + +{{< code >}} + +```javascript +// import necessary modules +import { check } from 'k6'; +import http from 'k6/http'; + +// define configuration +export const options = { + // define thresholds + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + http_req_duration: ['p(99)<1000'], // 99% of requests should be below 1s + }, +}; + +export default function () { + // define URL and request body + const url = 'https://test-api.k6.io/auth/basic/login/'; + const payload = JSON.stringify({ + username: 'test_case', + password: '1234', + }); + const params = { + headers: { + 'Content-Type': 'application/json', + }, + }; + + // send a post request and save response as a variable + const res = http.post(url, payload, params); + + // check that response is 200 + check(res, { + 'response code was 200': (res) => res.status == 200, + }); +} +``` + +{{< /code >}} + +Run the test. + +```bash +k6 run api-test.js +``` + +Inspect the console output to determine whether performance crossed a threshold. + +``` +✓ http_req_duration..............: avg=66.14ms min=0s med=0s max=198.42ms p(90)=158.73ms p(95)=178.58ms + { expected_response:true }...: avg=198.42ms min=198.42ms med=198.42ms max=198.42ms p(90)=198.42ms p(95)=198.42ms +✗ http_req_failed................: 0.00% ✓ 0 ✗ 1 +``` + +The ✓ and ✗ symbols indicate whether the performance thresholds passed or failed. + +## Test performance under increasing load + +Now your script has logic to simulate user behavior, and assertions for functionality (checks) and performance (thresholds). + +It's time to increase the load to see how it performs. +To increase the load, use the scenarios property. +Scenarios schedule load according to the number of VUs, number of iterations, VUs, or by iteration rate. + +### Run a smoke test + +Start small. Run a [smoke test](https://grafana.com/docs/k6//testing-guides/test-types/smoke-testing 'a small test to confirm the script works properly') to check that your script can handle a minimal load. + +To do so, use the [`--iterations`](https://grafana.com/docs/k6//using-k6/k6-options/reference#iterations) flag with an argument of 10 or fewer. + +```bash +k6 run --iterations 10 api-test.js +``` + +If the service can't receive 10 iterations, the system has some serious performance issues to debug. +Good thing you ran the test early! + +### Run a test against an average load + +Generally, traffic doesn't arrive all at once. +Rather, it gradually increases to a peak load. +To simulate this, testers increase the load in _stages_. + +Add the following `scenario` property to your `options` object and rerun the test. + +```javascript +export const options = { + // define thresholds + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + http_req_duration: ['p(99)<1000'], // 99% of requests should be below 1s + }, + // define scenarios + scenarios: { + // arbitrary name of scenario + average_load: { + executor: 'ramping-vus', + stages: [ + // ramp up to average load of 20 virtual users + { duration: '10s', target: 20 }, + // maintain load + { duration: '50s', target: 20 }, + // ramp down to zero + { duration: '5s', target: 0 }, + ], + }, + }, +}; +``` + +Since this is a learning environment, the stages are still quite short. +Where the smoke test defined the load in terms of iterations, this configuration uses the [`ramping-vus` executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus) to express load through virtual users and duration. + +Run the test with no command-line flags: + +```bash +k6 run api-test.js +``` + +The load is small, so the server should perform within thresholds. +However, this test server may be under load by many k6 learners, so the results are unpredictable. + +{{% admonition type="note" %}} + +At this point, it'd be nice to have a graphical interface to visualize metrics as they occur. +k6 has many output formats, which can serve as inputs for many visualization tools, both open source and commercial. +For ideas, read [Ways to visualize k6 results](https://k6.io/blog/ways-to-visualize-k6-results). + +{{% /admonition %}} + +### Ramp up until threshold fails + +Finally, run a [breakpoint test](https://grafana.com/docs/k6//testing-guides/test-types/breakpoint-testing), where you probe the system's limits. +In this case, run the test until the availability (error rate) threshold is crossed. + +To do this: + +1. Add the `abortOnFail` property to `http_req_failed`. + +```javascript +const options = { + thresholds: { + http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }], // http errors should be less than 1%, otherwise abort the test + }, +}; +``` + +1. Update the `scenarios` property to ramp the test up until it fails. + +```javascript +export const options = { + thresholds: { + http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }], + http_req_duration: ['p(99)<1000'], + }, + scenarios: { + // define scenarios + breaking: { + executor: 'ramping-vus', + stages: [ + { duration: '10s', target: 20 }, + { duration: '50s', target: 20 }, + { duration: '50s', target: 40 }, + { duration: '50s', target: 60 }, + { duration: '50s', target: 80 }, + { duration: '50s', target: 100 }, + { duration: '50s', target: 120 }, + { duration: '50s', target: 140 }, + //.... + ], + }, + }, +}; +``` + +Here is the full script. + +{{< code >}} + +```javascript +// import necessary modules +import { check } from 'k6'; +import http from 'k6/http'; + +// define configuration +export const options = { + // define thresholds + thresholds: { + http_req_failed: [{ threshold: 'rate<0.01', abortOnFail: true }], // availability threshold for error rate + http_req_duration: ['p(99)<1000'], // Latency threshold for percentile + }, + // define scenarios + scenarios: { + breaking: { + executor: 'ramping-vus', + stages: [ + { duration: '10s', target: 20 }, + { duration: '50s', target: 20 }, + { duration: '50s', target: 40 }, + { duration: '50s', target: 60 }, + { duration: '50s', target: 80 }, + { duration: '50s', target: 100 }, + { duration: '50s', target: 120 }, + { duration: '50s', target: 140 }, + //.... + ], + }, + }, +}; + +export default function () { + // define URL and request body + const url = 'https://test-api.k6.io/auth/basic/login/'; + const payload = JSON.stringify({ + username: 'test_case', + password: '1234', + }); + const params = { + headers: { + 'Content-Type': 'application/json', + }, + }; + + // send a post request and save response as a variable + const res = http.post(url, payload, params); + + // check that response is 200 + check(res, { + 'response code was 200': (res) => res.status == 200, + }); +} +``` + +{{< /code >}} + +Run the test. + +```bash +k6 run api-test.js +``` + +Did the threshold fail? If not, add another stage with a higher target and try again. Repeat until the threshold aborts the test: + +```bash +ERRO[0010] thresholds on metrics 'http_req_duration, http_req_failed' were breached; at least one has abortOnFail enabled, stopping test prematurely +``` + +## Next steps + +In this tutorial, you used [thresholds](https://grafana.com/docs/k6//using-k6/thresholds) to assert performance and [Scenarios](https://grafana.com/docs/k6//using-k6/scenarios) to schedule different load patterns. To learn more about the usual load patterns and their goals, read [Load Test Types](https://grafana.com/docs/k6//testing-guides/test-types/) + +The [next step of this tutorial shows how to interpret test results](https://grafana.com/docs/k6//examples/get-started-with-k6/analyze-results). This involves filtering results and adding custom metrics. diff --git a/docs/sources/v0.50.x/examples/get-timings-for-an-http-metric.md b/docs/sources/v0.50.x/examples/get-timings-for-an-http-metric.md new file mode 100644 index 000000000..03a464583 --- /dev/null +++ b/docs/sources/v0.50.x/examples/get-timings-for-an-http-metric.md @@ -0,0 +1,47 @@ +--- +title: Get timings for an HTTP metric +description: How to calculate timings for an individual k6 metric +weight: 23 +--- + +# Get timings for an HTTP metric + +To access the timing information from an individual HTTP request, the [Response.timings](https://grafana.com/docs/k6//javascript-api/k6-http/response) object provides the time spent on the various phases in `ms`. +One use case of this is to use the timings in a [Custom metric](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics) to make a trend for a specific endpoint. + +The timings are as follows: + +- blocked: equals to `http_req_blocked`. +- connecting: equals to `http_req_connecting`. +- tls_handshaking: equals to `http_req_tls_handshaking`. +- sending: equals to `http_req_sending`. +- waiting: equals to `http_req_waiting`. +- receiving: equals to `http_req_receiving`. +- duration: equals to `http_req_duration`. + +This script gets the request duration timing for a specific GET request and logs it to the console. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const res = http.get('https://httpbin.test.k6.io'); + console.log('Response time was ' + String(res.timings.duration) + ' ms'); +} +``` + +{{< /code >}} + +The expected (partial) output looks like this: + +{{< code >}} + +```bash +$ k6 run script.js + + INFO[0001] Response time was 337.962473 ms source=console +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/html-forms.md b/docs/sources/v0.50.x/examples/html-forms.md new file mode 100644 index 000000000..e640c3b04 --- /dev/null +++ b/docs/sources/v0.50.x/examples/html-forms.md @@ -0,0 +1,39 @@ +--- +title: 'HTML Forms' +description: 'Scripting example on how to handle HTML forms in a k6 test.' +weight: 07 +--- + +# HTML Forms + +Scripting example on how to handle HTML forms. + +In many cases using the [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) API (jQuery API clone) to interact with HTML data is enough, but for some use cases, like with forms, we can make things easier providing a higher-level API like the [Response.submitForm( [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/response/response-submitform) API. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + // Request page containing a form + let res = http.get('https://httpbin.test.k6.io/forms/post'); + + // Now, submit form setting/overriding some fields of the form + res = res.submitForm({ + formSelector: 'form', + fields: { custname: 'test', extradata: 'test2' }, + }); + sleep(3); +} +``` + +{{< /code >}} + +**Relevant k6 APIs**: + +- [Response.submitForm([params])](https://grafana.com/docs/k6//javascript-api/k6-http/response/response-submitform) +- [Selection.find(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-find) + (the [jQuery Selector API](http://api.jquery.com/category/selectors/) docs are also a good + resource on what possible selector queries can be made) diff --git a/docs/sources/v0.50.x/examples/http-authentication.md b/docs/sources/v0.50.x/examples/http-authentication.md new file mode 100644 index 000000000..216f4731f --- /dev/null +++ b/docs/sources/v0.50.x/examples/http-authentication.md @@ -0,0 +1,183 @@ +--- +title: 'HTTP Authentication' +description: 'Scripting examples on how to use different authentication or authorization methods in your load test.' +weight: 02 +--- + +# HTTP Authentication + +Scripting examples on how to use different authentication or authorization methods in your load test. + +## Basic authentication + +{{< code >}} + +```javascript +import encoding from 'k6/encoding'; +import http from 'k6/http'; +import { check } from 'k6'; + +const username = 'user'; +const password = 'passwd'; + +export default function () { + const credentials = `${username}:${password}`; + + // Passing username and password as part of the URL will + // allow us to authenticate using HTTP Basic Auth. + const url = `https://${credentials}@httpbin.test.k6.io/basic-auth/${username}/${password}`; + + let res = http.get(url); + + // Verify response + check(res, { + 'status is 200': (r) => r.status === 200, + 'is authenticated': (r) => r.json().authenticated === true, + 'is correct user': (r) => r.json().user === username, + }); + + // Alternatively you can create the header yourself to authenticate + // using HTTP Basic Auth + const encodedCredentials = encoding.b64encode(credentials); + const options = { + headers: { + Authorization: `Basic ${encodedCredentials}`, + }, + }; + + res = http.get(`https://httpbin.test.k6.io/basic-auth/${username}/${password}`, options); + + // Verify response (checking the echoed data from the httpbin.test.k6.io + // basic auth test API endpoint) + check(res, { + 'status is 200': (r) => r.status === 200, + 'is authenticated': (r) => r.json().authenticated === true, + 'is correct user': (r) => r.json().user === username, + }); +} +``` + +{{< /code >}} + +## Digest authentication + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +const username = 'user'; +const password = 'passwd'; + +export default function () { + // Passing username and password as part of URL plus the auth option will + // authenticate using HTTP Digest authentication. + const credentials = `${username}:${password}`; + const res = http.get( + `https://${credentials}@httpbin.test.k6.io/digest-auth/auth/${username}/${password}`, + { + auth: 'digest', + } + ); + + // Verify response (checking the echoed data from the httpbin.test.k6.io digest auth + // test API endpoint) + check(res, { + 'status is 200': (r) => r.status === 200, + 'is authenticated': (r) => r.json().authenticated === true, + 'is correct user': (r) => r.json().user === username, + }); +} +``` + +{{< /code >}} + +## NTLM authentication + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const username = 'user'; +const password = 'passwd'; + +export default function () { + // Passing username and password as part of URL and then specifying + // "ntlm" as auth type will do the trick! + const credentials = `${username}:${password}`; + const res = http.get(`http://${credentials}@example.com/`, { auth: 'ntlm' }); +} +``` + +{{< /code >}} + +## AWS Signature v4 authentication with the [k6-jslib-aws](https://github.com/grafana/k6-jslib-aws) + +To authenticate requests to AWS APIs using [AWS Signature Version 4](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html), k6 offers the [k6-jslib-aws](https://github.com/grafana/k6-jslib-aws) JavaScript library, which provides a dedicated `SignatureV4` class. This class can produce authenticated requests to send to AWS APIs using the `http` k6 module. + +Here's an example script to demonstrate how to sign a request to fetch an object from an S3 bucket: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { AWSConfig, SignatureV4 } from 'https://jslib.k6.io/aws/0.11.0/signature.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + + /** + * Optional session token for temporary credentials. + */ + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +export default function () { + /** + * Create a signer instance with the AWS credentials. + * The signer will be used to sign the request. + */ + const signer = new SignatureV4({ + service: 's3', + region: awsConfig.region, + credentials: { + accessKeyId: awsConfig.accessKeyId, + secretAccessKey: awsConfig.secretAccessKey, + sessionToken: awsConfig.sessionToken, + }, + }); + + /** + * Use the signer to prepare a signed request. + * The signed request can then be used to send the request to the AWS API. + */ + const signedRequest = signer.sign( + { + method: 'GET', + protocol: 'https', + hostname: 'test-jslib-aws.s3.us-east-1.amazonaws.com', + path: '/bonjour.txt', + headers: {}, + uriEscapePath: false, + applyChecksum: false, + }, + { + signingDate: new Date(), + signingService: 's3', + signingRegion: 'us-east-1', + } + ); + + /** + * The `signedRequest` object contains the signed request URL and headers. + * We can use them to send the request to the AWS API. + */ + http.get(signedRequest.url, { headers: signedRequest.headers }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/http2.md b/docs/sources/v0.50.x/examples/http2.md new file mode 100644 index 000000000..cf45edb81 --- /dev/null +++ b/docs/sources/v0.50.x/examples/http2.md @@ -0,0 +1,28 @@ +--- +title: 'HTTP2' +description: 'Information on how to load test HTTP/2.' +weight: 12 +--- + +# HTTP2 + +If the target system indicates that a connection can be upgraded from HTTP/1.1 to HTTP/2, k6 will do so automatically. + +## Making HTTP/2 requests + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res = http.get('https://test-api.k6.io/'); + check(res, { + 'status is 200': (r) => r.status === 200, + 'protocol is HTTP/2': (r) => r.proto === 'HTTP/2.0', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/instant-load-increase.md b/docs/sources/v0.50.x/examples/instant-load-increase.md new file mode 100644 index 000000000..0e5fbabe8 --- /dev/null +++ b/docs/sources/v0.50.x/examples/instant-load-increase.md @@ -0,0 +1,59 @@ +--- +title: 'Instant load increase' +description: 'Scripting example on how to instantly increase the number of VUs or iterations and hold them for a period of time' +draft: 'false' +weight: 22 +--- + +# Instant load increase + +One of the common usages of load testing tools it's the so-called stepped arrival rate. + +In k6 we can achieve it with the following configuration. + +Here's an example on how to instantly increase the number of iterations and hold them for a period of time. + +{{< code >}} + +```javascript +export const options = { + scenarios: { + contacts: { + executor: 'ramping-arrival-rate', + preAllocatedVUs: 50, + timeUnit: '1s', + startRate: 50, + stages: [ + { target: 200, duration: '30s' }, // linearly go from 50 iters/s to 200 iters/s for 30s + { target: 500, duration: '0' }, // instantly jump to 500 iters/s + { target: 500, duration: '10m' }, // continue with 500 iters/s for 10 minutes + ], + }, + }, +}; +``` + +{{< /code >}} + +Here's an example on how to instantly increase the number of VUs and hold them for a period of time. + +{{< code >}} + +```javascript +export const options = { + scenarios: { + contacts: { + executor: 'ramping-vus', + preAllocatedVUs: 10, + startVUs: 3, + stages: [ + { target: 20, duration: '30s' }, // linearly go from 3 VUs to 20 VUs for 30s + { target: 100, duration: '0' }, // instantly jump to 100 VUs + { target: 100, duration: '10m' }, // continue with 100 VUs for 10 minutes + ], + }, + }, +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/oauth-authentication.md b/docs/sources/v0.50.x/examples/oauth-authentication.md new file mode 100644 index 000000000..adcd58d46 --- /dev/null +++ b/docs/sources/v0.50.x/examples/oauth-authentication.md @@ -0,0 +1,332 @@ +--- +title: 'OAuth Authentication' +description: 'Scripting examples on how to use OAuth authentication in your load test.' +weight: 03 +--- + +# OAuth Authentication + +Scripting examples on how to use OAuth authentication in your load test. + +## OAuth authentication + +The following examples take a set of arguments, shown in the function documentation, and returns the response body as JSON so that you can extract the token from. + +### Azure Active Directory + +{{< code >}} + +```javascript +import http from 'k6/http'; + +/** + * Authenticate using OAuth against Azure Active Directory + * @function + * @param {string} tenantId - Directory ID in Azure + * @param {string} clientId - Application ID in Azure + * @param {string} clientSecret - Can be obtained from https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app#create-a-client-secret + * @param {string} scope - Space-separated list of scopes (permissions) that are already given consent to by admin + * @param {string} resource - Either a resource ID (as string) or an object containing username and password + */ +export function authenticateUsingAzure(tenantId, clientId, clientSecret, scope, resource) { + let url; + const requestBody = { + client_id: clientId, + client_secret: clientSecret, + scope: scope, + }; + + if (typeof resource == 'string') { + url = `https://login.microsoftonline.com/${tenantId}/oauth2/token`; + requestBody['grant_type'] = 'client_credentials'; + requestBody['resource'] = resource; + } else if ( + typeof resource == 'object' && + resource.hasOwnProperty('username') && + resource.hasOwnProperty('password') + ) { + url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + requestBody['grant_type'] = 'password'; + requestBody['username'] = resource.username; + requestBody['password'] = resource.password; + } else { + throw 'resource should be either a string or an object containing username and password'; + } + + const response = http.post(url, requestBody); + + return response.json(); +} +``` + +{{< /code >}} + +### Azure B2C + +The following example shows how you can authenticate with Azure B2C using the [Client Credentials Flow](https://docs.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-reference-oauth-code#client-credentials-flow). + +This example is based on a JMeter example found at the [azure-ad-b2c/load-tests](https://github.com/azure-ad-b2c/load-tests) repository. + +To use this script, you need to: + +1. [Set up your own Azure B2C tenant](https://learn.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant) + - Copy the tenant name, it will be used in your test script. +1. [Register a web application](https://learn.microsoft.com/en-us/azure/active-directory-b2c/tutorial-register-applications?tabs=app-reg-ga) + - Register a single page application with the redirect URL of: https://jwt.ms. That's needed for the flow to receive a token. + - After the creation, you can get the Application (client) ID, and the Directory (tenant) ID. Copy both of them, they'll be used in your test script. +1. [Create a user flow so that you can sign up and create a user](https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows) + - Create a new user, and copy the username and password. They'll be used in the test script. + +You can find the settings in the B2C settings in the Azure portal if you need to refer to them later on. Make sure to fill out all the variables for the `B2CGraphSettings` object, as well as replace `USERNAME` and `PASSWORD` in `export default function`. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import crypto from 'k6/crypto'; +import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +const B2CGraphSettings = { + B2C: { + client_id: '', // Application ID in Azure + user_flow_name: '', + tenant_id: '', // Directory ID in Azure + tenant_name: '', + scope: 'openid', + redirect_url: 'https://jwt.ms', + }, +}; + +/** + * Authenticate using OAuth against Azure B2C + * @function + * @param {string} username - Username of the user to authenticate + * @param {string} password + * @return {string} id_token + */ +export function GetB2cIdToken(username, password) { + const state = GetState(); + SelfAsserted(state, username, password); + const code = CombinedSigninAndSignup(state); + return GetToken(code, state.codeVerifier); +} + +/** + * @typedef {object} b2cStateProperties + * @property {string} csrfToken + * @property {string} stateProperty + * @property {string} codeVerifier + * + */ + +/** + * Get the id token from Azure B2C + * @function + * @param {string} code + * @returns {string} id_token + */ +const GetToken = (code, codeVerifier) => { + const url = + `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com/${B2CGraphSettings.B2C.tenant_id}` + + `/oauth2/v2.0/token` + + `?p=${B2CGraphSettings.B2C.user_flow_name}` + + `&client_id=${B2CGraphSettings.B2C.client_id}` + + `&grant_type=authorization_code` + + `&scope=${B2CGraphSettings.B2C.scope}` + + `&code=${code}` + + `&redirect_uri=${B2CGraphSettings.B2C.redirect_url}` + + `&code_verifier=${codeVerifier}`; + + const response = http.post(url, '', { + tags: { + b2c_login: 'GetToken', + }, + }); + + return JSON.parse(response.body).id_token; +}; + +/** + * Signs in the user using the CombinedSigninAndSignup policy + * extraqct B2C code from response + * @function + * @param {b2cStateProperties} state + * @returns {string} code + */ +const CombinedSigninAndSignup = (state) => { + const url = + `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com/${B2CGraphSettings.B2C.tenant_name}.onmicrosoft.com` + + `/${B2CGraphSettings.B2C.user_flow_name}/api/CombinedSigninAndSignup/confirmed` + + `?csrf_token=${state.csrfToken}` + + `&rememberMe=false` + + `&tx=StateProperties=${state.stateProperty}` + + `&p=${B2CGraphSettings.B2C.user_flow_name}`; + + const response = http.get(url, '', { + tags: { + b2c_login: 'CombinedSigninAndSignup', + }, + }); + const codeRegex = '.*code=([^"]*)'; + return response.url.match(codeRegex)[1]; +}; + +/** + * Signs in the user using the SelfAsserted policy + * @function + * @param {b2cStateProperties} state + * @param {string} username + * @param {string} password + */ +const SelfAsserted = (state, username, password) => { + const url = + `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com/${B2CGraphSettings.B2C.tenant_id}` + + `/${B2CGraphSettings.B2C.user_flow_name}/SelfAsserted` + + `?tx=StateProperties=${state.stateProperty}` + + `&p=${B2CGraphSettings.B2C.user_flow_name}` + + `&request_type=RESPONSE` + + `&email=${username}` + + `&password=${password}`; + + const params = { + headers: { + 'X-CSRF-TOKEN': `${state.csrfToken}`, + }, + tags: { + b2c_login: 'SelfAsserted', + }, + }; + http.post(url, '', params); +}; + +/** + * Calls the B2C login page to get the state property + * @function + * @returns {b2cStateProperties} b2cState + */ +const GetState = () => { + const nonce = randomString(50); + const challenge = crypto.sha256(nonce.toString(), 'base64rawurl'); + + const url = + `https://${B2CGraphSettings.B2C.tenant_name}.b2clogin.com` + + `/${B2CGraphSettings.B2C.tenant_id}/oauth2/v2.0/authorize?` + + `p=${B2CGraphSettings.B2C.user_flow_name}` + + `&client_id=${B2CGraphSettings.B2C.client_id}` + + `&nonce=${nonce}` + + `&redirect_uri=${B2CGraphSettings.B2C.redirect_url}` + + `&scope=${B2CGraphSettings.B2C.scope}` + + `&response_type=code` + + `&prompt=login` + + `&code_challenge_method=S256` + + `&code_challenge=${challenge}` + + `&response_mode=fragment`; + + const response = http.get(url, '', { + tags: { + b2c_login: 'GetCookyAndState', + }, + }); + + const vuJar = http.cookieJar(); + const responseCookies = vuJar.cookiesForURL(response.url); + + const b2cState = {}; + b2cState.codeVerifier = nonce; + b2cState.csrfToken = responseCookies['x-ms-cpim-csrf'][0]; + b2cState.stateProperty = response.body.match('.*StateProperties=([^"]*)')[1]; + return b2cState; +}; + +/** + * Helper function to get the authorization header for a user + * @param {user} user + * @returns {object} httpsOptions + */ +export const GetAuthorizationHeaderForUser = (user) => { + const token = GetB2cIdToken(user.username, user.password); + + return { + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token, + }, + }; +}; + +export default function () { + const token = GetB2cIdToken('USERNAME', 'PASSWORD'); + console.log(token); +} +``` + +{{< /code >}} + +### Okta + +{{< code >}} + +```javascript +import http from 'k6/http'; +import encoding from 'k6/encoding'; + +/** + * Authenticate using OAuth against Okta + * @function + * @param {string} oktaDomain - Okta domain to authenticate against (e.g. 'k6.okta.com') + * @param {string} authServerId - Authentication server identifier (default is 'default') + * @param {string} clientId - Generated by Okta automatically + * @param {string} clientSecret - Generated by Okta automatically + * @param {string} scope - Space-separated list of scopes + * @param {string|object} resource - Either a resource ID (as string) or an object containing username and password + */ +export function authenticateUsingOkta( + oktaDomain, + authServerId, + clientId, + clientSecret, + scope, + resource +) { + if (authServerId === 'undefined' || authServerId == '') { + authServerId = 'default'; + } + const url = `https://${oktaDomain}/oauth2/${authServerId}/v1/token`; + const requestBody = { scope: scope }; + let response; + + if (typeof resource == 'string') { + requestBody['grant_type'] = 'client_credentials'; + + const encodedCredentials = encoding.b64encode(`${clientId}:${clientSecret}`); + const params = { + auth: 'basic', + headers: { + Authorization: `Basic ${encodedCredentials}`, + }, + }; + + response = http.post(url, requestBody, params); + } else if ( + typeof resource == 'object' && + resource.hasOwnProperty('username') && + resource.hasOwnProperty('password') + ) { + requestBody['grant_type'] = 'password'; + requestBody['username'] = resource.username; + requestBody['password'] = resource.password; + requestBody['client_id'] = clientId; + requestBody['client_secret'] = clientSecret; + + response = http.post(url, requestBody); + } else { + throw 'resource should be either a string or an object containing username and password'; + } + + return response.json(); +} +``` + +{{< /code >}} + +For a detailed example, please visit this article: [How to Load Test OAuth secured APIs with k6?](https://k6.io/blog/how-to-load-test-oauth-secured-apis-with-k6) diff --git a/docs/sources/v0.50.x/examples/parse-html.md b/docs/sources/v0.50.x/examples/parse-html.md new file mode 100644 index 000000000..17c8960c6 --- /dev/null +++ b/docs/sources/v0.50.x/examples/parse-html.md @@ -0,0 +1,63 @@ +--- +title: 'Parse HTML' +description: 'Scripting examples parsing HTML content.' +weight: 06 +--- + +# Parse HTML + +Examples parsing HTML content. Use the `k6/html` module for HTML parsing. + +| Name | Type | Description | +| ------------------------------------------------------------------------------------------- | -------- | ----------------------------------------------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | Class | A jQuery-like API for accessing HTML DOM elements. | +| [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | Class | An HTML DOM element as returned by the Selection API. | +| [parseHTML(src)](https://grafana.com/docs/k6//javascript-api/k6-html/parsehtml) | function | Parse an HTML string and populate a Selection object. | + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import http from 'k6/http'; + +export default function () { + const res = http.get('https://k6.io'); + const doc = parseHTML(res.body); // equivalent to res.html() + const pageTitle = doc.find('head title').text(); + const langAttr = doc.find('html').attr('lang'); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
Value term 1
+
Value term 2
+
+ `; + const sel = parseHTML(content).find('dl').children(); + + const el1 = sel.get(0); + const el2 = sel.get(1); + + console.log(el1.nodeName()); + console.log(el1.id()); + console.log(el1.textContent()); + + console.log(el2.nodeName()); + console.log(el2.id()); + console.log(el2.textContent()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/single-request.md b/docs/sources/v0.50.x/examples/single-request.md new file mode 100644 index 000000000..eed874701 --- /dev/null +++ b/docs/sources/v0.50.x/examples/single-request.md @@ -0,0 +1,24 @@ +--- +title: 'Single request' +description: 'Example of one HTTP GET request' +draft: 'false' +weight: 01 +--- + +# Single request + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + iterations: 1, +}; + +export default function () { + const response = http.get('https://test-api.k6.io/public/crocodiles/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/soap.md b/docs/sources/v0.50.x/examples/soap.md new file mode 100644 index 000000000..4554a2e8b --- /dev/null +++ b/docs/sources/v0.50.x/examples/soap.md @@ -0,0 +1,49 @@ +--- +title: 'SOAP' +description: 'Load Testing SOAP API.' +weight: 14 +--- + +# SOAP + +Although k6 doesn't have any built-in APIs for working with SOAP or XML data in general, you +can still easily load test a SOAP-based API by crafting SOAP messages and using the HTTP request APIs. + +## Making SOAP requests + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +const soapReqBody = ` + + + + UnitedStates + + +`; + +export default function () { + // When making a SOAP POST request we must not forget to set the content type to text/xml + const res = http.post( + 'http://www.holidaywebservice.com/HolidayService_v2/HolidayService2.asmx', + soapReqBody, + { + headers: { 'Content-Type': 'text/xml' }, + } + ); + + // Make sure the response is correct + check(res, { + 'status is 200': (r) => r.status === 200, + 'black friday is present': (r) => r.body.indexOf('BLACK-FRIDAY') !== -1, + }); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/tls.md b/docs/sources/v0.50.x/examples/tls.md new file mode 100644 index 000000000..4e97b345d --- /dev/null +++ b/docs/sources/v0.50.x/examples/tls.md @@ -0,0 +1,34 @@ +--- +title: 'Transport Layer Security (TLS)' +description: | + TLS is the mechanism through which encrypted connections can be established between clients and + servers on the web and through which data can flow with integrity intact. +weight: 15 +--- + +# Transport Layer Security (TLS) + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export const options = { + tlsCipherSuites: ['TLS_RSA_WITH_RC4_128_SHA', 'TLS_RSA_WITH_AES_128_GCM_SHA256'], + tlsVersion: { + min: 'ssl3.0', + max: 'tls1.2', + }, +}; + +export default function () { + const res = http.get('https://sha256.badssl.com'); + check(res, { + 'is TLSv1.2': (r) => r.tls_version === http.TLS_1_2, + 'is sha256 cipher suite': (r) => r.tls_cipher_suite === 'TLS_RSA_WITH_AES_128_GCM_SHA256', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/tracking-data-per.md b/docs/sources/v0.50.x/examples/tracking-data-per.md new file mode 100644 index 000000000..523a5c141 --- /dev/null +++ b/docs/sources/v0.50.x/examples/tracking-data-per.md @@ -0,0 +1,72 @@ +--- +title: 'Track transmitted data per URL' +slug: '/track-transmitted-data-per-url' +description: 'This example shows how to track data sent and received for a individual URL.' +weight: 20 +--- + +# Track transmitted data per URL + +By default, k6 collects automatically two [built-in metrics](https://grafana.com/docs/k6//using-k6/metrics#built-in-metrics) related to the transmitted data during the test execution: + +- `data_received`: the amount of received data. +- `data_sent`: the amount of data sent. + +However, the reported values of these metrics don't tag the particular request or URL. Therefore, you cannot know the amount of data transmitted for a specific request or URL. + +This example shows how to track data sent and received for an individual URL. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; +import { Counter } from 'k6/metrics'; + +// Two custom metrics to track data sent and received. We will tag data points added with the corresponding URL +// so we can filter these metrics down to see the data for individual URLs and set threshold across all or per-URL as well. +export const epDataSent = new Counter('endpoint_data_sent'); +export const epDataRecv = new Counter('endpoint_data_recv'); + +export const options = { + duration: '10s', + vus: 10, + thresholds: { + // We can setup thresholds on these custom metrics, "count" means bytes in this case. + 'endpoint_data_sent': ['count < 2048'], + + // The above threshold would look at all data points added to the custom metric. + // If we want to only consider data points for a particular URL/endpoint we can filter by URL. + 'endpoint_data_sent{url:https://test.k6.io/contacts.php}': ['count < 1024'], + 'endpoint_data_recv{url:https://test.k6.io/}': ['count < 2048'], // "count" means bytes in this case + }, +}; + +function sizeOfHeaders(hdrs) { + return Object.keys(hdrs).reduce((sum, key) => sum + key.length + hdrs[key].length, 0); +} + +function trackDataMetricsPerURL(res) { + // Add data points for sent and received data + epDataSent.add(sizeOfHeaders(res.request.headers) + res.request.body.length, { + url: res.url, + }); + epDataRecv.add(sizeOfHeaders(res.headers) + res.body.length, { + url: res.url, + }); +} + +export default function () { + let res; + + res = http.get('https://test.k6.io/'); + trackDataMetricsPerURL(res); + + res = http.get('https://test.k6.io/contacts.php'); + trackDataMetricsPerURL(res); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/examples/tutorials/_index.md b/docs/sources/v0.50.x/examples/tutorials/_index.md new file mode 100644 index 000000000..5fd269862 --- /dev/null +++ b/docs/sources/v0.50.x/examples/tutorials/_index.md @@ -0,0 +1,18 @@ +--- +title: 'Tutorials' +description: 'k6 Tutorials' +weight: 26 +--- + +# Tutorials + +- [Get started with k6](https://grafana.com/docs/k6//examples/get-started-with-k6) +- [8 basic tasks to learn k6](https://k6.io/blog/learning-js-through-load-testing/) +- [k6 Learn](https://github.com/grafana/k6-learn) +- [Swagger/OpenAPI integration](https://k6.io/blog/load-testing-your-api-with-swagger-openapi-and-k6) +- [Schedule k6 tests with cron](https://k6.io/blog/performance-monitoring-with-cron-and-k6) +- [Load test a GraphQL service](https://k6.io/blog/load-testing-graphql-with-k6) +- [Use TypeScript in k6 scripts](https://github.com/k6io/template-typescript) +- [Debug using a Web Proxy](https://k6.io/blog/k6-load-testing-debugging-using-a-web-proxy/) +- [Distributed k6 tests on K8s](https://k6.io/blog/running-distributed-tests-on-k8s/) +- [Create a k6 extension](https://k6.io/blog/extending-k6-with-xk6) diff --git a/docs/sources/v0.50.x/examples/url-query-parameters.md b/docs/sources/v0.50.x/examples/url-query-parameters.md new file mode 100644 index 000000000..479141204 --- /dev/null +++ b/docs/sources/v0.50.x/examples/url-query-parameters.md @@ -0,0 +1,91 @@ +--- +title: 'URLs with query parameters' +description: 'Scripting examples using URL and URLSearchParams modules.' +weight: 21 +--- + +# URLs with query parameters + +How to use **URL** and **URLSearchParams** imported from [jslib.k6.io](https://jslib.k6.io/) to construct URLs with/without query parameters. + +## URL + +{{< code >}} + +```javascript +import { URL } from 'https://jslib.k6.io/url/1.0.0/index.js'; +import http from 'k6/http'; + +export default function () { + const url = new URL('https://k6.io'); + + url.searchParams.append('utm_medium', 'organic'); + url.searchParams.append('utm_source', 'test'); + url.searchParams.append('multiple', ['foo', 'bar']); + + const res = http.get(url.toString()); + // https://k6.io?utm_medium=organic&utm_source=test&multiple=foo%2Cbar +} +``` + +{{< /code >}} + + + +| Name | Type | Description | +| --------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| URLSearchParams(init) | Constructor | `init` Optional: One of [USVString, sequence of pairs or record ] | +| toString() | Method | Returns a USVString containing the whole URL | +| hash | Property | A USVString containing a '#' followed by the fragment identifier of the URL. | +| host | Property | A USVString containing the host, that is the `hostname`, and then, if the port of the URL is nonempty, a ':', followed by the `port` of the URL. | +| hostname | Property | A USVString containing the [domain name](https://developer.mozilla.org/en-US/docs/Glossary/Domain_name) of the URL. | +| href | Property | Returns a USVString containing the whole URL. | +| origin | Property | Returns a USVString containing the origin of the URL, that is its scheme, its domain and its port. | +| password | Property | A USVString containing the password specified before the domain name. | +| pathname | Property | Is a USVString containing an initial '/' followed by the path of the URL, not including the query string or fragment. | +| port | Property | A USVString containing the port number of the URL. | +| protocol | Property | A USVString containing the protocol scheme of the URL, including the final ':'. | +| search | Property | A USVString indicating the URL's parameter string; if any parameters are provided, this string includes all of them, beginning with the leading ? character. | +| searchParams | Property | A [URLSearchParams](#urlsearchparams) object which can be used to access the individual query parameters found in search. | +| username | Property | A USVString containing the username specified before the domain name. | + + + +## URLSearchParams + +{{< code >}} + +```javascript +import { URLSearchParams } from 'https://jslib.k6.io/url/1.0.0/index.js'; +import http from 'k6/http'; + +export default function () { + const searchParams = new URLSearchParams([ + ['utm_medium', 'organic'], + ['utm_source', 'test'], + ['multiple', 'foo'], + ['multiple', 'bar'], + ]); + + const res = http.get(`${'https://k6.io'}?${searchParams.toString()}`); + // https://k6.io?utm_medium=organic&utm_source=test&multiple=foo&multiple=bar +} +``` + +{{< /code >}} + +| Name | Type | Description | +| --------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| URLSearchParams(init) | Constructor | `init` Optional: One of [USVString, sequence of pairs or record ] | +| toString() | Method | Returns a query string (questionmark is omitted). | +| append() | Method | Appends a specified key/value pair as a new search parameter. | +| delete() | Method | Deletes the given search parameter, and its associated value, from the list of all search parameters. | +| entries() | Method | Returns an iterator allowing iteration through all key/value pairs contained in this object. | +| forEach() | Method | Allows iteration through all values contained in this object via a callback function. `callback(value, key)`. | +| get() | Method | Returns the first value associated with the given search parameter. | +| getAll() | Method | Returns all the values associated with a given search parameter. | +| has() | Method | Returns a Boolean that indicates whether a parameter with the specified name exists. | +| keys() | Method | Returns an iterator allowing iteration through all keys of the key/value pairs contained in this object. | +| values() | Method | Returns an iterator allowing iteration through all values of the key/value pairs contained in this object. | +| set() | Method | Sets the value associated with a given search parameter to the given value. If there were several matching values, this method deletes the others. If the search parameter doesn't exist, this method creates it. | +| sort() | Method | Sorts all key/value pairs, if any, by their keys. | diff --git a/docs/sources/v0.50.x/examples/websockets.md b/docs/sources/v0.50.x/examples/websockets.md new file mode 100644 index 000000000..6db2b362d --- /dev/null +++ b/docs/sources/v0.50.x/examples/websockets.md @@ -0,0 +1,84 @@ +--- +title: 'WebSockets' +description: | + Scripting example on how to use WebSocket API in k6. +weight: 13 +--- + +# WebSockets + +Here's a load test for CrocoChat - a WebSocket chat API available on [https://test-api.k6.io/](https://test-api.k6.io/). + +Multiple VUs join a chat room and discuss various things for up to 1 minute, after which they disconnect. + +Each VU receives messages sent by all chat participants. + +{{< code >}} + +```javascript +import { randomString, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; +import ws from 'k6/ws'; +import { check, sleep } from 'k6'; + +const sessionDuration = randomIntBetween(10000, 60000); // user session between 10s and 1m +const chatRoomName = 'publicRoom'; // choose your chat room name + +export const options = { + vus: 10, + iterations: 10, +}; + +export default function () { + const url = `wss://test-api.k6.io/ws/crocochat/${chatRoomName}/`; + const params = { tags: { my_tag: 'my ws session' } }; + + const res = ws.connect(url, params, function (socket) { + socket.on('open', function open() { + console.log(`VU ${__VU}: connected`); + + socket.send(JSON.stringify({ event: 'SET_NAME', new_name: `Croc ${__VU}` })); + + socket.setInterval(function timeout() { + socket.send(JSON.stringify({ event: 'SAY', message: `I'm saying ${randomString(5)}` })); + }, randomIntBetween(2000, 8000)); // say something every 2-8seconds + }); + + socket.on('ping', function () { + console.log('PING!'); + }); + + socket.on('pong', function () { + console.log('PONG!'); + }); + + socket.on('close', function () { + console.log(`VU ${__VU}: disconnected`); + }); + + socket.on('message', function (message) { + const msg = JSON.parse(message); + if (msg.event === 'CHAT_MSG') { + console.log(`VU ${__VU} received: ${msg.user} says: ${msg.message}`); + } else if (msg.event === 'ERROR') { + console.error(`VU ${__VU} received:: ${msg.message}`); + } else { + console.log(`VU ${__VU} received unhandled message: ${msg.message}`); + } + }); + + socket.setTimeout(function () { + console.log(`VU ${__VU}: ${sessionDuration}ms passed, leaving the chat`); + socket.send(JSON.stringify({ event: 'LEAVE' })); + }, sessionDuration); + + socket.setTimeout(function () { + console.log(`Closing the socket forcefully 3s after graceful LEAVE`); + socket.close(); + }, sessionDuration + 3000); + }); + + check(res, { 'Connected successfully': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/extensions/_index.md b/docs/sources/v0.50.x/extensions/_index.md new file mode 100644 index 000000000..83e2fc7cb --- /dev/null +++ b/docs/sources/v0.50.x/extensions/_index.md @@ -0,0 +1,80 @@ +--- +title: Extensions +description: 'The k6 extension ecosystem enables developers and testers to extend k6 to cover use cases not supported natively in the core. Explore the endless possibilities of k6 and xk6.' +weight: 09 +--- + +# Extensions + +Expand the potential use cases for k6. + +## Quickstart + + + +## Custom k6 builds + +With k6 extensions, you can create custom k6 binaries to support your specific reliability-testing needs. + +Currently, k6 supports two ways to extend its native functionality: + +- **JavaScript extensions** extend the JavaScript APIs available to your test scripts. Add support for new network protocols, improve performance compared to equivalent JS libraries, or add features. + +- **Output extensions** send metrics to a custom file format or service. Add custom processing and dispatching. + +## xk6 makes custom binaries + +[xk6](https://github.com/grafana/xk6/) is command-line tool and framework written in Go. With xk6, you build custom k6 binaries that bundle one or more extensions written in Go. You have two options for creating k6 binaries: using [Go and xk6](https://grafana.com/docs/k6//extensions/build-k6-binary-using-go/) or [Docker](https://grafana.com/docs/k6//extensions/build-k6-binary-using-docker/): + +{{< code >}} + +```go-and-xk6 +xk6 build \ + --with github.com/grafana/xk6-sql@v0.0.1 \ + --with github.com/grafana/xk6-output-prometheus-remote +``` + +```docker-in-linux +docker run --rm -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" grafana/xk6 build \ + --with github.com/grafana/xk6-sql@v0.0.1 \ + --with github.com/grafana/xk6-output-prometheus-remote +``` + +{{< /code >}} + +
+ +## Extension use cases + +The extensions ecosystem provides endless possibilities to expand the functionality for your k6 testing. Some reasons you might want to extend k6 include the following: + +- **To add support for a new network protocol** + + For example, [xk6-amqp](https://github.com/grafana/xk6-amqp) gives access to resources using the Advanced Message Queueing Protocol (AMQP). With xk6-amqp, your scripts can create message queues and seed messages for tests that include systems like RabbitMQ or ActiveMQ (among others). + +- **To incorporate a client library for test dependency** + + Everyone wants to run their services in Kubernetes these days. With [xk6-kubernetes](https://github.com/grafana/xk6-kubernetes), your JavaScript tests can interface directly with Kubernetes resources using functionality typically restricted to kubectl. Prepare isolated Namespaces for each test run, or inject environment variables as ConfigMaps. + +- **To format and send metrics to the output of your choice** + + Suppose your company has consolidated its observability metrics into Prometheus. Use [xk6-output-prometheus-remote](https://github.com/grafana/xk6-output-prometheus-remote) to publish your k6 test metrics to Prometheus as well! + +- **To improve script performance and efficiency** + + Perhaps your company uses OpenTelemetry to trace service requests through layers of microservices. Using [xk6-distributed-tracing](https://github.com/grafana/xk6-distributed-tracing), you can update the http client to link your test requests as the origin for your traces—no need to add JavaScript code to supply the required trace headers. + +Next, [Explore the available extensions](https://grafana.com/docs/k6//extensions/explore) to see how you can expand your use of k6 right now. diff --git a/docs/sources/v0.50.x/extensions/build-k6-binary-using-docker.md b/docs/sources/v0.50.x/extensions/build-k6-binary-using-docker.md new file mode 100644 index 000000000..e3d6aefea --- /dev/null +++ b/docs/sources/v0.50.x/extensions/build-k6-binary-using-docker.md @@ -0,0 +1,180 @@ +--- +title: 'Build a k6 binary using Docker' +description: 'Guide to build a k6 binary with extensions using Docker.' +weight: 03 +--- + +# Build a k6 binary using Docker + +Using the [xk6 Docker image](https://hub.docker.com/r/grafana/xk6/) can simplify the process of creating a custom k6 binary. It avoids having to setup a local Go environment, and install xk6 manually. + +{{% admonition type="note" %}} + +This tutorial is about creating a custom k6 binary (using Docker). If you want to create a Docker image with a custom k6 binary, refer instead to [Using modules with Docker](https://grafana.com/docs/k6//using-k6/modules/#using-modules-with-docker). + +{{% /admonition %}} + +## Building your first extension + +For example, to build a custom k6 binary with the latest versions of k6 and the [`xk6-kafka`](https://github.com/mostafa/xk6-kafka) and [`xk6-output-influxdb`](https://github.com/grafana/xk6-output-influxdb) extensions, run one of the commands below, depending on your operating system: + +{{< code >}} + +```linux +docker run --rm -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" grafana/xk6 build \ + --with github.com/mostafa/xk6-kafka \ + --with github.com/grafana/xk6-output-influxdb +``` + +```mac +docker run --rm -e GOOS=darwin -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" \ + grafana/xk6 build \ + --with github.com/mostafa/xk6-kafka \ + --with github.com/grafana/xk6-output-influxdb +``` + +```windows-powershell +docker run --rm -e GOOS=windows -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" ` + grafana/xk6 build --output k6.exe ` + --with github.com/mostafa/xk6-kafka ` + --with github.com/grafana/xk6-output-influxdb +``` + +```windows +docker run --rm -e GOOS=windows -v "%cd%:/xk6" ^ + grafana/xk6 build --output k6.exe ^ + --with github.com/mostafa/xk6-kafka ^ + --with github.com/grafana/xk6-output-influxdb +``` + +{{< /code >}} + +This creates a `k6` (or `k6.exe`) binary in the current working directory. + +To build the binary with concrete versions, see the example below (k6 `v0.45.1`, xk6-kafka `v0.19.1`, and xk6-output-influxdb `v0.4.1`): + +{{< code >}} + +```linux +docker run --rm -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" grafana/xk6 build v0.45.1 \ + --with github.com/mostafa/xk6-kafka@v0.19.1 \ + --with github.com/grafana/xk6-output-influxdb@v0.4.1 +``` + +```mac +docker run --rm -e GOOS=darwin -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" \ + grafana/xk6 build v0.45.1 \ + --with github.com/mostafa/xk6-kafka@v0.19.1 \ + --with github.com/grafana/xk6-output-influxdb@v0.4.1 +``` + +```windows-powershell +docker run --rm -e GOOS=windows -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" ` + grafana/xk6 build v0.45.1 --output k6.exe ` + --with github.com/mostafa/xk6-kafka@v0.19.1 ` + --with github.com/grafana/xk6-output-influxdb@v0.4.1 +``` + +```windows +docker run --rm -e GOOS=windows -v "%cd%:/xk6" ^ + grafana/xk6 build v0.45.1 --output k6.exe ^ + --with github.com/mostafa/xk6-kafka@v0.19.1 ^ + --with github.com/grafana/xk6-output-influxdb@v0.4.1 +``` + +{{< /code >}} + +## Breaking down the command + +The example command line may look a bit intimidating at first, but let's focus on the first part, which pertains strictly to Docker: + +```bash +docker run --rm -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" +``` + +This tells Docker to run a new container from an image. + +- `--rm` means the container will be destroyed once your build is completed. +- `-u` specifies the user and group IDs of the account on the host machine. This is important for the `k6` file to have the same file permissions as the host user. +- `-v` is required to mount the current working directory inside the container, so that the `k6` binary can be written to it. + +For Windows and Mac, we additionally include the target system as an environment variable: + +```bash +-e GOOS= +``` + +The remainder is straight from the [xk6 documentation](https://github.com/grafana/xk6/#command-usage), with the exception that we use the `grafana/xk6` _image_ rather than a local installation of `xk6`: + +{{< code >}} + +```plain +grafana/xk6 build [] + [--output ] + [--with ...] + [--replace ...] + +Flags: + --output specifies the new binary name [default: 'k6'] + --replace enables override of dependencies for k6 and extensions [default: none] + --with the extension module to be included in the binary [default: none] +``` + +{{< /code >}} + +{{% admonition type="caution" %}} + +The use of `--replace` should be considered an advanced feature to be avoided unless required. + +{{% /admonition %}} + +Referring back to our executed command, note that: + +- We specify the version as `v0.43.1`. When you omit the version or specify `latest`, you build using the _latest_ source code for k6. + Consider using a stable [release version](https://github.com/grafana/k6/releases) as a best practice unless you genuinely want the _bleeding edge_. +- We specify a full GitHub URI for the extension repository with each `--with`. + If a version is not specified, the default is again the `latest`. + Check your extension repository for stable release versions, if available, to lock in your version as we've done with `xk6-kafka@v0.17.0` and `xk6-output-influxdb@v0.3.0`. +- For Windows, we used the `--output` option to name our result as `k6.exe`; if not specified, our new binary is `k6` within the current directory. + If you specify a directory, the new binary will be `k6` within _that_ directory. + If you specify a path to a non-existent file, e.g. `/tmp/k6-extended`, this will be the path and filename for the binary. + +Run `./k6 version` (or `k6.exe version`) to check that your build is based on the appropriate `k6` version and contains the desired extensions. For example: + +{{< code >}} + +```bash +$ ./k6 version +k6 v0.43.1 ((devel), go1.20.1, darwin/amd64) +Extensions: + github.com/grafana/xk6-output-influxdb v0.3.0, xk6-influxdb [output] + github.com/mostafa/xk6-kafka v0.17.0, k6/x/kafka [js] +``` + +{{< /code >}} + +## Running your extended binary + +Now that we have our newly built k6 binary, we can run scripts using the functionalities +of the bundled extensions. + +{{< code >}} + +```bash +./k6 run my-script.js +``` + +```batch +k6.exe run my-script.js +``` + +{{< /code >}} + +> Be sure to specify the binary just built in the current directory as `./k6`, or else +> Linux/Mac could execute another k6 binary on your system path. Windows shells will +> first search for the binary in the current directory by default. + +## Encountering issues? + +If you're having issues, search the [k6 Community Forum](https://community.grafana.com/c/grafana-k6/extensions/82). +Someone may have had the same issue in the past. diff --git a/docs/sources/v0.50.x/extensions/build-k6-binary-using-go.md b/docs/sources/v0.50.x/extensions/build-k6-binary-using-go.md new file mode 100644 index 000000000..fdc921309 --- /dev/null +++ b/docs/sources/v0.50.x/extensions/build-k6-binary-using-go.md @@ -0,0 +1,126 @@ +--- +title: 'Build a k6 binary using Go' +description: 'Guide to build a k6 binary that includes one or many extensions using xk6.' +weight: 02 +--- + +# Build a k6 binary using Go + +To use an extension that you found on the [Extension page](https://grafana.com/docs/k6//extensions/explore) or the [xk6 GitHub topic](https://github.com/topics/xk6), +you need to build a binary using Go. + +{{% admonition type="note" %}} + +Not interested in setting up a Go environment? You can [use Docker instead](https://grafana.com/docs/k6//extensions/build-k6-binary-using-docker/). + +{{% /admonition %}} + +## Before you start + +- Set up [Go](https://golang.org/doc/install) and [Git](https://git-scm.com/). +- Make sure that your `$PATH` environment variable is updated so that `go version` returns the correct version. + +## Installing xk6 + +Given the prerequisite Go setup, installing [xk6](https://github.com/grafana/xk6) itself requires only the following command: + +```bash +$ go install go.k6.io/xk6/cmd/xk6@latest +``` + +To confirm your installation, run `which xk6` on Linux and Mac, or `where xk6` on Windows. +Make sure the command returns a valid path. + +If not, check that you've correctly defined the`$GOPATH` environment variable and that `$GOPATH/bin` +is in your `$PATH`. See the [Go documentation](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable) +for details. + +## Building your first extension + +Once installed, building a k6 binary with one or more extensions can be done with the `xk6 build` +command as follows: + +```bash +$ xk6 build latest \ + --with github.com/grafana/xk6-sql@v0.0.1 \ + --with github.com/grafana/xk6-output-prometheus-remote +``` + +Once completed, the **current directory** contains your newly built `k6` binary with +the [`xk6-sql`](https://github.com/grafana/xk6-sql) and [`xk6-output-prometheus-remote`](https://github.com/grafana/xk6-output-prometheus-remote) +extensions. + +```bash +... [INFO] Build environment ready +... [INFO] Building k6 +... [INFO] Build complete: ./k6 + +xk6 has now produced a new k6 binary which may be different than the command on your system path! +Be sure to run './k6 run ' from the '...' directory. +``` + +## Breaking down the xk6 command + +From the [xk6 documentation](https://github.com/grafana/xk6/#command-usage), the command-line usage is as follows: + +```plain +xk6 build [] + [--output ] + [--with ...] + [--replace ...] + +Flags: + --output specifies the new binary name [default: 'k6'] + --replace enables override of dependencies for k6 and extensions [default: none] + --with the extension module to be included in the binary [default: none] +``` + +> The use of `--replace` should be considered an advanced feature to be avoided unless required. + +Referring back to our executed command, note that: + +- We specify the version as `latest`, which is also the default if we hadn't supplied + a version. `latest` means that we'll build using the _latest_ source code for k6. Consider using + a stable [release version](https://github.com/grafana/k6/releases) as a best practice unless + you genuinely want the _bleeding edge_. +- With each `--with`, we specified a full GitHub URI for the extension repository. If not specifying + a version, the default is `latest` once again. Check your extension repository for stable + release versions, if available, to lock in your version as we've done with `xk6-sql@v0.0.1`. +- We did not use the `--output` option; therefore, our new binary is `k6` within the current directory. + Had we used `--output k6-extended`, our binary would be named `k6-extended` within the current + directory. If a directory is specified, then the new binary would be `k6` within + _that_ directory. If a path to a non-existent file, e.g. `/tmp/k6-extended`, this will be the + path and filename for the binary. + +Running `./k6 version` should show your build is based upon the appropriate version. + +## Building from a local repository + +Suppose now you've cloned the `xk6-sql` repository and want the local version included in your +custom binary? From the cloned directory, we would then use: + +```bash +--with github.com/grafana/xk6-sql=. +``` + +Based upon the command usage described in the previous section, this tells xk6 to use +the _current directory_ as the _replacement_ for the _module_. + +## Running your extended binary + +Now that we have our newly built k6 binary, we can run scripts using the functionalities +of the bundled extensions. + +```bash +$ ./k6 run my-script.js +``` + +> Be sure to specify the binary just built in the current directory as `./k6`, or else +> Linux/Mac could execute another k6 binary on your system path. Windows shells will +> first search for the binary in the current directory by default. + + diff --git a/docs/sources/v0.50.x/extensions/create/_index.md b/docs/sources/v0.50.x/extensions/create/_index.md new file mode 100644 index 000000000..717c37bff --- /dev/null +++ b/docs/sources/v0.50.x/extensions/create/_index.md @@ -0,0 +1,43 @@ +--- +title: 'Create a k6 extension' +menuTitle: 'Create an extension' +description: 'Creating k6 extensions does not have to be a daunting task, but there are some prerequisites to succeed.' +weight: 04 +--- + +# Create a k6 extension + +If you find a gap in your testing process that no k6 extension can fix, +consider building your own extension. + +These tutorials show you how to create custom JavaScript and output extensions. + +- [Create a JavaScript extension](https://grafana.com/docs/k6//extensions/create/javascript-extensions) to extend the JavaScript functionality of your script or add support for a new network protocol to test. +- [Create an Output extension](https://grafana.com/docs/k6//extensions/create/output-extensions) to process the metrics emitted by k6 or publish them to unsupported backend stores. + +## Necessary knowledge + +Anyone who can use the command line to edit files and install software should be able to follow along. +But, if you want to create an extension for more than the purposes of demonstration, +there's some background knowledge you should have: + +- You should be familiar with both Go(lang), JavaScript, and their tooling +- You should understand how the [_Go-to-JavaScript_](https://grafana.com/docs/k6//extensions/explanations/go-js-bridge) bridge works within k6 + +{{% admonition type="note" %}} + +If you maintain a public xk6 repository and wish to have your extension listed in our [registry](https://grafana.com/docs/k6//extensions/explore), +be sure to review the [requirements](https://grafana.com/docs/k6//extensions/explanations/extensions-registry#registry-requirements). + +{{% /admonition %}} + +## Avoid unneeded work + +These actions may save you the trouble of building a whole new extension when its not needed. + +- Confirm that a similar extension doesn't already exist for your use case. Take a look at + the [Extensions listing](https://grafana.com/docs/k6//extensions/explore) and the [`xk6` topic on GitHub](https://github.com/topics/xk6). +- Prefer generic solutions. For example, if you can test a system with a generic protocol like _MQTT_, prefer + [xk6-mqtt](https://github.com/pmalhaire/xk6-mqtt) over a new extension for some custom protocol. +- Lean toward writing pure JavaScript libraries over building an extension in Go. + A JavaScript library will be better supported, more straightforward, and reusable than an extension. diff --git a/docs/sources/v0.50.x/extensions/create/javascript-extensions.md b/docs/sources/v0.50.x/extensions/create/javascript-extensions.md new file mode 100644 index 000000000..0841d9b1f --- /dev/null +++ b/docs/sources/v0.50.x/extensions/create/javascript-extensions.md @@ -0,0 +1,348 @@ +--- +title: 'JavaScript Extensions' +description: 'Follow these steps to build a JS extension for k6.' +weight: 01 +--- + +# JavaScript Extensions + +Take advantage of Go's speed, power, and efficiency while providing the flexibility of using JavaScript APIs +within your test scripts. + +By implementing k6 interfaces, you can close various gaps in your testing setup: + +- New network protocols +- Improved performance +- Features not supported by k6 core + +## Before you start + +To run this tutorial, you'll need the following applications installed: + +- Go +- Git + +You also need to install xk6: + +```bash +$ go install go.k6.io/xk6/cmd/xk6@latest +``` + +## Write a simple extension + +1. First, set up a directory to work in: + +```bash +$ mkdir xk6-compare; cd xk6-compare; go mod init xk6-compare +``` + +1. In the directory, make a Go file for your JavaScript extension. + +A simple JavaScript extension requires a struct that exposes methods called by the test script. + + + +```go +package compare + +import "fmt" + +// Compare is the type for our custom API. +type Compare struct{ + ComparisonResult string // textual description of the most recent comparison +} + +// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message. +func (c *Compare) IsGreater(a, b int) bool { + if a > b { + c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b) + return true + } else { + c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b) + return false + } +} +``` + +1. Register the module to use these from k6 test scripts. + +{{% admonition type="note" %}} + +k6 extensions must have the `k6/x/` prefix, +and the short name must be unique among all extensions built in the same k6 binary. + +{{% /admonition %}} + +```go +import "go.k6.io/k6/js/modules" + +// init is called by the Go runtime at application startup. +func init() { + modules.Register("k6/x/compare", new(Compare)) +} +``` + +1. Save the file as something like `compare.go`. The final code looks like this: + +{{< code >}} + +```go +package compare + +import ( + "fmt" + "go.k6.io/k6/js/modules" +) + +// init is called by the Go runtime at application startup. +func init() { + modules.Register("k6/x/compare", new(Compare)) +} + +// Compare is the type for our custom API. +type Compare struct{ + ComparisonResult string // textual description of the most recent comparison +} + +// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message. +func (c *Compare) IsGreater(a, b int) bool { + if a > b { + c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b) + return true + } else { + c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b) + return false + } +} +``` + +{{< /code >}} + +## Compile your extended k6 + +To build a k6 binary with this extension, run this command: + +```bash +$ xk6 build --with xk6-compare=. +``` + +{{% admonition type="note" %}} + +When building from source code, `xk6-compare` is the Go module name passed to `go mod init`. +Usually, this would be a URL similar to `github.com/grafana/xk6-compare`. + +{{% /admonition %}} + +## Use your extension + +Now, use the extension in a test script! + +1. Make a file with a name like `test.js` then add this code: + +{{< code >}} + +```javascript +import compare from 'k6/x/compare'; + +export default function () { + console.log(`${compare.isGreater(2, 1)}, ${compare.comparison_result}`); + console.log(`${compare.isGreater(1, 3)}, ${compare.comparison_result}`); +} +``` + +{{< /code >}} + +1. Run the test with `./k6 run test.js`. + +It should output the following: + +```shell +INFO[0000] true, 2 is greater than 1 source=console +INFO[0000] false, 1 is NOT greater than 3 source=console +``` + +## Use the advanced module API + +Suppose your extension needs access to internal k6 objects to, for example, inspect the state of the test during execution. +We will need to make slightly more complicated changes to the above example. + +Our main `Compare` struct should implement the [`modules.Instance`](https://pkg.go.dev/go.k6.io/k6/js/modules#Instance) interface +to access the [`modules.VU`](https://pkg.go.dev/go.k6.io/k6/js/modules#VU) to inspect internal k6 objects such as: + +- [`lib.State`](https://pkg.go.dev/go.k6.io/k6/lib#State), the VU state with values like the VU ID and iteration number +- [`goja.Runtime`](https://pkg.go.dev/github.com/dop251/goja#Runtime), the JavaScript runtime used by the VU +- a global `context.Context` containing objects like the [`lib.ExecutionState`](https://pkg.go.dev/go.k6.io/k6/lib#ExecutionState) + +Additionally, there should be a root module implementation of the [`modules.Module`](https://pkg.go.dev/go.k6.io/k6/js/modules#Module) +interface to serve as a factory of `Compare` instances for each VU. + +{{% admonition type="caution" %}} + +The significance depends on the size of your module. + +{{% /admonition %}} + +Here's what that would look like: + +{{< code >}} + +```go +package compare + +import ( + "fmt" + "go.k6.io/k6/js/modules" +) + +// init is called by the Go runtime at application startup. +func init() { + modules.Register("k6/x/compare", New()) +} + +type ( + // RootModule is the global module instance that will create module + // instances for each VU. + RootModule struct{} + + // ModuleInstance represents an instance of the JS module. + ModuleInstance struct { + // vu provides methods for accessing internal k6 objects for a VU + vu modules.VU + // comparator is the exported type + comparator *Compare + } +) + +// Ensure the interfaces are implemented correctly. +var ( + _ modules.Instance = &ModuleInstance{} + _ modules.Module = &RootModule{} +) + +// New returns a pointer to a new RootModule instance. +func New() *RootModule { + return &RootModule{} +} + +// NewModuleInstance implements the modules.Module interface returning a new instance for each VU. +func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance { + return &ModuleInstance{ + vu: vu, + comparator: &Compare{vu: vu}, + } +} + +// Compare is the type for our custom API. +type Compare struct{ + vu modules.VU // provides methods for accessing internal k6 objects + ComparisonResult string // textual description of the most recent comparison +} + +// IsGreater returns true if a is greater than b, or false otherwise, setting textual result message. +func (c *Compare) IsGreater(a, b int) bool { + if a > b { + c.ComparisonResult = fmt.Sprintf("%d is greater than %d", a, b) + return true + } else { + c.ComparisonResult = fmt.Sprintf("%d is NOT greater than %d", a, b) + return false + } +} + +// Exports implements the modules.Instance interface and returns the exported types for the JS module. +func (mi *ModuleInstance) Exports() modules.Exports { + return modules.Exports{ + Default: mi.comparator, + } +} +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +Notice that we implemented the Module API and now `modules.Register` the _root module_ rather than our _Compare_ object! + +{{% /admonition %}} + +## Accessing runtime state + +At this time, we've provided access to the [`modules.VU`](https://pkg.go.dev/go.k6.io/k6/js/modules#VU) from the `Compare` +type; however, we aren't taking advantage of the methods provided. Here is a contrived example of how we can utilize the +runtime state: + +{{< code >}} + +```go +// InternalState holds basic metadata from the runtime state. +type InternalState struct { + ActiveVUs int64 `js:"activeVUs"` + Iteration int64 + VUID uint64 `js:"vuID"` + VUIDFromRuntime goja.Value `js:"vuIDFromRuntime"` +} + +// GetInternalState interrogates the current virtual user for state information. +func (c *Compare) GetInternalState() *InternalState { + state := c.vu.State() + ctx := c.vu.Context() + es := lib.GetExecutionState(ctx) + rt := c.vu.Runtime() + + return &InternalState{ + VUID: state.VUID, + VUIDFromRuntime: rt.Get("__VU"), + Iteration: state.Iteration, + ActiveVUs: es.GetCurrentlyActiveVUsCount(), + } +} +``` + +{{< /code >}} + +Create a test script to utilize the new `getInternalState()` function as in the following: + +{{< code >}} + +```javascript +import compare from 'k6/x/compare'; + +export default function () { + const state = compare.getInternalState(); + console.log( + `Active VUs: ${state.activeVUs}, Iteration: ${state.iteration}, VU ID: ${state.vuID}, VU ID from runtime: ${state.vuIDFromRuntime}` + ); +} +``` + +{{< /code >}} + +Executing the script as `./k6 run test-state.js --vus 2 --iterations 5` will produce output similar to the following: + +```shell +INFO[0000] Active VUs: 2, Iteration: 0, VU ID: 2, VU ID from runtime: 2 source=console +INFO[0000] Active VUs: 2, Iteration: 0, VU ID: 1, VU ID from runtime: 1 source=console +INFO[0000] Active VUs: 2, Iteration: 1, VU ID: 2, VU ID from runtime: 2 source=console +INFO[0000] Active VUs: 2, Iteration: 1, VU ID: 1, VU ID from runtime: 1 source=console +INFO[0000] Active VUs: 2, Iteration: 2, VU ID: 2, VU ID from runtime: 2 source=console +``` + +> For a more extensive usage example of this API, look at the +> [`k6/execution`](https://github.com/grafana/k6/blob/master/js/modules/k6/execution/execution.go) module. + +## Things to keep in mind + +- The code in the `default` function (or another function specified by + [`exec`](https://grafana.com/docs/k6//using-k6/scenarios#options)) will be executed many + times during a test run and possibly in parallel by thousands of VUs. + Any operation of your extension should therefore be performant + and [thread-safe](https://en.wikipedia.org/wiki/Thread_safety). +- Any _heavy_ initialization should be done in the [init context](https://grafana.com/docs/k6//javascript-api/init-context), + if possible, and not as part of the `default` function execution. +- Use the registry's [`NewMetric`](https://pkg.go.dev/go.k6.io/k6/metrics#Registry.NewMetric) method to create + custom metrics; to emit them, use [`metrics.PushIfNotDone()`](https://pkg.go.dev/go.k6.io/k6/metrics#PushIfNotDone). + +> Questions? Feel free to join the discussion on extensions in the [k6 Community Forum](https://community.grafana.com/c/grafana-k6/extensions/82). + +Next, create an [Output extension](https://grafana.com/docs/k6//extensions/create/output-extensions) to publish test metrics +to a destination not already supported by k6. diff --git a/docs/sources/v0.50.x/extensions/create/output-extensions.md b/docs/sources/v0.50.x/extensions/create/output-extensions.md new file mode 100644 index 000000000..d20910642 --- /dev/null +++ b/docs/sources/v0.50.x/extensions/create/output-extensions.md @@ -0,0 +1,248 @@ +--- +title: 'Output Extensions' +description: 'Follow these steps to build an output extension for k6.' +weight: 02 +--- + +# Output Extensions + +k6 provides many [metrics](https://grafana.com/docs/k6//using-k6/metrics) and [output formats](https://grafana.com/docs/k6//results-output/), but it cannot directly support all possibilities. +To store or alter metrics captured during an active k6 test, +you can create a custom output extension. + +Output extension binaries can use the `--out` flag to send metrics to a custom place. +Some potential reasons for a custom extension could include: + +- To support a time-series database not already supported +- To add derived metrics data for storage +- To filter metrics to only the data you care about + +Like [JavaScript extensions](https://grafana.com/docs/k6//extensions/create/javascript-extensions), +output extensions rely on the extension author to implement specific APIs. + +## Before you start: + +To run this tutorial, you'll need the following applications installed: + +- Go +- Git + +You also need to install xk6: + +```bash +$ go install go.k6.io/xk6/cmd/xk6@latest +``` + +## Write a simple extension + +1. Set up a directory to work in. + +```bash +$ mkdir xk6-output-logger; cd xk6-output-logger; go mod init xk6-output-logger +``` + +1. The core of an Output extension is a struct that implements the [`output.Output`](https://pkg.go.dev/go.k6.io/k6/output#Output) + interface. + +Create a simple example that outputs each set of metrics to the console as received by the `AddMetricSamples(samples []metrics.SampleContainer)` +method of the output interface. + +```go +package log + +import ( + "fmt" + "strings" + "time" + + "go.k6.io/k6/metrics" + "go.k6.io/k6/output" +) + +// AddMetricSamples receives metric samples from the k6 Engine as they're emitted. +func (l *Logger) AddMetricSamples(samples []metrics.SampleContainer) { + for _, sample := range samples { + all := sample.GetSamples() + fmt.Fprintf(l.out, "%s [%s]\n", all[0].GetTime().Format(time.RFC3339Nano), metricKeyValues(all)) + } +} + +// metricKeyValues returns a string of key-value pairs for all metrics in the sample. +func metricKeyValues(samples []metrics.Sample) string { + names := make([]string, 0, len(samples)) + for _, sample := range samples { + names = append(names, fmt.Sprintf("%s=%v", sample.Metric.Name, sample.Value)) + } + return strings.Join(names, ", ") +} +``` + +1. Register the module to use these from k6 test scripts. + +```go +import "go.k6.io/k6/output" + +// init is called by the Go runtime at application startup. +func init() { + output.RegisterExtension("logger", New) +} +``` + +{{% admonition type="note" %}} + +You must use the registered with the `-o`, or `--out` flag when running k6! + +{{% /admonition %}} + +The final extension code looks like this: + +{{< code >}} + +```go +package log + +import ( + "fmt" + "io" + "strings" + "time" + + "go.k6.io/k6/metrics" + "go.k6.io/k6/output" +) + +// init is called by the Go runtime at application startup. +func init() { + output.RegisterExtension("logger", New) +} + +// Logger writes k6 metric samples to stdout. +type Logger struct { + out io.Writer +} + +// New returns a new instance of Logger. +func New(params output.Params) (output.Output, error) { + return &Logger{params.StdOut}, nil +} + +// Description returns a short human-readable description of the output. +func (*Logger) Description() string { + return "logger" +} + +// Start initializes any state needed for the output, establishes network +// connections, etc. +func (o *Logger) Start() error { + return nil +} + +// AddMetricSamples receives metric samples from the k6 Engine as they're emitted. +func (l *Logger) AddMetricSamples(samples []metrics.SampleContainer) { + for _, sample := range samples { + all := sample.GetSamples() + fmt.Fprintf(l.out, "%s [%s]\n", all[0].GetTime().Format(time.RFC3339Nano), metricKeyValues(all)) + } +} + +// metricKeyValues returns a string of key-value pairs for all metrics in the sample. +func metricKeyValues(samples []metrics.Sample) string { + names := make([]string, 0, len(samples)) + for _, sample := range samples { + names = append(names, fmt.Sprintf("%s=%v", sample.Metric.Name, sample.Value)) + } + return strings.Join(names, ", ") +} + +// Stop finalizes any tasks in progress, closes network connections, etc. +func (*Logger) Stop() error { + return nil +} +``` + +{{< /code >}} + +Notice a couple of things: + +- The module initializer `New()` receives an instance of + [`output.Params`](https://pkg.go.dev/go.k6.io/k6/output#Params). + With this object, the extension can access the output-specific configuration, + interfaces to the filesystem, synchronized stdout and stderr, and more. + +- `AddMetricSamples` in this example writes to stdout. This output might have + to be buffered and flushed periodically in a real-world scenario to avoid memory + leaks. Below we'll discuss some helpers you can use for that. + +## Compile your extended k6 + +To build a k6 binary with this extension, run: + +```bash +$ xk6 build --with xk6-output-logger=. +``` + +{{% admonition type="note" %}} + +`xk6-output-logger` is the Go module name passed to `go mod init` + +Usually, this would be a URL similar to `github.com/grafana/xk6-output-logger`. + +{{% /admonition %}} + +## Use your extension + +Now we can use the extension with a test script. + +1. In new JavaScript file, make some simple test logic. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + http.get('https://test-api.k6.io'); + sleep(0.5); +} +``` + +{{< /code >}} + +1. Now, run the test. + +```bash +$ ./k6 run test.js --out logger --quiet --no-summary --iterations 2 +``` + +{{% admonition type="note" %}} + +The `--out logger` argument tells k6 to use your custom output. The flag +`--quiet --no-summary` configures k6 to show only custom output. + +{{% /admonition %}} + +Your output should look something like this: + +```shell +2022-07-01T08:55:09.59272-05:00 [http_reqs=1, http_req_duration=117.003, http_req_blocked=558.983, http_req_connecting=54.135, http_req_tls_handshaking=477.198, http_req_sending=0.102, http_req_waiting=116.544, http_req_receiving=0.357, http_req_failed=0] +2022-07-01T08:55:09.917036-05:00 [vus=1, vus_max=1] +2022-07-01T08:55:10.094196-05:00 [data_sent=446, data_received=21364, iteration_duration=1177.505083, iterations=1] +2022-07-01T08:55:10.213926-05:00 [http_reqs=1, http_req_duration=119.122, http_req_blocked=0.015, http_req_connecting=0, http_req_tls_handshaking=0, http_req_sending=0.103, http_req_waiting=118.726, http_req_receiving=0.293, http_req_failed=0] +2022-07-01T08:55:10.715323-05:00 [data_sent=102, data_received=15904, iteration_duration=620.862459, iterations=1] +``` + +## Things to keep in mind + +- Output structs can optionally implement additional interfaces that allow them to + receive [thresholds](https://pkg.go.dev/go.k6.io/k6/output#WithThresholds) or + [run-status updates](https://pkg.go.dev/go.k6.io/k6/output#WithRunStatusUpdates) + and even [interrupt a test](https://pkg.go.dev/go.k6.io/k6/output#WithTestRunStop). +- Consider using [`output.SampleBuffer`](https://pkg.go.dev/go.k6.io/k6/output#SampleBuffer) + and [`output.PeriodicFlusher`](https://pkg.go.dev/go.k6.io/k6/output#PeriodicFlusher) + to improve performance given the large amounts of data produced by k6. Refer to + [`statsd` output](https://github.com/grafana/k6/blob/master/output/statsd/output.go) for an example. +- Use the [project template](https://github.com/grafana/xk6-output-template) as a starting point + for your output extension. + +> Questions? Feel free to join the discussion on extensions in the [k6 Community Forum](https://community.grafana.com/c/grafana-k6/extensions/82). diff --git a/docs/sources/v0.50.x/extensions/explanations/_index.md b/docs/sources/v0.50.x/extensions/explanations/_index.md new file mode 100644 index 000000000..b8ac55466 --- /dev/null +++ b/docs/sources/v0.50.x/extensions/explanations/_index.md @@ -0,0 +1,10 @@ +--- +title: Explanations +weight: 05 +--- + +# Explanations + + + +{{< section >}} diff --git a/docs/sources/v0.50.x/extensions/explanations/extension-graduation.md b/docs/sources/v0.50.x/extensions/explanations/extension-graduation.md new file mode 100644 index 000000000..5f89bc8f9 --- /dev/null +++ b/docs/sources/v0.50.x/extensions/explanations/extension-graduation.md @@ -0,0 +1,56 @@ +--- +title: Extension Graduation +description: Some extensions are created with the intent to become a part of core of k6. +weight: 03 +--- + +# Extension Graduation + +Some _Go_ extensions may one day be available within the k6 binary. +These select extensions pass through different phases to become core functionality. + +This graduation process benefits both users and developers. +k6 users can access new features and provide feedback to influence its developments. +k6 developers meanwhile can iterate quickly and respond to feedback without worrying about breaking changes. + +A _core-bound_ extension passes through the following phases: +![Extension graduation](/media/docs/k6-oss/extension-graduation.png) + +### Extension + +Most extensions in the k6 ecosystem remain _extensions_ requiring [xk6](https://github.com/grafana/xk6) to incorporate the custom functionality. +These extensions might be provided by Grafana or by the community, and _may_ be included in the [Extensions Registry](https://grafana.com/docs/k6//extensions/explore). + +{{% admonition type="note" %}} + +Only Grafana-controlled extensions may progress beyond the _extension_ phase to become _experimental_ or _core modules_. + +{{% /admonition %}} + +### Experimental Module + +This phase is the first exposure to core k6. +The extension is still maintained outside the core of k6 but imported as a Go module, no longer requiring xk6. + +Once an extension is promoted as an _experimental module_, the extension will be removed from the [extension listing](https://grafana.com/docs/k6//extensions/explore). +At this time, documentation for the functionality will be provided in [k6 API](https://grafana.com/docs/k6//javascript-api/k6-experimental) and [output](https://grafana.com/docs/k6//results-output/real-time) for _JavaScript_ and _Output_ extensions, respectively. + +There should be a reasonably high degree of quality and stability at this point. +This phase makes the feature accessible to more users, which in turn gives k6 developers more chances to receive feedback. +The key will be to achieve a balance between usability and stability. + +{{% admonition type="caution" %}} + +Not all experimental modules will progress to become a core module! +The k6 team reserves the right to discontinue and remove any experimental module if is no longer deemed desirable. + +{{% /admonition %}} + +### Core Module + +The stabilized feature is now part of the standard k6 product as a built-in module. +An extension may be in the _experimental module_ phase for an extended time before progressing as a core module. + +The module code is in the main k6 repository, with the old extension repository archived. +Options from the _experimental module_ phase are deprecated and removed after two k6 releases, +providing time for users to upgrade scripts for the new module. diff --git a/docs/sources/v0.50.x/extensions/explanations/extensions-registry.md b/docs/sources/v0.50.x/extensions/explanations/extensions-registry.md new file mode 100644 index 000000000..dbcb075e7 --- /dev/null +++ b/docs/sources/v0.50.x/extensions/explanations/extensions-registry.md @@ -0,0 +1,103 @@ +--- +title: About the Extensions Registry +description: Reasons for the registry and what is required to be included. +weight: 01 +--- + +# About the Extensions Registry + +Did you create an extension and want to share it with your fellow k6 users? +We'd love to spread word of this new feature adding to our [registry](https://grafana.com/docs/k6//extensions/explore) of available extensions. +However, before an extension is added to the registry, we must ensure that it is maintained to the registry standard. + +Our desire is to provide the best developer experience when using k6. +This extends to the extensions ecosystem as well. +The adaptability provided by k6 extensions opens a wide array of potential use cases. + +To ensure quality, we need a well-maintained, curated listing of extensions. +Our pledge to the community is to make our best attempt to ensure the listed projects meet certain standards. +While we cannot guarantee the quality of community-provided extensions, we _can_ aid the evaluation by requiring certain consistencies. + +## Registry Requirements + +At minimum, each source code repository must have the following: + +- **a README file** + + The `README` must contain documentation such as the project description, build and usage instructions, as well as k6 version compatibility. + The goal is to provide enough information to quickly and easily evaluate the extension. + +- **the `xk6` topic set** + + GitHub allows setting _topics_ for a repository. + This supports querying all public repositories based upon a keyword for better discoverability, i.e. ["xk6"](https://github.com/topics/xk6). + See the [GitHub documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/classifying-your-repository-with-topics) to add topics. + +- **a non-restrictive license** + + Any open source software (OSS) license will suffice, but [Apache2](https://www.apache.org/licenses/LICENSE-2.0) is preferred. + +- **an `examples` folder with examples** + + Provide at least one script to show proper usage of your API. + If a [Docker Compose](https://docs.docker.com/compose/compose-file/) specification is provided, these could be used as integration tests to validate the extension works as intended. + +- **at least one versioned release** + + As features or fixes are ready to be consumed, create a [release](https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository). + This promotes stability by allowing a user to utilize a particular version. + Use [semantic versioning](https://semver.org/) to communicate changes as the extension evolves. + +- **builds with a recent k6 version** + + Ideally, the extension should build with the [latest release](https://github.com/grafana/k6/releases/latest). + But, it must build with a version of k6 that is no more than three releases old. + For example, if latest version of k6 is `v0.100`, the extension must build with at least version `v0.98`. + Be sure to also match the version of Go as determined by the version of k6. + +## Naming Conventions + +Some extensions may be very specific, where others are more general. +Multiple extensions may even be created for the same product with different levels of support based upon version. +By adhering to typical naming conventions, your extension name can remove some doubts as to what is supported. + +For any extension, we recommend the `xk6-` prefix as well as an optional `output-` for [Output extensions](https://grafana.com/docs/k6//extensions/create/output-extensions). +Next, provide the product or protocol name; don't be cryptic. +Ensure the usage is explicit by adopting only well-known acronyms or abbreviations if necessary. +If your extension supports only a specific version of a product, incorporate the version into the name, for example `v2`. + +As an example, suppose an extension that outputs test metrics to the _AwesomeLog_ application, and it uses only the v2 API. +In this case, say the latest v3 API is not backward-compatible. +Applying our conventions, we'd recommend naming this repository as `xk6-output-awesomelog-v2`. + +{{% admonition type="note" %}} + +Our goal is to quickly understand the intent of the extension. + +{{% /admonition %}} + +## Extension Tiers + +Extensions come from multiple sources. +To help distinguish extensions, we're now categorizing each extension into a _tier_. +Each tier definition is as follows: + +- **Official Extensions** + + _Official extensions_ are those owned and maintained by Grafana Labs. + They will have official documentation and have support provided by members of the Grafana organization. + +- **Community Extensions** + + _Community extensions_ are created and maintained by an individual or group from the community at large. + These have no implied warranty or level of support. + The Grafana team will make best-effort assistance to keep popular projects in compliance. + +## Potential for De-listing + +Given our desire to provide the best developer experience when using k6, we reserve the right to de-list any extension we deem is no longer maintaining standards. +Before any action takes place, the extension maintainers will be contacted to be given a chance to rectify the project and thus avoid de-listing. +Such contact may be in the form of GitHub issues or merge requests. + +Should any extension be de-listed, this does not constitute a permanent removal. +Any extension that has been de-listed may be reinstated once the reasons for the initial removal have been remediated. diff --git a/docs/sources/v0.50.x/extensions/explanations/go-js-bridge.md b/docs/sources/v0.50.x/extensions/explanations/go-js-bridge.md new file mode 100644 index 000000000..b80c8c625 --- /dev/null +++ b/docs/sources/v0.50.x/extensions/explanations/go-js-bridge.md @@ -0,0 +1,41 @@ +--- +title: About the Go-to-JS bridge +description: Technical details about how JavaScript works in the goja engine. +weight: 02 +--- + +# About the Go-to-JS bridge + +All k6 and xk6 binaries have an embedded JavaScript engine, [goja](https://github.com/dop251/goja), +which your test scripts run on. + +You will deepen your conceptual knowledge of how your k6 extension works if you understand the _bridge_ between Go internals and the JavaScript runtime. + +## Go-to-JavaScript bridge features + +The bridge has a few features we should highlight: + +- Go method names are converted from _Pascal_ to _Camel_ case when + accessed in JS. For example, `IsGreater` becomes `isGreater`. + +- Go field names convert from _Pascal_ to _Snake_ case. For example, the struct field `ComparisonResult string` + becomes `comparison_result` in JS. + +- Field names may be explicit using `js` struct tags. For example, declaring the field as ComparisonResult string `js:"result"` + or hiding from JS using `js:"-"`. + +## Type conversion and native constructors + +The JavaScript runtime transparently converts Go types like `int64` to their JS equivalent. +For complex types where this is impossible, your script might fail with a `TypeError`, requiring you to explicitly convert +your object to a [`goja.Object`](https://pkg.go.dev/github.com/dop251/goja#Object) or [`goja.Value`](https://pkg.go.dev/github.com/dop251/goja#Value). + +```go +func (*Compare) XComparator(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object { + return rt.ToValue(&Compare{}).ToObject(rt) +} +``` + +The preceding snippet also demonstrates the _native constructors_ feature from goja, where methods can become JS constructors. +Methods with this signature can create `Comparator` instances in JS with `new compare.Comparator()`. +While this is more idiomatic to JS, it still has the benefit of receiving the `goja.Runtime`. diff --git a/docs/sources/v0.50.x/extensions/explore.md b/docs/sources/v0.50.x/extensions/explore.md new file mode 100644 index 000000000..63f1e26c4 --- /dev/null +++ b/docs/sources/v0.50.x/extensions/explore.md @@ -0,0 +1,348 @@ +--- +title: 'Explore k6 extensions' +menuTitle: 'Explore extensions' +weight: 01 +--- + +# Explore k6 extensions + +With over 50 available extensions, the k6 extension ecosystem has many options to meet your requirements and help you incorporate new protocol access, embed a particular client, or improve your test performance. Extensions are developed both by the k6 developers and by the open-source developer community. + +Extensions are composable; you can combine any extensions, or mix and match different test cases. You can use [Go and xk6](https://grafana.com/docs/k6//extensions/build-k6-binary-using-go/) or [Docker](https://grafana.com/docs/k6//extensions/build-k6-binary-using-docker/) to build your custom k6 binary: + +{{< code >}} + +```go-and-xk6 +xk6 build \ + --with github.com/grafana/xk6-sql@v0.0.1 \ + --with github.com/grafana/xk6-output-prometheus-remote +``` + +```docker-in-linux +docker run --rm -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" grafana/xk6 build \ + --with github.com/grafana/xk6-sql@v0.0.1 \ + --with github.com/grafana/xk6-output-prometheus-remote +``` + +{{< /code >}} + +
+ +Use the table to explore the many extensions. Questions? Feel free to join the discussion about extensions in the [k6 Community Forum](https://community.grafana.com/c/grafana-k6/extensions/82). + + + +Don't see what you need? Learn how you can [create a custom extension](https://grafana.com/docs/k6//extensions/create/). diff --git a/docs/sources/v0.50.x/get-started/_index.md b/docs/sources/v0.50.x/get-started/_index.md new file mode 100644 index 000000000..6df58599b --- /dev/null +++ b/docs/sources/v0.50.x/get-started/_index.md @@ -0,0 +1,10 @@ +--- +weight: 01 +title: Get started +--- + +# Get started + + + +{{< section >}} diff --git a/docs/sources/v0.50.x/get-started/installation/_index.md b/docs/sources/v0.50.x/get-started/installation/_index.md new file mode 100644 index 000000000..3d7493a47 --- /dev/null +++ b/docs/sources/v0.50.x/get-started/installation/_index.md @@ -0,0 +1,81 @@ +--- +title: 'Installation' +description: 'k6 has packages for Linux, Mac, and Windows. As alternatives, you can also using a Docker container or a standalone binary.' +weight: 02 +weight: 02 +--- + +# Installation + +k6 has packages for Linux, Mac, and Windows. Alternatively, you can use a Docker container or a standalone binary. + +## Linux + +### Debian/Ubuntu + +```bash +sudo gpg -k +sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69 +echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list +sudo apt-get update +sudo apt-get install k6 +``` + +### Fedora/CentOS + +Using `dnf` (or `yum` on older versions): + +```bash +sudo dnf install https://dl.k6.io/rpm/repo.rpm +sudo dnf install k6 +``` + +## MacOS + +Using [Homebrew](https://brew.sh/): + +```bash +brew install k6 +``` + +## Windows + +If you use the [Chocolatey package manager](https://chocolatey.org/) you can install the unofficial k6 package with: + +``` +choco install k6 +``` + +If you use the [Windows Package Manager](https://github.com/microsoft/winget-cli), install the official packages from the k6 manifests [(created by the community)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/k/k6/k6): + +``` +winget install k6 --source winget +``` + +Alternatively, you can download and run [the latest official installer](https://dl.k6.io/msi/k6-latest-amd64.msi). + +## Docker + +```bash +docker pull grafana/k6 +``` + +We also have a separate image you can use with `chromium` installed to run k6 browser tests. + +```bash +docker pull grafana/k6:master-with-browser +``` + +## Download the k6 binary + +Our [GitHub Releases page](https://github.com/grafana/k6/releases) has a standalone binary for all platforms. After downloading and extracting the archive for your platform, place the `k6` or `k6.exe` binary in your `PATH` to run `k6` from any location. + +## Using k6 extensions + +If you use one or more [k6 extensions](https://grafana.com/docs/k6//extensions), you need a k6 binary built with your desired extensions. +Head to [Explore extension](https://grafana.com/docs/k6//extensions/explore) to get started. + +## Troubleshooting + +If installation fails, check the [list of common installation issues](https://grafana.com/docs/k6//get-started/installation/troubleshooting). +If your problem is not listed and persists, reach out via the channel `#community-discussion` on our [official Slack](https://k6io.slack.com/), or report it on our [community forum](https://community.grafana.com/). diff --git a/docs/sources/v0.50.x/get-started/installation/troubleshooting.md b/docs/sources/v0.50.x/get-started/installation/troubleshooting.md new file mode 100644 index 000000000..a8c624081 --- /dev/null +++ b/docs/sources/v0.50.x/get-started/installation/troubleshooting.md @@ -0,0 +1,37 @@ +--- +title: 'Troubleshooting' +description: 'Instructions to fix the most common installation issues.' +weight: 01 +--- + +# Troubleshooting + +## System lacks ca-certificates or gnupg2 + +Some Linux distributions don't come bundled with the `ca-certificates` and `gnupg2` packages. +If you use such a distribution, you'll need to install them with: + +```bash +sudo apt-get update && sudo apt-get install -y ca-certificates gnupg2 +``` + +This example is for Debian/Ubuntu and derivatives. Consult your distribution's documentation if you use another one. + +## Behind a firewall or proxy + +Some users have reported that they can't download the key from Ubuntu's keyserver. +When they run the `gpg` command, their firewalls or proxies block their requests to download. +If this issue affects you, you can try this alternative: + +```bash +curl -s https://dl.k6.io/key.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/k6-archive-keyring.gpg +``` + +## Old rpm-based Linux distributions + +Distributions like Amazon Linux 2 and CentOS before version 8 don't support the PGP V4 signature we use. +You'll need to disable the verification when you install k6: + +```bash +sudo yum install --nogpgcheck k6 +``` diff --git a/docs/sources/v0.50.x/get-started/resources.md b/docs/sources/v0.50.x/get-started/resources.md new file mode 100644 index 000000000..592cafdd2 --- /dev/null +++ b/docs/sources/v0.50.x/get-started/resources.md @@ -0,0 +1,45 @@ +--- +title: k6 resources +description: 'An overview of the k6 resources beyond the k6 docs: videos, repositories, test servers, courses, and more' +weight: 05 +--- + +# k6 resources + +The docs aim to cover everything necessary to use the core k6 products in your daily operational work. +But scripting and testing are skills that take time to learn. +What's more, k6 is an extensible tool, already incorporated with many other functionalities and protocols. + +These resources help you write and run k6 tests in a safe environment and explore how to use k6 with other applications. + +## Learning + +- [Get started with k6 tutorial](https://grafana.com/docs/k6//examples/get-started-with-k6). The getting started tutorial provides some procedures for common real-life uses of k6 and does not require prior knowledge of k6 or JavaScript + +- [k6 Learn](https://github.com/grafana/k6-learn). A repository with a course and a ton of learning resources +- [k6 YouTube channel](https://www.youtube.com/c/k6test/playlists). Office hours, specific playlists, and other interesting videos from the community. +- [Awesome k6](https://github.com/grafana/awesome-k6). A list of awesome stuff about k6. +- [Examples](https://github.com/grafana/k6/tree/master/examples). A directory full of example k6 scripts for different use cases. + +## Community + +- [The k6 community forum](https://community.grafana.com/). Get support from the k6 team and community. +- [Get in touch](https://k6.io/community/#join-the-conversation). Slack, Meetup, Twitter, Stack Overflow, LinkedIn, and more. + +## Test servers + +If you need a place to learn k6 and test your scripts, you can use these playground/demo servers: + +- [pizza.grafana.fun](https://pizza.grafana.fun/). A simple demo webapp. [grafana/quickpizza](https://github.com/grafana/quickpizza) +- [k6-http.grafana.fun](https://k6-http.grafana.fun). A simple HTTP Request & Response Service. [grafana/httpbin](https://github.com/grafana/httpbin) +- [grpcbin.test.k6.io](https://grpcbin.test.k6.io/). A simple gRPC Request & Response Service. [grafana/k6-grpcbin](https://github.com/grafana/k6-grpcbin) + +Note that these are shared testing environments - please avoid high-load tests. Alternatively, you can deploy and host them on your infrastructure and run the examples in the repository. + +## k6 + your favorite tool + +- [Kubernetes Operator](https://k6.io/blog/running-distributed-tests-on-k8s/). Distribute test execution across a Kubernetes cluster. +- [xk6 extensions](https://grafana.com/docs/k6//extensions). Custom k6 binaries to support the tool you need. +- [The browser recorder](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder). Make test scripts from browser sessions. +- [k6 TypeScript template](https://github.com/grafana/k6-template-typescript) +- [Integrations](https://grafana.com/docs/k6//misc/integrations) diff --git a/docs/sources/v0.50.x/get-started/results-output.md b/docs/sources/v0.50.x/get-started/results-output.md new file mode 100644 index 000000000..c3239c49e --- /dev/null +++ b/docs/sources/v0.50.x/get-started/results-output.md @@ -0,0 +1,105 @@ +--- +title: 'Results output' +description: 'For basic tests, the top-level summary that k6 provides might be enough. For detailed analysis, you can stream all data your test outputs to an external source.' +weight: 04 +--- + +# Results output + +As k6 generates load for your test, it also makes _metrics_ that measure the performance of the system. +Broadly, you can analyze metrics in two ways: + +- As summary statistics, in an _end-of-test_ summary report. +- In granular detail, with measurements for every data point across test (and timestamps) + +You can customize almost every aspect of result output: + +- Create custom metrics +- Configure new summary statistics and print them to any text format. +- Stream the results to one or multiple services of your choice (for example, InfluxDB or Prometheus). + +## Metrics + +**Documentation**: [Using metrics](https://grafana.com/docs/k6//using-k6/metrics) + +k6 comes with built-in metrics about the test load and the system response. +Key metrics include: + +- `http_req_duration`, the end-to-end time of all requests (that is, the total latency) +- `http_req_failed`, the total number of failed requests +- `iterations`, the total number of iterations + +## End-of-test summary + +**Documentation**: [End-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test) + +By default, k6 prints summarized results to `stdout`. + +When you run a test, k6 outputs a plain-text logo, your test progress, and some test details. +After the test finishes, k6 prints the full details and summary statistics of the test metrics. + +![k6 results - console/stdout output](/media/docs/k6-oss/k6-results-stdout.png) + +The end-of-test summary shows aggregated statistical values for your result metrics, including: + +- Median and average values +- Minimum and maximum values +- p90, p95, and p99 values + +You can configure the statistics to report with the [`--summary-trend-stats`](https://grafana.com/docs/k6//using-k6/k6-options/reference#summary-trend-stats) option. +For example, this command displays only median, p95, and p99.9 values. + +```sh +k6 run --iterations=100 --vus=10 \ +--summary-trend-stats="med,p(95),p(99.9)" script.js +``` + +### Custom reports with `handleSummary()` + +For completely customized end-of-summary reports, k6 provides the `handleSummary()` function. + +At the end of the test, k6 automatically creates an object with all aggregated statistics. +The `handleSummary()` function can process this object into a custom report in any text format: JSON, HTML, XML, and whatever else. + +## Time series and external outputs + +**Documentation**: [Real-time metrics](https://grafana.com/docs/k6//results-output/real-time) + +The condensed end-of-test summary provides a top-level view of the test. +For deeper analysis, you need to look at granular time-series data, +which has metrics and timestamps for every point of the test. + +You can access time-series metrics in two ways: + +- Write them to a JSON or CSV file. +- Stream them to an external service. + +In both cases, you can use the `--out` flag and declare your export format as the flag argument. +If you want to send the metrics to multiple sources, you can use multiple flags with multiple arguments: + +```sh +k6 run \ +--out json=test.json \ +--out influxdb=http://localhost:8086/k6 +``` + +The available built-in outputs include: + + + +- [Amazon CloudWatch](https://grafana.com/docs/k6//results-output/real-time/amazon-cloudwatch) +- [Cloud](https://grafana.com/docs/k6//results-output/real-time/cloud) +- [CSV](https://grafana.com/docs/k6//results-output/real-time/csv) +- [Datadog](https://grafana.com/docs/k6//results-output/real-time/datadog) +- [Dynatrace](https://grafana.com/docs/k6//results-output/real-time/dynatrace) +- [Elasticsearch](https://grafana.com/docs/k6//results-output/real-time/elasticsearch) +- [Grafana Cloud Prometheus](https://grafana.com/docs/k6//results-output/real-time/grafana-cloud-prometheus) +- [InfluxDB](https://grafana.com/docs/k6//results-output/real-time/influxdb) +- [JSON](https://grafana.com/docs/k6//results-output/real-time/json) +- [Netdata](https://grafana.com/docs/k6//results-output/real-time/netdata) +- [New Relic](https://grafana.com/docs/k6//results-output/real-time/new-relic) +- [Prometheus](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write) +- [TimescaleDB](https://grafana.com/docs/k6//results-output/real-time/timescaledb) +- [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd) + + diff --git a/docs/sources/v0.50.x/get-started/running-k6.md b/docs/sources/v0.50.x/get-started/running-k6.md new file mode 100644 index 000000000..d4ecf94d4 --- /dev/null +++ b/docs/sources/v0.50.x/get-started/running-k6.md @@ -0,0 +1,237 @@ +--- +title: 'Running k6' +description: 'Follow along to learn how to run a test, add virtual users, increase the test duration, and ramp the number of requests up and down as the test runs.' +weight: 03 +--- + +# Running k6 + +Follow along to learn how to: + +1. Run a test. +2. Add virtual users. +3. Increase the test duration. +4. Ramp the number of requests up and down as the test runs. + +With these example snippets, you'll run the test with your machine's resources. +But, if you have a k6 Cloud account, you can also use the `k6 cloud` command to outsource the test to k6 servers. + +## Run local tests + +To run a simple local script: + +1. Create and initialize a new script by running the following command: + + {{< code >}} + + ```linux + $ k6 new + ``` + + ```docker + $ docker run --rm -i -v $PWD:/app -w /app grafana/k6 new + ``` + + ```windows + PS C:\> docker run --rm -i -v ${PWD}:/app -w /app grafana/k6 init + ``` + + {{< /code >}} + + This command creates a new script file named `script.js` in the current directory. + You can also specify a different file name as an argument to the `k6 new` command, for example `k6 new my-test.js`. + +1. Run k6 with the following command: + + {{< code >}} + + ```linux + $ k6 run script.js + ``` + + ```docker + # When using the `k6` docker image, you can't just give the script name since + # the script file will not be available to the container as it runs. Instead + # you must tell k6 to read `stdin` by passing the file name as `-`. Then you + # pipe the actual file into the container with `<` or equivalent. This will + # cause the file to be redirected into the container and be read by k6. + + $ docker run --rm -i grafana/k6 run - cat script.js | docker run --rm -i grafana/k6 run - + ``` + + {{< /code >}} + +## Add VUs + +Now run a load test with more than one virtual user and a longer duration: + +{{< code >}} + +```linux +$ k6 run --vus 10 --duration 30s script.js +``` + +```docker +$ docker run --rm -i grafana/k6 run --vus 10 --duration 30s - cat script.js | docker run --rm -i grafana/k6 run --vus 10 --duration 30s - +``` + +{{< /code >}} + +_Running a 30-second, 10-VU load test_ + +{{% admonition type="note" %}} + +k6 runs multiple iterations in parallel with _virtual users_ (VUs). +In general terms, more virtual users means more simulated traffic. + +VUs are essentially parallel `while(true)` loops. +Scripts are written in JavaScript, as ES6 modules, +so you can break larger tests into smaller pieces or make reusable pieces as you like. + +{{% /admonition %}} + +### The init context and the default function + +For a test to run, you need to have _init code_, which prepares the test, and _VU code,_ which makes requests. + +Code in the init context defines functions and configures the test options (like `duration`). + +Every test also has a `default` function, +which defines the VU logic. + +```javascript +// init + +export default function () { + // vu code: do things here... +} +``` + +Init code runs first and is called only once per VU. +The `default` code runs as many times or as long as is configured in the test options. + +To learn more about how k6 executes, read about the [Test lifecycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +## Set options + +Instead of typing `--vus 10` and `--duration 30s` each time you run the script, +you can set the options in your JavaScript file: + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; +export const options = { + vus: 10, + duration: '30s', +}; +export default function () { + http.get('http://test.k6.io'); + sleep(1); +} +``` + +If you run the script without flags, k6 uses the options defined in the script: + +{{< code >}} + +```linux +$ k6 run script.js +``` + +```docker +$ docker run --rm -i grafana/k6 run - cat script.js | docker run --rm -i grafana/k6 run - +``` + +{{< /code >}} + +## Ramp VUs up and down in stages + +You can ramp the number of VUs up and down during the test. +To configure ramping, use the `options.stages` property. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + stages: [ + { duration: '30s', target: 20 }, + { duration: '1m30s', target: 10 }, + { duration: '20s', target: 0 }, + ], +}; + +export default function () { + const res = http.get('https://httpbin.test.k6.io/'); + check(res, { 'status was 200': (r) => r.status == 200 }); + sleep(1); +} +``` + +{{< /code >}} + +For more granular ramp configuration, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) and the `ramping-vus` executor. + +## Execution modes + +{{% admonition type="note" %}} + +Portability is a major design goal of k6. + +You can run the same test in different modes with minimal changes. + +{{% /admonition %}} + +k6 supports three execution modes to run a k6 test: local, distributed, and cloud. + +- **Local**: the test execution happens entirely on a single machine, container, or CI server. + + ```bash + k6 run script.js + ``` + +- **Distributed**: the test execution is [distributed across a Kubernetes cluster](https://k6.io/blog/running-distributed-tests-on-k8s/). + + Save the following YAML as `k6-resource.yaml`: + + ```yaml + --- + apiVersion: k6.io/v1alpha1 + kind: K6 + metadata: + name: k6-sample + spec: + parallelism: 4 + script: + configMap: + name: 'k6-test' + file: 'script.js' + ``` + + Apply the resource with the following command: + + ```bash + kubectl apply -f /path/to/k6-resource.yaml + ``` + +- **Cloud**: the test runs on [Grafana Cloud k6](https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/). + + ```bash + k6 cloud script.js + ``` + + Additionally, cloud-based solutions can run cloud tests on your [own cloud infrastructure](https://grafana.com/docs/grafana-cloud/k6/author-run/private-load-zone-v2/), and accept the test results from a [local](https://grafana.com/docs/k6//results-output/real-time/cloud) or [distributed test](https://github.com/grafana/k6-operator#k6-cloud-output). diff --git a/docs/sources/v0.50.x/javascript-api/_index.md b/docs/sources/v0.50.x/javascript-api/_index.md new file mode 100644 index 000000000..9f1bf26d8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/_index.md @@ -0,0 +1,268 @@ +--- +weight: 08 +title: JavaScript API +--- + +# JavaScript API + +The list of k6 modules natively supported in your k6 scripts. + +## Init context + +Before the k6 starts the test logic, code in the _init context_ prepares the script. +A few functions are available only in init context. +For details about the runtime, refer to the [Test lifecycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +| Function | Description | +| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| [open( filePath, [mode] )](https://grafana.com/docs/k6//javascript-api/init-context/open) | Opens a file and reads all the contents into memory. | + +## k6 + +The k6 module contains k6-specific functionality. + +| Function | Description | +| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| [check(val, sets, [tags])](https://grafana.com/docs/k6//javascript-api/k6/check) | Runs one or more checks on a value and generates a pass/fail result but does not throw errors or otherwise interrupt execution upon failure. | +| [fail([err])](https://grafana.com/docs/k6//javascript-api/k6/fail) | Throws an error, failing and aborting the current VU script iteration immediately. | +| [group(name, fn)](https://grafana.com/docs/k6//javascript-api/k6/group) | Runs code inside a group. Used to organize results in a test. | +| [randomSeed(int)](https://grafana.com/docs/k6//javascript-api/k6/random-seed) | Set seed to get a reproducible pseudo-random number using `Math.random`. | +| [sleep(t)](https://grafana.com/docs/k6//javascript-api/k6/sleep) | Suspends VU execution for the specified duration. | + +## k6/crypto + +The k6/crypto `module` provides common hashing functionality available in the GoLang [crypto](https://golang.org/pkg/crypto/) package. + +| Function | Description | +| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| [createHash(algorithm)](https://grafana.com/docs/k6//javascript-api/k6-crypto/createhash) | Create a Hasher object, allowing the user to add data to hash multiple times, and extract hash digests along the way. | +| [createHMAC(algorithm, secret)](https://grafana.com/docs/k6//javascript-api/k6-crypto/createhmac) | Create an HMAC hashing object, allowing the user to add data to hash multiple times, and extract hash digests along the way. | +| [hmac(algorithm, secret, data, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/hmac) | Use HMAC to sign an input string. | +| [md4(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/md4) | Use MD4 to hash an input string. | +| [md5(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/md5) | Use MD5 to hash an input string. | +| [randomBytes(int)](https://grafana.com/docs/k6//javascript-api/k6-crypto/randombytes) | Return an array with a number of cryptographically random bytes. | +| [ripemd160(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/ripemd160) | Use RIPEMD-160 to hash an input string. | +| [sha1(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha1) | Use SHA-1 to hash an input string. | +| [sha256(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha256) | Use SHA-256 to hash an input string. | +| [sha384(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha384) | Use SHA-384 to hash an input string. | +| [sha512(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha512) | Use SHA-512 to hash an input string. | +| [sha512_224(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha512_224) | Use SHA-512/224 to hash an input string. | +| [sha512_256(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha512_256) | Use SHA-512/256 to hash an input string. | + +| Class | Description | +| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Hasher](https://grafana.com/docs/k6//javascript-api/k6-crypto/hasher) | Object returned by [crypto.createHash()](https://grafana.com/docs/k6//javascript-api/k6-crypto/createhash). It allows adding more data to be hashed and to extract digests along the way. | + +## k6/data + +The data module provides helpers to work with data. + +| Class/Method | Description | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | +| [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | read-only array like structure that shares memory between VUs | + +## k6/encoding + +The encoding module provides [base64](https://en.wikipedia.org/wiki/Base64) +encoding/decoding as defined by [RFC4648](https://tools.ietf.org/html/rfc4648). + +| Function | Description | +| ----------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| [b64decode(input, [encoding], [format])](http://grafana.com/docs/k6//javascript-api/k6-encoding/b64decode/) | Base64 decode a string. | +| [b64encode(input, [encoding])](http://grafana.com/docs/k6//javascript-api/k6-encoding/b64encode/) | Base64 encode a string. | + +## k6/execution + +`k6/execution` provides the capability to get information about the current test execution state inside the test script. You can read in your script the execution state during the test execution and change your script logic based on the current state. + +The `k6/execution` module provides the test execution information with the following properties: + +- [instance](#instance) +- [scenario](#scenario) +- [test](#test) +- [vu](#vu) + +#### instance + +The instance object provides information associated with the load generator instance. You can think of it as the current running k6 process, which will likely be a single process if you are running k6 on your local machine. When running a cloud/distributed test with multiple load generator instances, the values of the following properties can differ across instances. + +| Property | Type | Description | +| ---------------------- | ------- | ------------------------------------------------------------------------- | +| iterationsInterrupted | integer | The number of prematurely interrupted iterations in the current instance. | +| iterationsCompleted | integer | The number of completed iterations in the current instance. | +| vusActive | integer | The number of active VUs. | +| vusInitialized | integer | The number of currently initialized VUs. | +| currentTestRunDuration | float | The time passed from the start of the current test run in milliseconds. | + +#### scenario + +Meta information and execution details about the current running [scenario](https://grafana.com/docs/k6//using-k6/scenarios). + +| Property | Type | Description | +| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| name | string | The assigned name of the running scenario. | +| executor | string | The name of the running [Executor](https://grafana.com/docs/k6//using-k6/scenarios#executors) type. | +| startTime | integer | The Unix timestamp in milliseconds when the scenario started. | +| progress | float | Percentage in a 0 to 1 interval of the scenario progress. | +| iterationInInstance | integer | The unique and zero-based sequential number of the current iteration in the scenario, across the current instance. | +| iterationInTest | integer | The unique and zero-based sequential number of the current iteration in the scenario. It is unique in all k6 execution modes - in local, cloud and distributed/segmented test runs. However, while every instance will get non-overlapping index values in cloud/distributed tests, they might iterate over them at different speeds, so the values won't be sequential across them. | + +#### test + +Control the test execution. + +| Property | Type | Description | +| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| abort([String]) | function | It aborts the test run with the exit code `108`, and an optional string parameter can provide an error message. Aborting the test will not prevent the `teardown()` execution. | +| options | Object | It returns an object with all the test options as properties. The options' values are consolidated following the [order of precedence](https://grafana.com/docs/k6//using-k6/k6-options/how-to#order-of-precedence) and derived if shortcuts have been used. It returns `null` for properties where the relative option hasn't been defined. | + +#### vu + +Meta information and execution details about the current vu. + +| Property | Type | Description | +| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| iterationInInstance | integer | The identifier of the iteration in the current instance for this VU. This is only unique for current VU and this instance (if multiple instances). This keeps being aggregated if a given VU is reused between multiple scenarios. | +| iterationInScenario | integer | The identifier of the iteration in the current scenario for this VU. This is only unique for current VU and scenario it is currently executing. | +| idInInstance | integer | The identifier of the VU across the instance. Not unique across multiple instances. | +| idInTest | integer | The globally unique (across the whole test run) identifier of the VU. | + +## k6/experimental + +| Modules | Description | +| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| [browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser) | Provides browser-level APIs to interact with browsers and collect frontend performance metrics as part of your k6 tests. | +| [redis](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis) | Functionality to interact with [Redis](https://redis.io/). | +| [timers](https://grafana.com/docs/k6//javascript-api/k6-experimental/timers) | `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` | +| [tracing](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing) | Support for instrumenting HTTP requests with tracing information. | +| [webcrypto](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto) | Implements the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). | +| [websockets](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets) | Implements the browser's [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). | +| [grpc](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc) | Extends `k6/net/grpc` with the streaming capabilities. | +| [fs](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs) | Provides a memory-efficient way to handle file interactions within your test scripts. | + +## k6/html + +The k6/html module contains functionality for HTML parsing. + +| Function | Description | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| [parseHTML(src)](https://grafana.com/docs/k6//javascript-api/k6-html/parsehtml) | Parse an HTML string and populate a [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) object. | + +| Class | Description | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | An HTML DOM element as returned by the [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) API. | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A jQuery-like API for accessing HTML DOM elements. | + +## k6/http + +The k6/http module contains functionality for performing HTTP transactions. + +| Function | Description | +| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| [batch( requests )](https://grafana.com/docs/k6//javascript-api/k6-http/batch) | Issue multiple HTTP requests in parallel (like e.g. browsers tend to do). | +| [cookieJar()](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) | Get active HTTP Cookie jar. | +| [del( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/del) | Issue an HTTP DELETE request. | +| [file( data, [filename], [contentType] )](https://grafana.com/docs/k6//javascript-api/k6-http/file) | Create a file object that is used for building multi-part requests. | +| [get( url, [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/get) | Issue an HTTP GET request. | +| [head( url, [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/head) | Issue an HTTP HEAD request. | +| [options( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/options) | Issue an HTTP OPTIONS request. | +| [patch( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/patch) | Issue an HTTP PATCH request. | +| [post( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/post) | Issue an HTTP POST request. | +| [put( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/put) | Issue an HTTP PUT request. | +| [request( method, url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/request) | Issue any type of HTTP request. | +| [asyncRequest( method, url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/asyncrequest) | Issue any type of HTTP request asynchronously. | +| [setResponseCallback(expectedStatuses)](https://grafana.com/docs/k6//javascript-api/k6-http/set-response-callback) | Sets a response callback to mark responses as expected. | +| [url\`url\`](https://grafana.com/docs/k6//javascript-api/k6-http/url) | Creates a URL with a name tag. Read more on [URL Grouping](https://grafana.com/docs/k6//using-k6/http-requests#url-grouping). | +| [expectedStatuses( statusCodes )](https://grafana.com/docs/k6//javascript-api/k6-http/expected-statuses) | Create a callback for setResponseCallback that checks status codes. | + +| Class | Description | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| [CookieJar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) | Used for storing cookies, set by the server and/or added by the client. | +| [FileData](https://grafana.com/docs/k6//javascript-api/k6-http/filedata) | Used for wrapping data representing a file when doing multipart requests (file uploads). | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) | Used for setting various HTTP request-specific parameters such as headers, cookies, etc. | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | Returned by the http.\* methods that generate HTTP requests. | + +## k6/metrics + +The metrics module provides functionality to [create custom metrics](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics) of various types. +All metrics (both the [built-in metrics](https://grafana.com/docs/k6//using-k6/metrics/reference) and the custom ones) have a type. + +You can optionally [tag](https://grafana.com/docs/k6//using-k6/tags-and-groups) all values added to a custom metric, which can be useful when analysing the test results. + +| Metric type | Description | +| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| [Counter](https://grafana.com/docs/k6//javascript-api/k6-metrics/counter) | A metric that cumulatively sums added values. | +| [Gauge](https://grafana.com/docs/k6//javascript-api/k6-metrics/gauge) | A metric that stores the min, max and last values added to it. | +| [Rate](https://grafana.com/docs/k6//javascript-api/k6-metrics/rate) | A metric that tracks the percentage of added values that are non-zero. | +| [Trend](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend) | A metric that calculates statistics on the added values (min, max, average, and percentiles). | + +## k6/net/grpc + +The `k6/net/grpc` module provides a [gRPC](https://grpc.io/) client for Remote Procedure Calls (RPC) over HTTP/2. + +| Class/Method | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Client](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client) | gRPC client used for making RPC calls to a gRPC Server. | +| [Client.load(importPaths, ...protoFiles)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-load) | Loads and parses the given protocol buffer definitions to be made available for RPC requests. | +| [Client.connect(address [,params])](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-connect) | Connects to a given gRPC service. | +| [Client.invoke(url, request [,params])](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-invoke) | Makes an unary RPC for the given service/method and returns a [Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response). | +| [Client.close()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-close) | Close the connection to the gRPC service. | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/params) | RPC Request specific options. | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response) | Returned by RPC requests. | +| [Constants](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/constants) | Define constants to distinguish between [gRPC Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response) statuses. | + +## k6/ws + +The ws module provides a [WebSocket](https://en.wikipedia.org/wiki/WebSocket) client implementing the [WebSocket protocol](http://www.rfc-editor.org/rfc/rfc6455.txt). + +| Function | Description | +| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [connect( url, params, callback )](https://grafana.com/docs/k6//javascript-api/k6-ws/connect) | Create a WebSocket connection, and provides a [Socket](https://grafana.com/docs/k6//javascript-api/k6-ws/socket) client to interact with the service. The method blocks the test finalization until the connection is closed. | + +| Class/Method | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-ws/params) | Used for setting various WebSocket connection parameters such as headers, cookie jar, compression, etc. | +| [Socket](https://grafana.com/docs/k6//javascript-api/k6-ws/socket) | WebSocket client used to interact with a WS connection. | +| [Socket.close()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-close) | Close the WebSocket connection. | +| [Socket.on(event, callback)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-on) | Set up an event listener on the connection for any of the following events:
- open
- binaryMessage
- message
- ping
- pong
- close
- error. | +| [Socket.ping()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-ping) | Send a ping. | +| [Socket.send(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-send) | Send string data. | +| [Socket.sendBinary(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-sendbinary) | Send binary data. | +| [Socket.setInterval(callback, interval)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-setinterval) | Call a function repeatedly at certain intervals, while the connection is open. | +| [Socket.setTimeout(callback, period)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-settimeout) | Call a function with a delay, if the connection is open. | + +## Error codes + +The following specific error codes are currently defined: + +- 1000: A generic error that isn't any of the ones listed below. +- 1010: A non-TCP network error - this is a place holder there is no error currently known to trigger it. +- 1020: An invalid URL was specified. +- 1050: The HTTP request has timed out. +- 1100: A generic DNS error that isn't any of the ones listed below. +- 1101: No IP for the provided host was found. +- 1110: Blacklisted IP was resolved or a connection to such was tried to be established. +- 1111: Blacklisted hostname using The [Block Hostnames](https://grafana.com/docs/k6//using-k6/k6-options/reference#block-hostnames) option. +- 1200: A generic TCP error that isn't any of the ones listed below. +- 1201: A "broken pipe" on write - the other side has likely closed the connection. +- 1202: An unknown TCP error - We got an error that we don't recognize but it is from the operating system and has `errno` set on it. The message in `error` includes the operation(write,read) and the errno, the OS, and the original message of the error. +- 1210: General TCP dial error. +- 1211: Dial timeout error - the timeout for the dial was reached. +- 1212: Dial connection refused - the connection was refused by the other party on dial. +- 1213: Dial unknown error. +- 1220: Reset by peer - the connection was reset by the other party, most likely a server. +- 1300: General TLS error +- 1310: Unknown authority - the certificate issuer is unknown. +- 1311: The certificate doesn't match the hostname. +- 1400 to 1499: error codes that correspond to the [HTTP 4xx status codes for client errors](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors) +- 1500 to 1599: error codes that correspond to the [HTTP 5xx status codes for server errors](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors) +- 1600: A generic HTTP/2 error that isn't any of the ones listed below. +- 1610: A general HTTP/2 GoAway error. +- 1611 to 1629: HTTP/2 GoAway errors with the value of the specific [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) added to 1611. +- 1630: A general HTTP/2 stream error. +- 1631 to 1649: HTTP/2 stream errors with the value of the specific [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) added to 1631. +- 1650: A general HTTP/2 connection error. +- 1651 to 1669: HTTP/2 connection errors with the value of the specific [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) added to 1651. +- 1701: Decompression error. + +Read more about [Error codes](https://grafana.com/docs/k6//javascript-api/error-codes). diff --git a/docs/sources/v0.50.x/javascript-api/error-codes.md b/docs/sources/v0.50.x/javascript-api/error-codes.md new file mode 100644 index 000000000..88100b0c9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/error-codes.md @@ -0,0 +1,53 @@ +--- +title: 'Error Codes' +description: 'Error codes are unique numbers that can be used to identify and handle different application and network errors more easily.' +weight: 13 +--- + +# Error Codes + +Error codes are unique numbers that can be used to identify and handle different application and network errors more easily. For the moment, these error codes are applicable only for errors that happen during HTTP requests, but they will be reused and extended to support other protocols in future k6 releases. + +When an error occurs, its code is determined and returned as both the `error_code` field of the [`http.Response`](https://grafana.com/docs/k6//javascript-api/k6-http/response) object, and also attached as the `error_code` [tag](https://grafana.com/docs/k6//using-k6/tags-and-groups) to any [metrics](https://grafana.com/docs/k6//using-k6/metrics) associated with that request. Additionally, for more details, the `error` metric tag and `http.Response` field will still contain the actual string error message. + +Error codes for different errors are as distinct as possible, but for easier handling and grouping, codes in different error categories are also grouped in broad ranges. The current error code ranges are: + +- 1000-1099 - General errors +- 1100-1199 - DNS errors +- 1200-1299 - TCP errors +- 1300-1399 - TLS errors +- 1400-1499 - HTTP 4xx errors +- 1500-1599 - HTTP 5xx errors +- 1600-1699 - HTTP/2 specific errors + +The following specific error codes are currently defined: + +- 1000: A generic error that isn't any of the ones listed below. +- 1010: A non-TCP network error - this is a place holder there is no error currently known to trigger it. +- 1020: An invalid URL was specified. +- 1050: The HTTP request has timed out. +- 1100: A generic DNS error that isn't any of the ones listed below. +- 1101: No IP for the provided host was found. +- 1110: Blacklisted IP was resolved or a connection to such was tried to be established. +- 1111: Blacklisted hostname using The [Block Hostnames](https://grafana.com/docs/k6//using-k6/k6-options/reference#block-hostnames) option. +- 1200: A generic TCP error that isn't any of the ones listed below. +- 1201: A "broken pipe" on write - the other side has likely closed the connection. +- 1202: An unknown TCP error - We got an error that we don't recognize but it is from the operating system and has `errno` set on it. The message in `error` includes the operation(write,read) and the errno, the OS, and the original message of the error. +- 1210: General TCP dial error. +- 1211: Dial timeout error - the timeout for the dial was reached. +- 1212: Dial connection refused - the connection was refused by the other party on dial. +- 1213: Dial unknown error. +- 1220: Reset by peer - the connection was reset by the other party, most likely a server. +- 1300: General TLS error +- 1310: Unknown authority - the certificate issuer is unknown. +- 1311: The certificate doesn't match the hostname. +- 1400 to 1499: error codes that correspond to the [HTTP 4xx status codes for client errors](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_Client_errors) +- 1500 to 1599: error codes that correspond to the [HTTP 5xx status codes for server errors](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_Server_errors) +- 1600: A generic HTTP/2 error that isn't any of the ones listed below. +- 1610: A general HTTP/2 GoAway error. +- 1611 to 1629: HTTP/2 GoAway errors with the value of the specific [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) added to 1611. +- 1630: A general HTTP/2 stream error. +- 1631 to 1649: HTTP/2 stream errors with the value of the specific [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) added to 1631. +- 1650: A general HTTP/2 connection error. +- 1651 to 1669: HTTP/2 connection errors with the value of the specific [HTTP/2 error code](https://tools.ietf.org/html/rfc7540#section-7) added to 1651. +- 1701: Decompression error. diff --git a/docs/sources/v0.50.x/javascript-api/init-context/_index.md b/docs/sources/v0.50.x/javascript-api/init-context/_index.md new file mode 100644 index 000000000..fce10b58f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/init-context/_index.md @@ -0,0 +1,15 @@ +--- +title: 'Init context' +description: 'The init context (aka "init code") is code in the global context that has access to a few functions not accessible during main script execution.' +weight: 01 +--- + +# Init context + +Before the k6 starts the test logic, code in the _init context_ prepares the script. +A few functions are available only in init context. +For details about the runtime, refer to the [Test lifecycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +| Function | Description | +| ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| [open( filePath, [mode] )](https://grafana.com/docs/k6//javascript-api/init-context/open) | Opens a file and reads all the contents into memory. | diff --git a/docs/sources/v0.50.x/javascript-api/init-context/open.md b/docs/sources/v0.50.x/javascript-api/init-context/open.md new file mode 100644 index 000000000..1b2fb1bd5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/init-context/open.md @@ -0,0 +1,117 @@ +--- +head_title: 'JavaScript API: open' +title: 'open( filePath, [mode] )' +description: 'Opens a file and reads all the contents into memory.' +description: 'Opens a file and reads all the contents into memory.' +--- + +# open( filePath, [mode] ) + +Opens a file, reading all its contents into memory for use in the script. + +{{% admonition type="note" %}} + +`open()` often consumes a large amount of memory because every VU keeps a separate copy of the file in memory. + +To reduce the memory consumption, we strongly advise the usage of [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) for CSV, JSON and other files intended for script parametrization. + +{{% /admonition %}} + +{{% admonition type="caution" %}} + +This function can only be called from the init context (aka _init code_), code in the global context that is, outside of the main export default function { ... }. + +By restricting it to the init context, we can easily determine what local files are needed to run the test and thus what we need to bundle up when distributing the test to multiple nodes in a clustered/distributed test. + +See the example further down on this page. For a more in-depth description, see [Test lifecycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +{{% /admonition %}} + +| Parameter | Type | Description | +| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| filePath | string | The path to the file, absolute or relative, that will be read into memory. The file will only be loaded once, even when running with several VUs. | +| mode | string | By default, the contents of the file are read as text, but if you specify `b`, the file will be read as binary data instead. | + +### Returns + +| Type | Description | +| -------------------- | ----------------------------------------------------------------------------------------------- | +| string / ArrayBuffer | The contents of the file, returned as string or ArrayBuffer (if `b` was specified as the mode). | + +{{< code >}} + +```json +[ + { + "username": "user1", + "password": "password1" + }, + { + "username": "user2", + "password": "password2" + }, + { + "username": "user3", + "password": "password3" + } +] +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { SharedArray } from 'k6/data'; +import { sleep } from 'k6'; + +const data = new SharedArray('users', function () { + // here you can open files, and then do additional processing or generate the array with data dynamically + const f = JSON.parse(open('./users.json')); + return f; // f must be an array[] +}); + +export default () => { + const randomUser = data[Math.floor(Math.random() * data.length)]; + console.log(`${randomUser.username}, ${randomUser.password}`); + sleep(3); +}; +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { sleep } from 'k6'; + +const users = JSON.parse(open('./users.json')); // consider using SharedArray for large files + +export default function () { + const user = users[__VU - 1]; + console.log(`${user.username}, ${user.password}`); + sleep(3); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +const binFile = open('/path/to/file.bin', 'b'); + +export default function () { + const data = { + field: 'this is a standard form field', + file: http.file(binFile, 'test.bin'), + }; + const res = http.post('https://example.com/upload', data); + sleep(3); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/_index.md new file mode 100644 index 000000000..9239a7161 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/_index.md @@ -0,0 +1,16 @@ +--- +title: 'jslib' +description: 'External JavaScript libraries for k6' +weight: 15 +--- + +# jslib + +The [jslib.k6.io](https://jslib.k6.io/) is a collection of external JavaScript libraries that can be [directly imported](https://grafana.com/docs/k6//using-k6/modules#remote-http-s-modules) in k6 scripts. + +| Library | Description | +| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| [aws](https://grafana.com/docs/k6//javascript-api/jslib/aws) | Library allowing to interact with Amazon AWS services | +| [httpx](https://grafana.com/docs/k6//javascript-api/jslib/httpx) | Wrapper around [k6/http](https://grafana.com/docs/k6//javascript-api/#k6http) to simplify session handling | +| [k6chaijs](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs) | BDD assertion style | +| [utils](https://grafana.com/docs/k6//javascript-api/jslib/utils) | Small utility functions useful in every day load testing | diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/EventBridgeClient/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/EventBridgeClient/_index.md new file mode 100644 index 000000000..c5a0516d2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/EventBridgeClient/_index.md @@ -0,0 +1,68 @@ +--- +title: 'EventBridgeClient' +head_title: 'EventBridgeClient' +description: 'EventBridgeClient allows interacting with AWS EventBridge service' +description: 'EventBridgeClient class allows sending custom events to Amazon EventBridge so that they can be matched to rules.' +weight: 00 +--- + +# EventBridgeClient + +`EventBridgeClient` interacts with the AWS EventBridge service. + +With it, you can send custom events to Amazon EventBridge. These events can then be matched to rules defined in EventBridge. For a full list of supported operations, see [Methods](#methods). + +Both the dedicated `event-bridge.js` jslib bundle and the all-encompassing `aws.js` bundle include the `EventBridgeClient`. + +### Methods + +| Function | Description | +| :---------------------------------------------------------------------------------------------------------------- | :---------------------------------------- | +| [putEvents(input)](https://grafana.com/docs/k6//javascript-api/jslib/aws/eventbridgeclient/putevents) | Send custom events to Amazon EventBridge. | + +### Throws + +EventBridgeClient methods will throw errors in case of failure. + +| Error | Condition | +| :---------------------- | :-------------------------------------------------------------------------- | +| InvalidSignatureError | when invalid credentials were provided or the request signature is invalid. | +| EventBridgeServiceError | when AWS replied to the requested operation with an error. | + +### Examples + +{{< code >}} + +```javascript +import { AWSConfig, EventBridgeClient } from 'https://jslib.k6.io/aws/0.11.0/event-bridge.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const eventBridge = new EventBridgeClient(awsConfig); + +export default async function () { + const eventDetails = { + Source: 'my.custom.source', + Detail: { key1: 'value1', key2: 'value2' }, + DetailType: 'MyDetailType', + Resources: ['arn:aws:resource1'], + }; + + const input = { + Entries: [eventDetails], + }; + + try { + await eventBridge.putEvents(input); + } catch (error) { + console.error(`Failed to put events: ${error.message}`); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/EventBridgeClient/putEvents.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/EventBridgeClient/putEvents.md new file mode 100644 index 000000000..5013541d7 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/EventBridgeClient/putEvents.md @@ -0,0 +1,73 @@ +--- +title: 'putEvents' +head_title: 'EventBridgeClient.putEvents' +description: 'EventBridgeClient.putEvents sends custom events to Amazon EventBridge' +description: 'EventBridgeClient.putEvents sends custom events to Amazon EventBridge' +weight: 10 +--- + +# putEvents + +`EventBridgeClient.putEvents` sends custom events to Amazon EventBridge so that they can be matched to rules. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :-------------------------------- | :------------------------------------------------------- | +| input | [PutEventsInput](#puteventsinput) | An array of objects representing events to be submitted. | + +#### PutEventsInput + +| Parameter | Type | Description | +| :--------- | :-------------------------------------- | :------------------------------------------------------- | +| Entries | [EventBridgeEntry](#eventbridgeentry)[] | An array of objects representing events to be submitted. | +| EndpointId | string (optional) | The ID of the target to receive the event. | + +#### EventBridgeEntry + +| Parameter | Type | Description | +| :----------- | :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------ | +| Source | string | The source of the event. | +| Detail | object | A JSON object containing event data. | +| DetailType | string | Free-form string used to decide what fields to expect in the event detail. | +| Resources | string[] (optional) | AWS resources, identified by Amazon Resource Name (ARN), which the event primarily concerns. | +| EventBusName | string (optional) | The event bus that will receive the event. If you omit this, the default event bus is used. Only the AWS account that owns a bus can write to it. | + +### Returns + +| Type | Description | +| :-------------- | :---------------------------------------------------------------------------- | +| `Promise` | A Promise that fulfills when the events have been sent to Amazon EventBridge. | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, EventBridgeClient } from 'https://jslib.k6.io/aws/0.11.0/event-bridge.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const eventBridge = new EventBridgeClient(awsConfig); +const eventEntry = { + Source: 'my.source', + Detail: { + key: 'value', + }, + DetailType: 'MyDetailType', + Resources: ['resource-arn'], +}; + +export default async function () { + await eventBridge.putEvents({ + Entries: [eventEntry], + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/KMSDataKey.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/KMSDataKey.md new file mode 100644 index 000000000..41d764c4f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/KMSDataKey.md @@ -0,0 +1,56 @@ +--- +title: 'KMSDataKey' +slug: 'kmsdatakey' +head_title: 'KMSDataKey' +description: 'KMSDataKey is returned by the KMSClient.*DataKey methods that query KMS data keys' +weight: 20 +--- + +# KMSDataKey + +`KMSClient.*DataKey` methods, querying Key Management Service data keys, return some KMSDataKey instances. +The KMSDataKey object describes an Amazon Key Management Service data key. +For instance, the [`generateDataKey`](https://grafana.com/docs/k6//javascript-api/jslib/aws/kmsclient/generatedatakey/) returns the generated KMSDataKey object. + +| Name | Type | Description | +| :-------------------------- | :----- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | +| `KMSDataKey.id` | string | The identifier of the Key Management Service key that encrypted the data key. | +| `KMSDataKey.ciphertextBlob` | string | The base64-encoded encrypted copy of the data key. | +| `KMSDataKey.plaintext` | string | The plain text data key. Use this data key to encrypt your data outside of Key Management Service. Then, remove it from memory as soon as possible. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, KMSClient } from 'https://jslib.k6.io/aws/0.11.0/kms.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const kms = new KMSClient(awsConfig); +const testKeyId = 'e67f95-4c047567-4-a0b7-62f7ce8ec8f48'; + +export default async function () { + // List the KMS keys the AWS authentication configuration + // gives us access to. + const keys = await kms.listKeys(); + + // If our test key does not exist, abort the execution. + if (keys.filter((b) => b.keyId === testKeyId).length == 0) { + exec.test.abort(); + } + + // Generate a data key from the KMS key. + const key = await kms.generateDataKey(testKeyId, 32); +} +``` + +_A k6 script that generating a data key from an AWS Key Management Service key_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/KMSKey.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/KMSKey.md new file mode 100644 index 000000000..9fd03fd64 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/KMSKey.md @@ -0,0 +1,50 @@ +--- +title: 'KMSKey' +head_title: 'KMSKey' +description: 'KMSKey is returned by the KMSClient.* methods that query KMS keys' +description: 'KMSKey is returned by the KMSClient.* methods that query KMS keys' +weight: 20 +--- + +# KMSKey + +`KMSClient.*` methods querying Key Management Service keys return some `KMSKey` instances. Namely, `listKeys()` returns an array of `KMSKey` objects. The `KMSKey` object describes an Amazon Key Management Service key. + +| Name | Type | Description | +| :-------------- | :----- | :--------------------------- | +| `KMSKey.keyId` | string | Unique identifier of the key | +| `KMSKey.keyArn` | string | ARN of the key | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, KMSClient } from 'https://jslib.k6.io/aws/0.11.0/kms.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const kms = new KMSClient(awsConfig); +const testKeyId = 'e67f95-4c047567-4-a0b7-62f7ce8ec8f48'; + +export default async function () { + // List the KMS keys the AWS authentication configuration + // gives us access to. + const keys = await kms.listKeys(); + + // If our test key does not exist, abort the execution. + if (keys.filter((b) => b.keyId === testKeyId).length == 0) { + exec.test.abort(); + } +} +``` + +_A k6 script querying the user's Key Management Service keys and verifying their test key exists_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/_index.md new file mode 100644 index 000000000..5bdff0654 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/_index.md @@ -0,0 +1,72 @@ +--- +title: 'KMSClient' +head_title: 'KMSClient' +description: 'KMSClient allows interacting with the AWS Key Management Service' +description: 'KMSClient allows interacting with the AWS Key Management Service' +weight: 00 +--- + +# KMSClient + +{{< docs/shared source="k6" lookup="blocking-aws-blockquote.md" version="" >}} + +`KMSClient` interacts with the AWS Key Management Service. + +With it, the user can list all the Key Management Service keys in the caller's AWS account and region. They can also generate symmetric data keys to use outside of AWS Key Management Service. + +Both the dedicated `kms.js` jslib bundle and the all-encompassing `aws.js` bundle include the `KMSClient`. + +### Methods + +| Function | Description | +| :------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | +| [listKeys](https://grafana.com/docs/k6//javascript-api/jslib/aws/kmsclient/listkeys) | List the all the Key Management Service keys in the caller's AWS account and region. | +| [generateDataKey](https://grafana.com/docs/k6//javascript-api/jslib/aws/kmsclient/generatedatakey) | Generate a symmetric data key for use outside of the AWS Key Management Service. | + +### Throws + +`KMSClient` methods throw errors in case of failure. + +| Error | Condition | +| :---------------------- | :-------------------------------------------------------- | +| `InvalidSignatureError` | when using invalid credentials | +| `KMSServiceError` | when AWS replied to the requested operation with an error | + +### Example + +{{< code >}} + +```javascript +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; + +import { AWSConfig, KMSClient } from 'https://jslib.k6.io/aws/0.11.0/kms.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const kms = new KMSClient(awsConfig); +const keyAlias = 'alias/k6-key'; + +export async function setup() { + // Create a symmetric data key + return { + dataKey: await kms.generateDataKey(keyAlias, 32), + }; +} + +export default async function (data) { + // Use the data key to encrypt data +} + +export function handleSummary(data) { + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), + './test-run.key': data.dataKey, + }; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/generateDataKey.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/generateDataKey.md new file mode 100644 index 000000000..fb68e0f09 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/generateDataKey.md @@ -0,0 +1,61 @@ +--- +aliases: + - ./kmsclient-generatedatakey/ # /docs/k6//javascript-api/jslib/aws/kmsclient/kmsclient-generatedatakey/ +title: 'generateDataKey' +description: 'KMSClient.generateDataKey generates a symmetric data key for use outside of the AWS Key Management Service' +weight: 10 +--- + +# generateDataKey + +`KMSClient.generateDataKey` generates a symmetric data key for use outside of the AWS Key Management Service. + +### Parameters + +| Name | Type | Description | +| :----- | :----- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `id` | string | The identifier of the key. This can be either the key ID or the Amazon Resource Name (ARN) of the key. | +| `size` | number | The length of the data key. For example, use the value 64 to generate a 512-bit data key (64 bytes is 512 bits). For 256-bit (32-byte) data keys, use the value 32, instead. | + +### Returns + +| Type | Description | +| :------------------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[KMSDataKey](https://grafana.com/docs/k6//javascript-api/jslib/aws/kmsclient/kmsdatakey)> | A Promise that fulfills with a [KMSDataKey](https://grafana.com/docs/k6//javascript-api/jslib/aws/kmsclient/kmskey) object. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, KMSClient } from 'https://jslib.k6.io/aws/0.11.0/kms.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const kms = new KMSClient(awsConfig); +const testKeyId = 'e67f95-4c047567-4-a0b7-62f7ce8ec8f48'; + +export default async function () { + // List the KMS keys the AWS authentication configuration + // gives us access to. + const keys = await kms.listKeys(); + + // If our test key does not exist, abort the execution. + if (keys.filter((b) => b.keyId === testKeyId).length == 0) { + exec.test.abort(); + } + + // Generate a data key from the KMS key. + const key = await kms.generateDataKey(testKeyId, 32); +} +``` + +_A k6 script that generating a data key from an AWS Key Management Service key_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/listKeys.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/listKeys.md new file mode 100644 index 000000000..8d41a5961 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/KMSClient/listKeys.md @@ -0,0 +1,51 @@ +--- +title: 'listKeys' +head_title: 'KMSClient.listKeys()' +description: "KMSClient.listKeys lists all the KMS keys in the caller's AWS account and region" +description: "KMSClient.listKeys lists all the KMS keys in the caller's AWS account and region" +weight: 10 +--- + +# listKeys + +`KMSClient.listKeys()` lists all the Key Management Service keys in the caller's AWS account and region. + +### Returns + +| Type | Description | +| :------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[KMSKey[]](https://grafana.com/docs/k6//javascript-api/jslib/aws/kmsclient/kmskey)> | A Promise that fulfills with an array of [`KMSKey`](https://grafana.com/docs/k6//javascript-api/jslib/aws/kmsclient/kmskey) objects. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, KMSClient } from 'https://jslib.k6.io/aws/0.11.0/kms.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const kms = new KMSClient(awsConfig); +const testKeyId = 'e67f95-4c047567-4-a0b7-62f7ce8ec8f48'; + +export default async function () { + // List the KMS keys the AWS authentication configuration + // gives us access to. + const keys = await kms.listKeys(); + + // If our test key does not exist, abort the execution. + if (keys.filter((b) => b.keyId === testKeyId).length == 0) { + exec.test.abort(); + } +} +``` + +_A k6 script querying the user's Key Management Service keys and verifying their test key exists_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/Bucket.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/Bucket.md new file mode 100755 index 000000000..287036095 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/Bucket.md @@ -0,0 +1,42 @@ +--- +title: 'Bucket' +head_title: 'Bucket' +description: 'Bucket is returned by the S3Client.* methods who query S3 buckets.' +weight: 20 +--- + +# Bucket + +Bucket is returned by the S3Client.\* methods that query S3 buckets. Namely, `listBuckets()` returns an array of Bucket objects. The Bucket object describes an Amazon S3 bucket. + +| Name | Type | Description | +| :-------------------- | :----- | :--------------------------- | +| `Bucket.name` | string | The S3 bucket's name | +| `Bucket.creationDate` | Date | The S3 bucket's creationDate | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); + +export default async function () { + // List the buckets the AWS authentication configuration + // gives us access to. + const buckets = await s3.listBuckets(); + console.log(JSON.stringify(buckets)); +} +``` + +_A k6 script that will query the user's S3 buckets and print all of their metadata_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/Object.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/Object.md new file mode 100755 index 000000000..4031a34f5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/Object.md @@ -0,0 +1,61 @@ +--- +title: 'Object' +head_title: 'Object' +description: "Object is returned by the S3Client.* methods who query S3 buckets' objects." +weight: 20 +--- + +# Object + +Object is returned by the S3Client.\* methods that query S3 buckets' objects. Namely, [`listObjects`](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/listobjects), [`getObject`](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/getobject), [`putObject`](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/putobject), +and [`deleteObject`](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/deleteobject). The Object construct describes an Amazon S3 object. + +| Name | Type | Description | +| :-------------------- | :---------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Object.key` | string | The S3 object's name. | +| `Object.lastModified` | number | The S3 object's last modification date. | +| `Object.etag` | string | The S3 object's `etag` is a hash of the object. The ETag reflects changes only to the contents of an object, not its metadata. The ETag may or may not be an MD5 digest of the object data. | +| `Object.size` | size | The S3 object's size in bytes. | +| `Object.storageClass` | `STANDARD` \| `REDUCED_REDUNDANCY` \| `GLACIER` \| `STANDARD_IA` \| `INTELLIGENT_TIERING` \| `DEEP_ARCHIVE` \| `OUTPOSTS` \| `GLACIER_IR` | The S3 object's class of storage used to store it. | +| `Object.data` | `string` or `bytes` or `null` | The S3 object's content. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { + // listBuckets, + AWSConfig, + S3Client, +} from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'bonjour.txt'; + +export default async function () { + const objects = await s3.listObjects(testBucketName); + + // If our test object does not exist, abort the execution. + if (objects.filter((o) => o.key === testFileKey).length == 0) { + exec.test.abort(); + } + + // Let's download our test object and print its content + const object = await s3.getObject(testBucketName, testFileKey); + console.log(JSON.stringify(object)); +} +``` + +_A k6 script that will query a S3 bucket's objects and print its content and metadata_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/S3MultipartUpload.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/S3MultipartUpload.md new file mode 100644 index 000000000..9549e00fc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/S3MultipartUpload.md @@ -0,0 +1,47 @@ +--- +title: 'S3MultipartUpload' +description: 'S3MultipartUpload is returned by the S3Client.createMultipartUpload method when creating a multipart upload.' +weight: 20 +--- + +# S3MultipartUpload + +S3MultipartUpload is returned by the [`createMultipartUpload(bucketName, objectKey)`](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/createmultipartupload) method when creating a [multipart upload](https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateMultipartUpload.html). + +| Name | Type | Description | +| :--------------------------- | :----- | :---------------------------- | +| `S3MultipartUpload.key` | string | The S3 Multipart object's key | +| `S3MultipartUpload.uploadId` | Date | The S3 Multipart upload Id | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const s3 = new S3Client(awsConfig); + +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'multipart.txt'; + +export default async function () { + // Initialize a multipart upload + const multipartUpload = await s3.createMultipartUpload(testBucketName, testFileKey); + console.log(multipartUpload.uploadId); + + // Abort multipart upload + await s3.abortMultipartUpload(testBucketName, testFileKey, multipartUpload.uploadId); +} +``` + +_A k6 script that will create a multipart upload and log the multipart `uploadId` and abort the multipart upload_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/S3Part.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/S3Part.md new file mode 100755 index 000000000..c84e89ecf --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/S3Part.md @@ -0,0 +1,83 @@ +--- +title: 'S3Part' +head_title: 'S3Part' +slug: 's3part' +description: 'S3Part is returned by the S3Client.uploadPart method when uploading a part to a multipart upload.' +weight: 20 +--- + +# S3Part + +S3Part is returned by the [`uploadPart(bucketName, objectKey, uploadId, partNumber, data)`](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/uploadpart) method when uploading a part to a multipart upload. The S3Part object describes an Amazon S3 Part. + +| Name | Type | Description | +| :-------------------- | :----- | :----------------- | +| `S3Part.partNumber` | number | The S3 Part'number | +| `S3Part.eTag ` | String | The S3 Part's etag | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const s3 = new S3Client(awsConfig); + +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'multipart.txt'; + +export default async function () { + // Produce random bytes to upload of size ~12MB, that + // we will upload in two 6MB parts. This is done as the + // minimum part size supported by S3 is 5MB. + const bigFile = crypto.randomBytes(12 * 1024 * 1024); + + // Initialize a multipart upload + const multipartUpload = await s3.createMultipartUpload(testBucketName, testFileKey); + + // Upload the first part + const firstPartData = bigFile.slice(0, 6 * 1024 * 1024); + const firstPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 1, + firstPartData + ); + + // Upload the second part + const secondPartData = bigFile.slice(6 * 1024 * 1024, 12 * 1024 * 1024); + const secondPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 2, + secondPartData + ); + + // Complete the multipart upload + await s3.completeMultipartUpload(testBucketName, testFileKey, multipartUpload.uploadId, [ + firstPart, + secondPart, + ]); + + // Let's redownload it verify it's correct, and delete it + const obj = await s3.getObject(testBucketName, testFileKey); + await s3.deleteObject(testBucketName, testFileKey); +} +``` + +_A k6 script that will upload a part in a multipart upload to an S3 bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/_index.md new file mode 100644 index 000000000..5327ee86b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/_index.md @@ -0,0 +1,178 @@ +--- +title: 'S3Client' +head_title: 'S3Client' +description: 'S3Client allows interacting with AWS S3 buckets and objects' +description: 'S3Client class allows interacting with AWS S3 buckets and objects' +weight: 00 +--- + +# S3Client + +{{< docs/shared source="k6" lookup="blocking-aws-blockquote.md" version="" >}} + +`S3Client` interacts with the AWS Simple Storage Service (S3). + +With it, you can do several operations such as list buckets, list objects in a bucket, or download objects from a bucket. For a full list of supported operations, see [Methods](#methods). + +Both the dedicated `s3.js` jslib bundle and the all-encompassing `aws.js` bundle include the `S3Client`. + +### Methods + +| Function | Description | +| :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------- | +| [listBuckets()](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/listbuckets) | List the buckets the authenticated user has access to | +| [listObjects(bucketName, [prefix])](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/listobjects) | List the objects contained in a bucket | +| [getObject(bucketName, objectKey)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/getobject) | Download an object from a bucket | +| [putObject(bucketName, objectKey, data)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/putobject) | Upload an object to a bucket | +| [deleteObject(bucketName, objectKey)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/deleteobject) | Delete an object from a bucket | +| [copyObject(sourceBucket, sourceKey, destinationBucket, destinationKey)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/copyobject) | Copy an object from one bucket to another | +| [createMultipartUpload(bucketName, objectKey)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/createmultipartupload) | Create a multipart upload for a given objectKey to a bucket | +| [uploadPart(bucketName, objectKey, uploadId, partNumber, data)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/uploadpart) | Upload a part in a multipart upload | +| [completeMultipartUpload(bucketName, objectKey, uploadId, parts)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/completemultipartupload) | Complete a previously assembled multipart upload | +| [abortMultipartUpload(bucketName, objectKey, uploadId)](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/abortmultipartupload) | Abort a multipart upload | + +### Throws + +S3 Client methods will throw errors in case of failure. + +| Error | Condition | +| :-------------------- | :--------------------------------------------------------- | +| InvalidSignatureError | when invalid credentials were provided. | +| S3ServiceError | when AWS replied to the requested operation with an error. | + +### Examples + +{{< code >}} + +```javascript +import { check } from 'k6'; +import exec from 'k6/execution'; +import http from 'k6/http'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; +const testInputFileKey = 'productIDs.json'; +const testOutputFileKey = `results-${Date.now()}.json`; + +export async function setup() { + // If our test bucket does not exist, abort the execution. + const buckets = await s3.listBuckets(); + if (buckets.filter((b) => b.name === testBucketName).length == 0) { + exec.test.abort(); + } + + // If our test object does not exist, abort the execution. + const objects = await s3.listObjects(testBucketName); + if (objects.filter((o) => o.key === testInputFileKey).length == 0) { + exec.test.abort(); + } + + // Download the S3 object containing our test data + const inputObject = await s3.getObject(testBucketName, testInputFileKey); + + // Let's return the downloaded S3 object's data from the + // setup function to allow the default function to use it. + return { + productIDs: JSON.parse(inputObject.data), + }; +} + +export default async function (data) { + // Pick a random product ID from our test data + const randomProductID = data.productIDs[Math.floor(Math.random() * data.productIDs.length)]; + + // Query our ecommerce website's product page using the ID + const res = await http.asyncRequest('GET', `http://your.website.com/product/${randomProductID}/`); + check(res, { 'is status 200': res.status === 200 }); +} + +export async function handleSummary(data) { + // Once the load test is over, let's upload the results to our + // S3 bucket. This is executed after teardown. + await s3.putObject(testBucketName, testOutputFileKey, JSON.stringify(data)); +} +``` + +{{< /code >}} + +#### Multipart uploads + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const s3 = new S3Client(awsConfig); + +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'multipart.txt'; + +export default async function () { + // List the buckets the AWS authentication configuration + // gives us access to. + const buckets = await s3.listBuckets(); + + // If our test bucket does not exist, abort the execution. + if (buckets.filter((b) => b.name === testBucketName).length == 0) { + exec.test.abort(); + } + + // Produce random bytes to upload of size ~12MB, that + // we will upload in two 6MB parts. This is done as the + // minimum part size supported by S3 is 5MB. + const bigFile = crypto.randomBytes(12 * 1024 * 1024); + + // Initialize a multipart upload + const multipartUpload = await s3.createMultipartUpload(testBucketName, testFileKey); + + // Upload the first part + const firstPartData = bigFile.slice(0, 6 * 1024 * 1024); + const firstPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 1, + firstPartData + ); + + // Upload the second part + const secondPartData = bigFile.slice(6 * 1024 * 1024, 12 * 1024 * 1024); + const secondPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 2, + secondPartData + ); + + // Complete the multipart upload + await s3.completeMultipartUpload(testBucketName, testFileKey, multipartUpload.uploadId, [ + firstPart, + secondPart, + ]); + + // Let's redownload it verify it's correct, and delete it + const obj = await s3.getObject(testBucketName, testFileKey); + await s3.deleteObject(testBucketName, testFileKey); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/abortMultipartUpload.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/abortMultipartUpload.md new file mode 100755 index 000000000..a2a437777 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/abortMultipartUpload.md @@ -0,0 +1,56 @@ +--- +title: 'abortMultipartUpload' +head_title: 'S3Client.abortMultipartUpload(bucketName, objectKey, uploadId)' +description: 'S3Client.abortMultipartUpload aborts a multipart upload to a bucket' +weight: 10 +--- + +# abortMultipartUpload + +`S3Client.abortMultipartUpload` aborts a multipart upload to an S3 bucket. + +### Parameters + +| Parameter | Type | Description | +| :--------- | :----- | :------------------------------------------------------ | +| bucketName | string | Name of the bucket to delete the multipart object from. | +| objectKey | string | Name of the multipart object to delete. | +| uploadId | number | UploadId of the multipart upload to abort. | + +### Returns + +| Type | Description | +| :-------------- | :------------------------------------------------------------------ | +| `Promise` | A promise that fulfills when the multipart upload has been aborted. | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const s3 = new S3Client(awsConfig); + +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'multipart.txt'; + +export default async function () { + // Initialize a multipart upload + const multipartUpload = await s3.createMultipartUpload(testBucketName, testFileKey); + + // Abort multipart upload + await s3.abortMultipartUpload(testBucketName, testFileKey, multipartUpload.uploadId); +} +``` + +_A k6 script that will create a multipart upload and abort the multipart_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/completeMultipartUpload.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/completeMultipartUpload.md new file mode 100755 index 000000000..73af544fe --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/completeMultipartUpload.md @@ -0,0 +1,101 @@ +--- +title: 'completeMultipartUpload' +head_title: 'S3Client.completeMultipartUpload(bucketName, objectKey, uploadId, parts)' +description: 'S3Client.completeMultipartUpload uploads a multipar object to a bucket' +weight: 10 +--- + +# completeMultipartUpload + +`S3Client.completeMultipartUpload` uploads a multipart object to an S3 bucket. + +### Parameters + +| Parameter | Type | Description | +| :--------- | :------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------ | +| bucketName | string | Name of the bucket to delete the object to. | +| objectKey | string | Name of the uploaded object. | +| uploadId | number | UploadId of the multipart upload to complete. | +| parts | Array<[S3Part](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/s3part)> | The [S3Part](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/s3part)s to assemble. | + +### Returns + +| Type | Description | +| :-------------- | :-------------------------------------------------------------------- | +| `Promise` | A Promise that fulfills when the multipart upload has been completed. | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const s3 = new S3Client(awsConfig); + +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'multipart.txt'; + +export default async function () { + // List the buckets the AWS authentication configuration + // gives us access to. + const buckets = await s3.listBuckets(); + + // If our test bucket does not exist, abort the execution. + if (buckets.filter((b) => b.name === testBucketName).length == 0) { + exec.test.abort(); + } + + // Produce random bytes to upload of size ~12MB, that + // we will upload in two 6MB parts. This is done as the + // minimum part size supported by S3 is 5MB. + const bigFile = crypto.randomBytes(12 * 1024 * 1024); + + // Initialize a multipart upload + const multipartUpload = await s3.createMultipartUpload(testBucketName, testFileKey); + + // Upload the first part + const firstPartData = bigFile.slice(0, 6 * 1024 * 1024); + const firstPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 1, + firstPartData + ); + + // Upload the second part + const secondPartData = bigFile.slice(6 * 1024 * 1024, 12 * 1024 * 1024); + const secondPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 2, + secondPartData + ); + + // Complete the multipart upload + await s3.completeMultipartUpload(testBucketName, testFileKey, multipartUpload.uploadId, [ + firstPart, + secondPart, + ]); + + // Let's redownload it verify it's correct, and delete it + const obj = await s3.getObject(testBucketName, testFileKey); + await s3.deleteObject(testBucketName, testFileKey); +} +``` + +_A k6 script that will upload a multipart upload to an S3 bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/copyObject.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/copyObject.md new file mode 100644 index 000000000..35fad4a8a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/copyObject.md @@ -0,0 +1,59 @@ +--- +title: 'copyObject' +head_title: 'S3Client.copyObject' +description: 'S3Client.copyObject copies an object from a bucket to another' +weight: 10 +--- + +# copyObject + +`S3Client.copyObject` copies an object from one bucket to another. + +### Parameters + +| Parameter | Type | Description | +| :---------------- | :----- | :------------------------------------------------- | +| sourceBucket | string | Name of the bucket to copy the object from. | +| sourceKey | string | Name of the object to copy from the source bucket. | +| destinationBucket | string | Name of the bucket to copy the object to. | +| destinationKey | string | Name of the destination object. | + +### Returns + +| Type | Description | +| :-------------- | :---------------------------------------------------------------------------------- | +| `Promise` | A Promise that fulfills when the object has been copied from one bucket to another. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const testFile = open('./bonjour.txt', 'r'); +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'bonjour.txt'; +const testDestinationBucketName = 'test-jslib-aws-destination'; + +export default async function () { + // Let's upload our test file to the bucket + await s3.putObject(testBucketName, testFileKey, testFile); + + // Let's create our destination bucket + await s3.copyObject(testBucketName, testFileKey, testDestinationBucketName, testFileKey); +} +``` + +_A k6 script that will copy an object from a source S3 bucket to a destination bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/createMultipartUpload.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/createMultipartUpload.md new file mode 100755 index 000000000..dfe9201da --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/createMultipartUpload.md @@ -0,0 +1,53 @@ +--- +title: 'createMultipartUpload' +head_title: 'S3Client.createMultipartUpload(bucketName, objectKey)' +description: 'S3Client.createMultipartUpload creates a multipart upload for an object key to a bucket' +weight: 10 +--- + +# createMultipartUpload + +`S3Client.createMultipartUpload` creates a new multipart upload for a given an object key in a bucket. + +| Parameter | Type | Description | +| :--------- | :----- | :------------------------------------------ | +| bucketName | string | Name of the bucket to upload the object to. | +| objectKey | string | Name of the uploaded object. | + +### Returns + +| Type | Description | +| :------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[S3MultipartUpload](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/s3multipartupload)> | A Promise that fulfills with a [S3MultipartUpload](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/s3multipartupload) representing a S3 Multipart Upload. | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const s3 = new S3Client(awsConfig); + +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'multipart.txt'; + +export default async function () { + // Initialize a multipart upload + const multipartUpload = await s3.createMultipartUpload(testBucketName, testFileKey); + + // Abort multipart upload + await s3.abortMultipartUpload(testBucketName, testFileKey, multipartUpload.uploadId); +} +``` + +_A k6 script that will create a multipart upload to an S3 bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/deleteObject.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/deleteObject.md new file mode 100755 index 000000000..34d4b8a69 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/deleteObject.md @@ -0,0 +1,58 @@ +--- +title: 'deleteObject' +head_title: 'S3Client.deleteObject(bucketName, objectKey)' +description: 'S3Client.deleteObject deletes an object from a bucket' +weight: 10 +--- + +# deleteObject + +`S3Client.deleteObject` deletes an object from a bucket. + +### Parameters + +| Parameter | Type | Description | +| :--------- | :----- | :-------------------------------------------- | +| bucketName | string | Name of the bucket to delete the object from. | +| objectKey | string | Name of the object to delete. | + +### Returns + +| Type | Description | +| :-------------- | :---------------------------------------------------------------- | +| `Promise` | A promise that fulfills when the object has been deleted from S3. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'bonjour.txt'; + +export default async function () { + // Let's delete our test object + await s3.deleteObject(testBucketName, testFileKey); + + // And make sure it was indeed deleted + const objects = await s3.listObjects(); + if (objects.filter((o) => o.name === testBucketName).length != 0) { + exec.test.abort(); + } +} +``` + +_A k6 script that will delete an object from a S3 bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/getObject.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/getObject.md new file mode 100755 index 000000000..6bace543d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/getObject.md @@ -0,0 +1,60 @@ +--- +title: 'getObject' +head_title: 'S3Client.getObject(bucketName, objectKey)' +description: 'S3Client.getObject downloads an object from a bucket' +weight: 10 +--- + +# getObject + +`S3Client.getObject` downloads an object from a bucket. + +### Parameters + +| Parameter | Type | Description | +| :--------- | :----- | :------------------------------------------- | +| bucketName | string | Name of the bucket to fetch the object from. | +| objectKey | string | Name of the object to download. | + +### Returns + +| Type | Description | +| :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[Object](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/object)> | A Promise that fulfills with an [Object](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/object) describing and holding the downloaded content. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'bonjour.txt'; + +export default async function () { + const objects = await s3.listObjects(testBucketName); + + // If our test object does not exist, abort the execution. + if (objects.filter((o) => o.key === testFileKey).length == 0) { + exec.test.abort(); + } + + // Let's download our test object and print its content + const object = await s3.getObject(testBucketName, testFileKey); + console.log(JSON.stringify(object)); +} +``` + +_A k6 script that will download an object from a bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/listBuckets.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/listBuckets.md new file mode 100644 index 000000000..50415e1eb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/listBuckets.md @@ -0,0 +1,50 @@ +--- +title: 'listBuckets' +head_title: 'S3Client.listBuckets()' +description: 'S3Client.listBuckets lists the buckets the authenticated user has access to' +weight: 10 +--- + +# listBuckets + +`S3Client.listBuckets()` lists the buckets the authenticated user has access to in the region set by the `S3Client` instance's configuration. + +### Returns + +| Type | Description | +| :---------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise/javascript-api/jslib/aws/s3client/bucket)>> | A Promise that fulfills with an array of [Bucket](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/bucket) objects. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; + +export default async function () { + // List the buckets the AWS authentication configuration + // gives us access to. + const buckets = await s3.listBuckets(); + + // If our test bucket does not exist, abort the execution. + if (buckets.filter((b) => b.name === testBucketName).length == 0) { + exec.test.abort(); + } + + // ... work with the bucket's content +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/listObjects.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/listObjects.md new file mode 100755 index 000000000..acdf5fb2c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/listObjects.md @@ -0,0 +1,58 @@ +--- +title: 'listObjects' +head_title: 'S3Client.listObjects(bucketName, [prefix])' +description: 'S3Client.listObjects lists the objects contained in a bucket' +weight: 10 +--- + +# listObjects + +`S3Client.listObjects()` lists the objects contained in a bucket. + +### Parameters + +| Parameter | Type | Description | +| :---------------- | :----- | :---------------------------------------------------------------- | +| bucketName | string | Name of the bucket to fetch the object from. | +| prefix (optional) | string | Limits the response to keys that begin with the specified prefix. | + +### Returns + +| Type | Description | +| :---------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise/javascript-api/jslib/aws/s3client/object)>> | A Promise that fulfills with an array of [Object](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/object) objects. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'bonjour.txt'; + +export default async function () { + // List our bucket's objects + const objects = await s3.listObjects(testBucketName); + + // If our test object does not exist, abort the execution. + if (objects.filter((o) => o.key === testFileKey).length == 0) { + exec.test.abort(); + } + + // ... work with the bucket's objects + console.log(JSON.stringify(objects)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/putObject.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/putObject.md new file mode 100755 index 000000000..e95e8b9bc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/putObject.md @@ -0,0 +1,70 @@ +--- +title: 'putObject' +head_title: 'S3Client.putObject(bucketName, objectKey, data)' +description: 'S3Client.putObject uploads an object to a bucket' +weight: 10 +--- + +# putObject + +`S3Client.putObject` uploads an object to a bucket. + +### Parameters + +| Parameter | Type | Description | +| :----------- | :--------------------------------------------- | :------------------------------------------ | +| `bucketName` | string | Name of the bucket to upload the object to. | +| `objectKey` | string | Name of the uploaded object. | +| `data` | string \| ArrayBuffer | Content of the object to upload. | +| `params` | [PutObjectParams](#putobjectparams) (optional) | Options for the request. | + +#### PutObjectParams + +| Name | Type | Description | +| :------------------- | :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `contentDisposition` | string (optional) | Specifies presentational information for the object. For more information, see [RFC 6266](https://tools.ietf.org/html/rfc6266). | +| `contentEncoding` | string (optional) | Specifies what content encodings have been applied to the object and thus what decoding mechanisms must be applied to obtain the media-type referenced by the Content-Type header field. For more information, see [RFC 2616](https://tools.ietf.org/html/rfc2616). | +| `contentLength` | number (optional) | Size of the body in bytes. This parameter is useful when the size of the body cannot be determined automatically. | +| `contentMD5` | string (optional) | The base64-encoded 128-bit MD5 digest of the message (without the headers) according to RFC 1864. This header can be used as a message integrity check to verify that the received message is identical to the message that was sent. | +| `contentType` | string (optional) | A standard MIME type describing the format of the object data. For more information, see [RFC 2616](https://tools.ietf.org/html/rfc2616). | + +### Returns + +| Type | Description | +| :-------------- | :-------------------------------------------------------------------------- | +| `Promise` | A Promise that fulfills when the object has been uploaded to the S3 bucket. | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const s3 = new S3Client(awsConfig); +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'bonjour.txt'; +const testFile = open('./bonjour.txt', 'r'); + +export default async function () { + // Let's upload our test file to the bucket + await s3.putObject(testBucketName, testFileKey, testFile, { + contentType: 'text/plain', + contentLength: testFile.length, + }); + + // And let's redownload it to verify it's correct + const obj = await s3.getObject(testBucketName, testFileKey); + console.log(JSON.stringify(obj)); +} +``` + +_A k6 script that will upload an object to a S3 bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/uploadPart.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/uploadPart.md new file mode 100755 index 000000000..a2eb64d96 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/S3Client/uploadPart.md @@ -0,0 +1,91 @@ +--- +title: 'uploadPart' +head_title: 'S3Client.uploadPart(bucketName, objectKey, uploadId,partNumber, data)' +description: 'S3Client.uploadPart a part in a multipart upload to a bucket' +weight: 10 +--- + +# uploadPart + +`S3Client.uploadPart` uploads a part to multipart upload in a bucket. + +| Parameter | Type | Description | +| :--------- | :-------------------- | :------------------------------------------ | +| bucketName | string | Name of the bucket to upload the object to. | +| objectKey | string | Name of the object to upload. | +| uploadId | string | UploadId of the multipart upload. | +| partNumber | number | The Part number of the Part to upload. | +| data | string \| ArrayBuffer | Content of the part to upload. | + +### Returns + +| Type | Description | +| :--------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[S3Part](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/s3part)> | A Promise that fulfills with a [S3Part](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client/s3part) representing a S3 Part Upload. | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; +import exec from 'k6/execution'; + +import { AWSConfig, S3Client } from 'https://jslib.k6.io/aws/0.11.0/s3.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const s3 = new S3Client(awsConfig); + +const testBucketName = 'test-jslib-aws'; +const testFileKey = 'multipart.txt'; + +export default async function () { + // Produce random bytes to upload of size ~12MB, that + // we will upload in two 6MB parts. This is done as the + // minimum part size supported by S3 is 5MB. + const bigFile = crypto.randomBytes(12 * 1024 * 1024); + + // Initialize a multipart upload + const multipartUpload = await s3.createMultipartUpload(testBucketName, testFileKey); + + // Upload the first part + const firstPartData = bigFile.slice(0, 6 * 1024 * 1024); + const firstPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 1, + firstPartData + ); + + // Upload the second part + const secondPartData = bigFile.slice(6 * 1024 * 1024, 12 * 1024 * 1024); + const secondPart = await s3.uploadPart( + testBucketName, + testFileKey, + multipartUpload.uploadId, + 2, + secondPartData + ); + + // Complete the multipart upload + await s3.completeMultipartUpload(testBucketName, testFileKey, multipartUpload.uploadId, [ + firstPart, + secondPart, + ]); + + // Let's redownload it verify it's correct, and delete it + const obj = await s3.getObject(testBucketName, testFileKey); + await s3.deleteObject(testBucketName, testFileKey); +} +``` + +_A k6 script that will upload a part in a multipart upload to an S3 bucket_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/_index.md new file mode 100644 index 000000000..e574a1396 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/_index.md @@ -0,0 +1,64 @@ +--- +title: 'SQSClient' +head_title: 'SQSClient' +description: 'SQSClient enables interaction with the AWS Simple Queue Service (SQS)' +description: 'SQSClient allows interacting with the AWS Simple Queue Service (SQS)' +weight: 00 +--- + +# SQSClient + +`SQSClient` interacts with the AWS Simple Queue Service (SQS). + +With it, the user can send messages to specified queues and list available queues in the current region. + +`SQSClient` is included in both the dedicated `sqs.js` jslib bundle and the all-encompassing `aws.js` one, containing all the services clients. + +### Methods + +| Function | Description | +| :------------------------------------------------------------------------------------------------------- | :--------------------------------------------------- | +| [`sendMessage`](https://grafana.com/docs/k6//javascript-api/jslib/aws/sqsclient/sendmessage) | Delivers a message to the specified queue. | +| [`listQueues`](https://grafana.com/docs/k6//javascript-api/jslib/aws/sqsclient/listqueues) | Returns a list of your queues in the current region. | + +### Throws + +`SQSClient` methods throw errors in case of failure. + +| Error | Condition | +| :---------------------- | :-------------------------------------------------------- | +| `InvalidSignatureError` | when using invalid credentials | +| `SQSServiceError` | when AWS replied to the requested operation with an error | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SQSClient } from 'https://jslib.k6.io/aws/0.11.0/sqs.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const sqs = new SQSClient(awsConfig); +const testQueue = 'https://sqs.us-east-1.amazonaws.com/000000000/test-queue'; + +export default async function () { + // If our test queue does not exist, abort the execution. + const queuesResponse = await sqs.listQueues(); + if (queuesResponse.queueUrls.filter((q) => q === testQueue).length == 0) { + exec.test.abort(); + } + + // Send message to test queue + await sqs.sendMessage(testQueue, JSON.stringify({ value: '123' })); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/listQueues.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/listQueues.md new file mode 100644 index 000000000..1bb3efaff --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/listQueues.md @@ -0,0 +1,58 @@ +--- +aliases: + - ./sqsclient-listqueues/ # /docs/k6//javascript-api/jslib/aws/sqsclient/sqsclient-listqueues/ +title: 'listQueues' +description: 'SQSClient.listQueues retrieves a list of available Amazon SQS queues' +weight: 10 +--- + +# listQueues + +`SQSClient.listQueues(options)` retrieves a list of available Amazon Simple Queue Service (SQS) queues. + +### Parameters + +| Name | Type | Description | +| :-------- | :---------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `options` | object (optional) | Options for the request. Accepted properties are: `queueNamePrefix` (optional string) setting the prefix filter for the returned queue list, `maxResults` (optional number) setting the maximum number of results to include in the response (1 <= `maxResults` <= 1000>), and `nextToken` (optional string) setting the pagination token to request the next set of results. | + +### Returns + +| Type | Description | +| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Promise` | A Promise that fulfills with an object with an `urls` property containing an array of queue URLs, and an optional `nextToken` containing a pagination token to include in the next request when relevant. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SQSClient } from 'https://jslib.k6.io/aws/0.11.0/sqs.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const sqs = new SQSClient(awsConfig); +const testQueue = 'https://sqs.us-east-1.amazonaws.com/000000000/test-queue'; + +export default async function () { + // List all queues in the AWS account + const queuesResponse = await sqs.listQueues(); + + // If our test queue does not exist, abort the execution. + if (queuesResponse.queueUrls.filter((q) => q === testQueue).length == 0) { + exec.test.abort(); + } + + // Send message to test queue + await sqs.sendMessage(testQueue, JSON.stringify({ value: '123' })); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/sendMessage.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/sendMessage.md new file mode 100644 index 000000000..4833ea9c8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SQSClient/sendMessage.md @@ -0,0 +1,82 @@ +--- +aliases: + - ./sqsclient-sendmessage/ # /docs/k6//javascript-api/jslib/aws/sqsclient/sqsclient-sendmessage/ +title: 'sendMessage' +description: 'SQSClient.sendMessage sends a message to the specified Amazon SQS queue' +weight: 10 +--- + +# sendMessage + +`SQSClient.sendMessage(queueUrl, messageBody, options)` sends a message to the specified Amazon Simple Queue Service (SQS) queue. + +### Parameters + +| Name | Type | Description | +| :------------ | :--------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | +| `queueUrl` | string | The URL of the Amazon SQS queue to which a message is sent. Queue URLs and names are case-sensitive. | +| `messageBody` | string | The message to send. The minimum size is one character. The maximum size is 256 KB. | +| `options` | [SendMessageOptions](#sendmessageoptions) (optional) | Options for the request. | + +#### SendMessageOptions + +| Name | Type | Description | +| :----------------------- | :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `messageDeduplicationId` | string (optional) | The token used for deduplication of sent messages. This parameter applies only to FIFO (first-in-first-out) queues. If a message with a particular MessageDeduplicationId is sent successfully, any messages with the same MessageDeduplicationId are accepted but not delivered during the 5-minute deduplication interval. | +| `messageGroupId` | string (optional) | The tag that specifies that a message belongs to a specific message group. Messages that belong to the same message group are processed in a FIFO manner. Messages in different message groups might be processed out of order. | +| `messageAttributes` | object (optional) | Each message attribute consists of a `Name`, `Type`, and `Value`. For more information, see [Amazon SQS Message Attributes](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-message-attributes.html). | +| `delaySeconds` | number (optional) | The length of time, in seconds, for which to delay a specific message. Valid values: 0 to 900. Maximum: 15 minutes. Messages with a positive `delaySeconds` value become available for processing after the delay period is finished. If you don't specify a value, the default value for the queue applies. | + +### Returns + +| Type | Description | +| :---------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Promise` | A Promise that fulfills with the message that was sent, as an object containing an `id` string property holding the unique identifier for the message, and a `bodyMD5` string property holding the MD5 digest of the non-URL-encoded message body string. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SQSClient } from 'https://jslib.k6.io/aws/0.11.0/sqs.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const sqs = new SQSClient(awsConfig); +const testQueue = 'https://sqs.us-east-1.amazonaws.com/000000000/test-queue'; + +export default async function () { + // If our test queue does not exist, abort the execution. + const queuesResponse = await sqs.listQueues(); + if (queuesResponse.queueUrls.filter((q) => q === testQueue).length == 0) { + exec.test.abort(); + } + + // Send message to test queue + await sqs.sendMessage(testQueue, 'test', { + messageAttributes: { + 'test-string': { + type: 'String', + value: 'test', + }, + 'test-number': { + type: 'Number', + value: '23', + }, + 'test-binary': { + type: 'Binary', + value: 'dGVzdA==', + }, + }, + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/Secret.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/Secret.md new file mode 100644 index 000000000..a96e22abb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/Secret.md @@ -0,0 +1,60 @@ +--- +title: 'Secret' +head_title: 'Secret' +description: 'Secret is returned by the SecretsManagerClient.* methods who query secrets from AWS secrets manager.' +weight: 20 +--- + +# Secret + +Secret is returned by the SecretsManagerClient.\* methods that query secrets. Namely, [listSecrets](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/listsecrets/), +[getSecret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/getsecret), +[createSecret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/createsecret), and +[putSecretValue](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/putsecretvalue) returns either an instance or array of Secret objects. The Secret object describes an Amazon Secrets Manager secret. + +| Name | Type | Description | +| :----------------------- | :---------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- | +| `Secret.name` | string | The friendly name of the secret. You can use forward slashes in the name to represent a path hierarchy. | +| `Secret.arn` | string | The Amazon Resource Name (ARN) of the secret. | +| `Secret.createdAt` | number | The date and time (timestamp) when a secret was created. | +| `Secret.lastAccessDate` | number | The last date that this secret was accessed. This value is truncated to midnight of the date and therefore shows only the date, not the time. | +| `Secret.lastChangedDate` | number | The last date and time that this secret was modified in any way. | +| `Secret.tags` | Array<{"key": "value"}> | The list of user-defined tags associated with the secret. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/secrets-manager.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); +const testSecretName = 'jslib-test-secret'; + +export default async function () { + // List the secrets the AWS authentication configuration + // gives us access to. + const secrets = await secretsManager.listSecrets(); + + // If our test secret does not exist, abort the execution. + if (secrets.filter((s) => s.name === testSecretName).length == 0) { + exec.test.abort('test secret not found'); + } + + // Let's get it and print its content + const downloadedSecret = await secretsManager.getSecret(testSecretName); + console.log(downloadedSecret.secret); +} +``` + +_A k6 script that will query the user's secrets and print a test secret's value_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/_index.md new file mode 100644 index 000000000..a9f3ce981 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/_index.md @@ -0,0 +1,90 @@ +--- +title: 'SecretsManagerClient' +head_title: 'SecretsManagerClient' +description: 'SecretsManagerClient allows interacting with AWS secrets stored in Secrets Manager' +weight: 00 +--- + +# SecretsManagerClient + +`SecretsManagerClient` interacts with the AWS Secrets Manager. + +With it, you can perform several operations such as listing, creating and downloading secrets owned by the authenticated user. For a full list of supported operations, see [Methods](#methods). + +`SecretsManagerClient` is included in both the dedicated jslib `secrets-manager.js` bundle, and the `aws.js` one, containing all the services clients. + +### Methods + +| Function | Description | +| :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------- | +| [listSecrets()](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/listsecrets) | List secrets owned by the authenticated user | +| [getSecret(secretID)](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/getsecret) | Download a secret | +| [createSecret(name, secretString, description, [versionID], [tags])](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/createsecret) | Create a new secret | +| [putSecretValue(secretID, secretString, [versionID])](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/putsecretvalue) | Update a secret | +| [deleteSecret(secretID, { recoveryWindow: 30, noRecovery: false}})](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/deletesecret) | Delete a secret | + +### Throws + +S3 Client methods will throw errors in case of failure. + +| Error | Condition | +| :------------------------- | :--------------------------------------------------------- | +| InvalidSignatureError | when invalid credentials were provided. | +| SecretsManagerServiceError | when AWS replied to the requested operation with an error. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/secrets-manager.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); +const testSecretName = 'jslib-test-secret'; +const testSecretValue = 'jslib-test-value'; + +export async function setup() { + // Let's make sure our test secret is created + const testSecret = await secretsManager.createSecret( + testSecretName, + testSecretValue, + 'this is a test secret, delete me.' + ); + + // List the secrets the AWS authentication configuration + // gives us access to, and verify the creation was successful. + const secrets = await secretsManager.listSecrets(); + if (!secrets.filter((s) => s.name === testSecret.name).length == 0) { + exec.test.abort('test secret not found'); + } +} + +export default async function () { + // Knnowing that we know the secret exist, let's update its value + const newTestSecretValue = 'new-test-value'; + await secretsManager.putSecretValue(testSecretName, newTestSecretValue); + + // Let's get its value and verify it was indeed updated + const updatedSecret = await secretsManager.getSecret(testSecretName); + if (updatedSecret.secret !== newTestSecretValue) { + exec.test.abort('unable to update test secret'); + } + + // Let's now use our secret in the context of our load test... +} + +export async function teardown() { + // Finally, let's clean after ourselves and delete our test secret + await secretsManager.deleteSecret(testSecretName, { noRecovery: true }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/createSecret.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/createSecret.md new file mode 100644 index 000000000..e40d06f4a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/createSecret.md @@ -0,0 +1,61 @@ +--- +title: 'createSecret' +head_title: 'SecretsManagerClient.createSecret(name, secretString, description, [versionID], [tags])' +description: 'SecretsManagerClient.createSecret creates a new secret' +weight: 10 +--- + +# createSecret + +`SecretsManagerClient.createSecret` creates a secret in AWS' secrets manager. + +### Parameters + +| Parameter | Type | Description | +| :------------------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | The friendly name of the secret. You can use forward slashes in the name to represent a path hierarchy. | +| secretString | string | The text data to encrypt and store in this new version of the secret. We recommend you use a JSON structure of key/value pairs for your secret value. | +| description | string | The description of the secret. | +| versionID (optional) | string | Optional unique version identifier for the created secret. If no versionID is provided, an auto-generated UUID will be used instead. | +| tags (optional) | Array<{"key": "value"},> | A list of tags to attach to the secret. Each tag is a key and value pair of strings in a JSON text string | + +### Returns + +| Type | Description | +| :--------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[Secret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/secret)> | A Promise that fulfills with a [Secret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/secret) object that contains the details of the created secret. | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/secrets-manager.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); +const testSecretName = 'jslib-test-secret'; +const testSecretValue = 'jslib-test-value'; + +export default async function () { + // Let's create our test secret. + const testSecret = await secretsManager.createSecret( + testSecretName, + testSecretValue, + 'this is a test secret, delete me.' + ); + + // Let's get its value and verify it was indeed created. + const createdSecret = await secretsManager.getSecret(testSecretName); + console.log(JSON.stringify(createdSecret)); +} +``` + +_A k6 script that will create a secret in AWS secrets manager_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/deleteSecret.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/deleteSecret.md new file mode 100644 index 000000000..9344644c1 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/deleteSecret.md @@ -0,0 +1,57 @@ +--- +title: 'deleteSecret' +head_title: 'SecretsManagerClient.deleteSecret(secretID, { recoveryWindow: 30, noRecovery: false}})' +description: 'SecretsManagerClient.deleteSecret deletes a secret' +weight: 10 +--- + +# deleteSecret + +`SecretsManagerClient.deleteSecret` deletes a secret from AWS' secrets manager. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :---------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| secretID | string | The ARN or name of the secret to update. | +| options | { recoveryWindow: 30, noRecovery: false } | Use options to control the deletion behavior. recoveryWindow defines how long a secret will remain “soft-deleted”, in days, before being hard-deleted. noRecovery set to true would hard-delete the secret immediately. Note that both options are exclusive. | + +### Returns + +| Type | Description | +| :-------------- | :--------------------------------------------------------- | +| `Promise` | A promise that will be resolved when the secret is deleted | + +### Example + +{{< code >}} + +```javascript +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/secrets-manager.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); +const testSecretName = 'jslib-test-secret'; +const testSecretValue = 'jslib-test-value'; + +export default async function () { + // Let's make sure our test secret is created + const testSecret = await secretsManager.createSecret( + testSecretName, + testSecretValue, + 'this is a test secret, delete me.' + ); + + // Let's hard delete our test secret and verify it worked + await secretsManager.deleteSecret(testSecretName, { noRecovery: true }); +} +``` + +_A k6 script that will delete a secret in AWS secrets manager_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/getSecret.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/getSecret.md new file mode 100644 index 000000000..0174687df --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/getSecret.md @@ -0,0 +1,56 @@ +--- +title: 'getSecret' +head_title: 'SecretsManagerClient.getSecret(secretID)' +description: 'SecretsManagerClient.getSecret(secretID) downloads a secret from AWS secrets manager' +weight: 10 +--- + +# getSecret + +`SecretsManagerClient.getSecret` downloads a secret from AWS secrets manager. + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------- | +| secretID | string | The ARN or name of the secret to retrieve. | + +### Returns + +| Type | Description | +| :--------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[Secret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/secret)> | A Promise that fulfills with a [Secret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/secret) describing and holding the downloaded secret. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/secrets-manager.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); +const testSecretName = 'jslib-test-secret'; + +export default async function () { + // List the secrets the AWS authentication configuration + // gives us access to. + const secrets = await secretsManager.listSecrets(); + if (secrets.filter((s) => s.name === testSecretName).length == 0) { + exec.test.abort('test secret not found'); + } + + // Let's get our test secret's value and print it. + const secret = await secretsManager.getSecret(testSecretName); + console.log(JSON.stringify(secret)); +} +``` + +_A k6 script that will download a user's secret from AWS secrets manager_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/listSecrets.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/listSecrets.md new file mode 100644 index 000000000..da8a744b2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/listSecrets.md @@ -0,0 +1,50 @@ +--- +title: 'listSecrets' +head_title: 'SecretsManagerClient.listSecrets()' +description: 'SecretsManagerClient.listSecrets lists the secrets the authenticated user has access to' +weight: 10 +--- + +# listSecrets + +`S3Client.listSecrets` lists the secrets the authenticated user has access to in the region set by the `SecretsManagerClient` instance's configuration. + +### Returns + +| Type | Description | +| :---------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise/javascript-api/jslib/aws/secretsmanagerclient/secret)>> | A Promise that fulfills with an array of [Secret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/secret) objects. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/secrets-manager.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); +const testSecretName = 'jslib-test-secret'; + +export default async function () { + // List the secrets the AWS authentication configuration + // gives us access to, and verify the test secret exists. + const secrets = await secretsManager.listSecrets(); + if (secrets.filter((s) => s.name === testSecretName).length == 0) { + exec.test.abort('test secret not found'); + } + + console.log(JSON.stringify(secrets)); +} +``` + +_A k6 script that will list a user's secrets from AWS secrets manager_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/putSecretValue.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/putSecretValue.md new file mode 100644 index 000000000..7bb1a62b8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SecretsManagerClient/putSecretValue.md @@ -0,0 +1,66 @@ +--- +title: 'putSecretValue' +head_title: 'SecretsManagerClient.putSecretValue(secretID, secretString, [versionID], [tags])' +description: "SecretsManagerClient.putSecretValue updates an existing secret's value" +weight: 10 +--- + +# putSecretValue + +`SecretsManagerClient.putSecretValue` updates a secret's value in AWS' secrets manager. + +| Parameter | Type | Description | +| :------------------- | :----------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- | +| secretID | string | The ARN or name of the secret to update. | +| secretString | string | The text data to encrypt and store in this new version of the secret. We recommend you use a JSON structure of key/value pairs for your secret value. | +| versionID (optional) | string | Optional unique version identifier for the updated version of the secret. If no versionID is provided, an auto-generated UUID will be used instead. | +| tags (optional) | Array<{"key": "value"},> | A list of tags to attach to the secret. Each tag is a key and value pair of strings in a JSON text string | + +### Returns + +| Type | Description | +| :--------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Promise<[Secret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/secret)> | A Promise that fulfills with the updated [Secret](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient/secret). | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/secrets-manager.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); +const testSecretName = 'jslib-test-secret'; +const testSecretValue = 'jslib-test-value'; + +export default async function () { + // Let's make sure our test secret is created + const testSecret = await secretsManager.createSecret( + testSecretName, + testSecretValue, + 'this is a test secret, delete me.' + ); + + // Now that we know the secret exist, let's update its value + const newTestSecretValue = 'new-test-value'; + const u = await secretsManager.putSecretValue(testSecretName, newTestSecretValue); + + // Let's get its value back and verify it was indeed updated + const updatedSecret = await secretsManager.getSecret(testSecretName); + if (updatedSecret.secret !== newTestSecretValue) { + exec.test.abort('unable to update test secret'); + } +} +``` + +_A k6 script that will update a secret's value in AWS secrets manager_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/_index.md new file mode 100644 index 000000000..7d290a4be --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/_index.md @@ -0,0 +1,157 @@ +--- +title: 'SignatureV4' +head_title: 'SignatureV4' +description: 'SignatureV4 is used to sign or pre-sign requests to AWS services using the Signature V4 algorithm' +description: 'SignatureV4 is used to sign and pre-sign requests to AWS services using the Signature V4 algorithm' +weight: 00 +--- + +# SignatureV4 + +{{< docs/shared source="k6" lookup="blocking-aws-blockquote.md" version="" >}} + +With SignatureV4, you can produce authenticated HTTP requests to AWS services. Namely, it lets you sign and pre-sign requests to AWS services using the [Signature V4](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) algorithm. The `sign` operation produces a signed request with authorization information stored in its headers. +The `presign` operation produces a pre-signed request with authorization information stored in its query string parameters. + +SignatureV4 is included in both the dedicated jslib `signature.js` bundle and the `aws.js` one, containing all the service's clients. + +Instantiating a new `SignatureV4` requires a single options object argument with the following properties: + +| Property | Type | Description | +| :------------ | :---------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| service | string | the AWS region to sign or pre-sign requests for. As described by [Amazon AWS docs](https://docs.aws.amazon.com/general/latest/gr/rande.html) | +| region | string | the AWS service to sign or pre-sign requests for. As described by [Amazon AWS docs](https://aws.amazon.com/about-aws/global-infrastructure/regions_az/)] | +| credentials | an object with `accessKeyId`, `secretAccessKeyId`, and optional `sessionToken` properties | the AWS credentials to sign or pre-sign requests with. | +| uriEscapePath | boolean | Whether to uri-escape the request URI path as part of computing the canonical request string. As of late 2017, this is **required for every AWS service** except Amazon S3. | +| applyChecksum | boolean | Whether to calculate a checksum of the request body and include it as either a request header (when signing) or as a query string parameter (when pre-signing). This is **required for AWS Glacier and Amazon S3** and optional for every other AWS service as of late 2017. | + +## Methods + + + +| Method | Description | +| :------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- | +| [sign()](https://grafana.com/docs/k6//javascript-api/jslib/aws/signaturev4/sign) | Signs an authenticated HTTP request using the AWS signature v4 algorithm | +| [presign()](https://grafana.com/docs/k6//javascript-api/jslib/aws/signaturev4/presign) | Produces an authenticated pre-signed URL using the AWS signature v4 algorithm | + + + +### Throws + +SignatureV4 methods throw errors on failure. + +| Error | Condition | +| :-------------------- | :-------------------------------------- | +| InvalidSignatureError | when invalid credentials were provided. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +import { AWSConfig, SignatureV4 } from 'https://jslib.k6.io/aws/0.11.0/aws.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +export default function () { + /** + * In order to be able to sign an HTTP request's, + * we need to instantiate a SignatureV4 object. + */ + const signer = new SignatureV4({ + service: 's3', + region: awsConfig.region, + credentials: { + accessKeyId: awsConfig.accessKeyId, + secretAccessKey: awsConfig.secretAccessKey, + sessionToken: awsConfig.sessionToken, + }, + + /** + * Whether the URI should be escaped or not. + */ + uriEscapePath: false, + + /** + * Whether or not the body's hash should be calculated and included + * in the request. + */ + applyChecksum: false, + }); + + /** + * The sign operation will return a new HTTP request with the + * AWS signature v4 protocol headers added. It returns an Object + * implementing the SignedHTTPRequest interface, holding a `url` and a `headers` + * properties, ready to use in the context of k6's http call. + */ + const signedRequest = signer.sign( + /** + * HTTP request description + */ + { + /** + * The HTTP method we will use in the request. + */ + method: 'GET', + + /** + * The network protocol we will use to make the request. + */ + protocol: 'https', + + /** + * The hostname of the service we will be making the request to. + */ + hostname: 'mybucket.s3.us-east-1.amazonaws.com', + + /** + * The path of the request. + */ + path: '/myfile.txt', + + /** + * The headers we will be sending in the request. + */ + headers: {}, + }, + + /** + * (optional) Signature operation options allows to override the + * SignatureV4's options in the context of this specific request. + */ + { + /** + * The date and time to be used as signature metadata. This value should be + * a Date object, a unix (epoch) timestamp, or a string that can be + * understood by the JavaScript `Date` constructor.If not supplied, the + * value returned by `new Date()` will be used. + */ + signingDate: new Date(), + + /** + * The service signing name. It will override the service name of the signer + * in current invocation + */ + signingService: 's3', + + /** + * The region name to sign the request. It will override the signing region of the + * signer in current invocation + */ + signingRegion: 'us-east-1', + } + ); + + http.get(signedRequest.url, { headers: signedRequest.headers }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/presign.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/presign.md new file mode 100644 index 000000000..0448f44ff --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/presign.md @@ -0,0 +1,202 @@ +--- +title: 'presign' +head_title: 'presign' +slug: 'presign' +description: 'Signaturev4.presign pre-signs a URL with the AWS Signature V4 algorithm' +description: 'SignatureV4.sign pre-signs a URL with the AWS Signature V4 algorithm' +--- + +# presign + +`SignatureV4.presign()` pre-signs a URL with the AWS Signature V4 algorithm. Given an HTTP request description, it returns a new HTTP request with the AWS signature v4 authorization added. It returns an Object holding a `url` containing the authorization information encoded in its query string, ready to use in the context of a k6 HTTP call. + +### Parameters + +The first parameter of the `presign` method consists of an Object with the following properties. + +| Property | Type | Description | +| :------- | :----------------------- | :---------------------------------- | +| method | string | The HTTP method of the request | +| protocol | `http` or `https` string | The network protocol of the request | +| hostname | string | The hostname the request is sent to | +| path | string | The path of the request | +| headers | Object | The headers of the HTTP request | + +You can provide further options and override SignatureV4 options in the context of this specific request. +To do this, pass a second parameter to the `presign` method, which is an Object with the following parameters. + +| Property | Type | Description | +| :---------------- | :------------ | :------------------------------------------------------------------------- | +| expiresIn | number | The number of seconds before the pre-signed URL expires | +| signingDate | Date | overrides the Date used in the signing operation | +| signingService | string | overrides the signer's AWS service in the context of the `sign` operation. | +| signingRegion | string | overrides the signer's AWS region in the context of the `sign` operation | +| unsignableHeaders | `Set` | excludes headers from the signing process | +| signableHeaders | `Set` | mark headers as signed | + +### Returns + +The `presign` operation returns an Object with the following properties. + +| Property | Type | Description | +| :------- | :----- | :------------------------------------------------------------------------ | +| headers | Object | The pre-signed request headers to use in the context of a k6 HTTP request | +| url | string | The pre-signed url to use in the context of a k6 HTTP request | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +import { + AWSConfig, + SignatureV4, + AMZ_CONTENT_SHA256_HEADER, + UNSIGNED_PAYLOAD, +} from 'https://jslib.k6.io/aws/0.11.0/kms.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +export default function () { + // In order to be able to produce pre-signed URLs, + // we need to instantiate a SignatureV4 object. + const signer = new SignatureV4({ + service: 's3', + region: awsConfig.region, + credentials: { + accessKeyId: awsConfig.accessKeyId, + secretAccessKey: awsConfig.secretAccessKey, + sessionToken: awsConfig.sessionToken, + }, + uriEscapePath: false, + applyChecksum: false, + }); + + // We can now use the signer to produce a pre-signed URL. + const signedRequest = signer.presign( + /** + * HTTP request description + */ + { + /** + * The HTTP method we will use in the request. + */ + method: 'GET', + + /** + * The network protocol we will use to make the request. + */ + protocol: 'https', + + /** + * The hostname of the service we will be making the request to. + */ + hostname: 'my-bucket.s3.us-east-1.amazonaws.com', + + /** + * The path of the request. + */ + path: '/my-file.txt', + + /** + * The headers we will be sending in the request. + * + * Note that in the specific case of this example, requesting + * an object from S3, we want to set the `x-amz-content-sha256` + * header to `UNSIGNED_PAYLOAD`. That way, we bypass the payload + * hash calculation, and communicate that value instead, as specified. + */ + headers: { [AMZ_CONTENT_SHA256_HEADER]: 'UNSIGNED-PAYLOAD' }, + }, + + /** + * (optional) pre-sign operation options. + */ + { + /** + * The number of seconds before the pre-signed URL expires + */ + expiresIn: 86400, + + /** + * A set of strings whose representing headers that should not be hoisted + * to pre-signed request's query string. If not supplied, the pre-signer + * moves all the AWS-specific headers (starting with `x-amz-`) to the request + * query string. If supplied, these headers remain in the pre-signed request's + * header. + * All headers in the provided request will have their names converted to + * lower case and then checked for existence in the unhoistableHeaders set. + * + * In the case of pre-signing S3 URLs, the body needs to be empty. + * however, the AMZ_CONTENT_SHA256_HEADER needs to be set to + * UNSIGNED_PAYLOAD. To do this, we need to set the header, + * but declare it as unhoistable, and unsignable. + */ + unhoistableHeaders: new Set([AMZ_CONTENT_SHA256_HEADER]), + + /** + * A set of strings whose members represents headers that cannot be signed. + * All headers in the provided request will have their names converted to + * lower case and then checked for existence in the unsignableHeaders set. + * + * In the case of pre-signing S3 URLs, the body needs to be empty. + * however, the AMZ_CONTENT_SHA256_HEADER needs to be set to + * UNSIGNED_PAYLOAD. To do this, we need to set the header, + * but declare it as unhoistable, and unsignable. + */ + unsignableHeaders: new Set([AMZ_CONTENT_SHA256_HEADER]), + + /** + * A set of strings whose members represents headers that should be signed. + * Any values passed here will override those provided via unsignableHeaders, + * allowing them to be signed. + * + * All headers in the provided request will have their names converted to + * lower case before signing. + */ + signableHeaders: new Set(), + + /** + * The date and time to be used as signature metadata. This value should be + * a Date object, a unix (epoch) timestamp, or a string that can be + * understood by the JavaScript `Date` constructor.If not supplied, the + * value returned by `new Date()` will be used. + */ + signingDate: new Date(), + + /** + * The service signing name. It will override the service name of the signer + * in current invocation + */ + signingService: 's3', + + /** + * The signingRegion and signingService options let the user + * specify a different region or service to pre-sign the request for. + */ + signingRegion: 'us-east-1', + } + ); + + console.log(`presigned URL: ${signedRequest.url}`); + + /** + * Our URL is now ready to be used. + */ + const res = http.get(signedRequest.url, { + headers: signedRequest.headers, + }); + + check(res, { 'status is 200': (r) => r.status === 200 }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/sign.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/sign.md new file mode 100644 index 000000000..d657e9fa6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SignatureV4/sign.md @@ -0,0 +1,145 @@ +--- +title: 'sign' +head_title: 'sign' +slug: 'sign' +description: 'Signaturev4.sign signs an HTTP request with the AWS Signature V4 algorithm' +description: 'SignatureV4.sign signs an HTTP request with the AWS Signature V4 algorithm' +--- + +# sign + +`SignatureV4.sign()` signs an HTTP request with the AWS Signature V4 algorithm. Given an HTTP request description, it returns a new HTTP request with the AWS signature v4 protocol headers added. It returns an Object holding a `url` and a `headers` properties, ready to use in the context of k6's HTTP call. + +### Parameters + +The first parameter of the `sign` method consists of an Object with the following properties. + +| Property | Type | Description | +| :--------------- | :------------------------------------------ | :------------------------------------------------ | +| method | string | The HTTP method of the request | +| protocol | `http` or `https` string | The network protocol of the request | +| hostname | string | The hostname the request is sent to | +| path | string | The path of the request | +| headers | Object | The headers of the HTTP request | +| body (optional) | string or ArrayBuffer | The optional body of the HTTP request | +| query (optional) | `Object.>` | The optional query parameters of the HTTP request | + +You can override SignatureV4 options in the context of this specific request. To do this, pass a second parameter to the `sign` method, which is an Object with the following parameters. + +| Property | Type | Description | +| :---------------- | :------------ | :------------------------------------------------------------------------- | +| signingDate | Date | overrides the Date used in the signing operation | +| signingService | string | overrides the signer's AWS service in the context of the `sign` operation. | +| signingRegion | string | overrides the signer's AWS region in the context of the `sign` operation | +| unsignableHeaders | `Set` | excludes headers from the signing process | +| signableHeaders | `Set` | mark headers as signed | + +### Returns + +| Property | Type | Description | +| :------- | :----- | :---------------------------------------------------------------------- | +| headers | Object | The signed request's headers to use in the context of a k6 HTTP request | +| url | string | The signed url to use in the context of a k6 HTTP request | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +import { AWSConfig, SignatureV4 } from 'https://jslib.k6.io/aws/0.11.0/signature.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +export default function () { + /** + * In order to be able to sign an HTTP request's, + * we need to instantiate a SignatureV4 object. + */ + const signer = new SignatureV4({ + service: 's3', + region: awsConfig.region, + credentials: { + accessKeyId: awsConfig.accessKeyId, + secretAccessKey: awsConfig.secretAccessKey, + sessionToken: awsConfig.sessionToken, + }, + uriEscapePath: false, + applyChecksum: false, + }); + + /** + * The sign operation will return a new HTTP request with the + * AWS signature v4 protocol headers added. It returns an Object + * implementing the SignedHTTPRequest interface, holding a `url` and a `headers` + * properties, ready to use in the context of k6's http call. + */ + const signedRequest = signer.sign( + /** + * HTTP request description + */ + { + /** + * The HTTP method we will use in the request. + */ + method: 'GET', + + /** + * The network protocol we will use to make the request. + */ + protocol: 'https', + + /** + * The hostname of the service we will be making the request to. + */ + hostname: 'mybucket.s3.us-east-1.amazonaws.com', + + /** + * The path of the request. + */ + path: '/myfile.txt', + + /** + * The headers we will be sending in the request. + */ + headers: {}, + }, + + /** + * (optional) Signature operation options allows to override the + * SignatureV4's options in the context of this specific request. + */ + { + /** + * The date and time to be used as signature metadata. This value should be + * a Date object, a unix (epoch) timestamp, or a string that can be + * understood by the JavaScript `Date` constructor.If not supplied, the + * value returned by `new Date()` will be used. + */ + signingDate: new Date(), + + /** + * The service signing name. It will override the service name of the signer + * in current invocation + */ + signingService: 's3', + + /** + * The region name to sign the request. It will override the signing region of the + * signer in current invocation + */ + signingRegion: 'us-east-1', + } + ); + + http.get(signedRequest.url, { headers: signedRequest.headers }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/SystemsManagerParameter.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/SystemsManagerParameter.md new file mode 100644 index 000000000..65fb13097 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/SystemsManagerParameter.md @@ -0,0 +1,72 @@ +--- +title: 'SystemsManagerParameter' +head_title: 'SystemsManagerParameter' +slug: 'systemsmanagerparameter' +description: 'SystemsManagerParameter is returned by the SystemsManagerClient.* methods that query parameters' +weight: 20 +--- + +# SystemsManagerParameter + +`SystemsManagerParameter.*` methods querying the Systems Manager Service parameters return some `SystemsManagerParameter` instances. Namely, `getParameter` returns an array of `SystemsManagerParameter` objects. The `SystemsManagerParameter` object describes an Amazon Systems Manager Service parameter. + +| Name | Type | Description | +| :----------------------------------------- | :----- | :------------------------------------------------------------------------------------ | +| `SystemsManagerParameter.arn` | string | The Amazon Resource Name (ARN) of the parameter | +| `SystemsManagerParameter.dataType` | string | The data type of the parameter, such as text or aws:ec2:image. The default is text. | +| `SystemsManagerParameter.lastModifiedDate` | number | Date the parameter was last changed or updated and the parameter version was created. | +| `SystemsManagerParameter.name` | string | The friendly name of the parameter. | +| `SystemsManagerParameter.selector` | string | Either the version number or the label used to retrieve the parameter value | +| `SystemsManagerParameter.sourceResult` | string | The raw result or response from the source. | +| `SystemsManagerParameter.type` | string | The type of parameter | +| `SystemsManagerParameter.value` | string | The parameter value | +| `SystemsManagerParameter.version` | string | The parameter version | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SystemsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/ssm.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const systemsManager = new SystemsManagerClient(awsConfig); +const testParameterName = 'jslib-test-parameter'; +const testParameterValue = 'jslib-test-value'; +const testParameterSecretName = 'jslib-test-parameter-secret'; +// this value was created with --type SecureString +const testParameterSecretValue = 'jslib-test-secret-value'; + +export default async function () { + // Currently the parameter needs to be created before hand + + // Let's get its value + // getParameter returns a parameter object: e.g. {name: string, value: string...} + const parameter = await systemsManager.getParameter(testParameterName); + if (parameter.value !== testParameterValue) { + exec.test.abort('test parameter not found'); + } + + // Let's get the secret value with decryption + // destructure the parameter object to get to the values you want + const { value: encryptedParameterValue } = await systemsManager.getParameter( + testParameterSecretName, + true + ); + if (encryptedParameterValue !== testParameterSecretValue) { + exec.test.abort('encrypted test parameter not found'); + } +} +``` + +_A k6 script querying a user Systems Manager Service parameter_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/_index.md new file mode 100644 index 000000000..0b86250a0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/_index.md @@ -0,0 +1,78 @@ +--- +title: 'SystemsManagerClient' +head_title: 'SystemsManagerClient' +description: 'SystemsManagerClient allows interacting with the AWS Systems Manager Service' +weight: 00 +--- + +# SystemsManagerClient + +`SystemsManagerClient` interacts with the AWS Systems Manager Service. + +With it, the user can get parameters from the Systems Manager Service in the caller's AWS account and region. + +Both the dedicated `ssm.js` jslib bundle and the all-encompassing `aws.js` bundle include the `SystemsManagerClient`. + +### Methods + +| Function | Description | +| :------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------- | +| [getParameter](https://grafana.com/docs/k6//javascript-api/jslib/aws/systemsmanagerclient/getparameter) | Retrieves a parameter from Amazon Systems Manager. | + +### Throws + +`SystemsManagerClient` methods throw errors in case of failure. + +| Error | Condition | +| :--------------------------- | :-------------------------------------------------------- | +| `InvalidSignatureError` | when using invalid credentials | +| `SystemsManagerServiceError` | when AWS replied to the requested operation with an error | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SystemsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/ssm.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const systemsManager = new SystemsManagerClient(awsConfig); +const testParameterName = 'jslib-test-parameter'; +const testParameterValue = 'jslib-test-value'; +const testParameterSecretName = 'jslib-test-parameter-secret'; +// this value was created with --type SecureString +const testParameterSecretValue = 'jslib-test-secret-value'; + +export default async function () { + // Currently the parameter needs to be created before hand + + // Let's get its value + // getParameter returns a parameter object: e.g. {name: string, value: string...} + const parameter = await systemsManager.getParameter(testParameterName); + if (parameter.value !== testParameterValue) { + exec.test.abort('test parameter not found'); + } + + // Let's get the secret value with decryption + // destructure the parameter object to get to the values you want + const { value: encryptedParameterValue } = await systemsManager.getParameter( + testParameterSecretName, + true + ); + if (encryptedParameterValue !== testParameterSecretValue) { + exec.test.abort('encrypted test parameter not found'); + } +} +``` + +_A k6 script querying a user's Systems Manager Service parameter_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/getParameter.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/getParameter.md new file mode 100644 index 000000000..b8562bd1e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/SystemsManagerClient/getParameter.md @@ -0,0 +1,65 @@ +--- +title: 'getParameter' +head_title: 'SystemsManagerClient.getParameter()' +description: "SystemsManagerClient.getParameter gets a Systems Manager parameter in the caller's AWS account and region" +weight: 10 +--- + +# getParameter + +`SystemsManagerClient.getParameter` gets a Systems Manager parameter in the caller's AWS account and region. + +### Returns + +| Type | Description | +| :------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`Promise`](https://grafana.com/docs/k6//javascript-api/jslib/aws/systemsmanagerclient/systemsmanagerparameter/) | A Promise that fulfills with an array of [`SystemsManagerParameter`](https://grafana.com/docs/k6//javascript-api/jslib/aws/systemsmanagerclient/systemsmanagerparameter/) objects. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +import { AWSConfig, SystemsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/ssm.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, + sessionToken: __ENV.AWS_SESSION_TOKEN, +}); + +const systemsManager = new SystemsManagerClient(awsConfig); +const testParameterName = 'jslib-test-parameter'; +const testParameterValue = 'jslib-test-value'; +const testParameterSecretName = 'jslib-test-parameter-secret'; +// this value was created with --type SecureString +const testParameterSecretValue = 'jslib-test-secret-value'; + +export default async function () { + // Currently the parameter needs to be created before hand + + // Let's get its value + // getParameter returns a parameter object: e.g. {name: string, value: string...} + const parameter = await systemsManager.getParameter(testParameterName); + if (parameter.value !== testParameterValue) { + exec.test.abort('test parameter not found'); + } + + // Let's get the secret value with decryption + // destructure the parameter object to get to the values you want + const { value: encryptedParameterValue } = await systemsManager.getParameter( + testParameterSecretName, + true + ); + if (encryptedParameterValue !== testParameterSecretValue) { + exec.test.abort('encrypted test parameter not found'); + } +} +``` + +_A k6 script querying a user's Systems Manager Service parameter_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/_index.md new file mode 100644 index 000000000..31014bbea --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/_index.md @@ -0,0 +1,10 @@ +--- +weight: 01 +title: aws +--- + +# aws + + + +{{< section >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/aws/awsconfig.md b/docs/sources/v0.50.x/javascript-api/jslib/aws/awsconfig.md new file mode 100644 index 000000000..09eb2a94f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/aws/awsconfig.md @@ -0,0 +1,64 @@ +--- +title: 'AWSConfig' +head_title: 'AWSConfig' +description: 'AWSConfig is used to configure an AWS service client instances' +description: 'AWSConfig is used to configure an AWS service client instances' +weight: 00 +--- + +# AWSConfig + +AWSConfig is used to configure an AWS service client instance, such as [S3Client](https://grafana.com/docs/k6//javascript-api/jslib/aws/s3client) or [SecretsManagerClient](https://grafana.com/docs/k6//javascript-api/jslib/aws/secretsmanagerclient). It effectively allows the user to select a [region](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) they wish to interact with, and the AWS credentials they wish to use to authenticate. + +AWSConfig is included in the `aws.js` bundle, which includes all the content of the library. It is also included in the various services clients dedicated bundles such as `s3.js` and `secrets-manager.js`. + +It takes an options object as its single parameter, with the following properties: + +| Property | Type | Description | +| :---------------------- | :----- | :------------------------------------------------------------------------------------------------------------------------ | +| region | string | the AWS region to connect to. As described by [Amazon AWS docs](https://docs.aws.amazon.com/general/latest/gr/rande.html) | +| accessKeyID | string | The AWS access key ID credential to use for authentication. | +| secretAccessKey | string | The AWS secret access credential to use for authentication. | +| sessionToken (optional) | string | The AWS secret access token to use for authentication. | + +### Methods + +| Name | Description | +| :------------------ | :----------------------------------------------------------------------------------------------------------------------------------------- | +| `fromEnvironment()` | Creates an `AWSConfig` using the `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` environment variables. | + +### Throws + +S3 Client methods will throw errors in case of failure. + +| Error | Condition | +| :----------------------- | :---------------------------------------------------- | +| InvalidArgumentException | when an invalid region name or credentials were used. | + +### Example + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +// Note that you AWSConfig is also included in the dedicated service +// client bundles such as `s3.js` and `secrets-manager.js` +import { AWSConfig, SecretsManagerClient } from 'https://jslib.k6.io/aws/0.11.0/aws.js'; + +const awsConfig = new AWSConfig({ + region: __ENV.AWS_REGION, + accessKeyId: __ENV.AWS_ACCESS_KEY_ID, + secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY, +}); + +const secretsManager = new SecretsManagerClient(awsConfig); + +export default function () { + // ... +} +``` + +_k6 will instantiate an `AWSConfig` object and use it to configure a `SecretsManagerClient` instance_ + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/_index.md new file mode 100644 index 000000000..5455a1ac4 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/_index.md @@ -0,0 +1,116 @@ +--- +title: "httpx" +description: "httpx is a wrapper library around the native k6 http module" +weight: 02 +weight: 02 +--- + +# httpx + +The `httpx` module is an external JavaScript library that wraps around the native [k6/http](https://grafana.com/docs/k6//javascript-api/k6-http) module. +It's a http client with features that are not yet available in the native module. + +- ability to set http options globally (such as timeout) +- ability to set default tags and headers that will be used for all requests +- more user-friendly arguments to request functions (get, post, put take the same arguments) + +httpx module integrates well with the expect library. + +The source code is [on GitHub](https://github.com/k6io/k6-jslib-httpx). +Please request features and report bugs through [GitHub issues](https://github.com/k6io/k6-jslib-httpx/issues). + +{{% admonition type="caution" %}} + +**This library is in active development.** + +This library is stable enough to be useful, but pay attention to the new versions released on [jslib.k6.io](https://jslib.k6.io). + +This documentation is for the only last version only. If you discover that some of the following doesn't work, you might be using an older version. + +{{% /admonition %}} + +### Methods + +| Function | Description | +| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [asyncRequest(method, url, [body], [params])](https://grafana.com/docs/k6//javascript-api/jslib/httpx/asyncrequest) | Generic method for making arbitrary, asynchronous HTTP requests. | +| [request(method, url, [body], [params])](https://grafana.com/docs/k6//javascript-api/jslib/httpx/request) | Generic method for making arbitrary HTTP requests. | +| [get(url, [body], [params])](https://grafana.com/docs/k6//javascript-api/jslib/httpx/get) | Makes GET request | +| [post(url, [body], [params])](https://grafana.com/docs/k6//javascript-api/jslib/httpx/post) | Makes POST request | +| [put(url, [body], [params])](https://grafana.com/docs/k6//javascript-api/jslib/httpx/put) | Makes PUT request | +| [patch(url, [body], [params])](https://grafana.com/docs/k6//javascript-api/jslib/httpx/patch) | Makes PATCH request | +| [delete(url, [body], [params])](https://grafana.com/docs/k6//javascript-api/jslib/httpx/delete) | Makes DELETE request | +| [batch(requests)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/batch) | Batches multiple HTTP requests together to issue them in parallel. | +| [setBaseUrl(url)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/setbaseurl) | Sets the base URL for the session | +| [addHeader(key, value)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/addheader) | Adds a header to the session | +| [addHeaders(object)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/addheaders) | Adds multiple headers to the session | +| [clearHeader(name)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/clearheader) | Removes header from the session | +| [addTag(key, value)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/addtag) | Adds a tag to the session | +| [addTags(object)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/addtags) | Adds multiple tags to the session | +| [clearTag(name)](https://grafana.com/docs/k6//javascript-api/jslib/httpx/cleartag) | Removes tag from the session | + +### Example + +{{< code >}} + +```javascript +import { fail } from 'k6'; +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; +import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +const USERNAME = `user${randomIntBetween(1, 100000)}@example.com`; // random email address +const PASSWORD = 'superCroc2021'; + +const session = new Httpx({ + baseURL: 'https://test-api.k6.io', + headers: { + 'User-Agent': 'My custom user agent', + 'Content-Type': 'application/x-www-form-urlencoded', + }, + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const registrationResp = session.post(`/user/register/`, { + first_name: 'Crocodile', + last_name: 'Owner', + username: USERNAME, + password: PASSWORD, + }); + + if (registrationResp.status !== 201) { + fail('registration failed'); + } + + const loginResp = session.post(`/auth/token/login/`, { + username: USERNAME, + password: PASSWORD, + }); + + if (loginResp.status !== 200) { + fail('Authentication failed'); + } + + const authToken = loginResp.json('access'); + + // set the authorization header on the session for the subsequent requests. + session.addHeader('Authorization', `Bearer ${authToken}`); + + const payload = { + name: `Croc Name`, + sex: 'M', + date_of_birth: '2019-01-01', + }; + + // this request uses the Authorization header set above. + const respCreateCrocodile = session.post(`/my/crocodiles/`, payload); + + if (respCreateCrocodile.status !== 201) { + fail('Crocodile creation failed'); + } else { + console.log('New crocodile created'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/addheader.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addheader.md new file mode 100644 index 000000000..e662c8c4e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addheader.md @@ -0,0 +1,31 @@ +--- +title: 'addHeader( key, value )' +description: 'adds a header to the session' +description: 'adds a header to the session' +weight: 21 +--- + +# addHeader( key, value ) + +| Parameter | Type | Description | +| --------- | ------ | ------------ | +| name | string | Header name | +| value | string | Header value | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ baseURL: 'https://test-api.k6.io' }); + +session.addHeader('Authorization', 'token1'); + +export default function () { + session.get('/public/crocodiles/1/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/addheaders.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addheaders.md new file mode 100644 index 000000000..0c3994262 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addheaders.md @@ -0,0 +1,34 @@ +--- +title: 'addHeaders( object )' +description: 'adds multiple headers to the session' +description: 'adds multiple headers to the session' +weight: 22 +--- + +# addHeaders( object ) + +| Parameter | Type | Description | +| --------- | ------ | ----------- | +| headers | object | Object | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx(); + +session.addHeaders({ + 'Authorization': 'token1', + 'User-Agent': 'My custom user agent', + 'Content-Type': 'application/x-www-form-urlencoded', +}); + +export default function () { + session.get('https://test-api.k6.io/public/crocodiles/1/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/addtag.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addtag.md new file mode 100644 index 000000000..cea24539b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addtag.md @@ -0,0 +1,32 @@ +--- +title: 'addTag( key, value )' +description: 'adds a tag to the session' +description: 'adds a tag to the session' +weight: 24 +--- + +# addTag( key, value ) + +| Parameter | Type | Description | +| --------- | ------ | ----------- | +| name | string | Tag name | +| value | string | Tag value | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ baseURL: 'https://test-api.k6.io' }); + +session.addTag('tagName', 'tagValue'); +session.addTag('AnotherTagName', 'tagValue2'); + +export default function () { + session.get('/public/crocodiles/1/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/addtags.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addtags.md new file mode 100644 index 000000000..ed87dbdb6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/addtags.md @@ -0,0 +1,34 @@ +--- +title: 'addTags( object )' +description: 'adds multiple tags to the session' +description: 'adds multiple tags to the session' +weight: 25 +--- + +# addTags( object ) + +| Parameter | Type | Description | +| --------- | ------ | ----------- | +| headers | object | Object | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx(); + +session.addTags({ + Tag1: 'value1', + Tag2: 'value2', + Tag3: 'value3', +}); + +export default function () { + session.get('https://test-api.k6.io/public/crocodiles/1/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/asyncrequest.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/asyncrequest.md new file mode 100644 index 000000000..1dfd7aa0d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/asyncrequest.md @@ -0,0 +1,55 @@ +--- +title: 'asyncRequest(method, url, [body], [params])' +head_title: 'httpx.asyncRequest()' +description: 'Generic method for making asynchronous HTTP requests' +description: 'Generic method for making asynchronous HTTP requests' +weight: 08 +--- + +# asyncRequest(method, url, [body], [params]) + +Generic method for making arbitrary asynchronous HTTP requests. +Note, this method returns a Promise. You must use the `await` keyword to resolve it. + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| method | string | HTTP method. Must be uppercase (GET, POST, PUT, PATCH, OPTIONS, HEAD, etc) | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects will be `x-www-form-urlencoded`. Set to `null` to omit the body. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| --------------------- | ------------------------------------------------------------------------------------------------- | +| Promise with Response | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default async function testSuite() { + const resp_get = await session.asyncRequest('GET', `/status/200`); + const resp_post = await session.asyncRequest('POST', `/status/200`, { key: 'value' }); + const resp_put = await session.asyncRequest('PUT', `/status/200`, { key: 'value' }); + const resp_patch = await session.asyncRequest('PATCH', `/status/200`, { key: 'value' }); + const resp_delete = await session.asyncRequest('DELETE', `/status/200`); + + // specific methods are also available. + const respGet = await session.asyncGet(`/status/200`); + const respPost = await session.asyncPost(`/status/200`, { key: 'value' }); + const respPut = await session.asyncPut(`/status/200`, { key: 'value' }); + const respPatch = await session.asyncPatch(`/status/200`, { key: 'value' }); + const respDelete = await session.asyncDelete(`/status/200`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/batch.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/batch.md new file mode 100644 index 000000000..34ea828f0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/batch.md @@ -0,0 +1,62 @@ +--- +title: 'batch( requests )' +head_title: 'httpx.batch(requests)' +description: 'Issue multiple HTTP requests in parallel (like e.g. browsers tend to do).' +description: 'Issue multiple HTTP requests in parallel (like e.g. browsers tend to do).' +weight: 19 +--- + +# batch( requests ) + +Batch multiple HTTP requests together, to issue them in parallel over multiple TCP connections. + +| Parameter | Type | Description | +| ----------------- | ------ | ------------------------------------------------------ | +| requests | array | An array containing requests, in string or object form | +| params (optional) | object | Additional parameters for all requests in the batch | + +### Returns + +| Type | Description | +| ----- | ----------------------------------------------------------------------------------------------------------------- | +| array | An array containing [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) objects. | + +### Example + +{{< code >}} + +```javascript +import { Httpx, Get } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; +import { describe } from 'https://jslib.k6.io/expect/0.0.4/index.js'; + +const session = new Httpx({ baseURL: 'https://test-api.k6.io' }); + +export default function () { + describe('01. Fetch public crocodiles all at once', (t) => { + const responses = session.batch( + [ + new Get('/public/crocodiles/1/'), + new Get('/public/crocodiles/2/'), + new Get('/public/crocodiles/3/'), + new Get('/public/crocodiles/4/'), + ], + { + tags: { name: 'PublicCrocs' }, + } + ); + + responses.forEach((response) => { + t.expect(response.status) + .as('response status') + .toEqual(200) + .and(response) + .toHaveValidJson() + .and(response.json('age')) + .as('croc age') + .toBeGreaterThan(7); + }); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/clearheader.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/clearheader.md new file mode 100644 index 000000000..721dfaaaa --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/clearheader.md @@ -0,0 +1,30 @@ +--- +title: 'clearHeader( name )' +description: 'removes header from the session' +description: 'removes header from the session' +weight: 23 +--- + +# clearHeader( name ) + +| Parameter | Type | Description | +| --------- | ------ | ------------------------- | +| name | string | Header name to be removed | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ headers: { Authorization: 'token1' } }); + +session.clearHeader('Authorization'); // removes header set in the constructor + +export default function () { + session.get('https://test-api.k6.io/public/crocodiles/1/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/cleartag.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/cleartag.md new file mode 100644 index 000000000..5328b36ba --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/cleartag.md @@ -0,0 +1,30 @@ +--- +title: 'clearTag( name )' +description: 'removes tag from the session' +description: 'removes tag from the session' +weight: 26 +--- + +# clearTag( name ) + +| Parameter | Type | Description | +| --------- | ------ | ---------------------- | +| name | string | Tag name to be removed | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ tags: { tagName: 'tagValue' } }); + +session.clearTag('tagName'); // removes tag set in the constructor + +export default function () { + session.get('https://test-api.k6.io/public/crocodiles/1/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/delete.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/delete.md new file mode 100644 index 000000000..7b1a1499d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/delete.md @@ -0,0 +1,41 @@ +--- +title: 'delete(url, [body], [params])' +description: 'httpx.delete makes DELETE requests' +description: 'httpx.delete makes DELETE requests' +weight: 14 +--- + +# delete(url, [body], [params]) + +`session.delete(url, body, params)` makes a DELETE request. Only the first parameter is required. Body is discouraged. + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects will be `x-www-form-urlencoded`. Set to `null` to omit the body. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.delete(`/delete`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/get.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/get.md new file mode 100644 index 000000000..3af87819b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/get.md @@ -0,0 +1,41 @@ +--- +title: 'get(url, [body], [params])' +description: 'httpx.get makes GET requests' +description: 'httpx.get makes GET requests' +weight: 10 +--- + +# get(url, [body], [params]) + +`session.get(url, body, params)` makes a GET request. Only the URL parameter is required + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Set to `null` to omit the body. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://test-api.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.get(`/public/crocodiles/`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/head.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/head.md new file mode 100644 index 000000000..7cf066111 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/head.md @@ -0,0 +1,42 @@ +--- +title: 'head(url, [body], [params])' +description: 'httpx.head makes HEAD requests' +description: 'httpx.head makes HEAD requests' +weight: 16 +--- + +# head(url, [body], [params]) + +`session.head(url, body, params)` makes a HEAD request. Only the first parameter is required + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects will be `x-www-form-urlencoded`. Set to `null` to omit the body. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.head(`/head`); + console.log(resp.status); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/options.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/options.md new file mode 100644 index 000000000..d7310722a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/options.md @@ -0,0 +1,42 @@ +--- +title: 'options(url, [body], [params])' +description: 'httpx.options makes OPTIONS requests' +description: 'httpx.options makes OPTIONS requests' +weight: 15 +--- + +# options(url, [body], [params]) + +`session.options(url, body, params)` makes an OPTIONS request. Only the first parameter is required + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects are `x-www-form-urlencoded`. To omit the body, set to `null`. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.options(`/options`); + console.log(resp.status); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/patch.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/patch.md new file mode 100644 index 000000000..c21488464 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/patch.md @@ -0,0 +1,47 @@ +--- +title: 'patch(url, [body], [params])' +head_title: 'httpx.patch' +description: 'httpx.patch makes PATCH requests' +description: 'httpx.patch makes PATCH requests' +weight: 13 +--- + +# patch(url, [body], [params]) + +`session.patch(url, body, params)` makes a PATCH request. Only the first parameter is required + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects are `x-www-form-urlencoded`. To omit body, set to `null` . | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.patch(`/patch`, { + first_name: 'Mr', + last_name: 'Croco', + username: 'my user', + password: 'my password', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/post.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/post.md new file mode 100644 index 000000000..877284a77 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/post.md @@ -0,0 +1,45 @@ +--- +title: 'post(url, [body], [params])' +head_title: 'httpx.post' +description: 'httpx.post makes POST requests' +description: 'httpx.post makes POST requests' +weight: 11 +--- + +# post(url, [body], [params]) + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects are `x-www-form-urlencoded`. To omit body, set to `null` . | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://test-api.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.post(`/user/register/`, { + first_name: 'Mr', + last_name: 'Croco', + username: 'my user', + password: 'my password', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/put.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/put.md new file mode 100644 index 000000000..b865d5522 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/put.md @@ -0,0 +1,48 @@ +--- +title: 'put(url, [body], [params])' +head_title: 'httpx.put' +description: 'httpx.put makes PUT requests' +description: 'httpx.put makes PUT requests' +weight: 12 +--- + +# put(url, [body], [params]) + +`session.put(url, body, params)` makes a PUT request. Only the first parameter is required + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects are `x-www-form-urlencoded`. To omit body, set to `null`. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.put(`/put`, { + first_name: 'Mr', + last_name: 'Croco', + username: 'my user', + password: 'my password', + }); + console.log(resp.status); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/request.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/request.md new file mode 100644 index 000000000..774fc143b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/request.md @@ -0,0 +1,49 @@ +--- +title: 'request(method, url, [body], [params])' +head_title: 'httpx.request()' +description: 'Generic method for making arbitrary HTTP requests' +description: 'Generic method for making arbitrary HTTP requests' +weight: 09 +--- + +# request(method, url, [body], [params]) + +Generic method for making arbitrary HTTP requests. + +Consider using specific methods for making common requests [get](https://grafana.com/docs/k6//javascript-api/jslib/httpx/get), [post](https://grafana.com/docs/k6//javascript-api/jslib/httpx/post), [put](https://grafana.com/docs/k6//javascript-api/jslib/httpx/put), [patch](https://grafana.com/docs/k6//javascript-api/jslib/httpx/patch). + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| method | string | HTTP method. Must be uppercase (GET, POST, PUT, PATCH, OPTIONS, HEAD, etc) | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects are `x-www-form-urlencoded`. To omit body, set to `null`. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp_get = session.request('GET', `/status/200`); + const resp_post = session.request('POST', `/status/200`, { key: 'value' }); + const resp_put = session.request('PUT', `/status/200`, { key: 'value' }); + const resp_patch = session.request('PATCH', `/status/200`, { key: 'value' }); + const resp_delete = session.request('DELETE', `/status/200`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/setbaseurl.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/setbaseurl.md new file mode 100644 index 000000000..5c0e06130 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/setbaseurl.md @@ -0,0 +1,30 @@ +--- +title: 'setBaseUrl( url )' +description: 'sets the base URL for the session' +description: 'sets the base URL for the session' +weight: 20 +--- + +# setBaseUrl( url ) + +| Parameter | Type | Description | +| --------- | ------ | ---------------------------------------------------------- | +| baseURL | string | Base URL to be used for all requests issued in the session | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx(); + +session.setBaseUrl('https://test-api.k6.io'); + +export default function () { + session.get('/public/crocodiles/1/'); // baseUrl doesn't need to be repeated on every request +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/httpx/trace.md b/docs/sources/v0.50.x/javascript-api/jslib/httpx/trace.md new file mode 100644 index 000000000..6057a953f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/httpx/trace.md @@ -0,0 +1,42 @@ +--- +title: 'trace(url, [body], [params])' +description: 'httpx.trace makes TRACE requests' +description: 'httpx.trace makes TRACE requests' +weight: 17 +--- + +# trace(url, [body], [params]) + +`session.trace(url, body, params)` makes a TRACE request. Only the first parameter is required + +| Parameter | Type | Description | +| ----------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| url | string | HTTP URL. If baseURL is set, provide only path. | +| body (optional) | null / string / object / ArrayBuffer / [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | Request body; objects will be `x-www-form-urlencoded`. Set to `null` to omit the body. | +| params (optional) | null or object {} | Additional [parameters](https://grafana.com/docs/k6//javascript-api/k6-http/params) for this specific request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import { Httpx } from 'https://jslib.k6.io/httpx/0.1.0/index.js'; + +const session = new Httpx({ + baseURL: 'https://httpbin.test.k6.io', + timeout: 20000, // 20s timeout. +}); + +export default function testSuite() { + const resp = session.trace(`/trace`); + console.log(resp.status); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/_index.md new file mode 100644 index 000000000..4d3dfb89d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/_index.md @@ -0,0 +1,82 @@ +--- +title: "k6chaijs" +description: "Assertion library for k6" +weight: 03 +weight: 03 +--- + +# k6chaijs + +`k6chaijs` is a library to provide BDD assertions in k6 based on [ChaiJS](https://www.chaijs.com/). You can use `k6chaijs` as an alternative to [check](https://grafana.com/docs/k6//javascript-api/k6/check) and [group](https://grafana.com/docs/k6//javascript-api/k6/group). + +With this library, you get the following: + +- BDD style of assertions for more expressive language +- chainable assertions +- more powerful assertions functions such as: `deep`, `nested`, `ordered`, etc. +- automatic assertion messages +- [exception handling](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs/error-handling) for better test stability + +## Installation + +There's nothing to install. This library is hosted on [jslib](https://jslib.k6.io/) and can be imported in the k6 script directly. + +{{< code >}} + +```javascript +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; +``` + +{{< /code >}} + +Alternatively, you can use a copy of this file stored locally. The source code is available on [GitHub](https://github.com/grafana/k6-jslib-k6chaijs). + +## Example + +The following example tests a hypothetical HTTP API that returns a JSON array of objects. Copy the following code, and save it as `script.js`: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +export default function testSuite() { + describe('Fetch a list of public crocodiles', () => { + const response = http.get('https://test-api.k6.io/public/crocodiles'); + + expect(response.status, 'response status').to.equal(200); + expect(response).to.have.validJsonBody(); + expect(response.json().length, 'number of crocs').to.be.above(4); + }); +} +``` + +{{< /code >}} + +When you run this test with `k6 run script.js`, the output at the end of the test shows: + +```bash +█ Fetch a list of public crocodiles + ✓ expected response status to equal 200 + ✓ has valid json body + ✓ expected number of crocs to be above 4 +``` + +If you are familiar with k6, the result is the same as using [check](https://grafana.com/docs/k6//javascript-api/k6/check) and [group](https://grafana.com/docs/k6//javascript-api/k6/group). Note that [expect](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs/expect) might add or extend the assertion message. + +## API + +| API | Description | +| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [config](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs/config) | Options to change `k6chaijs` behaviour. | +| [describe](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs/describe) | A wrapper of [group](https://grafana.com/docs/k6//javascript-api/k6/group) that catches exceptions to allow continuing the test execution. It returns a boolean to indicate the success of all its `k6chaijs` assertions. | +| [expect](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs/expect) | A wrapper of [check](https://grafana.com/docs/k6//javascript-api/k6/check) that provides BDD style of assertions. | + +## Plugins + +It's possible to extend the default functionality with [Chai plugins](https://www.chaijs.com/plugins/). To use a plugin or build a Chai version with plugins, follow the instructions in this [example](https://community.grafana.com/t/how-to-build-plugins-for-chaijs/97010/3). + +## Read more + +- [Using chai with k6](https://k6.io/blog/k6-chai-js/) diff --git a/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/config.md b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/config.md new file mode 100644 index 000000000..514f01483 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/config.md @@ -0,0 +1,53 @@ +--- +title: 'config' +description: 'Global configuration options for k6Chaijs' +weight: 31 +--- + +# config + +Chai exposes a few options to change the library configuration. + +| Config option | Default | Description | +| ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| truncateMsgThreshold | 300 | Check message length is truncated to this value. | +| truncateVariableThreshold | 100 | Variables interpolated into check message are truncated to this length. It prevents mistakes when the check name is very large, especially when `aggregateChecks` is `false`. | +| aggregateChecks | true | The actual values are not interpolated into the check message. Disable for tests with 1 iteration. | +| logFailures | false | When the check fails, debug messages are printed. | + +{{< code >}} + +```javascript +import http from 'k6/http'; +import chai, { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +// individual variables should be up to 20 chars after rendering. +chai.config.truncateVariableThreshold = 20; + +// whole check() message must be below 300 chars. +chai.config.truncateMsgThreshold = 300; + +// the variable values (resp.status) are inserted into the check message - useful for debugging or tests with 1 iteration +chai.config.aggregateChecks = false; + +// when the check fails, WARN messages with variables are printed. Useful for debugging. +chai.config.logFailures = true; + +export default function testSuite() { + describe('Testing bad assertion.', () => { + const response = http.get('https://test-api.k6.io/'); + + expect(response.body).to.have.lengthOf.at.least(500); + }); +} +``` + +{{< /code >}} + +The resulting + +```bash +█ Testing bad assertion. + ✓ expected '\n\n\n\n/javascript-api/k6/group) that adds the ability to: + +- [Catch exceptions](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs/error-handling) to allow continuing the execution outside of the `describe` function. +- Returns a boolean to indicate the success of all its `k6chaijs` assertions. + +{{< code >}} + +```javascript +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +export default function testSuite() { + const success1 = describe('Basic test', () => { + expect(1, 'number one').to.equal(1); + }); + console.log(success1); // true + + const success2 = describe('Another test', () => { + throw 'Something entirely unexpected happened'; + }); + console.log(success2); // false + + const success3 = describe('Yet another test', () => { + expect(false, 'my vaule').to.be.true(); + }); + console.log(success3); // false +} +``` + +{{< /code >}} + +{{< code >}} + +```bash +default ✓ [======================================] 1 VUs 00m00.0s/10m0s 1/1 iters, 1 per VU + + █ Basic test + ✓ expected number one to equal 1 + + █ Another test + ✗ Exception raised "Something entirely unexpected happened" + ↳ 0% — ✓ 0 / ✗ 1 + + █ Yet another test + ✗ expected my vaule to be true + ↳ 0% — ✓ 0 / ✗ 1 +``` + +{{< /code >}} + +## API + +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------------------------- | +| name | string | Test case name. The test case name should be unique. Otherwise, the test case will be grouped. | +| function | function | The test case function to be executed | + +### Returns + +| Type | Description | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| bool | Returns true when all `expect` conditions within the `describe()` body were successful, and no unhandled exceptions were raised, otherwise false. | + +## Chaining describe() blocks + +If you want to skip the execution of the following `describe` blocks, consider chaining them using `&&` as shown below. + + + +```javascript +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +export default function testSuite() { + describe('Basic test', () => { + expect(1, 'number one').to.equal(1); + }) && + describe('Another test', () => { + throw 'Something entirely unexpected happened'; + }) && + describe('Yet another test', () => { + // the will not be executed because the prior block returned `false` + expect(false, 'my vaule').to.be.true(); + }); +} +``` diff --git a/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/error-handling.md b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/error-handling.md new file mode 100644 index 000000000..c45ed6c98 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/error-handling.md @@ -0,0 +1,96 @@ +--- +title: 'Error handling' +description: 'How to handle errors in k6chaijs.' +weight: 35 +--- + +# Error handling + +When you execute a load test, your System Under Test (SUT) may often become over saturated and start responding with errors. In this case, you need to consider what the iteration execution should do: + +1. to embrace the system error and continue the execution of the scenario +2. or to exit + +It's not uncommon for performance testers to forget about these cases. We often write fragile test code that assumes our system's response will always succeed and contain the expected data. + +{{< code >}} + +```javascript +import { check, group } from 'k6'; +import http from 'k6/http'; + +export default function () { + group('Fetch a list of public crocodiles', () => { + const res = http.get('https://test-api.k6.io/public/crocodiles'); + check(res, { + 'is status 200': (r) => r.status === 200, + 'got more than 5 crocs': (r) => r.json().length > 5, + }); + // ... continue test within the group... + }); + + group('other group', () => { + //... + }); +} +``` + +{{< /code >}} + +This code will work fine when the SUT returns correct responses. But, when the SUT starts to fail, `r.json().length` will throw an exception: + +```bash +ERRO[0001] cannot parse json due to an error at line 1, character 2 , error: invalid character '<' looking for beginning of value +running at reflect.methodValueCall (native) +``` + +In this case, k6 throws a JavaScript exception and **exits the execution of the current iteration** but continues starting new iterations. + +This might not be ideal because: + +1. system errors are propagated as exceptions that are not reported on the test results +2. you might want to embrace these errors - as normal - and continue the execution + +It's possible to rewrite this test to be less fragile, but it can make our test code longer and less readable. + +## Handling exceptions + +Sometimes it's hard to predict how a SUT might fail. For those cases, [describe](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs/describe) catches any internal exceptions and: + +1. records them as failed assertions +2. continues the execution (outside of its `describe()` function) + +{{< code >}} + +```javascript +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +export default function testSuite() { + describe('Test case against a Shaky SUT', (t) => { + throw 'Something entirely unexpected happened'; + }); + + // this test case will be executed because + // the previous `describe` catched the exception + describe('Another test case', (t) => { + expect(2).to.equal(2); + }); +} +``` + +{{< /code >}} + +{{< code >}} + +```bash +█ Test case against a Shaky SUT + + ✗ Exception raised "Something entirely unexpected happened" + ↳ 0% — ✓ 0 / ✗ 1 + +█ Another test case + + ✓ expected ${this} to equal 2 +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/expect.md b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/expect.md new file mode 100644 index 000000000..abfd2ada9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/k6chaijs/expect.md @@ -0,0 +1,168 @@ +--- +title: 'expect()' +description: 'BDD style to construct k6 assertions.' +weight: 34 +--- + +# expect() + +`expect` is a wrapper of [check](https://grafana.com/docs/k6//javascript-api/k6/check) to provide BDD style of assertions in k6. It implements the [Chai Expect API](https://www.chaijs.com/api/bdd/): + + + +- [not](https://www.chaijs.com/api/bdd/#method_not) +- [deep](https://www.chaijs.com/api/bdd/#method_deep) +- [nested](https://www.chaijs.com/api/bdd/#method_nested) +- [own](https://www.chaijs.com/api/bdd/#method_own) +- [ordered](https://www.chaijs.com/api/bdd/#method_ordered) +- [any](https://www.chaijs.com/api/bdd/#method_any) +- [all](https://www.chaijs.com/api/bdd/#method_all) +- [a](https://www.chaijs.com/api/bdd/#method_a) +- [include](https://www.chaijs.com/api/bdd/#method_include) +- [ok](https://www.chaijs.com/api/bdd/#method_ok) +- [true](https://www.chaijs.com/api/bdd/#method_true) +- [false](https://www.chaijs.com/api/bdd/#method_false) +- [null](https://www.chaijs.com/api/bdd/#method_null) +- [undefined](https://www.chaijs.com/api/bdd/#method_undefined) +- [NaN](https://www.chaijs.com/api/bdd/#method_nan) +- [exist](https://www.chaijs.com/api/bdd/#method_exist) +- [empty](https://www.chaijs.com/api/bdd/#method_empty) +- [arguments](https://www.chaijs.com/api/bdd/#method_arguments) +- [equal](https://www.chaijs.com/api/bdd/#method_equal) +- [eql](https://www.chaijs.com/api/bdd/#method_eql) +- [above](https://www.chaijs.com/api/bdd/#method_above) +- [least](https://www.chaijs.com/api/bdd/#method_least) +- [below](https://www.chaijs.com/api/bdd/#method_below) +- [most](https://www.chaijs.com/api/bdd/#method_most) +- [within](https://www.chaijs.com/api/bdd/#method_within) +- [instanceof](https://www.chaijs.com/api/bdd/#method_instanceof) +- [property](https://www.chaijs.com/api/bdd/#method_property) +- [ownPropertyDescriptor](https://www.chaijs.com/api/bdd/#method_ownpropertydescriptor) +- [lengthOf](https://www.chaijs.com/api/bdd/#method_lengthOf) +- [match](https://www.chaijs.com/api/bdd/#method_match) +- [string](https://www.chaijs.com/api/bdd/#method_string) +- [keys](https://www.chaijs.com/api/bdd/#method_keys) +- [throw](https://www.chaijs.com/api/bdd/#method_throw) +- [respondTo](https://www.chaijs.com/api/bdd/#method_respondto) +- [itself](https://www.chaijs.com/api/bdd/#method_itself) +- [satisfy](https://www.chaijs.com/api/bdd/#method_satisfy) +- [closeTo](https://www.chaijs.com/api/bdd/#method_closeto) +- [members](https://www.chaijs.com/api/bdd/#method_members) +- [oneOf](https://www.chaijs.com/api/bdd/#method_oneOf) +- [change](https://www.chaijs.com/api/bdd/#method_change) +- [increase](https://www.chaijs.com/api/bdd/#method_increase) +- [decrease](https://www.chaijs.com/api/bdd/#method_decrease) +- [by](https://www.chaijs.com/api/bdd/#method_by) +- [extensible](https://www.chaijs.com/api/bdd/#method_extensible) +- [sealed](https://www.chaijs.com/api/bdd/#method_sealed) +- [frozen](https://www.chaijs.com/api/bdd/#method_frozen) +- [finite](https://www.chaijs.com/api/bdd/#method_finite) + + + +{{< code >}} + + + +```javascript +import http from 'k6/http'; +import { describe, expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.3/index.js'; + +export default function () { + describe('Basic test', () => { + expect({ a: 1 }).to.not.have.property('b'); + expect(2).to.equal(2); + expect({ a: 1 }).to.deep.equal({ a: 1 }); + expect({ a: { b: ['x', 'y'] } }).to.have.nested.property('a.b[1]'); + expect({ a: 1 }).to.have.own.property('a'); + expect([1, 2]).to.have.ordered.members([1, 2]).but.not.have.ordered.members([2, 1]); + expect({ a: 1, b: 2 }).to.not.have.any.keys('c', 'd'); + expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b'); + expect('foo').to.be.a('string'); + expect([1, 2, 3]).to.be.an('array').that.includes(2); + expect('foobar').to.include('foo'); + expect(1).to.equal(1); + expect(true).to.be.true; + expect(false).to.be.false; + expect(null).to.be.null; + expect(undefined).to.be.undefined; + expect(NaN).to.be.NaN; + expect(1).to.equal(1); + expect([]).to.be.empty; + expect({}).not.to.be.arguments; + expect(1).to.equal(1); + expect({ a: 1 }).to.eql({ a: 1 }).but.not.equal({ a: 1 }); + expect('foo').to.have.lengthOf(3); + expect(2).to.equal(2); + expect(1).to.equal(1); + expect(2).to.be.at.most(3); + expect(2).to.equal(2); + expect({ a: 1 }).to.not.be.an.instanceof(Array); + expect({ a: 1 }).to.have.property('a'); + expect([1, 2, 3]).to.have.lengthOf(3); + expect('foobar').to.match(/^foo/); + expect('foobar').to.have.string('bar'); + expect({ a: 1, b: 2 }).to.have.all.keys('a', 'b'); + const badFn = function () { + throw new TypeError('Illegal salmon!'); + }; + expect(badFn).to.throw(); + expect(1).to.satisfy(function (num) { + return num > 0; + }); + expect(1.5).to.equal(1.5); + expect([1, 2, 3]).to.have.members([2, 1, 3]); + expect('Today is sunny').to.contain.oneOf(['sunny', 'cloudy']); + expect({ a: 1 }).to.be.extensible; + expect(1).to.be.finite; + }); +} +``` + +{{< /code >}} + +When you run this test with `k6 run script.js`, the output at the end of the test shows: + +```bash +default ✓ [======================================] 1 VUs 00m00.0s/10m0s 1/1 iters, 1 per VU + + █ Basic test + + ✓ expected ${this} to not have property 'b' + ✓ expected ${this} to equal 2 + ✓ expected ${this} to deeply equal { a: 1 } + ✓ expected ${this} to have nested property 'a.b[1]' + ✓ expected ${this} to have own property 'a' + ✓ expected ${this} to be an array + ✓ expected ${this} to have the same ordered members as [ 1, 2 ] + ✓ expected ${this} to not have the same ordered members as [ 2, 1 ] + ✓ expected ${this} to not have keys 'c', or 'd' + ✓ expected ${this} to have keys 'a', and 'b' + ✓ expected ${this} to be a string + ✓ expected ${this} to include 2 + ✓ expected ${this} to include 'foo' + ✓ expected ${this} to equal 1 + ✓ expected ${this} to be true + ✓ expected ${this} to be false + ✓ expected ${this} to be null + ✓ expected ${this} to be undefined + ✓ expected ${this} to be NaN + ✓ expected ${this} to be empty + ✓ expected ${this} to not be arguments + ✓ expected ${this} to not equal { a: 1 } + ✓ expected ${this} to have property 'length' + ✓ expected ${this} to have a length of 3 got ${actual} + ✓ expected ${this} to be at most 3 + ✓ expected ${this} to not be an instance of Array + ✓ expected ${this} to have property 'a' + ✓ expected ${this} to match /^foo/ + ✓ expected ${this} to contain 'bar' + ✓ expected ${this} to be a function + ✓ expected ${this} to throw an error + ✓ expected ${this} to satisfy [Function] + ✓ expected ${this} to equal 1.5 + ✓ expected ${this} to have the same members as [ 2, 1, 3 ] + ✓ expected ${this} to contain one of [ 'sunny', 'cloudy' ] + ✓ expected ${this} to be extensible + ✓ expected ${this} to be a finite number +``` diff --git a/docs/sources/v0.50.x/javascript-api/jslib/utils/_index.md b/docs/sources/v0.50.x/javascript-api/jslib/utils/_index.md new file mode 100644 index 000000000..bb12a3cb5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/utils/_index.md @@ -0,0 +1,58 @@ +--- +title: 'utils' +description: 'A collection of small utility functions useful during load testing with k6. ' +weight: 04 +--- + +# utils + +The `utils` module contains number of small utility functions useful in every day load testing. + +> ⭐️ Source code available on [GitHub](https://github.com/k6io/k6-jslib-utils). +> Please request features and report bugs through [GitHub issues](https://github.com/k6io/k6-jslib-utils/issues). + +| Function | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [randomIntBetween(min, max)](https://grafana.com/docs/k6//javascript-api/jslib/utils/randomintbetween) | Random integer in a given range | +| [randomItem(array)](https://grafana.com/docs/k6//javascript-api/jslib/utils/randomitem) | Random item from a given array | +| [randomString(length, [charset])](https://grafana.com/docs/k6//javascript-api/jslib/utils/randomstring) | Random string of a given length, optionally selected from a custom character set | +| [uuidv4()](https://grafana.com/docs/k6//javascript-api/jslib/utils/uuidv4) | Random UUID v4 in a string representation | +| [findBetween(content, left, right, [repeat])](https://grafana.com/docs/k6//javascript-api/jslib/utils/findbetween) | Extract a string between two surrounding strings | +| [normalDistributionStages(maxVUs, durationSeconds, [numberOfStages])](https://grafana.com/docs/k6//javascript-api/jslib/utils/normaldistributionstages) | Creates [stages](https://grafana.com/docs/k6//using-k6/k6-options#stages) which will produce a normal distribution (bell-curve) of VUs for a test | +| getCurrentStageIndex | Get the index of the running stage as defined in the `stages` array options. It can be used only with the executors that support the `stages` option as [ramping-vus](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus) or [ramping-arrival-rate](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-arrival-rate). | +| tagWithCurrentStageIndex | Tag all the generated metrics in the iteration with the index of the current running stage. | +| tagWithCurrentStageProfile | Tag all the generated metrics in the iteration with the computed profile for the current running stage. | + +## Simple example + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import http from 'k6/http'; + +import { + randomIntBetween, + randomString, + randomItem, + uuidv4, + findBetween, +} from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; + +export default function () { + const res = http.post(`https://test-api.k6.io/user/register/`, { + first_name: randomItem(['Joe', 'Jane']), // random name + last_name: `Jon${randomString(1, 'aeiou')}s`, //random character from given list + username: `user_${randomString(10)}@example.com`, // random email address, + password: uuidv4(), // random password in form of uuid + }); + + // find a string between two strings to grab the username: + const username = findBetween(res.body, '"username":"', '"'); + console.log('username from response: ' + username); + + sleep(randomIntBetween(1, 5)); // sleep between 1 and 5 seconds. +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/utils/findbetween.md b/docs/sources/v0.50.x/javascript-api/jslib/utils/findbetween.md new file mode 100644 index 000000000..d19d37b92 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/utils/findbetween.md @@ -0,0 +1,43 @@ +--- +title: 'findBetween(content, left, right, [repeat])' +description: 'findBetween function' +weight: 45 +--- + +# findBetween(content, left, right, [repeat]) + +Function that returns a string from between two other strings. + +| Parameter | Type | Description | +| ----------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- | +| content | string | The string to search through (e.g. [Response.body](https://grafana.com/docs/k6//javascript-api/k6-http/response)) | +| left | string | The string immediately before the value to be extracted | +| right | string | The string immediately after the value to be extracted | +| repeat (optional) | boolean | If `true`, the result will be a string array containing all occurrences | + +### Returns + +| Type | Description | +| ------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| string | The extracted string, or an empty string if no match was found. If `repeat=true`, this will be an array of strings or an empty array | + +### Example + +{{< code >}} + +```javascript +import { findBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export default function () { + const response = '
Message 1
Message 2
'; + + const message = findBetween(response, '
', '
'); + + console.log(message); // Message 1 + + const allMessages = findBetween(response, '
', '
', true); + console.log(allMessages.length); // 2 +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/utils/normaldistributionstages.md b/docs/sources/v0.50.x/javascript-api/jslib/utils/normaldistributionstages.md new file mode 100644 index 000000000..036fa9b0a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/utils/normaldistributionstages.md @@ -0,0 +1,44 @@ +--- +title: 'normalDistributionStages(maxVus, durationSeconds, [numberOfStages])' +description: 'normalDistributionStages function' +weight: 46 +--- + +# normalDistributionStages(maxVus, durationSeconds, [numberOfStages]) + +Function to create [stages](https://grafana.com/docs/k6//using-k6/k6-options#stages) producing a _normal distribution (bell-curve)_ of VUs for a test. + +| Parameter | Type | Description | +| ------------------------- | ---- | ------------------------------------------------ | +| maxVus | int | Maximum virtual users at the height of the curve | +| durationSeconds | int | Overall duration for all stages combined | +| numberOfStages (optional) | int | Number of stages to create; default is `10` | + +### Returns + +| Type | Description | +| -------------- | ---------------------------------------------------------------------------------- | +| array\[object] | An array of `{"duration": "XXXs", "target": XXX}` JSON objects representing stages | + +### Example + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import exec from 'k6/execution'; +import { normalDistributionStages } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export const options = { + // Alters the number of VUs from 1 to 10 over a period + // of 20 seconds comprised of 5 stages. + stages: normalDistributionStages(10, 20, 5), +}; + +export default function () { + console.log(exec.instance.vusActive); + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/utils/randomintbetween.md b/docs/sources/v0.50.x/javascript-api/jslib/utils/randomintbetween.md new file mode 100644 index 000000000..c27a07671 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/utils/randomintbetween.md @@ -0,0 +1,37 @@ +--- +title: 'randomIntBetween(min, max)' +description: 'Random integer' +weight: 41 +--- + +# randomIntBetween(min, max) + +Function returns a random number between the specified range. The returned value is no lower than (and may possibly equal) min, and is no bigger than (and may possibly equal) max. + +| Parameter | Type | Description | +| --------- | ---- | -------------------------- | +| min | int | Lower-end bound. Inclusive | +| max | int | Upper-end bound. Inclusive | + +### Returns + +| Type | Description | +| ---- | -------------- | +| int | Random integer | + +### Example + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export default function () { + // code ... + + sleep(randomIntBetween(1, 5)); // sleep between 1 and 5 seconds. +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/utils/randomitem.md b/docs/sources/v0.50.x/javascript-api/jslib/utils/randomitem.md new file mode 100644 index 000000000..382c9efd5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/utils/randomitem.md @@ -0,0 +1,36 @@ +--- +title: 'randomItem(array)' +description: 'Random item from an array' +weight: 42 +--- + +# randomItem(array) + +Function returns a random item from an array. + +| Parameter | Type | Description | +| ------------ | ----- | ----------------- | +| arrayOfItems | array | Array [] of items | + +### Returns + +| Type | Description | +| ---- | -------------------------- | +| any | Random item from the array | + +### Example + +{{< code >}} + +```javascript +import { randomItem } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +const names = ['John', 'Jane', 'Bert', 'Ed']; + +export default function () { + const randomName = randomItem(names); + console.log(`Hello, my name is ${randomName}`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/utils/randomstring.md b/docs/sources/v0.50.x/javascript-api/jslib/utils/randomstring.md new file mode 100644 index 000000000..77a4b6420 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/utils/randomstring.md @@ -0,0 +1,41 @@ +--- +title: 'randomString(length, [charset])' +description: 'Random string' +weight: 43 +--- + +# randomString(length, [charset]) + +Function returns a random string of a given length, optionally selected from a custom character set. + +| Parameter | Type | Description | +| ------------------ | ------ | ------------------------------- | +| length | int | Length of the random string | +| charset (optional) | string | A customized list of characters | + +### Returns + +| Type | Description | +| ---- | ------------------------------------------- | +| any | Random item(s) from the array of characters | + +### Example + +{{< code >}} + +```javascript +import { randomString } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export default function () { + const randomFirstName = randomString(8); + console.log(`Hello, my first name is ${randomFirstName}`); + + const randomLastName = randomString(10, `aeioubcdfghijpqrstuv`); + console.log(`Hello, my last name is ${randomLastName}`); + + const randomCharacterWeighted = randomString(1, `AAAABBBCCD`); + console.log(`Chose a random character ${randomCharacterWeighted}`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/jslib/utils/uuidv4.md b/docs/sources/v0.50.x/javascript-api/jslib/utils/uuidv4.md new file mode 100644 index 000000000..bb9e9f272 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/jslib/utils/uuidv4.md @@ -0,0 +1,36 @@ +--- +title: 'uuidv4()' +description: 'uuid v4 function' +weight: 44 +--- + +# uuidv4() + +Function returns a random uuid v4 in a string form. + +### Parameters + +| Parameter | Type | Description | +| :---------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| secure (optional) | boolean | By default, `uuidv4()` uses a standard random number generator. If the `secure` option is set to `true`, `uuidv4` uses a cryptographically secure random number generator instead. While this adds security, the `secure` option also makes the function an order of magnitude slower. | + +### Returns + +| Type | Description | +| :----- | :-------------------- | +| string | Random UUID v4 string | + +### Example + +{{< code >}} + +```javascript +import { uuidv4 } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; + +export default function () { + const randomUUID = uuidv4(); + console.log(randomUUID); // 35acae14-f7cb-468a-9866-1fc45713149a +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/_index.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/_index.md new file mode 100644 index 000000000..ded916085 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/_index.md @@ -0,0 +1,31 @@ +--- +title: 'k6/crypto' +description: 'The k6/crypto module provides common hashing functionality available in the GoLang crypto.' +weight: 03 +--- + +# k6/crypto + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +The k6/crypto `module` provides common hashing functionality available in the GoLang [crypto](https://golang.org/pkg/crypto/) package. + +| Function | Description | +| ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| [createHash(algorithm)](https://grafana.com/docs/k6//javascript-api/k6-crypto/createhash) | Create a Hasher object, allowing the user to add data to hash multiple times, and extract hash digests along the way. | +| [createHMAC(algorithm, secret)](https://grafana.com/docs/k6//javascript-api/k6-crypto/createhmac) | Create an HMAC hashing object, allowing the user to add data to hash multiple times, and extract hash digests along the way. | +| [hmac(algorithm, secret, data, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/hmac) | Use HMAC to sign an input string. | +| [md4(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/md4) | Use MD4 to hash an input string. | +| [md5(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/md5) | Use MD5 to hash an input string. | +| [randomBytes(int)](https://grafana.com/docs/k6//javascript-api/k6-crypto/randombytes) | Return an array with a number of cryptographically random bytes. | +| [ripemd160(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/ripemd160) | Use RIPEMD-160 to hash an input string. | +| [sha1(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha1) | Use SHA-1 to hash an input string. | +| [sha256(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha256) | Use SHA-256 to hash an input string. | +| [sha384(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha384) | Use SHA-384 to hash an input string. | +| [sha512(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha512) | Use SHA-512 to hash an input string. | +| [sha512_224(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha512_224) | Use SHA-512/224 to hash an input string. | +| [sha512_256(input, outputEncoding)](https://grafana.com/docs/k6//javascript-api/k6-crypto/sha512_256) | Use SHA-512/256 to hash an input string. | + +| Class | Description | +| ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Hasher](https://grafana.com/docs/k6//javascript-api/k6-crypto/hasher) | Object returned by [crypto.createHash()](https://grafana.com/docs/k6//javascript-api/k6-crypto/createhash). It allows adding more data to be hashed and to extract digests along the way. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/createhash.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/createhash.md new file mode 100644 index 000000000..e591f2241 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/createhash.md @@ -0,0 +1,47 @@ +--- +title: 'createHash( algorithm )' +description: 'Create a Hasher object, allowing the user to add data to hash multiple times, and extract hash digests along the way.' +description: 'Create a Hasher object, allowing the user to add data to hash multiple times, and extract hash digests along the way.' +weight: 01 +--- + +# createHash( algorithm ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Creates a hashing object that can then be fed with data repeatedly, and from which you can extract a hash digest whenever you want. + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| algorithm | string | The name of the hashing algorithm you want to use. Can be any one of "md4", "md5", "sha1", "sha256", "sha384", "sha512", "sha512_224", "sha512_256", "ripemd160". | + +### Returns + +| Type | Description | +| ------ | -------------------------------------------------------------------------------------------- | +| object | A [Hasher](https://grafana.com/docs/k6//javascript-api/k6-crypto/hasher) object. | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + console.log(crypto.sha256('hello world!', 'hex')); + const hasher = crypto.createHash('sha256'); + hasher.update('hello '); + hasher.update('world!'); + console.log(hasher.digest('hex')); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 +INFO[0000] 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/createhmac.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/createhmac.md new file mode 100644 index 000000000..9bea04585 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/createhmac.md @@ -0,0 +1,48 @@ +--- +title: 'createHMAC( algorithm, secret )' +description: 'Create an HMAC hashing object, allowing the user to add data to hash multiple times, and extract hash digests along the way.' +description: 'Create an HMAC hashing object, allowing the user to add data to hash multiple times, and extract hash digests along the way.' +weight: 02 +--- + +# createHMAC( algorithm, secret ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Creates a HMAC hashing object that can then be fed with data repeatedly, and from which you can extract a signed hash digest whenever you want. + +| Parameter | Type | Description | +| --------- | :------------------: | :---------------------------------------------------------------------------------------------------------------------------------- | +| algorithm | string | The hashing algorithm to use. One of `md4`, `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `sha512_224`, `sha512_256` or `ripemd160`. | +| secret | string / ArrayBuffer | A shared secret used to sign the data. | + +### Returns + +| Type | Description | +| ------ | :------------------------------------------------------------------------------------------- | +| object | A [Hasher](https://grafana.com/docs/k6//javascript-api/k6-crypto/hasher) object. | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + console.log(crypto.hmac('sha256', 'a secret', 'my data', 'hex')); + const hasher = crypto.createHMAC('sha256', 'a secret'); + hasher.update('my '); + hasher.update('data'); + console.log(hasher.digest('hex')); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] 82f669c8fde13aef6d6977257588dc4953dfac505428f8fd6b52e19cd96d7ea5 +INFO[0000] 82f669c8fde13aef6d6977257588dc4953dfac505428f8fd6b52e19cd96d7ea5 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/hasher.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/hasher.md new file mode 100644 index 000000000..e8a129caf --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/hasher.md @@ -0,0 +1,53 @@ +--- +title: 'Hasher' +description: 'Object returned by crypto.createHash(). It allows adding more data to be hashed and to extract digests along the way.' +description: 'Object returned by crypto.createHash(). It allows adding more data to be hashed and to extract digests along the way.' +weight: 80 +--- + +# Hasher + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +This object is returned by [crypto.createHash()](https://grafana.com/docs/k6//javascript-api/k6-crypto/createhash) +and allows the user to successively add more string data to be hashed, and to extract digests along the way. + +| Name | Type | Description | +| --------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Hasher.update(string) | function | Add more data to the string we want to create a hash of. Takes one string argument, which is the new data we want to add. | +| Hasher.digest(string) | function | Return a digest from the data added (using `update()`) to the Hasher object so far. Takes one string argument, which is the encoding format to return. This can be either "base64", "base64url", "base64rawurl", "hex" or "binary". See the examples below for details. | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + console.log(crypto.sha256('hello world!', 'hex')); + const hasher = crypto.createHash('sha256'); + hasher.update('hello '); + hasher.update('world!'); + console.log(hasher.digest('hex')); + + // Other encodings + console.log('base64:', hasher.digest('base64')); + console.log('base64url:', hasher.digest('base64url')); + console.log('base64rawurl:', hasher.digest('base64rawurl')); + console.log('binary:', new Uint8Array(hasher.digest('binary'))); +} +``` + +{{< /code >}} + +The above code sample should produce this in its output: + +```bash +INFO[0000] 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 +INFO[0000] 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 +INFO[0000] base64: dQnlvaDHYtK6x/kNdYtbImP6Acy8VCq1498WO+CObKk= +INFO[0000] base64url: dQnlvaDHYtK6x_kNdYtbImP6Acy8VCq1498WO-CObKk= +INFO[0000] base64rawurl: dQnlvaDHYtK6x_kNdYtbImP6Acy8VCq1498WO-CObKk +INFO[0000] binary: 117,9,229,189,160,199,98,210,186,199,249,13,117,139,91,34,99,250,1,204,188,84,42,181,227,223,22,59,224,142,108,169 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/hmac.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/hmac.md new file mode 100644 index 000000000..7edc68512 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/hmac.md @@ -0,0 +1,54 @@ +--- +title: 'hmac( algorithm, secret, data, outputEncoding )' +description: 'Use HMAC to sign input data.' +description: 'Use HMAC to sign input data.' +weight: 03 +--- + +# hmac( algorithm, secret, data, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [HMAC](https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) to sign a piece of data using a shared secret. + +| Parameter | Type | Description | +| -------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| algorithm | string | The hashing algorithm to use. One of `md4`, `md5`, `sha1`, `sha256`, `sha384`, `sha512`, `sha512_224`, `sha512_256` or `ripemd160`. | +| secret | string / ArrayBuffer | A shared secret used to sign the data. | +| data | string / ArrayBuffer | The data to sign. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.hmac('sha256', 'mysecret', 'hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.hmac('sha256', 'mysecret', new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +{{< code >}} + +```bash +INFO[0000] 893a72d8cab129e5ba85aea4599fd53f59bfe652cff4098a3780313228d8c20f +INFO[0000] 893a72d8cab129e5ba85aea4599fd53f59bfe652cff4098a3780313228d8c20f +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/md4.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/md4.md new file mode 100644 index 000000000..60e661363 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/md4.md @@ -0,0 +1,48 @@ +--- +title: 'md4( input, outputEncoding )' +description: 'Use MD4 to hash input data.' +description: 'Use MD4 to hash input data.' +weight: 04 +--- + +# md4( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [md4](https://pkg.go.dev/golang.org/x/crypto/md4) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.md4('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.md4(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] 3363b72840acd5f49f922fef598ee85d +INFO[0000] 3363b72840acd5f49f922fef598ee85d +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/md5.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/md5.md new file mode 100644 index 000000000..a42306a55 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/md5.md @@ -0,0 +1,48 @@ +--- +title: 'md5( input, outputEncoding )' +description: 'Use MD5 to hash input data.' +description: 'Use MD5 to hash input data.' +weight: 05 +--- + +# md5( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [md5](https://golang.org/pkg/crypto/md5/) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.md5('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.md5(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] fc3ff98e8c6a0d3087d515c0473f8677 +INFO[0000] fc3ff98e8c6a0d3087d515c0473f8677 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/randombytes.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/randombytes.md new file mode 100644 index 000000000..873ca636a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/randombytes.md @@ -0,0 +1,38 @@ +--- +title: 'randomBytes( int )' +description: 'randomBytes.' +description: 'randomBytes.' +weight: 06 +--- + +# randomBytes( int ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Return an ArrayBuffer object with a number of cryptographically random bytes. It will either return exactly the amount of bytes requested or will throw an exception if something went wrong. + +| Parameter | Type | Description | +| --------- | ------- | --------------------------------------- | +| int | integer | The length of the returned ArrayBuffer. | + +### Returns + +| Type | Description | +| ----------- | --------------------------------------------------- | +| ArrayBuffer | An ArrayBuffer with cryptographically random bytes. | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + const bytes = crypto.randomBytes(42); + const view = new Uint8Array(bytes); + console.log(view); // 156,71,245,191,56,... +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/ripemd160.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/ripemd160.md new file mode 100644 index 000000000..fa7109d99 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/ripemd160.md @@ -0,0 +1,48 @@ +--- +title: 'ripemd160( input, outputEncoding )' +description: 'Use RIPEMD-160 to hash input data.' +description: 'Use RIPEMD-160 to hash input data.' +weight: 07 +--- + +# ripemd160( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [ripemd160](https://pkg.go.dev/golang.org/x/crypto/ripemd160) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.ripemd160('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.ripemd160(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] dffd03137b3a333d5754813399a5f437acd694e5 +INFO[0000] dffd03137b3a333d5754813399a5f437acd694e5 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/sha1.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha1.md new file mode 100644 index 000000000..26c57b54b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha1.md @@ -0,0 +1,48 @@ +--- +title: 'sha1( input, outputEncoding )' +description: 'Use SHA-1 to hash input data.' +description: 'Use SHA-1 to hash input data.' +weight: 08 +--- + +# sha1( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [sha1](https://golang.org/pkg/crypto/sha1/) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.sha1('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.sha1(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] 430ce34d020724ed75a196dfc2ad67c77772d169 +INFO[0000] 430ce34d020724ed75a196dfc2ad67c77772d169 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/sha256.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha256.md new file mode 100644 index 000000000..0dd4f1d63 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha256.md @@ -0,0 +1,48 @@ +--- +title: 'sha256( input, outputEncoding )' +description: 'Use SHA-256 to hash input data.' +description: 'Use SHA-256 to hash input data.' +weight: 09 +--- + +# sha256( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [sha256](https://golang.org/pkg/crypto/sha256/) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.sha256('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.sha256(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 +INFO[0000] 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/sha384.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha384.md new file mode 100644 index 000000000..60d3d0463 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha384.md @@ -0,0 +1,48 @@ +--- +title: 'sha384( input, outputEncoding )' +description: 'Use SHA-384 to hash input data.' +description: 'Use SHA-384 to hash input data.' +weight: 10 +--- + +# sha384( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [sha384](https://golang.org/pkg/crypto/sha512/) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.sha384('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.sha384(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] d33d40f7010ce34aa86efd353630309ed5c3d7ffac66d988825cf699f4803ccdf3f033230612f0945332fb580d8af805 +INFO[0000] d33d40f7010ce34aa86efd353630309ed5c3d7ffac66d988825cf699f4803ccdf3f033230612f0945332fb580d8af805 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512.md new file mode 100644 index 000000000..acb7141d3 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512.md @@ -0,0 +1,48 @@ +--- +title: 'sha512( input, outputEncoding )' +description: 'Use SHA-512 to hash input data.' +description: 'Use SHA-512 to hash input data.' +weight: 11 +--- + +# sha512( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [sha512](https://golang.org/pkg/crypto/sha512/) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.sha512('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.sha512(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] db9b1cd3262dee37756a09b9064973589847caa8e53d31a9d142ea2701b1b28abd97838bb9a27068ba305dc8d04a45a1fcf079de54d607666996b3cc54f6b67c +INFO[0000] db9b1cd3262dee37756a09b9064973589847caa8e53d31a9d142ea2701b1b28abd97838bb9a27068ba305dc8d04a45a1fcf079de54d607666996b3cc54f6b67c +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512_224.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512_224.md new file mode 100644 index 000000000..ac31edd39 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512_224.md @@ -0,0 +1,48 @@ +--- +title: 'sha512_224( input, outputEncoding )' +description: 'Use SHA-512/224 to hash input data.' +description: 'Use SHA-512/224 to hash input data.' +weight: 13 +--- + +# sha512_224( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [sha512_224](https://golang.org/pkg/crypto/sha512/) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.sha512_224('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.sha512_224(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] bc4ed196f7ba1c20f6fb6be1f91edf8293a35b065d6e7d6fd368c890 +INFO[0000] bc4ed196f7ba1c20f6fb6be1f91edf8293a35b065d6e7d6fd368c890 +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512_256.md b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512_256.md new file mode 100644 index 000000000..9e442247b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-crypto/sha512_256.md @@ -0,0 +1,48 @@ +--- +title: 'sha512_256( input, outputEncoding )' +description: 'Use SHA-512/256 to hash input data.' +description: 'Use SHA-512/256 to hash input data.' +weight: 12 +--- + +# sha512_256( input, outputEncoding ) + +{{< docs/shared source="k6" lookup="crypto-module.md" version="" >}} + +Use [sha512_256](https://golang.org/pkg/crypto/sha512/) to hash input data. + +| Parameter | Type | Description | +| -------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to hash. | +| outputEncoding | string | Describes the type of encoding to use for the hash value. Can be "base64", "base64url", "base64rawurl", "hex" or "binary". | + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| string / Array | The hash digest as string (for "base64", "base64url", "base64rawurl", "hex" `outputEncoding`) or raw array of integers (for "binary" `outputEncoding`). | + +### Example + +{{< code >}} + +```javascript +import crypto from 'k6/crypto'; + +export default function () { + let hash = crypto.sha512_256('hello world!', 'hex'); + console.log(hash); + const binArray = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33]; + hash = crypto.sha512_256(new Uint8Array(binArray).buffer, 'hex'); + console.log(hash); +} +``` + +{{< /code >}} + +The above script should result in the following being printed during execution: + +```bash +INFO[0000] 595b5926068b4828fb1c27db21281e31118b8475cb6c3ceeb09be7b685414d5f +INFO[0000] 595b5926068b4828fb1c27db21281e31118b8475cb6c3ceeb09be7b685414d5f +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-data/_index.md b/docs/sources/v0.50.x/javascript-api/k6-data/_index.md new file mode 100644 index 000000000..2e85ea78a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-data/_index.md @@ -0,0 +1,13 @@ +--- +title: 'k6/data' +description: 'k6 data API' +weight: 04 +--- + +# k6/data + +The data module provides helpers to work with data. + +| Class/Method | Description | +| ------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | +| [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) | read-only array like structure that shares memory between VUs | diff --git a/docs/sources/v0.50.x/javascript-api/k6-data/sharedarray.md b/docs/sources/v0.50.x/javascript-api/k6-data/sharedarray.md new file mode 100644 index 000000000..eb7f8bbbf --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-data/sharedarray.md @@ -0,0 +1,138 @@ +--- +title: SharedArray +description: 'SharedArray is an array-like object that shares the underlying memory between VUs.' +weight: 1 +--- + +# SharedArray + +`SharedArray` is an array-like object that shares the underlying memory between VUs. +The function executes only once, and its result is saved in memory once. +When a script requests an element, k6 gives a _copy_ of that element. + +You must construct a `SharedArray` in the [`init` context](https://grafana.com/docs/k6//using-k6/test-lifecycle). +Its constructor takes a name for the `SharedArray` and a function that needs to return an array object itself: + +```javascript +import { SharedArray } from 'k6/data'; + +const data = new SharedArray('some name', function () { + const dataArray = []; + // more operations + return dataArray; // must be an array +}); +``` + +The name argument is required. +VUs are completely separate JS VMs, and k6 needs some way to identify the `SharedArray` that it needs to return. +You can have multiple `SharedArrays` and even load only some of them for given VUs, +though this is unlikely to have any performance benefit. + +Supported operations on a `SharedArray` include: + +- Getting the number of elements with `length` +- Getting an element by its index using the normal syntax `array[index]` +- Using `for-of` loops + +In most cases, you should be able to reduce the memory usage of an array data structure by wrapping it in a `SharedArray`. +Once constructed, a `SharedArray` is read-only, so **you can't use a SharedArray to communicate data between VUs**. + +{{% admonition type="caution" %}} + +Attempting to instantiate a `SharedArray` outside of the [init context](https://grafana.com/docs/k6//using-k6/test-lifecycle) results in the exception `new SharedArray must be called in the init context`. + +This limitation will eventually be removed, but for now, the implication is that you can use `SharedArray` to populate test data only at the very beginning of your test and not as a result of receiving data from a response (for example). + +{{% /admonition %}} + +## Example + +{{< code >}} + +```javascript +import { SharedArray } from 'k6/data'; + +const data = new SharedArray('some name', function () { + // All heavy work (opening and processing big files for example) should be done inside here. + // This way it will happen only once and the result will be shared between all VUs, saving time and memory. + const f = JSON.parse(open('./somefile.json')); + return f; // f must be an array +}); + +export default function () { + const element = data[Math.floor(Math.random() * data.length)]; + // do something with element +} +``` + +{{< /code >}} + +## Performance characteristics + +Internally, the current implementation of `SharedArray` keeps the data marshaled as JSON and unmarshals elements only when they are requested. + +In general, this operation should be unnoticeable (relative to whatever else you do with the data). +But, for small data sets, `SharedArray` might perform worse. +However, this is highly dependent on use case. + +To test this, we ran the following script on version v0.31.0 with 100 VUs. + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; +import { SharedArray } from 'k6/data'; + +const n = parseInt(__ENV.N); +function generateArray() { + const arr = new Array(n); + for (let i = 0; i < n; i++) { + arr[i] = { something: 'something else' + i, password: '12314561' }; + } + return arr; +} + +let data; +if (__ENV.SHARED === 'true') { + data = new SharedArray('my data', generateArray); +} else { + data = generateArray(); +} + +export default function () { + const iterationData = data[Math.floor(Math.random() * data.length)]; + const res = http.post('https://httpbin.test.k6.io/anything', JSON.stringify(iterationData), { + headers: { 'Content-type': 'application/json' }, + }); + check(res, { 'status 200': (r) => r.status === 200 }); +} +``` + +{{< /code >}} + +As the table shows, performance didn't differ much at lower numbers of data lines: +up until around 1000 data lines, `SharedArray` shows little benefit in memory usage +and had a higher upper bound of CPU usage (though not substantially higher). + +At 10k lines and above, the memory savings started to heavily carry over to CPU savings as well. + +| data lines | shared | wall time | CPU % | MEM usage | http requests | +| ---------- | ------ | --------- | -------- | ----------- | ------------- | +| 100 | true | 2:01:70 | 70-79% | 213-217MB | 92191-98837 | +| 100 | false | 2:01:80 | 74-75% | 224-232MB | 96851-98643 | +| 1000 | true | 2:01:60 | 74-79% | 209-216MB | 98251-98806 | +| 1000 | false | 2:01:90 | 75-77% | 333-339MB | 98069-98951 | +| 10000 | true | 2:01:70 | 78-79% | 213-217MB | 97953-98735 | +| 10000 | false | 2:03:00 | 80-83% | 1364-1400MB | 96816-98852 | +| 100000 | true | 2:02:20 | 78-79% | 238-275MB | 98103-98540 | +| 100000 | false | 2:14:00 | 120-124% | 8.3-9.1GB | 96003-97802 | + +In v0.30.0, the difference in CPU usage at lower numbers was around 10-15%, but it also started to even out at around 10k data lines and was a clear winner at 100k. + +The CPU/memory data came from using `/usr/bin/time`. Refer to the [gist with the raw data](https://gist.github.com/MStoykov/1181cfa6f00bc56b90915155f885e2bb). + +These numbers are purely illustrative: the performance can be affected by any additional processing of the element retrieved from the `SharedArray`, or if an output is in use, or it gets multiple elements, etc. +While `SharedArray` has some CPU usage, +it might turn out to be negligible in a given situation with just 10 elements, or more problematic than the memory usage for a 100k elements. +So, if in doubt, you should probably run some benchmarks and decide which tradeoffs are more important for your use case. diff --git a/docs/sources/v0.50.x/javascript-api/k6-encoding/_index.md b/docs/sources/v0.50.x/javascript-api/k6-encoding/_index.md new file mode 100644 index 000000000..1673ec7bb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-encoding/_index.md @@ -0,0 +1,15 @@ +--- +title: 'k6/encoding' +description: 'The encoding module provides base64 encoding/decoding as defined by RFC4648.' +weight: 05 +--- + +# k6/encoding + +The encoding module provides [base64](https://en.wikipedia.org/wiki/Base64) +encoding/decoding as defined by [RFC4648](https://tools.ietf.org/html/rfc4648). + +| Function | Description | +| ------------------------------------------------------------------------------------------------------------------------ | ----------------------- | +| [b64decode(input, [encoding], [format])](https://grafana.com/docs/k6//javascript-api/k6-encoding/b64decode/) | Base64 decode a string. | +| [b64encode(input, [encoding])](https://grafana.com/docs/k6//javascript-api/k6-encoding/b64encode/) | Base64 encode a string. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-encoding/b64decode--input---encoding---format.md b/docs/sources/v0.50.x/javascript-api/k6-encoding/b64decode--input---encoding---format.md new file mode 100644 index 000000000..543f6e863 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-encoding/b64decode--input---encoding---format.md @@ -0,0 +1,50 @@ +--- +title: 'b64decode( input, [encoding], [format] )' +description: 'Base64 decode a string.' +description: 'Base64 decode a string.' +slug: 'b64decode' +--- + +# b64decode( input, [encoding], [format] ) + +Decode the passed base64 encoded `input` string into the unencoded original input in either binary or string formats. + +| Parameter | Type | Description | +| ------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| input | string | The string to base64 decode. | +| encoding (optional) | string | The base64 encoding to use.
Available options are:
- **"std"**: the standard encoding with `=` padding chars and `+` and `/` characters in encoding alphabet. This is the default.
- **"rawstd"**: like `std` but without `=` padding characters.
- **"url"**: URL safe version of `std`, encoding alphabet doesn't contain `+` and `/` characters, but rather `-` and `_` characters.
- **"rawurl"**: like `url` but without `=` padding characters. | +| format (optional) | string | If `"s"` return the data as a string, otherwise if unspecified an ArrayBuffer object is returned. | + +### Returns + +| Type | Description | +| -------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| ArrayBuffer / string | The base64 decoded version of the `input` string in either string or ArrayBuffer format, depending on the `format` parameter. | + +### Example + +{{< code >}} + +```javascript +import { check } from 'k6'; +import encoding from 'k6/encoding'; + +export default function () { + const str = 'hello world'; + const enc = 'aGVsbG8gd29ybGQ='; + const expBin = new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]); + check(null, { + 'is decoding to string correct': () => encoding.b64decode(enc, 'std', 's') === str, + 'is decoding to ArrayBuffer correct': () => { + const decBin = new Uint8Array(encoding.b64decode(enc)); + if (decBin.length != expBin.length) return false; + for (let i = 0; i < decBin.length; i++) { + if (decBin[i] !== expBin[i]) return false; + } + return true; + }, + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-encoding/b64encode--input---encoding.md b/docs/sources/v0.50.x/javascript-api/k6-encoding/b64encode--input---encoding.md new file mode 100644 index 000000000..affde5436 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-encoding/b64encode--input---encoding.md @@ -0,0 +1,40 @@ +--- +title: 'b64encode( input, [encoding] )' +description: 'Encode data in base64.' +description: 'Encode data in base64.' +slug: 'b64encode' +--- + +# b64encode( input, [encoding] ) + +| Parameter | Type | Description | +| ------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| input | string / ArrayBuffer | The input string or `ArrayBuffer` object to base64 encode. | +| encoding (optional) | string | The base64 encoding to use.
Available options are:
- **"std"**: the standard encoding with `=` padding chars and `+` and `/` characters in encoding alphabet. This is the default.
- **"rawstd"**: like `std` but without `=` padding characters.
- **"url"**: URL safe version of `std`, encoding alphabet doesn't contain `+` and `/` characters, but rather `-` and `_` characters.
- **"rawurl"**: like `url` but without `=` padding characters. | + +### Returns + +| Type | Description | +| ------ | ---------------------------------------- | +| string | The base64 encoding of the `input` data. | + +### Example + +{{< code >}} + +```javascript +import { check } from 'k6'; +import encoding from 'k6/encoding'; + +export default function () { + const str = 'hello world'; + const enc = 'aGVsbG8gd29ybGQ='; + const buf = new Uint8Array([104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]).buffer; + check(null, { + 'is encoding string correct': () => encoding.b64encode(str) === enc, + 'is encoding ArrayBuffer correct': () => encoding.b64encode(buf) === enc, + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-execution.md b/docs/sources/v0.50.x/javascript-api/k6-execution.md new file mode 100644 index 000000000..b0565ce7c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-execution.md @@ -0,0 +1,262 @@ +--- +title: 'k6/execution' +description: "Get information about the current test's execution state." +weight: 06 +--- + +# k6/execution + +`k6/execution` provides the capability to get information about the current test execution state inside the test script. You can read in your script the execution state during the test execution and change your script logic based on the current state. + +The `k6/execution` module provides the test execution information with the following properties: + +- [instance](#instance) +- [scenario](#scenario) +- [test](#test) +- [vu](#vu) + +
+ +```javascript +import exec from 'k6/execution'; + +export const options = { + scenarios: { + myscenario: { + // this will be the returned name + executor: 'shared-iterations', + maxDuration: '30m', + }, + }, +}; + +export default function () { + console.log(exec.scenario.name); // myscenario +} +``` + +
+ +> ℹ️ **Identifiers** +> +> All unique identifiers are sequentially generated starting from a base of zero (iterations) or one (VU IDs). In distributed/cloud test runs, the test-wide iteration numbers and VU identifiers are still going to be unique across instances, though there might be gaps in the sequences when, for example, some instances execute faster iterations than others or allocate more VUs mid-test. + +### instance + +The instance object provides information associated with the load generator instance. You can think of it as the current running k6 process, which will likely be a single process if you are running k6 on your local machine. When running a cloud/distributed test with multiple load generator instances, the values of the following properties can differ across instances. + +| Property | Type | Description | +| ---------------------- | ------- | ------------------------------------------------------------------------- | +| iterationsInterrupted | integer | The number of prematurely interrupted iterations in the current instance. | +| iterationsCompleted | integer | The number of completed iterations in the current instance. | +| vusActive | integer | The number of active VUs. | +| vusInitialized | integer | The number of currently initialized VUs. | +| currentTestRunDuration | float | The time passed from the start of the current test run in milliseconds. | + +### scenario + +Meta information and execution details about the current running [scenario](https://grafana.com/docs/k6//using-k6/scenarios). + +| Property | Type | Description | +| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| name | string | The assigned name of the running scenario. | +| executor | string | The name of the running [Executor](https://grafana.com/docs/k6//using-k6/scenarios#executors) type. | +| startTime | integer | The Unix timestamp in milliseconds when the scenario started. | +| progress | float | Percentage in a 0 to 1 interval of the scenario progress. | +| iterationInInstance | integer | The unique and zero-based sequential number of the current iteration in the scenario, across the current instance. | +| iterationInTest | integer | The unique and zero-based sequential number of the current iteration in the scenario. It is unique in all k6 execution modes - in local, cloud and distributed/segmented test runs. However, while every instance will get non-overlapping index values in cloud/distributed tests, they might iterate over them at different speeds, so the values won't be sequential across them. | + +### test + +Control the test execution. + +| Property | Type | Description | +| --------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| abort([String]) | function | It aborts the test run with the exit code `108`, and an optional string parameter can provide an error message. Aborting the test will not prevent the `teardown()` execution. | +| options | Object | It returns an object with all the test options as properties. The options' values are consolidated following the [order of precedence](https://grafana.com/docs/k6//using-k6/k6-options/how-to#order-of-precedence) and derived if shortcuts have been used. It returns `null` for properties where the relative option hasn't been defined. | + +### vu + +Meta information and execution details about the current vu. + +| Property | Type | Description | +| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| iterationInInstance | integer | The identifier of the iteration in the current instance for this VU. This is only unique for current VU and this instance (if multiple instances). This keeps being aggregated if a given VU is reused between multiple scenarios. | +| iterationInScenario | integer | The identifier of the iteration in the current scenario for this VU. This is only unique for current VU and scenario it is currently executing. | +| idInInstance | integer | The identifier of the VU across the instance. Not unique across multiple instances. | +| idInTest | integer | The globally unique (across the whole test run) identifier of the VU. | +| metrics.tags | object | The map that gives control over [VU's Tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#tags). The Tags will be included in every metric emitted by the VU and the Tags' state is maintained across different iterations of the same Scenario while the VU exists. Check how to use it in the [example](#tags) below. | +| metrics.metadata | object | The map that gives control over VU's Metadata. The Metadata will be included in every metric emitted by the VU and the Metadata's state is maintained across different iterations of the same Scenario while the VU exists. Check how to use it in the [example](#metadata) below. | + +{{< collapse title="Setting vu.metrics.tags" >}} + +Setting a Tag with the same key as a [system tag](https://grafana.com/docs/k6//using-k6/k6-options/reference#system-tags) is allowed, but it requires attention to avoid unexpected results. Overwriting system tags will not throw an error, but in most cases will not actually change the value of the emitted metrics as expected. For example, trying to set the `url` tag value will not result in a changed tag value when `http.get()` is called, since the tag value is determined by the HTTP request itself. However, it will add the tag `url` to the metric samples emitted by a `check()` or `metric.add()`, which is probably not the desired behavior. On the other hand, setting the `name` tag will work as expected, since that was already supported for `http.*` methods, for the purposes of the [URL Grouping](https://grafana.com/docs/k6//using-k6/http-requests#url-grouping) feature. + +Not all the types are accepted as a tag value: k6 supports strings, numbers and boolean types. Under the hood, the `tags` object handles a Tag as a `String` key-value pair, so all the types will be implicitly converted into a string. If one of the denied types is used (e.g. Object or Array) and the [`throw` option](https://grafana.com/docs/k6//using-k6/k6-options/reference#throw) is set, an exception will be thrown. Otherwise, only a warning is printed and the tag value will be discarded. + +{{< /collapse >}} + +## Examples and use cases + +### Getting unique data once + +This is a common use case for data parameterization, you can read the [examples](https://grafana.com/docs/k6//examples/data-parameterization#retrieving-unique-data) using `scenario.iterationInTest` and `vu.idInTest`. + +### Timing operations + +The `startTime` property from the `scenario` object can be used to time operations. + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +export default function () { + // do some long operations + // ... + console.log(`step1: scenario ran for ${new Date() - new Date(exec.scenario.startTime)}ms`); + + // some more long operations + //... + console.log(`step2: scenario ran for ${new Date() - new Date(exec.scenario.startTime)}ms`); +} +``` + +{{< /code >}} + +### Script naming + +The `name` property can be used for executing the logic based on which script is currently running. + +> **Tip**: +> If you need to run [multiple scenarios](https://grafana.com/docs/k6//using-k6/scenarios/advanced-examples#using-multiple-scenarios) in your script you can use `exec` option achieve a similar goal + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +export const options = { + scenarios: { + 'the-first': { + // ... + }, + 'the-second': { + // ... + }, + }, +}; + +export default function () { + if (exec.scenario.name === 'the-first') { + // do some logic during this scenario + } else { + // do some other logic in the others + } +} +``` + +{{< /code >}} + +### Test Abort + +Aborting is possible during initialization: + +{{< code >}} + +```javascript +import exec from 'k6/execution'; +exec.test.abort(); +``` + +{{< /code >}} + +As well as inside the `default` function: + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +export default function () { + // Note that you can abort with a specific message too + exec.test.abort('this is the reason'); +} + +export function teardown() { + console.log('teardown will still be called after test.abort()'); +} +``` + +{{< /code >}} + +### Get test options + +Get the consolidated and derived options' values + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +export const options = { + stages: [ + { duration: '5s', target: 100 }, + { duration: '5s', target: 50 }, + ], +}; + +export default function () { + console.log(exec.test.options.paused); // null + console.log(exec.test.options.scenarios.default.stages[0].target); // 100 +} +``` + +{{< /code >}} + +### Tags + +The `vu.metrics.tags` property can be used for getting or setting [VU's tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#tags). + +{{< code >}} + +```javascript +import http from 'k6/http'; +import exec from 'k6/execution'; + +export default function () { + exec.vu.metrics.tags['mytag'] = 'value1'; + exec.vu.metrics.tags['mytag2'] = 2; + + // the metrics these HTTP requests emit will get tagged with `mytag` and `mytag2`: + http.batch(['https://test.k6.io', 'https://test-api.k6.io']); +} +``` + +{{< /code >}} + +`vu.tags` (without `metrics`) can also be used, but is deprecated for the more context-specific variant. + +### Metadata + +The `vu.metrics.metadata` property can be used for getting or setting VU's metadata. It is similar to `tags`, but can be used for high cardinality data. It also can not be used in thresholds and will likely be handled differently by each output. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import exec from 'k6/execution'; + +export default function () { + exec.vu.metrics.metadata['trace_id'] = 'somecoolide'; + + // the metrics these HTTP requests emit will get the metadata `trace_id`: + http.batch(['https://test.k6.io', 'https://test-api.k6.io']); + + delete exec.vu.metrics.metadata['trace_id']; // this will unset it + // which will make the metrics these requests to not have the metadata `trace_id` set on them. + http.batch(['https://test.k6.io', 'https://test-api.k6.io']); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/_index.md new file mode 100644 index 000000000..500cd3b6b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/_index.md @@ -0,0 +1,20 @@ +--- +title: 'k6/experimental' +description: 'k6 experimental APIs' +weight: 07 +--- + +# k6/experimental + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +| Modules | Description | +| ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | +| [browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser) | Provides browser-level APIs to interact with browsers and collect frontend performance metrics as part of your k6 tests. | +| [fs](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs) | Provides a memory-efficient way to handle file interactions within your test scripts. | +| [grpc](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc) | Extends `k6/net/grpc` with the streaming capabilities. | +| [redis](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis) | Functionality to interact with [Redis](https://redis.io/). | +| [timers](https://grafana.com/docs/k6//javascript-api/k6-experimental/timers) | `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` | +| [tracing](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing) | Support for instrumenting HTTP requests with tracing information. | +| [webcrypto](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto) | Implements the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). | +| [websockets](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets) | Implements the browser's [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/_index.md new file mode 100644 index 000000000..cde7763fb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/_index.md @@ -0,0 +1,264 @@ +--- +title: 'browser' +description: 'An overview of the browser-level APIs from browser module.' +weight: 01 +--- + +# browser + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +The browser module APIs aim for rough compatibility with the [Playwright API for NodeJS](https://playwright.dev/docs/api/class-playwright). + +Note that because k6 does not run in NodeJS, the browser module APIs will slightly differ from their Playwright counterparts. + +You can find examples of using [the browser module API](#browser-module-api) in our [getting started guide](https://grafana.com/docs/k6//using-k6-browser). + +{{% admonition type="note" %}} + +To work with the browser module, make sure you are using the latest [k6 version](https://github.com/grafana/k6/releases). + +{{% /admonition %}} + +## Properties + +The table below lists the properties you can import from the browser module (`'k6/experimental/browser'`). + +| Property | Description | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| browser | The browser module API is the entry point for all your tests. See the [example](#example) and the [API](#browser-module-api) below. | +| devices | Returns predefined emulation settings for many end-user devices that can be used to simulate browser behavior on a mobile device. See the [devices example](#devices-example) below. | + +## Browser Module API + +The browser module is the entry point for all your tests, and it is what interacts with the actual web browser via [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) (CDP). It manages: + +- [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) which is where you can set a variety of attributes to control the behavior of pages; +- and [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) which is where your rendered site is displayed. + +| Method | Description | +| ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [browser.context()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/context) | Returns the current [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). | +| [browser.closeContext()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/closecontext) | Closes the current [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). | +| [browser.isConnected](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/isconnected) | Indicates whether the [CDP](https://chromedevtools.github.io/devtools-protocol/) connection to the browser process is active or not. | +| [browser.newContext([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/newcontext/) | Creates and returns a new [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). | +| [browser.newPage([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/newpage) | Creates a new [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) in a new [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) and returns the page. Pages that have been opened ought to be closed using [`Page.close`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/close). Pages left open could potentially distort the results of Web Vital metrics. | +| [browser.version()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/version) | Returns the browser application's version. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/'); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +Then, you can run the test with this command. Also, see the [browser module options](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-options) for customizing the browser module's behavior using environment variables. + +{{< code >}} + +```bash +$ k6 run script.js +``` + +```docker +# WARNING! +# The grafana/k6:master-with-browser image launches a Chrome browser by setting the +# 'no-sandbox' argument. Only use it with trustworthy websites. +# +# As an alternative, you can use a Docker SECCOMP profile instead, and overwrite the +# Chrome arguments to not use 'no-sandbox' such as: +# docker container run --rm -i -e K6_BROWSER_ARGS='' --security-opt seccomp=$(pwd)/chrome.json grafana/k6:master-with-browser run - k6 run script.js +``` + +```windows-powershell +PS C:\k6> k6 run script.js +``` + +{{< /code >}} + +## Browser-level APIs + +| k6 Class | Description | +| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) | Enables independent browser sessions with separate [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page)s, cache, and cookies. | +| [ElementHandle](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/elementhandle) | Represents an in-page DOM element. | +| [Frame](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame) | Access and interact with the [`Page`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page).'s `Frame`s. | +| [JSHandle](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/jshandle) | Represents an in-page JavaScript object. | +| [Keyboard](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/keyboard) | Used to simulate the keyboard interactions with the associated [`Page`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page). | +| [Locator](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator) | The Locator API makes it easier to work with dynamically changing elements. | +| [Mouse](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/mouse) | Used to simulate the mouse interactions with the associated [`Page`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page). | +| [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) | Provides methods to interact with a single tab in a browser. | +| [Request](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/request) | Used to keep track of the request the [`Page`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) makes. | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/response) | Represents the response received by the [`Page`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page). | +| [Touchscreen](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/touchscreen) | Used to simulate touch interactions with the associated [`Page`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page). | +| [Worker](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/worker) | Represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API). | + +## Browser Module Options + +You can customize the behavior of the browser module by providing browser options as environment variables. + +| Environment Variable | Description | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| K6_BROWSER_ARGS | Extra command line arguments to include when launching browser process. See [this link](https://peter.sh/experiments/chromium-command-line-switches/) for a list of Chromium arguments. Note that arguments should not start with `--` (see the [example](#example)). | +| K6_BROWSER_DEBUG | All CDP messages and internal fine grained logs will be logged if set to `true`. | +| K6_BROWSER_EXECUTABLE_PATH | Override search for browser executable in favor of specified absolute path. | +| K6_BROWSER_HEADLESS | Show browser GUI or not. `true` by default. | +| K6_BROWSER_IGNORE_DEFAULT_ARGS | Ignore any of the [default arguments](#default-arguments) included when launching a browser process. | +| K6_BROWSER_TIMEOUT | Default timeout to use for various actions and navigation. `'30s'` if not set. | +| K6_BROWSER_TRACES_METADATA | Sets additional _key-value_ metadata that is included as attributes in every span generated from browser module traces. Example: `K6_BROWSER_TRACES_METADATA=attr1=val1,attr2=val2`. This only applies if traces generation is enabled, refer to [Traces output](https://grafana.com/docs/k6//using-k6/k6-options/reference#traces-output) for more details. | + +The following command passes the [browser module options](#browser-module-options) as environment variables to launch a headful browser with custom arguments. + +{{< code >}} + +```bash +$ K6_BROWSER_HEADLESS=false K6_BROWSER_ARGS='show-property-changed-rects' k6 run script.js +``` + +```docker +# WARNING! +# The grafana/k6:master-with-browser image launches a Chrome browser by setting the +# 'no-sandbox' argument. Only use it with trustworthy websites. +# +# As an alternative, you can use a Docker SECCOMP profile instead, and overwrite the +# Chrome arguments to not use 'no-sandbox' such as: +# docker container run --rm -i -e K6_BROWSER_ARGS='' --security-opt seccomp=$(pwd)/chrome.json grafana/k6:master-with-browser run - set "K6_BROWSER_HEADLESS=false" && set "K6_BROWSER_ARGS='show-property-changed-rects' " && k6 run script.js +``` + +```windows-powershell +PS C:\k6> $env:K6_BROWSER_HEADLESS="false" ; $env:K6_BROWSER_ARGS='show-property-changed-rects' ; k6 run script.js +``` + +{{< /code >}} + +## Default arguments + +List of default arguments included when launching the browser process. You can pass one or more of the arguments to the `K6_BROWSER_IGNORE_DEFAULT_ARGS` environment variable when starting a test for the ones you want to ignore. + +{{% admonition type="note" %}} + +The starting '--' have been omitted from the argument names in these lists. + +{{% /admonition %}} + +| Argument | Value | Description | +| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| disable-background-networking | `true` | Disables several subsystems which run network requests in the background. This is used during network performance testing to avoid measurement noise. | +| enable-features | NetworkService,
NetworkServiceInProcess | Comma-separated list of feature names to enable. | +| disable-background-timer-throttling | `true` | Disables task throttling of timer tasks from background pages. | +| disable-backgrounding-occluded-windows | `true` | Disables backgrounding renders for occluded windows. Done for tests to avoid nondeterministic behavior. | +| disable-breakpad | `true` | Disables the crash reporting. | +| disable-component-extensions
-with-background-pages | `true` | Disables default component extensions with background pages. Useful for performance tests where these pages may interfere with results. | +| disable-default-apps | `true` | Disables the installation of default apps on the first run. This is used during automated testing. | +| disable-dev-shm-usage | `true` | The /dev/shm partition is too small in certain VM environments, causing Chrome to fail or crash. This flag provides a work-around for this issue (a temporary directory will always be used to create anonymous shared memory files). | +| disable-extensions | `true` | Disables extensions. | +| disable-features | ImprovedCookieControls,
LazyFrameLoading,
GlobalMediaControls,
DestroyProfileOnBrowserClose,
MediaRouter,
AcceptCHFrame | Comma-separated list of feature names to disable. | +| disable-hang-monitor | `true` | Suppresses hang monitor dialogs in renderer processes. This may allow slow unload handlers on a page to prevent the tab from closing, but the Task Manager can be used to terminate the offending process in this case. | +| disable-ipc-flooding-protection | `true` | Disables the IPC flooding protection. It is activated by default. Some javascript functions can be used to flood the browser process with IPC. This protection limits the rate at which they can be used. | +| disable-popup-blocking | `true` | Disables pop-up blocking. | +| disable-prompt-on-repost | `true` | Usually, when the user attempts to navigate to a page that was the result of a post request, the browser prompts to make sure that's the intention of the user. This switch may be used to disable that check during automated testing. | +| disable-renderer-backgrounding | `true` | Prevents renderer process backgrounding when set. | +| force-color-profile | `srgb` | Forces all monitors to be treated as though they have the specified color profile. Accepted values are "srgb" and "generic-rgb" (currently used by Mac layout tests) and "color-spin-gamma24" (used by layout tests). | +| metrics-recording-only | `true` | Enables the recording of metrics reports but disables reporting. This executes all the code that a normal client would use for reporting, except the report is dropped rather than sent to the server. This is useful for finding issues in the metrics code during UI and performance tests. | +| no-first-run | `true` | Skips the "First Run" tasks, whether or not it's the first run, and the "What's New" page. This does not drop the "First Run" sentinel and thus doesn't prevent "First Run" from occurring the next time Chromium is launched without this flag. It also does not update the last "What's New" milestone, so it does not prevent "What's New" from occurring the next time Chromium is launched without this flag. | +| enable-automation | `true` | Enables indication that the browser is controlled by automation. | +| password-store | `basic` | Specifies which encryption storage backend to use. The possible values are kwallet, kwallet5, gnome, gnome-keyring, gnome-libsecret, and basic. Any other value will lead to Chromium detecting the best backend automatically. | +| use-mock-keychain | `true` | Uses mock keychain on Mac to prevent the blocking permissions dialog about: "Chrome wants to use your confidential information stored in your keychain." | +| no-service-autorun | `true` | Disables the service process from adding itself as an autorun process. This does not delete existing autorun registrations, it just prevents the service from registering a new one. | +| no-startup-window | `true` | Does not automatically open a browser window on startup (used when launching Chrome for the purpose of hosting background apps). | +| no-default-browser-check | `true` | Disables the default browser check. Useful for UI/browser tests where we want to avoid having the default browser info-bar displayed. | +| headless | `true`/`false` | Run in headless mode, i.e., without a UI or display server dependencies. Set by `K6_BROWSER_HEADLESS` environment variable (default true). | +| window-size | `800,600` | Sets the initial window size. Provided as string in the format "800,600". | + +Additionally if headless mode is set to `true` in [browser options](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser_options), the following arguments are also set: + +| Argument | Value | Description | +| --------------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| hide-scrollbars | `true` | Prevents creating scrollbars for web content. Useful for taking consistent screenshots. | +| mute-audio | `true` | Mutes audio sent to the audio device so it is not audible during automated testing. | +| blink-settings | primaryHoverType=2,availableHoverTypes=2,
primaryPointerType=4,availablePointerTypes=4 | Sets blink settings. Format is [=],[=],... The names are declared in [settings.json5](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/settings.json5) from chromium project. For boolean type, use "true", "false", or omit '=' part to set to true. For enum type, use the int value of the enum value. | + +## Devices Example + +To emulate the browser behaviour on a mobile device and approximately measure the browser performance, you can import `devices` from `k6/experimental/browser`. + +{{< code >}} + +```javascript +import { browser, devices } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const iphoneX = devices['iPhone X']; + const context = browser.newContext(iphoneX); + const page = context.newPage(); + + try { + await page.goto('https://test.k6.io/'); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/_index.md new file mode 100644 index 000000000..414b6d356 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/_index.md @@ -0,0 +1,31 @@ +--- +title: "BrowserContext" +description: "Browser module: BrowserContext Class" +weight: 02 +weight: 02 +--- + +# BrowserContext + +`BrowserContext`s provide a way to operate multiple independent sessions, with separate pages, cache, and cookies. A default `BrowserContext` is created when a browser is launched. + +The [browser module API](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-api) is used to create a new `BrowserContext`. + +If a [page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) opens another page, e.g. with a `window.open` call, the popup will belong to the parent page's `BrowserContext`. + +| Method | Description | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [BrowserContext.addCookies()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/addcookies) | Adds [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) into the `BrowserContext`. | +| [BrowserContext.addInitScript()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/addinitscript) | Adds a script that will be evaluated on [page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) creation, [frame](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame) attached or a navigation occurs. | +| [BrowserContext.clearCookies()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/clearcookies) | Clear the `BrowserContext`'s [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie). | +| [BrowserContext.clearPermissions()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/clearpermissions) | Clears all permission overrides for the `BrowserContext`. | +| [BrowserContext.cookies()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookies) | Returns a list of [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) from the `BrowserContext`. | +| [BrowserContext.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/close) | Close the `BrowserContext` and all its [page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page)s. | +| [BrowserContext.grantPermissions(permissions[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/grantpermissions) | Grants specified permissions to the `BrowserContext`. | +| [BrowserContext.newPage()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/newpage) | Uses the `BrowserContext` to create a new [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) and returns it. | +| [BrowserContext.pages()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/pages) | Returns a list of [page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page)s that belongs to the `BrowserContext`. | +| [BrowserContext.setDefaultNavigationTimeout(timeout)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/setdefaultnavigationtimeout) | Sets the default navigation timeout in milliseconds. | +| [BrowserContext.setDefaultTimeout(timeout)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/setdefaulttimeout) | Sets the default maximum timeout for all methods accepting a timeout option in milliseconds. | +| [BrowserContext.setGeolocation(geolocation)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/setgeolocation) | Sets the `BrowserContext`'s geolocation. | +| [BrowserContext.setOffline(offline)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/setoffline) | Toggles the `BrowserContext`'s connectivity on/off. | +| [BrowserContext.waitForEvent(event[, optionsOrPredicate])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/waitforevent) | Waits for the event to fire and passes its value into the predicate function. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/addcookies.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/addcookies.md new file mode 100644 index 000000000..9b14138b6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/addcookies.md @@ -0,0 +1,89 @@ +--- +title: 'addCookies()' +description: 'Clears context cookies.' +--- + +# addCookies() + +Adds a list of [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) into the [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie). All pages within this [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) will have these [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) set. + +{{% admonition type="note" %}} + +If a [cookie](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie)'s `url` property is not provided, both `domain` and `path` properties must be specified. + +{{% /admonition %}} + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + try { + const unixTimeSinceEpoch = Math.round(new Date() / 1000); + const day = 60 * 60 * 24; + const dayAfter = unixTimeSinceEpoch + day; + const dayBefore = unixTimeSinceEpoch - day; + + context.addCookies([ + // this cookie expires at the end of the session + { + name: 'testcookie', + value: '1', + sameSite: 'Strict', + domain: 'httpbin.org', + path: '/', + httpOnly: true, + secure: true, + }, + // this cookie expires in a day + { + name: 'testcookie2', + value: '2', + sameSite: 'Lax', + domain: 'httpbin.org', + path: '/', + expires: dayAfter, + }, + // this cookie expires in the past, so it will be removed. + { + name: 'testcookie3', + value: '3', + sameSite: 'Lax', + domain: 'httpbin.org', + path: '/', + expires: dayBefore, + }, + ]); + + const response = await page.goto('https://httpbin.org/cookies', { + waitUntil: 'networkidle', + }); + console.log(response.json()); + // prints: + // {"cookies":{"testcookie":"1","testcookie2":"2"}} + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/addinitscript.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/addinitscript.md new file mode 100644 index 000000000..01bea0c36 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/addinitscript.md @@ -0,0 +1,68 @@ +--- +title: 'addInitScript()' +description: 'Adds an init script.' +--- + +# addInitScript() + +Adds a script which will be evaluated in one of the following scenarios: + +- Whenever a [page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/) is created in the [browserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) or is navigated. +- Whenever a child [frame](<[page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame/)>) is attached or navigated in any [page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/) in the [browserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). In this case, the script is evaluated in the context of the newly attached [frame](<[page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame/)>). + +The script is evaluated after the document is created but before any of its scripts are run. This is useful to amend the JavaScript environment, for example, to override `Math.random`. + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import { check } from 'k6'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const browserContext = browser.newContext(); + + // This will execute and override the existing implementation of Math.random. + browserContext.addInitScript('Math.random = function(){return 0}'); + + const page = browserContext.newPage(); + + // In this example we are going to set the content manually, so we first + // navigate to about:blank which will execute the init script, before setting + // the content on the page. + await page.goto('about:blank'); + + page.setContent(` + + + +
waiting...
+ + + `); + + check(page, { + zero: (p) => p.locator('#random').textContent() == '0', + }); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/clearcookies.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/clearcookies.md new file mode 100644 index 000000000..261e498c7 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/clearcookies.md @@ -0,0 +1,42 @@ +--- +title: 'clearCookies()' +description: 'Clears context cookies.' +--- + +# clearCookies() + +Clears the `BrowserContext`'s cookies. + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + await page.goto('https://httpbin.org/cookies/set?testcookie=testcookievalue'); + console.log(context.cookies().length); // prints: 1 + + context.clearCookies(); + console.log(context.cookies().length); // prints: 0 +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/clearpermissions.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/clearpermissions.md new file mode 100644 index 000000000..d3ce620aa --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/clearpermissions.md @@ -0,0 +1,38 @@ +--- +title: 'clearPermissions()' +description: 'Clears all permission overrides for the BrowserContext.' +--- + +# clearPermissions() + +Clears all permission overrides for the `BrowserContext`. + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const context = browser.newContext(); + context.grantPermissions(['clipboard-read']); + // do stuff ... + context.clearPermissions(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/close.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/close.md new file mode 100644 index 000000000..b2c0e1a59 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/close.md @@ -0,0 +1,38 @@ +--- +title: 'close()' +description: 'Close the BrowserContext and all its pages.' +--- + +# close() + +Close the `BrowserContext` and all its [page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/)s. The `BrowserContext` is unusable after this call and a new one must be created. This is typically called to cleanup before ending the test. + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const context = browser.newContext(); + context.newPage(); + + context.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/cookie.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/cookie.md new file mode 100644 index 000000000..1fe9ae4a8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/cookie.md @@ -0,0 +1,22 @@ +--- +title: 'Cookie' +description: 'Browser module: Cookie Class' +--- + +# Cookie + +Cookie class represents a cookie in the [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). + +See the [HTTP Cookies documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) on the Mozilla website for more details about cookies. + +| Property | Type | Default | Description | +| -------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | `""` | The cookie's name. Required. | +| value | string | `""` | The cookie's value. Required. | +| domain | string | `""` | The cookie's domain. | +| path | string | `'/'` | The cookie's path. | +| url | string | `""` | The cookie's URL. | +| expires | number | `-1` | The cookie's expiration date as the number of seconds since the UNIX epoch. `-1` means a session cookie. | +| httpOnly | bool | `false` | A cookie is inaccessible to the JavaScript [document.cookie](https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie) API when this property is `true`. | +| secure | bool | `false` | The cookie's secure flag. | +| sameSite | string | `'Lax'` | The cookie's same site flag. It can be one of `'Strict'`, `'Lax'`, and `'None'`. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/cookies.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/cookies.md new file mode 100644 index 000000000..7275651d8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/cookies.md @@ -0,0 +1,82 @@ +--- +title: 'cookies([urls])' +description: 'Retrieves context cookies.' +--- + +# cookies([urls]) + +Returns a list of [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) from the [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) filtered by the provided `urls`. If no `urls` are provided, all cookies are returned. + +| Parameter | Type | Description | +| --------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| urls | array | A string array of URLs to filter the [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) in the [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). | + +### Returns + +| Type | Description | +| ----- | --------------------------------------------------------------------------------------------------------------------------- | +| array | A list of [cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie). | + +{{% admonition type="note" %}} + +[Cookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/cookie) can be added with [BrowserContext.addCookies](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/addcookies/). + +{{% /admonition %}} + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + try { + // get cookies from the browser context + let cookies = context.cookies(); + console.log('initial cookies length:', cookies.length); // prints 0 + + // let's add more cookies to filter by urls + context.addCookies([ + { name: 'foo', value: 'foovalue', sameSite: 'Strict', url: 'http://foo.com' }, + { name: 'bar', value: 'barvalue', sameSite: 'Lax', url: 'https://bar.com' }, + { name: 'baz', value: 'bazvalue', sameSite: 'Lax', url: 'https://baz.com' }, + ]); + + // get all cookies + cookies = context.cookies(); + console.log('filtered cookies length:', cookies.length); // prints 3 + + // get cookies filtered by urls + cookies = context.cookies('http://foo.com', 'https://baz.com'); + console.log('filtered cookies length:', cookies.length); // prints 2 + + // the first filtered cookie + console.log("1st cookie's name :", cookies[0].name); // prints foo + console.log("1st cookie's value:", cookies[0].value); // prints foovalue + // the first filtered cookie + console.log("2nd cookie's name :", cookies[1].name); // prints baz + console.log("2nd cookie's value:", cookies[1].value); // prints bazvalue + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/grantpermissions.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/grantpermissions.md new file mode 100644 index 000000000..b98267bc3 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/grantpermissions.md @@ -0,0 +1,48 @@ +--- +title: 'grantPermissions(permissions[, options])' +description: 'Grants specified permissions to the BrowserContext.' +--- + +# grantPermissions(permissions[, options]) + +Grants specified permissions to the `BrowserContext`. Only grants corresponding permissions to the given origin if specified. + + + +| Parameter | Type | Description | +| -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| permissions | array | A string array of permissions to grant. A permission can be one of the following values: `'geolocation'`, `'midi'`, `'midi-sysex'` (system-exclusive midi), `'notifications'`, `'camera'`, `'microphone'`, `'background-sync'`, `'ambient-light-sensor'`, `'accelerometer'`, `'gyroscope'`, `'magnetometer'`, `'accessibility-events'`, `'clipboard-read'`, `'clipboard-write'`, `'payment-handler'`. | +| options | object | Optional. | +| options.origin | string | The [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) to grant permissions to, e.g. `'https://example.com'`. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const context = browser.newContext(); + context.grantPermissions(['clipboard-read', 'clipboard-write'], { + origin: 'https://example.com/', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/newpage.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/newpage.md new file mode 100644 index 000000000..6a99a1b61 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/newpage.md @@ -0,0 +1,35 @@ +--- +title: 'newPage()' +description: 'Creates a new page inside this BrowserContext.' +--- + +# newPage() + +Uses the `BrowserContext` to create a new [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/) and returns it. + +### Returns + +| Type | Description | +| ------ | ----------------------------------------------------------------------------------------------------------- | +| object | A new [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/) object. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + try { + await page.goto('https://test.k6.io/browser.php'); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/pages.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/pages.md new file mode 100644 index 000000000..b3c3f99c1 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/pages.md @@ -0,0 +1,52 @@ +--- +title: 'pages()' +description: 'Returns a list of pages inside this BrowserContext.' +--- + +# pages() + +{{% admonition type="caution" %}} + +This feature has **known issues**. For details, refer to +[#444](https://github.com/grafana/xk6-browser/issues/444). + +{{% /admonition %}} + +Returns all open [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/)s in the `BrowserContext`. + +### Returns + +| Type | Description | +| ----- | --------------- | +| array | All open pages. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const context = browser.newContext(); + context.newPage(); + const pages = context.pages(); + console.log(pages.length); // 1 + context.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setdefaultnavigationtimeout.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setdefaultnavigationtimeout.md new file mode 100644 index 000000000..edf1fa61f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setdefaultnavigationtimeout.md @@ -0,0 +1,47 @@ +--- +title: 'setDefaultNavigationTimeout(timeout)' +description: 'Sets the default navigation timeout in milliseconds.' +--- + +# setDefaultNavigationTimeout(timeout) + +Sets the default maximum navigation timeout for [Page.goto()](https://playwright.dev/docs/api/class-page#page-goto). + +| Parameter | Type | Default | Description | +| --------- | ------ | ------------------------ | ---------------------------- | +| timeout | number | Dependent on the action. | The timeout in milliseconds. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + context.setDefaultNavigationTimeout(1000); // 1s + + try { + await page.goto('https://httpbin.test.k6.io/delay/5'); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setdefaulttimeout.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setdefaulttimeout.md new file mode 100644 index 000000000..a6edfff90 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setdefaulttimeout.md @@ -0,0 +1,42 @@ +--- +title: 'setDefaultTimeout(timeout)' +description: 'Sets the default timeout in milliseconds.' +--- + +# setDefaultTimeout(timeout) + +Sets the default maximum timeout for all methods accepting a `timeout` option in milliseconds. + +| Parameter | Type | Default | Description | +| --------- | ------ | ------------------------ | ---------------------------- | +| timeout | number | Dependent on the action. | The timeout in milliseconds. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + context.setDefaultTimeout(1000); // 1s + const page = context.newPage(); + await page.click('h2'); // times out +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setgeolocation.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setgeolocation.md new file mode 100644 index 000000000..12b8f9bc6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setgeolocation.md @@ -0,0 +1,54 @@ +--- +title: 'setGeolocation(geolocation)' +description: "Sets the BrowserContext's geolocation." +--- + +# setGeolocation(geolocation) + +{{% admonition type="caution" %}} + +This feature has **known issues**. For details, refer to +[#435](https://github.com/grafana/xk6-browser/issues/435). + +{{% /admonition %}} + +Sets the context's geolocation. + + + +| Parameter | Type | Default | Description | +| --------------------- | ------ | ------- | ------------------------------------- | +| geolocation | object | `null` | | +| geolocation.latitude | number | `0` | Latitude between -90 and 90. | +| geolocation.longitude | number | `0` | Latitude between -180 and 180. | +| geolocation.accuracy | number | `0` | Optional non-negative accuracy value. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const context = browser.newContext(); + context.setGeolocation({ latitude: 59.95, longitude: 30.31667 }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setoffline.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setoffline.md new file mode 100644 index 000000000..45ff480fe --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/setoffline.md @@ -0,0 +1,50 @@ +--- +title: 'setOffline(offline)' +description: "Toggles the BrowserContext's connectivity on/off." +--- + +# setOffline(offline) + +Toggles the `BrowserContext`'s connectivity on/off. + +| Parameter | Type | Default | Description | +| --------- | ------- | ------- | ------------------------------------------------------------------------------------------- | +| offline | boolean | `false` | Whether to emulate the `BrowserContext` being disconnected (`true`) or connected (`false`). | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + + context.setOffline(true); + + const page = context.newPage(); + + try { + // Will not be able to load the page + await page.goto('https://test.k6.io/browser.php'); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/waitforevent.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/waitforevent.md new file mode 100644 index 000000000..a5fcae40b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/browsercontext/waitforevent.md @@ -0,0 +1,71 @@ +--- +title: 'waitForEvent(event[, optionsOrPredicate])' +description: 'Waits for event to fire and returns its value.' +--- + +Waits for the event to fire and returns its value. If a predicate function has been set it will pass the value to the predicate function, which must return `true` for the promise to resolve. + + + +| Parameter | Type | Default | Description | +| ---------------------------- | ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| event | string | `null` | Name of event to wait for. Currently the `'page'` event is the only one that is supported. | +| optionsOrPredicate | function\|object | `null` | Optional. If it's a function, the `'page'` event data will be passed to it and it must return `true` to continue. | +| optionsOrPredicate.predicate | function | `null` | Optional. Function that will be called when the `'page'` event is emitted. The event data will be passed to it and it must return `true` to continue. | +| optionsOrPredicate.timeout | number | `30000` | Optional. Maximum time to wait in milliseconds. | + + + +### Returns + +| Type | Description | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `Promise` | At the moment a [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) object is the only return value | + +### Example + + + +```javascript +import { browser } from 'k6/x/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + + // Call waitForEvent with a predicate which will return true once at least + // one page has been created. + let counter = 0; + const promise = context.waitForEvent('page', { + predicate: (page) => { + if (++counter >= 1) { + return true; + } + return false; + }, + }); + + // Now we create a page. + const page = context.newPage(); + + // Wait for the predicate to pass. + await promise; + console.log('predicate passed'); + + page.close(); +} +``` + + diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/closecontext.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/closecontext.md new file mode 100644 index 000000000..af2d2d285 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/closecontext.md @@ -0,0 +1,45 @@ +--- +title: 'closeContext()' +description: 'Browser module: close context method' +--- + +Closes the current [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). If there is no active browser context, because none has been created yet or because it has been previously closed, this method throws an error. + +### Example + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page1 = browser.newPage({ + isMobile: true, + }); // implicitly creates a new context + + await page1.goto('https:/test.k6.io/'); + page1.close(); + browser.closeContext(); // closes the context created on newPage + + const page2 = browser.newPage({ + isMobile: false, + }); // creates a new context with different settings + + await page2.goto('https://test.k6.io/'); + page2.close(); + browser.closeContext(); + + browser.closeContext(); // throws an error as browser has no active context +} +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/console-message.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/console-message.md new file mode 100644 index 000000000..9dedcd263 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/console-message.md @@ -0,0 +1,19 @@ +--- +title: 'ConsoleMessage' +slug: 'consolemessage' +description: 'Browser module: ConsoleMessage Class' +weight: 03 +--- + +# ConsoleMessage + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------- | +| consoleMessage.args() | - | +| consoleMessage.page() | - | +| consoleMessage.text() | - | +| consoleMessage.type() | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/context.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/context.md new file mode 100644 index 000000000..2ce53ab33 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/context.md @@ -0,0 +1,51 @@ +--- +title: 'context()' +description: 'Browser module: context method' +--- + +# context() + +Returns the current [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/). + +{{% admonition type="note" %}} + +A 1-to-1 mapping between [Browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser) and `BrowserContext` means you cannot run `BrowserContexts` concurrently. If you wish to create a new `BrowserContext` while one already exists, you will need to [close](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/close) the current one, and create a new one with either [newContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/newcontext/) or [newPage](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/newpage). All resources associated to the closed `BrowserContext` will also be closed and cleaned up (such as [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/)s). + +{{% /admonition %}} + +### Returns + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| object \| null | The current [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) if one has been created, otherwise `null`. | + +### Example + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + console.log(browser.context()); // null + + const page1 = browser.newPage(); // implicitly creates a new browserContext + const context = browser.context(); // underlying live browserContext associated with browser + const page2 = context.newPage(); // shares the browserContext with page1 + + page1.close(); + page2.close(); + context.close(); +} +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/_index.md new file mode 100644 index 000000000..c3fdaff0b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/_index.md @@ -0,0 +1,148 @@ +--- +title: 'ElementHandle' +slug: 'elementhandle' +description: 'Browser module: ElementHandle Class' +weight: 04 +--- + +# ElementHandle + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | +| elementHandle.$(selector) | - | +| elementHandle.$$(selector) | - | +| elementHandle.boundingBox() | - | +| elementHandle.check([options]) | - | +| elementHandle.click([options]) | - | +| elementHandle.contentFrame() | - | +| [elementHandle.dblclick([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/elementhandle/dblclick) | Double click on the element. | +| elementHandle.dispatchEvent(type[, eventInit]) | - | +| elementHandle.fill(value[, options]) | - | +| elementHandle.focus() | - | +| elementHandle.getAttribute() | - | +| elementHandle.hover([options]) | - | +| elementHandle.innerHTML() | - | +| elementHandle.innerText() | - | +| elementHandle.inputValue([options]) | - | +| elementHandle.isChecked() | - | +| elementHandle.isDisabled() | - | +| elementHandle.isEditable() | - | +| elementHandle.isEnabled() | - | +| [elementHandle.isHidden()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/elementhandle/ishidden/) | Checks if the element is `hidden`. | +| [elementHandle.isVisible()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/elementhandle/isvisible/) | Checks if the element is `visible`. | +| elementHandle.ownerFrame() | - | +| elementHandle.press(key[, options]) | - | +| elementHandle.screenshot([options]) | - | +| elementHandle.scrollIntoViewIfNeeded([options]) | - | +| elementHandle.selectOptions(values[, options]) | - | +| elementHandle.selectText([options]) | - | +| elementHandle.setChecked(checked[, options]) | - | +| [elementHandle.setInputFiles(file[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/elementhandle/setinputfiles) | Sets the file input element's value to the specified files. | +| elementHandle.tap([options]) | - | +| elementHandle.textContent() | - | +| elementHandle.type(text[, options]) | - | +| elementHandle.uncheck([options]) | - | +| elementHandle.waitForElementState(state[, options]) | - | +| elementHandle.waitForSelector(selector[, options]) | - | + +## Examples + +{{< code >}} + +```javascript +import { check } from 'k6'; +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + // Goto front page, find login link and click it + try { + await page.goto('https://test.k6.io/'); + const messagesLink = page.locator('a[href="/my_messages.php"]'); + + await Promise.all([page.waitForNavigation(), messagesLink.click()]); + // Enter login credentials and login + page.locator('input[name="login"]').type('admin'); + page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + + await Promise.all([page.waitForNavigation(), submitButton.click()]); + check(page, { + header: (p) => p.locator('h2').textContent() == 'Welcome, admin!', + }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import { check } from 'k6'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const page = browser.newPage(); + + // Inject page content + page.setContent(` +
Hello world
+ +
Edit me
+ + + + + `); + + // Check state + check(page, { + visible: (p) => p.$('.visible').isVisible(), + hidden: (p) => p.$('.hidden').isHidden(), + editable: (p) => p.$('.editable').isEditable(), + enabled: (p) => p.$('.enabled').isEnabled(), + disabled: (p) => p.$('.disabled').isDisabled(), + checked: (p) => p.$('.checked').isChecked(), + unchecked: (p) => p.$('.unchecked').isChecked() === false, + }); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/dblclick.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/dblclick.md new file mode 100644 index 000000000..0778e4daf --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/dblclick.md @@ -0,0 +1,58 @@ +--- +title: 'dblclick([, options])' +description: 'Browser module: elementHandle.dblclick([, options]) method' +--- + +# dblclick([, options]) + +Mouse double clicks on the element. + + + +| Parameter | Type | Default | Description | +| ------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| options | object | `null` | | +| options.button | string | `left` | The mouse button (`left`, `middle` or `right`) to use during the action. | +| options.delay | number | `0` | Milliseconds to wait between `mousedown` and `mouseup`. | +| options.force | boolean | `false` | Bypasses the actionability checks (`visible`, `stable`, `enabled`) if set to `true`. | +| options.modifiers | string[] | `null` | `Alt`, `Control`, `Meta` or `Shift` modifiers keys pressed during the action. If not specified, currently pressed modifiers are used. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it doesn't wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Passing `0` disables the timeout. The `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/) can override the default timeout. | +| options.trial | boolean | `false` | Performs the actionability checks without performing the double click action if set to `true`. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + + const elementHandle = page.$('#counter-button'); + elementHandle.dblclick(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/ishidden.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/ishidden.md new file mode 100644 index 000000000..c093287d4 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/ishidden.md @@ -0,0 +1,48 @@ +--- +title: 'isHidden()' +description: 'Browser module: elementHandle.isHidden() method' +--- + +# isHidden() + +Checks if the element is `hidden`. + +### Returns + +| Type | Description | +| ---- | ----------------------------------------------------- | +| bool | `true` if the element is `hidden`, `false` otherwise. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + + const elementHandle = page.$('#input-text-hidden'); + if (elementHandle.isHidden()) { + console.log('element is hidden'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/isvisible.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/isvisible.md new file mode 100644 index 000000000..270843c67 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/isvisible.md @@ -0,0 +1,48 @@ +--- +title: 'isVisible()' +description: 'Browser module: elementHandle.isVisible() method' +--- + +# isVisible() + +Checks if the element is `visible`. + +### Returns + +| Type | Description | +| ---- | ------------------------------------------------------ | +| bool | `true` if the element is `visible`, `false` otherwise. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + + const elementHandle = page.$('#text1'); + if (elementHandle.isVisible()) { + console.log('element is visible'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/setinputfiles.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/setinputfiles.md new file mode 100644 index 000000000..46911151a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/elementhandle/setinputfiles.md @@ -0,0 +1,72 @@ +--- +title: 'setInputFiles(file[, options])' +description: 'Sets the file input element' +--- + +# setInputFiles(file[, options]) + +Sets the file input element's value to the specified files. + +To work with local files on the file system, use the [experimental fs module](https://grafana.com/docs/k6/latest/javascript-api/k6-experimental/fs/) to load and read the file contents. + +| Parameter | Type | Default | Description | +| ------------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| file | object | `null` | This is a required parameter. | +| file.name | string | `''` | The name of the file. For example, `file.txt`. | +| file.mimeType | string | `''` | The type of the file content. For example, `text/plain`. | +| file.buffer | ArrayBuffer | `[]` | Base64 encoded content of the file. | +| options | object | `null` | This is an optional parameter. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import encoding from 'k6/encoding'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + // In this example we create a simple web page with an upload input field. + page.setContent(` + + + + +
+ + +
+ + `); + + const eh = page.$('input[id="upload"]'); + + // The file is set to the input element with the id "upload". + eh.setInputFiles({ + name: 'file.txt', + mimetype: 'text/plain', + buffer: encoding.b64encode('hello world'), + }); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/_index.md new file mode 100644 index 000000000..9e30adc95 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/_index.md @@ -0,0 +1,61 @@ +--- +title: 'Frame' +description: 'Browser module: Frame Class' +weight: 05 +--- + +# Frame + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| frame.$(selector[, options]) | - | +| frame.check(selector[, options]) | - | +| frame.childFrames() | - | +| frame.click(selector[, options]) | - | +| frame.content() | - | +| [frame.dblclick(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame/dblclick) | Double click on an element matching the provided `selector`. | +| frame.dblclick(selector[, options]) | - | +| frame.dispatchEvent(selector, type[, eventInit, options]) | - | +| frame.evaluate(pageFunction[, arg]) | - | +| frame.evaluateHandle(pageFunction[, arg]) | - | +| frame.fill(selector, value[, options]) | - | +| frame.focus(selector[, options]) | - | +| frame.frameElement() | - | +| frame.getAttribute(selector, name[, options]) | - | +| frame.goto(url[, options]) | - | +| frame.hover(selector[, options]) | - | +| frame.innerHTML(selector[, options]) | - | +| frame.innerText(selector[, options]) | - | +| frame.inputValue(selector[, options]) | - | +| frame.isChecked(selector[, options]) | - | +| frame.isDetached() | - | +| frame.isDisabled(selector[, options]) | - | +| frame.isEditable(selector[, options]) | - | +| frame.isEnabled(selector[, options]) | - | +| [frame.isHidden(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame/ishidden/) | Checks if the matched element is `hidden`. | +| [frame.isVisible(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame/isvisible/) | Checks if the matched element is `visible`. | +| frame.locator(selector[, options]) | - | +| frame.name() | - | +| frame.page() | - | +| frame.parentFrame() | - | +| frame.press(selector, key[, options]) | - | +| frame.selectOption(selector, values[, options]) | - | +| frame.setChecked(selector, checked[, options]) | - | +| frame.setContent(html[, options]) | - | +| [frame.setInputFiles(selector, file[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame/setinputfiles) | Sets the file input element's value to the specified files. | +| frame.tap(selector[, options]) | - | +| frame.textContent(selector[, options]) | - | +| frame.title() | - | +| frame.title() | - | +| frame.uncheck(selector[, options]) | - | +| frame.url() | - | +| frame.waitForFunction(pageFunction[, arg, options]) | - | +| frame.waitForLoadState([state, options]) | - | +| frame.waitForNavigation([options]) | - | +| frame.waitForSelector(selector[, options]) | - | +| frame.waitForTimeout(timeout) | - | +| frame.waitForURL(url[, options]) | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/dblclick.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/dblclick.md new file mode 100644 index 000000000..dc1095ec6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/dblclick.md @@ -0,0 +1,66 @@ +--- +title: 'dblclick(selector[, options])' +description: 'Browser module: frame.dblclick(selector[, options]) method' +--- + +# dblclick(selector[, options]) + +{{% admonition type="warning" %}} + +Use locator-based [`locator.dblclick([options])`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/dblclick/) instead. + +{{% /admonition %}} + +Double clicks on an element matching the provided selector. + + + +| Parameter | Type | Default | Description | +| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. The first element will be used if multiple elements match the selector. | +| options | object | `null` | | +| options.button | string | `left` | The mouse button (`left`, `middle` or `right`) to use during the action. | +| options.delay | number | `0` | Milliseconds to wait between `mousedown` and `mouseup`. | +| options.force | boolean | `false` | Bypasses the actionability checks (`visible`, `stable`, `enabled`) if set to `true`. | +| options.modifiers | string[] | `null` | `Alt`, `Control`, `Meta` or `Shift` modifiers keys pressed during the action. If not specified, currently pressed modifiers are used. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.strict | boolean | `false` | When `true`, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + + const frames = page.frames(); + frames[0].dblclick('#counter-button'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/ishidden.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/ishidden.md new file mode 100644 index 000000000..882bbc57e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/ishidden.md @@ -0,0 +1,62 @@ +--- +title: 'isHidden(selector[, options])' +description: 'Browser module: frame.isHidden(selector[, options]) method' +--- + +# isHidden(selector[, options]) + +{{% admonition type="warning" %}} + +Use locator-based [`locator.isHidden([options])`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/ishidden/) instead. + +{{% /admonition %}} + +Checks if the element is `hidden`. + + + +| Parameter | Type | Default | Description | +| -------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first one will be used. | +| options | object | `null` | | +| options.strict | boolean | `false` | When `true`, the call requires the selector to resolve to a single element. If the given selector resolves to more than one element, the call throws an exception. | + + + +### Returns + +| Type | Description | +| ---- | ----------------------------------------------------- | +| bool | `true` if the element is `hidden`, `false` otherwise. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + if (page.isHidden('#input-text-hidden')) { + console.log('element is hidden'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/isvisible.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/isvisible.md new file mode 100644 index 000000000..043da601c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/isvisible.md @@ -0,0 +1,65 @@ +--- +title: 'isVisible(selector[, options])' +description: 'Browser module: frame.isVisible(selector[, options]) method' +--- + +# isVisible(selector[, options]) + +{{% admonition type="warning" %}} + +Use locator-based [`locator.isVisible([options])`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/isvisible/) instead. + +{{% /admonition %}} + +Checks if the element is `visible`. + + + +| Parameter | Type | Default | Description | +| -------------- | ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first one will be used. | +| options | object | `null` | | +| options.strict | boolean | `false` | When `true`, the call requires the selector to resolve to a single element. If the given selector resolves to more than one element, the call throws an exception. | + + + +### Returns + +| Type | Description | +| ---- | ------------------------------------------------------ | +| bool | `true` if the element is `visible`, `false` otherwise. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + + const frames = page.frames(); + + if (frames[0].isVisible('#text1')) { + console.log('element is visible'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/setinputfiles.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/setinputfiles.md new file mode 100644 index 000000000..75e36643c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/frame/setinputfiles.md @@ -0,0 +1,73 @@ +--- +title: 'setInputFiles(selector, file[, options])' +description: 'Sets the file input element' +--- + +# setInputFiles(selector, file[, options]) + +Sets the file input element's value to the specified files. + +To work with local files on the file system, use the [experimental fs module](https://grafana.com/docs/k6/latest/javascript-api/k6-experimental/fs/) to load and read the file contents. + +| Parameter | Type | Default | Description | +| ------------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| file | object | `null` | This is a required parameter. | +| file.name | string | `''` | The name of the file. For example, `file.txt`. | +| file.mimeType | string | `''` | The type of the file content. For example, `text/plain`. | +| file.buffer | ArrayBuffer | `[]` | Base64 encoded content of the file. | +| options | object | `null` | This is an optional parameter. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import encoding from 'k6/encoding'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + // In this example we create a simple web page with an upload input field. + page.setContent(` + + + + +
+ + +
+ + `); + + const frame = page.mainFrame(); + + // The file is set to the input element with the id "upload". + frame.setInputFiles('input[id="upload"]', { + name: 'file.txt', + mimetype: 'text/plain', + buffer: encoding.b64encode('hello world'), + }); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/isconnected.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/isconnected.md new file mode 100644 index 000000000..333e76a37 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/isconnected.md @@ -0,0 +1,49 @@ +--- +title: 'isConnected()' +description: 'Browser module: isConnected method' +--- + +# isConnected() + +{{% admonition type="caution" %}} + +This feature has **known issues**. +For details, refer to [#453](https://github.com/grafana/xk6-browser/issues/453). + +{{% /admonition %}} + +Indicates whether the [CDP](https://chromedevtools.github.io/devtools-protocol/) connection to the browser process is active or not. + +### Returns + +| Type | Description | +| ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| boolean | Returns `true` if the [browser module](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser) is connected to the browser application. Otherwise, returns `false`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const isConnected = browser.isConnected(); + console.log(isConnected); // true +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/js-handle.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/js-handle.md new file mode 100644 index 000000000..7a80bf30a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/js-handle.md @@ -0,0 +1,22 @@ +--- +title: 'JSHandle' +slug: 'jshandle' +description: 'Browser module: JSHandle Class' +weight: 06 +--- + +# JSHandle + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | +| jsHandle.asElement() | - | +| jsHandle.dispose() | - | +| jsHandle.evaluate(pageFunction[, arg]) | - | +| jsHandle.evaluateHandle(pageFunction[, arg]) | - | +| jsHandle.getProperties() | - | +| jsHandle.getProperty(propertyName) | - | +| jsHandle.jsonValue() | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/keyboard.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/keyboard.md new file mode 100644 index 000000000..d1b6dc969 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/keyboard.md @@ -0,0 +1,19 @@ +--- +title: 'Keyboard' +description: 'Browser module: Keyboard Class' +weight: 07 +--- + +# Keyboard + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | +| keyboard.down(key) | - | +| keyboard.insertText(text) | - | +| keyboard.press(key[, options]) | - | +| keyboard.type(text[, options]) | - | +| keyboard.up(key) | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/_index.md new file mode 100644 index 000000000..093b27bfb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/_index.md @@ -0,0 +1,113 @@ +--- +title: "Locator" +description: "Browser module: Locator Class" +weight: 08 +weight: 08 +--- + +# Locator + +The Locator API makes it easier to work with dynamically changing elements. Some of the benefits of using it over existing ways to locate an element (e.g. `Page.$()`) include: + +- Helps with writing robust tests by finding an element even if the underlying frame navigates. +- Makes it easier to work with dynamic web pages and SPAs built with Svelte, React, Vue, etc. +- Enables the use of test abstractions like the Page Object Model (POM) pattern to simplify and organize tests. +- `strict` mode is enabled for all `locator` methods, which means that if more than one element matches the given selector it will throw an error. + +Locator can be created with the [page.locator(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/locator) method. + +| Method | Description | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | +| [locator.check([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/check) | Select the input checkbox. | +| [locator.clear([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/clear) | Clears text boxes and input fields of any existing values. | +| [locator.click([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/click) | Mouse click on the chosen element. | +| [locator.dblclick([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/dblclick) | Mouse double click on the chosen element. | +| [locator.dispatchEvent(type, eventInit, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/dispatchevent) | Dispatches HTML DOM event types e.g. `'click'`. | +| [locator.fill(value, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/fill) | Fill an `input`, `textarea` or `contenteditable` element with the provided value. | +| [locator.focus([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/focus) | Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element, if it can be focused. | +| [locator.getAttribute(name, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/getattribute) | Returns the element attribute value for the given attribute name. | +| [locator.hover([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/hover) | Hovers over the element. | +| [locator.innerHTML([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/innerhtml) | Returns the `element.innerHTML`. | +| [locator.innerText([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/innertext) | Returns the `element.innerText`. | +| [locator.inputValue([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/inputvalue) | Returns `input.value` for the selected `input`, `textarea` or `select` element. | +| [locator.isChecked([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/ischecked) | Checks if the `checkbox` `input` type is selected. | +| [locator.isDisabled([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/isdisabled) | Checks if the element is `disabled`. | +| [locator.isEditable([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/iseditable) | Checks if the element is `editable`. | +| [locator.isEnabled([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/isenabled) | Checks if the element is `enabled`. | +| [locator.isHidden()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/ishidden) | Checks if the element is `hidden`. | +| [locator.isVisible()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/isvisible) | Checks if the element is `visible`. | +| [locator.press(key, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/press) | Press a single key on the keyboard or a combination of keys. | +| [locator.selectOption(values, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/selectoption) | Select one or more options which match the values. | +| [locator.tap([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/tap) | Tap on the chosen element. | +| [locator.textContent([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/textcontent) | Returns the `element.textContent`. | +| [locator.type(text, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/type) | Type in the text into the input field. | +| [locator.uncheck([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/uncheck) | Unselect the `input` checkbox. | +| [locator.waitFor([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/waitfor) | Wait for the element to be in a particular state e.g. `visible`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/flip_coin.php'); + + /* + In this example, we will use two locators, matching a + different betting button on the page. If you were to query + the buttons once and save them as below, you would see an + error after the initial navigation. Try it! + + const heads = page.$("input[value='Bet on heads!']"); + const tails = page.$("input[value='Bet on tails!']"); + + The Locator API allows you to get a fresh element handle each + time you use one of the locator methods. And, you can carry a + locator across frame navigations. Let's create two locators; + each locates a button on the page. + */ + const heads = page.locator("input[value='Bet on heads!']"); + const tails = page.locator("input[value='Bet on tails!']"); + + const currentBet = page.locator("//p[starts-with(text(),'Your bet: ')]"); + + // In the following Promise.all the tails locator clicks + // on the tails button by using the locator's selector. + // Since clicking on each button causes page navigation, + // waitForNavigation is needed -- this is because the page + // won't be ready until the navigation completes. + // Setting up the waitForNavigation first before the click + // is important to avoid race conditions. + await Promise.all([page.waitForNavigation(), tails.click()]); + console.log(currentBet.innerText()); + // the heads locator clicks on the heads button + // by using the locator's selector. + await Promise.all([page.waitForNavigation(), heads.click()]); + console.log(currentBet.innerText()); + await Promise.all([page.waitForNavigation(), tails.click()]); + console.log(currentBet.innerText()); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/check.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/check.md new file mode 100644 index 000000000..a6d9922ba --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/check.md @@ -0,0 +1,60 @@ +--- +title: 'check([options])' +description: 'Browser module: locator.check method' +--- + +# check([options]) + +{{% admonition type="caution" %}} + +This feature has known issues. For details, refer to +[#471](https://github.com/grafana/xk6-browser/issues/471) and [#475](https://github.com/grafana/xk6-browser/issues/475). + +{{% /admonition %}} + +Use this method to select an `input` checkbox. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const checkbox = page.locator('#checkbox1'); + checkbox.check(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/clear.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/clear.md new file mode 100644 index 000000000..314448999 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/clear.md @@ -0,0 +1,68 @@ +--- +title: 'clear([options])' +description: 'Browser module: locator.clear method' +--- + +# clear([options]) + +Clears text boxes and input fields (`input`, `textarea` or `contenteditable` elements) of any existing values. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Example + +{{< code >}} + +```javascript +import { check } from 'k6'; +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + await page.goto('https://test.k6.io/my_messages.php', { waitUntil: 'networkidle' }); + + // Fill an input element with some text that we will later clear. + page.locator('input[name="login"]').type('admin'); + + // This checks that the element has been filled with text. + check(page, { + not_empty: (p) => p.locator('input[name="login"]').inputValue() != '', + }); + + // Now clear the text from the element. + page.locator('input[name="login"]').clear(); + + // This checks that the element is now empty. + check(page, { + empty: (p) => p.locator('input[name="login"]').inputValue() == '', + }); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/click.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/click.md new file mode 100644 index 000000000..44b30ac22 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/click.md @@ -0,0 +1,96 @@ +--- +title: 'click([options])' +description: 'Browser module: locator.click method' +--- + +# click([options]) + +{{% admonition type="caution" %}} + +This method has **known issues**. For details, refer to [#471](https://github.com/grafana/xk6-browser/issues/471) and [#474](https://github.com/grafana/xk6-browser/issues/474). + +{{% /admonition %}} + +Mouse click on the chosen element. + + + +| Parameter | Type | Default | Description | +| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.button | string | `left` | The mouse button (`left`, `middle` or `right`) to use during the action. | +| options.clickCount | number | `1` | The number of times the action is performed. | +| options.delay | number | `0` | Milliseconds to wait between `mousedown` and `mouseup`. | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.modifiers | string[] | `null` | `Alt`, `Control`, `Meta` or `Shift` modifiers keys pressed during the action. If not specified, currently pressed modifiers are used. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Examples + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + const button = page.locator('#counter-button'); + await button.click(); +} +``` + +{{< /code >}} + +When a click action results in a page navigation, remember to work with `Promise.all()` and `page.waitForNavigation()` to properly handle the asynchronous operation. + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/'); + const messagesLink = page.locator('a[href="/my_messages.php"]'); + + await Promise.all([page.waitForNavigation(), messagesLink.click()]); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/dblclick.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/dblclick.md new file mode 100644 index 000000000..862b5f16a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/dblclick.md @@ -0,0 +1,62 @@ +--- +title: 'dblclick([options])' +description: 'Browser module: locator.dblclick method' +--- + +# dblclick([options]) + +{{% admonition type="caution" %}} + +This feature has known issues. For details, refer to [#471](https://github.com/grafana/xk6-browser/issues/471). + +{{% /admonition %}} + +Mouse double click on the chosen element. + + + +| Parameter | Type | Default | Description | +| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.button | string | `left` | The mouse button (`left`, `middle` or `right`) to use during the action. | +| options.delay | number | `0` | Milliseconds to wait between `mousedown` and `mouseup`. | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.modifiers | string[] | `null` | `Alt`, `Control`, `Meta` or `Shift` modifiers keys pressed during the action. If not specified, currently pressed modifiers are used. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const button = page.locator('#counter-button'); + button.dblclick(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/dispatchevent.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/dispatchevent.md new file mode 100644 index 000000000..9d6f8c385 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/dispatchevent.md @@ -0,0 +1,51 @@ +--- +title: 'dispatchEvent(type, eventInit, [options])' +description: 'Browser module: locator.dispatchEvent method' +--- + +# dispatchEvent(type, eventInit, [options]) + +Dispatches HTML DOM event types e.g. `'click'`. + + + +| Parameter | Type | Defaults | Description | +| --------------- | ------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| type | string | `''` | DOM event type e.g. `'click'`. | +| eventInit | object | `null` | Optional event specific properties. See [eventInit](#eventinit) for more details. | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### eventInit + +Since eventInit is event-specific, please refer to the events documentation for the lists of initial properties: + +- [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent) +- [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent) +- [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent) +- [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent) +- [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent) +- [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent) +- [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event) + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + const button = page.locator('#counter-button'); + button.dispatchEvent('click'); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/fill.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/fill.md new file mode 100644 index 000000000..cd7a610cb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/fill.md @@ -0,0 +1,50 @@ +--- +title: 'fill(value, [options])' +description: 'Browser module: locator.fill method' +--- + +# fill(value, [options]) + +Fill an `input`, `textarea` or `contenteditable` element with the provided value. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | string | `''` | Value to set for the `input`, `textarea` or `contenteditable` element. | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const textbox = page.locator('#text1'); + textbox.fill('hello world!'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/focus.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/focus.md new file mode 100644 index 000000000..72ecc68d9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/focus.md @@ -0,0 +1,47 @@ +--- +title: 'focus([options])' +description: 'Browser module: locator.focus method' +--- + +# focus([options]) + +Calls [focus](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) on the element, if it can be focused on. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const textbox = page.locator('#text1'); + textbox.focus(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/getattribute.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/getattribute.md new file mode 100644 index 000000000..74ebdb917 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/getattribute.md @@ -0,0 +1,55 @@ +--- +title: 'getAttribute(name, [options])' +description: 'Browser module: locator.getAttribute method' +--- + +# getAttribute(name, [options]) + +Returns the element attribute value for the given attribute name. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | string | `''` | Attribute name to get the value for. | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ------ | ----------------------------------- | +| string | The value of the attribute or null. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const textbox = page.locator('#text1'); + const attribute = textbox.getAttribute('onfocus'); + console.log(attribute); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/hover.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/hover.md new file mode 100644 index 000000000..5035eec32 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/hover.md @@ -0,0 +1,61 @@ +--- +title: 'hover([options])' +description: 'Browser module: locator.hover method' +--- + +# hover([options]) + +{{% admonition type="caution" %}} + +This feature has known issues. For details, refer to +[#471](https://github.com/grafana/xk6-browser/issues/471). + +{{% /admonition %}} + +Hovers over the element. + + + +| Parameter | Type | Default | Description | +| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.modifiers | string[] | `null` | `Alt`, `Control`, `Meta` or `Shift` modifiers keys pressed during the action. If not specified, currently pressed modifiers are used. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const offScreenElement = page.locator('#off-screen'); + offScreenElement.hover(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/innerhtml.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/innerhtml.md new file mode 100644 index 000000000..5488946e2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/innerhtml.md @@ -0,0 +1,54 @@ +--- +title: 'innerHTML([options])' +description: 'Browser module: locator.innerHTML method' +--- + +# innerHTML([options]) + +Returns the `element.innerHTML`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ------ | ----------------------------- | +| string | The innerHTML of the element. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const offScreen = page.locator('#off-screen'); + const innerHTML = offScreen.innerHTML(); + console.log(innerHTML); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/innertext.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/innertext.md new file mode 100644 index 000000000..669ffd082 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/innertext.md @@ -0,0 +1,54 @@ +--- +title: 'innerText([options])' +description: 'Browser module: locator.innerText method' +--- + +# innerText([options]) + +Returns the `element.innerText`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ------ | ----------------------------- | +| string | The innerText of the element. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const offScreen = page.locator('#off-screen'); + const innerText = offScreen.innerText(); + console.log(innerText); // Off page div +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/inputvalue.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/inputvalue.md new file mode 100644 index 000000000..e3d4debea --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/inputvalue.md @@ -0,0 +1,55 @@ +--- +title: 'inputValue([options])' +description: 'Browser module: locator.inputValue method' +--- + +# inputValue([options]) + +Returns `input.value` for the selected `input`, `textarea` or `select` element. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ------ | ------------------------------- | +| string | The input value of the element. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const textInput = page.locator('#text1'); + textInput.fill('Hello world!'); + const inputValue = textInput.inputValue(); + console.log(inputValue); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/ischecked.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/ischecked.md new file mode 100644 index 000000000..65111c24b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/ischecked.md @@ -0,0 +1,55 @@ +--- +title: 'isChecked([options])' +description: 'Browser module: locator.isChecked method' +--- + +# isChecked([options]) + +Checks to see if the `checkbox` `input` type is selected or not. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ---- | ------------------------------------------------- | +| bool | `true` if the checkbox is selected, else `false`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const checkbox = page.locator('#checkbox1'); + if (!checkbox.isChecked()) { + checkbox.check(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isdisabled.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isdisabled.md new file mode 100644 index 000000000..5c8cf2913 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isdisabled.md @@ -0,0 +1,55 @@ +--- +title: 'isDisabled([options])' +description: 'Browser module: locator.isDisabled method' +--- + +# isDisabled([options]) + +Checks if the element is `disabled`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ---- | -------------------------------------------------- | +| bool | `true` if the element is `disabled`, else `false`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#input-text-disabled'); + if (text.isDisabled()) { + console.log('element is disabled'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/iseditable.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/iseditable.md new file mode 100644 index 000000000..4ac02df02 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/iseditable.md @@ -0,0 +1,55 @@ +--- +title: 'isEditable([options])' +description: 'Browser module: locator.isEditable method' +--- + +# isEditable([options]) + +Checks if the element is `editable`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ---- | -------------------------------------------------- | +| bool | `true` if the element is `editable`, else `false`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#text1'); + if (text.isEditable()) { + text.fill('hello world!'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isenabled.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isenabled.md new file mode 100644 index 000000000..b538b9439 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isenabled.md @@ -0,0 +1,56 @@ +--- +title: 'isEnabled([options])' +description: 'Browser module: locator.isEnabled method' +--- + +# isEnabled([options]) + +Checks if the element is `enabled`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ---- | ------------------------------------------------- | +| bool | `true` if the element is `enabled`, else `false`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#text1'); + if (text.isEnabled()) { + console.log('element is enabled'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/ishidden.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/ishidden.md new file mode 100644 index 000000000..e62124306 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/ishidden.md @@ -0,0 +1,46 @@ +--- +title: 'isHidden()' +description: 'Browser module: locator.isHidden method' +--- + +# isHidden() + +Checks if the element is `hidden`. + +### Returns + +| Type | Description | +| ---- | ------------------------------------------------ | +| bool | `true` if the element is `hidden`, else `false`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#input-text-hidden'); + if (text.isHidden()) { + console.log('element is hidden'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isvisible.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isvisible.md new file mode 100644 index 000000000..ee5db22dd --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/isvisible.md @@ -0,0 +1,46 @@ +--- +title: 'isVisible()' +description: 'Browser module: locator.isVisible method' +--- + +# isVisible() + +Checks if the element is `visible`. + +### Returns + +| Type | Description | +| ---- | ------------------------------------------------- | +| bool | `true` if the element is `visible`, else `false`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#text1'); + if (text.isVisible()) { + console.log('element is visible'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/press.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/press.md new file mode 100644 index 000000000..390769848 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/press.md @@ -0,0 +1,52 @@ +--- +title: 'press(key, [options])' +description: 'Browser module: locator.press method' +--- + +# press(key, [options]) + +Press a single key on the keyboard or a combination of keys. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| key | string | `''` | Name of the key to press or a character to generate, such as `ArrowLeft` or `a`. A superset of the key values can be found [here](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values). | +| options | object | `null` | | +| options.delay | number | `0` | Milliseconds to wait between `keydown` and `keyup`. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#text1'); + text.press('i'); + text.press('ArrowLeft'); + text.press('h'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/selectoption.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/selectoption.md new file mode 100644 index 000000000..20956a78e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/selectoption.md @@ -0,0 +1,63 @@ +--- +title: 'selectOption(values, [options])' +description: 'Browser module: locator.selectOption method' +--- + +# selectOption(values, [options]) + +{{% admonition type="caution" %}} + +This feature has **known issues**. For details, refer to +[#470](https://github.com/grafana/xk6-browser/issues/470) and [#471](https://github.com/grafana/xk6-browser/issues/471). + +{{% /admonition %}} + +Select one or more options which match the values. + + + +| Parameter | Type | Default | Description | +| ------------------- | ---------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| values | string or string[] or object | `''` | If the `select` has the multiple attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. Object can be made up of keys with `value`, `label` or `index`. | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| -------- | ----------------------------- | +| string[] | List of the selected options. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const options = page.locator('#numbers-options'); + options.selectOption('three'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/tap.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/tap.md new file mode 100644 index 000000000..18c97091a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/tap.md @@ -0,0 +1,51 @@ +--- +title: 'tap([options])' +description: 'Browser module: locator.tap method' +--- + +# tap([options]) + +{{% admonition type="caution" %}} + +This feature has **known issues**. For details, refer to +[#436](https://github.com/grafana/xk6-browser/issues/436) and [#471](https://github.com/grafana/xk6-browser/issues/471). + +{{% /admonition %}} + +Tap on the chosen element. + + + +| Parameter | Type | Default | Description | +| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.modifiers | string[] | `null` | `Alt`, `Control`, `Meta` or `Shift` modifiers keys pressed during the action. If not specified, currently pressed modifiers are used. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export default async function () { + const page = browser.newPage({ + hasTouch: true, + }); + + await page.goto('https://test.k6.io/browser.php'); + const options = page.locator('#numbers-options'); + options.tap(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/textcontent.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/textcontent.md new file mode 100644 index 000000000..8735522e3 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/textcontent.md @@ -0,0 +1,53 @@ +--- +title: 'textContent([options])' +description: 'Browser module: locator.textContent method' +--- + +# textContent([options]) + +Returns the `element.textContent`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ------ | ----------------------------------------- | +| string | The text content of the selector or null. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const options = page.locator('#checkbox1'); + console.log(options.textContent()); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/type.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/type.md new file mode 100644 index 000000000..6cc843583 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/type.md @@ -0,0 +1,50 @@ +--- +title: 'type(text, [options])' +description: 'Browser module: locator.type method' +--- + +# type(text, [options]) + +Type in the text into the input field. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| text | string | `''` | A text to type into a focused element. | +| options | object | `null` | | +| options.delay | number | `0` | Milliseconds to wait between key presses. Defaults to `0`. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#text1'); + text.type('hello world!'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/uncheck.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/uncheck.md new file mode 100644 index 000000000..549006d52 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/uncheck.md @@ -0,0 +1,61 @@ +--- +title: 'uncheck([options])' +description: 'Browser module: locator.uncheck method' +--- + +# uncheck([options]) + +{{% admonition type="caution" %}} + +This feature has **known issues**. +For details, refer to [#471](https://github.com/grafana/xk6-browser/issues/471). + +{{% /admonition %}} + +Unselect the `input` checkbox. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + const checkbox = page.locator('#checkbox1'); + checkbox.check(); + checkbox.uncheck(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/waitfor.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/waitfor.md new file mode 100644 index 000000000..fc30d3b1b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/locator/waitfor.md @@ -0,0 +1,58 @@ +--- +title: 'waitFor([options])' +description: 'Browser module: locator.waitFor method' +--- + +# waitFor([options]) + +{{% admonition type="caution" %}} + +This feature has **known issues**. For details, +refer to [#472](https://github.com/grafana/xk6-browser/issues/472). + +{{% /admonition %}} + +Wait for the element to be in a particular state e.g. `visible`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.state | string | `visible` | Can be `attached`, `detached`, `visible` or `hidden`. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + const text = page.locator('#input-text-hidden'); + text.waitFor({ + state: 'hidden', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/mouse.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/mouse.md new file mode 100644 index 000000000..1e1c2b17e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/mouse.md @@ -0,0 +1,20 @@ +--- +title: 'Mouse' +description: 'Browser module: Mouse Class' +weight: 09 +--- + +# Mouse + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| ------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | +| mouse.click(x, y[, options]) | - | +| mouse.dblclick(x, y[, options]) | - | +| mouse.down([options]) | - | +| mouse.move(x, y[, options]) | - | +| mouse.up([options]) | - | +| mouse.wheel(deltaX, deltaY) | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/newcontext.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/newcontext.md new file mode 100644 index 000000000..0ed9c8e35 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/newcontext.md @@ -0,0 +1,95 @@ +--- +title: 'newContext([options])' +description: 'Browser module: newContext method' +--- + +# newContext([options]) + +Creates and returns a new [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/), if one hasn't already been initialized for the [Browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser). If one has already been initialized an error is thrown. + +{{% admonition type="note" %}} + +A 1-to-1 mapping between [Browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser) and `BrowserContext` means you cannot run `BrowserContexts` concurrently. Due to this restriction, if one already exists, it must be [close](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/close)d first before creating a new one. + +{{% /admonition %}} + + + +| Parameter | Type | Default | Description | +| ------------------------------------------- | ------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.bypassCSP | boolean | `false` | Whether to bypass a page's Content-Security-Policy. | +| options.colorScheme | string | `'light'` | Whether to display a page in dark or light mode by emulating the 'prefers-colors-scheme' media feature. It can be one of `'light'`, `'dark'`, `'no-preference'`. | +| options.deviceScaleFactor | number | `1` | Sets the resolution ratio in physical pixels to the resolution in CSS pixels i.e. if set higher than `1`, then images will look sharper on high pixel density screens. See an [example](#devicescalefactor-example) below. | +| options.extraHTTPHeaders | object | `null` | Contains additional HTTP headers to be sent with every request, where the keys are HTTP headers and values are HTTP header values. | +| options.geolocation | object | `null` | Sets the user's geographical location. | +| options.geolocation.latitude | number | `0` | Latitude should be between `-90` and `90`. | +| options.geolocation.longitude | number | `0` | Longitude should be between `-180` and `180`. | +| options.geolocation.accuracy | number | `0` | Accuracy should only be a non-negative number. Defaults to `0`. | +| options.hasTouch | boolean | `false` | Whether to simulate a device with touch events. | +| options.httpCredentials | object | `null` | Sets the credentials for HTTP authentication using Basic Auth. | +| options.httpCredentials.username | string | `''` | Username to pass to the web browser for Basic HTTP Authentication. | +| options.httpCredentials.password | string | `''` | Password to pass to the web browser for Basic HTTP Authentication. | +| options.ignoreHTTPSErrors | boolean | `false` | Whether to ignore HTTPS errors that may be caused by invalid certificates. | +| options.isMobile | boolean | `false` | Whether to simulate a mobile device. | +| options.javaScriptEnabled | boolean | `true` | Whether to activate JavaScript support for the context. | +| options.locale | string | system | Specifies the user's locale, such as `'en-US'`, `'tr-TR'`, etc. | +| options.offline | boolean | `false` | Whether to emulate an offline network. | +| options.permissions | Array | `null` | Permissions to grant for the context's pages. See [browserContext.grantPermissions()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/grantpermissions) for the options. | +| options.reducedMotion | string | `'no-preference'` | Minimizes the amount of motion by emulating the 'prefers-reduced-motion' media feature. It can be one of `'reduce'` and `'no-preference'`. See [page.emulateMedia()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/emulatemedia) for the options. | +| options.screen | object | `{'width': 1280, 'height': 720}` | Sets a window screen size for all pages in the context. It can only be used when the viewport is set. | +| options.screen.width | number | `1280` | Page width in pixels. | +| options.screen.height | number | `720` | Page height in pixels. | +| options.timezoneID | string | system | Changes the context's timezone. See [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) for a list of supported timezone IDs. | +| options.userAgent | string | browser | Specifies the user agent to use in the context. | +| options.viewport | object | `{'width': 1280, 'height': 720}` | Sets a viewport size for all pages in the context. `null` disables the default viewport. | +| options.viewport.width | number | `1280` | Page width in pixels. | +| options.viewport.height | number | `720` | Page height in pixels. | + + + +### Returns + +| Type | Description | +| ------ | ------------------------------------------------------------------------------------------------------------------------ | +| object | [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) object | + +### deviceScaleFactor example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext({ + viewport: { + width: 375, + height: 812, + }, + deviceScaleFactor: 3, + }); + const page = context.newPage(); + + try { + await page.goto('https://test.k6.io/'); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/newpage.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/newpage.md new file mode 100644 index 000000000..92f5e525a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/newpage.md @@ -0,0 +1,100 @@ +--- +title: 'newPage([options])' +description: 'Browser module: newPage method' +--- + +# newPage([options]) + +Creates and returns a new [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/) in a new [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) if a `BrowserContext` hasn't already been initialized for the [Browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser). If a `BrowserContext` has already been initialized an error is thrown. + +{{% admonition type="note" %}} + +A 1-to-1 mapping between [Browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser) and `BrowserContext` means you cannot run `BrowserContexts` concurrently. Due to this restriction, if one already exists, it must be [retrieved](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/context) and [close](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/close)d first before creating a new one. + +{{% /admonition %}} + +{{% admonition type="caution" %}} + +Pages that have been opened ought to be closed using [`Page.close`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/close/). Pages left open could potentially distort the results of Web Vital metrics. + +{{% /admonition %}} + + + +| Parameter | Type | Default | Description | +| ------------------------------------------- | ------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.bypassCSP | boolean | `false` | Whether to bypass a page's Content-Security-Policy. | +| options.colorScheme | string | `'light'` | Whether to display a page in dark or light mode by emulating the 'prefers-colors-scheme' media feature. It can be one of `'light'`, `'dark'`, `'no-preference'`. | +| options.deviceScaleFactor | number | `1` | Sets the resolution ratio in physical pixels to the resolution in CSS pixels i.e. if set higher than `1`, then images will look sharper on high pixel density screens. See an [example](#devicescalefactor-example) below. | +| options.extraHTTPHeaders | object | `null` | Contains additional HTTP headers to be sent with every request, where the keys are HTTP headers and values are HTTP header values. | +| options.geolocation | object | `null` | Sets the user's geographical location. | +| options.geolocation.latitude | number | `0` | Latitude should be between `-90` and `90`. | +| options.geolocation.longitude | number | `0` | Longitude should be between `-180` and `180`. | +| options.geolocation.accuracy | number | `0` | Accuracy should only be a non-negative number. Defaults to `0`. | +| options.hasTouch | boolean | `false` | Whether to simulate a device with touch events. | +| options.httpCredentials | object | `null` | Sets the credentials for HTTP authentication using Basic Auth. | +| options.httpCredentials.username | string | `''` | Username to pass to the web browser for Basic HTTP Authentication. | +| options.httpCredentials.password | string | `''` | Password to pass to the web browser for Basic HTTP Authentication. | +| options.ignoreHTTPSErrors | boolean | `false` | Whether to ignore HTTPS errors that may be caused by invalid certificates. | +| options.isMobile | boolean | `false` | Whether to simulate a mobile device. | +| options.javaScriptEnabled | boolean | `true` | Whether to activate JavaScript support for the context. | +| options.locale | string | system | Specifies the user's locale, such as `'en-US'`, `'tr-TR'`, etc. | +| options.offline | boolean | `false` | Whether to emulate an offline network. | +| options.permissions | Array | `null` | Permissions to grant for the context's pages. See [browserContext.grantPermissions()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/grantpermissions) for the options. | +| options.reducedMotion | string | `'no-preference'` | Minimizes the amount of motion by emulating the 'prefers-reduced-motion' media feature. It can be one of `'reduce'` and `'no-preference'`. See [page.emulateMedia()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/emulatemedia) for the options. | +| options.screen | object | `{'width': 1280, 'height': 720}` | Sets a window screen size for all pages in the context. It can only be used when the viewport is set. | +| options.screen.width | number | `1280` | Page width in pixels. | +| options.screen.height | number | `720` | Page height in pixels. | +| options.timezoneID | string | system | Changes the context's timezone. See [ICU's metaZones.txt](https://cs.chromium.org/chromium/src/third_party/icu/source/data/misc/metaZones.txt?rcl=faee8bc70570192d82d2978a71e2a615788597d1) for a list of supported timezone IDs. | +| options.userAgent | string | browser | Specifies the user agent to use in the context. | +| options.viewport | object | `{'width': 1280, 'height': 720}` | Sets a viewport size for all pages in the context. `null` disables the default viewport. | +| options.viewport.width | number | `1280` | Page width in pixels. | +| options.viewport.height | number | `720` | Page height in pixels. | + + + +### Returns + +| Type | Description | +| ------ | ---------------------------------------------------------------------------------------------------- | +| object | [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/) object | + +### deviceScaleFactor example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage({ + viewport: { + width: 375, + height: 812, + }, + deviceScaleFactor: 3, + }); + + try { + await page.goto('https://test.k6.io/'); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/_index.md new file mode 100644 index 000000000..b9bf00a45 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/_index.md @@ -0,0 +1,74 @@ +--- +title: 'Page' +description: 'Browser module: Page Class' +weight: 10 +--- + +# Page + +Page provides methods to interact with a single tab in a running web browser. A single [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) can have many `pages`. + +| Method | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [page.bringToFront()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/bringtofront) | Activates a browser tab. | +| [page.check(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/check/) | Select the input checkbox. | +| [page.click(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/click/) | Clicks on an element matching a `selector`. | +| [page.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/close) | Closes a tab that the `page` is associated with. | +| [page.content()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/content) | Gets the HTML contents of the page. | +| [page.context()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/context) | Gets the [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) that the page belongs to. | +| [page.dblclick(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/dblclick/) | With the [Mouse](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/mouse), double click on an element matching the provided `selector`. | +| [page.dispatchEvent(selector, type, eventInit[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/dispatchevent/) | Dispatches HTML DOM event types e.g. `'click'` | +| [page.$(selector)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/page-dollar) | Finds an element matching the specified `selector` within the page. | +| [page.$$(selector)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/page-doubledollar) | Finds all elements matching the specified `selector` within the page. | +| [page.emulateMedia([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/emulatemedia/) | Changes the CSS media type and the color scheme feature. | +| [page.emulateVisionDeficiency(type)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/emulatevisiondeficiency) | Emulates your website with the specified vision deficiency `type`. | +| [page.evaluate(pageFunction[, arg])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/evaluate/) | Returns the value of the `pageFunction` invocation. | +| [page.evaluateHandle(pageFunction[, arg])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/evaluatehandle/) | Returns the value of the `pageFunction` invocation as a [JSHandle](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/jshandle). | +| [page.fill(selector, value[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/fill/) | Fill an `input`, `textarea` or `contenteditable` element with the provided value. | +| [page.focus(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/focus/) | Fetches an element with `selector` and focuses on it. | +| [page.frames()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/frames) | Returns an array of frames on the page. | +| [page.getAttribute(selector, name[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/getattribute/) | Returns the element attribute value for the given attribute name. | +| [page.goto(url[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/goto/) | Navigates to the specified `url`. | +| [page.hover(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/hover/) | Hovers over an element matching `selector`. | +| [page.innerHTML(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/innerhtml/) | Returns the `element.innerHTML`. | +| [page.innerText(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/innertext/) | Returns the `element.innerText`. | +| [page.inputValue(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/inputvalue/) | Returns `input.value` for the selected `input`, `textarea` or `select` element. | +| [page.isChecked(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/ischecked/) | Checks to see if the `checkbox` `input` type is selected or not. | +| [page.isClosed()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/isclosed) | Checks if the page has been closed. | +| [page.isDisabled(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/isdisabled/) | Checks if the element is `disabled`. | +| [page.isEditable(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/iseditable/) | Checks if the element is `editable`. | +| [page.isEnabled(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/isenabled/) | Checks if the element is `enabled`. | +| [page.isHidden(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/ishidden/) | Checks if the element is `hidden`. | +| [page.isVisible(selector[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/isvisible/) | Checks if the element is `visible`. | +| [page.keyboard](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/keyboard) | Returns the [Keyboard](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/keyboard) instance to interact with a virtual keyboard on the page. | +| [page.locator(selector)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/locator) | Returns a [Locator](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator) for the given `selector`. | +| [page.mainFrame()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/mainframe) | Returns the page's main [Frame](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/frame). | +| [page.mouse](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/mouse) | Returns the [Mouse](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/mouse) instance to interact with a virtual mouse on the page. | +| [page.on(event, handler)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/on) | Registers a handler to be called whenever the specified event occurs. | +| [page.opener()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/opener) | Returns the `page` that opened the current `page`. | +| [page.press(selector, key[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/press/) | Focuses the element, and then presses the given `key` on the [Keyboard](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/keyboard). | +| [page.reload([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/reload/) | Reloads the current page. | +| [page.screenshot([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/screenshot/) | Returns a buffer with the captured screenshot from the web browser. | +| [page.selectOption(selector, values[, options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/selectoption/) | Selects one or more options which match the values from a `` element. + + + +| Parameter | Type | Default | Description | +| ------------------- | ---------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| values | string or string[] or object | `''` | If the `select` has the multiple attribute, all matching options are selected, otherwise only the first option matching one of the passed options is selected. Object can be made up of keys with `value`, `label` or `index`. | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.strict | boolean | `false` | When `true`, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| -------- | ----------------------------- | +| string[] | List of the selected options. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + page.selectOption('#numbers-options', 'three'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setcontent.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setcontent.md new file mode 100644 index 000000000..c4ce08626 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setcontent.md @@ -0,0 +1,58 @@ +--- +title: 'setContent(html[, options])' +description: 'Browser module: page.setContent(html[, options]) method' +--- + +# setContent(html[, options]) + + + +Sets the supplied HTML string to the current page. + + + +| Parameter | Type | Default | Description | +| ----------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| html | string | `''` | HTML markup to assign to the page. | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum operation time in milliseconds. Pass `0` to disable the timeout. The default value can be changed via the [browserContext.setDefaultNavigationTimeout(timeout)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/setdefaultnavigationtimeout/), [browserContext.setDefaultTimeout(timeout)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/setdefaulttimeout/), [page.setDefaultNavigationTimeout(timeout)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/setdefaultnavigationtimeout/) or [page.setDefaultTimeout(timeout)](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/setdefaulttimeout/) methods. Setting the value to `0` will disable the timeout. | +| options.waitUntil | string | `load` | When to consider operation to have succeeded. See Events for more details. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + const htmlContent = ` + + + Test + Test + + `; + + page.setContent(htmlContent); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setdefaultnavigationtimeout.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setdefaultnavigationtimeout.md new file mode 100644 index 000000000..da3b59b9a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setdefaultnavigationtimeout.md @@ -0,0 +1,47 @@ +--- +title: 'setDefaultNavigationTimeout(timeout)' +description: 'Browser module: page.setDefaultNavigationTimeout(timeout) method' +--- + +# setDefaultNavigationTimeout(timeout) + +This setting will change the navigation timeout for the following methods: + +- [page.goto(url, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/goto/) +- [page.reload([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/reload/) +- [page.setContent(html, [options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/setcontent/) +- [page.waitForNavigation([options])](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/waitfornavigation/) + +| Parameter | Type | Default | Description | +| --------- | ------ | ------- | ------------------------ | +| timeout | number | | Timeout in milliseconds. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + page.setDefaultNavigationTimeout(60000); + await page.goto('https://test.k6.io/browser.php'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setdefaulttimeout.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setdefaulttimeout.md new file mode 100644 index 000000000..8c7a9fd22 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setdefaulttimeout.md @@ -0,0 +1,42 @@ +--- +title: 'setDefaultTimeout(timeout)' +description: 'Browser module: page.setDefaultTimeout(timeout) method' +--- + +# setDefaultTimeout(timeout) + +This setting will change the timeout for all the methods accepting a `timeout` option. + +| Parameter | Type | Default | Description | +| --------- | ------ | ------- | ------------------------ | +| timeout | number | | Timeout in milliseconds. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + page.setDefaultTimeout(60000); + await page.goto('https://test.k6.io/browser.php'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setextrahttpheaders.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setextrahttpheaders.md new file mode 100644 index 000000000..e9ae3e14b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setextrahttpheaders.md @@ -0,0 +1,44 @@ +--- +title: 'setExtraHTTPHeaders(headers)' +description: 'Browser module: page.setExtraHTTPHeaders(headers) method' +--- + +# setExtraHTTPHeaders(headers) + +This sets extra HTTP headers which will be sent with subsequent HTTP requests. + +| Parameter | Type | Default | Description | +| --------- | ---------------------- | ------- | ------------------------------------------------------------------------------------ | +| headers | Object | | An object containing the additional HTTP headers. All header values must be strings. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + page.setExtraHTTPHeaders({ foo: 'bar' }); + const url = await page.goto('https://test.k6.io/browser.php'); + + console.log(url.request().headers().foo); // prints bar +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setinputfiles.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setinputfiles.md new file mode 100644 index 000000000..d1473989b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setinputfiles.md @@ -0,0 +1,71 @@ +--- +title: 'setInputFiles(selector, file[, options])' +description: 'Sets the file input element' +--- + +# setInputFiles(selector, file[, options]) + +Sets the file input element's value to the specified files. + +To work with local files on the file system, use the [experimental fs module](https://grafana.com/docs/k6/latest/javascript-api/k6-experimental/fs/) to load and read the file contents. + +| Parameter | Type | Default | Description | +| ------------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| file | object | `null` | This is a required parameter. | +| file.name | string | `''` | The name of the file. For example, `file.txt`. | +| file.mimeType | string | `''` | The type of the file content. For example, `text/plain`. | +| file.buffer | ArrayBuffer | `[]` | Base64 encoded content of the file. | +| options | object | `null` | This is an optional parameter. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import encoding from 'k6/encoding'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + // In this example we create a simple web page with an upload input field. + page.setContent(` + + + + +
+ + +
+ + `); + + // The file is set to the input element with the id "upload". + page.setInputFiles('input[id="upload"]', { + name: 'file.txt', + mimetype: 'text/plain', + buffer: encoding.b64encode('hello world'), + }); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setviewportsize.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setviewportsize.md new file mode 100644 index 000000000..ccad9f26d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/setviewportsize.md @@ -0,0 +1,51 @@ +--- +title: 'setViewportSize(viewportSize)' +description: 'Browser module: page.setViewportSize(viewportSize) method' +--- + +# setViewportSize(viewportSize) + +This will update the page's width and height. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------ | ------- | ------------------------------------------------------------------------------------ | +| viewportSize | Object | | An object containing the additional HTTP headers. All header values must be strings. | +| viewportSize.width | number | | Page width in pixels. | +| viewportSize.height | number | | Page height in pixels. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + page.setViewportSize({ + width: 640, + height: 480, + }); + await page.goto('https://test.k6.io/browser.php'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/tap.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/tap.md new file mode 100644 index 000000000..90664328c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/tap.md @@ -0,0 +1,62 @@ +--- +title: 'tap(selector[, options])' +description: 'Browser module: locator.tap(selector[, options]) method' +--- + +# tap(selector[, options]) + +{{% admonition type="warning" %}} + +Use locator-based [`locator.tap([options])`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/tap/) instead. + +{{% /admonition %}} + +Tap the first element that matches the selector. + + + +| Parameter | Type | Default | Description | +| ------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.modifiers | string[] | `null` | `Alt`, `Control`, `Meta` or `Shift` modifiers keys pressed during the action. If not specified, currently pressed modifiers are used. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.strict | boolean | `false` | When `true`, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + page.tap('#numbers-options'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/textcontent.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/textcontent.md new file mode 100644 index 000000000..e0c7fe5eb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/textcontent.md @@ -0,0 +1,61 @@ +--- +title: 'textContent(selector[, options])' +description: 'Browser module: locator.textContent(selector[, options]) method' +--- + +# textContent(selector[, options]) + +{{% admonition type="warning" %}} + +Use locator-based [`locator.textContent([options])`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/textcontent/) instead. + +{{% /admonition %}} + +Returns the `element.textContent`. + + + +| Parameter | Type | Default | Description | +| --------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| options | object | `null` | | +| options.strict | boolean | `false` | When `true`, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| ------ | ----------------------------------------- | +| string | The text content of the selector or null. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + console.log(page.textContent('#checkbox1')); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/throttlecpu.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/throttlecpu.md new file mode 100644 index 000000000..f105c1a06 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/throttlecpu.md @@ -0,0 +1,49 @@ +--- +title: 'throttleCPU(cpuProfile)' +description: 'Browser module: page.throttleCPU(cpuProfile) method' +--- + +# throttleCPU(cpuProfile) + +Throttles the CPU in Chrome/Chromium to slow it down by the specified `rate` in `cpuProfile`. + +| Parameter | Type | Default | Description | +| --------------- | ---------- | ------- | -------------------------------------------------------------------- | +| cpuProfile | CPUProfile | `null` | This is a mandatory parameter. | +| cpuProfile.rate | number | `1` | rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc). | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + try { + page.throttleCPU({ rate: 4 }); + + await page.goto('https://test.k6.io/', { waitUntil: 'networkidle' }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/throttlenetwork.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/throttlenetwork.md new file mode 100644 index 000000000..e3b1889ba --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/throttlenetwork.md @@ -0,0 +1,59 @@ +--- +title: 'throttleNetwork(networkProfile)' +description: 'Browser module: page.throttleNetwork(networkProfile) method' +--- + +# throttleNetwork(networkProfile) + +Throttles the network in Chrome/Chromium to slow it down by the specified fields in the `networkProfile` object. + +| Parameter | Type | Default | Description | +| ----------------------- | -------------- | ------- | -------------------------------------------------------------------------------------- | +| networkProfile | NetworkProfile | `null` | This is a mandatory parameter. | +| networkProfile.latency | number | `0` | Minimum latency from request sent to response headers received (ms). | +| networkProfile.download | number | `-1` | Maximal aggregated download throughput (bytes/sec). `-1` disables download throttling. | +| networkProfile.upload | number | `-1` | Maximal aggregated upload throughput (bytes/sec). `-1` disables upload throttling. | + +To work with the most commonly tested network profiles, import `networkProfiles` from the browser module. There are three profiles available: + +| Name | Notes | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `'No Throttling'` | No throttling, which is the default before applying any network throttling. This can be used to remove the network throttling. | +| `'Fast 3G'` | Emulates a typical fast 3G connection | +| `'Slow 3G'` | Emulates a typical slow 3G connection | + +### Example + +{{< code >}} + +```javascript +import { browser, networkProfiles } from 'k6/x/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + try { + page.throttleNetwork(networkProfiles['Slow 3G']); + + await page.goto('https://test.k6.io/', { waitUntil: 'networkidle' }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/title.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/title.md new file mode 100644 index 000000000..9ad60f07e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/title.md @@ -0,0 +1,44 @@ +--- +title: 'title()' +description: 'Browser module: page.title method' +--- + +# title() + +Returns the page's title. + +### Returns + +| Type | Description | +| ------ | ----------------- | +| string | The page's title. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + console.log(page.title()); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/touchscreen.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/touchscreen.md new file mode 100644 index 000000000..f241f4d9b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/touchscreen.md @@ -0,0 +1,44 @@ +--- +title: 'touchScreen' +description: 'Browser module: page.touchScreen method' +--- + +# touchScreen + +Returns the [Touchscreen](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/touchscreen/) instance to interact with a virtual touchscreen on the page. + +### Returns + +| Type | Description | +| ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | +| [Touchscreen](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/touchscreen/) | The `Touchscreen` instance associated with the page. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + page.touchScreen.tap(50, 50); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/type.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/type.md new file mode 100644 index 000000000..104e8c4fb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/type.md @@ -0,0 +1,58 @@ +--- +title: 'type(selector, text[, options])' +description: 'Browser module: page.type(selector, text[, options]) method' +--- + +# type(selector, text[, options]) + +{{% admonition type="warning" %}} + +Use locator-based [`locator.type()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/type/) instead. + +{{% /admonition %}} + +Type the `text` in the first element found that matches the selector. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| text | string | `''` | A text to type into a focused element. | +| options | object | `null` | | +| options.delay | number | `0` | Milliseconds to wait between key presses. Defaults to `0`. | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.strict | boolean | `false` | When `true`, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + page.type('#text1', 'hello world!'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/uncheck.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/uncheck.md new file mode 100644 index 000000000..4d9331794 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/uncheck.md @@ -0,0 +1,62 @@ +--- +title: 'uncheck(selector[, options])' +description: 'Browser module: page.uncheck(selector[, options]) method' +--- + +# uncheck(selector[, options]) + +{{% admonition type="warning" %}} + +Use locator-based [`locator.uncheck([options])`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/uncheck/) instead. + +{{% /admonition %}} + +This method is used to unselect an input checkbox. + + + +| Parameter | Type | Default | Description | +| ------------------- | ------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| options | object | `null` | | +| options.force | boolean | `false` | Setting this to `true` will bypass the actionability checks (`visible`, `stable`, `enabled`). | +| options.noWaitAfter | boolean | `false` | If set to `true` and a navigation occurs from performing this action, it will not wait for it to complete. | +| options.position | object | `null` | A point to use relative to the top left corner of the element. If not supplied, a visible point of the element is used. | +| options.position.x | number | `0` | The x coordinate. | +| options.position.y | number | `0` | The y coordinate. | +| options.strict | boolean | `false` | When `true`, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.trial | boolean | `false` | Setting this to `true` will perform the actionability checks without performing the action. | + + + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + page.check('#checkbox1'); + page.uncheck('#checkbox1'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/url.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/url.md new file mode 100644 index 000000000..f3807fe69 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/url.md @@ -0,0 +1,44 @@ +--- +title: 'url()' +description: 'Browser module: page.url method' +--- + +# url() + +Returns the page's URL. + +### Returns + +| Type | Description | +| ------ | --------------- | +| string | The page's URL. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + console.log(page.url()); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/viewportsize.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/viewportsize.md new file mode 100644 index 000000000..e97f50b92 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/viewportsize.md @@ -0,0 +1,49 @@ +--- +title: 'viewportSize()' +descriptiontion: 'Browser module: page.viewportSize method' +--- + +# viewportSize() + +Returns the page's size (width and height). + +### Returns + +| Type | Description | +| ------ | ------------------------------------------------- | +| Object | An object containing the page's width and height. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + page.setViewportSize({ + width: 640, + height: 480, + }); + await page.goto('https://test.k6.io/browser.php'); + + console.log(page.viewportSize()); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforfunction.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforfunction.md new file mode 100644 index 000000000..0ef0c96f5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforfunction.md @@ -0,0 +1,74 @@ +--- +title: 'waitForFunction(pageFunction, arg[, options])' +description: 'Browser module: page.waitForFunction(pageFunction, arg[, options]) method' +--- + +# waitForFunction(pageFunction, arg[, options]) + +Returns when the `pageFunction` returns a truthy value. + + + +| Parameter | Type | Default | Description | +| --------------- | --------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| pageFunction | function | | Function to be evaluated in the page context. | +| arg | string | `''` | Optional argument to pass to `pageFunction` | +| options | object | `null` | | +| options.polling | number or `raf` | `raf` | If `polling` is `'raf'`, then `pageFunction` is constantly executed in `requestAnimationFrame` callback. If `polling` is a number, then it is treated as an interval in milliseconds at which the function would be executed. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | +| Promise<[JSHandle](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/jshandle/)> | The `JSHandle` instance associated with the page. | + +### Example + +{{< code >}} + + + +```javascript +import { browser } from 'k6/experimental/browser'; +import { check } from 'k6'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + page.evaluate(() => { + setTimeout(() => { + const el = document.createElement('h1'); + el.innerHTML = 'Hello'; + document.body.appendChild(el); + }, 1000); + }); + + const ok = await page.waitForFunction("document.querySelector('h1')", { + polling: 'mutation', + timeout: 2000, + }); + check(ok, { 'waitForFunction successfully resolved': ok.innerHTML() == 'Hello' }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforloadstate.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforloadstate.md new file mode 100644 index 000000000..61a668101 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforloadstate.md @@ -0,0 +1,84 @@ +--- +title: 'waitForLoadState(state[, options])' +description: 'Browser module: page.waitForLoadState(state[, options]) method' +--- + +# waitForLoadState(state[, options]) + +{{% admonition type="caution" %}} + +This method has **known issues**. For details, refer to [#880](https://github.com/grafana/xk6-browser/issues/880). + +{{% /admonition %}} + +This waits for the given load state to be reached. It will immediately unblock if that lifecycle event has already been received. + + + +| Parameter | Type | Default | Description | +| --------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| state | string | `load` | Optional load state to wait for. See [Events](#events) for more details. | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Events + +{{% admonition type="caution" %}} + +`networkidle` is DISCOURAGED. Don't use this method for testing especially with chatty websites where the event may never fire, rely on web assertions to assess readiness instead. + +{{% /admonition %}} + +Events can be either: + +- `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired. +- `'load'` - consider operation to be finished when the `load` event is fired. +- `'networkidle'` - Consider operation to be finished when there are no network connections for at least `500` ms. + +### Example + +{{< code >}} + +```javascript +import { check } from 'k6'; +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + page.locator('input[name="login"]').type('admin'); + page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + await submitButton.click(); + + page.waitForLoadState(); // waits for the default `load` event + + check(page, { + header: (p) => p.locator('h2').textContent() == 'Welcome, admin!', + }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitfornavigation.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitfornavigation.md new file mode 100644 index 000000000..349ef3e7b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitfornavigation.md @@ -0,0 +1,83 @@ +--- +title: 'waitForNavigation([options])' +description: 'Browser module: page.waitForNavigation([options]) method' +--- + +# waitForNavigation([options]) + +Waits for the given navigation lifecycle event to occur and returns the main resource response. + + + +| Parameter | Type | Default | Description | +| ----------------- | ------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| options | object | `null` | | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | +| options.waitUntil | string | `load` | When to consider operation to have succeeded. See [Events](#events) for more details. | + + + +### Events + +{{% admonition type="caution" %}} + +`networkidle` is DISCOURAGED. Don't use this method for testing especially with chatty websites where the event may never fire, rely on web assertions to assess readiness instead. + +{{% /admonition %}} + +Events can be either: + +- `'domcontentloaded'` - consider operation to be finished when the `DOMContentLoaded` event is fired. +- `'load'` - consider operation to be finished when the `load` event is fired. +- `'networkidle'` - Consider operation to be finished when there are no network connections for at least `500` ms. + +### Returns + +| Type | Description | +| ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Promise/javascript-api/k6-experimental/browser/response/)> | The `Response` instance associated with the page. Else, it returns `null` | + +### Example + +{{< code >}} + +```javascript +import { check } from 'k6'; +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + page.locator('input[name="login"]').type('admin'); + page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + + await Promise.all([page.waitForNavigation(), submitButton.click()]); + + check(page, { + header: (p) => p.locator('h2').textContent() == 'Welcome, admin!', + }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforselector.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforselector.md new file mode 100644 index 000000000..e7e5188ad --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitforselector.md @@ -0,0 +1,70 @@ +--- +title: 'waitForSelector(selector[, options])' +description: 'Browser module: page.waitForSelector(selector[, options]) method' +--- + +# waitForSelector(selector[, options]) + +{{% admonition type="note" %}} + +Use web assertions that assert visibility or a locator-based [`locator.waitFor([options])`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/waitfor/) instead. + +{{% /admonition %}} + +Returns when element specified by selector satisfies `state` option. + + + +| Parameter | Type | Default | Description | +| --------------- | ------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | `''` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. | +| options | object | `null` | | +| options.state | string | `visible` | Can be either `attached`, `detached`, `visible`, `hidden` See [Element states](#element-states) for more details. | +| options.strict | boolean | `false` | When `true`, the call requires selector to resolve to a single element. If given selector resolves to more than one element, the call throws an exception. | +| options.timeout | number | `30000` | Maximum time in milliseconds. Pass `0` to disable the timeout. Default is overridden by the `setDefaultTimeout` option on [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/) or [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/). | + + + +### Element states + +Element states can be either: + +- `'attached'` - wait for element to be present in DOM. +- `'detached'` - wait for element to not be present in DOM. +- `'visible'` - wait for element to have non-empty bounding box and no `visibility:hidden`. +- `'hidden'` - wait for element to be either detached from DOM, or have an empty bounding box or `visibility:hidden`. + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | +| null \| [ElementHandle](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/keyboard/) | `ElementHandle` when a matching element is found. Else, it returns `null`. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + await page.goto('https://test.k6.io/browser.php'); + page.waitForSelector('#text1'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitfortimeout.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitfortimeout.md new file mode 100644 index 000000000..91559715b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/waitfortimeout.md @@ -0,0 +1,48 @@ +--- +title: 'waitForTimeout(timeout)' +description: 'Browser module: waitForTimeout(timeout) method' +--- + +# waitForTimeout(timeout) + +{{% admonition type="note" %}} + +Never wait for timeout in production, use this only for debugging. Tests that wait for time are inherently flaky. Use [`Locator`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator/) actions and web assertions that wait automatically. + +{{% /admonition %}} + +Waits for the given `timeout` in milliseconds. + +| Parameter | Type | Default | Description | +| --------- | ------ | ------- | ------------------------ | +| timeout | number | | Timeout in milliseconds. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + page.waitForTimeout(5000); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/workers.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/workers.md new file mode 100644 index 000000000..af978a935 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/page/workers.md @@ -0,0 +1,44 @@ +--- +title: 'workers()' +description: 'Browser module: page.workers method' +--- + +# workers() + +This method returns an array of the dedicated [WebWorkers](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/worker/) associated with the page. + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| [WebWorkers](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/worker/)[] | Array of `WebWorkers` associated with the page. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + await page.goto('https://test.k6.io/browser.php'); + console.log(page.workers()); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/request/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/request/_index.md new file mode 100644 index 000000000..01a41f08c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/request/_index.md @@ -0,0 +1,31 @@ +--- +title: "Request" +descriptiontiontiontion: "Browser module: Request Class" +weight: 11 +weight: 11 +--- + +# Request + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| request.allHeaders() | - | +| request.frame() | - | +| request.headers() | - | +| request.headersArray() | - | +| request.headerValue(name) | - | +| request.isNavigationRequest() | - | +| request.method() | - | +| request.postData() | - | +| request.postDataBuffer() | - | +| request.redirectedFrom() | - | +| request.redirectedTo() | - | +| request.resourceType() | - | +| request.response() | - | +| [request.size()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/request/size) | Unlike Playwright, this method returns an object containing the sizes of request headers and body. | +| request.timing() | - | +| request.url() | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/request/size.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/request/size.md new file mode 100644 index 000000000..81d078d1e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/request/size.md @@ -0,0 +1,14 @@ +--- +title: 'size()' +description: 'Browser module: Request.size method' +--- + +# size() + +Similar to Playwright's [`request.sizes()`](https://playwright.dev/docs/api/class-request#request-sizes), this method returns the size (in bytes) of body and header sections of the [Request](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/request). + +### Returns + +| Type | Description | +| ------ | ------------------------------------- | +| object | `{ body: , headers: }` | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/response.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/response.md new file mode 100644 index 000000000..648cc5f7f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/response.md @@ -0,0 +1,30 @@ +--- +title: 'Response' +description: 'Browser module: Response Class' +weight: 12 +--- + +# Response + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | Description | +| ---------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| response.allHeaders() | - | - | +| response.body() | - | - | +| response.frame() | - | - | +| response.headers() | - | - | +| response.headersArray() | - | - | +| response.headerValue(name) | - | - | +| response.headerValues(name) | - | - | +| response.json() | - | - | +| response.ok() | - | - | +| response.request() | - | - | +| response.securityDetails() | - | - | +| response.serverAddr() | - | - | +| response.status() | - | - | +| response.statusText() | - | - | +| response.size() | - | Similar to [`Request.size()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/request/size), this returns the size of response headers and body sections. | +| response.url() | - | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/touchscreen.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/touchscreen.md new file mode 100644 index 000000000..f019efac6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/touchscreen.md @@ -0,0 +1,15 @@ +--- +title: 'Touchscreen' +description: 'Browser module: Touchscreen Class' +weight: 13 +--- + +# Touchscreen + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| ---------------------------------------------------------------------------------------------------------------------- | -------------------------------- | +| touchscreen.tap(x, y) | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/version.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/version.md new file mode 100644 index 000000000..b8c56d04c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/version.md @@ -0,0 +1,42 @@ +--- +title: 'version()' +description: 'Browser module: version method' +--- + +# version() + +Returns the browser application's version. + +### Returns + +| Type | Description | +| ------ | ---------------------------------- | +| string | The browser application's version. | + +### Example + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default function () { + const version = browser.version(); + console.log(version); // 105.0.5195.52 +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/worker.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/worker.md new file mode 100644 index 000000000..2fa17625e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/browser/worker.md @@ -0,0 +1,17 @@ +--- +title: 'Worker' +description: 'Browser module: Worker Class' +weight: 14 +--- + +# Worker + +{{< docs/shared source="k6" lookup="browser-module-wip.md" version="" >}} + +## Supported APIs + +| Method | Playwright Relevant Distinctions | +| --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | --- | +| worker.url() | - | +| worker.evaluate(pageFunction[, arg]) | - | - | +| worker.evaluateHandle(pageFunction[, arg]) | - | - | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/FileInfo.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/FileInfo.md new file mode 100644 index 000000000..bde24c408 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/FileInfo.md @@ -0,0 +1,39 @@ +--- +title: 'FileInfo' +description: 'FileInfo represents information about a file.' +weight: 30 +--- + +# FileInfo + +The `FileInfo` class represents information about a [file](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file). + +## Properties + +| Property | Type | Description | +| :------- | :----- | :----------------------------- | +| name | string | The name of the file. | +| size | number | The size of the file in bytes. | + +## Example + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + // Retrieve information about the file + const fileinfo = await file.stat(); + if (fileinfo.name != 'bonjour.txt') { + throw new Error('Unexpected file name'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/SeekMode.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/SeekMode.md new file mode 100644 index 000000000..07c03943c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/SeekMode.md @@ -0,0 +1,43 @@ +--- +title: 'SeekMode' +description: 'SeekMode is used to specify the position from which to seek in a file.' +weight: 40 +--- + +# SeekMode + +The `SeekMode` enum specifies the position from which to [seek](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file/seek) in a file. + +## Members + +| Member | Value | Description | +| :------ | :---- | :------------------------------------------ | +| Start | 0 | Seek from the start of the file. | +| Current | 1 | Seek from the current position in the file. | +| End | 2 | Seek from the end of the file. | + +## Example + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + // Seek 6 bytes from the start of the file + await file.seek(6, SeekMode.Start); + + // Seek 2 more bytes from the current position + await file.seek(2, SeekMode.Current); + + // Seek backwards 2 bytes from the end of the file + await file.seek(-2, SeekMode.End); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/_index.md new file mode 100644 index 000000000..122fa81a2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/_index.md @@ -0,0 +1,84 @@ +--- +title: 'fs' +description: 'k6 fs experimental API' +weight: 10 +--- + +# fs + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +The k6 filesystem experimental module provides a memory-efficient way to handle file interactions within your test scripts. It currently offers support for opening files, reading their content, seeking through their content, and retrieving metadata about them. + +### Memory efficiency + +One of the key advantages of the filesystem module is its memory efficiency. Unlike the traditional [open](https://grafana.com/docs/k6//javascript-api/init-context/open/) function, which loads a file multiple times into memory, the filesystem module reduces memory usage by loading the file as little as possible and sharing the same memory space between all VUs. This approach reduces the risk of encountering out-of-memory errors, especially in load tests involving large files. + +### Notes on usage + +An important consideration when using the filesystem module is its handling of external file modifications. Once k6 loads a file, it behaves like a "view" over its contents. If you modify the underlying file during a test, k6 will not reflect those changes in the loaded [File](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file/) instance. + +## API Overview + +The module exports functions and objects to interact with the file system: + +| Function/Object | Description | +| ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| [open](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/open) | Opens a file and returns a promise resolving to a `File` instance. | +| [File](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file) | Represents a file with methods for reading, seeking, and obtaining file stats. | +| [SeekMode](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/seekmode) | Enum for specifying the reference point for seek operations. Includes `Start`, `Current`, and `End`. | + +## Example + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +// k6 doesn't support async in the init context. We use a top-level async function for `await`. +// +// Each Virtual User gets its own `file` copy. +// So, operations like `seek` or `read` won't impact other VUs. +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + // About information about the file + const fileinfo = await file.stat(); + if (fileinfo.name != 'bonjour.txt') { + throw new Error('Unexpected file name'); + } + + const buffer = new Uint8Array(4); + + let totalBytesRead = 0; + while (true) { + // Read into the buffer + const bytesRead = await file.read(buffer); + if (bytesRead == null) { + // EOF + break; + } + + // Do something useful with the content of the buffer + totalBytesRead += bytesRead; + + // If bytesRead is less than the buffer size, we've read the whole file + if (bytesRead < buffer.byteLength) { + break; + } + } + + // Check that we read the expected number of bytes + if (totalBytesRead != fileinfo.size) { + throw new Error('Unexpected number of bytes read'); + } + + // Seek back to the beginning of the file + await file.seek(0, SeekMode.Start); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/_index.md new file mode 100644 index 000000000..f91d7b826 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/_index.md @@ -0,0 +1,74 @@ +--- +title: 'File' +description: 'File represents a file with methods for reading, seeking, and obtaining file stats.' +weight: 10 +--- + +# File + +The `File` class represents a file with methods for reading, seeking, and obtaining file stats. It's returned by the [open](https://grafana.com/docs/k6//javascript-api/init-context/open/) function. + +## Properties + +| Property | Type | Description | +| :------- | :----- | :----------------------------- | +| path | string | The absolute path to the file. | + +## Methods + +| Method | Description | +| :------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [read](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file/read) | Reads up to `buffer.byteLength` bytes from the file into the passed `buffer`. Returns a promise resolving to the number of bytes read. | +| [seek](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file/seek) | Sets the file position indicator for the file to the passed `offset` bytes. Returns a promise resolving to the new offset. | +| [stat](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file/stat) | Returns a promise resolving to a [FileInfo](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/fileinfo/) object with information about the file. | + +## Example + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + // About information about the file + const fileinfo = await file.stat(); + if (fileinfo.name != 'bonjour.txt') { + throw new Error('Unexpected file name'); + } + + const buffer = new Uint8Array(4); + + let totalBytesRead = 0; + while (true) { + // Read into the buffer + const bytesRead = await file.read(buffer); + if (bytesRead == null) { + // EOF + break; + } + + // Do something useful with the content of the buffer + totalBytesRead += bytesRead; + + // If bytesRead is less than the buffer size, we've read the whole file + if (bytesRead < buffer.byteLength) { + break; + } + } + + // Check that we read the expected number of bytes + if (totalBytesRead != fileinfo.size) { + throw new Error('Unexpected number of bytes read'); + } + + // Seek back to the beginning of the file + await file.seek(0, SeekMode.Start); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/read.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/read.md new file mode 100644 index 000000000..7e285145a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/read.md @@ -0,0 +1,109 @@ +--- +title: 'read' +description: 'the read method is used to read a chunk of the file.' +weight: 20 +--- + +# read + +The `read` method is used to read a chunk of the file into an [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) buffer. + +It resolves to either the number of bytes read during the operation, or `null` if there was nothing more to read. + +## Parameters + +| Parameter | Type | Description | +| :-------- | :-------------------------------------------------------------------------------------------------------- | :-------------------------------- | +| buffer | [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) | The buffer to read the data into. | + +## Returns + +A [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving to the number of bytes read or `null` if the end of the file has been reached. + +## Example + +### Reading a file + +In the following example, we open a file and read it in chunks of 128 bytes until we reach the end of the file. + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + const buffer = new Uint8Array(128); + + let totalBytesRead = 0; + while (true) { + // Read into the buffer + const bytesRead = await file.read(buffer); + if (bytesRead == null) { + // EOF + break; + } + + // Do something useful with the content of the buffer + totalBytesRead += bytesRead; + + // If bytesRead is less than the buffer size, we've read the whole file + if (bytesRead < buffer.byteLength) { + break; + } + } + + // Seek back to the beginning of the file + await file.seek(0, SeekMode.Start); +} +``` + +{{< /code >}} + +### `readAll` helper function + +The following helper function can be used to read the entire contents of a file into a [Uint8Array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array) buffer. + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +async function readAll(file) { + const fileInfo = await file.stat(); + const buffer = new Uint8Array(fileInfo.size); + + const bytesRead = await file.read(buffer); + if (bytesRead !== fileInfo.size) { + throw new Error( + 'unexpected number of bytes read; expected ' + + fileInfo.size + + ' but got ' + + bytesRead + + ' bytes' + ); + } + + return buffer; +} + +export default async function () { + // Read the whole file + const fileContent = await readAll(file); + console.log(JSON.stringify(fileContent)); + + // Seek back to the beginning of the file + await file.seek(0, SeekMode.Start); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/seek.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/seek.md new file mode 100644 index 000000000..654b49dc1 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/seek.md @@ -0,0 +1,52 @@ +--- +title: 'seek' +description: 'seek sets the file position indicator for the file to the passed offset bytes.' +weight: 30 +--- + +# seek + +The `seek` method sets the file position indicator for the file to the passed `offset` bytes, under the mode given by `whence`. The call resolves to the new position within the resource (bytes from the start). + +Based on the [SeekMode](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/seekmode) passed, the offset is interpreted as follows: + +- when using `SeekMode.Start`, the offset must be greater than or equal to zero. +- when using `SeekMode.Current`, the offset can be positive or negative. +- when using `SeekMode.End`, the offset must be less than or equal to zero. + +## Parameters + +| Parameter | Type | Description | +| :-------- | :---------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | +| offset | number | The offset in bytes from the position specified by `whence`. | +| whence | [SeekMode](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/seekmode) | The position from which the offset is applied. | + +## Returns + +A [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving to the new offset within the file. + +## Example + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + // Seek 6 bytes from the start of the file + await file.seek(6, SeekMode.Start); + + // Seek 2 more bytes from the current position + await file.seek(2, SeekMode.Current); + + // Seek backwards 2 bytes from the end of the file + await file.seek(-2, SeekMode.End); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/stat.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/stat.md new file mode 100644 index 000000000..a7cd8be43 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/file/stat.md @@ -0,0 +1,38 @@ +--- +title: 'stat' +description: 'stat returns a promise resolving to a FileInfo object with information about the file.' +weight: 40 +--- + +# stat + +The `stat` method returns a promise resolving to a [FileInfo](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/fileinfo) object with information about the file. + +## Returns + +A [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) resolving to a [FileInfo](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/fileinfo) object with information about the file. + +## Examples + +{{< code >}} + +```javascript +import { open, SeekMode } from 'k6/experimental/fs'; + +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + // About information about the file + const fileinfo = await file.stat(); + if (fileinfo.name != 'bonjour.txt') { + throw new Error('Unexpected file name'); + } + + console.log(JSON.stringify(fileinfo)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/open.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/open.md new file mode 100644 index 000000000..9875a3dc9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/fs/open.md @@ -0,0 +1,63 @@ +--- +title: 'open' +description: 'open opens a file and returns a promise resolving to a File instance.' +weight: 20 +--- + +# open + +The `open` function opens a file and returns a promise that resolves to a [File](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file) instance. Unlike the traditional [open](https://grafana.com/docs/k6//javascript-api/init-context/open/) function, which loads a file multiple times into memory, the filesystem module reduces memory usage by loading the file as little possible, and sharing the same memory space between all VUs. This approach reduces the risk of encountering out-of-memory errors, especially in load tests involving large files. + +### Asynchronous nature + +It's important to note that `open` is asynchronous and returns a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). Due to k6's current limitation with the Init context (which doesn't support asynchronous functions directly), you need to use an asynchronous wrapper like this: + +{{< code >}} + +```javascript +let file; +(async function () { + file = await open('bonjour.txt'); +})(); +``` + +{{< /code >}} + +## Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------------------------------------------------- | +| path | string | The path to the file to open. Relative paths are resolved relative to the k6 script. | + +## Returns + +A promise resolving to a [File](https://grafana.com/docs/k6//javascript-api/k6-experimental/fs/file) instance. + +## Example + +{{< code >}} + +```javascript +import { open } from 'k6/experimental/fs'; + +// k6 doesn't support async in the init context. We use a top-level async function for `await`. +// +// Each Virtual User gets its own `file` copy. +// So, operations like `seek` or `read` won't impact other VUs. +let file; +(async function () { + file = await open('bonjour.txt'); +})(); + +export default async function () { + // About information about the file + const fileinfo = await file.stat(); + if (fileinfo.name != 'bonjour.txt') { + throw new Error('Unexpected file name'); + } + + console.log(JSON.stringify(fileinfo)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/_index.md new file mode 100644 index 000000000..dc41a14c5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/_index.md @@ -0,0 +1,65 @@ +--- +title: 'grpc' +description: 'Experimental GRPC module' +weight: 02 +--- + +# grpc + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +The `k6/experimental/grpc` module is an extension of the [`k6/net/grpc`](https://grafana.com/docs/k6//javascript-api/k6-net-grpc). It provides a [gRPC](https://grpc.io/) client for Remote Procedure Calls (RPC) over HTTP/2. + +Starting on k6 v0.49, the `k6/net/grpc` module provides streaming support. We recommend using the `k6/net/grpc` module instead of `k6/experimental/grpc`. + +| Class/Method | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Client](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client) | gRPC client used for making RPC calls to a gRPC Server. | +| [Client.load(importPaths, ...protoFiles)](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-load) | Loads and parses the given protocol buffer definitions to be made available for RPC requests. | +| [Client.connect(address [,params])](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-connect) | Connects to a given gRPC service. | +| [Client.invoke(url, request [,params])](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-invoke) | Makes a unary RPC for the given service/method and returns a [Response](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/response). | +| [Client.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-close) | Close the connection to the gRPC service. | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/params) | RPC Request specific options. | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/response) | Returned by RPC requests. | +| [Constants](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/constants) | Define constants to distinguish between [gRPC Response](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/response) statuses. | +| [Stream](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream) | Creates a new GRPC stream. | +| [Stream.on(event, handler)](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream/stream-on) | Adds a new listener to one of the possible stream event's. | +| [Stream.write(message)](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream/stream-write) | Writes a message to the stream. | +| [Stream.end()](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream/stream-end) | Signals to server that client finished sending. | + +## Metrics + +k6 takes specific measurements for gRPC requests. +For the complete list, refer to the [Metrics reference](https://grafana.com/docs/k6//using-k6/metrics/reference#grpc). + +### Example + +{{< code >}} + +```javascript +import grpc from 'k6/experimental/grpc'; +import { check, sleep } from 'k6'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { + // plaintext: false + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + console.log(JSON.stringify(response.message)); + + client.close(); + sleep(1); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/_index.md new file mode 100644 index 000000000..636589151 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/_index.md @@ -0,0 +1,107 @@ +--- +title: Client +description: 'Client is a gRPC client that can interact with a gRPC server.' +weight: 10 +--- + +# Client + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +`Client` is a gRPC client that can interact with a gRPC server. + +| Method | Description | +| -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Client.load(importPaths, ...protoFiles)](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-load) | Loads and parses the given protocol buffer definitions to be made available for RPC requests. | +| [Client.loadProtoset(protosetPath)](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-loadprotoset) | Loads and parses the given protoset file to be made available for RPC requests. | +| [Client.connect(address [,params])](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-connect) | Opens a connection to the given gRPC server. | +| [Client.invoke(url, request [,params])](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-invoke) | Makes a unary RPC for the given service/method and returns a [Response](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/response). | +| [Client.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-close) | Close the connection to the gRPC service. | + +### Examples + +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); +// Download addsvc.proto for https://grpcbin.test.k6.io/, located at: +// https://raw.githubusercontent.com/moul/pb/master/addsvc/addsvc.proto +// and put it in the same folder as this script. +client.load(null, 'addsvc.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { timeout: '5s' }); + + const response = client.invoke('addsvc.Add/Sum', { + a: 1, + b: 2, + }); + console.log(response.message.v); // should print 3 + + client.close(); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/experimental/grpc'; +import { check } from 'k6'; + +const client = new grpc.Client(); +client.load([], 'authorization.proto', 'route_guide.proto'); + +export function setup() { + client.connect('auth.googleapis.com:443'); + const resp = client.invoke('google.cloud.authorization.v1.AuthService/GetAccessToken', { + username: 'john.smith@k6.io', + password: 'its-a-secret', + }); + client.close(); + return resp.message.accessToken; +} + +export default (token) => { + client.connect('route.googleapis.com:443'); + const metadata = { + authorization: `bearer ${token}`, + }; + const response = client.invoke( + 'google.cloud.route.v1.RoutingService/GetFeature', + { + latitude: 410248224, + longitude: -747127767, + }, + { metadata } + ); + check(response, { 'status is OK': (r) => r && r.status === grpc.StatusOK }); + client.close(); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/experimental/grpc'; +import { check } from 'k6'; + +const client = new grpc.Client(); +client.load([], 'language_service.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('language.googleapis.com:443'); + } + const response = client.invoke('google.cloud.language.v1.LanguageService/AnalyzeSentiment', {}); + check(response, { 'status is OK': (r) => r && r.status === grpc.StatusOK }); + // Do NOT close the client +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-close.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-close.md new file mode 100644 index 000000000..bd9bb8dd4 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-close.md @@ -0,0 +1,29 @@ +--- +title: 'Client.close()' +description: 'Close the connection to the gRPC service. Tear down all underlying connections.' +weight: 40 +--- + +# Client.close() + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Close the connection to the gRPC service. Tear down all underlying connections. + +### Examples + +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('localhost:8080'); + client.close(); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-connect-connect-address-params.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-connect-connect-address-params.md new file mode 100644 index 000000000..fe5722dfa --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-connect-connect-address-params.md @@ -0,0 +1,135 @@ +--- +title: 'Client.connect(address [,params])' +slug: 'client-connect' +description: 'Opens a connection to a gRPC server; will block until a connection is made or a connection error is thrown.' +weight: 20 +--- + +# Client.connect(address [,params]) + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Opens a connection to a gRPC server; will block until a connection is made or a connection error is thrown. Cannot be called during the [`init` phase](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +See [Client.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-close) to close the connection. + +| Parameter | Type | Description | +| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| address | string | The address of the gRPC server. Should be in the form: `host:port` with no protocol prefix e.g. `grpc.k6.io:443`. The host must be a literal IP address, or a host name that can be resolved to IP addresses. The port must be a literal port number or a service name e.g. `:443` or `:https`. If the host is a literal IPv6 address it must be enclosed in square brackets, as in `[2001:db8::1]:80` or `[fe80::1%zone]:80`. | +| params (optional) | object | [ConnectParams](#connectparams) object containing additional connect parameters. | + +## ConnectParams + +| Name | Type | Description | +| ------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ConnectParams.plaintext` | bool | If `true` will connect to the gRPC server using plaintext i.e. insecure. Defaults to `false` i.e. secure via TLS. | +| `ConnectParams.reflect` | boolean | Whether to use the [gRPC server reflection protocol](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) when connecting. | +| `ConnectParams.reflectMetadata` | object | Object with key-value pairs representing custom metadata the user would like to add to the reflection request. | +| `ConnectParams.timeout` | string / number | Connection timeout to use. Default timeout is `"60s"`.
The type can also be a number, in which case k6 interprets it as milliseconds, e.g., `60000` is equivalent to `"60s"`. | +| `ConnectParams.maxReceiveSize` | number | Sets the maximum message size in bytes the client can receive. Defaults to `grpc-go` default, which is 4MB. | +| `ConnectParams.maxSendSize` | number | Sets the maximum message size in bytes the client can send. Defaults to `grpc-go` default, which is approximately 2GB. | +| `ConnectParams.tls` (optional) | object | [TLS](#tls) settings of the connection. Defaults to `null`. | + +## TLS + +TLS settings of the connection. If not defined, the main TLS config from options will be used. + +| Name | Type | Description | +| -------------- | -------------- | ----------------------------------------------------- | +| `tls.cert` | string | PEM formatted client certificate. | +| `tls.key` | string | PEM formatted client private key. | +| `tls.password` | string | Password for decrypting the client's private key. | +| `tls.cacerts` | string / array | PEM formatted strings of the certificate authorities. | + +### Examples + +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); + +export default () => { + client.connect('localhost:8080'); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); + +export default () => { + client.connect('localhost:8080', { plaintext: true }); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/experimental/grpc'; +import { check } from 'k6'; +import { SharedArray } from 'k6/data'; +import exec from 'k6/execution'; + +// note: the services in this example don't exist. If you would like +// to run this example, make sure to replace the URLs, and +// the cacerts, cert, key, and password variables. +const grpcArgs = new SharedArray('grpc', () => { + // Using SharedArray here so that not every VU gets a copy of every certificate a key + return [ + { + host: 'foo1.grpcbin.test.k6.io:9001', + plaintext: false, + params: { + tls: { + cacerts: [open('cacerts0.pem')], + cert: open('cert0.pem'), + key: open('key0.pem'), + }, + }, + }, + { + host: 'foo2.grpcbin.test.k6.io:9002', + params: { + plaintext: false, + tls: { + cacerts: open('cacerts1.pem'), + cert: open('cert1.pem'), + key: open('key1.pem'), + password: 'cert1-passphrase', + }, + }, + }, + ]; +}); + +const client = new grpc.Client(); + +export default () => { + if (__ITER === 0) { + // Take one config and use it for this one VU + const grpcArg = grpcArgs[exec.vu.idInTest % grpcArgs.length]; + client.connect(grpcArg.host, grpcArg.params); + } + + const response = client.invoke('hello.HelloService/SayHello', { + greeting: 'Bert', + }); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + console.log(JSON.stringify(response.message)); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-invoke.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-invoke.md new file mode 100644 index 000000000..65f72861b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-invoke.md @@ -0,0 +1,55 @@ +--- +title: 'Client.invoke(url, request [,params])' +description: 'Invokes an unary RPC request to the given method.' +weight: 30 +--- + +# Client.invoke(url, request [,params]) + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Invokes an unary RPC request to the given method. + +The given method to invoke must have its RPC schema previously loaded via the [Client.load()](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-load) function, otherwise an +error will be thrown. + +[Client.connect()](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/client/client-connect) must be called first before invoking a request, otherwise an error will be thrown. + +| Parameter | Type | Description | +| ----------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| url | string | The gRPC method url to invoke, in the form `/package.Service/Method`, e.g. `/google.cloud.language.v1.LanguageService/AnalyzeSentiment`. The leading slash `/` is optional. | +| request | object | The canonical request object, as-per the [Protobuf JSON Mapping](https://developers.google.com/protocol-buffers/docs/proto3#json). | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| ---------- | -------------------------------------------------------------------------------------------------------------- | +| `Response` | gRPC [Response](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/response) object. | + +### Examples + +
+ +```javascript +import grpc from 'k6/experimental/grpc'; +import { check } from 'k6'; + +const client = new grpc.Client(); +client.load([], 'routeguide.proto'); + +export default () => { + client.connect('localhost:10000', { plaintext: true }); + const response = client.invoke('main.RouteGuide/GetFeature', { + latitude: 410248224, + longitude: -747127767, + }); + check(response, { 'status is OK': (r) => r && r.status === grpc.StatusOK }); + console.log(response.message.name); + // output: 3 Hasta Way, Newton, NJ 07860, USA + + client.close(); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-load-protoset.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-load-protoset.md new file mode 100644 index 000000000..b04d19acc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-load-protoset.md @@ -0,0 +1,31 @@ +--- +title: 'Client.loadProtoset(protosetPath)' +slug: 'client-loadprotoset' +description: 'Loads and parses the protoset file (serialized FileDescriptor set) so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema.' +weight: 11 +--- + +# Client.loadProtoset(protosetPath) + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Loads and parses the protoset file (serialized FileDescriptor set) so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema. + +Must be called within the [`init` phase](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +| Parameter | Type | Description | +| ------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| protosetPath | string | The path of the protoset file. If no import paths are provided then "." (current directory) is assumed to be the only import path. | + +### Examples + +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); +client.loadProtoset('./dummy.protoset'); +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-load.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-load.md new file mode 100644 index 000000000..789539690 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/client/client-load.md @@ -0,0 +1,48 @@ +--- +title: 'Client.load(importPaths, ...protoFiles)' +description: 'Loads and parses the protocol buffer descriptors so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema.' +weight: 10 +--- + +# Client.load(importPaths, ...protoFiles) + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Loads and parses the protocol buffer descriptors so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema. + +Must be called within the [`init` phase](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +| Parameter | Type | Description | +| ----------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| importPaths | Array<string> \| `null` | The paths used to search for dependencies that are referenced in import statements in proto source files. If no import paths are provided then "." (current directory) is assumed to be the only import path. | +| protoFiles | Array<string> | [Rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) for the list of proto files to load/parse. | + +### Examples + +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); +client.load([], 'language_service.proto'); +``` + +
+ +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); + +client.load( + ['../googleapis/google'], + 'spanner/admin/instance/v1/spanner_instance_admin.proto', + 'spanner/admin/instance/v1/spanner_instance_admin.proto', + 'spanner/v1/spanner.proto' +); +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/constants.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/constants.md new file mode 100644 index 000000000..682c3322a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/constants.md @@ -0,0 +1,61 @@ +--- +title: 'Constants' +description: 'Define constants to distinguish between gRPC Response' +weight: 40 +--- + +# Constants + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Define constants to distinguish between [gRPC Response](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/response) statuses. + +| Constant | Description | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `StatusOK` | OK is returned on success. | +| `StatusCanceled` | Canceled indicates the operation was canceled (typically by the caller). | +| `StatusUnknown` | Unknown error. | +| `StatusInvalidArgument` | InvalidArgument indicates the client specified an invalid argument. | +| `StatusDeadlineExceeded` | DeadlineExceeded means operation expired before completion. | +| `StatusNotFound` | NotFound means some requested entity (e.g., file or directory) was not found. | +| `StatusAlreadyExists` | AlreadyExists means an attempt to create an entity failed because one already exists. | +| `StatusPermissionDenied` | PermissionDenied indicates the caller does not have permission to execute the specified operation. | +| `StatusResourceExhausted` | ResourceExhausted indicates some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. | +| `StatusFailedPrecondition` | FailedPrecondition indicates operation was rejected because the system is not in a state required for the operation's execution. | +| `StatusAborted` | Aborted indicates the operation was aborted, typically due to a concurrency issue like sequencer check failures, transaction aborts, etc. | +| `StatusOutOfRange` | OutOfRange means operation was attempted past the valid range. E.g., seeking or reading past end of file. | +| `StatusUnimplemented` | Unimplemented indicates operation is not implemented or not supported/enabled in this service. | +| `StatusInternal` | Internal errors. Means some invariants expected by the underlying system have been broken. | +| `StatusUnavailable` | Unavailable indicates the service is currently unavailable. This is a most likely a transient condition and may be corrected by retrying with a backoff. Note that it is not always safe to retry non-idempotent operations. | +| `StatusDataLoss` | DataLoss indicates unrecoverable data loss or corruption. | +| `StatusUnauthenticated` | Unauthenticated indicates the request does not have valid authentication credentials for the operation. | + +### Example + +{{< code >}} + +```javascript +import grpc from 'k6/experimental/grpc'; +import { check, sleep } from 'k6'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { + // plaintext: false + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + client.close(); + sleep(1); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/params.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/params.md new file mode 100644 index 000000000..f175261ce --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/params.md @@ -0,0 +1,46 @@ +--- +title: 'Params' +head_title: 'gRPC.params' +description: 'Params is an object used by the gRPC methods that generate RPC requests.' +weight: 20 +--- + +# Params + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +_Params_ is an object used by the gRPC methods that generate RPC requests. _Params_ contains request-specific options like headers that should be inserted into the request. + +| Name | Type | Description | +| ----------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Params.metadata` | object | Object with key-value pairs representing custom metadata the user would like to add to the request. Values of [keys ending with `-bin`](https://grpc.io/docs/what-is-grpc/core-concepts/#metadata) will be treated as binary data. | +| `Params.tags` | object | Key-value pairs where the keys are names of tags and the values are tag values. Response time metrics generated as a result of the request will have these tags added to them, allowing the user to filter out those results specifically, when looking at results data. | +| `Params.timeout` | string / number | Request timeout to use. Default timeout is 60 seconds (`"60s"`).
The type can also be a number, in which case k6 interprets it as milliseconds, e.g., `60000` is equivalent to `"60s"`. | + +### Example of custom metadata headers and tags + +
+ +```javascript +import grpc from 'k6/experimental/grpc'; + +const client = new grpc.Client(); +client.load([], 'route_guide.proto'); + +export default function () { + const req = { + latitude: 410248224, + longitude: -747127767, + }; + const params = { + metadata: { + 'x-my-header': 'k6test', + 'x-my-header-bin': new Uint8Array([1, 2, 3]), + }, + tags: { k6test: 'yes' }, + }; + const response = client.invoke('main.RouteGuide/GetFeature', req, params); +} +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/response.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/response.md new file mode 100644 index 000000000..b98850748 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/response.md @@ -0,0 +1,48 @@ +--- +title: 'Response' +head_title: 'gRPC.Response' +description: 'The response object of a gRPC request.' +weight: 30 +--- + +# Response + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +| Name | Type | Description | +| ------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Response.status` | number | The response gRPC status code. Use the gRPC [status constants](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/constants) to check equality. | +| `Response.message` | object | The successful protobuf message, serialized to JSON. Will be `null` if `status !== grpc.StatusOK`. | +| `Response.headers` | object | Key-value pairs representing all the metadata headers returned by the gRPC server. | +| `Response.trailers` | object | Key-value pairs representing all the metadata trailers returned by the gRPC server. | +| `Response.error` | object | If `status !== grpc.StatusOK` then the error protobuf message, serialized to JSON; otherwise `null`. | + +### Example + +{{< code >}} + +```javascript +import grpc from 'k6/experimental/grpc'; +import { check, sleep } from 'k6'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { + // plaintext: false + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + client.close(); + sleep(1); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/_index.md new file mode 100644 index 000000000..0767df9af --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/_index.md @@ -0,0 +1,206 @@ +--- +title: Stream +description: 'GRPC Streams' +weight: 30 +--- + +# Stream + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Using a GRPC client creates a stream. An important note that the client should be already connected (client.connect called) to the server before creating a stream. + +| Method | Description | +| -------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| [Stream(client, url, [,params])](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream) | Using a GRPC client creates a stream. | +| [Stream.write(message)](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream/stream-write) | Writes a message to the stream. | +| [Stream.on(event, handler)](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream/stream-on) | Set up handler functions for various events on the GRPC stream. | +| [Stream.end()](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream/stream-end) | Signals to the server that the client has finished sending. | + +### Examples + +_A k6 script that sends several randomly chosen points from the pre-generated feature database with a variable delay in between. Prints the statistics when they are sent from the server._ + +{{< code >}} + +```javascript +import { Client, Stream } from 'k6/experimental/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const GRPC_ADDR = __ENV.GRPC_ADDR || '127.0.0.1:10000'; +const GRPC_PROTO_PATH = __ENV.GRPC_PROTO_PATH || '../../grpc_server/route_guide.proto'; + +const client = new Client(); +client.load([], GRPC_PROTO_PATH); + +// a sample DB of points +const DB = [ + { + location: { latitude: 407838351, longitude: -746143763 }, + name: 'Patriots Path, Mendham, NJ 07945, USA', + }, + { + location: { latitude: 408122808, longitude: -743999179 }, + name: '101 New Jersey 10, Whippany, NJ 07981, USA', + }, + { + location: { latitude: 413628156, longitude: -749015468 }, + name: 'U.S. 6, Shohola, PA 18458, USA', + }, + { + location: { latitude: 419999544, longitude: -740371136 }, + name: '5 Conners Road, Kingston, NY 12401, USA', + }, + { + location: { latitude: 414008389, longitude: -743951297 }, + name: 'Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA', + }, + { + location: { latitude: 419611318, longitude: -746524769 }, + name: '287 Flugertown Road, Livingston Manor, NY 12758, USA', + }, + { + location: { latitude: 406109563, longitude: -742186778 }, + name: '4001 Tremley Point Road, Linden, NJ 07036, USA', + }, + { + location: { latitude: 416802456, longitude: -742370183 }, + name: '352 South Mountain Road, Wallkill, NY 12589, USA', + }, + { + location: { latitude: 412950425, longitude: -741077389 }, + name: 'Bailey Turn Road, Harriman, NY 10926, USA', + }, + { + location: { latitude: 412144655, longitude: -743949739 }, + name: '193-199 Wawayanda Road, Hewitt, NJ 07421, USA', + }, +]; + +export default () => { + if (__ITER == 0) { + client.connect(GRPC_ADDR, { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + console.log('Passed', stats.featureCount, 'features'); + console.log('Travelled', stats.distance, 'meters'); + console.log('It took', stats.elapsedTime, 'seconds'); + }); + + stream.on('error', (err) => { + console.log('Stream Error: ' + JSON.stringify(err)); + }); + + stream.on('end', () => { + client.close(); + console.log('All done'); + }); + + // send 5 random items + for (let i = 0; i < 5; i++) { + const point = DB[Math.floor(Math.random() * DB.length)]; + pointSender(stream, point); + } + + // close the client stream + stream.end(); + + sleep(1); +}; + +const pointSender = (stream, point) => { + console.log( + 'Visiting point ' + + point.name + + ' ' + + point.location.latitude / COORD_FACTOR + + ', ' + + point.location.longitude / COORD_FACTOR + ); + + // send the location to the server + stream.write(point.location); + + sleep(0.5); +}; +``` + +{{< /code >}} + +_A k6 script that sends a rectangle message and results (features) are streamed back to the client._ + +{{< code >}} + +```javascript +import { Client, Stream } from 'k6/experimental/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const GRPC_ADDR = __ENV.GRPC_ADDR || '127.0.0.1:10000'; +const GRPC_PROTO_PATH = __ENV.GRPC_PROTO_PATH || '../../grpc_server/route_guide.proto'; + +const client = new Client(); + +client.load([], GRPC_PROTO_PATH); + +export default () => { + client.connect(GRPC_ADDR, { plaintext: true }); + + const stream = new Stream(client, 'main.FeatureExplorer/ListFeatures', null); + + stream.on('data', function (feature) { + console.log( + 'Found feature called "' + + feature.name + + '" at ' + + feature.location.latitude / COORD_FACTOR + + ', ' + + feature.location.longitude / COORD_FACTOR + ); + }); + + stream.on('end', function () { + // The server has finished sending + client.close(); + console.log('All done'); + }); + + stream.on('error', function (e) { + // An error has occurred and the stream has been closed. + console.log('Error: ' + JSON.stringify(e)); + }); + + // send a message to the server + stream.write({ + lo: { + latitude: 400000000, + longitude: -750000000, + }, + hi: { + latitude: 420000000, + longitude: -730000000, + }, + }); + + sleep(0.5); +}; +``` + +{{< /code >}} + +The preceding examples use a demo server, which you can run with the following command (Golang should be installed) in [k6 repository's root](https://github.com/grafana/k6): + +{{< code >}} + +```bash +$ go run -mod=mod examples/grpc_server/*.go +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-end.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-end.md new file mode 100644 index 000000000..7d26e8141 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-end.md @@ -0,0 +1,51 @@ +--- +title: 'Stream.end()' +description: 'Signals to the server that the client has finished sending.' +weight: 40 +--- + +# Stream.end() + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Signals to the server that the client has finished sending messages. + +### Example + +
+ +```javascript +import { Client, Stream } from 'k6/experimental/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const client = new Client(); +client.load([], '../../grpc_server/route_guide.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + console.log('Passed', stats.featureCount, 'features'); + console.log('Traveled', stats.distance, 'meters'); + console.log('It took', stats.elapsedTime, 'seconds'); + }); + + // send 2 items + stream.write({ latitude: 406109563, longitude: -742186778 }); + stream.write({ latitude: 416802456, longitude: -742370183 }); + + // send end-signal to the server + stream.end(); + + sleep(1); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-error.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-error.md new file mode 100644 index 000000000..62a6279e9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-error.md @@ -0,0 +1,18 @@ +--- +title: 'Error' +head_title: 'gRPC.Error' +description: 'The error object of a gRPC stream.' +weight: 15 +--- + +# Error + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +The error object is the object that is passed to the `error` event handler function. + +| Name | Type | Description | +| --------------- | ------ | ------------------------------------- | +| `Error.code` | number | A gRPC error code. | +| `Error.details` | array | A list details attached to the error. | +| `Error.message` | string | An original error message. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-on.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-on.md new file mode 100644 index 000000000..9404d5b96 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-on.md @@ -0,0 +1,71 @@ +--- +title: 'Stream.on()' +description: 'Set up handler functions for various events on the GRPC stream.' +weight: 10 +--- + +# Stream.on() + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Set up handler functions for various events on the GRPC stream. + +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------- | +| event | string | The event name to define a handler for. | +| handler | function | The function to call when the event happens. | + +Possible events: + +| Event name | Description | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| data | Emitted when the server sends data. | +| error | Emitted when an error occurs. In case of the error, an [`Error`](https://grafana.com/docs/k6//javascript-api/k6-experimental/grpc/stream/stream-error) object sends to the handler function. | +| end | Emitted when the server closes the incoming stream. | + +### Example + +
+ +```javascript +import { Client, Stream } from 'k6/experimental/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const client = new Client(); +client.load([], '../../grpc_server/route_guide.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + // sets up a handler for the data (server sends data) event + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + console.log('Passed', stats.featureCount, 'features'); + console.log('Traveled', stats.distance, 'meters'); + console.log('It took', stats.elapsedTime, 'seconds'); + }); + + // sets up a handler for the end event (stream closes) + stream.on('end', function () { + // The server has finished sending + client.close(); + console.log('All done'); + }); + + // sets up a handler for the error event (an error occurs) + stream.on('error', function (e) { + // An error has occurred and the stream has been closed. + console.log('Error: ' + JSON.stringify(e)); + }); + + sleep(1); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-write.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-write.md new file mode 100644 index 000000000..89fddedf0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/grpc/stream/stream-write.md @@ -0,0 +1,47 @@ +--- +title: 'Stream.write()' +description: 'Writes a message to the stream.' +weight: 40 +--- + +# Stream.write() + +{{< docs/shared source="k6" lookup="experimental-grpc-module.md" version="" >}} + +Writes a message to the stream. + +### Example + +
+ +```javascript +import { Client, Stream } from 'k6/experimental/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const client = new Client(); +client.load([], '../../grpc_server/route_guide.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + }); + + // send an item + stream.write({ latitude: 406109563, longitude: -742186778 }); + + // send end-signal to the server + stream.end(); + + sleep(1); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/_index.md new file mode 100644 index 000000000..6145d3fd3 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/_index.md @@ -0,0 +1,35 @@ +--- +title: "redis" +description: "k6 Redis experimental API" +weight: 02 +weight: 02 +--- + +# redis + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +The [Redis](https://redis.io/) module provides a client library that makes it possible to interact with Redis directly from a k6 script. +With this module, you can: + +- Load test Redis +- Use Redis as a data store for test-script logic. + +Though the API intends to be thorough and extensive, it does not expose the whole Redis API. +Instead, the intent is to expose Redis for use cases most appropriate to k6. + +| Class | Description | +| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Client](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client) | Client that exposes allowed interactions with Redis. | +| [Options](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/redis-options) | Options used to configure the behavior of the [Redis Client](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client). | + +### Notes on usage + +The `Client` exposes a [promise](https://javascript.info/promise-basics)-based API. +Unlike most other current k6 modules and extensions, +which operate in a synchronous manner, +the Redis `Client` operates in an asynchronous manner. +In practice, this means that using the Redis `Client`'s methods won't block test execution, +and that the test will continue to run even if the Redis `Client` isn't ready to respond to the request. +The `async` and `await` keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains +(for details, refer to the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)). diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/_index.md new file mode 100644 index 000000000..4d0bf1d4f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/_index.md @@ -0,0 +1,352 @@ +--- +title: 'Client' +description: 'Client is a Redis client to interact with a Redis server, cluster, or sentinel.' +weight: 10 +weight: 10 +--- + +# Client + +`Client` is a [Redis](https://redis.io) client to interact with a Redis server, sentinel, or cluster. It exposes a promise-based API, which users can interact with in an asynchronous manner. + +Though the API intends to be thorough and extensive, it does not expose the whole Redis API. Instead, the intent is to expose Redis for use cases most appropriate to k6. + +## Usage + +### Single-node server + +You can create a new `Client` instance that connects to a single Redis server by passing a URL string. +It must be in the format: + +``` +redis[s]://[[username][:password]@][host][:port][/db-number] +``` + +Here's an example of a URL string that connects to a Redis server running on localhost, on the default port (6379), and using the default database (0): + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client('redis://localhost:6379'); +``` + +{{< /code >}} + +A client can also be instantiated using an [options](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/redis-options) object to support more complex use cases, and for more flexibility: + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client({ + socket: { + host: 'localhost', + port: 6379, + }, + username: 'someusername', + password: 'somepassword', +}); +``` + +{{< /code >}} + +### TLS + +You can configure a TLS connection in a couple of ways. + +If the server has a certificate signed by a public Certificate Authority, you can use the `rediss` URL scheme: + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client('rediss://example.com'); +``` + +{{< /code >}} + +Otherwise, you can supply your own self-signed certificate in PEM format using the [socket.tls](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/redis-options#tls-configuration-options-tlsoptions) object: + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client({ + socket: { + host: 'localhost', + port: 6379, + tls: { + ca: [open('ca.crt')], + }, + }, +}); +``` + +{{< /code >}} + +Note that for self-signed certificates, k6's [insecureSkipTLSVerify](https://grafana.com/docs/k6//using-k6/k6-options/reference/#insecure-skip-tls-verify) option must be enabled (set to `true`). + +#### TLS client authentication (mTLS) + +You can also enable mTLS by setting two additional properties in the [socket.tls](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/redis-options#tls-configuration-options-tlsoptions) object: + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client({ + socket: { + host: 'localhost', + port: 6379, + tls: { + ca: [open('ca.crt')], + cert: open('client.crt'), // client certificate + key: open('client.key'), // client private key + }, + }, +}); +``` + +{{< /code >}} + +### Cluster client + +You can connect to a cluster of Redis servers by using the [cluster](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/redis-options#redis-cluster-options-clusteroptions) configuration property, and passing 2 or more node URLs: + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client({ + cluster: { + // Cluster options + maxRedirects: 3, + readOnly: true, + routeByLatency: true, + routeRandomly: true, + nodes: ['redis://host1:6379', 'redis://host2:6379'], + }, +}); +``` + +{{< /code >}} + +Or the same as above, but passing [socket](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/redis-options#socket-connection-options-socketoptions) objects to the nodes array instead of URLs: + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client({ + cluster: { + nodes: [ + { + socket: { + host: 'host1', + port: 6379, + }, + }, + { + socket: { + host: 'host2', + port: 6379, + }, + }, + ], + }, +}); +``` + +{{< /code >}} + +### Sentinel client + +A [Redis Sentinel](https://redis.io/docs/management/sentinel/) provides high availability features, as an alternative to a Redis cluster. + +You can connect to a sentinel instance by setting additional [options](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/redis-options) in the object passed to the `Client` constructor: + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +const client = new redis.Client({ + username: 'someusername', + password: 'somepassword', + socket: { + host: 'localhost', + port: 6379, + }, + // Sentinel options + masterName: 'masterhost', + sentinelUsername: 'sentineluser', + sentinelPassword: 'sentinelpass', +}); +``` + +{{< /code >}} + +## Real world example + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; +import redis from 'k6/experimental/redis'; +import exec from 'k6/execution'; +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; + +export const options = { + scenarios: { + redisPerformance: { + executor: 'shared-iterations', + vus: 10, + iterations: 200, + exec: 'measureRedisPerformance', + }, + usingRedisData: { + executor: 'shared-iterations', + vus: 10, + iterations: 200, + exec: 'measureUsingRedisData', + }, + }, +}; + +// Instantiate a new redis client +const redisClient = new redis.Client(`redis://localhost:6379`); + +// Prepare an array of crocodile ids for later use +// in the context of the measureUsingRedisData function. +const crocodileIDs = new Array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + +export async function measureRedisPerformance() { + // VUs are executed in a parallel fashion, + // thus, to ensure that parallel VUs are not + // modifying the same key at the same time, + // we use keys indexed by the VU id. + const key = `foo-${exec.vu.idInTest}`; + + await redisClient.set(key, 1); + await redisClient.incrBy(key, 10); + const value = await redisClient.get(key); + if (value !== '11') { + throw new Error('foo should have been incremented to 11'); + } + + await redisClient.del(key); + if ((await redisClient.exists(key)) !== 0) { + throw new Error('foo should have been deleted'); + } +} + +export async function setup() { + await redisClient.sadd('crocodile_ids', ...crocodileIDs); +} + +export async function measureUsingRedisData() { + // Pick a random crocodile id from the dedicated redis set, + // we have filled in setup(). + const randomID = await redisClient.srandmember('crocodile_ids'); + const url = `https://test-api.k6.io/public/crocodiles/${randomID}`; + const res = await http.asyncRequest('GET', url); + + check(res, { 'status is 200': (r) => r.status === 200 }); + + await redisClient.hincrby('k6_crocodile_fetched', url, 1); +} + +export async function teardown() { + await redisClient.del('crocodile_ids'); +} + +export function handleSummary(data) { + redisClient + .hgetall('k6_crocodile_fetched') + .then((fetched) => Object.assign(data, { k6_crocodile_fetched: fetched })) + .then((data) => redisClient.set(`k6_report_${Date.now()}`, JSON.stringify(data))) + .then(() => redisClient.del('k6_crocodile_fetched')); + + return { + stdout: textSummary(data, { indent: ' ', enableColors: true }), + }; +} +``` + +{{< /code >}} + +## API + +### Key value methods + +| Method | Redis command | Description | +| :-------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------- | :-------------------------------------------------------------------------- | +| [`Client.set(key, value, expiration)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-set) | **[SET](https://redis.io/commands/set)** | Set `key` to hold `value`, with a time to live equal to `expiration`. | +| [`Client.get(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-get) | **[GET](https://redis.io/commands/get)** | Get the value of `key`. | +| [`Client.getSet(key, value)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-getset) | **[GETSET](https://redis.io/commands/getset)** | Atomically sets `key` to `value` and returns the old value stored at `key`. | +| [`Client.del(keys)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-del) | **[DEL](https://redis.io/commands/del)** | Removes the specified keys. | +| [`Client.getDel(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-getdel) | **[GETDEL](https://redis.io/commands/getdel)** | Get the value of `key` and delete the key. | +| [`Client.exists(keys)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-exists) | **[EXISTS](https://redis.io/commands/exists)** | Returns the number of `key` arguments that exist. | +| [`Client.incr(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-incr) | **[INCR](https://redis.io/commands/incr)** | Increments the number stored at `key` by one. | +| [`Client.incrBy(key, increment)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-incrby) | **[INCRBY](https://redis.io/commands/incrby)** | Increments the number stored at `key` by `increment`. | +| [`Client.decr(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-decr) | **[DECR](https://redis.io/commands/decr)** | Decrements the number stored at `key` by one. | +| [`Client.decrBy(key, decrement)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-decrby) | **[DECRBY](https://redis.io/commands/decrby)** | Decrements the number stored at `key` by `decrement`. | +| [`Client.randomKey()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-randomkey) | **[RANDOMKEY](https://redis.io/commands/randomkey)** | Returns a random key's value. | +| [`Client.mget(keys)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-mget) | **[MGET](https://redis.io/commands/mget)** | Returns the values of all specified keys. | +| [`Client.expire(key, seconds)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-expire) | **[EXPIRE](https://redis.io/commands/expire)** | Sets a timeout on key, after which the key will automatically be deleted. | +| [`Client.ttl(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-ttl) | **[TTL](https://redis.io/commands/ttl)** | Returns the remaining time to live of a key that has a timeout. | +| [`Client.persist(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-persist) | **[PERSIST](https://redis.io/commands/persist)** | Removes the existing timeout on key. | + +### List methods + +| Method | Redis command | Description | +| :-------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------- | :------------------------------------------------------------------------------ | +| [`Client.lpush(key, values)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-lpush) | **[LPSUH](https://redis.io/commands/lpush)** | Inserts all the specified values at the head of the list stored at `key`. | +| [`Client.rpush(key, values)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-rpush) | **[RPUSH](https://redis.io/commands/rpush)** | Inserts all the specified values at the tail of the list stored at `key`. | +| [`Client.lpop(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-lpop) | **[LPOP](https://redis.io/commands/lpop)** | Removes and returns the first element of the list stored at `key`. | +| [`Client.rpop(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-rpop) | **[RPOP](https://redis.io/commands/rpop)** | Removes and returns the last element of the list stored at `key`. | +| [`Client.lrange(key, start, stop)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-lrange) | **[LRANGE](https://redis.io/commands/lrange)** | Returns the specified elements of the list stored at `key`. | +| [`Client.lindex(key, start, stop)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-lindex) | **[LINDEX](https://redis.io/commands/lindex)** | Returns the specified element of the list stored at `key`. | +| [`Client.lset(key, index, element)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-lset) | **[LSET](https://redis.io/commands/lset)** | Sets the list element at `index` to `element`. | +| [`Client.lrem(key, count, value)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-lrem) | **[LREM](https://redis.io/commands/lrem)** | Removes the first `count` occurrences of `value` from the list stored at `key`. | +| [`Client.llen(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-llen) | **[LLEN](https://redis.io/commands/llen)** | Returns the length of the list stored at `key`. | + +### Hash methods + +| Method | Redis command | Description | +| :--------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------- | :--------------------------------------------------------------------------------------------------- | +| [`Client.hset(key, field, value)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hset) | **[HSET](https://redis.io/commands/hset)** | Sets the specified field in the hash stored at `key` to `value`. | +| [`Client.hsetnx(key, field, value)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hsetnx) | **[HSETNX](https://redis.io/commands/hsetnx)** | Sets the specified field in the hash stored at `key` to `value`, only if `field` does not yet exist. | +| [`Client.hget(key, field)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hget) | **[HGET](https://redis.io/commands/hget)** | Returns the value associated with `field` in the hash stored at `key`. | +| [`Client.hdel(key, fields)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hdel) | **[HDEL](https://redis.io/commands/hdel)** | Deletes the specified fields from the hash stored at `key`. | +| [`Client.hgetall(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hgetall) | **[HGETALL](https://redis.io/commands/hgetall)** | Returns all fields and values of the hash stored at `key`. | +| [`Client.hkeys(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hkeys) | **[HKEYS](https://redis.io/commands/hkeys)** | Returns all fields of the hash stored at `key`. | +| [`Client.hvals(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hvals) | **[HVALS](https://redis.io/commands/hvals)** | Returns all values of the hash stored at `key`. | +| [`Client.hlen(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hlen) | **[HLEN](https://redis.io/commands/hlen)** | Returns the number of fields in the hash stored at `key`. | +| [`Client.hincrby(key, field, increment)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-hincrby) | **[HINCRBY](https://redis.io/commands/hincrby)** | Increments the integer value of `field` in the hash stored at `key` by `increment`. | + +### Set methods + +| Method | Redis command | Description | +| :--------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------- | :----------------------------------------------------------------------- | +| [`Client.sadd(key, members)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-sadd) | **[SADD](https://redis.io/commands/sadd)** | Adds the specified members to the set stored at `key`. | +| [`Client.srem(key, members)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-srem) | **[SREM](https://redis.io/commands/srem)** | Removes the specified members from the set stored at `key`. | +| [`Client.sismember(key, member)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-sismember) | **[SISMEMBER](https://redis.io/commands/sismember)** | Returns if member is a member of the set stored at `key`. | +| [`Client.smembers(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-smembers) | **[SMEMBERS](https://redis.io/commands/smembers)** | Returns all the members of the set values stored at `keys`. | +| [`Client.srandmember(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-srandmember) | **[SRANDMEMBER](https://redis.io/commands/srandmember)** | Returns a random element from the set value stored at `key`. | +| [`Client.spop(key)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-spop) | **[SPOP](https://redis.io/commands/spop)** | Removes and returns a random element from the set value stored at `key`. | + +### Miscellaneous + +| Method | Description | +| :--------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------- | +| [`Client.sendCommand(command, args)`](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client/client-sendcommand) | Send a command to the Redis server. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-decr.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-decr.md new file mode 100644 index 000000000..685dea8f2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-decr.md @@ -0,0 +1,47 @@ +--- +title: 'Client.decr(key)' +description: 'Decrements the number stored at `key` by one.' +--- + +# Client.decr(key) + +Decrements the number stored at `key` by one. If the key does not exist, it is set to zero before performing the operation. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :-------------------- | +| `key` | string | the key to decrement. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of `key` after the decrement. | If the key contains a value of the wrong type, or contains a string that cannot be represented as an integer, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 10, 0); + + let value; + value = await redisClient.incr('mykey'); + value = await redisClient.incrBy('mykey', value); + value = await redisClient.decrBy('mykey', value); + value = await redisClient.decr('mykey'); + + if (value !== -1) { + throw new Error('mykey should have been -1'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-decrby.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-decrby.md new file mode 100644 index 000000000..65b20088e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-decrby.md @@ -0,0 +1,43 @@ +--- +title: 'Client.decrBy(key, decrement)' +description: 'Decrements the number stored at `key` by `decrement`.' +--- + +# Client.decrBy(key, decrement) + +Decrements the number stored at `key` by `decrement`. If the key does not exist, it is set to zero before performing the operation. + +### Parameters + +| Parameter | Type | Description | +| :---------- | :----- | :------------------------- | +| `key` | string | the key to decrement. | +| `decrement` | number | the amount to decrement by | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of `key` after the decrement. | If the key contains a value of the wrong type, or contains a string that cannot be represented as an integer, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 10, 0); + + const value = await redisClient.decrBy('mykey', 2); + if (value !== 8) { + throw new Error('mykey should have been 8'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-del.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-del.md new file mode 100644 index 000000000..92d2e61b0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-del.md @@ -0,0 +1,47 @@ +--- +title: 'Client.del(keys)' +description: '' +--- + +# Client.del(keys) + +Removes the specified keys. A key is ignored if it does not exist. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------- | :------------------ | +| `keys` | string[] | the keys to delete. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the `number` of keys that were removed. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'myvalue', 0); + + const exists = await redisClient.exists('mykey'); + if (exists === false) { + throw new Error('mykey should exist'); + } + + const value = await redisClient.get('mykey'); + console.log(`set key 'mykey' to value: ${value}`); + + await redisClient.del('mykey'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-exists.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-exists.md new file mode 100644 index 000000000..f568447de --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-exists.md @@ -0,0 +1,49 @@ +--- +title: 'Client.exists(keys)' +description: 'Returns the number of `key` arguments that exist.' +--- + +# Client.exists(keys) + +Returns the number of `key` arguments that exist. Note that if the same existing key is mentioned in the argument multiple times, it will be counted multiple times. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------- | :---------------------------------- | +| `keys` | string[] | the keys to check the existence of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :------------------------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the `number` of keys that exist from those specified as arguments. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + let exists = await redisClient.exists('mykey'); + if (exists === true) { + throw new Error('mykey should not exist'); + } + + await redisClient.set('mykey', 'myvalue', 0); + + exists = await redisClient.exists('mykey'); + if (exists === false) { + throw new Error('mykey should exist'); + } + + await redisClient.del('mykey'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-expire.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-expire.md new file mode 100644 index 000000000..dff437a70 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-expire.md @@ -0,0 +1,44 @@ +--- +title: 'Client.expire(key, seconds)' +description: 'Sets an expiration date (a timeout) on the key `key`.' +--- + +# Client.expire(key, seconds) + +Sets a timeout on key, after which the key will automatically be deleted. Note that calling Expire with a non-positive timeout will result in the key being deleted rather than expired. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :--------------------------------------------- | +| `key` | string | the key to set the expiration of. | +| `seconds` | number | the value in seconds to set the expiration to. | + +### Returns + +| Type | Resolves with | Rejected when | +| :----------------- | :---------------------------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with `true` if the timeout was set, and `false` if the timeout wasn't set. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'myvalue', 10); + await redisClient.expire('mykey', 100); + + const ttl = await redisClient.ttl('mykey'); + if (ttl <= 10 || ttl >= 100) { + throw new Error('mykey should have a ttl of 10 <= x < 100'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-get.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-get.md new file mode 100644 index 000000000..ce1420be9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-get.md @@ -0,0 +1,47 @@ +--- +title: 'Client.get(key)' +description: 'Get the value of `key`.' +--- + +# Client.get(key) + +Get the key's value. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :------------------------- | +| `key` | string | the name of the key to get | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :------------------------------------------------------------ | :---------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of the `key`. | If the key does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'myvalue', 0); + + const exists = await redisClient.exists('mykey'); + if (exists === false) { + throw new Error('mykey should exist'); + } + + const value = await redisClient.get('mykey'); + console.log(`set key 'mykey' to value: ${value}`); + + await redisClient.del('mykey'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-getdel.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-getdel.md new file mode 100644 index 000000000..9326ab9ad --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-getdel.md @@ -0,0 +1,43 @@ +--- +title: 'Client.getDel(key)' +description: 'Get the value of `key` and delete the key.' +--- + +# Client.getDel(key) + +Get the value of `key` and delete the key. This functionality is similar to `get`, except for the fact that it also deletes the key on success. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :------------------------ | +| `key` | string | the key to get and delete | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :-------------------------------------------------------- | :---------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of `key`. | If the key does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'oldvalue', 0); + let value = await redisClient.getSet('mykey', 'newvalue'); + + value = await redisClient.getDel('mykey'); + if (value !== 'newvalue') { + throw new Error('mykey should have been newvalue'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-getset.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-getset.md new file mode 100644 index 000000000..07cbc08e0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-getset.md @@ -0,0 +1,40 @@ +--- +title: 'Client.getSet(key, value)' +description: 'Atomically sets `key` to `value` and returns the old value stored at `key`.' +--- + +# Client.getSet(key, value) + +Atomically sets `key` to `value` and returns the value previously stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------------------------- | :--------------------- | +| `key` | string | the key to get and set | +| `value` | string, number, or boolean | the value to set | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the old value stored at `key`. | If `key` does not exist, or does not hold a string value, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'oldvalue', 0); + await redisClient.getSet('mykey', 'newvalue'); + await redisClient.getDel('mykey'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hdel.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hdel.md new file mode 100644 index 000000000..781877bbb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hdel.md @@ -0,0 +1,40 @@ +--- +title: 'Client.hdel(key, fields)' +description: 'Deletes fields from the hash stored at `key`.' +--- + +# Client.hdel(key, fields) + +Deletes the specified fields from the hash stored at `key`. The number of fields that were removed from the hash is returned on resolution (non including non existing fields). + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------- | :-------------------------------------------- | +| `key` | string | key holding the hash to delete the fields of. | +| `fields` | string[] | fields to delete from the hash. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the number of fields that were removed from the hash, not including specified, but non existing, fields. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 'myvalue'); + await redisClient.hget('myhash', 'myfield'); + await redisClient.hdel('myhash', 'myfield'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hget.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hget.md new file mode 100644 index 000000000..0839f1e9b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hget.md @@ -0,0 +1,40 @@ +--- +title: 'Client.hget(key, field)' +description: 'Returns the value of field in the hash stored at `key`.' +--- + +# Client.hget(key, field) + +Returns the value associated with `field` in the hash stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :---------------------------------------- | +| `key` | string | key holding the hash to get the field of. | +| `field` | string | field to get from the hash. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :----------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value associated with `field`. | If the hash does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 'myvalue'); + await redisClient.hget('myhash', 'myfield'); + await redisClient.hdel('myhash', 'myfield'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hgetall.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hgetall.md new file mode 100644 index 000000000..e9a3aedcc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hgetall.md @@ -0,0 +1,40 @@ +--- +title: 'Client.hgetall(key)' +description: 'Returns all fields and values of the hash stored at `key`.' +--- + +# Client.hgetall(key) + +Returns all fields and values of the hash stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------- | +| `key` | string | key holding the hash to get the fields of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :----------------------------- | :-------------------------------------------------------------------------------------------- | :------------ | +| `Promise<[key: string]string>` | On success, the promise resolves with the list of fields and their values stored in the hash. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 'myvalue'); + await redisClient.hset('myhash', 'myotherfield', 'myothervalue'); + const object = await redisClient.hgetall('myhash'); + console.log(`myhash has key:value pairs ${JSON.stringify(object)}`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hincrby.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hincrby.md new file mode 100644 index 000000000..54a95bafa --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hincrby.md @@ -0,0 +1,40 @@ +--- +title: 'Client.hincrby(key, field, increment)' +description: 'Increments the value of a hash field by the given number.' +--- + +# Client.hincrby(key, field, increment) + +Increments the integer value of `field` in the hash stored at `key` by `increment`. If `key` does not exist, a new key holding a hash is created. If `field` does not exist the value is set to 0 before the operation is set to 0 before the operation is performed. + +### Parameters + +| Parameter | Type | Description | +| :---------- | :----- | :---------------------------------------------- | +| `key` | string | key holding the hash to increment the field of. | +| `field` | string | field to increment in the hash. | +| `increment` | number | amount to increment the field by. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the value at `field` after the increment operation. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 10); + await redisClient.hincrby('myhash', 'myfield', 20); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hkeys.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hkeys.md new file mode 100644 index 000000000..2dbe8ec1b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hkeys.md @@ -0,0 +1,45 @@ +--- +title: 'Client.hkeys(key)' +description: 'Returns all fields of the hash stored at `key`.' +--- + +# Client.hkeys(key) + +Returns all fields of the hash stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------- | +| `key` | string | key holding the hash to get the fields of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :------------------ | :-------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the list of fields in the hash. | If the hash does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 'myvalue'); + await redisClient.hset('myhash', 'myotherfield', 'myothervalue'); + + const keys = await redisClient.hkeys('myhash'); + if (keys.length !== 2) { + throw new Error('myhash should have 2 keys'); + } + + console.log(`myhash has keys ${keys}`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hlen.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hlen.md new file mode 100644 index 000000000..b6d8e9b00 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hlen.md @@ -0,0 +1,39 @@ +--- +title: 'Client.hlen(key)' +description: 'Returns the number of fields in the hash stored at `key`.' +--- + +# Client.hlen(key) + +Returns the number of fields in the hash stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------- | +| `key` | string | key holding the hash to get the fields of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the number of fields in the hash. | If the hash does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 10); + await redisClient.hset('myhash', 'myotherfield', 20); + await redisClient.hlen('myhash'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hset.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hset.md new file mode 100644 index 000000000..491e199e5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hset.md @@ -0,0 +1,41 @@ +--- +title: 'Client.hset(key, field, value)' +description: 'Sets the value of field in the hash stored at `key` to `value`.' +--- + +# Client.hset(key, field, value) + +Sets the specified field in the hash stored at `key` to `value`. If the `key` does not exist, a new key holding a hash is created. If `field` already exists in the hash, it is overwritten. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :---------------------------------------- | +| `key` | string | key holding the hash to set the field of. | +| `field` | string | field to set in the hash. | +| `value` | string | value to set the field to. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :-------------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the number of fields that were added. | If the hash does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 'myvalue'); + await redisClient.hget('myhash', 'myfield'); + await redisClient.hdel('myhash', 'myfield'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hsetnx.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hsetnx.md new file mode 100644 index 000000000..1bdc8784a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hsetnx.md @@ -0,0 +1,45 @@ +--- +title: 'Client.hsetnx(key, field, value)' +description: 'Sets the value of field in the hash stored at `key` to `value` only if field does not exist in the hash.' +--- + +# Client.hsetnx(key, field, value) + +Sets the specified field in the hash stored at `key` to `value`, only if `field` does not yet exist. If `key` does not exist, a new key holding a hash is created. If `field` already exists, this operation has no effect. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :---------------------------------------- | +| `key` | string | key holding the hash to set the field of. | +| `field` | string | field to set in the hash. | +| `value` | string | value to set the field to. | + +### Returns + +| Type | Resolves with | Rejected when | +| :----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------ | +| `Promise` | On success, the promise resolves with `1` if `field` is a new field in the hash and value was set, and with `0` if `field` already exists in the hash and no operation was performed. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hsetnx('myhash', 'myfield', 'myvalue'); + await redisClient.hsetnx('myhash', 'myotherfield', 'myothervalue'); + + const set = await redisClient.hsetnx('myhash', 'myfield', 'mynewvalue'); + if (set === true) { + throw new Error('hsetnx should have failed on existing field'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hvals.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hvals.md new file mode 100644 index 000000000..7fd8dc368 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-hvals.md @@ -0,0 +1,45 @@ +--- +title: 'Client.hvals(key)' +description: 'Returns all values of the hash stored at `key`.' +--- + +# Client.hvals(key) + +Returns all values of the hash stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------- | +| `key` | string | key holding the hash to get the fields of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :------------------ | :-------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the list of values in the hash. | If the hash does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.hset('myhash', 'myfield', 'myvalue'); + await redisClient.hset('myhash', 'myotherfield', 'myothervalue'); + + const values = await redisClient.hvals('myhash'); + if (values.length !== 2) { + throw new Error('myhash should have 2 values'); + } + + console.log(`myhash has values ${values}`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-incr.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-incr.md new file mode 100644 index 000000000..e8f304cb9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-incr.md @@ -0,0 +1,45 @@ +--- +title: 'Client.incr(key)' +description: 'Increments the number stored at `key` by one.' +--- + +# Client.incr(key) + +Increments the number stored at `key` by one. If the key does not exist, it is set to zero before performing the operation. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :------------------- | +| `key` | string | the key to increment | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of `key` after the increment. | If the key contains a value of the wrong type, or contains a string that cannot be represented as an integer, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 10, 0); + + let value = await redisClient.incr('mykey'); + value = await redisClient.incrBy('mykey', value); + value = await redisClient.decrBy('mykey', value); + value = await redisClient.decr('mykey'); + if (value !== -1) { + throw new Error('mykey should have been -1'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-incrby.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-incrby.md new file mode 100644 index 000000000..f94fb6a04 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-incrby.md @@ -0,0 +1,46 @@ +--- +title: 'Client.incrBy(key, increment)' +description: 'Increments the number stored at `key` by `increment`.' +--- + +# Client.incrBy(key, increment) + +Increments the number stored at `key` by `increment`. If the key does not exist, it is set to zero before performing the operation. + +### Parameters + +| Parameter | Type | Description | +| :---------- | :----- | :------------------------- | +| `key` | string | the key to increment | +| `increment` | number | the amount to increment by | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of `key` after the increment. | If the key contains a value of the wrong type, or contains a string that cannot be represented as an integer, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 10, 0); + + let value = await redisClient.incr('mykey'); + value = await redisClient.incrBy('mykey', value); + value = await redisClient.decrBy('mykey', value); + value = await redisClient.decr('mykey'); + if (value !== -1) { + throw new Error('mykey should have been -1'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lindex.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lindex.md new file mode 100644 index 000000000..232a6153d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lindex.md @@ -0,0 +1,46 @@ +--- +title: 'Client.lindex(key)' +description: 'Returns the element at index `index` of the list stored at `key`.' +--- + +# Client.lindex(key) + +Returns the specified element of the list stored at `key`. The index is zero-based. Negative indices can be used to designate elements starting at the tail of the list. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :------------------------------------------ | +| `key` | string | key holding the list to get the element of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :----------------------------------------------------------- | :------------------------------------------------------------------------------------------------ | +| `Promise` | On success, the promise resolves with the requested element. | If the list does not exist, or the index is out of bounds, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.rpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + await redisClient.rpush('mylist', 'third'); + + const item = await redisClient.lindex('mylist', 0); + if (item !== 'first') { + throw new Error('lindex operation should have returned first'); + } + + await redisClient.lrange('mylist', 1, 2); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-llen.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-llen.md new file mode 100644 index 000000000..e17856323 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-llen.md @@ -0,0 +1,46 @@ +--- +title: 'Client.llen(key)' +description: 'Returns the length of the list stored at `key`.' +--- + +# Client.llen(key) + +Returns the length of the list stored at `key`. If `key` does not exist, it is interpreted as an empty list and 0 is returned. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------- | +| `key` | string | key holding the list to get the length of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :--------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the length of the list at `key`. | If the list does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default function () { + redisClient + .rpush('mylist', 'first') + .then((_) => redisClient.rpush('mylist', 'second')) + .then((_) => redisClient.rpush('mylist', 'third')) + .then((_) => redisClient.llen('mylist')) + .then((length) => { + if (length !== 3) { + throw new Error('llen operations should have returned 3'); + } + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lpop.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lpop.md new file mode 100644 index 000000000..cb1889518 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lpop.md @@ -0,0 +1,42 @@ +--- +title: 'Client.lpop(key)' +description: 'Removes and returns the first element of the list stored at `key`.' +--- + +# Client.lpop(key) + +Removes and returns the first element of the list stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :------------------------------------- | +| `key` | string | key holding the list to left pop from. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of the first element. | If the list does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.lpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + + const item = await redisClient.lpop('mylist'); + await redisClient.rpush('mylist', item); + await redisClient.rpop('mylist'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lpush.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lpush.md new file mode 100644 index 000000000..b7532b05a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lpush.md @@ -0,0 +1,43 @@ +--- +title: 'Client.lpush(key, values)' +description: 'Adds the string `value` to the left of the list stored at `key`.' +--- + +# Client.lpush(key, values) + +Inserts all the specified values at the head of the list stored at `key`. If `key` does not exist, it is created as empty list before performing the push operations. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------------------------------------------------ | :------------------------------------ | +| `key` | string | key holding the list to left push to. | +| `values` | a variadic array of strings, numbers, or booleans | values to push to the list. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :--------------------------------------------------------------------------- | :---------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the length of the list after the push. | When `key` holds a value that is not a list, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.lpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + + let item = await redisClient.lpop('mylist'); + item = redisClient.rpush('mylist', item); + item = redisClient.rpop('mylist'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lrange.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lrange.md new file mode 100644 index 000000000..983b7c04c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lrange.md @@ -0,0 +1,48 @@ +--- +title: 'Client.lrange(key, start, stop)' +description: 'Returns the specified elements of the list stored at `key`.' +--- + +# Client.lrange(key, start, stop) + +Returns the specified elements of the list stored at `key`. The offsets start and stop are zero-based indexes. These offsets can be negative numbers, where they indicate offsets starting at the end of the list. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :---------------------------------------- | +| `key` | string | key holding the list to get the range of. | +| `start` | number | index of the first element to return. | +| `stop` | number | index of the last element to return. | + +### Returns + +| Type | Resolves with | Rejected when | +| :------------------ | :--------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the list of elements in the specified range. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.rpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + await redisClient.rpush('mylist', 'third'); + + const item = redisClient.lindex('mylist', 0); + if (item !== 'first') { + throw new Error('lindex operation should have returned first'); + } + + await redisClient.lrange('mylist', 1, 2); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lrem.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lrem.md new file mode 100644 index 000000000..a0153beee --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lrem.md @@ -0,0 +1,50 @@ +--- +title: 'Client.lrem(key, count, value)' +descriptiontion: 'Removes the first count occurrences of elements equal to value from the list stored at `key`.' +--- + +# Client.lrem(key, count, value) + +Removes the first `count` occurrences of `value` from the list stored at `key`. If `count` is positive, elements are removed from the beginning of the list. If `count` is negative, elements are removed from the end of the list. If `count` is zero, all elements matching `value` are removed. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :---------------------------------------------- | +| `key` | string | key holding the list to remove the elements of. | +| `count` | number | number of elements to remove. | +| `value` | string | value to remove from the list. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the number of removed elements. | If the list does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.rpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + await redisClient.rpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + await redisClient.lrem('mylist', 0, 'second'); + + const length = await redisClient.lrem('mylist', 1, 'first'); + if (length !== 1) { + throw new Error('lrem operations should have left 1 item behind'); + } + + await redisClient.lpop('mylist'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lset.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lset.md new file mode 100644 index 000000000..78d4bd02d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-lset.md @@ -0,0 +1,43 @@ +--- +title: 'Client.lset(key, index, element)' +description: 'Sets the list element at index `index` of the list stored at `key` to `value`.' +--- + +# Client.lset(key, index, element) + +Sets the list element at `index` to `element`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :------------------------------------------ | +| `key` | string | key holding the list to set the element of. | +| `index` | number | index of the element to set. | +| `element` | string | value to set the element to. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :------------------------------------------ | :------------------------------------------------------------------------------------------------ | +| `Promise` | On success, the promise resolves with `OK`. | If the list does not exist, or the index is out of bounds, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.rpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + await redisClient.rpush('mylist', 'third'); + await redisClient.lset('mylist', 0, 1); + await redisClient.lset('mylist', 1, 2); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-mget.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-mget.md new file mode 100644 index 000000000..122d07978 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-mget.md @@ -0,0 +1,42 @@ +--- +title: 'Client.mget(keys)' +description: 'Returns the values of all specified keys.' +--- + +# Client.mget(keys) + +Returns the values of all specified keys. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------- | :----------------------------- | +| `keys` | string[] | the keys to get the values of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the list of values at the specified keys. For every key that does not hold a string value, or does not exist, the value `null` will be returned. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('first', 1, 0); + await redisClient.set('second', 2, 0); + await redisClient.set('third', 3, 0); + + const values = await redisClient.mget('first', 'second', 'third'); + console.log(`set values are: ${values}`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-persist.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-persist.md new file mode 100644 index 000000000..a49ede209 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-persist.md @@ -0,0 +1,45 @@ +--- +title: 'Client.persist(key)' +description: 'Remove the expiration from a key.' +--- + +# Client.persist(key) + +Removes the existing timeout on `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :-------------------------------- | +| `key` | string | the key to remove the timeout of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :----------------- | :------------------------------------------------------------------------------------------ | :------------ | +| `Promise` | On success, the promise resolves with `true` if the timeout was removed, `false` otherwise. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'myvalue', 10); + await redisClient.expire('mykey', 100); + + const ttl = await redisClient.ttl('mykey'); + if (ttl <= 10) { + throw new Error('mykey should have a ttl of 10 <= x < 100'); + } + + await redisClient.persist('mykey', 100); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-randomkey.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-randomkey.md new file mode 100644 index 000000000..a72f7613b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-randomkey.md @@ -0,0 +1,36 @@ +--- +title: 'Client.randomKey()' +description: 'Returns a random key from the keyspace.' +--- + +# Client.randomKey() + +Returns a random key. + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :--------------------------------------------------------- | :--------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the random key name. | If the database is empty, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('first', 1, 0); + await redisClient.set('second', 2, 0); + await redisClient.set('third', 3, 0); + + const key = await redisClient.randomKey(); + console.log(`picked random key is: ${key}`); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-rpop.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-rpop.md new file mode 100644 index 000000000..4e72fc98d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-rpop.md @@ -0,0 +1,42 @@ +--- +title: 'Client.rpop(key)' +description: 'Removes and returns the last element of the list stored at `key`.' +--- + +# Client.rpop(key) + +Removes and returns the last element of the list stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :-------------------------------------- | +| `key` | string | key holding the list to right pop from. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the value of the first element. | If the list does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.lpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + + const item = await redisClient.lpop('mylist'); + await redisClient.rpush('mylist', item); + await redisClient.rpop('mylist'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-rpush.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-rpush.md new file mode 100644 index 000000000..0876a14da --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-rpush.md @@ -0,0 +1,43 @@ +--- +title: 'Client.rpush(key, values)' +description: 'Adds the string `value` to the right of the list stored at `key`.' +--- + +# Client.rpush(key, values) + +Inserts all the specified values at the tail of the list stored at `key`. If `key` does not exist, it is created as empty list before performing the push operation. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------------------------------------------------ | :------------------------------------- | +| `key` | string | key holding the list to right push to. | +| `values` | a variadic array of strings, numbers, or booleans | values to push to the list. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the length of the list after the push operation. | When `key` holds a value that is not a list, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.lpush('mylist', 'first'); + await redisClient.rpush('mylist', 'second'); + + const item = await redisClient.lpop('mylist'); + await redisClient.rpush('mylist', item); + await redisClient.rpop('mylist'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sadd.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sadd.md new file mode 100644 index 000000000..90bb2a0e7 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sadd.md @@ -0,0 +1,44 @@ +--- +title: 'Client.sadd(key, members)' +description: 'Adds the specified members to the set stored at `key`.' +--- + +# Client.sadd(key, members) + +Adds the specified members to the set stored at `key`. Specified members that are already a member of this set are ignored. If key does not exist, a new set is created before adding the specified members. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------------------------------------------------ | :----------------------------------------- | +| `key` | string | key holding the set to add the members to. | +| `members` | a variadic array of strings, numbers, or booleans | members to add to the set. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------------ | :------------ | +| `Promise` | On success, the promise resolves with the number of elements that were added to the set, not including elements already present in the set. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.sadd('myset', 'foo'); + await redisClient.sadd('myset', 'bar'); + + const isit = await redisClient.sismember('myset', 'foo'); + if (isit === false) { + throw new Error('sismember should have returned true'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sendcommand.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sendcommand.md new file mode 100644 index 000000000..04f02bcd2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sendcommand.md @@ -0,0 +1,41 @@ +--- +title: 'Client.sendCommand(command, args)' +description: 'Issue a command to the Redis server.' +--- + +# Client.sendCommand(command, args) + +In the event a Redis command you wish to use is not implemented yet, the `sendCommand` method can be used to send a custom commands to the server. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------------------------------------------------ | :------------------------------------------------------------------------------------------------------------- | +| `command` | string | command name to issue to the Redis server, as described in [Redis' documentation](https://redis.io/commands/). | +| `args` | a variadic array of strings, numbers, or booleans | command arguments to pass to the Redis server. | + +### Returns + +| Type | Resolves with | Rejected when | +| :------------- | :------------------------------------------------------------------------------------------------------------------ | :------------ | +| `Promise` | On success, the promise resolves with string, number, or boolean result the server would reply to the command sent. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + const result = await redisClient.sendCommand('ECHO', 'Hello world'); + if (result !== 'Hello world') { + throw new Error('ECHO should have returned "Hello world"'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-set.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-set.md new file mode 100644 index 000000000..2e243c188 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-set.md @@ -0,0 +1,49 @@ +--- +title: 'Client.set(key, value, expiration)' +description: 'Set `key` to hold `value`, with a time to live equal to `expiration`.' +--- + +# Client.set(key, value, expiration) + +Set the value of a key, with a time to live equal to the expiration time parameter (in seconds). If the key already holds a value, it is overwritten. + +### Parameters + +| Parameter | Type | Description | +| :----------- | :------------------------- | :------------------------------------------------------------------ | +| `key` | string | the key to set | +| `value` | string, number, or boolean | the value to set | +| `expiration` | integer | the time to live in seconds. the `0` value indicates no expiration. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :------------------------------------------ | :----------------------------------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with 'OK'. | If the provided `value` is not of a supported type, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'myvalue', 0); + + const exists = await redisClient.exists('mykey'); + if (exists === false) { + throw new Error('mykey should exist'); + } + + const value = await redisClient.get('mykey'); + console.log(`set key 'mykey' to value: ${value}`); + + await redisClient.del('mykey'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sismember.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sismember.md new file mode 100644 index 000000000..ad45e1dc2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-sismember.md @@ -0,0 +1,44 @@ +--- +title: 'Client.sismember(key, member)' +description: 'Determines if a given value is a member of the set stored at `key`.' +--- + +# Client.sismember(key, member) + +Returns if member is a member of the set stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------------------------- | :--------------------------------------------------------- | +| `key` | string | key holding the set to check if the member is a member of. | +| `member` | string, number, or boolean | member to check if is a member of the set. | + +### Returns + +| Type | Resolves with | Rejected when | +| :----------------- | :----------------------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with `true` if the element is a member of the set, `false` otherwise. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.sadd('myset', 'foo'); + await redisClient.sadd('myset', 'bar'); + + const isit = await redisClient.sismember('myset', 'foo'); + if (isit === false) { + throw new Error('sismember should have returned true'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-smembers.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-smembers.md new file mode 100644 index 000000000..e85b83f81 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-smembers.md @@ -0,0 +1,44 @@ +--- +title: 'Client.smembers(key)' +description: 'Returns all the members of the set stored at `key`.' +--- + +# Client.smembers(key) + +Returns all the members of the set values stored at `keys`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :----------------------------------------- | +| `key` | string | key holding the set to get the members of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :------------------ | :--------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with an array containing the values present in the set. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.sadd('myset', 'foo'); + await redisClient.sadd('myset', 'bar'); + await redisClient.sadd('myset', 'foo'); + + const members = await redisClient.smembers('myset'); + if (members.length !== 2) { + throw new Error('sismember should have length 2'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-spop.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-spop.md new file mode 100644 index 000000000..61bb396c1 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-spop.md @@ -0,0 +1,43 @@ +--- +title: 'Client.spop(key)' +description: 'Removes and returns a random member of the set stored at `key`.' +--- + +# Client.spop(key) + +Removes and returns a random element from the set value stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :--------------------------------------------- | +| `key` | string | key holding the set to get a random member of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :----------------------------------------------------------- | :---------------------------------------------------------------- | +| `Promise` | On success, the promise resolves to the returned set member. | If the set does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.sadd('myset', 'foo'); + await redisClient.sadd('myset', 'bar'); + await redisClient.spop('myset', 'foo'); + const members = await redisClient.smembers('myset'); + if (members.length !== 1) { + throw new Error('sismember should have length 1'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-srandmember.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-srandmember.md new file mode 100644 index 000000000..d949136a2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-srandmember.md @@ -0,0 +1,44 @@ +--- +title: 'Client.srandmember(key)' +description: 'Returns a random member of the set stored at `key`.' +--- + +# Client.srandmember(key) + +Returns a random element from the set value stored at `key`. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :--------------------------------------------- | +| `key` | string | key holding the set to get a random member of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :---------------------------------------------------------------- | :---------------------------------------------------------------- | +| `Promise` | On success, the promise resolves with the selected random member. | If the set does not exist, the promise is rejected with an error. | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.sadd('myset', 'foo'); + await redisClient.sadd('myset', 'bar'); + await redisClient.spop('myset', 'foo'); + + const members = await redisClient.smembers('myset'); + if (members.length !== 1) { + throw new Error('sismember should have length 1'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-srem.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-srem.md new file mode 100644 index 000000000..b77a21390 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-srem.md @@ -0,0 +1,45 @@ +--- +title: 'Client.srem(key, members)' +description: 'Removes the specified members from the set stored at `key`.' +--- + +# Client.srem(key, members) + +Removes the specified members from the set stored at `key`. Specified members that are not a member of this set are ignored. If key does not exist, it is treated as an empty set and this command returns 0. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :------------------------------------------------ | :---------------------------------------------- | +| `key` | string | key holding the set to remove the members from. | +| `members` | a variadic array of strings, numbers, or booleans | members to remove from the set. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the number of members that were remove from the set, not including non existing members. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.sadd('myset', 'foo'); + await redisClient.sadd('myset', 'bar'); + await redisClient.srem('myset', 'foo'); + + const members = await redisClient.smembers('myset'); + if (members.length !== 1) { + throw new Error('sismember should have length 1'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-ttl.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-ttl.md new file mode 100644 index 000000000..6fd438df5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/client/client-ttl.md @@ -0,0 +1,45 @@ +--- +title: 'Client.ttl(key)' +description: 'Returns the remaining time to live of a key.' +--- + +# Client.ttl(key) + +Returns the remaining time to live of a key that has a timeout. + +### Parameters + +| Parameter | Type | Description | +| :-------- | :----- | :------------------------- | +| key | string | the key to get the TTL of. | + +### Returns + +| Type | Resolves with | Rejected when | +| :---------------- | :-------------------------------------------------------------- | :------------ | +| `Promise` | On success, the promise resolves with the TTL value in seconds. | | + +### Example + +{{< code >}} + +```javascript +import redis from 'k6/experimental/redis'; + +// Instantiate a new redis client +const redisClient = new redis.Client('redis://localhost:6379'); + +export default async function () { + await redisClient.set('mykey', 'myvalue', 10); + await redisClient.expire('mykey', 100); + + const ttl = await redisClient.ttl('mykey'); + if (ttl <= 10) { + throw new Error('mykey should have a ttl of 10 <= x < 100'); + } + + await redisClient.persist('mykey', 100); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/redis-options.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/redis-options.md new file mode 100644 index 000000000..3ad823a0f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/redis/redis-options.md @@ -0,0 +1,66 @@ +--- +title: 'Options' +description: 'Options allow you to fine-tune how a Redis client behaves and interacts with a Redis server or cluster.' +weight: 20 +--- + +# Redis options + +You can configure the [Redis Client](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client) by using a Redis connection URL as demonstrated in the [client documentation](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/client#usage), or by using an [Options](#options) object to access more advanced configuration. + +## Options + +The configuration for the overall Redis client, including authentication and connection settings. + +| Option Name | Type | Description | +| ---------------- | --------------------------------------------------- | ------------------------------------------------------------------------------- | +| socket | [SocketOptions](#socket-connection-options) | The configuration of the connection socket used to connect to the Redis server. | +| username | String (optional) | Username for client authentication. | +| password | String (optional) | Password for client authentication. | +| clientName | String (optional) | Name for the client connection. | +| database | Number (optional) | Database ID to select after connecting. | +| masterName | String (optional) | Master instance name for Sentinel. | +| sentinelUsername | String (optional) | Username for Sentinel authentication. | +| sentinelPassword | String (optional) | Password for Sentinel authentication. | +| cluster | [ClusterOptions](#redis-cluster-options) (optional) | The configuration for Redis Cluster connections. | + +### Socket Connection Options + +Socket-level settings for connecting to a Redis server. + +| Option Name | Type | Description | +| ------------------ | -------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| host | String | IP address or hostname of the Redis server. | +| port | Number (optional) | Port number of the Redis server. | +| tls | [TLSOptions](#tls-configuration-options) (optional) | The configuration for TLS/SSL. | +| dialTimeout | Number (optional, default is _5_ (seconds)) | Timeout for establishing a connection, in seconds. | +| readTimeout | Number (optional, default is _3_ (seconds)) | Timeout for socket reads, in seconds. A value of `-1` disables the timeout. | +| writeTimeout | Number (optional, default is `readTimeout`) | Timeout for socket writes, in seconds. A value of `-1` disables the timeout. | +| poolSize | Number (optional, default is _10_ (per CPU)) | Number of socket connections in the pool per CPU. | +| minIdleConns | Number (optional) | Minimum number of idle connections in the pool. | +| maxConnAge | Number (optional, default is _0_ (no maximum idle time)) | Maximum idle time before closing a connection. | +| poolTimeout | Number (optional, `readTimeout + 1`) | Timeout for acquiring a connection from the pool. | +| idleTimeout | Number (optional, `readTimeout + 1`) | Timeout for idle connections in the pool. | +| idleCheckFrequency | Number (optional, default is _1_ (minute)) | Frequency of idle connection checks, in minutes. A value of `-1` disables the checks. | + +#### TLS Configuration Options + +Options for establishing a secure TLS connection. + +| Option Name | Type | Description | +| ----------- | ---------------------- | --------------------------------------------------- | +| ca | ArrayBuffer[] | Array of CA certificates. | +| cert | ArrayBuffer (optional) | Client certificate for mutual TLS. | +| key | ArrayBuffer (optional) | Private key associated with the client certificate. | + +### Redis Cluster Options + +Options for behavior in a Redis Cluster setup. + +| Option Name | Type | Description | +| -------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------- | +| maxRedirects | Number (optional, default is _3_ retries) | Maximum number of command redirects. | +| readOnly | Boolean (optional) | Enables read-only mode for replicas. | +| routeByLatency | Boolean (optional) | Route read commands by latency. | +| routeRandomly | Boolean (optional) | Random routing for read commands. | +| nodes | String[] or [SocketOptions](#socket-connection-options)[] | List of cluster nodes as URLs or [SocketOptions](#socket-connection-options). | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/timers.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/timers.md new file mode 100644 index 000000000..94cc81905 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/timers.md @@ -0,0 +1,18 @@ +--- +title: 'timers' +description: 'k6 timers experimental API' +weight: 03 +--- + +# timers + +{{< docs/shared source="k6" lookup="experimental-timers-module.md" version="" >}} + +This modules implements the commonly found in browsers: + +| Function | Description | +| :---------------------------------------------------------------------------- | :-------------------------------------------------- | +| [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) | sets a function to be run after a given timeout | +| [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout) | clears a previously set timeout with `setTimeout` | +| [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) | sets a function to be run on a given interval | +| [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) | clears a previously set interval with `setInterval` | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/_index.md new file mode 100644 index 000000000..ea9cd4365 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/_index.md @@ -0,0 +1,64 @@ +--- +title: "tracing" +descriptiontion: "k6 Tracing experimental API" +weight: 04 +weight: 04 +--- + +# tracing + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +With this experimental module, you can _instrument_ HTTP requests so that they emit traces as the test runs. + +## About trace contexts + +A _trace context_ is a set of standardized HTTP headers added to a request that lets a tracing system correlate the request with other requests as they navigate through a system. The trace context specifications, such as the supported [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [Jaeger Trace Context](https://www.jaegertracing.io/docs/1.21/client-libraries/#propagation-format), define specific header names and an encoding format for the header values. + +A trace context generally consists of, at least, a `trace_id`, a `span_id`, and a `sampled` flag. The `trace_id` is a unique identifier for the trace, the `span_id` is a unique identifier for the request, and the `sampled` flag is a boolean that indicates whether the request should be traced. For instance, the [W3C Trace Context](https://www.w3.org/TR/trace-context/) defines the `Traceparent` header, whose value contains a `trace_id`, a `span_id` and a `sampled` flag, encoded as a dash (`-`) separated list of hexadecimal values. When a trace context header is attached to an HTTP request, we refer to it as being _propagated_ to the downstream service. + +## API + +| Class/Function | Description | +| :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------ | +| [instrumentHTTP](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/instrumenthttp) | instruments the k6 http module with tracing capabilities. | +| [Client](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/client) | configurable Client that exposes instrumented HTTP operations and allows selectively instrumenting requests with tracing. | + +## Example + +This example demonstrates how to use the tracing API to instrument every HTTP request made in a script with tracing information. + +{{< code >}} + +```javascript +import { check } from 'k6'; +import tracing from 'k6/experimental/tracing'; +import http from 'k6/http'; + +// instrumentHTTP will ensure that all requests made by the http module +// from this point forward will have a trace context attached. +// +// The first argument is a configuration object that +// can be used to configure the tracer. +tracing.instrumentHTTP({ + // possible values: "w3c", "jaeger" + propagator: 'w3c', +}); + +export default () => { + // the instrumentHTTP call in the init context replaced + // the http module with a version that will automatically + // attach a trace context to every request. + // + // Because the instrumentHTTP call was configured with the + // w3c trace propagator, this request will as a result have + // a `traceparent` header attached. + const res = http.get('http://httpbin.org/get', { + headers: { + 'X-Example-Header': 'instrumented/get', + }, + }); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/client.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/client.md new file mode 100644 index 000000000..28a8710a9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/client.md @@ -0,0 +1,85 @@ +--- +title: 'Client' +description: 'Client is a HTTP client attaching tracing information to its requests.' +weight: 02 +--- + +# Client + +`Client` is an HTTP client constructor that attaches tracing information to its requests. Use it to include a tracing context in HTTP requests so that tracing backends (such as [Grafana Tempo](https://grafana.com/oss/tempo/)) can incorporate their results. + +The `Client` class acts as a drop-in replacement for the standard `http` module and attaches a [trace context](https://www.w3.org/TR/trace-context/) to the requests headers, and add a `trace_id` to HTTP-related k6 output's data points metadata. It currently supports the [W3C Trace Context](https://www.w3.org/TR/trace-context/) and [Jaeger](https://www.jaegertracing.io/docs/1.21/client-libraries/#propagation-format) trace context propagation formats. For details about propagation, refer to [About trace contexts](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing#about-trace-contexts). + +The `Client` constructor accepts an [`Options`](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/options) object as its only parameter. + +## Example + +This example demonstrates how to instantiate a tracing client and use it to instrument HTTP calls with trace context headers. It also illustrates how you can use it alongside the standard `http` module to perform non-instrumented HTTP calls. + +{{< code >}} + +```javascript +import { check } from 'k6'; +import tracing from 'k6/experimental/tracing'; +import http from 'k6/http'; + +// Explicitly instantiating a tracing client allows to distringuish +// instrumented from non-instrumented HTTP calls, by keeping APIs separate. +// It also allows for finer-grained configuration control, by letting +// users changing the tracing configuration on the fly during their +// script's execution. +const instrumentedHTTP = new tracing.Client({ + propagator: 'w3c', +}); + +const testData = { name: 'Bert' }; + +export default () => { + // Using the tracing client instance, HTTP calls will have + // their trace context headers set. + let res = instrumentedHTTP.request('GET', 'http://httpbin.org/get', null, { + headers: { + 'X-Example-Header': 'instrumented/request', + }, + }); + + // The tracing client offers more flexibility over + // the `instrumentHTTP` function, as it leaves the + // imported standard http module untouched. Thus, + // one can still perform non-instrumented HTTP calls + // using it. + res = http.post('http://httpbin.org/post', JSON.stringify(testData), { + headers: { 'X-Example-Header': 'noninstrumented/post' }, + }); + + res = instrumentedHTTP.del('http://httpbin.org/delete', null, { + headers: { 'X-Example-Header': 'instrumented/delete' }, + }); +}; +``` + +{{< /code >}} + +## HTTP module functions equivalents + +The `Client` class exposes the same API as the standard `http` module. Except for the `batch` method, which is absent from `Client`. The following table lists the `Client` methods which have an equivalent in the standard `http` module: + +| Method | HTTP equivalent | Description | +| :--------------------------------------------------- | :------------------------------------------------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Client.del(url, [body], [params])` | [http.del](https://grafana.com/docs/k6//javascript-api/k6-http/del) | Performs an instrumented HTTP DELETE request. The method has the same prototype as the `http.del` function and should transparently act as a drop-in replacement for it. | +| `Client.get(url, [params])` | [http.get](https://grafana.com/docs/k6//javascript-api/k6-http/get) | Performs an instrumented HTTP GET request. The method has the same prototype as the `http.get` function and should transparently act as a drop-in replacement for it. | +| `Client.head(url, [params])` | [http.head](https://grafana.com/docs/k6//javascript-api/k6-http/head) | Performs an instrumented HTTP HEAD request. The method has the same prototype as the `http.head` function and should transparently act as a drop-in replacement for it. | +| `Client.options(url, [body], [params])` | [http.options](https://grafana.com/docs/k6//javascript-api/k6-http/options) | Performs an instrumented HTTP OPTIONS request. The method has the same prototype as the `http.options` function and should transparently act as a drop-in replacement for it. | +| `Client.patch(url, [body], [params])` | [http.patch](https://grafana.com/docs/k6//javascript-api/k6-http/patch) | Performs an instrumented HTTP PATCH request. The method has the same prototype as the `http.patch` function and should transparently act as a drop-in replacement for it. | +| `Client.post(url, [body], [params])` | [http.post](https://grafana.com/docs/k6//javascript-api/k6-http/post) | Performs an instrumented HTTP POST request. The method has the same prototype as the `http.post` function and should transparently act as a drop-in replacement for it. | +| `Client.put(url, [body], [params])` | [http.put](https://grafana.com/docs/k6//javascript-api/k6-http/head) | Performs an instrumented HTTP HEAD request. The method has the same prototype as the `http.put` function and should transparently act as a drop-in replacement for it. | +| `Client.request(method, url, [body], [params])` | [http.request](https://grafana.com/docs/k6//javascript-api/k6-http/request) | Performs an instrumented HTTP request. The method has the same prototype as the `http.request` function and should transparently act as a drop-in replacement for it. | +| `Client.asyncRequest(method, url, [body], [params])` | [http.asyncRequest](https://grafana.com/docs/k6//javascript-api/k6-http/asyncrequest) | Performs an instrumented HTTP asynchronous request. The method has the same prototype as the `http.asyncRequest` function and should transparently act as a drop-in replacement for it. | + +## Configuration + +`Client` instances support being reconfigured using the following API: + +| Method | Description | +| :-------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Client.configure(options)` | Reconfigures the tracing client instance with the provided [`Options`](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/options) | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/instrumenthttp.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/instrumenthttp.md new file mode 100644 index 000000000..29677691f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/instrumenthttp.md @@ -0,0 +1,55 @@ +--- +title: 'instrumentHTTP' +description: 'instrumentHTTP instruments the k6 http module with tracing capabilities.' +weight: 01 +--- + +# instrumentHTTP + +The `instrumentHTTP` function instruments the k6 http module with tracing capabilities. It transparently replaces each of the k6 http module functions with versions that automatically attach a trace context to every request. Instrumented functions include [del](https://grafana.com/docs/k6//javascript-api/k6-http/del),[get](https://grafana.com/docs/k6//javascript-api/k6-http/get),[head](https://grafana.com/docs/k6//javascript-api/k6-http/head),[options](https://grafana.com/docs/k6//javascript-api/k6-http/options),[patch](https://grafana.com/docs/k6//javascript-api/k6-http/patch),[post](https://grafana.com/docs/k6//javascript-api/k6-http/post),[put](https://grafana.com/docs/k6//javascript-api/k6-http/head), and [request](https://grafana.com/docs/k6//javascript-api/k6-http/request). + +The `instrumentHTTP` automatically adds tracing information to HTTP requests performed using the `k6/http` module functions (mentioned above). +This means that, to instrument the HTTP requests, you don't need to rewrite any code. +Instead, call it once in the init context. +From that point forward, all requests made by the HTTP module from that point forward will have a trace context header added to them, and the metadata for the data-point output will have the used `trace_id`. For details about propagation, refer to [About trace contexts](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing#about-trace-contexts). + +## Parameters + +| Name | Type | Description | +| :-------- | :--------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `options` | [`Options`](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/options) | Configures the tracing behavior with the provided [`Options`](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/options) object. | + +## Example + +This example demonstrates how calling the `instrumentHTTP` function in the init context of a script once ensures that all requests made by the HTTP module from that point forward will have a trace context header attached. + +{{< code >}} + +```javascript +import { check } from 'k6'; +import tracing from 'k6/experimental/tracing'; +import http from 'k6/http'; + +// instrumentHTTP will ensure that all requests made by the http module +// will be traced. The first argument is a configuration object that +// can be used to configure the tracer. +// +// Currently supported HTTP methods are: get, post, put, patch, head, +// del, options, and request. +tracing.instrumentHTTP({ + // propagator defines the trace context propagation format. + // Currently supported: w3c and jaeger. + // Default: w3c + propagator: 'w3c', +}); + +export default () => { + const res = http.get('http://httpbin.org/get', { + headers: { + 'X-Example-Header': 'instrumented/get', + }, + }); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/options.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/options.md new file mode 100644 index 000000000..496dda37f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/tracing/options.md @@ -0,0 +1,15 @@ +--- +title: 'Options' +description: 'Options allows to configure the tracing instrumentation behavior.' +weight: 03 +--- + +# Options + +Use the `Options` object to configure the tracing instrumentation behavior. It is used during the instantiation of a [`Client`](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/client) instance and also as a parameter to the [`instrumentHTTP`](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing/instrumenthttp) function. It controls the general behavior of the tracing instrumentation and is unspecific to any particular tracing client instance. + +## Options + +| Option name | Type | Default | Description | +| :----------- | :------- | :------ | :----------------------------------------------------------------------------- | +| `propagator` | `string` | `w3c` | The trace context propagation format. Currently supported: `w3c` and `jaeger`. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/_index.md new file mode 100644 index 000000000..8f06b918c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/_index.md @@ -0,0 +1,95 @@ +--- +title: 'webcrypto' +description: "k6 webcrypto experimental API" +weight: 06 +weight: 06 +--- + +# webcrypto + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +With this experimental module, you can use the [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API) in your k6 scripts. However, note that this API is not yet fully implemented and some algorithms and features might still be missing. + +The Web Crypto API is a JavaScript API for performing cryptographic operations such as encryption, decryption, digital signature generation and verification, and key generation and management. It provides a standard interface to access cryptographic functionality, which can help ensure that cryptographic operations are performed correctly and securely. + +## API + +The module exports a top-level `crypto` object with the following properties and methods: + +| Interface/Function | Description | +| :-------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [getRandomValues](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/crypto/getrandomvalues) | Fills the passed `TypedArray` with cryptographically sound random values. | +| [randomUUID](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/crypto/randomuuid) | Returns a randomly generated, 36 character long v4 UUID. | +| [subtle](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto) | The [SubtleCrypto](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto) interface provides access to common cryptographic primitives, such as hashing, signing, encryption, or decryption. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const plaintext = stringToArrayBuffer('Hello, World!'); + + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const key = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Encrypt the plaintext using the AES-CBC key with + * have generated. + */ + const iv = crypto.getRandomValues(new Uint8Array(16)); + const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + plaintext + ); + + /** + * Decrypt the ciphertext using the same key to verify + * that the resulting plaintext is the same as the original. + */ + const deciphered = await crypto.subtle.decrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + ciphertext + ); + + console.log( + 'deciphered text == original plaintext: ', + arrayBufferToHex(deciphered) === arrayBufferToHex(plaintext) + ); +} + +function arrayBufferToHex(buffer) { + return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join(''); +} + +function stringToArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aescbcparams.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aescbcparams.md new file mode 100644 index 000000000..81a342838 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aescbcparams.md @@ -0,0 +1,66 @@ +--- +title: 'AesCbcParams' +description: 'AesCbcParams represents the object that should be passed as the algorithm parameter into the encrypt and decrypt operation when using the AES-CBC algorithm.' +weight: 07 +--- + +# AesCbcParams + +The `AesCbcParams` object represents the object that should be passed as the algorithm parameter into the [encrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt) and [decrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt) operation when using the AES-CBC algorithm. + +For more details, head to the [MDN Web Crypto API documentation on AES-CBC](https://developer.mozilla.org/en-US/docs/Web/API/AesCbcParams). + +## Properties + +| Property | Type | Description | +| :------- | :----------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | Should be set to `AES-CBC`. | +| iv | `ArrayBuffer`, `TypedArray`, or `DataView` | The initialization vector. Must be 16 bytes, unpredictable and cryptographically random. Yet, it doesn't need to be secret and can be transmitted along with the ciphertext. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const plaintext = stringToArrayBuffer('Hello, World!'); + + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const key = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Encrypt the plaintext using the AES-CBC key with + * have generated. + */ + const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: crypto.getRandomValues(new Uint8Array(16)), + }, + key, + plaintext + ); +} + +function stringToArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aesctrparams.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aesctrparams.md new file mode 100644 index 000000000..efeeb82aa --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aesctrparams.md @@ -0,0 +1,68 @@ +--- +title: 'AesCtrParams' +description: 'AesCtrParams represents the object that should be passed as the algorithm parameter into the encrypt and decrypt operation when using the AES-CTR algorithm.' +weight: 06 +--- + +# AesCtrParams + +The `AesCtrParams` object represents the object that should be passed as the algorithm parameter into the [encrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt) and [decrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt) operation when using the AES-CTR algorithm. + +For more details, head to the MDN Web Crypto API documentation on [AES-CTR](https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams). + +## Properties + +| Property | Type | Description | +| :------- | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | Should be set to `AES-CTR`. | +| counter | `ArrayBuffer`, `TypedArray`, or `DataView` | The initial value of the counter block. This must be 16-bytes long, like the AES block size. | +| length | `number` | The number of bits in the counter block that are used for the actual counter. It is recommended to use 64 (half of the counter block). | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const plaintext = stringToArrayBuffer('Hello, World!'); + + /** + * Generate a symmetric key using the AES-CTR algorithm. + */ + const key = await crypto.subtle.generateKey( + { + name: 'AES-CTR', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Encrypt the plaintext using the AES-CTR key with + * have generated. + */ + const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-CTR', + counter: crypto.getRandomValues(new Uint8Array(16)), + length: 128, + }, + key, + plaintext + ); +} + +function stringToArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aesgcmparams.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aesgcmparams.md new file mode 100644 index 000000000..f3fcd4562 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aesgcmparams.md @@ -0,0 +1,68 @@ +--- +title: 'AesGcmParams' +description: 'AesGcmParams represents the object that should be passed as the algorithm parameter into the encrypt and decrypt operation when using the AES-GCM algorithm.' +weight: 08 +--- + +# AesGcmParams + +The `AesGcmParams` object represents the object that should be passed as the algorithm parameter into the [encrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt) and [decrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt) operation when using the AES-GCM algorithm. + +For more details, head to the [MDN Web Crypto API documentation on AES-GCM](https://developer.mozilla.org/en-US/docs/Web/API/AesGcmParams). + +## Properties + +| Property | Type | Description | +| :------------------------ | :----------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | Should be set to `AES-GCM`. | +| iv | `ArrayBuffer`, `TypedArray`, or `DataView` | The initialization vector. It must be 12 bytes long, unpredictable and cryptographically random. It must be unique for every encryption operation carried out with a given key. Never reuse an iv with the same key. Yet, it doesn't need to be secret and can be transmitted along with the ciphertext. | +| additionalData (optional) | `ArrayBuffer`, `TypedArray` or `DataView` | Additional data that should be authenticated, but not encrypted. It must be included in the calculation of the authentication tag, but not encrypted itself. | +| tagLength (optional) | `number` | The length of the authentication tag in bits. Should be set, and will default to 96. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const plaintext = stringToArrayBuffer('Hello, World!'); + + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const key = await crypto.subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Encrypt the plaintext using the AES-CBC key with + * have generated. + */ + const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: crypto.getRandomValues(new Uint8Array(12)), + }, + key, + plaintext + ); +} + +function stringToArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aeskeygenparams.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aeskeygenparams.md new file mode 100644 index 000000000..ca50c9631 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/aeskeygenparams.md @@ -0,0 +1,37 @@ +--- +title: 'AesKeyGenParams' +description: 'AesKeyGenParams represents the object that should be passed as the algorithm parameter into the generateKey operation, when generating an AES key.' +weight: 04 +--- + +# AesKeyGenParams + +The `AesKeyGenParams` object represents the object that should be passed as the algorithm parameter into the [generateKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/generatekey) operation when generating an AES key. + +## Properties + +| Property | Type | Description | +| :------- | :------- | :---------------------------------------------------------------------------------- | +| name | `string` | The name of the algorithm. Possible values are `AES-CBC`, `AES-CTR`, and `AES-GCM`. | +| length | `number` | The length of the key in bits. Possible values are 128, 192 or 256. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const key = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/_index.md new file mode 100644 index 000000000..a529c2721 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/_index.md @@ -0,0 +1,25 @@ +--- +title: 'Crypto' +description: 'Crypto offers basic cryptography features.' +weight: 01 +weight: 01 +--- + +# Crypto + +`Crypto` allows access to a cryptographically strong random number generator and to cryptographic primitives. + +## Properties + +| Name | Type | Description | +| :-------------- | :------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | +| `Crypto.subtle` | [SubtleCrypto](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto) | The SubtleCrypto interface provides access to common cryptographic primitives, such as hashing, signing, encryption, or decryption. | + +## Methods + +| Name | Type | Description | +| :--------------------------------------------------------------------------------------------- | :---------------------------------------------------------- | :---------- | +| `Crypto.getRandomValues()` | [ArrayBuffer] | +| (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) | Returns an array of cryptographically secure random values. | +| `Crypto.randomUUID()` | [ArrayBuffer] | +| (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) | Returns a randomly generated, 36 character long v4 UUID. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/getrandomvalues.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/getrandomvalues.md new file mode 100644 index 000000000..12299df15 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/getrandomvalues.md @@ -0,0 +1,50 @@ +--- +title: 'getRandomValues' +description: 'getRandomValues fills the passed TypedArray with cryptographically sound random values.' +weight: 01 +--- + +# getRandomValues + +The `getRandomValues()` method fills the passed `TypedArray` with cryptographically sound random values. + +## Usage + +``` +getRandomValues(typedArray) +``` + +## Parameters + +| Name | Type | Description | +| :----------- | :----------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `typedArray` | `TypedArray` | An integer-based `TypedArray` to fill with random values. Accepted `TypedArray` specific types are: `Int8Array`, `Uint8Array`, `Uint8ClampedArray`, `Int16Array`, `Uint16Array`, `Int32Array`, or `Uint32Array`. | + +## Return Value + +The same array is passed as the `typedArray` parameter with its contents replaced with the newly generated random numbers. The `typedArray` parameter is modified in place, and no copy is made. + +## Throws + +| Type | Description | +| :------------------- | :------------------------------------------------------------------------ | +| `QuotaExceededError` | Thrown when `typedArray` is too large and its `byteLength` exceeds 65536. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default function () { + const array = new Uint32Array(10); + crypto.getRandomValues(array); + + for (const num of array) { + console.log(num); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/randomuuid.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/randomuuid.md new file mode 100644 index 000000000..0a3a0f693 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/crypto/randomuuid.md @@ -0,0 +1,35 @@ +--- +title: 'randomUUID' +description: 'randomUUID produces a 36-characters long string containing a cryptographically random UUID v4.' +weight: 02 +--- + +# randomUUID + +The `randomUUID` method produces a 36-characters long string that contains a cryptographically random UUID v4. + +## Usage + +``` +randomUUID() +``` + +## Return Value + +A string containing a 36-character cryptographically random UUID v4. + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default function () { + const myUUID = crypto.randomUUID(); + + console.log(myUUID); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/cryptokey.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/cryptokey.md new file mode 100644 index 000000000..1449e18f8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/cryptokey.md @@ -0,0 +1,18 @@ +--- +title: 'CryptoKey' +description: 'CryptoKey represents a cryptographic key used for encryption, decryption, signing, or verification.' +weight: 03 +--- + +# CryptoKey + +The `CryptoKey` object represents a cryptographic key used for [encryption](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt),[decryption](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt),[signing](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/sign), or [verification](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/verify) within the webcrypto module. The `CryptoKey` object is created using the SubtleCrypto.generateKey() or SubtleCrypto.importKey() methods. + +## Properties + +| Property | Type | Description | +| :---------- | :--------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| type | `string` | Indicates the type of the key material. Possible values are `public`, `private`, `secret`, `unspecified`, or `unknown`. | +| extractable | `boolean` | Indicates whether the raw key material can be exported. | +| algorithm | `object` | An object containing the algorithm used to generate or import the key. | +| usages | `string[]` | An array of strings indicating the cryptographic operations that the key can be used for. Possible values are `encrypt`, `decrypt`, `sign`, `verify`, `deriveKey`, `deriveBits`, `wrapKey`, and `unwrapKey`. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/hmackeygenparams.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/hmackeygenparams.md new file mode 100644 index 000000000..3ab71d400 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/hmackeygenparams.md @@ -0,0 +1,39 @@ +--- +title: 'HmacKeyGenParams' +description: 'HmacKeyGenParams represents the object that should be passed as the algorithm parameter into the generateKey operation, when generating an HMAC key.' +weight: 05 +--- + +# HmacKeyGenParams + +The `HmacKeyGenParams` object represents the object that should be passed as the algorithm parameter into the [generateKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/generatekey) operation when generating an HMAC key. + +## Properties + +| Property | Type | Description | +| :---------------- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| name | `string` | The should be set to `HMAC`. | +| hash | `string` | The name of the digest function to use. Possible values are `SHA-1`, `SHA-256`, `SHA-384` and `SHA-512`. | +| length (optional) | `number` | The length of the key in bits. If this is omitted, the length of the key is equal to the block size of the hash function you have chosen. We recommend to leave this parameter empty, unless you have a good reason to use something different. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const key = await crypto.subtle.generateKey( + { + name: 'HMAC', + hash: { name: 'SHA-512' }, + length: 256, + }, + true, + ['sign', 'verify'] + ); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/_index.md new file mode 100644 index 000000000..cac4086b1 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/_index.md @@ -0,0 +1,94 @@ +--- +title: 'SubtleCrypto' +description: 'SubtleCrypto offers low-level cryptographic functions.' +weight: 02 +weight: 02 +--- + +# SubtleCrypto + +The `SubtleCrypto` interface provides a set of low-level cryptographic primitives such as encryption, decryption, digital signature generation and verification, and key generation and management. It is useful for using secure and efficient cryptographic operations within k6 scripts. + +## Methods + +| Method | Description | +| :------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------- | +| [encrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt) | Encrypts the given plaintext data using the specified algorithm and key. | +| [decrypt](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt) | Decrypts the given ciphertext data using the specified algorithm and key. | +| [sign](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/sign) | Signs the given data using the specified algorithm and key. | +| [verify](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/verify) | Verifies the signature of the given data using the specified algorithm and key. | +| [digest](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/digest) | Computes the digest (hash) of the given data using the specified algorithm. | +| [generateKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/generatekey) | Generates a new cryptographic key for use with the specified algorithm. | +| [importKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/importkey) | Imports a raw key material into the Web Crypto API, generating a new key object to use with the specified algorithm. | +| [exportKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/exportkey) | Exports the raw key material of the given key object. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const plaintext = stringToArrayBuffer('Hello, World!'); + + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const key = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Encrypt the plaintext using the AES-CBC key with + * have generated. + */ + const iv = crypto.getRandomValues(new Uint8Array(16)); + const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + plaintext + ); + + /** + * Decrypt the ciphertext using the same key to verify + * that the resulting plaintext is the same as the original. + */ + const deciphered = await crypto.subtle.decrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + ciphertext + ); + + console.log( + 'deciphered text == original plaintext: ', + arrayBufferToHex(deciphered) === arrayBufferToHex(plaintext) + ); +} + +function arrayBufferToHex(buffer) { + return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join(''); +} + +function stringToArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt.md new file mode 100644 index 000000000..ff7473794 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/decrypt.md @@ -0,0 +1,105 @@ +--- +title: 'decrypt' +description: 'decrypt decrypts some encrypted data' +weight: 01 +--- + +# decrypt + +The `decrypt()` method decrypts some encrypted data. + +## Usage + +``` +decrypt(algorithm, key, data) +``` + +## Parameters + +| Name | Type | Description | +| :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `algorithm` | [AesCtrParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aesctrparams),[AesCbcParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aescbcparams), or [AesGcmParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aesgcmparams) object | Defines the algorithm to use and any extra-parameters. The values given for the extra parameters must match those used in the corresponding [encrypt] call. | +| `key` | [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) | The [key](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) to use for decryption. | +| `data` | `ArrayBuffer`, `TypedArray`, or `DataView` | The encrypted data to be decrypted (also known as _ciphertext_). | + +## Return Value + +A `Promise` that resolves to a new `ArrayBuffer` containing the decrypted data. + +## Throws + +| Type | Description | +| :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `InvalidAccessError` | Raised when the requested operation is not valid with the provided key. For instance when an invalid encryption algorithm is used, or a key not matching the selected algorithm is provided. | +| `OperationError` | Raised when the operation failed for an operation-specific reason. For instance, if the algorithm size is invalid, or errors occurred during the process of decrypting the ciphertext. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const plaintext = stringToArrayBuffer('Hello, World!'); + + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const key = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Encrypt the plaintext using the AES-CBC key with + * have generated. + */ + const iv = crypto.getRandomValues(new Uint8Array(16)); + const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + plaintext + ); + + /** + * Decrypt the ciphertext using the same key to verify + * that the resulting plaintext is the same as the original. + */ + const deciphered = await crypto.subtle.decrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + ciphertext + ); + + console.log( + 'deciphered text == original plaintext: ', + arrayBufferToHex(deciphered) === arrayBufferToHex(plaintext) + ); +} + +function arrayBufferToHex(buffer) { + return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join(''); +} + +function stringToArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/digest.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/digest.md new file mode 100644 index 000000000..b145929a1 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/digest.md @@ -0,0 +1,50 @@ +--- +title: 'digest' +description: 'digest decrypts some encrypted data' +weight: 02 +--- + +# digest + +The `digest()` method generates a cryptographically secure [digest](https://developer.mozilla.org/en-US/docs/Glossary/Digest) of the given data. A digest is a short fixed-length value derived from some input data. The `digest()` method is commonly used to compute a checksum of data or to verify the integrity of data. + +## Usage + +``` +digest(algorithm, data) +``` + +## Parameters + +| Name | Type | Description | +| :---------- | :-------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `algorithm` | a `string` or object with a single `name` string property | Names the hash function to use. Supported values are `"SHA-1"`, `"SHA-256"`, `"SHA-384"` and `"SHA-512"`. Note that the SHA-1 hash function is not considered safe for cryptographic use. | +| `data` | `ArrayBuffer`, `TypedArray`, or `DataView` | The data to be digested. | + +## Return Value + +A `Promise` that resolves to a new `ArrayBuffer` containing the digest. + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const digest = await crypto.subtle.digest('SHA-256', stringToArrayBuffer('Hello, world!')); + + console.log(arrayBufferToHex(digest)); +} + +function arrayBufferToHex(buffer) { + return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join(''); +} + +function stringToArrayBuffer(s) { + return Uint8Array.from(new String(s), (x) => x.charCodeAt(0)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt.md new file mode 100644 index 000000000..6be1d7dd3 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/encrypt.md @@ -0,0 +1,105 @@ +--- +title: 'encrypt' +description: 'encrypt decrypts some encrypted data' +weight: 03 +--- + +# encrypt + +The `encrypt()` method encrypts some data. + +## Usage + +``` +encrypt(algorithm, key, data) +``` + +## Parameters + +| Name | Type | Description | +| :---------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------- | +| `algorithm` | [AesCtrParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aesctrparams),[AesCbcParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aescbcparams), or [AesGcmParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aesgcmparams) object | Defines the algorithm to use and any extra-parameters. | +| `key` | [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) | The [key](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) to use for encryption. | +| `data` | `ArrayBuffer`, `TypedArray`, or `DataView` | The data to be encrypted (also known as "plaintext"). | + +## Return Value + +A `Promise` that resolves to a new `ArrayBuffer` containing the encrypted data. + +## Throws + +| Type | Description | +| :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `InvalidAccessError` | Raised when the requested operation is not valid with the provided key. For instance when an invalid encryption algorithm is used, or a key not matching the selected algorithm is provided. | +| `OperationError` | Raised when the operation failed for an operation-specific reason. For instance, if the algorithm size is invalid, or errors occurred during the process of decrypting the ciphertext. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const plaintext = stringToArrayBuffer('Hello, World!'); + + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const key = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Encrypt the plaintext using the AES-CBC key with + * have generated. + */ + const iv = crypto.getRandomValues(new Uint8Array(16)); + const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + plaintext + ); + + /** + * Decrypt the ciphertext using the same key to verify + * that the resulting plaintext is the same as the original. + */ + const deciphered = await crypto.subtle.decrypt( + { + name: 'AES-CBC', + iv: iv, + }, + key, + ciphertext + ); + + console.log( + 'deciphered text == original plaintext: ', + arrayBufferToHex(deciphered) === arrayBufferToHex(plaintext) + ); +} + +function arrayBufferToHex(buffer) { + return [...new Uint8Array(buffer)].map((x) => x.toString(16).padStart(2, '0')).join(''); +} + +function stringToArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/exportkey.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/exportkey.md new file mode 100644 index 000000000..b14ba851b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/exportkey.md @@ -0,0 +1,75 @@ +--- +title: 'exportKey' +description: 'exportKey exports a key in an external, portable format.' +weight: 04 +--- + +# exportKey + +The `exportKey()` method takes a [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) object as input and exports it in an external, portable format. + +Note that for a key to be exportable, it must have been created with the `extractable` flag set to `true`. + +## Usage + +``` +exportKey(format, key) +``` + +## Parameters + +| Name | Type | Description | +| :------- | :------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------- | +| `format` | `string` | Defines the data format the key should be exported in. Currently supported formats: `raw`, `jwk`. | +| `key` | [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) | The [key](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) to export. | + +## Return Value + +A `Promise` that resolves to a new `ArrayBuffer` containing the key. + +## Throws + +| Type | Description | +| :------------------- | :-------------------------------------------------- | +| `InvalidAccessError` | Raised when trying to export a non-extractable key. | +| `NotSupportedError` | Raised when trying to export in an unknown format. | +| `TypeError` | Raised when trying to use an invalid format. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const generatedKey = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: '256', + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Export the key in raw format. + */ + const exportedKey = await crypto.subtle.exportKey('raw', generatedKey); + + /** + * Reimport the key in raw format to verify its integrity. + */ + const importedKey = await crypto.subtle.importKey('raw', exportedKey, 'AES-CBC', true, [ + 'encrypt', + 'decrypt', + ]); + + console.log(JSON.stringify(importedKey)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/generatekey.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/generatekey.md new file mode 100644 index 000000000..4b5ca1ecc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/generatekey.md @@ -0,0 +1,56 @@ +--- +title: 'generateKey' +description: 'generateKey generates a new key.' +weight: 05 +--- + +# generateKey + +The `generateKey()` generates a new cryptographic key and returns it as a [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) object that can be used with the Web Crypto API. + +## Usage + +``` +generateKey(algorithm, extractable, keyUsages) +``` + +## Parameters + +| Name | Type | Description | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `algorithm` | `string`,[AesKeyGenParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aeskeygenparams) or [HmacKeyGenParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/hmackeygenparams) | The type of key to generate. Can be either a string with any of the currently supported algorithms: `AES-CBC`, `AES-GCM`, `AES-CTR`, and `HMAC` as value, or any of the [AesKeyGenParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/aeskeygenparams) or [HmacKeyGenParams](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/hmackeygenparams) objects. | +| `extractable` | `boolean` | Whether the key can be exported using [exportKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/exportkey). | +| `keyUsages` | `Array` | An array of strings describing what operations can be performed with the key. Currently supported usages include `encrypt`, `decrypt`, `sign`, and `verify`. | + +## Return Value + +A `Promise` that resolves with the generated key as a [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) object. + +## Throws + +| Type | Description | +| :------------ | :------------------------------------------------------------------------------------------- | +| `SyntaxError` | Raised when the `keyUsages` parameter is empty but the key is of type `secret` or `private`. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const key = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + console.log(JSON.stringify(key)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/importkey.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/importkey.md new file mode 100644 index 000000000..83d72666c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/importkey.md @@ -0,0 +1,222 @@ +--- +title: 'importKey' +description: 'importKey imports a key from an external, portable format and gives you a CryptoKey object.' +weight: 06 +--- + +# importKey + +The `importKey()` imports a key from an external, portable format, and gives you a [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) object that can be used with the Web Crypto API. + +## Usage + +``` +importKey(format, keyData, algorithm, extractable, keyUsages) +``` + +## Parameters + +| Name | Type | Description | +| :------------ | :-------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `format` | `string` | Defines the data format of the key to import. Currently supported formats: `raw`, `jwk`. | +| `keyData` | `ArrayBuffer`, `TypedArray` or `DataView` | the data to import the key from. | +| `algorithm` | a `string` or object with a single `name` string property | The algorithm to use to import the key. Currently supported algorithms: `AES-CBC`, `AES-GCM`, `AES-CTR`, and `HMAC`. | +| `extractable` | `boolean` | Indicates whether it will be possible to export the key using [exportKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/subtlecrypto/exportkey). | +| `keyUsages` | `Array` | An array of strings describing what operations can be performed with the key. Currently supported usages include `encrypt`, `decrypt`, `sign`, and `verify`. | + +## Return Value + +A `Promise` that resolves with the imported key as a [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) object. + +## Throws + +| Type | Description | +| :------------ | :---------------------------------------------------------------------------------------------- | +| `SyntaxError` | Raised when the `keyUsages` parameter is empty but the key is of type `secret` or `private`. | +| `TypeError` | Raised when trying to use an invalid format, or if the `keyData` is not suited for that format. | + +## Examples + +### Round-trip key export/import + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + /** + * Generate a symmetric key using the AES-CBC algorithm. + */ + const generatedKey = await crypto.subtle.generateKey( + { + name: 'AES-CBC', + length: '256', + }, + true, + ['encrypt', 'decrypt'] + ); + + /** + * Export the key in raw format. + */ + const exportedKey = await crypto.subtle.exportKey('raw', generatedKey); + + /** + * Reimport the key in raw format to verify its integrity. + */ + const importedKey = await crypto.subtle.importKey('raw', exportedKey, 'AES-CBC', true, [ + 'encrypt', + 'decrypt', + ]); + + console.log(JSON.stringify(importedKey)); +} +``` + +{{< /code >}} + +### Import a static raw key and decrypt transmitted data + +This example demonstrates how to import a static `raw` key and decrypt some transmitted data in `base64`. The transmitted data in this example represents an initialization vector and encoded data, and in a real-world scenario, it can be a response body or other data received from a request. + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; +import { b64decode } from 'k6/encoding'; + +export default async function () { + const transmittedData = base64Decode( + 'whzEN310mrlWIH/icf0dMquRZ2ENyfOzkvPuu92WR/9F8dbeFM8EGUVNIhaS' + ); + + // keyData is the key used to decrypt the data, which is usually stored in a secure location + // for this example, we are using a static key + const keyData = new Uint8Array([ + 109, 151, 76, 33, 232, 253, 176, 90, 94, 40, 146, 227, 139, 208, 245, 139, 69, 215, 55, 197, 43, + 122, 160, 178, 228, 104, 4, 115, 138, 159, 119, 49, + ]); + + try { + const result = await decrypt(keyData, transmittedData); + + // should output decrypted message + // INFO[0000] result: 'my secret message' source=console + console.log("result: '" + result + "'"); + } catch (e) { + console.log('Error: ' + JSON.stringify(e)); + } +} + +const decrypt = async (keyData, transmittedData) => { + const initializeVectorLength = 12; + + // the first 12 bytes are the initialization vector + const iv = new Uint8Array(transmittedData.subarray(0, initializeVectorLength)); + + // the rest of the transmitted data is the encrypted data + const encryptedData = new Uint8Array(transmittedData.subarray(initializeVectorLength)); + + const importedKey = await crypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-GCM', length: '256' }, + true, + ['decrypt'] + ); + + const plain = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: iv }, + importedKey, + encryptedData + ); + + return arrayBufferToString(plain); +}; + +const arrayBufferToString = (buffer) => { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); +}; + +const base64Decode = (base64String) => { + return new Uint8Array(b64decode(base64String)); +}; +``` + +{{< /code >}} + +### Import a static JWK key and decrypt transmitted data + +This example is similar to the previous one. It demonstrates how to import a static `jwk` key and decrypt some transmitted data (which contains the initialization vector and encoded data) in `base64`. + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; +import { b64decode } from 'k6/encoding'; + +export default async function () { + // transmitted data is the base64 of the initialization vector + encrypted data + // that unusually transmitted over the network + const transmittedData = base64Decode( + 'drCfxl4O+5FcrHe8Bs0CvKlw3gZpv+S5if3zn7c4BJzHJ35QDFV4sJB0pbDT' + ); + + // keyData is the key used to decrypt the data, which is usually stored in a secure location + // for this example, we are using a static key + const jwkKeyData = { + kty: 'oct', + ext: true, + key_ops: ['decrypt', 'encrypt'], + alg: 'A256GCM', + k: '9Id_8iG6FkGOWmc1S203vGVnTExtpDGxdQN7v7OV9Uc', + }; + + try { + const result = await decrypt(jwkKeyData, transmittedData); + + // should output decrypted message + // INFO[0000] result: 'my secret message' source=console + console.log("result: '" + result + "'"); + } catch (e) { + console.log('Error: ' + JSON.stringify(e)); + } +} + +const decrypt = async (keyData, transmittedData) => { + const initializeVectorLength = 12; + + // the first 12 bytes are the initialization vector + const iv = new Uint8Array(transmittedData.subarray(0, initializeVectorLength)); + + // the rest of the transmitted data is the encrypted data + const encryptedData = new Uint8Array(transmittedData.subarray(initializeVectorLength)); + + const importedKey = await crypto.subtle.importKey( + 'jwk', + keyData, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); + + const plain = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: iv }, + importedKey, + encryptedData + ); + + return arrayBufferToString(plain); +}; + +const arrayBufferToString = (buffer) => { + return String.fromCharCode.apply(null, new Uint8Array(buffer)); +}; + +const base64Decode = (base64String) => { + return new Uint8Array(b64decode(base64String)); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/sign.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/sign.md new file mode 100644 index 000000000..1f06dee25 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/sign.md @@ -0,0 +1,78 @@ +--- +title: 'sign' +description: 'sign generates a digital signature.' +weight: 07 +--- + +# sign + +The `sign()` operation generates a digital signature of the provided `data`, using the given [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) object. + +## Usage + +``` +sign(algorithm, key, data) +``` + +## Parameters + +| Name | Type | Description | +| :---------- | :------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------- | +| `algorithm` | `string` or object with a single `name` string property | The signature algorithm to use. Currently supported: `HMAC`. | +| `key` | [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) | The key to use for signing. | +| `data` | `ArrayBuffer`, `TypedArray`, or `DataView` | The data to be signed. | + +## Return Value + +A `Promise` that resolves with the signature as an `ArrayBuffer`. + +## Throws + +| Type | Description | +| :------------------- | :--------------------------------------------------------------------------------------------------------------------- | +| `InvalidAccessError` | Raised when the signing key either does not support signing operation, or is incompatible with the selected algorithm. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const generatedKey = await crypto.subtle.generateKey( + { + name: 'HMAC', + hash: { name: 'SHA-1' }, + }, + true, + ['sign', 'verify'] + ); + + const data = string2ArrayBuffer('Hello World'); + + /** + * Signes the encoded data with the provided key using the HMAC algorithm + * the returned signature can be verified using the verify method. + */ + const signature = await crypto.subtle.sign('HMAC', generatedKey, data); + + /** + * Verifies the signature of the encoded data with the provided key using the HMAC algorithm. + */ + const verified = await crypto.subtle.verify('HMAC', generatedKey, signature, data); + + console.log('verified: ', verified); +} + +function string2ArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/verify.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/verify.md new file mode 100644 index 000000000..da94143a4 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/webcrypto/subtlecrypto/verify.md @@ -0,0 +1,79 @@ +--- +title: 'verify' +description: 'verify verifies a digital signature.' +weight: 08 +--- + +# verify + +The `verify()` operation verifies a digital signature. It ensures that some data was signed by a known key and that the data has not been tampered with since it was signed. + +## Usage + +``` +verify(algorithm, key, signature, data) +``` + +## Parameters + +| Name | Type | Description | +| :---------- | :------------------------------------------------------------------------------------------------------- | :------------------------------------------------- | +| `algorithm` | `string` or object with a single `name` string property | The algorithm to use. Currently supported: `HMAC`. | +| `key` | [CryptoKey](https://grafana.com/docs/k6//javascript-api/k6-experimental/webcrypto/cryptokey) | The key that will be used to verify the signature. | +| `signature` | `ArrayBuffer` | The signature to verify. | +| `data` | `ArrayBuffer` | The data whose signature is to be verified. | + +## Return Value + +A `Promise` that resolves to a `boolean` value indicating if the signature is valid. + +## Throws + +| Type | Description | +| :------------------- | :------------------------------------------------------------------------------------------------------------------ | +| `InvalidAccessError` | Raised when the key either does not support the `verify` operation, or is incompatible with the selected algorithm. | + +## Example + +{{< code >}} + +```javascript +import { crypto } from 'k6/experimental/webcrypto'; + +export default async function () { + const generatedKey = await crypto.subtle.generateKey( + { + name: 'HMAC', + hash: { name: 'SHA-1' }, + }, + true, + ['sign', 'verify'] + ); + + const data = string2ArrayBuffer('Hello World'); + + /** + * Signes the encoded data with the provided key using the HMAC algorithm + * the returned signature can be verified using the verify method. + */ + const signature = await crypto.subtle.sign('HMAC', generatedKey, data); + + /** + * Verifies the signature of the encoded data with the provided key using the HMAC algorithm. + */ + const verified = await crypto.subtle.verify('HMAC', generatedKey, signature, data); + + console.log('verified: ', verified); +} + +function string2ArrayBuffer(str) { + const buf = new ArrayBuffer(str.length * 2); // 2 bytes for each char + const bufView = new Uint16Array(buf); + for (let i = 0, strLen = str.length; i < strLen; i++) { + bufView[i] = str.charCodeAt(i); + } + return buf; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/_index.md new file mode 100644 index 000000000..ec07617db --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/_index.md @@ -0,0 +1,123 @@ +--- +title: 'websockets' +descriptiontion: "k6 websockets experimental API" +weight: 05 +weight: 05 +--- + +# websockets + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +This experimental API implements the browser [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) with additional k6-specific functionalities (cookies, tags, headers and so on). + +The main difference between this module and [`k6/ws`](https://grafana.com/docs/k6//javascript-api/k6-ws) is that this module uses a global event loop instead of a local one. +A global event loop lets a single VU have multiple concurrent connections, which improves performance. + +The `WebSocket API` is not fully implemented, and we're working on it, but we believe it's usable for most users. So whether you're writing a new WebSocket test, or currently using the `k6/ws` module, we invite you to give it a try, and report any issues in the project's [issue tracker](https://github.com/grafana/xk6-websockets/). Our midterm goal is to make this module part of k6 core, and long-term to replace the [`k6/ws`](https://grafana.com/docs/k6//javascript-api/k6-ws) module. + +| Class/Method | Description | +| --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/params) | Used for setting various WebSocket connection parameters such as headers, cookie jar, compression, etc. | +| [WebSocket(url, protocols, params)](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket) | Constructs a new WebSocket connection. | +| [WebSocket.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-close) | Close the WebSocket connection. | +| [WebSocket.ping()](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-ping) | Send a ping. | +| [WebSocket.send(data)](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-send) | Send string data. | +| [WebSocket.addEventListener(event, handler)](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener) | Add an event listener on the connection for specific event. | + +A WebSocket instance also has the following properties: + + + +| Class/Property | Description | +| --------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| WebSocket.readyState | The current state of the connection. Could be one of [the four states](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState). | +| WebSocket.url | The URL of the connection as resolved by the constructor. | +| WebSocket.bufferedAmount | The number of bytes of data that have been queued using calls to `send()` but not yet transmitted to the network. | +| WebSocket.binaryType | The [`binaryType`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType) is by default `"ArrayBuffer"`. Setting it throws an exception, as k6 does not support the Blob type. | +| [WebSocket.onmessage](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-onmessage) | A handler for `message` events. | +| [WebSocket.onerror](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-onerror) | A handler for `error` events. | +| [WebSocket.onopen](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-onopen) | A handler for `open` events. | +| [WebSocket.onclose](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-onclose) | A handler for `close` events. | +| [WebSocket.onping](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-onping) | A handler for `ping` events. | +| [WebSocket.onpong](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-onpong) | A handler for `pong` events. | + + + +## Websocket metrics + +k6 takes specific measurements for Websockets. +For the complete list, refer to the [Metrics reference](https://grafana.com/docs/k6//using-k6/metrics/reference#websockets). + +## Example + +This example shows: + +- How a single VU can run multiple WebSockets connections asynchronously. +- How to use the timeout and interval functions to stop the connections after some period. + +{{< code >}} + +```javascript +import { randomString, randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.1.0/index.js'; +import { WebSocket } from 'k6/experimental/websockets'; +import { setTimeout, clearTimeout, setInterval, clearInterval } from 'k6/experimental/timers'; + +const chatRoomName = 'publicRoom'; // choose any chat room name +const sessionDuration = randomIntBetween(5000, 60000); // user session between 5s and 1m + +export default function () { + for (let i = 0; i < 4; i++) { + startWSWorker(i); + } +} + +function startWSWorker(id) { + // create a new websocket connection + const ws = new WebSocket(`wss://test-api.k6.io/ws/crocochat/${chatRoomName}/`); + + ws.addEventListener('open', () => { + // change the user name + ws.send(JSON.stringify({ event: 'SET_NAME', new_name: `Croc ${__VU}:${id}` })); + + // listen for messages/errors and log them into console + ws.addEventListener('message', (e) => { + const msg = JSON.parse(e.data); + if (msg.event === 'CHAT_MSG') { + console.log(`VU ${__VU}:${id} received: ${msg.user} says: ${msg.message}`); + } else if (msg.event === 'ERROR') { + console.error(`VU ${__VU}:${id} received:: ${msg.message}`); + } else { + console.log(`VU ${__VU}:${id} received unhandled message: ${msg.message}`); + } + }); + + // send a message every 2-8 seconds + const intervalId = setInterval(() => { + ws.send(JSON.stringify({ event: 'SAY', message: `I'm saying ${randomString(5)}` })); + }, randomIntBetween(2000, 8000)); // say something every 2-8 seconds + + // after a sessionDuration stop sending messages and leave the room + const timeout1id = setTimeout(function () { + clearInterval(intervalId); + console.log(`VU ${__VU}:${id}: ${sessionDuration}ms passed, leaving the chat`); + ws.send(JSON.stringify({ event: 'LEAVE' })); + }, sessionDuration); + + // after a sessionDuration + 3s close the connection + const timeout2id = setTimeout(function () { + console.log(`Closing the socket forcefully 3s after graceful LEAVE`); + ws.close(); + }, sessionDuration + 3000); + + // when connection is closing, clean up the previously created timers + ws.addEventListener('close', () => { + clearTimeout(timeout1id); + clearTimeout(timeout2id); + console.log(`VU ${__VU}:${id}: disconnected`); + }); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/params.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/params.md new file mode 100644 index 000000000..b55ed8281 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/params.md @@ -0,0 +1,54 @@ +--- +title: 'Params' +description: 'Used for setting various WebSocket request-specific parameters such as headers, tags, etc.' +description: 'Used for setting various WebSocket request-specific parameters such as headers, tags, etc.' +weight: 20 +--- + +# Params + +`Params` is an object used by the WebSocket constructor. The `Params` object contains request-specific options, such as headers that should be inserted into the connection initialization request. + +| Name | Type | Description | +| -------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Params.compression` | string | Compression algorithm to be used by the WebSocket connection. The only supported algorithm currently is `deflate`. | +| `Params.jar` | [http.CookieJar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) | The cookie jar that will be used when making the initial HTTP request to establish the WebSocket connection. If empty, the [default VU cookie jar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) will be used. | +| `Params.headers` | object | Custom HTTP headers in key-value pairs that will be added to the initial HTTP request to establish the WebSocket connection. Keys are header names and values are header values. | +| `Params.tags` | object | [Custom metric tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#user-defined-tags) in key-value pairs where the keys are names of tags and the values are tag values. The WebSocket connection will [generate metrics samples](https://grafana.com/docs/k6//javascript-api/k6-ws/socket#websocket-built-in-metrics) with these tags attached, allowing users to filter the results data or set [thresholds on sub-metrics](https://grafana.com/docs/k6//using-k6/thresholds#thresholds-on-tags). | + +### Example of custom metadata headers and tags + +_A k6 script that makes a WebSocket request with a custom header and tags results data with a specific tag_ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const url = 'ws://localhost:10000'; + const params = { + headers: { 'X-MyHeader': 'k6test' }, + tags: { k6test: 'yes' }, + }; + + const ws = new WebSocket(url, null, params); + + ws.onopen = () => { + console.log('WebSocket connection established!'); + ws.close(); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/_index.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/_index.md new file mode 100644 index 000000000..b97042d43 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/_index.md @@ -0,0 +1,60 @@ +--- +title: 'WebSocket' +description: 'Create a WebSocket connection, and provides a WebSocket instance to interact with the service.' +description: 'Create a WebSocket connection, and provides a WebSocket instance to interact with the service.' +weight: 10 +weight: 10 +--- + +# WebSocket + +Creates a WebSocket instance for connection to a remote host. + +The following events can close the connection: + +- remote host close event. +- [WebSocket.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-close). +- k6 VU interruption based on test configuration or CLI commands. + +| Parameter | Type | Description | +| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| url | string | The URL to which to connect (e.g. "ws://localhost:10000"). | +| protocols | array | Not yet implemented, reserved for the future use. | +| params | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| --------- | -------------------------------- | +| WebSocket | An instance of WebSocket object. | + +### Example + +_A k6 script that initiates a WebSocket connection._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onopen = () => { + console.log('WebSocket connection established!'); + ws.close(); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener.md new file mode 100644 index 000000000..52ed0ce6c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener.md @@ -0,0 +1,65 @@ +--- +title: 'WebSocket.addEventListener(event, handler)' +description: 'Set up handler functions for various events on the WebSocket connection.' +weight: 10 +--- + +# WebSocket.addEventListener(event, handler) + +Set up handler functions for various events on the WebSocket connection. You can define multiple handlers for the same event. + +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------- | +| event | string | The event name to define a handler for. | +| handler | function | The function to call when the event happens. | + +| Event name | Description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| open | Emitted when the connection is established. | +| message | Emitted when a message is received from the server. | +| ping | Emitted when a ping is received from the server. The client will automatically send back a `pong`. | +| pong | Emitted when a pong is received from the server. | +| close | Emitted when the connection is closed by the client [WebSocket.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-close) or when the server sends the `close` event with code status 1000 (normal closure). | +| error | Emitted when an error occurs. | + +### Example + +_A k6 script that demonstrates how to add multiple event listeners for the WebSocket `message` connection event._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onopen = () => { + console.log('connected'); + ws.send(Date.now().toString()); + }; + + ws.onmessage = () => { + console.log('onmessage event handler!'); + }; + + // Multiple event handlers on the same event + ws.addEventListener('message', () => { + console.log('addEventListener event handler!'); + + ws.close(); + }); +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-close.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-close.md new file mode 100644 index 000000000..e9d1f2604 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-close.md @@ -0,0 +1,44 @@ +--- +title: 'WebSocket.close([code])' +description: 'Close the WebSocket connection.' +weight: 15 +--- + +# WebSocket.close([code]) + +Close the WebSocket connection. + +| Parameter | Type | Description | +| --------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| code (optional) | number | WebSocket status code. (default: 1000) . See [the list of supported](https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code) close codes. | + +### Example + +_A k6 script that initiates a WebSocket connection and closes it using the `onopen` handler._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onopen = () => { + ws.close(); + console.log('connection closed'); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onclose.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onclose.md new file mode 100644 index 000000000..001527a4e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onclose.md @@ -0,0 +1,46 @@ +--- +title: 'WebSocket.onclose' +description: 'A handler function for WebSocket connection close event.' +weight: 30 +--- + +# WebSocket.onclose + +A handler for a WebSocket connection `close` event. +For multiple, simultaneous event handlers, use [`WebSocket.addEventListener()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener). + +### Example + +_A k6 script that initiates a WebSocket connection, sends a ping, and closes it using `onopen` handler. +The console should also log `WebSocket connection closed` from the `onclose` handler._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onopen = () => { + console.log('WebSocket connection established!'); + ws.close(); + }; + + ws.onclose = () => { + console.log('WebSocket connection closed!'); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onerror.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onerror.md new file mode 100644 index 000000000..b44c79199 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onerror.md @@ -0,0 +1,41 @@ +--- +title: 'WebSocket.onerror' +description: 'A handler function for WebSocket connection error event.' +weight: 30 +--- + +# WebSocket.onerror + +A handler for a WebSocket connection `error` event. +For multiple, simultaneous event handlers, use [`WebSocket.addEventListener()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener). + +### Example + +_A k6 script that initiates a WebSocket connection and sets up a handler for the `error` event._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onerror = (e) => { + console.log(e); + ws.close(); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onmessage.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onmessage.md new file mode 100644 index 000000000..832fa7392 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onmessage.md @@ -0,0 +1,42 @@ +--- +title: 'WebSocket.onmessage' +descriptiontion: 'A handler function for message event WebSocket.' +weight: 30 +--- + +# WebSocket.onmessage + +A handler for a WebSocket connection `message` event. +For multiple, simultaneous event handlers, use [`WebSocket.addEventListener()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener). + +### Example + +_A k6 script that initiates a WebSocket connection and sets up a handler for the `message` event._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onmessage = (data) => { + console.log('a message received'); + console.log(data); + ws.close(); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onopen.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onopen.md new file mode 100644 index 000000000..ba6cd48ef --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onopen.md @@ -0,0 +1,41 @@ +--- +title: 'WebSocket.onopen' +description: 'A handler function for WebSocket connection open event.' +weight: 30 +--- + +# WebSocket.onopen + +A handler for a WebSocket connection `open` event. +For multiple, simultaneous event handlers, use [`WebSocket.addEventListener()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener). + +### Example + +_A k6 script that initiates a WebSocket connection and sets up a handler for the `open` event._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onopen = () => { + console.log('WebSocket connection established!'); + ws.close(); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onping.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onping.md new file mode 100644 index 000000000..72c405150 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onping.md @@ -0,0 +1,42 @@ +--- +title: 'WebSocket.onping' +description: 'A handler function for WebSocket connection ping event.' +weight: 30 +--- + +# WebSocket.onping + +A handler for a WebSocket connection `ping` event. +For multiple, simultaneous event handlers, use [`WebSocket.addEventListener()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener). + +### Example + +_A k6 script that initiates a WebSocket connection and sets up a handler for the `ping` event._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('wss://test-api.k6.io/ws/crocochat/publicRoom/'); + + ws.onping = () => { + console.log('A ping happened!'); + ws.close(); + }; + + ws.onclose = () => { + console.log('WebSocket connection closed!'); + }; + + ws.onopen = () => { + ws.send(JSON.stringify({ event: 'SET_NAME', new_name: `Croc ${__VU}` })); + }; + ws.onerror = (err) => { + console.log(err); + }; +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onpong.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onpong.md new file mode 100644 index 000000000..5336cc15b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-onpong.md @@ -0,0 +1,45 @@ +--- +title: 'WebSocket.onpong' +description: 'A handler function for WebSocket connection pong event.' +weight: 30 +--- + +# WebSocket.onpong + +A handler for a WebSocket connection `pong` event. +For multiple, simultaneous event handlers, use [`WebSocket.addEventListener()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets/websocket/websocket-addeventlistener). + +### Example + +_A k6 script that initiates a WebSocket connection and setups a handler for the `pong` event._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onpong = () => { + console.log('A pong happened!'); + ws.close(); + }; + + ws.onopen = () => { + ws.ping(); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-ping.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-ping.md new file mode 100644 index 000000000..1fde998cc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-ping.md @@ -0,0 +1,46 @@ +--- +title: 'WebSocket.ping()' +descriptiontion: 'Send a ping. Ping messages can be used to verify that the remote endpoint is responsive.' +weight: 20 +--- + +# WebSocket.ping() + +Send a ping. You can use ping messages to verify that the remote endpoint is responsive. + +### Example + +_A k6 script that initiates a WebSocket connection, sends a ping, and closes it using the `onopen` handler. The console should log `connection is alive`, since the recipient should automatically emit the `pong` event._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onopen = () => { + console.log('WebSocket connection established!'); + ws.ping(); + ws.close(); + }; + + ws.onpong = () => { + // As required by the spec, when the ping is received, the recipient must send back a pong. + console.log('connection is alive'); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-send.md b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-send.md new file mode 100644 index 000000000..600533b27 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-experimental/websockets/websocket/websocket-send.md @@ -0,0 +1,45 @@ +--- +title: 'WebSocket.send(data)' +description: 'Send a data string through the connection.' +weight: 10 +--- + +# WebSocket.send(data) + +Send a data string through the connection. +You can use `JSON.stringify` to convert a JSON or JavaScript values to a JSON string. + +| Parameter | Type | Description | +| --------- | -------------------- | ----------------- | +| data | string / ArrayBuffer | The data to send. | + +### Example + +_A k6 script that demonstrates how to add an event listener for the `open` WebSocket connection event sends a message and closes the connection._ + +{{< code >}} + +```javascript +import { WebSocket } from 'k6/experimental/websockets'; + +export default function () { + const ws = new WebSocket('ws://localhost:10000'); + + ws.onopen = () => { + ws.send('lorem ipsum'); + ws.close(); + }; +} +``` + +{{< /code >}} + +The preceding example uses a WebSocket echo server, which you can run with the following command: + +{{< code >}} + +```bash +$ docker run --detach --rm --name ws-echo-server -p 10000:8080 jmalloc/echo-server +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/_index.md b/docs/sources/v0.50.x/javascript-api/k6-html/_index.md new file mode 100644 index 000000000..9ae182ebe --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/_index.md @@ -0,0 +1,18 @@ +--- +title: 'k6/html' +description: 'The k6/html module contains functionality for HTML parsing.' +weight: 08 +--- + +# k6/html + +The k6/html module contains functionality for HTML parsing. + +| Function | Description | +| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| [parseHTML(src)](https://grafana.com/docs/k6//javascript-api/k6-html/parsehtml) | Parse an HTML string and populate a [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) object. | + +| Class | Description | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | An HTML DOM element as returned by the [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) API. | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A jQuery-like API for accessing HTML DOM elements. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/element/_index.md b/docs/sources/v0.50.x/javascript-api/k6-html/element/_index.md new file mode 100644 index 000000000..cac1805d7 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/element/_index.md @@ -0,0 +1,164 @@ +--- +title: 'Element' +description: 'An HTML DOM element as returned by the Selection API.' +description: 'An HTML DOM element as returned by the Selection API.' +weight: 20 +--- + +# Element + +Represents a DOM element matched by a [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection), +and provides an API to inspect the element content. + +Use [Selection.get(index)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-get) to return an Element object. + +The Element object provides a similar API to the [DOM Element API](https://developer.mozilla.org/en-US/Web/API/Element) to retrieve element information. + +| Method | Description | +| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [selection](https://grafana.com/docs/k6//javascript-api/k6-html/element/element-selection) | The selection matching the element. | +| nodeName | The name of the element. | +| nodeType | The type of the element. | +| nodeValue | The element value. | +| id | The id of the element. | +| innerHTML | Is a DOMString representing the markup of the element's content. | +| textContent | The element content. | +| ownerDocument | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| attributes | An array of attributes. | +| firstChild | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| lastChild | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| childElementCount | The number of children elements. | +| firstElementChild | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| lastElementChild | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| previousSibling | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| nextSibling | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| previousElementSibling | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| nextElementSibling | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| parentElement | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| parentNode | [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| childNodes | Array of [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| children | Array of [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) | +| classList | An array of class names. | +| className | The class name string | +| lang | The value of the lang attribute. | +| toString | The element string representation. | +| hasAttribute | Boolean | +| getAttribute | getAttributeNode | +| hasAttributes | Boolean | +| hasChildNodes | Boolean | +| isSameNode | Boolean | +| isEqualNode | Boolean | +| getElementsByClassName | Return an array of [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element). | +| getElementsByTagName | Return an array of [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element). | +| querySelector | Returns the first [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) which matches the specified selector string relative to the element | +| querySelectorAll | Returns all the [Element](https://grafana.com/docs/k6//javascript-api/k6-html/element) which matches the specified selector string relative to the element | +| contains | | +| matches | Returns a Boolean indicating whether or not the element would be selected by the specified selector string | +| namespaceURI | The namespace URI of the element. | +| isDefaultNamespace | Returns a Boolean indicating whether the element has the default namespace. | + +Additionally, Element can provide more methods depending on the Element type. + +- **AnchorElement**: hash, host, hostname, port, username, password, origin, pathname, protocol, relist, search, text. + +- **ButtonElement**: form, formAction, formEnctype, formMethod, formNoValidate, formTarget, labels, name, value. + +- **CanvasElement**: width, height + +- **DataListElement**: options + +- **FieldSetElement**: elements, type, form + +- **FormElement**: elements, length, method + +- **InputElement**: form + +- **LabelElement**: control, form + +- **LegendElement**: form + +- **LinkElement**: relList + +- **MapElement**: areas, images + +- **ObjectElement**: form + +- **OptionElement**: disabled, form, index, label, text, value + +- **OutputElement**: value, labels + +- **ProgressElement**: max, value, position + +- **ScriptElement**: text + +- **SelectElement**: form, length, options, selectedOptions, selectedIndex, value + +- **StyleElement**: text + +- **TableElement**: caption, thead, tbody, tfoot, rows + +- **TableCellElement**: cellIndex, colSpan, rowSpan, headers + +- **TableRowElement**: cells, colSpan, sectionRowIndex, rowIndex + +- **VideoElement**: textTracks + +- **TitleElement**: text + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
Value term 1
+
Value term 2
+
+ `; + const sel = parseHTML(content).find('dl').children(); + + const el1 = sel.get(0); + const el2 = sel.get(1); + + console.log(el1.nodeName()); + console.log(el1.id()); + console.log(el1.textContent()); + + console.log(el2.nodeName()); + console.log(el2.id()); + console.log(el2.textContent()); + + sleep(1); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +\t6 + `; + const el = parseHTML(content).find('a').get(0); + + console.log(el.nodeName()); + console.log(el.innerHTML()); + console.log(el.host()); + console.log(el.hostname()); + console.log(el.protocol()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/element/element-selection.md b/docs/sources/v0.50.x/javascript-api/k6-html/element/element-selection.md new file mode 100644 index 000000000..184316e26 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/element/element-selection.md @@ -0,0 +1,34 @@ +--- +title: 'Element.selection()' +description: 'Retrieve the Selection matching this element.' +--- + +# Element.selection() + +Retrieve the Selection matching this element. + +Mimics `$(element)`. + +### Returns + +| Type | Description | +| --------- | ------------------------------------ | +| Selection | The Selection matching this element. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default () => { + const li = http.get('https://test.k6.io').html().find('li'); + li.each(function (_, element) { + const container = element.selection().closest('ul.header-icons'); + console.log('li.each', container.html()); + }); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/parsehtml.md b/docs/sources/v0.50.x/javascript-api/k6-html/parsehtml.md new file mode 100644 index 000000000..9e621beae --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/parsehtml.md @@ -0,0 +1,38 @@ +--- +title: 'parseHTML( src )' +description: 'Parse an HTML string and populate a Selection object.' +description: 'Parse an HTML string and populate a Selection object.' +weight: 10 +--- + +# parseHTML( src ) + +Parse an HTML string and populate a [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) object. + +| Parameter | Type | Description | +| --------- | ------ | ------------ | +| src | string | HTML source. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection object. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import http from 'k6/http'; + +export default function () { + const res = http.get('https://k6.io'); + const doc = parseHTML(res.body); // equivalent to res.html() + const pageTitle = doc.find('head title').text(); + const langAttr = doc.find('html').attr('lang'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/_index.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/_index.md new file mode 100644 index 000000000..8a59f4bd5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/_index.md @@ -0,0 +1,73 @@ +--- +title: 'Selection' +description: 'A jQuery-like API for accessing HTML DOM elements.' +description: 'A jQuery-like API for accessing HTML DOM elements.' +weight: 50 +weight: 50 +--- + +# Selection + +Represents a set of nodes in a DOM tree. + +Selections have a jQuery-compatible API, but with two caveats: + +- CSS and screen layout are not processed, thus calls like css() and offset() are unavailable. +- DOM trees are read-only, you can't set attributes or otherwise modify nodes. + +(Note that the read-only nature of the DOM trees is purely to avoid a maintenance burden on code with seemingly no practical use - if a compelling use case is presented, modification can easily be implemented.) + +| Method | Description | +| ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Selection.attr(name)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-attr) | Get the value of an attribute for the first element in the Selection. | +| [Selection.children([selector])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-children) | Get the children of each element in the set of matched elements, optionally filtered by a selector. | +| [Selection.closest(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-closest) | Get the first element that matches the selector by testing the element itself and traversing up through its ancestors | +| [Selection.contents()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-contents) | Get the children of each element in the set of matched elements, including text and comment nodes. | +| [Selection.data([key])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-data) | Return the value at the named data store for the first element in the set of matched elements. | +| [Selection.each(fn)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-each) | Iterate and execute a function for each matched element. | +| [Selection.eq(index)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-eq) | Reduce the set of matched elements to the one at the specified index. | +| [Selection.filter(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-filter) | Reduce the set of matched elements to those that match the selector or pass the function's test. | +| [Selection.find(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-find) | Find the selection descendants, filtered by a selector. | +| [Selection.first()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-first) | Reduce the set of matched elements to the first in the set. | +| [Selection.get(index)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-get) | Retrieve the [Element (k6/html)](https://grafana.com/docs/k6//javascript-api/k6-html/element) matched by the selector | +| [Selection.has(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-has) | Reduce the set of matched elements to those that have a descendant that matches the selector | +| [Selection.html()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-html) | Get the HTML contents of the first element in the set of matched elements | +| [Selection.is(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-is) | Check the current matched set of elements against a selector or element and return true if at least one of these elements matches the given arguments. | +| [Selection.last()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-last) | Reduce the set of matched elements to the final one in the set. | +| [Selection.map(fn)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-map) | Pass each selection in the current matched set through a function, producing a new Array containing the return values. | +| [Selection.nextAll([selector])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-nextall) | Get all following siblings of each element in the set of matched elements, optionally filtered by a selector. | +| [Selection.next([selector])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-next) | Get the immediately following sibling of each element in the set of matched element | +| [Selection.nextUntil([selector], [filter])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-nextuntil) | Get all following siblings of each element up to but not including the element matched by the selector. | +| [Selection.not(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-not) | Remove elements from the set of matched elements | +| [Selection.parent([selector])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-parent) | Get the parent of each element in the current set of matched elements, optionally filtered by a selector. | +| [Selection.parents([selector])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-parents) | Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector. | +| [Selection.parentsUntil([selector], [filter])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-parentsuntil) | Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector. | +| [Selection.prevAll([selector])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-prevall) | Get all preceding siblings of each element in the set of matched elements, optionally filtered by a selector. | +| [Selection.prev([selector])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-prev) | Get the immediately preceding sibling of each element in the set of matched elements. | +| [Selection.prevUntil([selector], [filter])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-prevuntil) | Get all preceding siblings of each element up to but not including the element matched by the selector. | +| [Selection.serialize()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-serialize) | Encode a set of form elements as a string in standard URL-encoded notation for submission. | +| [Selection.serializeArray()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-serializearray) | Encode a set of form elements as an array of names and values. | +| [Selection.serializeObject()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-serializeobject) | Encode a set of form elements as an object. | +| [Selection.size()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-size) | Return the number of elements in the Selection. | +| [Selection.slice(start [, end])](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-slice) | Reduce the set of matched elements to a subset specified by a range of indices. | +| [Selection.text()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-text) | Get the text content of the selection. | +| [Selection.toArray()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-toarray) | Retrieve all the elements contained in the Selection, as an array. | +| [Selection.val()](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-val) | Get the current value of the first element in the set of matched elements. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import http from 'k6/http'; + +export default function () { + const res = http.get('https://k6.io'); + const doc = parseHTML(res.body); + const pageTitle = doc.find('head title').text(); + const langAttr = doc.find('html').attr('lang'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-attr.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-attr.md new file mode 100644 index 000000000..a4fabd973 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-attr.md @@ -0,0 +1,36 @@ +--- +title: 'Selection.attr(name)' +description: 'Get the value of an attribute for the first element in the Selection.' +--- + +# Selection.attr(name) + +Get the value of an attribute for the first element in the Selection. +Mimics [jquery.attr](https://api.jquery.com/attr/) + +| Parameter | Type | Description | +| --------- | ------ | -------------------------------- | +| name | string | The name of the attribute to get | + +### Returns + +| Type | Description | +| ------ | -------------------------- | +| string | The value of the attribute | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import http from 'k6/http'; + +export default function () { + const res = http.get('https://k6.io'); + const doc = parseHTML(res.body); + const langAttr = doc.find('html').attr('lang'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-children.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-children.md new file mode 100644 index 000000000..de08abd75 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-children.md @@ -0,0 +1,58 @@ +--- +title: 'Selection.children([selector])' +description: 'Get the children of each element in the set of matched elements, optionally filtered by a selector.' +--- + +# Selection.children([selector]) + +Get the children of each element in the set of matched elements, optionally filtered by a selector. +Mimics [jquery.children](https://api.jquery.com/children/) + +| Parameter | Type | Description | +| ------------------- | ------ | -------------------------------------------------------------------- | +| selector (optional) | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ----------------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | The children Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + const sel = doc.find('dl'); + + console.log(sel.children().size()); + console.log(sel.children('dt').size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-closest.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-closest.md new file mode 100644 index 000000000..0ce23d805 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-closest.md @@ -0,0 +1,57 @@ +--- +title: 'Selection.closest(selector)' +description: 'For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree.' +--- + +# Selection.closest(selector) + +For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. +Mimics [jquery.closest](https://api.jquery.com/closest/) + +| Parameter | Type | Description | +| --------- | ------ | ------------------------------------------------------------------- | +| selector | string | A string containing a selector expression to match elements against | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ----------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • I
  • +
  • II +
      +
    • A
    • +
    • B +
        +
      • 1
      • +
      • 2
      • +
      • 3
      • +
      +
    • +
    • C
    • +
    +
  • +
  • III
  • +
+ `; + const doc = parseHTML(content); + + const sel = doc.find('li.item-a').closest('ul'); + console.log(sel.attr('class')); + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-contents.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-contents.md new file mode 100644 index 000000000..264673066 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-contents.md @@ -0,0 +1,54 @@ +--- +title: 'Selection.contents()' +description: 'Get the children of each element in the set of matched elements, including text and comment nodes.' +--- + +# Selection.contents() + +Get the children of each element in the set of matched elements, including text and comment nodes. +Mimics [jquery.contents](https://api.jquery.com/contents/). + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ----------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('dt'); + + console.log(sel.contents().text()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-data.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-data.md new file mode 100644 index 000000000..14d852ef2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-data.md @@ -0,0 +1,45 @@ +--- +title: 'Selection.data([key])' +description: 'Return the value at the named data store for the first element in the set of matched elements.' +--- + +# Selection.data([key]) + +Return the value at the named data store for the first element in the set of matched elements. +Mimics [jquery.data](https://api.jquery.com/data/) + +| Parameter | Type | Description | +| -------------- | ------ | ----------------------------------------- | +| key (optional) | string | A string naming the piece of data to set. | + +### Returns + +| Type | Description | +| ------ | ---------------------------------- | +| string | The value at the named data store. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +

Hola

+ `; + + const doc = parseHTML(content); + const sel = doc.find('h1'); + + console.log(sel.data().testID); + console.log(sel.data('test-id')); + console.log(sel.data('testId')); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-each.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-each.md new file mode 100644 index 000000000..0f18da3e4 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-each.md @@ -0,0 +1,52 @@ +--- +title: 'Selection.each(fn)' +description: 'Iterate over a Selection, executing a function for each matched element.' +--- + +# Selection.each(fn) + +Iterate over a [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection), executing a function for each matched element. +Mimics [jquery.each](https://api.jquery.com/each/) + +| Parameter | Type | Description | +| --------- | -------- | --------------------------------------------------------- | +| fn | function | A function to iterate all the Elements of the Collection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + doc.find('dl').each(function (idx, el) { + console.log(el.innerHTML()); + }); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-eq.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-eq.md new file mode 100644 index 000000000..37a577c9b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-eq.md @@ -0,0 +1,60 @@ +--- +title: 'Selection.eq(index)' +description: 'Reduce the set of matched elements to the one at the specified index.' +--- + +# Selection.eq(index) + +Reduce the set of matched elements to the one at the specified index. +Mimics [jquery.eq](https://api.jquery.com/eq/). + +| Parameter | Type | Description | +| --------- | ------ | ---------------------------------------------------------- | +| index | Number | An integer indicating the 0-based position of the element. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('dt'); + + console.log(sel.eq(0).html()); + console.log(sel.eq(1).html()); + console.log(sel.eq(2).html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-filter.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-filter.md new file mode 100644 index 000000000..3eacde516 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-filter.md @@ -0,0 +1,69 @@ +--- +title: 'Selection.filter(selector)' +description: 'Reduce the set of matched elements to those that match the selector or pass the function test.' +--- + +# Selection.filter(selector) + +Reduce the set of matched elements to those that match the selector or pass the function's test. +Mimics [jquery.filter](https://api.jquery.com/filter/) + +| Parameter | Type | Description | +| --------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| selector | function | A function used as a test for each element in the set. | +| selector | string | A string containing a selector expression to match elements against. | +| selector | [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A selection to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | --------------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | The filter selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + let sel; + const els = doc.find('dl').children(); + + sel = els.filter('#term-2'); + console.log(sel.text()); + + sel = els.filter(function (idx, el) { + return el.text() === 'definition 3-a'; + }); + console.log(sel.text()); + + sel = els.filter(doc.find('dl dt#term-1')); + console.log(sel.text()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-find.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-find.md new file mode 100644 index 000000000..65bb82d65 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-find.md @@ -0,0 +1,38 @@ +--- +title: 'Selection.find(selector)' +description: 'Find the selection descendants, filtered by a selector.' +--- + +# Selection.find(selector) + +Find the selection descendants, filtered by a selector. It returns a [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) object. +Mimics [jquery.find](https://api.jquery.com/find/) + +| Parameter | Type | Description | +| --------- | ------ | -------------------------------------------------------------------- | +| selector | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ----------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | Selection object. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import http from 'k6/http'; + +export default function () { + const res = http.get('https://k6.io'); + const doc = parseHTML(res.body); + + const titleDoc = doc.find('head title'); + const title = titleDoc.text(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-first.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-first.md new file mode 100644 index 000000000..b7273e29e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-first.md @@ -0,0 +1,54 @@ +--- +title: 'Selection.first()' +description: 'Reduce the set of matched elements to the first in the set.' +--- + +# Selection.first() + +Reduce the set of matched elements to the first in the set. +Mimics [jquery.first](https://api.jquery.com/first/). + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ----------------------------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | The first element of the Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('dt'); + + console.log(sel.first().html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-get.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-get.md new file mode 100644 index 000000000..3fbe50d6f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-get.md @@ -0,0 +1,59 @@ +--- +title: 'Selection.get(index)' +description: 'Retrieve the Element matched by the selector.' +--- + +# Selection.get(index) + +Retrieve the Element matched by the selector. +Mimics [jquery.get](https://api.jquery.com/get/) + +| Parameter | Type | Description | +| --------- | ------ | ---------------------------------------------------------- | +| index | Number | A zero-based integer indicating which element to retrieve. | + +### Returns + +| Type | Description | +| ------- | ------------------------------------ | +| Element | The Element matched by the selector. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('dl').children(); + + console.log(sel.get(0).innerHTML()); + console.log(sel.get(1).innerHTML()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-has.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-has.md new file mode 100644 index 000000000..c47a1989e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-has.md @@ -0,0 +1,53 @@ +--- +title: 'Selection.has(selector)' +descriptiontion: 'Reduce the set of matched elements to those that have a descendant that matches the selector.' +--- + +# Selection.has(selector) + +Reduce the set of matched elements to those that have a descendant that matches the selector. +Mimics [jquery.has](https://api.jquery.com/has/). + +| Parameter | Type | Description | +| --------- | ------ | -------------------------------------------------------------------- | +| selector | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • list item 1
  • +
  • list item 2 +
      +
    • list item 2-a
    • +
    • list item 2-b
    • +
    +
  • +
  • list item 3
  • +
  • list item 4
  • +
+ `; + const doc = parseHTML(content); + + const sel = doc.find('li').has('ul'); + + console.log(sel.html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-html.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-html.md new file mode 100644 index 000000000..4b62bc752 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-html.md @@ -0,0 +1,54 @@ +--- +title: 'Selection.html()' +description: 'Get the HTML contents of the first element in the set of matched elements.' +--- + +# Selection.html() + +Get the HTML contents of the first element in the set of matched elements. +Mimics [jquery.html](https://api.jquery.com/html/) + +### Returns + +| Type | Description | +| ------ | --------------------------------------------------------------------- | +| string | The HTML content of the first element in the set of matched elements. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('dt'); + + console.log(sel.html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-is.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-is.md new file mode 100644 index 000000000..90983f3cf --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-is.md @@ -0,0 +1,71 @@ +--- +title: 'Selection.is(selector)' +descriptiontion: 'Check the current matched set of elements against a selector or element and return true if at least one of these elements matches the given arguments.' +--- + +# Selection.is(selector) + +Check the current matched set of elements against a selector or element and return true if at least one of these elements matches the given arguments. +Mimics [jquery.is](https://api.jquery.com/is/) + +| Parameter | Type | Description | +| --------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| selector | function | A function used as a test for each element in the set | +| selector | string | A string containing a selector expression to match elements against. | +| selector | [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A selection. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | --------------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | The filter selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + let result; + + const els = doc.find('dl').children(); + + result = els.is('dd'); + console.log(result); + + result = els.is(function (idx, el) { + return el.text() === 'hola'; + }); + console.log(result); + + result = els.is(els.first()); + console.log(result); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-last.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-last.md new file mode 100644 index 000000000..f064fbb7b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-last.md @@ -0,0 +1,54 @@ +--- +title: 'Selection.last()' +descriptiontion: 'Reduce the set of matched elements to the final one in the set.' +--- + +# Selection.last() + +Reduce the set of matched elements to the final one in the set. +Mimics [jquery.last](https://api.jquery.com/last/). + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ----------------------------------- | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | The final element of the Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('dt'); + + console.log(sel.last().html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-map.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-map.md new file mode 100644 index 000000000..b29c49273 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-map.md @@ -0,0 +1,63 @@ +--- +title: 'Selection.map(fn)' +description: 'Pass each selection in the current matched set through a function, producing a new Array containing the return values.' +--- + +# Selection.map(fn) + +Pass each selection in the current matched set through a function, producing a new Array containing the return values. +Mimics [jquery.each](https://api.jquery.com/each/) + +| Parameter | Type | Description | +| --------- | -------- | ----------------------------------------------------------- | +| fn | function | A function to iterate all the Selections of the Collection. | + +### Returns + +| Type | Description | +| ----- | --------------------------------------- | +| Array | The array containing the return values. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const newEls = doc + .find('dl') + .children() + .map(function (idx, el) { + return 'hola ' + el.text(); + }); + + console.log(newEls); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-next.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-next.md new file mode 100644 index 000000000..7cf046c53 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-next.md @@ -0,0 +1,50 @@ +--- +title: 'Selection.next([selector])' +description: 'Get the immediately following sibling of each element in the set of matched elements +Mimics jquery.next.' +--- + +# Selection.next([selector]) + +Get the immediately following sibling of each element in the set of matched elements +Mimics [jquery.next](https://api.jquery.com/next/). + +| Parameter | Type | Description | +| ------------------- | ------ | -------------------------------------------------------------------- | +| selector (optional) | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • list item 1
  • +
  • list item 2
  • +
  • list item 3
  • +
  • list item 4
  • +
  • list item 5
  • +
+ `; + const doc = parseHTML(content); + + const sel = doc.find('li.third-item').next(); + + console.log(sel.html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-nextall.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-nextall.md new file mode 100644 index 000000000..bc2566cac --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-nextall.md @@ -0,0 +1,49 @@ +--- +title: 'Selection.nextAll([selector])' +description: 'Get all following siblings of each element in the set of matched elements, optionally filtered by a selector.' +--- + +# Selection.nextAll([selector]) + +Get all following siblings of each element in the set of matched elements, optionally filtered by a selector. +Mimics [jquery.nextAll](https://api.jquery.com/nextAll/). + +| Parameter | Type | Description | +| ------------------- | ------ | ------------------------------------------------------------------- | +| selector (optional) | string | A string containing a selector expression to match elements against | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • list item 1
  • +
  • list item 2
  • +
  • list item 3
  • +
  • list item 4
  • +
  • list item 5
  • +
+ `; + const doc = parseHTML(content); + + const sel = doc.find('li.third-item').nextAll(); + + console.log(sel.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-nextuntil.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-nextuntil.md new file mode 100644 index 000000000..9e1d15700 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-nextuntil.md @@ -0,0 +1,61 @@ +--- +title: 'Selection.nextUntil([selector], [filter])' +description: 'Get all following siblings of each element in the set of matched elements, optionally filtered by a selector.' +--- + +# Selection.nextUntil([selector], [filter]) + +Get all following siblings of each element up to but not including the element matched by the selector. +Mimics [jquery.nextUntil](https://api.jquery.com/nextUntil/) + +| Parameter | Type | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| selector (optional) | string \| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) \| `null` | A selector expression or object to match elements against. | +| filter (optional) | string \| `null` | A selector expression to filter matched elements. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('#term-2').nextUntil('dt'); + console.log(sel.size()); + + const selFilter = doc.find('#term-1').nextUntil('#term-3', 'dd'); + console.log(selFilter.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-not.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-not.md new file mode 100644 index 000000000..f93c948be --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-not.md @@ -0,0 +1,64 @@ +--- +title: 'Selection.not(selector)' +description: 'Remove elements from the set of matched elements.' +--- + +# Selection.not(selector) + +Remove elements from the set of matched elements. +Mimics [jquery.not](https://api.jquery.com/not/) + +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------------------------------- | +| selector | string | A string containing a selector expression to match elements against. | +| selector | function | A function used as a test for each element in the set. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + let sel = doc.find('dl').children(); + + console.log(sel.not('dt').size()); + + sel = sel.not(function (idx, item) { + return item.text().startsWith('definition'); + }); + + console.log(sel.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parent.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parent.md new file mode 100644 index 000000000..308a3251a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parent.md @@ -0,0 +1,49 @@ +--- +title: 'Selection.parent([selector])' +description: 'Get the parent of each element in the current set of matched elements, optionally filtered by a selector.' +--- + +# Selection.parent([selector]) + +Get the parent of each element in the current set of matched elements, optionally filtered by a selector. +Mimics [jquery.parent](https://api.jquery.com/parent/). + +| Parameter | Type | Description | +| ------------------- | ------ | -------------------------------------------------------------------- | +| selector (optional) | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • list item 1
  • +
  • list item 2
  • +
  • list item 3
  • +
  • list item 4
  • +
  • list item 5
  • +
+ `; + const doc = parseHTML(content); + + const sel = doc.find('li.third-item').parent(); + + console.log(sel.html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parents.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parents.md new file mode 100644 index 000000000..defcb8640 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parents.md @@ -0,0 +1,49 @@ +--- +title: 'Selection.parents([selector])' +description: 'Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector.' +--- + +# Selection.parents([selector]) + +Get the ancestors of each element in the current set of matched elements, optionally filtered by a selector. +Mimics [jquery.parents](https://api.jquery.com/parents/). + +| Parameter | Type | Description | +| ------------------- | ------ | -------------------------------------------------------------------- | +| selector (optional) | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • list item 1
  • +
  • list item 2
  • +
  • list item 3
  • +
  • list item 4
  • +
  • list item 5
  • +
+ `; + const doc = parseHTML(content); + + const sel = doc.find('li.third-item').parents(); + + console.log(sel.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parentsuntil.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parentsuntil.md new file mode 100644 index 000000000..b3e3f4f2b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-parentsuntil.md @@ -0,0 +1,61 @@ +--- +title: 'Selection.parentsUntil([selector], [filter])' +description: 'Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector.' +--- + +# Selection.parentsUntil([selector], [filter]) + +Get the ancestors of each element in the current set of matched elements, up to but not including the element matched by the selector. +Mimics [jquery.parentsUntil](https://api.jquery.com/parentsUntil/) + +| Parameter | Type | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| selector (optional) | string \| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) \| `null` | A selector expression or object to match elements against. | +| filter (optional) | string \| `null` | A selector expression to filter matched elements. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('#term-2').parentsUntil('dt'); + console.log(sel.size()); + + const selFilter = doc.find('#term-3').parentsUntil('dt', 'dd'); + console.log(selFilter.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prev.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prev.md new file mode 100644 index 000000000..30753e12d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prev.md @@ -0,0 +1,48 @@ +--- +title: 'Selection.prev([selector])' +description: 'Get the immediately preceding sibling of each element in the set of matched elements.' +--- + +# Selection.prev([selector]) + +Get the immediately preceding sibling of each element in the set of matched elements. +Mimics [jquery.prev](https://api.jquery.com/prev/). + +| Parameter | Type | Description | +| ------------------- | ------ | -------------------------------------------------------------------- | +| selector (optional) | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • list item 1
  • +
  • list item 2
  • +
  • list item 3
  • +
  • list item 4
  • +
  • list item 5
  • +
+ `; + const doc = parseHTML(content); + const sel = doc.find('li.third-item').prev(); + + console.log(sel.html()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prevall.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prevall.md new file mode 100644 index 000000000..2209d02e7 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prevall.md @@ -0,0 +1,49 @@ +--- +title: 'Selection.prevAll([selector])' +descriptiontiontion: 'Get all preceding siblings of each element in the set of matched elements, optionally filtered by a selector.' +--- + +# Selection.prevAll([selector]) + +Get all preceding siblings of each element in the set of matched elements, optionally filtered by a selector. +Mimics [jquery.prevAll](https://api.jquery.com/prevAll/). + +| Parameter | Type | Description | +| ------------------- | ------ | -------------------------------------------------------------------- | +| selector (optional) | string | A string containing a selector expression to match elements against. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
    +
  • list item 1
  • +
  • list item 2
  • +
  • list item 3
  • +
  • list item 4
  • +
  • list item 5
  • +
+ `; + const doc = parseHTML(content); + + const sel = doc.find('li.third-item').prevAll(); + + console.log(sel.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prevuntil.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prevuntil.md new file mode 100644 index 000000000..aee58a4e5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-prevuntil.md @@ -0,0 +1,61 @@ +--- +title: 'Selection.prevUntil([selector], [filter])' +description: 'Get all preceding siblings of each element up to but not including the element matched by the selector.' +--- + +# Selection.prevUntil([selector], [filter]) + +Get all preceding siblings of each element up to but not including the element matched by the selector. +Mimics [jquery.prevUntil](https://api.jquery.com/prevUntil/). + +| Parameter | Type | Description | +| ------------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| selector (optional) | string \| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) \| `null` | A selector expression or object to match elements against. | +| filter (optional) | string \| `null` | A selector expression to filter matched elements. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('#term-2').prevUntil('dt'); + console.log(sel.size()); + + const selFilter = doc.find('#term-3').prevUntil('#term-1', 'dd'); + console.log(selFilter.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serialize.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serialize.md new file mode 100644 index 000000000..c3517aa70 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serialize.md @@ -0,0 +1,40 @@ +--- +title: 'Selection.serialize()' +description: 'Encode a set of form elements as a string in standard URL-encoded notation for submission.' +--- + +# Selection.serialize() + +Encode a set of form elements as a string in standard URL-encoded notation for submission. +Mimics [jquery.serialize](https://api.jquery.com/serialize/) + +### Returns + +| Type | Description | +| ------ | -------------------------------------------------------------------- | +| string | The URL-encoded representation of the matched form or form elements. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+ `; + + const doc = parseHTML(content); + const sel = doc.find('form'); + const serialized = sel.serialize(); + + console.log(serialized); // "username=" + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serializearray.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serializearray.md new file mode 100644 index 000000000..43219adb4 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serializearray.md @@ -0,0 +1,40 @@ +--- +title: 'Selection.serializeArray()' +description: 'Encode a set of form elements as an array of names and values.' +--- + +# Selection.serializeArray() + +Encode a set of form elements as an array of names and values (`[{ name: "name", value: "value" }, ...]`). +Mimics [jquery.serializeArray](https://api.jquery.com/serializeArray/) + +### Returns + +| Type | Description | +| ----- | --------------------------------------------------------------- | +| array | Array of names and values of the matched form or form elements. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+ `; + + const doc = parseHTML(content); + const sel = doc.find('form'); + const serialized = sel.serializeArray(); + + console.log(JSON.stringify(serialized)); // [{"name": "username", "value": ""}] + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serializeobject.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serializeobject.md new file mode 100644 index 000000000..653b5381f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-serializeobject.md @@ -0,0 +1,39 @@ +--- +title: 'Selection.serializeObject()' +description: 'Encode a set of form elements as an object.' +--- + +# Selection.serializeObject() + +Encode a set of form elements as an object (`{ "inputName": "value", "checkboxName": "value" }`). + +### Returns + +| Type | Description | +| ------ | ------------------------------------------------------------------------------------------------------- | +| object | Object representation of the matched form or form elements, key is field name and value is field value. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+ `; + + const doc = parseHTML(content); + const sel = doc.find('form'); + const serialized = sel.serializeObject(); + + console.log(JSON.stringify(serialized)); // {"username": ""} + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-size.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-size.md new file mode 100644 index 000000000..b21bee996 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-size.md @@ -0,0 +1,54 @@ +--- +title: 'Selection.size()' +description: 'Return the number of elements in the Selection.' +--- + +# Selection.size() + +Return the number of elements in the Selection. +Mimics [jquery.size](https://api.jquery.com/size/) + +### Returns + +| Type | Description | +| ------ | ---------------------------------------- | +| Number | The number of elements in the Selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const sel = doc.find('dt'); + + console.log(sel.size()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-slice.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-slice.md new file mode 100644 index 000000000..a10f8c3ab --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-slice.md @@ -0,0 +1,62 @@ +--- +title: 'Selection.slice(start [, end])' +description: 'Reduce the set of matched elements to a subset specified by a range of indices.' +--- + +# Selection.slice(start [, end]) + +Reduce the set of matched elements to a subset specified by a range of indices. +Mimics [jquery.slice](https://api.jquery.com/slice/) + +| Parameter | Type | Description | +| --------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| start | Number | An integer indicating the 0-based position at which the elements begin to be selected. | +| end | Number | An integer indicating the 0-based position at which the elements stop being selected. | +| selector | [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A selection. | + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | The new selection. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + const els = doc.find('dl').children(); + + console.log(els.slice(4).text()); + + console.log(els.slice(2, 4).text()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-text.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-text.md new file mode 100644 index 000000000..9f4022de3 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-text.md @@ -0,0 +1,32 @@ +--- +title: 'Selection.text()' +description: 'Get the text content of the Selection.' +--- + +# Selection.text() + +Get the text content of the Selection. +Mimics [jquery.text](https://api.jquery.com/text/). + +### Returns + +| Type | Description | +| ------ | ----------------------- | +| string | Selection text content. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import http from 'k6/http'; + +export default function () { + const res = http.get('https://k6.io'); + const doc = parseHTML(res.body); + const pageTitle = doc.find('head title').text(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-toarray.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-toarray.md new file mode 100644 index 000000000..b3875ab20 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-toarray.md @@ -0,0 +1,58 @@ +--- +title: 'Selection.toArray()' +description: 'Retrieve all the elements contained in the Selection, as an array.' +--- + +# Selection.toArray() + +Retrieve all the elements contained in the Selection, as an array. +Mimics [jquery.toArray](https://api.jquery.com/toArray/). + +### Returns + +| Type | Description | +| ----------------------------------------------------------------------------------------------- | --------------------------- | +| Array of [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | Array of Selection objects. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` +
+
term 1
+
definition 1-a
+
definition 1-b
+
definition 1-c
+
definition 1-d
+ +
term 2
+
definition 2-a
+
definition 2-b
+
definition 2-c
+ +
term 3
+
definition 3-a
+
definition 3-b
+
+ `; + const doc = parseHTML(content); + + doc + .find('dl') + .children() + .toArray() + .forEach(function (item) { + console.log(item.text()); + }); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-val.md b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-val.md new file mode 100644 index 000000000..1b4147248 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-html/selection/selection-val.md @@ -0,0 +1,56 @@ +--- +title: 'Selection.val()' +description: 'Get the current value of the first element in the set of matched elements.' +--- + +# Selection.val() + +Get the current value of the first element in the set of matched elements. +Mimics [jquery.val](https://api.jquery.com/val/). + +### Returns + +| Type | Description | +| ------ | -------------------------------------------------------------- | +| string | The value of the first element in the set of matched elements. | + +### Example + +{{< code >}} + +```javascript +import { parseHTML } from 'k6/html'; +import { sleep } from 'k6'; + +export default function () { + const content = ` + + + + + + `; + const doc = parseHTML(content); + + console.log(doc.find('#text_input').val()); + console.log(doc.find('#select_one option[selected]').val()); + console.log(doc.find('#select_one').val()); + console.log(doc.find('#select_text').val()); + console.log(doc.find('#select_multi').val()); + console.log(doc.find('#textarea').val()); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/_index.md b/docs/sources/v0.50.x/javascript-api/k6-http/_index.md new file mode 100644 index 000000000..546fe87c9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/_index.md @@ -0,0 +1,34 @@ +--- +title: 'k6/http' +descriptiontiontion: 'The k6/http module contains functionality for performing HTTP transactions.' +weight: 09 +--- + +# k6/http + +The k6/http module contains functionality for performing HTTP transactions. + +| Function | Description | +| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| [batch( requests )](https://grafana.com/docs/k6//javascript-api/k6-http/batch) | Issue multiple HTTP requests in parallel (like e.g. browsers tend to do). | +| [cookieJar()](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar-method) | Get active HTTP Cookie jar. | +| [del( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/del) | Issue an HTTP DELETE request. | +| [file( data, [filename], [contentType] )](https://grafana.com/docs/k6//javascript-api/k6-http/file) | Create a file object that is used for building multi-part requests. | +| [get( url, [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/get) | Issue an HTTP GET request. | +| [head( url, [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/head) | Issue an HTTP HEAD request. | +| [options( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/options) | Issue an HTTP OPTIONS request. | +| [patch( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/patch) | Issue an HTTP PATCH request. | +| [post( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/post) | Issue an HTTP POST request. | +| [put( url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/put) | Issue an HTTP PUT request. | +| [request( method, url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/request) | Issue any type of HTTP request. | +| [asyncRequest( method, url, [body], [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/asyncrequest) | Issue any type of HTTP request asynchronously. | +| [setResponseCallback(expectedStatuses)](https://grafana.com/docs/k6//javascript-api/k6-http/set-response-callback) | Sets a response callback to mark responses as expected. | +| [url\`url\`](https://grafana.com/docs/k6//javascript-api/k6-http/url) | Creates a URL with a name tag. Read more on [URL Grouping](https://grafana.com/docs/k6//using-k6/http-requests#url-grouping). | +| [expectedStatuses( statusCodes )](https://grafana.com/docs/k6//javascript-api/k6-http/expected-statuses) | Create a callback for setResponseCallback that checks status codes. | + +| Class | Description | +| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| [CookieJar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) | Used for storing cookies, set by the server and/or added by the client. | +| [FileData](https://grafana.com/docs/k6//javascript-api/k6-http/filedata) | Used for wrapping data representing a file when doing multipart requests (file uploads). | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) | Used for setting various HTTP request-specific parameters such as headers, cookies, etc. | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | Returned by the http.\* methods that generate HTTP requests. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/asyncrequest.md b/docs/sources/v0.50.x/javascript-api/k6-http/asyncrequest.md new file mode 100644 index 000000000..ebc7cda86 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/asyncrequest.md @@ -0,0 +1,91 @@ +--- +title: 'asyncRequest( method, url, [body], [params] )' +description: 'Issue any type of HTTP request asynchronously.' +description: 'Issue any type of HTTP request asynchronously.' +weight: 10 +--- + +# asyncRequest( method, url, [body], [params] ) + +| Parameter | Type | Description | +| ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| method | string | Request method (e.g. `'POST'`). Must be uppercase. | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `'http://example.com'`). | +| body (optional) | string / object / ArrayBuffer | Request body. Objects will be `x-www-form-urlencoded` encoded. | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| --------------------- | ------------------------------------------------------------------------------------------------- | +| Promise with Response | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Examples + +Using http.asyncRequest() to issue a POST request: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url = 'https://httpbin.test.k6.io/post'; + +export default async function () { + const data = { name: 'Bert' }; + + // Using a JSON string as body + let res = await http.asyncRequest('POST', url, JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); + console.log(res.json().json.name); // Bert + + // Using an object as body, the headers will automatically include + // 'Content-Type: application/x-www-form-urlencoded'. + res = await http.asyncRequest('POST', url, data); + console.log(res.json().form.name); // Bert +} +``` + +{{< /code >}} + +Using `http.asyncRequest()` to issue multiple requests, then [Promise.race](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) to determine which requests finish first: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default async () => { + const urlOne = `https://httpbin.test.k6.io/delay/${randomInt(1, 5)}`; + const urlTwo = `https://httpbin.test.k6.io/delay/${randomInt(1, 5)}`; + const urlThree = `https://httpbin.test.k6.io/delay/${randomInt(1, 5)}`; + + const one = http.asyncRequest('GET', urlOne); + const two = http.asyncRequest('GET', urlTwo); + const three = http.asyncRequest('GET', urlThree); + + console.log('Racing:'); + console.log(urlOne); + console.log(urlTwo); + console.log(urlThree); + + const res = await Promise.race([one, two, three]); + console.log('winner is', res.url, 'with duration of', res.timings.duration + 'ms'); +}; + +function randomInt(min, max) { + return Math.floor(Math.random() * (max - min) + min); +} +``` + +{{% admonition type="note" %}} + +`http.asyncRequest` has no current way to abort a request. + +In the preceding script, after `res` gets the value from the fastest request, the other requests will continue to execute. +This might block the end of the iteration, because the iteration only stops once all async jobs finish. + +{{% /admonition %}} + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/batch.md b/docs/sources/v0.50.x/javascript-api/k6-http/batch.md new file mode 100644 index 000000000..eb45ebf5e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/batch.md @@ -0,0 +1,182 @@ +--- +title: 'batch( requests )' +description: 'Issue multiple HTTP requests in parallel (like e.g. browsers tend to do).' +description: 'Issue multiple HTTP requests in parallel (like e.g. browsers tend to do).' +weight: 10 +--- + +# batch( requests ) + +Batch multiple HTTP requests together to issue them in parallel over multiple TCP connections. +To set batch size, use the [batch per host](https://grafana.com/docs/k6//using-k6/k6-options/reference#batch-per-host) option. + +| Parameter | Type | Description | +| --------- | --------------- | ---------------------------------------------------------------- | +| requests | array \| object | An array or object containing requests, in string or object form | + +### Request definition + +You have multiple ways to structure batch requests: + +- In an array of arrays +- As an object or array of objects +- As an array of URL strings + +Defining batch requests as URL strings is a shortcut for GET requests. +You can use this GET shortcut in objects—name a key and give it a URL value +(refer to subsequent sections for example syntax). + +#### Array and Object + +You can define a request specified as an array or object with the following parameters. + +{{% admonition type="caution" %}} + +When you define requests as an array, you must use a specific order of items. +Note the `Position` column for the correct order. + +{{% /admonition %}} + +| Array position | Name | Type | Description | +| -------------- | ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| 1 | method | string | Mandatory. The HTTP method of the request. One of GET, POST, PUT, PATCH, DELETE, HEAD or OPTION. | +| 2 | url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Mandatory. The URL to request. | +| 3 | body (optional) | string / object / ArrayBuffer | The body of the request if relevant. Can be set to `null` if not applicable but you want to set the last `params` argument. | +| 4 | params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) like auth, custom headers and tags. | + +#### String + +If you pass an array of string values, k6 automatically parses them into a batch of `GET` requests, where the target is the value of the strings. + +### Returns + +| Type | Description | +| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| object | The returned object contains [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) objects.

It is an array when users pass an array as `requests`, and is an ordinary object with string keys when named requests are used (see below). | + +### Example with arrays + +This example batches three URLs in arrays for parallel fetching: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const responses = http.batch([ + ['GET', 'https://test.k6.io', null, { tags: { ctype: 'html' } }], + ['GET', 'https://test.k6.io/style.css', null, { tags: { ctype: 'css' } }], + ['GET', 'https://test.k6.io/images/logo.png', null, { tags: { ctype: 'images' } }], + ]); + check(responses[0], { + 'main page status was 200': (res) => res.status === 200, + }); +} +``` + +{{< /code >}} + +### Example with request objects + +This example uses objects to define a batch of POST requests (along with custom HTTP headers in a [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object to the request): + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const req1 = { + method: 'GET', + url: 'https://httpbin.test.k6.io/get', + }; + const req2 = { + method: 'GET', + url: 'https://test.k6.io', + }; + const req3 = { + method: 'POST', + url: 'https://httpbin.test.k6.io/post', + body: { + hello: 'world!', + }, + params: { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }, + }; + const responses = http.batch([req1, req2, req3]); + // httpbin.test.k6.io should return our POST data in the response body, so + // we check the third response object to see that the POST worked. + check(responses[2], { + 'form data OK': (res) => JSON.parse(res.body)['form']['hello'] == 'world!', + }); +} +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +In the preceding example, `req1` can happen before `req2` or `req3`. + +{{% /admonition %}} + +### Example with array of strings + +This example uses an array of URL strings to send a batch of GET requests. + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + const responses = http.batch(['http://test.k6.io', 'http://test.k6.io/pi.php']); + + check(responses[0], { + 'main page 200': (res) => res.status === 200, + }); + + check(responses[1], { + 'pi page 200': (res) => res.status === 200, + 'pi page has right content': (res) => res.body === '3.14', + }); +} +``` + +{{< /code >}} + +### Example object with named properties + +Finally, you can also send in named requests by using an object instead of an array as the parameter to `http.batch()`. +This example mixes string URLs and request objects. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const requests = { + 'front page': 'https://k6.io', + 'features page': { + method: 'GET', + url: 'https://k6.io/features', + params: { headers: { 'User-Agent': 'k6' } }, + }, + }; + const responses = http.batch(requests); + // when accessing results, we use the name of the request as index + // in order to find the corresponding Response object + check(responses['front page'], { + 'front page status was 200': (res) => res.status === 200, + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar.md b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar.md new file mode 100644 index 000000000..08ca682d8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar.md @@ -0,0 +1,28 @@ +--- +title: 'cookieJar()' +slug: 'cookiejar-method' +description: 'Get active HTTP Cookie jar.' +weight: 10 +--- + +# cookieJar() + +Get the active cookie jar. + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------------- | +| [CookieJar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) | A CookieJar object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const jar = http.cookieJar(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/_index.md b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/_index.md new file mode 100644 index 000000000..cb001cf38 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/_index.md @@ -0,0 +1,43 @@ +--- +title: 'CookieJar' +description: 'Used for storing cookies, set by the server and/or added by the client.' +weight: 60 +--- + +# CookieJar + +_CookieJar_ is an object for storing cookies that are set by the server, added by the client, or both. As described in the how-to guide on using [Cookies](https://grafana.com/docs/k6//using-k6/cookies), k6 handles cookies automatically by default. If you need more control over cookies you can however create your own cookie jar and select it as the active jar (instead of the default one created by k6) for one or more requests. + +| Method | Description | +| --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| [cookiesForURL(url)](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar/cookiejar-cookiesforurl) | Get Object of cookies where the key is the cookie name and the value is an array. | +| [set(url, name, value, [options])](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar/cookiejar-set) | Set a cookie in the jar by specifying name, value and some other optional settings like domain, path etc. | +| [clear(url)](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar/cookiejar-clear) | Delete all cookies for the given URL. | +| [delete(url, name)](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar/cookiejar-delete) | Deletes the `name` cookie for the given URL. | + +### Example + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res1 = http.get('https://httpbin.test.k6.io/cookies/set?my_cookie=hello%20world', { + redirects: 0, + }); + const jar = http.cookieJar(); + const cookies = jar.cookiesForURL('https://httpbin.test.k6.io/'); + check(res1, { + "has cookie 'my_cookie'": (r) => cookies.my_cookie.length > 0, + 'cookie has correct value': (r) => cookies.my_cookie[0] === 'hello world', + }); + + jar.clear('https://httpbin.test.k6.io/cookies'); + + const res2 = http.get('https://httpbin.test.k6.io/cookies'); + check(res2, { + 'has status 200': (r) => r.status === 200, + "hasn't cookie 'my_cookie'": (r) => r.json().cookies.my_cookie == null, + }); +} +``` diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-clear.md b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-clear.md new file mode 100644 index 000000000..b0dcc88b0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-clear.md @@ -0,0 +1,47 @@ +--- +title: 'CookieJar.clear(url)' +description: 'Delete all cookies for the given URL.' +--- + +# CookieJar.clear(url) + +| Parameter | Type | Description | +| --------- | ------ | ---------------------------------- | +| url | string | The URL to delete all cookies for. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + http.get('https://httpbin.test.k6.io/cookies/set?one=1&two=2'); + + // We'll use httpbin's reflection to see what cookies we + // are actually sending to the server after every change + let httpbinResp; + httpbinResp = http.get('https://httpbin.test.k6.io/cookies'); + console.log(JSON.stringify(httpbinResp.json().cookies)); + // Will print '{"one":"1","two":"2"}' + + const jar = http.cookieJar(); // get the VU specific jar + jar.set('https://httpbin.test.k6.io/cookies', 'three', '3'); + httpbinResp = http.get('https://httpbin.test.k6.io/cookies'); + console.log(JSON.stringify(httpbinResp.json().cookies)); + // Will print '{"one":"1","three":"3","two":"2"}' + + jar.delete('https://httpbin.test.k6.io/cookies', 'one'); + httpbinResp = http.get('https://httpbin.test.k6.io/cookies'); + console.log(JSON.stringify(httpbinResp.json().cookies)); + // Will print '{"three":"3","two":"2"}' + + jar.clear('https://httpbin.test.k6.io/cookies'); + httpbinResp = http.get('https://httpbin.test.k6.io/cookies'); + console.log(JSON.stringify(httpbinResp.json().cookies)); + // Will print '{}' +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-cookiesforurl.md b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-cookiesforurl.md new file mode 100644 index 000000000..533a0ad03 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-cookiesforurl.md @@ -0,0 +1,39 @@ +--- +title: 'CookieJar.cookiesForURL(url)' +description: 'Get object with all cookies for the given URL, where the key is the cookie name and the value is an array.' +--- + +# CookieJar.cookiesForURL(url) + +| Parameter | Type | Description | +| --------- | ------ | --------------------------- | +| url | string | The URL to get cookies for. | + +### Returns + +| Type | Description | +| ------ | ----------------------------------------------------------------------------------------------------------- | +| object | A JS object with all cookies for the given URL, where the key is the cookie name and the value is an array. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res = http.get('https://httpbin.test.k6.io/cookies/set?my_cookie=hello%20world', { + redirects: 0, + }); + const jar = http.cookieJar(); + const cookies = jar.cookiesForURL('https://httpbin.test.k6.io/'); + check(res, { + "has cookie 'my_cookie'": (r) => cookies.my_cookie.length > 0, + 'cookie has correct value': (r) => cookies.my_cookie[0] === 'hello world', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-delete.md b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-delete.md new file mode 100644 index 000000000..558dc1c24 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-delete.md @@ -0,0 +1,47 @@ +--- +title: 'CookieJar.delete(url, name)' +description: 'Delete a cookie of a specified `name` for the given URL.' +--- + +# CookieJar.delete(url, name) + +| Parameter | Type | Description | +| --------- | ------ | ------------------------------------------ | +| url | string | The URL to delete cookies for. | +| name | string | The name of the cookie you want to delete. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie_1', 'hello world_1'); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie_2', 'hello world_2'); + + const res1 = http.get('https://httpbin.test.k6.io/cookies'); + check(res1, { + 'res1 has status 200': (r) => r.status === 200, + "res1 has cookie 'my_cookie_1'": (r) => r.json().cookies.my_cookie_1 !== null, + 'res1 cookie has correct value_1': (r) => r.json().cookies.my_cookie_1 == 'hello world_1', + "res1 has cookie 'my_cookie_2'": (r) => r.json().cookies.my_cookie_2 !== null, + 'res1 cookie has correct value_2': (r) => r.json().cookies.my_cookie_2 == 'hello world_2', + }); + + jar.delete('https://httpbin.test.k6.io/cookies', 'my_cookie_1'); + + const res2 = http.get('https://httpbin.test.k6.io/cookies'); + check(res2, { + 'res2 has status 200': (r) => r.status === 200, + "res2 doesn't have cookie 'my_cookie_1'": (r) => r.json().cookies.my_cookie_1 == null, + "res2 has cookie 'my_cookie_2'": (r) => r.json().cookies.my_cookie_2 !== null, + 'res2 cookie has correct value_2': (r) => r.json().cookies.my_cookie_2 == 'hello world_2', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-set.md b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-set.md new file mode 100644 index 000000000..170fb114a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/cookiejar/cookiejar-set.md @@ -0,0 +1,42 @@ +--- +title: 'CookieJar.set(url, name, value, [options])' +description: 'Set a cookie in the jar by specifying url, name, value and some other optional settings like domain, path, etc.' +--- + +# CookieJar.set(url, name, value, [options]) + +Set a cookie in the jar by specifying url, name, value and some other optional settings like domain, path, etc. + +| Parameter | Type | Description | +| ------------------ | ------ | ------------------------------------------------------------------------------------------- | +| url | string | Cookie URL | +| name | string | Cookie name | +| value | string | Cookie value | +| options (optional) | object | Specific cookie settings: `domain`, `path`, `expires`, `max_age`, `secure` and `http_only`. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie', 'hello world', { + domain: 'httpbin.test.k6.io', + path: '/cookies', + secure: true, + max_age: 600, + }); + const res = http.get('https://httpbin.test.k6.io/cookies'); + check(res, { + 'has status 200': (r) => r.status === 200, + "has cookie 'my_cookie'": (r) => r.json().cookies.my_cookie !== null, + 'cookie has correct value': (r) => r.json().cookies.my_cookie == 'hello world', + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/del.md b/docs/sources/v0.50.x/javascript-api/k6-http/del.md new file mode 100644 index 000000000..8a2327025 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/del.md @@ -0,0 +1,39 @@ +--- +title: 'del( url, [body], [params] )' +description: 'Issue an HTTP DELETE request.' +description: 'Issue an HTTP DELETE request.' +weight: 10 +--- + +# del( url, [body], [params] ) + +Make a DELETE request. + +| Parameter | Type | Description | +| ---------------------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `http://example.com`). | +| body (optional, discouraged) | string / object / ArrayBuffer | Request body; objects will be `x-www-form-urlencoded`. This is discouraged, because sending a DELETE request with a body has [no defined semantics](https://tools.ietf.org/html/rfc7231#section-4.3.5) and may cause some servers to reject it. | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| -------- | ------------------------------------------------------------------------------------------------- | +| Response | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url = 'https://httpbin.test.k6.io/delete'; + +export default function () { + const params = { headers: { 'X-MyHeader': 'k6test' } }; + http.del(url, null, params); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/expected-statuses.md b/docs/sources/v0.50.x/javascript-api/k6-http/expected-statuses.md new file mode 100644 index 000000000..4912667f2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/expected-statuses.md @@ -0,0 +1,37 @@ +--- +title: 'expectedStatuses( statuses )' +description: 'generates a responseCallback to check status codes' +description: 'generates a responseCallback to check status codes' +weight: 11 +--- + +# expectedStatuses( statuses ) + +Returns a callback to be used with [setResponseCallback](https://grafana.com/docs/k6//javascript-api/k6-http/set-response-callback) to mark responses as expected based only on their status codes. + +| Parameter | Type | Description | +| --------- | --------------- | -------------------------------------------------------------------------------------------------------------- | +| statuses | integer/objects | either an integer or an object like {min:100, max:300} which gives a minimum and maximum expected status codes | + +You can have as many arguments as wanted in any order. + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +// setting some pretty strange status codes as expected +http.setResponseCallback( + http.expectedStatuses(406, 500, { min: 200, max: 204 }, 302, { min: 305, max: 405 }) +); + +export default () => { + // this one will actually be marked as failed as it doesn't match any of the above listed status + // codes + http.get('https://httpbin.test.k6.io/status/205'); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/file.md b/docs/sources/v0.50.x/javascript-api/k6-http/file.md new file mode 100644 index 000000000..564d131bc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/file.md @@ -0,0 +1,43 @@ +--- +title: 'file( data, [filename], [contentType] )' +description: 'Create a file object that is used for building multi-part requests.' +description: 'Create a file object that is used for building multi-part requests.' +weight: 10 +--- + +# file( data, [filename], [contentType] ) + +Create a file object that is used for building [Multipart requests (file uploads)](https://grafana.com/docs/k6//examples/data-uploads#multipart-request-uploading-a-file). + +| Parameter | Type | Description | +| ----------- | ---------------------------- | -------------------------------------------------------------------------------- | +| data | string / Array / ArrayBuffer | File data as string, array of numbers, or an `ArrayBuffer` object. | +| filename | string | The filename to specify for this field (or "part") of the multipart request. | +| contentType | string | The content type to specify for this field (or "part") of the multipart request. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ------------------ | +| [FileData](https://grafana.com/docs/k6//javascript-api/k6-http/filedata) | A FileData object. | + +### Example + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import { md5 } from 'k6/crypto'; +import http from 'k6/http'; + +const binFile = open('/path/to/file.bin', 'b'); + +export default function () { + const f = http.file(binFile, 'test.bin'); + console.log(md5(f.data, 'hex')); + console.log(f.filename); + console.log(f.content_type); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/filedata.md b/docs/sources/v0.50.x/javascript-api/k6-http/filedata.md new file mode 100644 index 000000000..87522f4f6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/filedata.md @@ -0,0 +1,39 @@ +--- +title: 'FileData' +description: 'Used for wrapping data representing a file when doing multipart requests (file uploads).' +description: 'Used for wrapping data representing a file when doing multipart requests (file uploads).' +weight: 60 +--- + +# FileData + +_FileData_ is an object for wrapping data representing a file when doing +[multipart requests (file uploads)](https://grafana.com/docs/k6//examples/data-uploads#multipart-request-uploading-a-file). +You create it by calling [http.file( data, [filename], [contentType] )](https://grafana.com/docs/k6//javascript-api/k6-http/file). + +| Name | Type | Description | +| --------------------- | ---------------------------- | ------------------------------------------------------------------ | +| FileData.data | string / Array / ArrayBuffer | File data as string, array of numbers, or an `ArrayBuffer` object. | +| FileData.content_type | string | The content type that will be specified in the multipart request. | +| FileData.filename | string | The filename that will be specified in the multipart request. | + +### Example + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import { md5 } from 'k6/crypto'; +import http from 'k6/http'; + +const binFile = open('/path/to/file.bin', 'b'); + +export default function () { + const f = http.file(binFile, 'test.bin'); + console.log(md5(f.data, 'hex')); + console.log(f.filename); + console.log(f.content_type); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/get.md b/docs/sources/v0.50.x/javascript-api/k6-http/get.md new file mode 100644 index 000000000..70a33a370 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/get.md @@ -0,0 +1,36 @@ +--- +title: 'get( url, [params] )' +description: 'Issue an HTTP GET request.' +description: 'Issue an HTTP GET request.' +weight: 10 +--- + +# get( url, [params] ) + +Make a GET request. + +| Parameter | Type | Description | +| ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `http://example.com`). | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | --------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP Response object. | + +### Example fetching a URL + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const res = http.get('https://test.k6.io'); + console.log(JSON.stringify(res.headers)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/head.md b/docs/sources/v0.50.x/javascript-api/k6-http/head.md new file mode 100644 index 000000000..2c8d0fd54 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/head.md @@ -0,0 +1,36 @@ +--- +title: 'head( url, [params] )' +description: 'Issue an HTTP HEAD request.' +description: 'Issue an HTTP HEAD request.' +weight: 10 +--- + +# head( url, [params] ) + +Make a HEAD request. + +| Parameter | Type | Description | +| ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `http://example.com`). | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | --------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP Response object. | + +### Example fetching a URL + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const res = http.head('https://test.k6.io'); + console.log(JSON.stringify(res.headers)); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/options.md b/docs/sources/v0.50.x/javascript-api/k6-http/options.md new file mode 100644 index 000000000..bfe15af36 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/options.md @@ -0,0 +1,38 @@ +--- +title: 'options( url, [body], [params] )' +description: 'Issue an HTTP OPTIONS request.' +description: 'Issue an HTTP OPTIONS request.' +weight: 10 +--- + +# options( url, [body], [params] ) + +| Parameter | Type | Description | +| ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `http://example.com`). | +| body (optional) | string / object / ArrayBuffer | Request body; objects will be `x-www-form-urlencoded`. | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| -------- | ------------------------------------------------------------------------------------------------- | +| Response | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url = 'https://httpbin.test.k6.io/'; + +export default function () { + const params = { headers: { 'X-MyHeader': 'k6test' } }; + const res = http.options(url, null, params); + console.log(res.headers['Allow']); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/params.md b/docs/sources/v0.50.x/javascript-api/k6-http/params.md new file mode 100644 index 000000000..b0b45db02 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/params.md @@ -0,0 +1,124 @@ +--- +title: 'Params' +description: 'Used for setting various HTTP request-specific parameters such as headers, cookies, etc.' +description: 'Used for setting various HTTP request-specific parameters such as headers, cookies, etc.' +weight: 60 +--- + +# Params + +_Params_ is an object used by the http.\* methods that generate HTTP requests. _Params_ contains request-specific options like e.g. HTTP headers that should be inserted into the request. + +| Name | Type | Description | +| ------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Params.auth` | string | The authentication method used for the request. It currently supports `digest`, `ntlm`, and `basic` authentication methods. | +| `Params.cookies` | object | Object with key-value pairs representing request scoped cookies (they won't be added to VU cookie jar)
`{cookies: { key: "val", key2: "val2" }}`

You also have the option to say that a request scoped cookie should override a cookie in the VU cookie jar:
`{cookies: { key: { value: "val", replace: true }}}` | +| `Params.headers` | object | Object with key-value pairs representing custom HTTP headers the user would like to add to the request. | +| `Params.jar` | object | http.CookieJar object to override default VU cookie jar with. Cookies added to request will be sourced from this jar and cookies set by server will be added to this jar. | +| `Params.redirects` | number | The number of redirects to follow for this request. Overrides the global test option [`maxRedirects`](https://grafana.com/docs/k6//using-k6/k6-options/reference). | +| `Params.tags` | object | Key-value pairs where the keys are names of tags and the values are tag values. Response time metrics generated as a result of the request will have these tags added to them, allowing the user to filter out those results specifically, when looking at results data. | +| `Params.timeout` | string / number | Maximum time to wait for the request to complete. Default timeout is 60 seconds (`"60s"`).
The type can also be a number, in which case k6 interprets it as milliseconds, e.g., `60000` is equivalent to `"60s"`. | +| `Params.compression` | string | Sets whether the request body should be compressed. If set to `gzip` it will use gzip to compress the body and set the appropriate `Content-Length` and `Content-Encoding` headers.

Possible values: `gzip`, `deflate`, `br`, `zstd`, and any comma-separated combination of them (for stacked compression) | +| `Params.responseType` | string | ResponseType is used to specify how to treat the body of the response. The three options are:
- text: k6 will return it as a string. This might not be what you want in cases of binary data as the conversation to UTF-16 will probably break it. This is also the default if
[discardResponseBodies](https://grafana.com/docs/k6//using-k6/k6-options/reference) is set to false or not set at all.
- `binary`: k6 will return an ArrayBuffer object
- `none`: k6 will return null as the body. The whole body will be ignored. This is the default when [discardResponseBodies](https://grafana.com/docs/k6//using-k6/k6-options/reference) is set to true. | +| `Params.responseCallback` | [expectedStatuses](https://grafana.com/docs/k6//javascript-api/k6-http/expected-statuses) | sets a [responseCallback](https://grafana.com/docs/k6//javascript-api/k6-http/set-response-callback) only for this request. For performance reasons it's better to initialize it once and reference it for each time the same callback will need to be used | + +### Example of custom HTTP headers and tags + +_A k6 script that will make an HTTP request with a custom HTTP header and tag results data with a specific tag_ + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const params = { + cookies: { my_cookie: 'value' }, + headers: { 'X-MyHeader': 'k6test' }, + redirects: 5, + tags: { k6test: 'yes' }, + }; + const res = http.get('https://k6.io', params); +} +``` + +{{< /code >}} + +### Example using http.batch() with Params + +Here is another example using [http.batch()](https://grafana.com/docs/k6//javascript-api/k6-http/batch) with a `Params` argument: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url1 = 'https://api.k6.io/v3/account/me'; +const url2 = 'https://httpbin.test.k6.io/get'; +const apiToken = 'f232831bda15dd233c53b9c548732c0197619a3d3c451134d9abded7eb5bb195'; +const requestHeaders = { + 'User-Agent': 'k6', + 'Authorization': 'Token ' + apiToken, +}; + +export default function () { + const res = http.batch([ + { method: 'GET', url: url1, params: { headers: requestHeaders } }, + { method: 'GET', url: url2 }, + ]); +} +``` + +{{< /code >}} + +### Example of Digest Authentication + +Here is one example of how to use the `Params` to Digest Authentication. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + // Passing username and password as part of URL plus the auth option will authenticate using HTTP Digest authentication + const res = http.get('http://user:passwd@httpbin.test.k6.io/digest-auth/auth/user/passwd', { + auth: 'digest', + }); + + // Verify response + check(res, { + 'status is 200': (r) => r.status === 200, + 'is authenticated': (r) => r.json().authenticated === true, + 'is correct user': (r) => r.json().user === 'user', + }); +} +``` + +{{< /code >}} + +### Example of overriding discardResponseBodies + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { discardResponseBodies: true }; +export default function () {} +export function setup() { + // Get 10 random bytes as an ArrayBuffer. Without the responseType the body + // will be null. + const response = http.get('https://httpbin.test.k6.io/bytes/10', { + responseType: 'binary', + }); + // response.body is an ArrayBuffer, so wrap it in a typed array view to access + // its elements. + const bodyView = new Uint8Array(response.body); + // This will output something like `176,61,15,66,233,98,223,196,43,1` + console.log(bodyView); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/patch.md b/docs/sources/v0.50.x/javascript-api/k6-http/patch.md new file mode 100644 index 000000000..81de685c9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/patch.md @@ -0,0 +1,41 @@ +--- +title: 'patch( url, [body], [params] )' +description: 'Issue an HTTP PATCH request.' +description: 'Issue an HTTP PATCH request.' +weight: 10 +--- + +# patch( url, [body], [params] ) + +| Parameter | Type | Description | +| ----------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `http://example.com`). | +| body (optional) | string / object / ArrayBuffer | Request body; objects will be `x-www-form-urlencoded`. | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | --------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP Response object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url = 'https://httpbin.test.k6.io/patch'; + +export default function () { + const headers = { 'Content-Type': 'application/json' }; + const data = { name: 'Bert' }; + + const res = http.patch(url, JSON.stringify(data), { headers: headers }); + + console.log(JSON.parse(res.body).json.name); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/post.md b/docs/sources/v0.50.x/javascript-api/k6-http/post.md new file mode 100644 index 000000000..2f1dfb0df --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/post.md @@ -0,0 +1,57 @@ +--- +title: 'post( url, [body], [params] )' +description: 'Issue an HTTP POST request.' +description: 'Issue an HTTP POST request.' +weight: 10 +--- + +# post( url, [body], [params] ) + +| Parameter | Type | Description | +| ------------------- | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `url` | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `http://example.com`). | +| `body` | string / object / ArrayBuffer | Request body; objects will be `x-www-form-urlencoded`. | +| `params` (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters | + +### Returns + +| Type | Description | +| ---------- | ------------------------------------------------------------------------------------------------- | +| `Response` | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url = 'https://httpbin.test.k6.io/post'; +const logoBin = open('./logo.png', 'b'); + +export default function () { + let data = { name: 'Bert' }; + + // Using a JSON string as body + let res = http.post(url, JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); + console.log(res.json().json.name); // Bert + + // Using an object as body, the headers will automatically include + // 'Content-Type: application/x-www-form-urlencoded'. + res = http.post(url, data); + console.log(res.json().form.name); // Bert + + // Using a binary array as body. Make sure to open() the file as binary + // (with the 'b' argument). + http.post(url, logoBin, { headers: { 'Content-Type': 'image/png' } }); + + // Using an ArrayBuffer as body. Make sure to pass the underlying ArrayBuffer + // instance to http.post(), and not the TypedArray view. + data = new Uint8Array([104, 101, 108, 108, 111]); + http.post(url, data.buffer, { headers: { 'Content-Type': 'image/png' } }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/put.md b/docs/sources/v0.50.x/javascript-api/k6-http/put.md new file mode 100644 index 000000000..5b5f2a765 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/put.md @@ -0,0 +1,41 @@ +--- +title: 'put( url, [body], [params] )' +description: 'Issue an HTTP PUT request.' +description: 'Issue an HTTP PUT request.' +weight: 10 +--- + +# put( url, [body], [params] ) + +| Parameter | Type | Description | +| ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `http://example.com`). | +| body (optional) | string / object / ArrayBuffer | Request body; objects will be `x-www-form-urlencoded`. | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| -------- | ------------------------------------------------------------------------------------------------- | +| Response | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url = 'https://httpbin.test.k6.io/put'; + +export default function () { + const headers = { 'Content-Type': 'application/json' }; + const data = { name: 'Bert' }; + + const res = http.put(url, JSON.stringify(data), { headers: headers }); + + console.log(JSON.parse(res.body).json.name); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/request.md b/docs/sources/v0.50.x/javascript-api/k6-http/request.md new file mode 100644 index 000000000..4766ae22b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/request.md @@ -0,0 +1,50 @@ +--- +title: 'request( method, url, [body], [params] )' +description: 'Issue any type of HTTP request.' +description: 'Issue any type of HTTP request.' +weight: 10 +--- + +# request( method, url, [body], [params] ) + +| Parameter | Type | Description | +| ----------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | +| method | string | Request method (e.g. `'POST'`). Must be uppercase. | +| url | string /[HTTP URL](https://grafana.com/docs/k6//javascript-api/k6-http/url#returns) | Request URL (e.g. `'http://example.com'`). | +| body (optional) | string / object / ArrayBuffer | Request body; Objects will be `x-www-form-urlencoded` encoded. | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| -------- | ------------------------------------------------------------------------------------------------- | +| Response | HTTP [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) object. | + +### Example + +Using http.request() to issue a POST request: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const url = 'https://httpbin.test.k6.io/post'; + +export default function () { + const data = { name: 'Bert' }; + + // Using a JSON string as body + let res = http.request('POST', url, JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' }, + }); + console.log(res.json().json.name); // Bert + + // Using an object as body, the headers will automatically include + // 'Content-Type: application/x-www-form-urlencoded'. + res = http.request('POST', url, data); + console.log(res.json().form.name); // Bert +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/response/_index.md b/docs/sources/v0.50.x/javascript-api/k6-http/response/_index.md new file mode 100644 index 000000000..dc3b4c5d5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/response/_index.md @@ -0,0 +1,78 @@ +--- +title: 'Response' +description: 'Returned by the http.* methods that generate HTTP requests.' +description: 'Returned by the http.* methods that generate HTTP requests.' +weight: 61 +weight: 61 +--- + +# Response + +Response is used by the http.\* methods that generate HTTP request. Those methods return one (or more, in the case of `http.batch()`) Response objects that contain HTTP response contents and performance timing measurements. + +Note that in the case of redirects, all the information in the Response object will pertain to the last request (the one that doesn't get redirected). + +| Name | Type | Description | +| ------------------------------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Response.body` | string | Response body content, often used to extract dynamic data (see examples [here](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data)) and when verifying the presence of content using [checks](https://grafana.com/docs/k6//javascript-api/k6/check).

See [Params.responseType](https://grafana.com/docs/k6//javascript-api/k6-http/params) and [options.discardResponseBodies](https://grafana.com/docs/k6//using-k6/k6-options/reference) for how to discard the body when it is not needed (and to save memory) or when handling bodies with binary data. | +| `Response.cookies` | object | Response cookies. The object properties are the cookie names and the value is an array of cookie objects (with `name`, `value`, `domain`, `path`, `httpOnly`, `secure`, `maxAge` and `expires` fields). | +| `Response.error` | string | Error message if there was a non-HTTP error while trying to send the request. | +| `Response.error_code` | number | [Error code](https://grafana.com/docs/k6//javascript-api/error-codes) if there was a non-HTTP error or 4xx or 5xx HTTP error it will be set to a specific code that describes the error. (Added in 0.24.0) | +| `Response.headers` | object | Key-value pairs representing all HTTP headers sent by the server. Note that the header names are in [canonical form](https://pkg.go.dev/net/http#CanonicalHeaderKey), i.e.: if the server responds with "accept-encoding", the key will be "Accept-Encoding". | +| `Response.ocsp.produced_at` | number | If a stapled OSCP response was provided by server, the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time when this OCSP stapled response was signed by CA (or by CA entrusted OCSP responder) | +| `Response.ocsp.this_update` | number | If a stapled OSCP response was provided by server, the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time at which the status being indicated was known to be correct. | +| `Response.ocsp.next_update` | number | If a stapled OSCP response was provided by server, the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time when this OCSP stapled response will be refreshed with CA (or by CA entrusted OCSP responder). | +| `Response.ocsp.revocation_reason` | string | The reason for revocation of the certificate (if that is the status), one of the following constants: `http.OCSP_REASON_UNSPECIFIED`, `http.OCSP_REASON_KEY_COMPROMISE`, `http.OCSP_REASON_CA_COMPROMISE`,
`http.OCSP_REASON_AFFILIATION_CHANGED`,
`http.OCSP_REASON_SUPERSEDED`,
`http.OCSP_REASON_CESSATION_OF_OPERATION`,
`http.OCSP_REASON_CERTIFICATE_HOLD`,
`http.OCSP_REASON_REMOVE_FROM_CRL`,
`http.OCSP_REASON_PRIVILEGE_WITHDRAWN` or
`http.OCSP_REASON_AA_COMPROMISE`. | +| `Response.ocsp.revoked_at` | number | If a stapled OSCP response was provided by server, the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time when this certificate was revoked (if that is the status). | +| `Response.ocsp.status` | string | The status of the certificate, one of the following constants: `http.OCSP_STATUS_GOOD`, `http.OCSP_STATUS_REVOKED`, `http.OCSP_STATUS_UNKNOWN` or `http.OCSP_STATUS_SERVER_FAILED`. | +| `Response.proto` | string | Protocol used to perform the transfer. Possible values are "HTTP/1.0", "HTTP/1.1", or "HTTP/2.0". | +| `Response.remote_ip` | string | The IP address of the server handling the request. | +| `Response.remote_port` | number | The port that was connected to on the server side. | +| `Response.request.body` | string | Request body content. | +| `Response.request.cookies` | object | Request cookies. The object properties are the cookie names and the value is an array of cookie objects (with `name`, `value` and `replace` fields). | +| `Response.request.headers` | object | Request headers. | +| `Response.request.method` | string | Request HTTP method. | +| `Response.request.url` | string | Request URL. | +| `Response.status` | number | HTTP status code returned by the server. | +| `Response.status_text` | string | _(new in k6 v0.29.0)_ HTTP status text returned by the server. | +| `Response.timings` | object | Performance timing information for the HTTP request. | +| `Response.timings.blocked` | float | Containing time (ms) spent blocked before initiating request. | +| `Response.timings.connecting` | float | Containing time (ms) spent setting up TCP connection to host. | +| `Response.timings.tls_handshaking` | float | Containing time (ms) spent handshaking TLS session with host. | +| `Response.timings.sending` | float | Containing time (ms) spent sending request. | +| `Response.timings.waiting` | float | Containing time (ms) spent waiting for server response. | +| `Response.timings.receiving` | float | Containing time (ms) spent receiving response data. | +| `Response.timings.duration` | float | Total time for the request (ms). It's equal to `sending + waiting + receiving`, i.e. how long did the remote server take to process the request and respond, without the initial DNS lookup/connection times. | +| `Response.tls_cipher_suite` | string | If a TLS session was established, the cipher suite that was used. | +| `Response.tls_version` | string | If a TLS session was established, the version of SSL/TLS that was used. | +| `Response.url` | string | The URL that was ultimately fetched (i.e. after any potential redirects). | +| [Response.clickLink( [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/response/response-clicklink) | function | Parses response as HTML, looks for a specific link and does the request-level equivalent of a click on that link. | +| [Response.html()](https://grafana.com/docs/k6//javascript-api/k6-http/response/response-html) | function | Returns an object that supports [Selection.find(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-find). | +| [Response.json( [selector] )](https://grafana.com/docs/k6//javascript-api/k6-http/response/response-json) | function | Parses the response body data as JSON and returns a JS object or array. This call caches the deserialized JSON data, additional calls will return the cached data. An optional selector can be specified to extract a specific part of the data, see [here for selector syntax](https://github.com/tidwall/gjson#path-syntax). | +| [Response.submitForm( [params] )](https://grafana.com/docs/k6//javascript-api/k6-http/response/response-submitform) | function | Parses response as HTML, parses the specified form (defaults to looking for a "form" element) with option to override fields and then submits form taking form's `method` and `action` into account. | + +### Example + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + const res = http.get('https://k6.io'); + for (const p in res.headers) { + if (res.headers.hasOwnProperty(p)) { + console.log(p + ' : ' + res.headers[p]); + } + } + check(res, { + 'status is 200': (r) => r.status === 200, + 'caption is correct': (r) => r.html('h1').text() == 'Example Domain', + }); +} +``` + +{{< /code >}} + +_A k6 script that will make an HTTP request and print all HTTP response headers_ diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/response/response-clicklink---params.md b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-clicklink---params.md new file mode 100644 index 000000000..1f9ac592c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-clicklink---params.md @@ -0,0 +1,40 @@ +--- +title: 'Response.clickLink( [params] )' +slug: 'response-clicklink' +description: 'Create and make a request corresponding to a link, found in the HTML of response, being clicked.' +--- + +# Response.clickLink( [params] ) + +Create and make a request corresponding to a link, found in the HTML of response, being clicked. By default it will look for the first `a` tag with a `href` attribute in the HTML, but this can be overridden using the `selector` option. + +This method takes an object argument where the following properties can be set: + +| Param | Type | Description | +| -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | A selector string passed to [Selection.find(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-find) to locate the link to click. By default this is `"a[href]"`. | +| params | object | A [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) object that will be forwarded to the link click request. Can be used to set headers, cookies etc. | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | ----------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | The link click response | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + // Request page with links + let res = http.get('https://httpbin.test.k6.io/links/10/0'); + + // Now, click the 4th link on the page + res = res.clickLink({ selector: 'a:nth-child(4)' }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/response/response-html.md b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-html.md new file mode 100644 index 000000000..37efe69b9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-html.md @@ -0,0 +1,36 @@ +--- +title: 'Response.html()' +description: 'Parses response as HTML and populate a Selection.' +--- + +# Response.html() + +Parses response as HTML and populate a [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) object. + +### Returns + +| Type | Description | +| -------------------------------------------------------------------------------------- | ------------------ | +| [Selection](https://grafana.com/docs/k6//javascript-api/k6-html/selection) | A Selection object | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const res = http.get('https://stackoverflow.com'); + + const doc = res.html(); + doc + .find('link') + .toArray() + .forEach(function (item) { + console.log(item.attr('href')); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/response/response-json.md b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-json.md new file mode 100644 index 000000000..94cf51edf --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-json.md @@ -0,0 +1,36 @@ +--- +title: 'Response.json( [selector] )' +description: 'Parses the response body data as JSON and returns a JS object or array.' +--- + +# Response.json( [selector] ) + +Parses the response body data as JSON and returns a JS object or array. This call caches the deserialized JSON data, additional calls will return the cached data. An optional selector can be specified to extract a specific part of the data, see [here for selector syntax](https://github.com/tidwall/gjson#path-syntax). + +This method takes an object argument where the following properties can be set: + +| Param | Type | Description | +| -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| selector | string | An optional selector can be specified to extract a specific part of the data, see [here for selector syntax](https://github.com/tidwall/gjson#path-syntax). | + +### Returns + +| Type | Description | +| --------------- | ----------------------------------------- | +| Object or array | Returns the response body as JSON object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const res = http.get('https://test-api.k6.io/public/crocodiles/'); + + console.log(res.json()); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/response/response-submitform---params.md b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-submitform---params.md new file mode 100644 index 000000000..bf59c0e41 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/response/response-submitform---params.md @@ -0,0 +1,47 @@ +--- +title: 'Response.submitForm( [params] )' +slug: 'response-submitform' +descriptiontion: 'Fill in and submit form found in HTML of response.' +--- + +# Response.submitForm( [params] ) + +Fill in and submit form found in HTML of response. By default it will look for the first `form` tag in the HTML, but this can be overridden using the `formSelector` option. To set/override the form fields you set properties of an object in the `fields` option. + +This method takes an object argument where the following properties can be set: + +| Param | Type | Description | +| -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| formSelector | string | A selector string passed to [Selection.find(selector)](https://grafana.com/docs/k6//javascript-api/k6-html/selection/selection-find) to locate the form to fill in and submit. By default this is `"form"`. | +| fields | object | The form fields to set/override. The keys are the form fields names and the values are the form field values. | +| submitSelector | string | A selector string used to locate the submit button in the form. By default this is `'[type="submit"]'`. | +| params | object | A [Params (k6/http)](https://grafana.com/docs/k6//javascript-api/k6-http/params) object that will be forwarded to the form submit request. Can be used to set headers, cookies etc. | + +### Returns + +| Type | Description | +| ---------------------------------------------------------------------------------------------- | ------------------------- | +| [Response (k6/http)](https://grafana.com/docs/k6//javascript-api/k6-http/response) | The form submit response. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + // Request page containing a form + let res = http.get('https://httpbin.test.k6.io/forms/post'); + + // Now, submit form setting/overriding some fields of the form + res = res.submitForm({ + formSelector: 'form', + fields: { custname: 'test', extradata: 'test2' }, + }); + sleep(3); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/set-response-callback.md b/docs/sources/v0.50.x/javascript-api/k6-http/set-response-callback.md new file mode 100644 index 000000000..9a62e2d2e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/set-response-callback.md @@ -0,0 +1,55 @@ +--- +title: 'setResponseCallback( callback )' +description: 'set responseCallback to mark responses as expected' +description: 'set responseCallback to mark responses as expected' +weight: 10 +--- + +# setResponseCallback( callback ) + +Set the response callback to be called to determine if a response was expected/successful or not. + +The result of this is that requests will be tagged with `expected_response` `"true"` or `"false"` and `http_req_failed` will be emitted with the reverse. This does include all redirects so if for a request only 200 is the expected response and there is 301 redirect before that, the redirect will be marked as failed as only status code 200 was marked as expected. + +#### Exceptions + +Due to implementation specifics: + +- Requests with authentication `digest` are always expected to first get 401 and then to get whatever was specified. +- Requests with authentication `ntlm` will let a 401 status code on the first request as well as anything defined by `expectedStatuses` + +| Parameter | Type | Description | +| --------- | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| callback | [expectedStatuses](https://grafana.com/docs/k6//javascript-api/k6-http/expected-statuses) | an object returned from [expectedStatuses](https://grafana.com/docs/k6//javascript-api/k6-http/expected-statuses) | + +Currently only the very special [expectedStatuses](https://grafana.com/docs/k6//javascript-api/k6-http/expected-statuses) objects are supported but in the future it is planned that a JavaScript callback will be supported as well. By default requests with status codes between 200 and 399 are considered "expected". + +Setting the callback to `null` disables the tagging with `expected_response` and the emitting of `http_req_failed`. + +It is recommended that if a per request responseCallback is used with [Params](https://grafana.com/docs/k6//javascript-api/k6-http/params) it is actually defined once and used instead of creating it on each request. + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +http.setResponseCallback(http.expectedStatuses({ min: 200, max: 300 })); + +const only300Callback = http.expectedStatuses(300); + +export default () => { + // this will use the default response callback and be marked as successful + http.get('https://httpbin.test.k6.io/status/200'); + + // this will be marked as a failed request as it won't get the expected status code of 300 + http.get('https://httpbin.test.k6.io/status/200', { responseCallback: only300Callback }); + + http.setResponseCallback(http.expectedStatuses(301)); + // from here on for this VU only the 301 status code will be successful so on the next iteration of + // the VU the first request will be marked as failure +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-http/url.md b/docs/sources/v0.50.x/javascript-api/k6-http/url.md new file mode 100644 index 000000000..4162bf5b8 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-http/url.md @@ -0,0 +1,37 @@ +--- +title: 'url\`url\`' +description: 'Creates a URL with a name tag.' +description: 'Creates a URL with a name tag.' +weight: 10 +--- + +# url\`url\` + +URLs that contain dynamic parts can introduce a large number of unique URLs in the metrics stream. You can use `http.url` to set a consistent name tag in your requests to remedy this issue. Read more on [URL Grouping](https://grafana.com/docs/k6//using-k6/http-requests#url-grouping). + +| Parameter | Type | Description | +| --------- | ---------------- | ---------------------------------------- | +| url | template literal | Request URL (e.g. `http://example.com`). | + +### Returns + +| Type | Description | +| -------- | ---------------- | +| HTTP URL | HTTP URL object. | + +### Example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + for (let id = 1; id <= 100; id++) { + // tags.name="https://test.k6.io?id=${}", + http.get(http.url`https://test.k6.io?id=${id}`); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/_index.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/_index.md new file mode 100644 index 000000000..991a681b0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/_index.md @@ -0,0 +1,19 @@ +--- +title: 'k6/metrics' +description: 'k6 Custom Metrics API' +weight: 10 +--- + +# k6/metrics + +The metrics module provides functionality to [create custom metrics](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics) of various types. +All metrics (both the [built-in metrics](https://grafana.com/docs/k6//using-k6/metrics/reference) and the custom ones) have a type. + +You can optionally [tag](https://grafana.com/docs/k6//using-k6/tags-and-groups) all values added to a custom metric, which can be useful when analysing the test results. + +| Metric type | Description | +| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| [Counter](https://grafana.com/docs/k6//javascript-api/k6-metrics/counter) | A metric that cumulatively sums added values. | +| [Gauge](https://grafana.com/docs/k6//javascript-api/k6-metrics/gauge) | A metric that stores the min, max and last values added to it. | +| [Rate](https://grafana.com/docs/k6//javascript-api/k6-metrics/rate) | A metric that tracks the percentage of added values that are non-zero. | +| [Trend](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend) | A metric that calculates statistics on the added values (min, max, average, and percentiles). | diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/counter/_index.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/counter/_index.md new file mode 100644 index 000000000..30105182d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/counter/_index.md @@ -0,0 +1,106 @@ +--- +title: 'Counter' +description: 'Counter is an object for representing a custom cumulative counter metric. It is one of the four custom metric types.' +weight: 70 +weight: 70 +--- + +# Counter + +_Counter_ is an object for representing a custom cumulative counter metric. It is one of the four custom metric types. + +| Parameter | Type | Description | +| --------- | ------ | ------------------------------ | +| `name` | string | The name of the custom metric. | + +| Method | Description | +| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| [Counter.add(value, [tags])](https://grafana.com/docs/k6//javascript-api/k6-metrics/counter/counter-add) | Add a value to the counter metric. | + +## Counter usage in Thresholds + +When `Counter` is used in a threshold expression, the variable must be called `count` or `rate` (lower case). +For example: + +- `count >= 200` // value of the counter must be larger or equal to 200 +- `count < 10` // less than 10. + +### Examples + +{{< code >}} + +```javascript +import { Counter } from 'k6/metrics'; + +const myCounter = new Counter('my_counter'); + +export default function () { + myCounter.add(1); + myCounter.add(2, { tag1: 'myValue', tag2: 'myValue2' }); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { Counter } from 'k6/metrics'; + +const CounterErrors = new Counter('Errors'); + +export const options = { thresholds: { Errors: ['count<100'] } }; + +export default function () { + const res = http.get('https://test-api.k6.io/public/crocodiles/1/'); + const contentOK = res.json('name') === 'Bert'; + CounterErrors.add(!contentOK); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { Counter } from 'k6/metrics'; +import { sleep } from 'k6'; +import http from 'k6/http'; + +const allErrors = new Counter('error_counter'); + +export const options = { + vus: 1, + duration: '1m', + thresholds: { + 'error_counter': [ + 'count < 10', // 10 or fewer total errors are tolerated + ], + 'error_counter{errorType:authError}': [ + // Threshold on a sub-metric (tagged values) + 'count <= 2', // max 2 authentication errors are tolerated + ], + }, +}; + +export default function () { + const auth_resp = http.post('https://test-api.k6.io/auth/token/login/', { + username: 'test-user', + password: 'supersecure', + }); + + if (auth_resp.status >= 400) { + allErrors.add(1, { errorType: 'authError' }); // tagged value creates submetric (useful for making thresholds specific) + } + + const other_resp = http.get('https://test-api.k6.io/public/crocodiles/1/'); + if (other_resp.status >= 400) { + allErrors.add(1); // untagged value + } + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/counter/counter-add-value---tags.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/counter/counter-add-value---tags.md new file mode 100644 index 000000000..0cb4ba4c2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/counter/counter-add-value---tags.md @@ -0,0 +1,16 @@ +--- +title: 'Counter.add(value, [tags])' +slug: 'counter-add' +description: 'Add a value to the Counter metric.' +--- + +# Counter.add(value, [tags]) + +Add a value to the `Counter` metric. + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | number | The value to add to the counter. | +| tags | object | Set of [tags](https://grafana.com/docs/k6//using-k6/tags-and-groups) that will be tagged to the added data point (note that tags are added per data point and not for the entire metric). | + +[Counter examples](https://grafana.com/docs/k6//javascript-api/k6-metrics/counter#examples) diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/gauge/_index.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/gauge/_index.md new file mode 100644 index 000000000..5e38d0625 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/gauge/_index.md @@ -0,0 +1,68 @@ +--- +title: 'Gauge' +description: 'Gauge is an object for representing a custom metric holding only the latest value added.' +weight: 71 +--- + +# Gauge + +_Gauge_ is an object for representing a custom metric holding only the latest value added. It is one of the four [custom metrics](https://grafana.com/docs/k6//javascript-api/k6-metrics). + +| Parameter | Type | Description | +| --------- | ------- | --------------------------------------------------------------------------------------------------- | +| `name` | string | The name of the custom metric. | +| `isTime` | boolean | A boolean indicating whether the values added to the metric are time values or just untyped values. | + +| Method | Description | +| -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | +| [Gauge.add(value, [tags])](https://grafana.com/docs/k6//javascript-api/k6-metrics/gauge/gauge-add) | Add a value to the gauge metric. Only the latest value added will be kept. | + +## Gauge usage in Thresholds + +When gauge is used in a threshold expression, the variable must be called `value` (lower case). +For example: + +- `value < 200` +- `value > 1` + +### Examples + +{{< code >}} + +```javascript +import { Gauge } from 'k6/metrics'; + +const myGauge = new Gauge('my_gauge'); + +export default function () { + myGauge.add(3); + myGauge.add(1); + myGauge.add(2, { tag1: 'value', tag2: 'value2' }); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; +import { Gauge } from 'k6/metrics'; + +const GaugeContentSize = new Gauge('ContentSize'); + +export const options = { + thresholds: { + ContentSize: ['value<4000'], + }, +}; + +export default function () { + const res = http.get('https://test-api.k6.io/public/crocodiles/1/'); + GaugeContentSize.add(res.body.length); + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/gauge/gauge-add-value---tags.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/gauge/gauge-add-value---tags.md new file mode 100644 index 000000000..5f58406d0 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/gauge/gauge-add-value---tags.md @@ -0,0 +1,16 @@ +--- +title: 'Gauge.add(value, [tags])' +slug: 'gauge-add' +description: 'Set the value of the Gauge metric.' +--- + +# Gauge.add(value, [tags]) + +Set the value of the `Gauge` metric. + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | number | The value to set the gauge to. | +| tags | object | Set of [tags](https://grafana.com/docs/k6//using-k6/tags-and-groups) that will be tagged to the added data point (note that tags are added per data point and not for the entire metric). | + +[Gauge examples](https://grafana.com/docs/k6//javascript-api/k6-metrics/gauge#examples) diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/rate/_index.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/rate/_index.md new file mode 100644 index 000000000..d19199edc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/rate/_index.md @@ -0,0 +1,78 @@ +--- +title: 'Rate' +description: 'Rate is an object for representing a custom metric keeping track of the percentage of added values that are non-zero.' +weight: 72 +weight: 72 +--- + +# Rate + +_Rate_ is an object for representing a custom metric keeping track of the percentage of added values that are non-zero. It is one of the four [custom metrics](https://grafana.com/docs/k6//javascript-api/k6-metrics). + +| Parameter | Type | Description | +| --------- | ------ | ------------------------------ | +| `name` | string | The name of the custom metric. | + +| Method | Description | +| ----------------------------------------------------------------------------------------------------------- | ------------------------------- | +| [Rate.add(value, [tags])](https://grafana.com/docs/k6//javascript-api/k6-metrics/rate/rate-add) | Add a value to the rate metric. | + +## Rate usage in Thresholds + +When `Rate` is used in a threshold expression, the variable must be called `rate` (lower case). +For example: + +- `rate < 0.1` // less than 10% +- `rate >= 0.9` // more or equal to 90% + +The value of the `rate` variable ranges between `0.00` and `1.00`. + +### Examples + +{{< code >}} + +```javascript +import { Rate } from 'k6/metrics'; + +const myRate = new Rate('my_rate'); + +export default function () { + myRate.add(true); + myRate.add(false); + myRate.add(1); + myRate.add(0, { tag1: 'value', tag2: 'value2' }); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { Rate } from 'k6/metrics'; +import { sleep } from 'k6'; +import http from 'k6/http'; + +const errorRate = new Rate('errorRate'); + +export const options = { + vus: 1, + duration: '5m', + thresholds: { + errorRate: [ + // more than 10% of errors will abort the test + { threshold: 'rate < 0.1', abortOnFail: true, delayAbortEval: '1m' }, + ], + }, +}; + +export default function () { + const resp = http.get('https://test-api.k6.io/public/crocodiles/1/'); + + errorRate.add(resp.status >= 400); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/rate/rate-add-value---tags.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/rate/rate-add-value---tags.md new file mode 100644 index 000000000..fd57d5a37 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/rate/rate-add-value---tags.md @@ -0,0 +1,16 @@ +--- +title: 'Rate.add(value, [tags])' +slug: 'rate-add' +description: 'Set the value of the Rate metric.' +--- + +# Rate.add(value, [tags]) + +Set the value of the `Rate` metric. + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | number | The value to add to the rate metric. | +| tags | object | Set of [tags](https://grafana.com/docs/k6//using-k6/tags-and-groups) that will be tagged to the added data point (note that tags are added per data point and not for the entire metric). | + +[Rate examples](https://grafana.com/docs/k6//javascript-api/k6-metrics/rate#examples) diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/trend/_index.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/trend/_index.md new file mode 100644 index 000000000..08c7c5986 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/trend/_index.md @@ -0,0 +1,90 @@ +--- +title: 'Trend' +description: 'Trend is an object for representing a custom metric that allows for calculating different statistics on the added values (min, max, average or percentiles)' +weight: 73 +weight: 73 +--- + +# Trend + +_Trend_ is an object for representing a custom metric that allows for calculating different statistics on the added values (min, max, average or percentiles). It is one of the four [custom metrics](https://grafana.com/docs/k6//javascript-api/k6-metrics). + +| Parameter | Type | Description | +| --------- | ------- | --------------------------------------------------------------------------------------------------- | +| `name` | string | The name of the custom metric. | +| `isTime` | boolean | A boolean indicating whether the values added to the metric are time values or just untyped values. | + +| Method | Description | +| -------------------------------------------------------------------------------------------------------------- | -------------------------------- | +| [Trend.add(value, [tags])](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend/trend-add) | Add a value to the trend metric. | + +## Trend usage in Thresholds + +When `Trend` is used in a [threshold expression](https://grafana.com/docs/k6//using-k6/thresholds), there are a range of variables that can be used. + +- `avg` for average +- `min` for minimum +- `max` for maximum +- `med` for median +- `p(N)` for specific percentile. `N` is a number between `0.0` and `100.0` meaning the percentile value to look at, e.g. `p(99.99)` means the 99.99th percentile. + +The unit of these variables and functions are all in milliseconds. + +### Example threshold expressions: + +- `p(95) < 400` // 95% of requests must finish below 400ms +- `p(99) < 1000` // 99% of requests must finish within 1s. +- `p(50) < 200` // half of requests must finish within 200ms. +- `max < 3000` // the slowest request must finish within 3s. + +> #### ⚠️ Don't use `min` and `max` in thresholds +> +> We don't recommend using `min` and `max` for specifying thresholds because these +> values represent outliers. Use percentiles instead. + +### Examples + +{{< code >}} + +```javascript +import { Trend } from 'k6/metrics'; + +const myTrend = new Trend('my_trend'); + +export default function () { + myTrend.add(1); + myTrend.add(2, { tag1: 'value', tag2: 'value2' }); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { Trend } from 'k6/metrics'; +import { sleep } from 'k6'; +import http from 'k6/http'; + +const serverWaitingTimeOnLogin = new Trend('serverWaitingTimeOnLogin', true); + +export const options = { + vus: 1, + duration: '1m', + thresholds: { + serverWaitingTimeOnLogin: ['p(95) < 200'], + }, +}; + +export default function () { + const resp = http.post('https://test-api.k6.io/auth/token/login/', { + username: 'test-user', + password: 'supersecure', + }); + + serverWaitingTimeOnLogin.add(resp.timings.waiting); + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-metrics/trend/trend-add-value---tags.md b/docs/sources/v0.50.x/javascript-api/k6-metrics/trend/trend-add-value---tags.md new file mode 100644 index 000000000..5aa517287 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-metrics/trend/trend-add-value---tags.md @@ -0,0 +1,16 @@ +--- +title: 'Trend.add(value, [tags])' +slug: 'trend-add' +description: 'Add a value to the Trend metric.' +--- + +# Trend.add(value, [tags]) + +Add a value to the `Trend` metric. + +| Parameter | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| value | number | The value to add to the trend metric. | +| tags | object | Set of [tags](https://grafana.com/docs/k6//using-k6/tags-and-groups) that will be tagged to the added data point (note that tags are added per data point and not for the entire metric). | + +[Trend examples](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend#examples) diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/_index.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/_index.md new file mode 100644 index 000000000..3e1fcd42c --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/_index.md @@ -0,0 +1,61 @@ +--- +title: 'k6/net/grpc' +description: 'k6 gRPC API' +weight: 11 +--- + +# k6/net/grpc + +The `k6/net/grpc` module provides a [gRPC](https://grpc.io/) client for Remote Procedure Calls (RPC) over HTTP/2. It supports unary and streaming (starting on [v0.49](https://github.com/grafana/k6/releases/tag/v0.49.0)) RPCs. + +| Class/Method | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Client](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client) | gRPC client used for making RPC calls to a gRPC Server. | +| [Client.load(importPaths, ...protoFiles)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-load) | Loads and parses the given protocol buffer definitions to be made available for RPC requests. | +| [Client.connect(address [,params])](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-connect) | Connects to a given gRPC service. | +| [Client.invoke(url, request [,params])](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-invoke) | Makes an unary RPC for the given service/method and returns a [Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response). | +| [Client.close()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-close) | Close the connection to the gRPC service. | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/params) | RPC Request specific options. | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response) | Returned by RPC requests. | +| [Constants](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/constants) | Define constants to distinguish between [gRPC Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response) statuses. | +| [Stream](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream) | Creates a new GRPC stream. | +| [Stream.on(event, handler)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-on) | Adds a new listener to one of the possible stream events. | +| [Stream.write(message)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-write) | Writes a message to the stream. | +| [Stream.end()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-end) | Signals to the server that the client has finished sending. | + +## gRPC metrics + +k6 takes specific measurements for gRPC requests. +For the complete list, refer to the [Metrics reference](https://grafana.com/docs/k6//using-k6/metrics/reference#grpc). + +### Example + +{{< code >}} + +```javascript +import grpc from 'k6/net/grpc'; +import { check, sleep } from 'k6'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { + // plaintext: false + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + console.log(JSON.stringify(response.message)); + + client.close(); + sleep(1); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/_index.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/_index.md new file mode 100644 index 000000000..470ad28c6 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/_index.md @@ -0,0 +1,105 @@ +--- +title: Client +description: 'Client is a gRPC client that can interact with a gRPC server.' +weight: 10 +--- + +# Client + +`Client` is a gRPC client that can interact with a gRPC server. + +| Method | Description | +| ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Client.load(importPaths, ...protoFiles)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-load) | Loads and parses the given protocol buffer definitions to be made available for RPC requests. | +| [Client.loadProtoset(protosetPath)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-loadprotoset) | Loads and parses the given protoset file to be made available for RPC requests. | +| [Client.connect(address [,params])](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-connect) | Opens a connection to the given gRPC server. | +| [Client.invoke(url, request [,params])](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-invoke) | Makes an unary RPC for the given service/method and returns a [Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response). | +| [Client.close()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-close) | Close the connection to the gRPC service. | + +### Examples + +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); +// Download addsvc.proto for https://grpcbin.test.k6.io/, located at: +// https://raw.githubusercontent.com/moul/pb/master/addsvc/addsvc.proto +// and put it in the same folder as this script. +client.load(null, 'addsvc.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { timeout: '5s' }); + + const response = client.invoke('addsvc.Add/Sum', { + a: 1, + b: 2, + }); + console.log(response.message.v); // should print 3 + + client.close(); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/net/grpc'; +import { check } from 'k6'; + +const client = new grpc.Client(); +client.load([], 'authorization.proto', 'route_guide.proto'); + +export function setup() { + client.connect('auth.googleapis.com:443'); + const resp = client.invoke('google.cloud.authorization.v1.AuthService/GetAccessToken', { + username: 'john.smith@k6.io', + password: 'its-a-secret', + }); + client.close(); + return resp.message.accessToken; +} + +export default (token) => { + client.connect('route.googleapis.com:443'); + const metadata = { + authorization: `bearer ${token}`, + }; + const response = client.invoke( + 'google.cloud.route.v1.RoutingService/GetFeature', + { + latitude: 410248224, + longitude: -747127767, + }, + { metadata } + ); + check(response, { 'status is OK': (r) => r && r.status === grpc.StatusOK }); + client.close(); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/net/grpc'; +import { check } from 'k6'; + +const client = new grpc.Client(); +client.load([], 'language_service.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('language.googleapis.com:443'); + } + const response = client.invoke('google.cloud.language.v1.LanguageService/AnalyzeSentiment', {}); + check(response, { 'status is OK': (r) => r && r.status === grpc.StatusOK }); + // Do NOT close the client +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-close.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-close.md new file mode 100644 index 000000000..ccd127105 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-close.md @@ -0,0 +1,27 @@ +--- +title: 'Client.close()' +description: 'Close the connection to the gRPC service. Tear down all underlying connections.' +weight: 40 +--- + +# Client.close() + +Close the connection to the gRPC service. Tear down all underlying connections. + +### Examples + +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('localhost:8080'); + client.close(); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-connect-connect-address-params.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-connect-connect-address-params.md new file mode 100644 index 000000000..5db6fcde3 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-connect-connect-address-params.md @@ -0,0 +1,133 @@ +--- +title: 'Client.connect(address [,params])' +description: 'Opens a connection to a gRPC server; will block until a connection is made or a connection error is thrown.' +weight: 20 +slug: 'client-connect' +--- + +# Client.connect(address [,params]) + +Opens a connection to a gRPC server; will block until a connection is made or a connection error is thrown. Cannot be called during the [`init` phase](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +See [Client.close()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-close) to close the connection. + +| Parameter | Type | Description | +| ----------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| address | string | The address of the gRPC server. Should be in the form: `host:port` with no protocol prefix e.g. `grpc.k6.io:443`. The host must be a literal IP address, or a host name that can be resolved to IP addresses. The port must be a literal port number or a service name e.g. `:443` or `:https`. If the host is a literal IPv6 address it must be enclosed in square brackets, as in `[2001:db8::1]:80` or `[fe80::1%zone]:80`. | +| params (optional) | object | [ConnectParams](#connectparams) object containing additional connect parameters. | + +## ConnectParams + +| Name | Type | Description | +| ------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ConnectParams.plaintext` | bool | If `true` will connect to the gRPC server using plaintext i.e. insecure. Defaults to `false` i.e. secure via TLS. | +| `ConnectParams.reflect` | boolean | Whether to use the [gRPC server reflection protocol](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) when connecting. | +| `ConnectParams.reflectMetadata` | object | Object with key-value pairs representing custom metadata the user would like to add to the reflection request. | +| `ConnectParams.timeout` | string / number | Connection timeout to use. Default timeout is `"60s"`.
The type can also be a number, in which case k6 interprets it as milliseconds, e.g., `60000` is equivalent to `"60s"`. | +| `ConnectParams.maxReceiveSize` | number | Sets the maximum message size in bytes the client can receive. Defaults to `grpc-go` default, which is 4MB. | +| `ConnectParams.maxSendSize` | number | Sets the maximum message size in bytes the client can send. Defaults to `grpc-go` default, which is approximately 2GB. | +| `ConnectParams.tls` (optional) | object | [TLS](#tls) settings of the connection. Defaults to `null`. | + +## TLS + +TLS settings of the connection. If not defined, the main TLS config from options will be used. + +| Name | Type | Description | +| -------------- | -------------- | ----------------------------------------------------- | +| `tls.cert` | string | PEM formatted client certificate. | +| `tls.key` | string | PEM formatted client private key. | +| `tls.password` | string | Password for decrypting the client's private key. | +| `tls.cacerts` | string / array | PEM formatted strings of the certificate authorities. | + +### Examples + +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); + +export default () => { + client.connect('localhost:8080'); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); + +export default () => { + client.connect('localhost:8080', { plaintext: true }); +}; +``` + +
+ +
+ +```javascript +import grpc from 'k6/experimental/grpc'; +import { check } from 'k6'; +import { SharedArray } from 'k6/data'; +import exec from 'k6/execution'; + +// note: the services in this example don't exist. If you would like +// to run this example, make sure to replace the URLs, and +// the cacerts, cert, key, and password variables. +const grpcArgs = new SharedArray('grpc', () => { + // Using SharedArray here so that not every VU gets a copy of every certificate a key + return [ + { + host: 'foo1.grpcbin.test.k6.io:9001', + plaintext: false, + params: { + tls: { + cacerts: [open('cacerts0.pem')], + cert: open('cert0.pem'), + key: open('key0.pem'), + }, + }, + }, + { + host: 'foo2.grpcbin.test.k6.io:9002', + params: { + plaintext: false, + tls: { + cacerts: open('cacerts1.pem'), + cert: open('cert1.pem'), + key: open('key1.pem'), + password: 'cert1-passphrase', + }, + }, + }, + ]; +}); + +const client = new grpc.Client(); + +export default () => { + if (__ITER === 0) { + // Take one config and use it for this one VU + const grpcArg = grpcArgs[exec.vu.idInTest % grpcArgs.length]; + client.connect(grpcArg.host, grpcArg.params); + } + + const response = client.invoke('hello.HelloService/SayHello', { + greeting: 'Bert', + }); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + console.log(JSON.stringify(response.message)); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-invoke.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-invoke.md new file mode 100644 index 000000000..17f70c22e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-invoke.md @@ -0,0 +1,53 @@ +--- +title: 'Client.invoke(url, request [,params])' +description: 'Invokes an unary RPC request to the given method.' +weight: 30 +--- + +# Client.invoke(url, request [,params]) + +Invokes an unary RPC request to the given method. + +The given method to invoke must have its RPC schema previously loaded via the [Client.load()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-load) function, otherwise an +error will be thrown. + +[Client.connect()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/client/client-connect) must be called first before invoking a request, otherwise an error will be thrown. + +| Parameter | Type | Description | +| ----------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| url | string | The gRPC method url to invoke, in the form `/package.Service/Method`, e.g. `/google.cloud.language.v1.LanguageService/AnalyzeSentiment`. The leading slash `/` is optional. | +| request | object | The canonical request object, as-per the [Protobuf JSON Mapping](https://grafana.com/docs/k6//using-k6/protocols/grpc/#protocol-buffers-json-mapping). | +| params (optional) | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/params) object containing additional request parameters. | + +### Returns + +| Type | Description | +| ---------- | ----------------------------------------------------------------------------------------------------- | +| `Response` | gRPC [Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response) object. | + +### Examples + +
+ +```javascript +import grpc from 'k6/net/grpc'; +import { check } from 'k6'; + +const client = new grpc.Client(); +client.load([], 'routeguide.proto'); + +export default () => { + client.connect('localhost:10000', { plaintext: true }); + const response = client.invoke('main.RouteGuide/GetFeature', { + latitude: 410248224, + longitude: -747127767, + }); + check(response, { 'status is OK': (r) => r && r.status === grpc.StatusOK }); + console.log(response.message.name); + // output: 3 Hasta Way, Newton, NJ 07860, USA + + client.close(); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-load-protoset.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-load-protoset.md new file mode 100644 index 000000000..08bc809e7 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-load-protoset.md @@ -0,0 +1,29 @@ +--- +title: 'Client.loadProtoset(protosetPath)' +slug: 'client-loadprotoset' +description: 'Loads and parses the protoset file (serialized FileDescriptor set) so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema.' +weight: 11 +--- + +# Client.loadProtoset(protosetPath) + +Loads and parses the protoset file (serialized FileDescriptor set) so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema. + +Must be called within the [`init` phase](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +| Parameter | Type | Description | +| ------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| protosetPath | string | The path of the protoset file. If no import paths are provided then "." (current directory) is assumed to be the only import path. | + +### Examples + +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); +client.loadProtoset('./dummy.protoset'); +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-load.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-load.md new file mode 100644 index 000000000..fa2c788cb --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/client/client-load.md @@ -0,0 +1,46 @@ +--- +title: 'Client.load(importPaths, ...protoFiles)' +description: 'Loads and parses the protocol buffer descriptors so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema.' +weight: 10 +--- + +# Client.load(importPaths, ...protoFiles) + +Loads and parses the protocol buffer descriptors so they are available to the client to marshal/unmarshal the correct request and response data structures for the RPC schema. + +Must be called within the [`init` phase](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +| Parameter | Type | Description | +| ----------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| importPaths | Array<string> \| `null` | The paths used to search for dependencies that are referenced in import statements in proto source files. If no import paths are provided then "." (current directory) is assumed to be the only import path. | +| protoFiles | Array<string> | [Rest parameters](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters) for the list of proto files to load/parse. | + +### Examples + +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); +client.load([], 'language_service.proto'); +``` + +
+ +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); + +client.load( + ['../googleapis/google'], + 'spanner/admin/instance/v1/spanner_instance_admin.proto', + 'spanner/admin/instance/v1/spanner_instance_admin.proto', + 'spanner/v1/spanner.proto' +); +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/constants.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/constants.md new file mode 100644 index 000000000..0253ccf00 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/constants.md @@ -0,0 +1,59 @@ +--- +title: 'Constants' +description: 'Define constants to distinguish between gRPC Response' +weight: 40 +--- + +# Constants + +Define constants to distinguish between [gRPC Response](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/response) statuses. + +| Constant | Description | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `StatusOK` | OK is returned on success. | +| `StatusCanceled` | Canceled indicates the operation was canceled (typically by the caller). | +| `StatusUnknown` | Unknown error. | +| `StatusInvalidArgument` | InvalidArgument indicates the client specified an invalid argument. | +| `StatusDeadlineExceeded` | DeadlineExceeded means operation expired before completion. | +| `StatusNotFound` | NotFound means some requested entity (e.g., file or directory) was not found. | +| `StatusAlreadyExists` | AlreadyExists means an attempt to create an entity failed because one already exists. | +| `StatusPermissionDenied` | PermissionDenied indicates the caller does not have permission to execute the specified operation. | +| `StatusResourceExhausted` | ResourceExhausted indicates some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. | +| `StatusFailedPrecondition` | FailedPrecondition indicates operation was rejected because the system is not in a state required for the operation's execution. | +| `StatusAborted` | Aborted indicates the operation was aborted, typically due to a concurrency issue like sequencer check failures, transaction aborts, etc. | +| `StatusOutOfRange` | OutOfRange means operation was attempted past the valid range. E.g., seeking or reading past end of file. | +| `StatusUnimplemented` | Unimplemented indicates operation is not implemented or not supported/enabled in this service. | +| `StatusInternal` | Internal errors. Means some invariants expected by the underlying system have been broken. | +| `StatusUnavailable` | Unavailable indicates the service is currently unavailable. This is a most likely a transient condition and may be corrected by retrying with a backoff. Note that it is not always safe to retry non-idempotent operations. | +| `StatusDataLoss` | DataLoss indicates unrecoverable data loss or corruption. | +| `StatusUnauthenticated` | Unauthenticated indicates the request does not have valid authentication credentials for the operation. | + +### Example + +{{< code >}} + +```javascript +import grpc from 'k6/net/grpc'; +import { check, sleep } from 'k6'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { + // plaintext: false + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + client.close(); + sleep(1); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/params.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/params.md new file mode 100644 index 000000000..addc4a97f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/params.md @@ -0,0 +1,44 @@ +--- +title: 'Params' +head_title: 'gRPC.params' +description: 'Params is an object used by the gRPC methods that generate RPC requests.' +weight: 20 +--- + +# Params + +_Params_ is an object used by the gRPC methods that generate RPC requests. _Params_ contains request-specific options like headers that should be inserted into the request. + +| Name | Type | Description | +| ----------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `Params.metadata` | object | Object with key-value pairs representing custom metadata the user would like to add to the request. Values of [keys ending with `-bin`](https://grpc.io/docs/what-is-grpc/core-concepts/#metadata) will be treated as binary data. | +| `Params.tags` | object | Key-value pairs where the keys are names of tags and the values are tag values. Response time metrics generated as a result of the request will have these tags added to them, allowing the user to filter out those results specifically, when looking at results data. | +| `Params.timeout` | string / number | Request timeout to use. Default timeout is 60 seconds (`"60s"`).
The type can also be a number, in which case k6 interprets it as milliseconds, e.g., `60000` is equivalent to `"60s"`. | + +### Example of custom metadata headers and tags + +
+ +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); +client.load([], 'route_guide.proto'); + +export default function () { + const req = { + latitude: 410248224, + longitude: -747127767, + }; + const params = { + metadata: { + 'x-my-header': 'k6test', + 'x-my-header-bin': new Uint8Array([1, 2, 3]), + }, + tags: { k6test: 'yes' }, + }; + const response = client.invoke('main.RouteGuide/GetFeature', req, params); +} +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/response.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/response.md new file mode 100644 index 000000000..4dc1c5abd --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/response.md @@ -0,0 +1,46 @@ +--- +title: 'Response' +head_title: 'gRPC.Response' +description: 'The response object of a gRPC request.' +weight: 30 +--- + +# Response + +| Name | Type | Description | +| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Response.status` | number | The response gRPC status code. Use the gRPC [status constants](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/constants) to check equality. | +| `Response.message` | object | The successful protobuf message, serialized to JSON. Will be `null` if `status !== grpc.StatusOK`. | +| `Response.headers` | object | Key-value pairs representing all the metadata headers returned by the gRPC server. | +| `Response.trailers` | object | Key-value pairs representing all the metadata trailers returned by the gRPC server. | +| `Response.error` | object | If `status !== grpc.StatusOK` then the error protobuf message, serialized to JSON; otherwise `null`. | + +### Example + +{{< code >}} + +```javascript +import grpc from 'k6/net/grpc'; +import { check, sleep } from 'k6'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001', { + // plaintext: false + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + client.close(); + sleep(1); +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/_index.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/_index.md new file mode 100644 index 000000000..2733feede --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/_index.md @@ -0,0 +1,203 @@ +--- +title: Stream +description: gRPC Streams +weight: 30 +--- + +# Stream + +Using a gRPC client creates a stream. The client should be connected to the server (`client.connect` called) before creating a stream. + +| Method | Description | +| ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [Stream.write(message)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-write) | Writes a message to the stream. | +| [Stream.on(event, handler)](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-on) | Sets up handler functions for various events on the gRPC stream. | +| [Stream.end()](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-end) | Signals to the server that the client has finished sending. | + +### Examples + +_A k6 script that sends several randomly chosen points from the pre-generated feature database with a variable delay in between. Prints the statistics when they are sent from the server._ + +{{< code >}} + +```javascript +import { Client, Stream } from 'k6/net/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const GRPC_ADDR = __ENV.GRPC_ADDR || '127.0.0.1:10000'; +const GRPC_PROTO_PATH = __ENV.GRPC_PROTO_PATH || '../../grpc_server/route_guide.proto'; + +const client = new Client(); +client.load([], GRPC_PROTO_PATH); + +// a sample DB of points +const DB = [ + { + location: { latitude: 407838351, longitude: -746143763 }, + name: 'Patriots Path, Mendham, NJ 07945, USA', + }, + { + location: { latitude: 408122808, longitude: -743999179 }, + name: '101 New Jersey 10, Whippany, NJ 07981, USA', + }, + { + location: { latitude: 413628156, longitude: -749015468 }, + name: 'U.S. 6, Shohola, PA 18458, USA', + }, + { + location: { latitude: 419999544, longitude: -740371136 }, + name: '5 Conners Road, Kingston, NY 12401, USA', + }, + { + location: { latitude: 414008389, longitude: -743951297 }, + name: 'Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA', + }, + { + location: { latitude: 419611318, longitude: -746524769 }, + name: '287 Flugertown Road, Livingston Manor, NY 12758, USA', + }, + { + location: { latitude: 406109563, longitude: -742186778 }, + name: '4001 Tremley Point Road, Linden, NJ 07036, USA', + }, + { + location: { latitude: 416802456, longitude: -742370183 }, + name: '352 South Mountain Road, Wallkill, NY 12589, USA', + }, + { + location: { latitude: 412950425, longitude: -741077389 }, + name: 'Bailey Turn Road, Harriman, NY 10926, USA', + }, + { + location: { latitude: 412144655, longitude: -743949739 }, + name: '193-199 Wawayanda Road, Hewitt, NJ 07421, USA', + }, +]; + +export default () => { + if (__ITER == 0) { + client.connect(GRPC_ADDR, { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + console.log('Passed', stats.featureCount, 'features'); + console.log('Travelled', stats.distance, 'meters'); + console.log('It took', stats.elapsedTime, 'seconds'); + }); + + stream.on('error', (err) => { + console.log('Stream Error: ' + JSON.stringify(err)); + }); + + stream.on('end', () => { + client.close(); + console.log('All done'); + }); + + // send 5 random items + for (let i = 0; i < 5; i++) { + const point = DB[Math.floor(Math.random() * DB.length)]; + pointSender(stream, point); + } + + // close the client stream + stream.end(); + + sleep(1); +}; + +const pointSender = (stream, point) => { + console.log( + 'Visiting point ' + + point.name + + ' ' + + point.location.latitude / COORD_FACTOR + + ', ' + + point.location.longitude / COORD_FACTOR + ); + + // send the location to the server + stream.write(point.location); + + sleep(0.5); +}; +``` + +{{< /code >}} + +_A k6 script that sends a rectangle message and results (features) are streamed back to the client._ + +{{< code >}} + +```javascript +import { Client, Stream } from 'k6/net/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const GRPC_ADDR = __ENV.GRPC_ADDR || '127.0.0.1:10000'; +const GRPC_PROTO_PATH = __ENV.GRPC_PROTO_PATH || '../../grpc_server/route_guide.proto'; + +const client = new Client(); + +client.load([], GRPC_PROTO_PATH); + +export default () => { + client.connect(GRPC_ADDR, { plaintext: true }); + + const stream = new Stream(client, 'main.FeatureExplorer/ListFeatures', null); + + stream.on('data', function (feature) { + console.log( + 'Found feature called "' + + feature.name + + '" at ' + + feature.location.latitude / COORD_FACTOR + + ', ' + + feature.location.longitude / COORD_FACTOR + ); + }); + + stream.on('end', function () { + // The server has finished sending + client.close(); + console.log('All done'); + }); + + stream.on('error', function (e) { + // An error has occurred and the stream has been closed. + console.log('Error: ' + JSON.stringify(e)); + }); + + // send a message to the server + stream.write({ + lo: { + latitude: 400000000, + longitude: -750000000, + }, + hi: { + latitude: 420000000, + longitude: -730000000, + }, + }); + + sleep(0.5); +}; +``` + +{{< /code >}} + +The preceding examples use a demo server, which you can run with the following command (Golang should be installed) in [k6 repository's root](https://github.com/grafana/k6): + +{{< code >}} + +```bash +$ go run -mod=mod examples/grpc_server/*.go +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-end.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-end.md new file mode 100644 index 000000000..598ca2632 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-end.md @@ -0,0 +1,49 @@ +--- +title: 'Stream.end()' +description: 'Signals to the server that the client has finished sending.' +weight: 40 +--- + +# Stream.end() + +Signals to the server that the client has finished sending messages. + +### Example + +
+ +```javascript +import { Client, Stream } from 'k6/net/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const client = new Client(); +client.load([], '../../grpc_server/route_guide.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + console.log('Passed', stats.featureCount, 'features'); + console.log('Traveled', stats.distance, 'meters'); + console.log('It took', stats.elapsedTime, 'seconds'); + }); + + // send 2 items + stream.write({ latitude: 406109563, longitude: -742186778 }); + stream.write({ latitude: 416802456, longitude: -742370183 }); + + // send end-signal to the server + stream.end(); + + sleep(1); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-error.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-error.md new file mode 100644 index 000000000..8d04a4c38 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-error.md @@ -0,0 +1,16 @@ +--- +title: 'Error' +head_title: 'gRPC.Error' +description: 'The error object of a gRPC stream.' +weight: 15 +--- + +# Error + +The error object is the object that is passed to the `error` event handler function. + +| Name | Type | Description | +| --------------- | ------ | ---------------------------------------- | +| `Error.code` | number | A gRPC error code. | +| `Error.details` | array | A list of details attached to the error. | +| `Error.message` | string | An original error message. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-on.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-on.md new file mode 100644 index 000000000..df95f5c55 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-on.md @@ -0,0 +1,69 @@ +--- +title: 'Stream.on()' +description: 'Set up handler functions for various events on the GRPC stream.' +weight: 10 +--- + +# Stream.on() + +Set up handler functions for various events on the gRPC stream. + +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------- | +| event | string | The event name to define a handler for. | +| handler | function | The function to call when the event happens. | + +Possible events: + +| Event name | Description | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| data | Emitted when the server sends data. | +| error | Emitted when an error occurs. In case of the error, an [`Error`](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-error) object sends to the handler function. | +| end | Emitted when the server closes the incoming stream. | + +### Example + +
+ +```javascript +import { Client, Stream } from 'k6/net/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const client = new Client(); +client.load([], '../../grpc_server/route_guide.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + // sets up a handler for the data (server sends data) event + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + console.log('Passed', stats.featureCount, 'features'); + console.log('Traveled', stats.distance, 'meters'); + console.log('It took', stats.elapsedTime, 'seconds'); + }); + + // sets up a handler for the end event (stream closes) + stream.on('end', function () { + // The server has finished sending + client.close(); + console.log('All done'); + }); + + // sets up a handler for the error event (an error occurs) + stream.on('error', function (e) { + // An error has occurred and the stream has been closed. + console.log('Error: ' + JSON.stringify(e)); + }); + + sleep(1); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-write.md b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-write.md new file mode 100644 index 000000000..136733715 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-net-grpc/stream/stream-write.md @@ -0,0 +1,45 @@ +--- +title: 'Stream.write()' +description: 'Writes a message to the stream.' +weight: 40 +--- + +# Stream.write() + +Writes a message to the stream. The message is a canonical request object, as-per the [Protobuf JSON Mapping](https://grafana.com/docs/k6//using-k6/protocols/grpc/#protocol-buffers-json-mapping). + +### Example + +
+ +```javascript +import { Client, Stream } from 'k6/net/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const client = new Client(); +client.load([], '../../grpc_server/route_guide.proto'); + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + stream.on('data', (stats) => { + console.log('Finished trip with', stats.pointCount, 'points'); + }); + + // send an item + stream.write({ latitude: 406109563, longitude: -742186778 }); + + // send end-signal to the server + stream.end(); + + sleep(1); +}; +``` + +
diff --git a/docs/sources/v0.50.x/javascript-api/k6-timers/_index.md b/docs/sources/v0.50.x/javascript-api/k6-timers/_index.md new file mode 100644 index 000000000..4f3fe911e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-timers/_index.md @@ -0,0 +1,16 @@ +--- +title: 'k6/timers' +description: 'k6 timers API' +weight: 13 +--- + +# k6/timers + +Implement timers to work with k6's event loop. They mimic the functionality found in browsers and other JavaScript runtimes. + +| Function | Description | +| :---------------------------------------------------------------------------- | :--------------------------------------------------- | +| [setTimeout](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) | Sets a function to be run after a given timeout. | +| [clearTimeout](https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout) | Clears a previously set timeout with `setTimeout`. | +| [setInterval](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) | Sets a function to be run on a given interval. | +| [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/setInterval) | Clears a previously set interval with `setInterval`. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/_index.md b/docs/sources/v0.50.x/javascript-api/k6-ws/_index.md new file mode 100644 index 000000000..554456914 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/_index.md @@ -0,0 +1,27 @@ +--- +title: 'k6/ws' +description: 'k6 WebSocket API' +weight: 12 +--- + +# k6/ws + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +The ws module provides a [WebSocket](https://en.wikipedia.org/wiki/WebSocket) client implementing the [WebSocket protocol](http://www.rfc-editor.org/rfc/rfc6455.txt). + +| Function | Description | +| --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [connect( url, params, callback )](https://grafana.com/docs/k6//javascript-api/k6-ws/connect) | Create a WebSocket connection, and provides a [Socket](https://grafana.com/docs/k6//javascript-api/k6-ws/socket) client to interact with the service. The method blocks the test finalization until the connection is closed. | + +| Class/Method | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Params](https://grafana.com/docs/k6//javascript-api/k6-ws/params) | Used for setting various WebSocket connection parameters such as headers, cookie jar, compression, etc. | +| [Socket](https://grafana.com/docs/k6//javascript-api/k6-ws/socket) | WebSocket client used to interact with a WS connection. | +| [Socket.close()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-close) | Close the WebSocket connection. | +| [Socket.on(event, callback)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-on) | Set up an event listener on the connection for any of the following events:
- open
- binaryMessage
- message
- ping
- pong
- close
- error. | +| [Socket.ping()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-ping) | Send a ping. | +| [Socket.send(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-send) | Send string data. | +| [Socket.sendBinary(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-sendbinary) | Send binary data. | +| [Socket.setInterval(callback, interval)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-setinterval) | Call a function repeatedly at certain intervals, while the connection is open. | +| [Socket.setTimeout(callback, period)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-settimeout) | Call a function with a delay, if the connection is open. | diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/connect.md b/docs/sources/v0.50.x/javascript-api/k6-ws/connect.md new file mode 100644 index 000000000..be9761fed --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/connect.md @@ -0,0 +1,52 @@ +--- +title: 'connect( url, params, callback )' +description: 'Create a WebSocket connection, and provides a Socket client to interact with the service.' +description: 'Create a WebSocket connection, and provides a Socket client to interact with the service.' +weight: 10 +--- + +# connect( url, params, callback ) + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Initiate a WebSocket connection to a remote host. + +Calling connect will block the VU finalization until the WebSocket connection is closed. Instead of continuously looping the main function (`export default function() { ... }`) over an over, each VU will be halted listening to async events and executing their event handlers until the connection is closed. + +The following events can close the connection: + +- remote host close event. +- [Socket.close()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-close). +- k6 VU interruption based on test configuration or CLI commands. + +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| url | string | Request URL (e.g. "ws://echo.websocket.org"). | +| params | object | [Params](https://grafana.com/docs/k6//javascript-api/k6-ws/params) object containing additional request parameters. | +| callback | function | The callback function that will be called when the WebSocket connection is initiated. A [Socket](https://grafana.com/docs/k6//javascript-api/k6-ws/socket) object will be passed to the function, and this object can be used to set up callbacks etc when things happen on the WebSocket connection | + +### Returns + +| Type | Description | +| ------------------------------------------------------------------------------------ | --------------------- | +| [Response](https://grafana.com/docs/k6//javascript-api/k6-http/response) | HTTP Response object. | + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const resp = ws.connect(url, null, function (socket) { + socket.on('open', function () { + console.log('WebSocket connection established!'); + socket.close(); + }); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/params.md b/docs/sources/v0.50.x/javascript-api/k6-ws/params.md new file mode 100644 index 000000000..659917264 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/params.md @@ -0,0 +1,45 @@ +--- +title: 'Params' +description: 'Used for setting various WebSocket request-specific parameters such as headers, tags, etc.' +description: 'Used for setting various WebSocket request-specific parameters such as headers, tags, etc.' +weight: 20 +--- + +# Params + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +_Params_ is an object used by the WebSocket methods that generate WebSocket requests. _Params_ contains request-specific options like headers that should be inserted into the request. + +| Name | Type | Description | +| -------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `Params.compression` | string | Compression algorithm to be used by the WebSocket connection. The only supported algorithm currently is `deflate`. If the option is left unset or empty, it defaults to no compression. | +| `Params.jar` | [http.CookieJar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) | The cookie jar that will be used when making the initial HTTP request to establish the WebSocket connection. If empty, the [default VU cookie jar](https://grafana.com/docs/k6//javascript-api/k6-http/cookiejar) will be used. | +| `Params.headers` | object | Custom HTTP headers in key-value pairs that will be added to the initial HTTP request to establish the WebSocket connection. Keys are header names and values are header values. | +| `Params.tags` | object | [Custom metric tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#user-defined-tags) in key-value pairs where the keys are names of tags and the values are tag values. The WebSocket connection will [generate metrics samples](https://grafana.com/docs/k6//javascript-api/k6-ws/socket#websocket-built-in-metrics) with these tags attached, allowing users to filter the results data or set [thresholds on sub-metrics](https://grafana.com/docs/k6//using-k6/thresholds#thresholds-on-tags). | + +### Example of custom metadata headers and tags + +_A k6 script that will make a WebSocket request with a custom header and tag results data with a specific tag_ + +{{< code >}} + +```javascript +import ws from 'k6/ws'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { + headers: { 'X-MyHeader': 'k6test' }, + tags: { k6test: 'yes' }, + }; + const res = ws.connect(url, params, function (socket) { + socket.on('open', function () { + console.log('WebSocket connection established!'); + socket.close(); + }); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/_index.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/_index.md new file mode 100644 index 000000000..8165fa067 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/_index.md @@ -0,0 +1,92 @@ +--- +title: 'Socket' +description: 'Socket is a WebSocket client to interact with a WebSocket connection.' +weight: 80 +weight: 80 +--- + +# Socket + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +`Socket` is a WebSocket client to interact with a WebSocket connection. You can use it to listen various events happening on the WebSocket connection and send messages to the server. Additionally, you can use socket.setTimeout() and socket.setInterval() to execute code in the background, or repeatedly, while the WebSocket connection is open. + +| Method | Description | +| --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Socket.close()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-close) | Close the WebSocket connection. | +| [Socket.on(event, callback)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-on) | Set up an event listener on the connection for any of the following events:
- open
- binaryMessage
- message
- ping
- pong
- close
- error. | +| [Socket.ping()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-ping) | Send a ping. | +| [Socket.send(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-send) | Send string data. | +| [Socket.sendBinary(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-sendbinary) | Send binary data. | +| [Socket.setInterval(callback, interval)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-setinterval) | Call a function repeatedly at certain intervals, while the connection is open. | +| [Socket.setTimeout(callback, period)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-settimeout) | Call a function with a delay, if the connection is open. | + +### WebSocket built-in metrics + +`k6` automatically collects metrics when interacting with a WebSocket service through the `k6/ws` API. +Review the list in the [metrics reference](https://grafana.com/docs/k6//using-k6/metrics/reference#websockets). + +Check out the [Results output article](https://grafana.com/docs/k6//get-started/results-output) for more information about how to process the metric information. + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const response = ws.connect(url, params, function (socket) { + socket.on('open', function open() { + console.log('connected'); + socket.send(Date.now()); + + socket.setInterval(function timeout() { + socket.ping(); + console.log('Pinging every 1sec (setInterval test)'); + }, 1000); + }); + + socket.on('ping', function () { + console.log('PING!'); + }); + + socket.on('pong', function () { + console.log('PONG!'); + }); + + socket.on('pong', function () { + // Multiple event handlers on the same event + console.log('OTHER PONG!'); + }); + + socket.on('message', function (message) { + console.log(`Received message: ${message}`); + }); + + socket.on('close', function () { + console.log('disconnected'); + }); + + socket.on('error', function (e) { + if (e.error() != 'websocket: close sent') { + console.log('An unexpected error occured: ', e.error()); + } + }); + + socket.setTimeout(function () { + console.log('2 seconds passed, closing the socket'); + socket.close(); + }, 2000); + }); + + check(response, { 'status is 101': (r) => r && r.status === 101 }); +} +//VU execution won't be completely finished until the connection is closed. +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-close.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-close.md new file mode 100644 index 000000000..359e7eed1 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-close.md @@ -0,0 +1,33 @@ +--- +title: 'Socket.close([code])' +description: 'Close the WebSocket connection.' +--- + +# Socket.close([code]) + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Close the WebSocket connection. + +| Parameter | Type | Description | +| --------------- | ------ | -------------------------------------- | +| code (optional) | number | WebSocket status code. (default: 1001) | + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const response = ws.connect(url, null, function (socket) { + socket.on('open', function () { + socket.close(); + }); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-on.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-on.md new file mode 100644 index 000000000..a9a49e4ac --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-on.md @@ -0,0 +1,86 @@ +--- +title: 'Socket.on(event, callback)' +description: 'Set up callback functions for various events on the WebSocket connection.' +--- + +# Socket.on(event, callback) + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Set up callback functions for various events on the WebSocket connection. Multiple handlers can be defined for the same event. + +| Parameter | Type | Description | +| --------- | -------- | -------------------------------------------- | +| event | string | The event name to define a callback for. | +| callback | function | The function to call when the event happens. | + +| Event name | Description | +| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| open | Emitted when the connection is established | +| message | Emitted when a message is received from the server. | +| ping | Emitted when a ping is received from the server. The client will automatically send back a `pong`. | +| pong | Emitted when a pong is received from the server. | +| close | Emitted when the connection is closed by the client [Socket.close()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-close) or when the server sends the `close` event with code status 1000 (normal closure). | +| error | Emitted when an error occurs. Non-normal closure errors will be forwarded. | + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const response = ws.connect(url, params, function (socket) { + socket.on('open', function open() { + console.log('connected'); + socket.send(Date.now()); + + socket.setInterval(function timeout() { + socket.ping(); + console.log('Pinging every 1sec (setInterval test)'); + }, 1000); + }); + + socket.on('ping', function () { + console.log('PING!'); + }); + + socket.on('pong', function () { + console.log('PONG!'); + }); + + socket.on('pong', function () { + // Multiple event handlers on the same event + console.log('OTHER PONG!'); + }); + + socket.on('message', function (message) { + console.log(`Received message: ${message}`); + }); + + socket.on('close', function () { + console.log('disconnected'); + }); + + socket.on('error', function (e) { + if (e.error() != 'websocket: close sent') { + console.log('An unexpected error occured: ', e.error()); + } + }); + + socket.setTimeout(function () { + console.log('2 seconds passed, closing the socket'); + socket.close(); + }, 2000); + }); + + check(response, { 'status is 101': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-ping.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-ping.md new file mode 100644 index 000000000..88a387bdd --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-ping.md @@ -0,0 +1,34 @@ +--- +title: 'Socket.ping()' +description: 'Send a ping. Ping messages can be used to verify that the remote endpoint is responsive.' +--- + +# Socket.ping() + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Send a ping. Ping messages can be used to verify that the remote endpoint is responsive. + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const response = ws.connect(url, null, function (socket) { + socket.on('open', function () { + socket.on('pong', function () { + // As required by the spec, when the ping is received, the recipient must send back a pong. + console.log('connection is alive'); + }); + + socket.ping(); + }); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-send.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-send.md new file mode 100644 index 000000000..c1f6bc07d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-send.md @@ -0,0 +1,37 @@ +--- +title: 'Socket.send(data)' +description: 'Send a data string through the connection.' +--- + +# Socket.send(data) + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Send a data string through the connection. +You can use `JSON.stringify` to convert a JSON or JavaScript values to a JSON string. + +| Parameter | Type | Description | +| --------- | ------ | ----------------- | +| data | string | The data to send. | + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const response = ws.connect(url, null, function (socket) { + socket.on('open', function () { + socket.send('my-message'); + socket.send(JSON.stringify({ data: 'hola' })); + }); + }); +} +``` + +{{< /code >}} + +- See also [Socket.sendBinary(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-sendbinary) diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-sendbinary.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-sendbinary.md new file mode 100644 index 000000000..819bde99a --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-sendbinary.md @@ -0,0 +1,41 @@ +--- +title: 'Socket.sendBinary(data)' +description: 'Send binary data through the connection.' +--- + +# Socket.sendBinary(data) + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Send binary data through the connection. + +| Parameter | Type | Description | +| --------- | ----------- | ----------------- | +| data | ArrayBuffer | The data to send. | + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; + +const binFile = open('./file.pdf', 'b'); + +export default function () { + ws.connect('http://wshost/', function (socket) { + socket.on('open', function () { + socket.sendBinary(binFile); + }); + + socket.on('binaryMessage', function (msg) { + // msg is an ArrayBuffer, so we can wrap it in a typed array directly. + new Uint8Array(msg); + }); + }); +} +``` + +{{< /code >}} + +- See also [Socket.send(data)](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-send) diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-setinterval.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-setinterval.md new file mode 100644 index 000000000..0a3b84dae --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-setinterval.md @@ -0,0 +1,48 @@ +--- +title: 'Socket.setInterval(callback, interval)' +descriptiontion: 'Call a function repeatedly, while the WebSocket connection is open.' +--- + +# Socket.setInterval(callback, interval) + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Call a function repeatedly, while the WebSocket connection is open. + +| Parameter | Type | Description | +| --------- | -------- | ----------------------------------------------------------- | +| callback | function | The function to call every `interval` milliseconds. | +| interval | number | The number of milliseconds between two calls to `callback`. | + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const res = ws.connect(url, params, function (socket) { + socket.on('open', function open() { + console.log('connected'); + + socket.setInterval(function timeout() { + socket.ping(); + console.log('Pinging every 1sec (setInterval test)'); + }, 1000); + }); + + socket.on('pong', function () { + console.log('PONG!'); + }); + }); + + check(res, { 'status is 101': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-settimeout.md b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-settimeout.md new file mode 100644 index 000000000..b23f2ebfd --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6-ws/socket/socket-settimeout.md @@ -0,0 +1,47 @@ +--- +title: 'Socket.setTimeout(callback, delay)' +description: 'Call a function at a later time, if the WebSocket connection is still open then.' +--- + +# Socket.setTimeout(callback, delay) + +{{< docs/shared source="k6" lookup="ws-module.md" version="" >}} + +Call a function at a later time, if the WebSocket connection is still open then. + +| Parameter | Type | Description | +| --------- | -------- | ---------------------------------------------- | +| callback | function | The function to call when `delay` has expired. | +| delay | number | The delay time, in milliseconds. | + +### Example + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { sleep } from 'k6'; + +export default function () { + console.log('T0: Script started'); + const url = 'ws://echo.websocket.org'; + const response = ws.connect(url, null, function (socket) { + console.log('T0: Entered WebSockets run loop'); + socket.setTimeout(function () { + console.log('T0+1: This is printed'); + }, 1000); + socket.setTimeout(function () { + console.log('T0+2: Closing socket'); + socket.close(); + }, 2000); + socket.setTimeout(function () { + console.log('T0+3: This is not printed, because socket is closed'); + }, 3000); + }); + console.log('T0+2: Exited WebSockets run loop'); + sleep(2); + console.log('T0+4: Script finished'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6/_index.md b/docs/sources/v0.50.x/javascript-api/k6/_index.md new file mode 100644 index 000000000..6bb6907ed --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6/_index.md @@ -0,0 +1,17 @@ +--- +title: 'k6' +description: 'The k6 module contains k6-specific functionality.' +weight: 02 +--- + +# k6 + +The k6 module contains k6-specific functionality. + +| Function | Description | +| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| [check(val, sets, [tags])](https://grafana.com/docs/k6//javascript-api/k6/check) | Runs one or more checks on a value and generates a pass/fail result but does not throw errors or otherwise interrupt execution upon failure. | +| [fail([err])](https://grafana.com/docs/k6//javascript-api/k6/fail) | Throws an error, failing and aborting the current VU script iteration immediately. | +| [group(name, fn)](https://grafana.com/docs/k6//javascript-api/k6/group) | Runs code inside a group. Used to organize results in a test. | +| [randomSeed(int)](https://grafana.com/docs/k6//javascript-api/k6/random-seed) | Set seed to get a reproducible pseudo-random number using `Math.random`. | +| [sleep(t)](https://grafana.com/docs/k6//javascript-api/k6/sleep) | Suspends VU execution for the specified duration. | diff --git a/docs/sources/v0.50.x/javascript-api/k6/check.md b/docs/sources/v0.50.x/javascript-api/k6/check.md new file mode 100644 index 000000000..5a155583d --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6/check.md @@ -0,0 +1,74 @@ +--- +title: 'check( val, sets, [tags] )' +description: 'Runs one or more checks on a value and generates a pass/fail result but does not throw errors or otherwise interrupt execution upon failure.' +description: 'Runs one or more checks on a value and generates a pass/fail result but does not throw errors or otherwise interrupt execution upon failure.' +--- + +# check( val, sets, [tags] ) + +Run checks on a value. A check is a test condition that can give a truthy or +falsy result. The `sets` parameter contains one or more checks, and the `check()` +function will return `false` if any of them fail. + +Note that checks are not _asserts_ in their traditional sense - a failed assertion +will throw an error, while a check will always return with a pass or a failure. +Failure conditions can then instead be controlled by thresholds, for more power and flexibility. + +| Parameter | Type | Description | +| --------------- | ------ | ---------------------------------------- | +| val | any | Value to test. | +| sets | object | Tests (checks) to run on the value. | +| tags (optional) | object | Extra tags to attach to metrics emitted. | + +### Returns + +| Type | Description | +| ------- | ------------------------------------------------------- | +| boolean | `true` if all checks have succeeded, `false` otherwise. | + +### Examples + +Using `check()` to verify that an HTTP response code was 200: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res = http.get('https://httpbin.test.k6.io'); + check(res, { + 'response code was 200': (res) => res.status == 200, + }); +} +``` + +{{< /code >}} + +Using `check()` with a custom tag to verify that an HTTP response code was 200 and that body was 1234 bytes. The `checkOutput` can be used for any condition in your script logic: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, fail } from 'k6'; + +export default function () { + const res = http.get('https://httpbin.test.k6.io'); + const checkOutput = check( + res, + { + 'response code was 200': (res) => res.status == 200, + 'body size was 1234 bytes': (res) => res.body.length == 1234, + }, + { myTag: "I'm a tag" } + ); + + if (!checkOutput) { + fail('unexpected response'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6/fail.md b/docs/sources/v0.50.x/javascript-api/k6/fail.md new file mode 100644 index 000000000..a6773f9ed --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6/fail.md @@ -0,0 +1,43 @@ +--- +title: 'fail( [err] )' +description: 'Throws an error, failing and aborting the current VU script iteration immediately.' +description: 'Throws an error, failing and aborting the current VU script iteration immediately.' +--- + +# fail( [err] ) + +Immediately throw an error, aborting the current iteration. + +`fail()` does not abort the test, nor does it make the test exit with non-0 status. +If you are looking to fail the test by halting the execution, use [test.abort()](https://grafana.com/docs/k6//javascript-api/k6-execution/#test) instead + +`fail()` is a simple convenience wrapper on top of JavaScript's `throw()`, +because the latter cannot be used as `[expr] || throw`, which is a convenient way to write k6 test code. + +| Parameter | Type | Description | +| -------------- | ------ | ------------------------------------------ | +| err (optional) | string | Error message that gets printed to stderr. | + +### Example + +Aborting the current script iteration if a check fails: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, fail } from 'k6'; + +export default function () { + const res = http.get('https://k6.io'); + if ( + !check(res, { + 'status code MUST be 200': (res) => res.status == 200, + }) + ) { + fail('status code was *not* 200'); + } +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6/group.md b/docs/sources/v0.50.x/javascript-api/k6/group.md new file mode 100644 index 000000000..1b4e6d750 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6/group.md @@ -0,0 +1,65 @@ +--- +title: 'group( name, fn )' +description: 'Runs code inside a group. Used to organize results in a test.' +description: 'Runs code inside a group. Used to organize results in a test.' +--- + +# group( name, fn ) + +Run code inside a group. Groups are used to organize results in a test. + +| Parameter | Type | Description | +| --------- | -------- | ------------------------------------------------------ | +| name | string | Name of the group. | +| fn | function | Group body - code to be executed in the group context. | + +### Returns + +| Type | Description | +| ---- | ------------------------- | +| any | The return value of _fn_. | + +{{% admonition type="warning" %}} + +Avoid using `group` with async functions or asynchronous code. +If you do, k6 might apply tags in a way that is unreliable or unintuitive. + +If you start promise chains or even use `await` within `group`, some code within the group will be waited for and tagged with the proper `group` tag, but others won't be. + +To avoid confusion, `async` functions are forbidden as `group()` arguments. This still lets users make and chain promises within a group, but doing so is unsupported and not recommended. + +For more information, refer to [k6 #2728](https://github.com/grafana/k6/issues/2728), which tracks possible solutions and provides detailed explanations. + +{{% /admonition %}} + +### Example + +{{< code >}} + +```javascript +import { group } from 'k6'; + +export default function () { + group('visit product listing page', function () { + // ... + }); + group('add several products to the shopping cart', function () { + // ... + }); + group('visit login page', function () { + // ... + }); + group('authenticate', function () { + // ... + }); + group('checkout process', function () { + // ... + }); +} +``` + +{{< /code >}} + +The above code will present the results separately depending on the group execution. + +Learn more on [Groups and Tags](https://grafana.com/docs/k6//using-k6/tags-and-groups). diff --git a/docs/sources/v0.50.x/javascript-api/k6/random-seed.md b/docs/sources/v0.50.x/javascript-api/k6/random-seed.md new file mode 100644 index 000000000..74a4677fc --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6/random-seed.md @@ -0,0 +1,36 @@ +--- +title: 'randomSeed( int )' +description: 'Set seed to get a reproducible pseudo-random number using `Math.random`.' +description: 'Set seed to get a reproducible pseudo-random number using `Math.random`.' +--- + +# randomSeed( int ) + +Set seed to get a reproducible pseudo-random number using `Math.random`. + +| Parameter | Type | Description | +| --------- | ------- | --------------- | +| int | integer | The seed value. | + +### Example + +Use `randomSeed` to get the same random number in all the iterations. + +{{< code >}} + +```javascript +import { randomSeed } from 'k6'; + +export const options = { + vus: 10, + duration: '5s', +}; + +export default function () { + randomSeed(123456789); + const rnd = Math.random(); + console.log(rnd); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/k6/sleep.md b/docs/sources/v0.50.x/javascript-api/k6/sleep.md new file mode 100644 index 000000000..f9ba44fa5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/k6/sleep.md @@ -0,0 +1,50 @@ +--- +title: 'sleep( t )' +description: 'Suspends VU execution for the specified duration.' +description: 'Suspends VU execution for the specified duration.' +--- + +# sleep( t ) + +Suspend VU execution for the specified duration. + +| Parameter | Type | Description | +| --------- | ------ | --------------------- | +| t | number | Duration, in seconds. | + +### Examples + +Fetching two different pages with a 0-30 second random sleep in between: + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import http from 'k6/http'; + +export default function () { + http.get('https://k6.io'); + sleep(Math.random() * 30); + http.get('https://k6.io/features'); +} +``` + +{{< /code >}} + +Using the [k6-utils](https://grafana.com/docs/k6//javascript-api/jslib/utils) library to specify a range between a minimum and maximum: + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import http from 'k6/http'; +import { randomIntBetween } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export default function () { + http.get('https://k6.io'); + sleep(randomIntBetween(20, 30)); + http.get('https://k6.io/features'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/_index.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/_index.md new file mode 100644 index 000000000..5933b2dc9 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/_index.md @@ -0,0 +1,14 @@ +--- +title: xk6-disruptor API +description: 'An overview of the API for xk6-disruptor.' +weight: 14 +--- + +# xk6-disruptor API + +The xk6-disruptor API is organized around _disruptors_ that affect specific targets such as pods or services. These disruptors can inject different types of [faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults) on their targets. + +| Class | Description | +| ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | +| [PodDisruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor) | Targets the Pods that match selection attributes such as labels. | +| [ServiceDisruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor) | Targets the Pods that back a Kubernetes Service | diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/_index.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/_index.md new file mode 100644 index 000000000..677e689cf --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/_index.md @@ -0,0 +1,15 @@ +--- +title: 'Faults' +description: 'xk6-disruptor: Fault Description' +weight: 100 +--- + +# Faults + +A fault is as an abnormal condition that affects a system component and which may lead to a failure. + +| Fault type | Description | +| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | +| [gRPC Fault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/grpc) | Fault affecting gRPC requests from a target | +| [HTTP Fault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/http) | Fault affecting HTTP requests from a target | +| [Pod Termination Fault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/pod-termination) | Fault terminating a number of target Pods | diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/grpc.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/grpc.md new file mode 100644 index 000000000..4ddb962c2 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/grpc.md @@ -0,0 +1,39 @@ +--- +title: 'gRPC' +description: 'xk6-disruptor: gRPC Fault attributes' +weight: 01 +--- + +# gRPC + +A gRPC Fault describes the characteristics of the faults to be injected in the gRPC requests served by a target. + +A gRPC fault is described by the following attributes: + +| Attribute | Type | Description | +| -------------- | ------ | -------------------------------------------------------------------------------------------------------------- | +| averageDelay | string | average delay added to requests represented as a string (default `0`) | +| delayVariation | string | variation in the injected delay (default `0`) | +| statusMessage | string | message to be returned when an error is injected | +| statusCode | number | status to be returned when an error is injected | +| errorRate | number | rate of requests that will return an error, represented as a float in the range `0.0` to `1.0` (default `0.0`) | +| exclude | string | comma-separated list of services to be excluded from disruption | +| port | number | port on which the requests will be intercepted | + +{{% admonition type="note" %}} + +`averageDelay` and `delayVariation` are applied to all requests affected by the fault, regardless of the value of `errorRate`. `statusCode` is returned only to a fraction of requests defined by `errorRate`. + +{{% /admonition %}} + +## Example + +This example defines a gRPC fault that introduces a delay of `50ms` in all requests and returns a status code `13` in `10%` of the requests. + +```javascript +const fault = { + averageDelay: '50ms', + statusCode: 10, + errorRate: 0.1, +}; +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/http.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/http.md new file mode 100644 index 000000000..704ceec9e --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/http.md @@ -0,0 +1,39 @@ +--- +title: 'HTTP' +description: 'xk6-disruptor: HTTP Fault attributes' +weight: 02 +--- + +# HTTP + +A HTTP Fault describes the characteristics of the faults to be injected in the HTTP requests served by a target. + +A HTTP fault is described by the following attributes: + +| Attribute | Type | Description | +| -------------- | ------ | -------------------------------------------------------------------------------------------------------------- | +| averageDelay | string | average delay added to requests represented as a string (default `0`) | +| delayVariation | string | variation in the injected delay (default `0`) | +| errorBody | string | body to be returned when an error is injected | +| errorCode | number | error code to return | +| errorRate | number | rate of requests that will return an error, represented as a float in the range `0.0` to `1.0` (default `0.0`) | +| exclude | string | comma-separated list of urls to be excluded from disruption (e.g. /health) | +| port | number | port on which the requests will be intercepted | + +{{% admonition type="note" %}} + +`averageDelay` and `delayVariation` are applied to all requests affected by the fault, regardless of the value of `errorRate`. `errorCode` is returned only to a fraction of requests defined by `errorRate`. + +{{% /admonition %}} + +## Example + +This example defines a HTTP fault that introduces a delay of `50ms` in all requests and returns an error code `500` in `10%` of the requests. + +```javascript +const fault = { + averageDelay: '50ms', + errorCde: 500, + errorRate: 0.1, +}; +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/pod-termination.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/pod-termination.md new file mode 100644 index 000000000..30902aaff --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/faults/pod-termination.md @@ -0,0 +1,33 @@ +--- +title: 'Pod Termination' +description: 'xk6-disruptor: Pod Termination Fault attributes' +weight: 03 +--- + +# Pod Termination + +A Pod Termination Fault allows terminating either a fixed number or a percentage of the pods that matching a selector or back a service. + +A Pod Termination fault is defined by the following attributes: + +| Attribute | Type | Description | +| --------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| count | integer or percentage | the number of pods to be terminated. It can be specified as a integer number or as a percentage, for example `30%`, that defines the fraction of target pods to be terminated | + +{{% admonition type="note" %}} + +If the count is a percentage and there are no enough elements in the target pod list, the number is rounded up. +For example '25%' of a list of 2 target pods will terminate one pod. +If the list of target pods is not empty, at least one pod is always terminated. + +{{% /admonition %}} + +## Example + +This example defines a PorTermination fault that will terminate `30%` of target pods + +```javascript +const fault = { + count: '30%', +}; +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/get-started/_index.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/get-started/_index.md new file mode 100644 index 000000000..405b1eaad --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/get-started/_index.md @@ -0,0 +1,14 @@ +--- +title: 'Get started' +description: 'xk6-disruptor is an extension that adds fault injection capabilities to k6. Start here to learn the basics and how to use the disruptor' +weight: 01 +--- + +# Get started + +Inject faults into kubernetes-based applications with `xk6-disruptor`. Start here to learn the basics to use the disruptor: + +- [About `xk6-disruptor`](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/) +- [Requirements](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/requirements) +- [Installation](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/installation) +- [Exposing your Kubernetes application](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/expose--your-application) diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/_index.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/_index.md new file mode 100644 index 000000000..84289c3f5 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/_index.md @@ -0,0 +1,63 @@ +--- +title: 'PodDisruptor' +description: 'xk6-disruptor: PodDisruptor class' +weight: 200 +--- + +# PodDisruptor + +The `PodDisruptor` class can inject different types of faults into the pods that match a selection criteria. + +To construct a `PodDisruptor`, use the [PodDisruptor() constructor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/constructor). + +## Methods + +| Method | Description | +| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| [PodDisruptor.injectGrpcFaults()](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/injectgrpcfaults) | Inject [gRPC faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/grpc) in the target Pods | +| [PodDisruptor.injectHTTPFaults()](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/injecthttpfaults) | Inject [HTTP faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/http) in the target Pods | +| PodDisruptor.targets() | Returns the list of target Pods of the PodDisruptor | +| [PodDisruptor.terminatePods()](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/terminate-pods) | executes a [Pod Termination fault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/pod-termination) in the target Pods | + +## Example + +This example: + +- Creates a selector that matches all pods in the `default` namespace with the `run=nginx` label +- Injects a delay of 100ms and makes 10 percent of requests return an http response code `500`. + +```javascript +import { PodDisruptor } from 'k6/x/disruptor'; + +const selector = { + namespace: 'default', + select: { + labels: { + run: 'nginx', + }, + }, +}; + +const fault = { + averageDelay: '100ms', + errorRate: 0.1, + errorCode: 500, +}; + +export default function () { + const disruptor = new PodDisruptor(selector); + disruptor.injectHTTPFaults(fault, '30s'); +} +``` + +{{% admonition type="note" %}} + +You can test this script by first creating a pod running nginx with the command below, assuming you have [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) installed in your environment: + +```bash +$ kubectl run nginx --image=nginx +``` + +You can also use the [xk6-kubernetes](https://github.com/grafana/xk6-kubernetes) extension for creating these resources from your test script. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/constructor.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/constructor.md new file mode 100644 index 000000000..1add8fc9b --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/constructor.md @@ -0,0 +1,56 @@ +--- +title: 'Constructor' +description: 'xk6-disruptor: PodDisruptor constructor' +weight: 100 +--- + +# Constructor + +The `PodDisruptor()` constructor creates a new instance of a [PodDisruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor) class. + +| Parameter | Type | Description | +| ------------------ | ------ | ----------------------------------------------------------------- | +| selector | object | [criteria](#selector) for selecting the target pods | +| options (optional) | object | [options](#options) for controlling the behavior of the disruptor | + +### Selector + +The `selector` defines the criteria a pod must satisfy to be a valid target: + +| Attribute | Type | Description | +| --------- | ------ | ------------------------------------------------------------------------------------------- | +| namespace | string | namespace the selector will look for pods | +| select | object | [attributes](#pod-attributes) that a pod must match to be selected | +| exclude | object | [attributes](#pod-attributes) that exclude a pod (even if it matches the select attributes) | + +You can use the following attributes to select or exclude pods: + +### Pod attributes + +| Attribute | Type | Description | +| --------- | ------ | ------------------------------------------------------------ | +| labels | object | map with the labels to be matched for selection or exclusion | + +### Options + +The `options` control the creation and behavior of the `PodDisruptor`: + +| Attribute | Type | Description | +| ------------- | ------ | ----------------------------------------------------------------------------------- | +| injectTimeout | string | maximum time to wait for the disruptor to be ready in the target pods (default 30s) | + +## Example + + + +```javascript +const selector = { + namespace: 'my-namespace', + select: { + labels: { + app: 'my-app', + }, + }, +}; +const podDisruptor = new PodDisruptor(selector); +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/injectgrpcfaults.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/injectgrpcfaults.md new file mode 100644 index 000000000..b4274fdd4 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/injectgrpcfaults.md @@ -0,0 +1,36 @@ +--- +title: 'injectGrpcFaults()' +description: 'xk6-disruptor: PodDisruptor.injectGrpcFaults method' +weight: 200 +--- + +# injectGrpcFaults() + +injectGrpcFaults injects gRPC faults in the requests served by a target Pod. + +| Parameter | Type | Description | +| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| fault | object | description of the [gRPC faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/grpc) to be injected | +| duration | string | duration of the disruption | +| options (optional) | object | [options](#options) that control the injection of the fault | + +## options + +The injection of the fault is controlled by the following options: + +| Option | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------- | +| proxyPort | number | port the agent will use to listen for requests in the target pods ( default `8000`) | + +## Example + + + +```javascript +const fault = { + averageDelay: '50ms', + statusCode: 13, + errorRate: 0.1, +}; +disruptor.injectGrpcFaults(fault, '30s'); +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/injecthttpfaults.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/injecthttpfaults.md new file mode 100644 index 000000000..c81b4f935 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/injecthttpfaults.md @@ -0,0 +1,46 @@ +--- +title: 'injectHTTPFaults()' +description: 'xk6-disruptor: PodDisruptor.injectHTTPFaults method' +weight: 300 +--- + +# injectHTTPFaults() + +injectHTTPFaults injects HTTP faults in the requests served by a target Pod. + +| Parameter | Type | Description | +| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| fault | object | description of the [http faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/http) to be injected | +| duration | string | duration of the disruption | +| options (optional) | object | [options](#options) that control the injection of the fault | + +## options + +The injection of the fault is controlled by the following options: + +| Option | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------- | +| proxyPort | number | port the agent will use to listen for requests in the target pods ( default `8000`) | + +{{% admonition type="note" %}} + +When injecting faults you may find the following error message during the test execution: + +WARN\[0035\] Request Failed error="read tcp 172.18.0.1:43564->172.18.255.200:80: read: connection reset by peer" + +This is normal and means that one request was "in transit" at the time the faults were injected, causing the request to fail from a network connection reset. + +{{% /admonition %}} + +## Example + + + +```javascript +const fault = { + averageDelay: '50ms', + errorCode: 500, + errorRate: 0.1, +}; +disruptor.injectHTTPFaults(fault, '30s'); +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/terminate-pods.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/terminate-pods.md new file mode 100644 index 000000000..aea02db3f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/poddisruptor/terminate-pods.md @@ -0,0 +1,24 @@ +--- +title: 'terminatePods()' +description: 'xk6-disruptor: PodDisruptor.terminatePods method' +weight: 400 +--- + +# terminatePods() + +`terminatePods` terminates a number of the pods matching the selector configured in the PodDisruptor. + +| Parameter | Type | Description | +| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| fault | object | description of the [Pod Termination fault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/pod-termination) | + +## Example + + + +```javascript +const fault = { + count: 2, +}; +disruptor.terminatePods(fault); +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/_index.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/_index.md new file mode 100644 index 000000000..7ac76ed87 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/_index.md @@ -0,0 +1,55 @@ +--- +title: 'ServiceDisruptor' +description: 'xk6-disruptor: ServiceDisruptor class' +weight: 300 +--- + +# ServiceDisruptor + +The `ServiceDisruptor` class can inject different types of faults into the pods that back a Kubernetes service. + +To construct a `ServiceDisruptor`, use the [ServiceDisruptor() constructor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor/constructor). + +## Methods + +| Method | Description | +| ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| [ServiceDisruptor.injectGrpcFaults()](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor/injectgrpcfaults) | Inject [gRPC faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/grpc) in the target Pods | +| [ServiceDisruptor.injectHTTPFaults()](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor/injecthttpfaults) | Inject [HTTTP faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/http) in the target Pods | +| ServiceDisruptor.targets() | Returns the list of target Pods of the ServiceDisruptor | +| [ServiceDisruptor.terminatePods()](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor/terminate-pods) | executes a [Pod Termination fault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/pod-termination) in the target Pods | + +## Example + +The following example: + +- Creates a disruptor for the `nginx` service +- Injects a delay of 100ms and a 10 percent of requests that return an http response code `500`. + +```javascript +import { ServiceDisruptor } from 'k6/x/disruptor'; + +const fault = { + averageDelay: '100ms', + errorRate: 0.1, + errorCode: 500, +}; + +export default function () { + const disruptor = new ServiceDisruptor('nginx', 'default'); + disruptor.injectHTTPFaults(fault, '30s'); +} +``` + +{{% admonition type="note" %}} + +You can test this script by creating first a pod running nginx and exposing it as a service with the commands below, assuming you have [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) installed in your environment: + +```bash +> kubectl run nginx --image=nginx +> kubectl expose pod nginx --port 80 +``` + +You can also use the [xk6-kubernetes](https://github.com/grafana/xk6-kubernetes) extension for creating these resources from your test script. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/constructor.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/constructor.md new file mode 100644 index 000000000..33cb90118 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/constructor.md @@ -0,0 +1,31 @@ +--- +title: 'Constructor' +descriptiontiontion: 'xk6-disruptor: ServiceDisruptor constructor' +weight: 100 +--- + +# Constructor + +The `ServiceDisruptor()` constructor creates a new instance of a [ServiceDisruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor) class. + +| Parameter | Type | Description | +| ------------------ | ------ | ----------------------------------------------------------------- | +| service | string | name of the service | +| namespace | string | namespace on which the service is defined | +| options (optional) | object | [options](#options) for controlling the behavior of the disruptor | + +### Options + +The following options control the creation and behavior of the `ServiceDisruptor`: + +| Attribute | Type | Description | +| ------------- | ------ | ----------------------------------------------------------------------------------- | +| injectTimeout | string | maximum time for waiting the disruptor to be ready in the target pods (default 30s) | + +## Example + + + +```javascript +const disruptor = new ServiceDisruptor('my-service', 'my-namespace'); +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/injectgrpcfaults.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/injectgrpcfaults.md new file mode 100644 index 000000000..45c69ac0f --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/injectgrpcfaults.md @@ -0,0 +1,36 @@ +--- +title: 'injectGrpcFaults' +description: 'xk6-disruptor: ServiceDisruptor.injectGrpcFaults method' +weight: 200 +--- + +# injectGrpcFaults + +injectGrpcFaults injects gRPC faults in the requests served by a target Service. + +| Parameters | Type | Description | +| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| fault | object | description of the [gRPC faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/grpc) to be injected | +| duration | string | duration of the disruption | +| options (optional) | object | [options](#options) that control the injection of the fault | + +## Options + +The injection of the fault is controlled by the following options: + +| Option | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------- | +| proxyPort | number | port the agent will use to listen for requests in the target pods ( default `8000`) | + +## Example + + + +```javascript +const fault = { + averageDelay: '50ms', + statusCode: 13, + errorRate: 0.1, +}; +disruptor.injectGrpcFaults(fault, '30s'); +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/injecthttpfaults.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/injecthttpfaults.md new file mode 100644 index 000000000..ca489c879 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/injecthttpfaults.md @@ -0,0 +1,46 @@ +--- +title: 'injectHTTPFaults' +description: 'xk6-disruptor: ServiceDisruptor.injectHTTPFaults method' +weight: 300 +--- + +# injectHTTPFaults + +injectHTTPFaults injects HTTP faults in the requests served by a target Service. + +| Parameters | Type | Description | +| ------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------- | +| fault | object | description of the [http faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/http) to be injected | +| duration | string | duration of the disruption | +| options (optional) | object | [options](#options) that control the injection of the fault | + +## Options + +The injection of the fault is controlled by the following options: + +| Option | Type | Description | +| --------- | ------ | ----------------------------------------------------------------------------------- | +| proxyPort | number | port the agent will use to listen for requests in the target pods ( default `8000`) | + +{{% admonition type="note" %}} + +When injecting faults you may find the following error message during the test execution: + +WARN\[0035\] Request Failed error="read tcp 172.18.0.1:43564->172.18.255.200:80: read: connection reset by peer". + +This is normal and means that one request was "in transit" at the time the faults were injected causing the request to fail due to a network connection reset. + +{{% /admonition %}} + +## Example + + + +```javascript +const fault = { + averageDelay: '50ms', + errorCode: 500, + errorRate: 0.1, +}; +disruptor.injectHTTPFaults(fault, '30s'); +``` diff --git a/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/terminate-pods.md b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/terminate-pods.md new file mode 100644 index 000000000..44ebf7643 --- /dev/null +++ b/docs/sources/v0.50.x/javascript-api/xk6-disruptor/servicedisruptor/terminate-pods.md @@ -0,0 +1,24 @@ +--- +title: 'terminatePods()' +description: 'xk6-disruptor: ServiceDisruptor.terminatePods method' +weight: 400 +--- + +# terminatePods() + +`terminatePods` terminates a number of pods that belong to the service specified in the ServiceDisruptor. + +| Parameter | Type | Description | +| --------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------- | +| fault | object | description of the [Pod Termination fault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/pod-termination) | + +## Example + + + +```javascript +const fault = { + count: 2, +}; +disruptor.terminatePods(fault); +``` diff --git a/docs/sources/v0.50.x/misc/_index.md b/docs/sources/v0.50.x/misc/_index.md new file mode 100644 index 000000000..214a3bc80 --- /dev/null +++ b/docs/sources/v0.50.x/misc/_index.md @@ -0,0 +1,10 @@ +--- +weight: 12 +title: Misc +--- + +# Misc + + + +{{< section >}} diff --git a/docs/sources/v0.50.x/misc/archive.md b/docs/sources/v0.50.x/misc/archive.md new file mode 100644 index 000000000..62117d57d --- /dev/null +++ b/docs/sources/v0.50.x/misc/archive.md @@ -0,0 +1,231 @@ +--- +title: 'Archive Command' +description: 'A k6 archive is simply a tar file with all files needed to execute a k6 test.' +weight: 05 +--- + +# Archive Command + +## What is an archive? + +When the complexity of a k6 test goes beyond a single JS file it quickly becomes cumbersome to +find and bundle up all the dependencies (JS, [open()](https://grafana.com/docs/k6//javascript-api/init-context/open)'ed data files, TLS +client certs, etc.). k6 archives are a native way to bundle and distribute, or share, a test. + +A k6 archive is simply a [tar](https://en.wikipedia.org/wiki/Tar_%28computing%29) file with all +files needed to execute a k6 test. + +## How to create and run an archive + +Let's say that you normally execute a test using: + +{{< code >}} + +```bash +$ k6 run script.js +``` + +{{< /code >}} + +Now if you replace `run` with `archive` k6 will run the [init stage](https://grafana.com/docs/k6//using-k6/test-lifecycle) of +the code to determine which JS files are being imported and what data files are being +[`open()`](https://grafana.com/docs/k6//javascript-api/init-context/open)'ed and bundles all of the files up +into a tar file: + +{{< code >}} + +```bash +$ k6 archive script.js +``` + +{{< /code >}} + +This would produce a tar file on disk called `archive.tar` (you can change that by setting +`-O filename.tar`). It's also easy to run an archive, as `k6 run` is compatible with archive +files you can execute: + +> ### ⚠️ Overriding options +> +> As always you can override options using CLI flags or environment variables when +> running an archive. + +{{< code >}} + +```bash +$ k6 run archive.tar +``` + +{{< /code >}} + +## Use cases + +Archive files have a variety of use cases, but they all share the common need to bundle +of a test's files into a single file for easy distribution. + +### Sharing a test + +By bundling up a test into an archive it's easy to share the test with your teammates by +simply storing or sending a single tar file. As we saw in the previous section, your teammates +can execute the archive by running `k6 run archive.tar`. + +### Preparing tests for CI + +If you have a complex CI pipeline and your load tests are separated from your application +code, you could store k6 archives as build artifacts whenever the load test source code +is changed, and then pull in those k6 archives from the artifacts storage for test execution +as needed. + +### k6 Cloud Execution + +k6 offers a commercial service for running large scale and geographically +distributed load tests on managed cloud infrastructure. Cloud executed tests are triggered +from the k6 command-line via the `k6 cloud script.js` command (similar to `k6 run`) which will +trigger an implicit creation of a k6 archive that is uploaded and distributed to k6 cloud +load generators for execution. + +### Distributed Execution + +[k6-operator](https://github.com/grafana/k6-operator#multi-file-tests) can distribute a k6 test across a Kubernetes cluster. + +When a k6 test has multiple files, you can use the archive functionality to bundle the k6 test in a single _archived_ file and pass this file to run the test. + +## Contents of an archive file + +An archive contains the original source of the JS code, any [`open()`](https://grafana.com/docs/k6//javascript-api/init-context/open)'ed +data files, [SSL/TLS client certificates](https://grafana.com/docs/k6//using-k6/protocols/ssl-tls/ssl-tls-client-certificates) as well as a +`metadata.json` with all the options (a cascading of the options set on the [CLI](https://grafana.com/docs/k6//using-k6/k6-options), +via [Environment variables](https://grafana.com/docs/k6//using-k6/k6-options) and [in-script options](https://grafana.com/docs/k6//using-k6/k6-options) +(`export let options = {...}`)). + +Let's create an archive from the following sample test. Here is the layout in the filesystem +of the files: + +{{< code >}} + +```bash +/home/johndoe/tests/api-test $ tree +. +├── utils +| └-- common.js +├── endpoints +| ├── login.js +| └-- search.js +├── node_modules +| └-- somelib +| └-- lib.js +├── data +| └-- users.json +└-- script.js +``` + +{{< /code >}} + +Now, if the current working directory is `/home/johndoe/tests/api-test/` and we run +`k6 archive script.js` we'd get a tar file called `archive.tar` (you can change the name of the +file using `-O filename.tar`). The contents of the archive file would look like something like +this: + +{{< code >}} + +```bash +├-- data +├-- files +| └-- home +| └-- nobody <-- the username has been anonymized (see section further down) +| └-- tests +| └-- api-test +| └-- data +| └-- users.json +├-- metadata.json +└-- scripts + └-- home + └-- nobody <-- the username has been anonymized (see section further down) + └-- tests + └-- api-test + ├-- script.js + ├-- utils + | └-- common.js + ├-- endpoints + | ├-- login.js + | └-- search.js + └-- node_modules + └-- somelib + └-- lib.js +``` + +{{< /code >}} + +Breaking down the file structure we get: + +**data** contains the source code of the main JS file (`script.js` in this example). + +**files** contains the full original directory tree of all [`open()`](https://grafana.com/docs/k6//javascript-api/init-context/open)'ed data files. + +**metadata.json** The resolved "default" options for this test based on [CLI flags](https://grafana.com/docs/k6//using-k6/k6-options), +[Environment variables](https://grafana.com/docs/k6//using-k6/k6-options) and [in-script options](https://grafana.com/docs/k6//using-k6/k6-options). + +**_scripts_** contains the full original directory tree of all `import`'ed JS dependencies. + +{{< code >}} + +```json +{ + "type": "js", + "options": { + "paused": null, + "vus": null, + "duration": null, + "iterations": null, + "stages": null, + "setupTimeout": null, + "teardownTimeout": null, + "rps": null, + "maxRedirects": null, + "userAgent": null, + "batch": null, + "batchPerHost": null, + "httpDebug": null, + "insecureSkipTLSVerify": null, + "tlsCipherSuites": null, + "tlsVersion": { + "min": "", + "max": "" + }, + "tlsAuth": null, + "throw": null, + "thresholds": null, + "blacklistIPs": null, + "hosts": null, + "noConnectionReuse": null, + "ext": null, + "summaryTrendStats": null, + "systemTags": [ + "url", + "name", + "check", + "error", + "tls_version", + "method", + "subproto", + "status", + "group", + "proto" + ], + "tags": null + }, + "filename": "/home/johndoe/tests/api-test/script.js", + "pwd": "/home/johndoe/tests/api-test/", + "env": {} +} +``` + +{{< /code >}} + +## What an archive file does not contain + +We try to be cautious with what we include in an archive file. Some things we do to that end: + +- We anonymize the username found in any path to JS and data file dependencies +- By default, k6 omits environment variables from the system in the archive. + However, if you use the flag `--include-system-env-vars`, k6 includes the environment variables in the archive (along with any variables passed with `--env`). + To avoid including system environment variables in the archive, you can use the k6 archive flag `--exclude-env-vars`. diff --git a/docs/sources/v0.50.x/misc/fine-tuning-os.md b/docs/sources/v0.50.x/misc/fine-tuning-os.md new file mode 100644 index 000000000..ddf63b8eb --- /dev/null +++ b/docs/sources/v0.50.x/misc/fine-tuning-os.md @@ -0,0 +1,346 @@ +--- +title: 'Fine-tuning OS' +description: 'In this article we will show you how to inspect the OS imposed limits of your system, tweak them and scale for larger tests.' +weight: 04 +--- + +# Fine-tuning OS + +When running large test scripts locally, users sometimes run into limits within their OS that prevent them from making the necessary number of requests to complete the test. +This limit usually manifests itself in a `Too Many Open Files` error. +These limits, if unchanged, can be a severe bottleneck if you choose to run a bigger or complicated test locally on your machine. + +This article shows you how to inspect the OS-imposed limits of your system, tweak them, and scale for larger tests. + +Important to note here is that everything that we cover in this article needs to be approached with a healthy dose of caution. +As with any changes to your OS, we discourage blindly changing your system settings to a specific value. You should document ways of testing that shows a clear before-and-after relation. +E.g. before changing MSL / TIME_WAIT period, confirm that you’re experiencing the issue (error messages, netstat, ss, etc.), change settings conservatively, re-run the test, and note any improvement. +This way you can gauge the effect of the optimization, find any negative side-effects, and come up with a range of recommended values. + +{{% admonition type="note" %}} + +The following modifications have been tested for macOS Sierra 10.12 and above. +If you're on an older version, the process for changing these settings might differ. + +{{% /admonition %}} + +## Network resource limit + +Unix operating system derivatives such as GNU/Linux, BSDs and macOS, can limit the amount of system resources available to a process to safeguard system stability. This includes the total amount of memory, CPU time, or amount of open files a single process is allowed to manage. + +Since in Unix everything is a file, including network connections, application testing tools that heavily use the network, such as k6, might reach the configured limit of allowed open files, depending on the amount of network connections used in a particular test. + +As mentioned in our opening section, this results in a message like the following being shown during a test: + +```bash +WARN[0127] Request Failed error="Get http://example.com/: dial tcp example.com: socket: too many open files" +``` + +This message means that the network resource limit has been reached, which will prevent k6 from creating new connections, thus altering the test result. In some cases this may be desired, to measure overall system performance, for example, but in most cases this will be a bottleneck towards testing the HTTP server and web application itself. + +The next sections describe ways to increase this resource limit, and allow k6 to run tests with hundreds or thousands of concurrent VUs from a single system. + +### Viewing limits configuration + +Unix systems have two types of resource limits: + +- **Hard limits.** The absolute maximum allowed for each user, and can be configured only by the root user. +- **Soft limits.** These can be configured by each user, but cannot be above the hard limit setting. + +### Linux + +On GNU/Linux, you can see the configured limits with the ulimit command. + +`ulimit -Sa` will show all soft limits for the current user: + +```bash +$ ulimit -Sa +core file size (blocks, -c) 0 +data seg size (kbytes, -d) unlimited +scheduling priority (-e) 0 +file size (blocks, -f) unlimited +pending signals (-i) 3736 +max locked memory (kbytes, -l) 16384 +max memory size (kbytes, -m) unlimited +open files (-n) 1024 +pipe size (512 bytes, -p) 8 +POSIX message queues (bytes, -q) 819200 +real-time priority (-r) 0 +stack size (kbytes, -s) 8192 +cpu time (seconds, -t) unlimited +max user processes (-u) 3736 +virtual memory (kbytes, -v) unlimited +file locks (-x) unlimited +``` + +While `ulimit -Ha` will show all hard limits for the current user: + +```bash +$ ulimit -Ha +core file size (blocks, -c) unlimited +data seg size (kbytes, -d) unlimited +scheduling priority (-e) 0 +file size (blocks, -f) unlimited +pending signals (-i) 3736 +max locked memory (kbytes, -l) 16384 +max memory size (kbytes, -m) unlimited +open files (-n) 1048576 +pipe size (512 bytes, -p) 8 +POSIX message queues (bytes, -q) 819200 +real-time priority (-r) 0 +stack size (kbytes, -s) unlimited +cpu time (seconds, -t) unlimited +max user processes (-u) 3736 +virtual memory (kbytes, -v) unlimited +file locks (-x) unlimited +``` + +Note the difference of open files being a maximum of 1024 for the soft limit, while it's 1048576 for the hard limit. + +### macOS + +In macOS however, you will have a couple of different system imposed limits to take into consideration. + +The first one is `launchctl limit maxfiles` which prints the per-process limits which are specified also as a soft limit and a hard limit. When a soft limit is exceeded a process may receive a signal (for example, if the CPU time or file size is exceeded), but it will be allowed to continue execution until it reaches the hard limit (or modifies its resource limit). +`kern.maxfiles` is the limit of total file descriptors on the entire system - the sum total of all the open files for all processes plus all the files the kernel has open for its own purposes. + +`sysctl kern.maxfiles` + +`sysctl kern.maxfilesperproc` + +So, to reiterate, running commands above will show you the system limits on open files and running processes. + +## Changing limits configuration + +The first thing you should consider before changing the configuration is the amount of network connections you expect your test to require. The http_reqs metric in the k6 result summary can hint at this, but a baseline calculation of number of max. VUs \* number of HTTP requests in a single VU iteration will deliver a fair approximation. Note that k6 also deals with text files and other resources that count towards the "open files" quota, but network connections are the biggest consumers. + +### Disabling limits in macOS + +Before we can change any system imposed limits in macOS we will need to disable a security feature put in place to prevent us in doing so. You will need to disable System Integrity Protection that was introduced in OS X El Capitan to prevent certain system-owned files and directories from being modified by processes without the proper privileges. + +To disable it you will need to restart your Mac and hold down `Command + R` while it boots. This will boot it into Recovery Mode. + +There you should navigate to `Utilities` which are located in the menu bar at the top of the screen, then open `Terminal`. Once you have it open, enter the following command: + +`csrutil disable` + +Once you press enter and close the Terminal, you can reboot your Mac normally and log into your account. + +### Changing soft limits + +#### Linux + +So, let's say that we want to run a 1000 VU test which makes 4 HTTP requests per iteration. In this case we could increase the open files limit to 5000, to account for additional non-network file usage. This can be done with the following command: + +```bash +$ ulimit -n 5000 +``` + +This changes the limit only for the current shell session. + +If we want to persist this change for future sessions, we can add this to a shell startup file. For Bash this would be: + +```bash +$ echo "ulimit -n 5000" >> ~/.bashrc +``` + +#### macOS + +If the soft limit is too low, set the current session to (values written here are usually close to default ones) : + +```bash +sudo launchctl limit maxfiles 65536 200000 +``` + +Since sudo is needed, you are prompted for a password. + +### Changing hard limits + +#### Linux + +If the above command results in an error like cannot modify limit: Operation not permitted or value exceeds hard limit, that means that the hard limit is too low, which as mentioned before, can only be changed by the root user. + +This can be done by modifying the `/etc/security/limits.conf` file. + +For example, to set both soft and hard limits of the amount of open files per process for the alice account, open `/etc/security/limits.conf` as root in your text editor of choice and add the following lines: + +```bash +alice soft nofile 5000 +alice hard nofile 1048576 +``` + +The new limits will be in place after logging out and back in. + +Alternatively, \* hard nofile 1048576 would apply the setting for all non-root user accounts, and root hard nofile 1048576 for the root user. See the documentation in that file or man bash for the ulimit command documentation. + +#### macOS + +Next step will be to configure your new file limits. Open terminal and paste the following command: + +```bash +sudo nano /Library/LaunchDaemons/limit.maxfiles.plist +``` + +This will open a text editor inside your terminal window where you will be prompted to provide your user password and then paste the following: + +```markup + + + + + Label + limit.maxfiles + ProgramArguments + + launchctl + limit + maxfiles + 64000 + 524288 + + RunAtLoad + + ServiceIPC + + + +``` + +Pressing `Control + X` will save the changes and exit the editor. By pasting and saving this we have introduced two different limitations to your maxfiles limit. The first one (64000) is a soft limit, which if reached, will prompt your Mac to prepare to stop allowing new file opens but still let them open. If the second one is reached (524288), a hard limit, you will again start seeing your old friend, the 'too many open files' error message. + +We will use the same procedure to increase the processes limit next. + +While in Terminal create a similar file with this command: + +```bash +sudo nano /Library/LaunchDaemons/limit.maxproc.plist +``` + +Again, after prompted for your password, you can paste the following and save and close with `Control + X` + +```markup + + + + + Label + limit.maxproc + ProgramArguments + + launchctl + limit + maxproc + 2048 + 4096 + + RunAtLoad + + ServiceIPC + + + +``` + +All that is left after this is to reboot your Mac back to the Recovery Mode, open the Terminal, turn the SIP back on with `csrutil enable` and check if the limits were changed with commands we used at the beginning. + +In most cases these limits should be enough to run most of your simple tests locally for some time, but you can modify the files above to any values you will need in your testing. + +{{% admonition type="warning" %}} + +Please be aware that all of these limitations are put in place to protect your operating system from files and applications that are poorly written and might leak memory like in huge quantities. We would suggest not going too overboard with the values, or you might find your system slowing down to a crawl if or when it runs out of RAM. + +{{% /admonition %}} + +## Local port range + +When creating an outgoing network connection the kernel allocates a local (source) port for the connection from a range of available ports. + +### GNU/Linux + +On GNU/Linux you can see this range with: + +```bash +$ sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_port_range = 32768 60999 +``` + +While 28,231 ports might be sufficient for most use cases, this might be a limiting factor if you’re testing with thousands of connections. You can increase it with, for example: + +```bash +sysctl -w net.ipv4.ip_local_port_range="16384 65000" +``` + +Be aware that this range applies to both TCP and UDP, so be conservative with the values you choose and increase as needed. + +To make the changes permanent, add `net.ipv4.ip_local_port_range=16384 65000` to `/etc/sysctl.conf`. +Last resort tweaks +If you still experience network issues with the above changes, consider enabling net.ipv4.tcp_tw_reuse: + +```bash +sysctl -w net.ipv4.tcp_tw_reuse=1 +``` + +This will enable a feature to quickly reuse connections in TIME_WAIT state, potentially yielding higher throughput. + +### macOS/Linux + +On macOS the default ephemeral port range is 49152 to 65535, for a total of 16384 ports. You can check this with the sysctl command: + +```bash +$ sysctl net.inet.ip.portrange.first net.inet.ip.portrange.last + +net.inet.ip.portrange.first: 49152 +net.inet.ip.portrange.last: 65535 +``` + +Once you run out of ephemeral ports, you will normally need to wait until the TIME_WAIT state expires (2 \* maximum segment lifetime) until you can reuse a particular port number. You can double the number of ports by changing the range to start at 32768, which is the default on Linux and Solaris. (The maximum port number is 65535 so you cannot increase the high end.) + +```bash +$ sudo sysctl -w net.inet.ip.portrange.first=32768 + +net.inet.ip.portrange.first: 49152 -> 32768 +``` + +Note that the official range designated by IANA is 49152 to 65535, and some firewalls may assume that dynamically assigned ports fall within that range. You may need to reconfigure your firewall to use a larger range outside of your local network. + +## General optimizations + +This section goes over some optimizations that are not necessarily dependant on your OS, but may affect your testing. + +### RAM usage + +Depending on the particular k6 test: maximum number of VUs used, number and size of JavaScript dependencies, and complexity of the test script itself, k6 can consume large amounts of system RAM during test execution. While the development is focused on reducing RAM usage as much as possible, a single test run might use tens of gigabytes of RAM under certain scenarios. + +As a baseline, count each VU instance to require between 1MB and 5MB of RAM, depending on your script complexity and dependencies. This is roughly between `GB and 5GB of required system RAM for a 1,000 VU test, so make sure that sufficient physical RAM is available to meet your test demands. + +If you need to decrease the RAM usage, you could use the option `--compatibility-mode=base`. Read more on [JavaScript Compatibility Mode](https://grafana.com/docs/k6//using-k6/javascript-compatibility-mode). + +### Virtual memory + +In addition to physical RAM, ensure that the system is configured with an appropriate amount of virtual memory, or swap space, in case higher memory usage bursts are required. + +You can see the status and amount of available swap space on your system with the commands swapon or free. + +We won't go into swap configuration details here, but you can find several guides online. + +### Network performance + +Because k6 can generate and sustain large amounts of network traffic, it also stresses the network stack of modern operating systems. Under certain loads or network conditions it's possible to achieve higher throughput and better performance by tweaking some network settings of the operating system or restructuring the network conditions of the test. + +### TCP TIME_WAIT period + +TCP network applications, such as web clients and servers, are assigned a network socket pair (a unique combination of local address, local port, remote address, and remote port) for each incoming or outgoing connection. Typically this socket pair is used for a single HTTP request/response session, and closed soon after. However, even after a connection is successfully closed by the application, the kernel might still reserve resources for quickly reopening the same socket if a new matching TCP segment arrives. This also occurs during network congestion where some packets get lost in transmission. This places the socket in a TIME_WAIT state, and is released once the TIME_WAIT period expires. This period is typically configured between 15 seconds and 2 minutes. + +The problem some applications like k6 might run into is causing a high number of connections to end up in the TIME_WAIT state, which can prevent new network connections being created. + +In these scenarios, before making changes to the system network configuration, which might have adverse side-effects for other applications, it's preferable to first take some common testing precautions. +Use different server ports or IPs + +Since sockets are uniquely created for a combination of local address, local port, remote address and remote port, a safe workaround for avoiding TIME_WAIT congestion is using different server ports or IP addresses. + +For example, you can configure your application to run on ports :8080, :8081, :8082, etc. and spread out your HTTP requests across these endpoints. + +## Read more + +- [Running large tests](https://grafana.com/docs/k6//testing-guides/running-large-tests) +- [JavaScript Compatibility Mode](https://grafana.com/docs/k6//using-k6/javascript-compatibility-mode) diff --git a/docs/sources/v0.50.x/misc/glossary.md b/docs/sources/v0.50.x/misc/glossary.md new file mode 100644 index 000000000..468972dee --- /dev/null +++ b/docs/sources/v0.50.x/misc/glossary.md @@ -0,0 +1,245 @@ +--- +title: Glossary +description: 'A list of technical terms commonly used when discussing k6, with definitions.' +weight: 07 +--- + +# Glossary + +What we talk about when we talk about k6. + +In discussion about k6, some terms have a precise, technical meaning. +If a certain term in these docs confuses you, consult this list for a definition. + + + +- [Application performance monitoring](#application-performance-monitoring) +- [Concurrent sessions](#concurrent-sessions) +- [Checks](#checks) +- [Custom resource](#custom-resource) +- [Data correlation](#data-correlation) +- [Data parameterization](#data-parameterization) +- [Dynamic data](#dynamic-data) +- [Endurance testing](#endurance-testing) +- [Environment variables](#environment-variables) +- [Execution segment](#execution-segment) +- [Goja](#goja) +- [Graceful stop](#graceful-stop) +- [Happy path](#happy-path) +- [HTTP archive](#http-archive) +- [Iteration](#iteration) +- [k6 Cloud](#k6-cloud) +- [k6 options](#k6-options) +- [Kubernetes](#kubernetes) +- [Load test](#load-test) +- [Load zone](#load-zone) +- [Lifecycle function](#lifecycle-function) +- [Metric](#metric) +- [Metric sample](#metric-sample) +- [Operator pattern](#operator-pattern) +- [Parallelism](#parallelism) +- [Reliability](#reliability) +- [Requests per second](#requests-per-second) +- [Saturation](#saturation) +- [Scenario](#scenario) +- [Scenario executor](#scenario-executor) +- [Smoke test](#smoke-test) +- [Soak test](#soak-test) +- [Stability](#stability) +- [Stress test](#stress-test) +- [System under test](#system-under-test) +- [Test run](#test-run) +- [Test concurrency](#test-concurrency) +- [Test duration](#test-duration) +- [Test script](#test-script) +- [Threshold](#threshold) +- [Throughput](#throughput) +- [Virtual user](#virtual-user) +- [YAML](#yaml) + + + + + +## Application performance monitoring + +_(Or APM)_. The practice of monitoring the performance, availability, and reliability of a system. + +## Concurrent sessions + +The number of simultaneous VU requests in a test run. + +## Checks + +Checks are true/false conditions that evaluate the content of some value in the JavaScript runtime.

[Checks reference](https://grafana.com/docs/k6//using-k6/checks) + +## Custom resource + +An extension to the [Kubernetes](#kubernetes) API.

[Kubernetes reference](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) + +## Data correlation + +The process of taking [dynamic data](#dynamic-data) received from the system under test and reusing the data in a subsequent request. +
+
+[Correlation and dynamic data example](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data),[Correlation in testing APIs](https://grafana.com/docs/k6//testing-guides/api-load-testing#correlation-and-data-parameterization) + +## Data parameterization + +The process of turning test values into reusable parameters, e.g. through variables and shared arrays.

[Data parameterization examples](https://grafana.com/docs/k6//examples/data-parameterization) + +## Dynamic data + +Data that might change or will change during test runs or across test runs. Common examples are order IDs, session tokens, or timestamps.

[Correlation and dynamic data example](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data) + +## Endurance testing + +A synonym for [soak testing](#soak-test). + +## Environment variables + +User-definable values which may be utilized by the operating system and other programs.

[Using environment variables](https://grafana.com/docs/k6//using-k6/environment-variables) + +## Execution segment + +A partition, or fractional portion, of an overall [test run](#test-run).

[The execution-segment options](https://grafana.com/docs/k6//using-k6/k6-options/reference#execution-segment) + +## Goja + +A JavaScript engine written in Go. k6 binaries are embedded with Goja, enabling test scripting in JavaScript.

[Goja repository](https://github.com/dop251/goja) + +## Graceful stop + +A period that lets VUs finish an iteration at the end of a load test. Graceful stops prevent abrupt halts in execution.

[Graceful stop reference](https://grafana.com/docs/k6//using-k6/scenarios/concepts/graceful-stop) + +## Happy path + +The default system behavior that happens when a known input produces an expected output. A common mistake in performance testing happens when scripts account only for the best case (in other words, the happy path). Most load tests try to discover system errors, so test scripts should include exception handling.

[Happy path (Wikipedia)](https://en.wikipedia.org/wiki/Happy_path) + +## HTTP archive + +_(Or HAR file)_. A file containing logs of browser interactions with the system under test. All included transactions are stored as JSON-formatted text. You can use these archives to generate test scripts (for example, with the har-to-k6 Converter).

[HAR 1.2 Specification](http://www.softwareishard.com/blog/har-12-spec/),[HAR converter](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-har-converter) + +## Iteration + +A single run in the execution of the `default function`, or scenario `exec` function. You can set iterations across all VUs, or per VU.

The [test life cycle](https://grafana.com/docs/k6//using-k6/test-lifecycle) document breaks down each stage of a k6 script, including iterations in VU code. + +## JSON + +An open standard, human-readable data-serialization format originally derived from JavaScript. + +## k6 Cloud + +The proper name for the entire cloud product, comprising both k6 Cloud Execution and k6 Cloud Test Results.

[k6 Cloud docs](https://grafana.com/docs/grafana-cloud/k6/) + +## k6 options + +Values that configure a k6 test run. You can set options with command-line flags, environment variables, and in the script.

[k6 Options](https://grafana.com/docs/k6//using-k6/k6-options) + +## Kubernetes + +An open-source system for automating the deployment, scaling, and management of containerized applications.

[Kubernetes website](https://kubernetes.io/) + +## Load test + +A test that assesses the performance of the system under test in terms of concurrent users or requests per second.

[Load Testing](https://grafana.com/docs/k6//testing-guides/test-types/load-testing) + +## Load zone + +The geographical instance from which a test runs.

[Private load zones](https://grafana.com/docs/grafana-cloud/k6/author-run/private-load-zone-v2/),[Declare load zones from the CLI](https://grafana.com/docs/grafana-cloud/k6/author-run/cloud-scripting-extras/load-zones/) + +## Lifecycle function + +A function called in a specific sequence in the k6 runtime. The most important lifecycle function is the default function, which runs the VU code.

[Test lifecycle](https://grafana.com/docs/k6//using-k6/test-lifecycle) + +## Metric + +A measure of how the system performs during a test run. `http_req_duration` is an example of a built-in k6 metric. Besides built-ins, you can also create custom metrics.

[Metrics](https://grafana.com/docs/k6//using-k6/metrics) + +## Metric sample + +A single value for a metric in a test run. For example, the value of `http_req_duration` from a single VU request. + +## Operator pattern + +Extends [Kubernetes](#kubernetes), enabling cluster management of [custom resources](#custom-resource).

[Kubernetes reference](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) + +## Parallelism + +The simultaneous execution of multiple tasks by dividing a problem into smaller independent parts. + +## Reliability + +The probability that a system under test performs as intended. + +## Requests per second + +The rate at which a test sends requests to the system under test. + +## Saturation + +A condition when a system's reaches full resource utilization and can handle no additional request. + +## Scenario + +An object in a test script that makes in-depth configurations to how VUs and iterations are scheduled. With scenarios, your test runs can model diverse traffic patterns.

[Scenarios reference](https://grafana.com/docs/k6//using-k6/scenarios) + +## Scenario executor + +An property of a [scenario](#scenario) that configures VU behavior. + +You can use executors to configure whether to designate iterations as shared between VUs or to run per VU, or to configure or whether the VU concurrency is constant or changing.

[Executor reference](https://grafana.com/docs/k6//using-k6/scenarios/executors) + +## Smoke test + +A regular load test configured for minimum load. Smoke tests verify that the script has no errors and that the system under test can handle a minimal amount of load.

[Smoke Testing](https://grafana.com/docs/k6//testing-guides/test-types/smoke-testing) + +## Soak test + +A test that tries to uncover performance and reliability issues stemming from a system being under pressure for an extended duration.

[Soak Testing](https://grafana.com/docs/k6//testing-guides/test-types/soak-testing) + +## Stability + +A system under test’s ability to withstand failures and errors. + +## Stress test + +A test that assess the availability and stability of the system under heavy load.

[Stress Testing](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing) + +## System under test + +The software that the load test tests. This could be an API, a website, infrastructure, or any combination of these. + +## Test run + +An individual execution of a test script over all configured iterations.

[Running k6](https://grafana.com/docs/k6//get-started/running-k6) + +## Test concurrency + +In k6 Cloud, the number of tests running at the same time. + +## Test duration + +The length of time that a test runs. When duration is set as an option, VU code runs for as many iterations as possible in the length of time specified.

[Duration option reference](https://grafana.com/docs/k6//using-k6/k6-options/reference#duration) + +## Test script + +The actual code that defines how the test behaves and what requests it makes, along with all (or at least most) configuration needed to run the test.

[Single Request example](https://grafana.com/docs/k6//examples/single-request). + +## Threshold + +A pass/fail criteria that evaluates whether a metric reaches a certain value. Testers often use thresholds to codify SLOs.

[Threshold reference](https://grafana.com/docs/k6//using-k6/thresholds) + +## Throughput + +The rate of successful message delivery. In k6, throughput is measured in requests per second. + +## Virtual user + +_(Or VU)_. The simulated users that run separate and concurrent iterations of your test script.

[The VU option](https://grafana.com/docs/k6//using-k6/k6-options/reference#vus) + +## YAML + +Rhymes with "camel," provides a human-readable data-serialization format commonly used for configuration files. + +
diff --git a/docs/sources/v0.50.x/misc/integrations.md b/docs/sources/v0.50.x/misc/integrations.md new file mode 100644 index 000000000..747e97ee8 --- /dev/null +++ b/docs/sources/v0.50.x/misc/integrations.md @@ -0,0 +1,83 @@ +--- +title: Integrations & Tools +weight: 01 +--- + +# Integrations & Tools + +## Test authoring + +Codeless tools to speed up the test creation. + +- [Test Builder](https://grafana.com/docs/k6//using-k6/test-authoring/test-builder) - Inspired by the Postman API Builder. Codeless UI tool to generate a k6 test quickly. +- [Browser Recorder](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder) - Record a user journey to create your k6 test. + +## IDE extensions + +Code k6 scripts in your IDE of choice. Empower your development workflow with IDE extensions. + +- [Visual Studio Code Extension](https://marketplace.visualstudio.com/items?itemName=k6.k6) - Run k6 tests from VS Code. +- [IntelliJ IDEA](https://plugins.jetbrains.com/plugin/16141-k6) - Run k6 tests from IntelliJ IDEs. +- [IntelliSense](https://grafana.com/docs/k6//misc/intellisense) - Get code autocompletion and in-context documentation. + +## Converters + +Generate a k6 script quickly from an existing file. + +- [HAR-to-k6](https://github.com/k6io/har-to-k6) - Convert a HAR file to a k6 script. +- [Postman-to-k6](https://github.com/apideck-libraries/postman-to-k6) - Convert a Postman collection to a k6 script. +- [OpenAPI generator](https://k6.io/blog/load-testing-your-api-with-swagger-openapi-and-k6) - Convert a Swagger/OpenAPI specification to a k6 script. + +## Result store and visualization + +k6 can output test results to various formats and 3rd-party services. + +- [Amazon CloudWatch](https://grafana.com/docs/k6//results-output/real-time/amazon-cloudwatch) +- [Apache Kafka](https://grafana.com/docs/k6//results-output/real-time/apache-kafka) +- [CSV](https://grafana.com/docs/k6//results-output/real-time/csv) +- [Datadog](https://grafana.com/docs/k6//results-output/real-time/datadog) +- [Dynatrace](https://grafana.com/docs/k6//results-output/real-time/dynatrace) +- [Elasticsearch](https://grafana.com/docs/k6//results-output/real-time/elasticsearch) +- [Grafana Cloud k6](https://grafana.com/docs/k6//results-output/real-time/cloud) +- [Grafana Cloud Prometheus](https://grafana.com/docs/k6//results-output/real-time/grafana-cloud-prometheus) +- [Grafana Dashboards](https://grafana.com/docs/k6//results-output/grafana-dashboards) +- [InfluxDB](https://grafana.com/docs/k6//results-output/real-time/influxdb) +- [JSON file](https://grafana.com/docs/k6//results-output/real-time/json) +- [Netdata](https://grafana.com/docs/k6//results-output/real-time/netdata) +- [New Relic](https://grafana.com/docs/k6//results-output/real-time/new-relic) +- [Prometheus](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write) +- [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd) +- [TimescaleDB](https://grafana.com/docs/k6//results-output/real-time/timescaledb) + +## Continuous Integration and Continuous Delivery + +By automating load testing with your CI / CD tools, you can quickly see when a code change has introduced a performance regression. + +- [AWS CodeBuild](https://k6.io/blog/integrating-k6-with-aws-codebuild/) +- [Azure Pipelines](https://k6.io/blog/integrating-load-testing-with-azure-pipelines/) +- [Bamboo](https://k6.io/blog/integrating-k6-with-bamboo/) +- [Buddy](https://k6.io/blog/integrating-k6-with-buddy-devops/) +- [CircleCI](https://k6.io/blog/integrating-load-testing-with-circleci/) +- [Flagger](https://docs.flagger.app/usage/webhooks#k6-load-tester) +- [GitHub Actions](https://k6.io/blog/load-testing-using-github-actions/) +- [GitLab](https://k6.io/blog/integrating-load-testing-with-gitlab/) +- [Google Cloud Build](https://k6.io/blog/integrating-k6-with-google-cloud-build/) +- [Jenkins](https://k6.io/blog/integrating-load-testing-with-jenkins/) +- [Keptn](https://k6.io/blog/performance-testing-in-keptn-using-k6/) +- [TeamCity](https://k6.io/blog/load-testing-using-teamcity-and-k6/) + +## Chaos engineering + +- [Steadybit](https://k6.io/blog/chaos-engineering-with-k6-and-steadybit) - Using k6 and Steadybit for chaos engineering. +- [xk6-disruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor) - A k6 extension for injecting faults into k6 tests. +- [ChaosToolkit](http://chaostoolkit.org/drivers/k6/) - A collection of k6 actions and probes. + +## Test management + +- [Azure Test Plan](https://medium.com/microsoftazure/load-testing-with-azure-devops-and-k6-839be039b68a) - k6 load testing with Azure DevOps. +- [TestRail](https://dev.to/kwidera/introducing-testrail-in-you-k6-tests-eck) - Introducing TestRail in your k6 tests. +- [Testkube](https://kubeshop.github.io/testkube/test-types/executor-k6) - Load testing with Testkube. +- [Tracetest](https://docs.tracetest.io/tools-and-integrations/integrations/k6) - Trace-based testing with Tracetest. +- [Xray](https://docs.getxray.app/display/XRAYCLOUD/Performance+and+load+testing+with+k6) - Using Xray to validate test results in JIRA. + +[Reach out to us](mailto:support@k6.io) if you have a tool or add-on that works well with k6, and we will list it here! diff --git a/docs/sources/v0.50.x/misc/intellisense.md b/docs/sources/v0.50.x/misc/intellisense.md new file mode 100644 index 000000000..06afc3ca2 --- /dev/null +++ b/docs/sources/v0.50.x/misc/intellisense.md @@ -0,0 +1,35 @@ +--- +title: 'IntelliSense' +description: 'k6 has its TypeScript Type Definition that you can configure with your editor to unlock code editing features.' +weight: 03 +--- + +# IntelliSense + +[IntelliSense](https://code.visualstudio.com/docs/editor/intellisense) refers to code editing features like **intelligent code completion** and **quick access to documentation**. These features can significantly improve the developer experience and productivity when working on k6 scripts in your editor of choice. Notable features are: + +- Auto-completion of k6 functions, methods, and classes. +- Auto imports of k6 modules. +- Access to k6 documentation when writing and hovering code. + +![Intellisense enabled in a code editor, showing autocompletion of k6 libraries](/media/docs/k6-oss/intellisense-k6-demo.gif) + +## VS Code & IntelliJ + +k6 has its [TypeScript Type Definition](https://www.npmjs.com/package/@types/k6) that you can configure with your editor to unlock code editing features. + +In Visual Studio Code and IntelliJ IDEA Ultimate, you can configure IntelliSense to recognize the [k6 JavaScript API](https://grafana.com/docs/k6//javascript-api) by installing the k6 Types with a package manager. + +```bash +# create a `package.json` file +$ npm init --yes + +# install the k6 types as dev dependency +$ npm install --save-dev @types/k6 +``` + +## Read more + +- [Visual Studio Code - k6 Extension](https://marketplace.visualstudio.com/items?itemName=k6.k6) +- [IntelliJ IDEA - k6 Plugin](https://plugins.jetbrains.com/plugin/16141-k6) +- [TypeScript Editor Support](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support) diff --git a/docs/sources/v0.50.x/misc/k6-rest-api.md b/docs/sources/v0.50.x/misc/k6-rest-api.md new file mode 100644 index 000000000..7575bf43a --- /dev/null +++ b/docs/sources/v0.50.x/misc/k6-rest-api.md @@ -0,0 +1,612 @@ +--- +title: 'k6 REST API' +description: 'With this API you can see and control different execution aspects like +number of VUs, pause or resume the test, list groups, set and get the +setup data and more.' +_build: + list: false +weight: 04 +--- + +# k6 REST API + +When k6 starts, it spins up an HTTP server with a REST API that can be used to control some +parameters of the test execution. By default, that server listens on `localhost:6565`, but +that can be modified by the `--address` CLI flag. + +With this API you can see and control different execution aspects like number of VUs, Max +VUs, pause or resume the test, list groups, set and get the setup data and so on. + +You can also find practical usage examples in +[this blog post](https://k6.io/blog/how-to-control-a-live-k6-test). + +## Get Status + +**GET** `http://localhost:6565/v1/status` + +{{< code >}} + +```bash +curl -X GET \ + http://localhost:6565/v1/status \ + -H 'Content-Type: application/json' +``` + +```json +{ + "data": { + "attributes": { + "paused": false, + "running": true, + "tainted": false, + "vus": 1 + }, + "id": "default", + "type": "status" + } +} +``` + +{{< /code >}} + +## Update Status + +**PATCH** `http://localhost:6565/v1/status` + +{{< code >}} + +```bash +curl -X PATCH \ + http://localhost:6565/v1/status \ + -H 'Content-Type: application/json' \ + -d '{ + "data": { + "attributes": { + "paused": true, + "vus": 1, + }, + "id": "default", + "type": "status" + } +}' +``` + +```json +{ + "data": { + "type": "status", + "id": "default", + "attributes": { + "paused": true, + "vus": 1, + "running": true, + "tainted": false + } + } +} +``` + +{{< /code >}} + +This endpoint lets you pause/resume a running test and set the number of `vus` and `vus-max` +during the test. + +## List Metrics + +**GET** `http://localhost:6565/v1/metrics` + +{{< code >}} + +```bash +curl -X GET \ + http://localhost:6565/v1/metrics \ + -H 'Content-Type: application/json' +``` + +```json +{ + "data": [ + { + "type": "metrics", + "id": "http_req_duration", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 122.529465, + "max": 179.098624, + "med": 115.83006, + "min": 107.743524, + "p(90)": 136.9331272, + "p(95)": 158.01587559999996 + } + } + }, + { + "type": "metrics", + "id": "http_req_connecting", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 11.2357072, + "max": 112.357072, + "med": 0, + "min": 0, + "p(90)": 11.235707199999961, + "p(95)": 61.796389599999884 + } + } + }, + { + "type": "metrics", + "id": "http_req_sending", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 0.027994200000000004, + "max": 0.106594, + "med": 0.0192965, + "min": 0.017486, + "p(90)": 0.03165189999999997, + "p(95)": 0.0691229499999999 + } + } + }, + { + "type": "metrics", + "id": "http_req_waiting", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 122.33937080000001, + "max": 179.021285, + "med": 115.74006299999999, + "min": 107.650352, + "p(90)": 136.8561833, + "p(95)": 157.93873414999996 + } + } + }, + { + "type": "metrics", + "id": "data_received", + "attributes": { + "type": "counter", + "contains": "data", + "tainted": null, + "sample": { + "count": 13830, + "rate": 1119.9222882571698 + } + } + }, + { + "type": "metrics", + "id": "http_req_blocked", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 11.364957999999998, + "max": 113.611988, + "med": 0.004173, + "min": 0.003867, + "p(90)": 11.365557499999959, + "p(95)": 62.48877274999988 + } + } + }, + { + "type": "metrics", + "id": "http_req_receiving", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 0.16209999999999997, + "max": 0.757392, + "med": 0.078622, + "min": 0.057306, + "p(90)": 0.2315264999999998, + "p(95)": 0.4944592499999994 + } + } + }, + { + "type": "metrics", + "id": "http_reqs", + "attributes": { + "type": "counter", + "contains": "default", + "tainted": null, + "sample": { + "count": 10, + "rate": 0.8097775041628127 + } + } + }, + { + "type": "metrics", + "id": "http_req_tls_handshaking", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 0, + "max": 0, + "med": 0, + "min": 0, + "p(90)": 0, + "p(95)": 0 + } + } + }, + { + "type": "metrics", + "id": "data_sent", + "attributes": { + "type": "counter", + "contains": "data", + "tainted": null, + "sample": { + "count": 860, + "rate": 69.64086535800189 + } + } + }, + { + "type": "metrics", + "id": "iteration_duration", + "attributes": { + "type": "trend", + "contains": "time", + "tainted": null, + "sample": { + "avg": 1134.89821, + "max": 1238.377413, + "med": 1118.223518, + "min": 1108.405498, + "p(90)": 1185.348477, + "p(95)": 1211.8629449999999 + } + } + }, + { + "type": "metrics", + "id": "iterations", + "attributes": { + "type": "counter", + "contains": "default", + "tainted": null, + "sample": { + "count": 10, + "rate": 0.8097775041628127 + } + } + }, + { + "type": "metrics", + "id": "vus", + "attributes": { + "type": "gauge", + "contains": "default", + "tainted": null, + "sample": { + "value": 1 + } + } + } + ] +} +``` + +{{< /code >}} + +This endpoint will give you all the metrics in the current time. You can see more details on all +metrics available and how to create new ones in [Metrics](https://grafana.com/docs/k6//using-k6/metrics). + +## Get Metric + +**GET** `http://localhost:6565/v1/metrics/id` + +{{< code >}} + +```bash +curl -X GET \ + http://localhost:6565/v1/metrics/http_req_receiving \ + -H 'Content-Type: application/json' +``` + +```json +{ + "data": { + "attributes": { + "contains": "time", + "sample": { + "avg": 0.12641856097560983, + "max": 1.1397, + "med": 0.074412, + "min": 0.057858, + "p(90)": 0.208553, + "p(95)": 0.218015 + }, + "tainted": null, + "type": "trend" + }, + "id": "http_req_receiving", + "type": "metrics" + } +} +``` + +{{< /code >}} + +This endpoint will give you details for the given metric in the current time. + +You can see more on all metrics available and how to create new ones in [Metrics](https://grafana.com/docs/k6//using-k6/metrics). + +## List Groups + +**GET** `http://localhost:6565/v1/groups` + +{{< code >}} + +```bash +curl -X GET \ + http://localhost:6565/v1/groups \ + -H 'Content-Type: application/json' +``` + +```json +{ + "data": [ + { + "type": "groups", + "id": "d41d8cd98f00b204e9800998ecf8427e", + "attributes": { + "path": "", + "name": "", + "checks": null + }, + "relationships": { + "groups": { + "data": [ + { + "type": "groups", + "id": "b0470a9324a4ae563b04e9ac49fbc9cf" + } + ] + }, + "parent": { + "data": null + } + } + }, + { + "type": "groups", + "id": "b0470a9324a4ae563b04e9ac49fbc9cf", + "attributes": { + "path": "::visit homepage", + "name": "visit homepage", + "checks": null + }, + "relationships": { + "groups": { + "data": [] + }, + "parent": { + "data": { + "type": "groups", + "id": "d41d8cd98f00b204e9800998ecf8427e" + } + } + } + } + ] +} +``` + +{{< /code >}} + +This endpoint returns all groups available on the test. + +For more details on how to create groups please go to [Tags and Groups](https://grafana.com/docs/k6//using-k6/tags-and-groups). + +## Get Group + +**GET** `http://localhost:6565/v1/groups/id` + +{{< code >}} + +```bash +curl -X GET \ + http://localhost:6565/v1/groups/b0470a9324a4ae563b04e9ac49fbc9cf \ + -H 'Content-Type: application/json' +``` + +```json +{ + "data": { + "type": "groups", + "id": "b0470a9324a4ae563b04e9ac49fbc9cf", + "attributes": { + "path": "::visit homepage", + "name": "visit homepage", + "checks": null + }, + "relationships": { + "groups": { + "data": [] + }, + "parent": { + "data": { + "type": "groups", + "id": "d41d8cd98f00b204e9800998ecf8427e" + } + } + } + } +} +``` + +{{< /code >}} + +This endpoint returns the Group with the given ID. + +For more details on how to create groups, please go to [Tags and Groups](https://grafana.com/docs/k6//using-k6/tags-and-groups). + +## Get Setup Data + +**GET** `http://localhost:6565/v1/setup` + +{{< code >}} + +```bash +curl -X GET \ + http://localhost:6565/v1/setup \ + -H 'Content-Type: application/json' +``` + +```json +{ + "data": { + "type": "setupData", + "id": "default", + "attributes": { + "data": { + "a": 1 + } + } + } +} +``` + +{{< /code >}} + +This endpoint returns the current JSON-encoded setup data. + +For more detail about the setup stage please go to [Test life cycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +## Run Setup + +**PUT** `http://localhost:6565/v1/setup` + +{{< code >}} + +```bash +curl -X POST \ + http://localhost:6565/v1/setup \ + -H 'Content-Type: application/json' +``` + +```json +{ + "data": { + "type": "setupData", + "id": "default", + "attributes": { + "data": { + "a": 1 + } + } + } +} +``` + +{{< /code >}} + +This endpoint executes the Setup stage and returns the result. + +For more detail about the setup stage please go to [Test life cycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +## Update Setup + +**PUT** `http://localhost:6565/v1/setup` + +{{< code >}} + +```bash +curl -X PUT \ + http://localhost:6565/v1/setup \ + -H 'Content-Type: application/json' \ + -d '{ + "data": { + "attributes": { + "data": { + "a": 1, + "b": 2 + } + }, + "id": "default", + "type": "setupData" + } +}' +``` + +```json +{ + "data": { + "type": "setupData", + "id": "default", + "attributes": { + "data": { + "a": 1, + "b": 2 + } + } + } +} +``` + +{{< /code >}} + +This endpoint parses the JSON request body and sets the result as Setup data. + +For more detail about the setup stage please go to [Test life cycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +## Stop Test + +**PATCH** `http://localhost:6565/v1/status` + +{{< code >}} + +```bash +curl -X PATCH \ + http://localhost:6565/v1/status \ + -H 'Content-Type: application/json' \ + -d '{ + "data": { + "type": "status", + "id": "default", + "attributes": { + "stopped": true + } + } +}' +``` + +```json +{ + "data": { + "type": "status", + "id": "default", + "attributes": { + "stopped": true + } + } +} +``` + +{{< /code >}} + +This call parses the JSON request body to update the status and stop a running test. diff --git a/docs/sources/v0.50.x/misc/release-notes.md b/docs/sources/v0.50.x/misc/release-notes.md new file mode 100644 index 000000000..e27d9eac8 --- /dev/null +++ b/docs/sources/v0.50.x/misc/release-notes.md @@ -0,0 +1,9 @@ +--- +title: Release notes +redirect: 'https://github.com/grafana/k6/releases' +weight: 06 +--- + +# Release notes + +You can find the k6 release notes in the k6 GitHub repository on the [Releases page](https://github.com/grafana/k6/releases), and the "[release notes](https://github.com/grafana/k6/tree/master/release%20notes)" folder. diff --git a/docs/sources/v0.50.x/misc/usage-collection.md b/docs/sources/v0.50.x/misc/usage-collection.md new file mode 100644 index 000000000..6a714ce8b --- /dev/null +++ b/docs/sources/v0.50.x/misc/usage-collection.md @@ -0,0 +1,29 @@ +--- +title: 'Usage collection' +description: 'By default, k6 sends a usage report each time it is run, so that we can track how often people use it. This report can be turned off by setting an environment variable or option.' +weight: 02 +--- + +# Usage collection + +By default, k6 sends an anonymous usage report each time it is run, so that we can track relevant information to be able to build the product making better data-driven decisions. Prioritizing the features that benefit the most and reducing the impact of changes. + +The report can be turned off by setting the [no usage report](https://grafana.com/docs/k6//using-k6/k6-options/reference/#no-usage-report) option setting the environment variable `K6_NO_USAGE_REPORT` or by adding the flag `--no-usage-report` when executing k6. + +The usage report does not contain any information about what you are testing. The contents are the following: + +- The k6 version (string, e.g. "0.17.2") +- Max VUs configured (number) +- Test duration (number) +- Total stages duration (number) +- VU iterations configured (number) +- The running program's operating system target (`darwin`, `freebsd`, `linux`...) +- The running program's architecture target (386, amd64, arm, s390x...) +- The list of JavaScript imported modules (`k6/http`, `k6/experimental/webcrypto`, ...) +- The list of used outputs (`json`, `influxdb`, ...) + +> Only k6 built-in JavaScript modules and outputs are considered. Private modules and custom extensions are excluded. + +This report is sent to an HTTPS server that collects statistics on k6 usage. + +k6 is an open-source project and for those interested, the actual code that generates and sends the usage report can be directly reviewed [here](https://github.com/grafana/k6/blob/d031d2b65e9e28143742b4b109f383e6b103ab31/cmd/report.go). diff --git a/docs/sources/v0.50.x/results-output/_index.md b/docs/sources/v0.50.x/results-output/_index.md new file mode 100644 index 000000000..9851c214a --- /dev/null +++ b/docs/sources/v0.50.x/results-output/_index.md @@ -0,0 +1,26 @@ +--- +weight: 04 +title: Results output +descriptiontion: All the ways you can look at k6 results. While the test runs, after the test runs, on an external platform, as summary statistics. +--- + +# Results output + +k6 emits [metrics](https://grafana.com/docs/k6//using-k6/metrics) with timestamps at every point of the test. +You can output the metric results as either **aggregated statistics** or **individual data points**. + +- For a top-level test overview, use the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test). +- For granular output of all metrics (with timestamps), you [stream metrics in real time](https://grafana.com/docs/k6//results-output/real-time). + +If you stream your metrics, you can either write them to a file, like JSON, or stream them to a service, like InfluxDB. + +![A diagram of the two broad ways to handle results: aggregated and granular](/media/docs/k6-oss/k6-results-diagram.png) + +## Read more + +- [End of test summary](https://grafana.com/docs/k6//results-output/end-of-test) +- [Real time results](https://grafana.com/docs/k6//results-output/real-time) +- [Web dashboard](https://grafana.com/docs/k6//results-output/web-dashboard) +- [Ways to visualize k6 results](https://k6.io/blog/ways-to-visualize-k6-results/) +- [Build an output extension](https://grafana.com/docs/k6//extensions/create/output-extensions) +- [k6 data collection pipeline](https://grafana.com/blog/2023/08/10/understanding-grafana-k6-a-simple-guide-to-the-load-testing-tool/) diff --git a/docs/sources/v0.50.x/results-output/end-of-test/_index.md b/docs/sources/v0.50.x/results-output/end-of-test/_index.md new file mode 100644 index 000000000..214798e0d --- /dev/null +++ b/docs/sources/v0.50.x/results-output/end-of-test/_index.md @@ -0,0 +1,119 @@ +--- +title: End of test +description: When a test finishes, k6 prints a summary of results, with aggregated metrics and meta-data about the test. You can customize this, or configure the test to write granular metrics to a file. +weight: 100 +weight: 100 +--- + +# End of test + +When a test finishes, k6 prints a top-level overview of the aggregated results to `stdout`. + +```bash +checks.........................: 50.00% ✓ 45 ✗ 45 +data_received..................: 1.3 MB 31 kB/s +data_sent......................: 81 kB 2.0 kB/s +group_duration.................: avg=6.45s min=4.01s med=6.78s max=10.15s p(90)=9.29s p(95)=9.32s +http_req_blocked...............: avg=57.62ms min=7µs med=12.25µs max=1.35s p(90)=209.41ms p(95)=763.61ms +http_req_connecting............: avg=20.51ms min=0s med=0s max=1.1s p(90)=100.76ms p(95)=173.41ms +http_req_duration..............: avg=144.56ms min=104.11ms med=110.47ms max=1.14s p(90)=203.54ms p(95)=215.95ms + { expected_response:true }...: avg=144.56ms min=104.11ms med=110.47ms max=1.14s p(90)=203.54ms p(95)=215.95ms +http_req_failed................: 0.00% ✓ 0 ✗ 180 +http_req_receiving.............: avg=663.96µs min=128.46µs med=759.82µs max=1.66ms p(90)=1.3ms p(95)=1.46ms +http_req_sending...............: avg=88.01µs min=43.07µs med=78.03µs max=318.81µs p(90)=133.15µs p(95)=158.3µs +http_req_tls_handshaking.......: avg=29.25ms min=0s med=0s max=458.71ms p(90)=108.31ms p(95)=222.46ms +http_req_waiting...............: avg=143.8ms min=103.5ms med=109.5ms max=1.14s p(90)=203.19ms p(95)=215.56ms +http_reqs......................: 180 4.36938/s +iteration_duration.............: avg=12.91s min=12.53s med=12.77s max=14.35s p(90)=13.36s p(95)=13.37s +iterations.....................: 45 1.092345/s +vus............................: 1 min=1 max=19 +vus_max........................: 20 min=20 max=20 +``` + +
+ +Besides this default summary, k6 can output the results in other formats at the end of the test: + +| On this page | Result format | Read about... | +| ---------------------------------------------------------------------------------------------------- | ------------------------ | ------------------------------------------------------------- | +| [Custom summary](https://grafana.com/docs/k6//results-output/end-of-test/custom-summary) | Aggregated | Using the `handleSummary()` to make completely custom reports | +| [CSV](https://grafana.com/docs/k6//results-output/real-time/csv) | Time-stamped data points | Writing results as a CSV file, and the structure of the data | +| [JSON](https://grafana.com/docs/k6//results-output/real-time/json) | Time-stamped data points | Writing results as a JSON file, and the structure of the data | + +## The default summary + +The end-of-test summary reports details and aggregated statistics for the primary aspects of the test: + +- Summary statistics about each built-in and custom [metric](https://grafana.com/docs/k6//using-k6/metrics#built-in-metrics) (e.g. mean, median, p95, etc). +- A list of the test's [groups](https://grafana.com/docs/k6//using-k6/tags-and-groups#groups) and [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) +- The pass/fail results of the test's [thresholds](https://grafana.com/docs/k6//using-k6/thresholds) and [checks](https://grafana.com/docs/k6//using-k6/checks). + +{{< code >}} + +```bash +Ramp_Up ✓ [======================================] 00/20 VUs 30s + █ GET home - https://example.com/ + + ✓ status equals 200 + + █ Create resource - https://example.com/create + + ✗ status equals 201 + ↳ 0% — ✓ 0 / ✗ 45 + + checks.........................: 50.00% ✓ 45 ✗ 45 + data_received..................: 1.3 MB 31 kB/s + data_sent......................: 81 kB 2.0 kB/s + group_duration.................: avg=6.45s min=4.01s med=6.78s max=10.15s p(90)=9.29s p(95)=9.32s + http_req_blocked...............: avg=57.62ms min=7µs med=12.25µs max=1.35s p(90)=209.41ms p(95)=763.61ms + http_req_connecting............: avg=20.51ms min=0s med=0s max=1.1s p(90)=100.76ms p(95)=173.41ms + ✗ http_req_duration..............: avg=144.56ms min=104.11ms med=110.47ms max=1.14s p(90)=203.54ms p(95)=215.95ms + { expected_response:true }...: avg=144.56ms min=104.11ms med=110.47ms max=1.14s p(90)=203.54ms p(95)=215.95ms + http_req_failed................: 0.00% ✓ 0 ✗ 180 + http_req_receiving.............: avg=663.96µs min=128.46µs med=759.82µs max=1.66ms p(90)=1.3ms p(95)=1.46ms + http_req_sending...............: avg=88.01µs min=43.07µs med=78.03µs max=318.81µs p(90)=133.15µs p(95)=158.3µs + http_req_tls_handshaking.......: avg=29.25ms min=0s med=0s max=458.71ms p(90)=108.31ms p(95)=222.46ms + http_req_waiting...............: avg=143.8ms min=103.5ms med=109.5ms max=1.14s p(90)=203.19ms p(95)=215.56ms + http_reqs......................: 180 4.36938/s + iteration_duration.............: avg=12.91s min=12.53s med=12.77s max=14.35s p(90)=13.36s p(95)=13.37s + iterations.....................: 45 1.092345/s + vus............................: 1 min=1 max=19 + vus_max........................: 20 min=20 max=20 + +ERRO[0044] some thresholds have failed +``` + +{{< /code >}} + +Above's an example of a report that k6 generated after a test run. + +- It has a scenario, `Ramp_Up` +- The requests are split into two groups: + - `GET home`, which has a check that responses are `200` (all passed) + - `Create resource`, which has a check that responses are `201` (all failed) +- The test has one threshold, requiring that 95% of requests have a duration under 200ms (failed) + +## Summary options + +k6 provides some options to filter or silence summary output: + +- The [`--summary-trend-stats` option](https://grafana.com/docs/k6//using-k6/k6-options/reference#summary-trend-stats) defines which [Trend metric](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend) statistics to calculate and show. +- The [`--summary-time-unit` option](https://grafana.com/docs/k6//using-k6/k6-options/reference#summary-time-unit) forces k6 to use a fixed-time unit for all time values in the summary. +- The [`--no-summary` option](https://grafana.com/docs/k6//using-k6/k6-options/reference#no-summary) completely disables report generation, including `--summary-export` and `handleSummary()`. + +{{< collapse title="Summary export to a JSON file (Discouraged)" >}} + +### Summary export to a JSON file + +k6 also has the [`--summary-export=path/to/file.json` option](https://grafana.com/docs/k6//using-k6/k6-options/reference#summary-export), which exports some summary report data to a JSON file. + +The format of `--summary-export` is similar to the `data` parameter of the `handleSummary()` function. +Unfortunately, the `--summary-export` format is limited and has a few confusing peculiarities. +For example, groups and checks are unordered, +and threshold values are unintuitive: `true` indicates the threshold failed, and `false` that it succeeded. + +We couldn't change the `--summary-export` data format, because it would have broken backward compatibility in a feature that people depended on in CI pipelines. +But, the recommended approach to export to a JSON file is to use the `handleSummary()` function]. +The `--summary-export` option will likely be deprecated in the future. + +{{< /collapse >}} diff --git a/docs/sources/v0.50.x/results-output/end-of-test/custom-summary.md b/docs/sources/v0.50.x/results-output/end-of-test/custom-summary.md new file mode 100644 index 000000000..ac3f79a2c --- /dev/null +++ b/docs/sources/v0.50.x/results-output/end-of-test/custom-summary.md @@ -0,0 +1,446 @@ +--- +title: Custom summary +description: With handlesummary(), you can customize every part of your report. Change the content, redirect output, and more. +weight: 150 +--- + +# Custom summary + +With `handleSummary()`, you can completely customize your end-of-test summary. +In this document, read about: + +- How `handleSummary()` works +- How to customize the content and output location of your summary +- The data structure of the summary object + +{{% admonition type="note" %}} + +However, we plan to support the feature for k6 Cloud tests, too. +[Track progress in this issue](https://github.com/grafana/k6-cloud-feature-requests/issues/24). + +{{% /admonition %}} + +## About `handleSummary()` + +After your test runs, k6 aggregates your metrics into a JavaScript object. +The `handleSummary()` function takes this object as an argument (called `data` in all examples here). + +You can use `handleSummary()` to create a custom summary or return the default summary object. +To get an idea of what the data looks like, +run this script and open the output file, `summary.json`. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + http.get('https://test.k6.io'); +} + +export function handleSummary(data) { + return { + 'summary.json': JSON.stringify(data), //the default data object + }; +} +``` + +{{< /code >}} + +Fundamentally, `handleSummary()` is just a function that can access a data object. +As such, you can transform the summary data into any text format: JSON, HTML, console, XML, and so on. +You can pipe your custom summary to [standard output or standard error](https://en.wikipedia.org/wiki/Standard_streams), write it to a file, or send it to a remote server. + +k6 calls `handleSummary()` at the end of the [test lifecycle](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +## Use handleSummary() + +The following sections go over the `handleSummary()` syntax and provide some examples. + +To look up the structure of the summary object, refer to the reference section. + +### Syntax + +k6 expects `handleSummary()` to return a `{key1: value1, key2: value2, ...}` map that represents the summary metrics. + +The keys must be strings. +They determine where k6 displays or saves the content: + +- `stdout` for standard output +- `stderr` for standard error, +- any relative or absolute path to a file on the system (this operation overwrites existing files) + +The value of a key can have a type of either `string` or [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). + +You can return multiple summary outputs in a script. +As an example, this `return` statement sends a report to standard output and writes the `data` object to a JSON file. + +{{< code >}} + +``` + return { + 'stdout': textSummary(data, { indent: ' ', enableColors: true }), // Show the text summary to stdout... + 'other/path/to/summary.json': JSON.stringify(data), // and a JSON with all the details... + }; +``` + +{{< /code >}} + +### Example: Extract data properties + +This minimal `handleSummary()` extracts the `median` value for the `iteration_duration` metric and prints it to standard output: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + http.get('https://test.k6.io'); +} + +export function handleSummary(data) { + const med_latency = data.metrics.iteration_duration.values.med; + const latency_message = `The median latency was ${med_latency}\n`; + + return { + stdout: latency_message, + }; +} +``` + +{{< /code >}} + +### Example: Modify default output + +If `handleSummary()` is exported, k6 _does not_ print the default summary. +However, if you want to keep the default output, you could import `textSummary` from the [K6 JS utilities library](https://jslib.k6.io/). +For example, you could write a custom HTML report to a file, and use the `textSummary()` function to print the default report to the console. + +You can also use `textSummary()` to make minor modifications to the default end-of-test summary. +To do so: + +1. Modify the `data` object however you want. +1. In your `return` statement, pass the modified object as an argument to the `textSummary()` function. + +The `textSummary()` function comes with a few options: + +| Option | Description | +| ------------- | -------------------------------------- | +| `indent` | How to start the summary indentation | +| `enableColor` | Whether to print the summary in color. | + +For example, this `handleSummary()` modifies the default summary in the following ways: + +- It deletes the `http_req_duration{expected_response:true}` sub-metric. +- It deletes all metrics whose key starts with `iteration`. +- It begins each line with the `→` character. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; + +export default function () { + http.get('https://test.k6.io'); +} + +export function handleSummary(data) { + delete data.metrics['http_req_duration{expected_response:true}']; + + for (const key in data.metrics) { + if (key.startsWith('iteration')) delete data.metrics[key]; + } + + return { + stdout: textSummary(data, { indent: '→', enableColors: true }), + }; +} +``` + +{{< /code >}} + +In the collapsible, you can use the tabs to compare default and modified reports. + +{{< collapse title="Compare the default and modified reports" >}} + +To see the output of the preceding script, +select **Modified**. +For compactness, these outputs were limited with the `summaryTrendStats` option. + +{{< code >}} + +``` + data_received..................: 63 kB 42 kB/s + data_sent......................: 830 B 557 B/s + http_req_blocked...............: med=10.39µs count=5 p(99)=451.07ms p(99.99)=469.67ms + http_req_connecting............: med=0s count=5 p(99)=223.97ms p(99.99)=233.21ms + http_req_duration..............: med=202.26ms count=5 p(99)=225.81ms p(99.99)=226.71ms + { expected_response:true }...: med=202.26ms count=5 p(99)=225.81ms p(99.99)=226.71ms + http_req_failed................: 0.00% ✓ 0 ✗ 5 + http_req_receiving.............: med=278.27µs count=5 p(99)=377.64µs p(99.99)=381.29µs + http_req_sending...............: med=47.57µs count=5 p(99)=108.42µs p(99.99)=108.72µs + http_req_tls_handshaking.......: med=0s count=5 p(99)=204.42ms p(99.99)=212.86ms + http_req_waiting...............: med=201.77ms count=5 p(99)=225.6ms p(99.99)=226.5ms + http_reqs......................: 5 3.352646/s + iteration_duration.............: med=204.41ms count=5 p(99)=654.78ms p(99.99)=672.43ms + iterations.....................: 5 3.352646/s + vus............................: 1 min=1 max=1 + vus_max........................: 1 min=1 max=1 +``` + +``` +→ data_received..............: 63 kB 39 kB/s +→ data_sent..................: 830 B 507 B/s +→ http_req_blocked...........: med=10.98µs count=5 p(99)=485.16ms p(99.99)=505.18ms +→ http_req_connecting........: med=0s count=5 p(99)=245.05ms p(99.99)=255.15ms +→ http_req_duration..........: med=208.68ms count=5 p(99)=302.87ms p(99.99)=306.61ms +→ http_req_failed............: 0.00% ✓ 0 ✗ 5 +→ http_req_receiving.........: med=206.12µs count=5 p(99)=341.05µs p(99.99)=344.13µs +→ http_req_sending...........: med=47.8µs count=5 p(99)=166.94µs p(99.99)=170.92µs +→ http_req_tls_handshaking...: med=0s count=5 p(99)=207.2ms p(99.99)=215.74ms +→ http_req_waiting...........: med=208.47ms count=5 p(99)=302.49ms p(99.99)=306.23ms +→ http_reqs..................: 5 3.054928/s +→ vus........................: 1 min=1 max=1 +→ vus_max....................: 1 min=1 max=1 +``` + +{{< /code >}} + +{{< /collapse >}} + +### Example: Make custom file format + +This script imports a helper function to turn the summary into a JUnit XML. +The output is a short XML file that reports whether the test thresholds failed. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +// Use example functions to generate data +import { jUnit } from 'https://jslib.k6.io/k6-summary/0.0.2/index.js'; +import k6example from 'https://raw.githubusercontent.com/grafana/k6/master/examples/thresholds_readme_example.js'; + +export default k6example; +export const options = { + vus: 5, + iterations: 10, + thresholds: { + http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms + }, +}; + +export function handleSummary(data) { + console.log('Preparing the end-of-test summary...'); + + return { + 'junit.xml': jUnit(data), // Transform summary and save it as a JUnit XML... + }; +} +``` + +{{< /code >}} + +Output for a test that crosses a threshold looks something like this: + +```xml + + + + + +``` + +### Example: Send data to remote server + +You can also send the generated reports to a remote server (over any protocol that k6 supports). + +{{< code >}} + +```javascript +import http from 'k6/http'; + +// use example function to generate data +import k6example from 'https://raw.githubusercontent.com/grafana/k6/master/examples/thresholds_readme_example.js'; +export const options = { vus: 5, iterations: 10 }; + +export function handleSummary(data) { + console.log('Preparing the end-of-test summary...'); + + // Send the results to some remote server or trigger a hook + const resp = http.post('https://httpbin.test.k6.io/anything', JSON.stringify(data)); + if (resp.status != 200) { + console.error('Could not send summary, got status ' + resp.status); + } +} +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +The last examples use imported helper functions. +These functions might change, so keep an eye on [jslib.k6.io](https://jslib.k6.io/) for the latest. + +Of course, we always welcome [PRs to the jslib](https://github.com/grafana/jslib.k6.io), too! + +{{% /admonition %}} + +## Summary data reference + +Summary data includes information about your test run time and all built-in and custom metrics (including checks). + +All metrics are in a top-level `metrics` object. +In this object, each metric has an object whose key is the name of the metric. +For example, if your `handleSummary()` argument is called `data`, +the function can access the object about the `http_req_duration` metric at `data.metrics.http_req_duration`. + +### Metric schema + +The following table describes the schema for the metrics object. +The specific values depend on the [metric type](https://grafana.com/docs/k6//using-k6/metrics): + + + +| Property | Description | +| -------------------- | ------------------------------------------------------------------------------ | +| type | String that gives the metric type | +| contains | String that describes the data | +| values | Object with the summary metric values (properties differ for each metric type) | +| thresholds | Object with info about the thresholds for the metric (if applicable) | +| thresholds.{name} | Name of threshold (object) | +| thresholds.{name}.ok | Whether threshold was crossed (boolean) | + + + +{{% admonition type="note" %}} + +If you change the default trend metrics with the [`summaryTrendStats`](https://grafana.com/docs/k6//using-k6/k6-options/reference#summary-trend-stats) option, +the keys for the values of the trend will change accordingly. + +{{% /admonition %}} + +### Example summary JSON + +To see what the summary `data` looks like in your specific test run: + +1. Add this to your handleSummary() function: + + ``` + return { 'raw-data.json': JSON.stringify(data)};` + ``` + +1. Inspect the resulting `raw-data.json` file. + + The following is an abridged example of how it might look: + +{{< code >}} + +```json +{ + "root_group": { + "path": "", + "groups": [ + // Sub-groups of the root group... + ], + "checks": [ + { + "passes": 10, + "fails": 0, + "name": "check name", + "path": "::check name" + } + // More checks... + ], + "name": "" + }, + "options": { + // Some of the global options of the k6 test run, + // Currently only summaryTimeUnit and summaryTrendStats + }, + + "state": { + "testRunDurationMs": 30898.965069 + // And information about TTY checkers + }, + + "metrics": { + // A map with metric and sub-metric names as the keys and objects with + // details for the metric. These objects contain the following keys: + // - type: describes the metric type, e.g. counter, rate, gauge, trend + // - contains: what is the type of data, e.g. time, default, data + // - values: the specific metric values, depends on the metric type + // - thresholds: any thresholds defined for the metric or sub-metric + // + "http_reqs": { + "type": "counter", + "contains": "default", + "values": { + "count": 40, + "rate": 19.768856959496336 + } + }, + "vus": { + "type": "gauge", + "contains": "default", + "values": { + "value": 1, + "min": 1, + "max": 5 + } + }, + "http_req_duration": { + "type": "trend", + "contains": "time", + "values": { + // actual keys depend depend on summaryTrendStats + + "avg": 268.31137452500013, + "max": 846.198634, + "p(99.99)": 846.1969478817999 + // ... + }, + "thresholds": { + "p(95)<500": { + "ok": false + } + } + }, + "http_req_duration{staticAsset:yes}": { + // sub-metric from threshold + "contains": "time", + "values": { + // actual keys depend on summaryTrendStats + "min": 135.092841, + "avg": 283.67766343333335, + "max": 846.198634, + "p(99.99)": 846.1973802197999 + // ... + }, + "thresholds": { + "p(99)<250": { + "ok": false + } + }, + "type": "trend" + } + // ... + } +} +``` + +{{< /code >}} + +## Custom output examples + +These examples are community contributions. +We thank everyone who has shared! + +- [Using the JUnit output with Azure Test Plan](https://medium.com/microsoftazure/load-testing-with-azure-devops-and-k6-839be039b68a) +- [Using the JUnit output with TestRail](https://dev.to/kwidera/introducing-testrail-in-you-k6-tests-eck) +- [handleSummary and custom Slack integration](https://medium.com/geekculture/k6-custom-slack-integration-metrics-are-the-magic-of-tests-527aaf613595) +- [Reporting to Xray](https://docs.getxray.app/display/XRAYCLOUD/Performance+and+load+testing+with+k6) +- [HTML reporter](https://github.com/benc-uk/k6-reporter) diff --git a/docs/sources/v0.50.x/results-output/grafana-dashboards.md b/docs/sources/v0.50.x/results-output/grafana-dashboards.md new file mode 100644 index 000000000..80eb0730c --- /dev/null +++ b/docs/sources/v0.50.x/results-output/grafana-dashboards.md @@ -0,0 +1,35 @@ +--- +title: Grafana dashboards +description: With multiple k6 output formats, you also have multiple ways to visualize test results in a Grafana dashboard. +weight: 300 +--- + +# Grafana dashboards + +You have multiple ways to query k6 results in Grafana. +Having test results in a dashboard brings various benefits: + +- Visualize your results to analyze performance during the test run or over multiple test runs. +- Correlate test results with application and system metrics in the same dashboard to get a holistic overview of your system's performance and quickly find the root causes of performance issues. + +![A grafana dashboard correlating k6 results with observability data](/media/docs/k6-oss/correlated-grafana-dashboard-grafana-cloud-k6.png) + +## Options + +With [Grafana](https://grafana.com/grafana/), you can create a custom dashboard to query and **visualize data from multiple sources and any type of backend**. +Using k6, you can [stream your local test results to any backend](https://grafana.com/docs/k6//results-output/real-time). + +The flexibility and interoperability of Grafana and k6 let you visualize test and observability data in one dashboard, regardless of where the data is stored. +The following outputs include pre-built Grafana dashboards for their storage: + +| Output | Grafana Dashboard | +| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| [AWSTimestream](https://github.com/leonyork/xk6-output-timestream) | [leonyork/xk6-output-timestream](https://github.com/leonyork/xk6-output-timestream/tree/main/grafana/dashboards/) | +| [InfluxDB](https://grafana.com/docs/k6//results-output/real-time/influxdb) | [grafana/xk6-output-influxdb](https://github.com/grafana/xk6-output-influxdb/tree/main/grafana/dashboards) | +| [Prometheus remote write](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write) | [k6 Prometheus](https://grafana.com/grafana/dashboards/19665-k6-prometheus/) | +| [Prometheus remote write (Native Histograms)](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write) | [k6 Prometheus (Native Histograms)](https://grafana.com/grafana/dashboards/18030-k6-prometheus-native-histograms/) | +| [Grafana Cloud Prometheus](https://grafana.com/docs/k6//results-output/real-time/grafana-cloud-prometheus) | [k6 Prometheus](https://grafana.com/grafana/dashboards/19665-k6-prometheus/) | +| [TimescaleDB](https://grafana.com/docs/k6//results-output/real-time/timescaledb) | [grafana/xk6-output-timescaledb](https://github.com/grafana/xk6-output-timescaledb/tree/main/grafana/dashboards) | +| ---- | [More public dashboards from the community](https://grafana.com/grafana/dashboards/?search=k6) | + +For a fully managed solution, [Grafana Cloud k6](https://grafana.com/products/cloud/k6/) is our commercial product to store, view, scale, and manage your tests with ease. It provides custom views to access your testing and analyze test results, enhanced collaboration features, and many more additional capabilities. diff --git a/docs/sources/v0.50.x/results-output/real-time/_index.md b/docs/sources/v0.50.x/results-output/real-time/_index.md new file mode 100644 index 000000000..a634d9d43 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/_index.md @@ -0,0 +1,53 @@ +--- +title: Real time +descriptiontion: Send your time-series k6 metrics to multiple file formats and services +weight: 200 +--- + +# Real time + +Besides the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test), you can also view metrics as granular data points. +k6 can stream the metrics in real-time and either: + +- Write output to a file +- Send output to an external service. + +## Write to file {#write} + +Currently, k6 supports writing to the following file formats: + +- [CSV](https://grafana.com/docs/k6//results-output/real-time/csv) +- [JSON](https://grafana.com/docs/k6//results-output/real-time/json) + +## Stream to service {#service} + +You can also stream real-time metrics to: + +- [Grafana Cloud k6](https://grafana.com/docs/k6//results-output/real-time/cloud) + +As well as the following third-party services: + +- [Amazon CloudWatch](https://grafana.com/docs/k6//results-output/real-time/amazon-cloudwatch) +- [Apache Kafka](https://grafana.com/docs/k6//results-output/real-time/apache-kafka) +- [Datadog](https://grafana.com/docs/k6//results-output/real-time/datadog) +- [Dynatrace](https://grafana.com/docs/k6//results-output/real-time/dynatrace) +- [Elasticsearch](https://grafana.com/docs/k6//results-output/real-time/elasticsearch) +- [Grafana Cloud Prometheus](https://grafana.com/docs/k6//results-output/real-time/grafana-cloud-prometheus) +- [InfluxDB](https://grafana.com/docs/k6//results-output/real-time/influxdb) +- [Netdata](https://grafana.com/docs/k6//results-output/real-time/netdata) +- [New Relic](https://grafana.com/docs/k6//results-output/real-time/new-relic) +- [Prometheus remote write](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write) +- [TimescaleDB](https://grafana.com/docs/k6//results-output/real-time/timescaledb) +- [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd) +- [Other alternative with a custom output extension](https://grafana.com/docs/k6//extensions/create/output-extensions) + +{{% admonition type="note" %}} + +This list applies to local tests, not to [cloud tests](https://grafana.com/docs/grafana-cloud/k6/). + +{{% /admonition %}} + +## Read more + +- [Ways to visualize k6 results](https://k6.io/blog/ways-to-visualize-k6-results/) +- [k6 data collection pipeline](https://grafana.com/blog/2023/08/10/understanding-grafana-k6-a-simple-guide-to-the-load-testing-tool/) diff --git a/docs/sources/v0.50.x/results-output/real-time/amazon-cloudwatch.md b/docs/sources/v0.50.x/results-output/real-time/amazon-cloudwatch.md new file mode 100644 index 000000000..ffd59d9a2 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/amazon-cloudwatch.md @@ -0,0 +1,102 @@ +--- +title: 'Amazon CloudWatch' +description: 'You can send k6 results output to Amazon CloudWatch and later visualize them.' +weight: 00 +--- + +# Amazon CloudWatch + +{{% admonition type="warning" %}} + +The built-in StatsD output has been deprecated on k6 v0.47.0. You can continue to use this feature by using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd), and this guide has been updated to include instructions for how to use it. + +For more information on the reason behind this change, you can follow [this issue](https://github.com/grafana/k6/issues/2982) in the k6 repository. + +{{% /admonition %}} + +k6 can send metrics data to [Amazon CloudWatch](https://aws.amazon.com/cloudwatch/) through the [CloudWatch Agent](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Install-CloudWatch-Agent.html) by using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd). These metrics can then be visualized in dashboards. + +This guide covers how to: + +- Run the CloudWatch agent +- Run the k6 test +- Visualize k6 metrics in Amazon CloudWatch + +## Before you begin + +To use the StatsD output option, you have to build a k6 binary using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd). For more details, refer to [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd). + +## Run the CloudWatch agent + +We presume that you already have a machine that supports both running k6 and CloudWatch agent, which either runs a flavor of GNU/Linux or Windows. Just go ahead and [download](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/download-cloudwatch-agent-commandline.html) the suitable CloudWatch agent version for your operating system. + +1. Create an [IAM role](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/create-iam-roles-for-cloudwatch-agent.html) to send metrics to CloudWatch via the agent. Then, if you are running on Amazon EC2, just [attach](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/iam-roles-for-amazon-ec2.html#attach-iam-role) the role to your EC2 instance, so that you can send metrics to CloudWatch. Otherwise, if you are running on-premises servers, follow this [guide](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/install-CloudWatch-Agent-commandline-fleet.html#install-CloudWatch-Agent-iam_user-first). + +2. Download the CloudWatch Agent package suitable for your operating system. For example, on Debian 10 (Buster), we've used the following link. For other operating systems, please refer to this [guide](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/download-cloudwatch-agent-commandline.html): + + ```bash + $ wget https://s3.amazonaws.com/amazoncloudwatch-agent/debian/amd64/latest/amazon-cloudwatch-agent.deb + ``` + +3. Install the package: + + ```bash + $ sudo dpkg -i amazon-cloudwatch-agent.deb + ``` + +4. Configure the agent to receive data from k6. For this, create a file called "_/opt/aws/amazon-cloudwatch-agent/etc/statsd.json_" and paste the following JSON config object into it. This configuration means that the agent would listen on port number 8125, which is the default port number for k6 and StatsD. The interval for collecting metrics is 5 seconds and we don't aggregate them, since we need the raw data later in CloudWatch. + + ```json + { + "metrics": { + "namespace": "k6", + "metrics_collected": { + "statsd": { + "service_address": ":8125", + "metrics_collection_interval": 5, + "metrics_aggregation_interval": 0 + } + } + } + } + ``` + +5. Run the following command to start the agent: + + ```bash + $ sudo amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -s -c file:/opt/aws/amazon-cloudwatch-agent/etc/statsd.json + ``` + +6. You can check the status of the agent using the following command: + + ```bash + $ amazon-cloudwatch-agent-ctl -a status + ``` + +## Run the k6 test + +Once the agent is running, you can run your test with: + +{{< code >}} + +```bash +$ K6_STATSD_ENABLE_TAGS=true k6 run --out output-statsd script.js +``` + +{{< /code >}} + +Make sure you're using the k6 binary you built with the xk6-output-statsd extension. + +You can look at the [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd) output page for configuration options. + +## Visualize k6 metrics in Amazon CloudWatch + +Visualization of the exported metrics to CloudWatch is done by creating a dashboard and selecting desired metrics to be displayed. + +![List of k6 metrics](/media/docs/k6-oss/cloudwatch-k6-metrics.png) + +Here's an example dashboard we've created to visualize the test results. + +![k6 Dashboard on Amazon CloudWatch](/media/docs/k6-oss/cloudwatch-k6-dashboard.png) + +The above dashboard is exported as JSON and is available [here](https://github.com/k6io/example-cloudwatch-dashboards). diff --git a/docs/sources/v0.50.x/results-output/real-time/apache-kafka.md b/docs/sources/v0.50.x/results-output/real-time/apache-kafka.md new file mode 100644 index 000000000..0568e374b --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/apache-kafka.md @@ -0,0 +1,100 @@ +--- +title: 'Apache Kafka' +descriptiontion: 'You can use xk6-output-kafka to send k6 metrics in real-time to Kafka, and, optionally, ingest them from InfluxDB.' +weight: 00 +--- + +# Apache Kafka + +[Apache Kafka](https://kafka.apache.org) is a stream-processing platform for handling real-time data. Using [xk6-output-kafka extension](https://github.com/grafana/xk6-output-kafka), you can send k6 metrics in real-time to Kafka, and, optionally, ingest them from InfluxDB. + +## Build the k6 version + +To build a k6 binary with the extension, first, make sure you have [Go](https://golang.org/doc/install) and [Git](https://git-scm.com/) installed on your machine. + +Then, open your terminal and run the following commands: + +```bash +# Install xk6 +go install go.k6.io/xk6/cmd/xk6@latest + +# Build the k6 binary +xk6 build --with github.com/grafana/xk6-output-kafka + +... [INFO] Build environment ready +... [INFO] Building k6 +... [INFO] Build complete: ./k6 +``` + +xk6 will create the new k6 binary in the local folder. + +{{% admonition type="note" %}} + +To learn more about how to build custom k6 versions, check out [xk6](https://github.com/grafana/xk6). + +{{% /admonition %}} + +## Run the k6 test + +You can configure the broker (or multiple ones), topic and message format directly from the command line parameter like this: + +{{< code >}} + +```bash +$ k6 run --out xk6-kafka=brokers=broker_host:8000,topic=k6 +``` + +{{< /code >}} + +or if you want multiple brokers: + +{{< code >}} + +```bash +--out xk6-kafka=brokers={broker1,broker2},topic=k6,format=json +``` + +{{< /code >}} + +You can also specify the message `format` k6 will use. By default, it will be the same as the JSON output, but you can also use the InfluxDB line protocol for direct "consumption" by InfluxDB: + +{{< code >}} + +```bash +--out xk6-kafka=brokers=someBroker,topic=someTopic,format=influxdb +``` + +{{< /code >}} + +You can even modify some of the `format` settings such as `tagsAsFields`: + +{{< code >}} + +```bash +--out xk6-kafka=brokers=someBroker,topic=someTopic,format=influxdb,influxdb.tagsAsFields={url,myCustomTag} +``` + +{{< /code >}} + +### Options + +Here is the full list of options that can be configured and passed to the extension: + +| Name | Type | Description | +| ----------------------------------- | --------- | --------------------------------------------------------------- | +| `K6_KAFKA_BROKERS` | `string` | List of brokers | +| `K6_KAFKA_TOPIC` | `string` | The name of the topic to be sent | +| `K6_KAFKA_AUTH_MECHANISM` | `string` | Authentication mechanism. Default `none`. | +| `K6_KAFKA_SASL_USER` | `string` | Kafka User | +| `K6_KAFKA_SASL_PASSWORD` | `string` | Kafka User Password | +| `K6_KAFKA_SSL` | `boolean` | | +| `K6_KAFKA_VERSION` | `string` | Kafka version. Default the latest | +| `K6_KAFKA_INSECURE_SKIP_TLS_VERIFY` | `boolean` | Whether should ignore TLS verifications | +| `K6_KAFKA_PUSH_INTERVAL` | `string` | Interval of the metrics' aggregation and upload to the endpoint | +| `K6_KAFKA_FORMAT` | `string` | Message format. `json` or `influxdb` | +| `K6_KAFKA_LOG_ERROR` | `boolean` | Boolean indicating to log kafka errors | + +## Read more + +- [xk6-output-kafka extension](https://github.com/grafana/xk6-output-kafka) +- [Integrating k6 with Apache Kafka](https://k6.io/blog/integrating-k6-with-apache-kafka) diff --git a/docs/sources/v0.50.x/results-output/real-time/cloud.md b/docs/sources/v0.50.x/results-output/real-time/cloud.md new file mode 100644 index 000000000..8d23c87d3 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/cloud.md @@ -0,0 +1,138 @@ +--- +title: 'Grafana Cloud k6' +description: 'When streaming the results to the cloud, the machine - where you execute the k6 CLI command - runs the test and uploads the results to the cloud. Then, you will be able to visualize and analyze the results on the web app in real-time.' +weight: 00 +--- + +# Grafana Cloud k6 + +Besides [running cloud tests](https://grafana.com/docs/k6//get-started/running-k6#execution-modes), you can also run a test locally and stream the results to [Grafana Cloud k6](https://grafana.com/docs/grafana-cloud/k6/get-started/run-cloud-tests-from-the-cli/#run-locally-and-stream-to-the-cloud). + +When streaming the results to the cloud, the machine - where you execute the k6 CLI command - runs the test and uploads the results to the cloud-based solution. Then, you will be able to visualize and analyze the results on the web app in real-time. + +## Streaming results vs. running on cloud servers + +Don't confuse `k6 run --out cloud script.js` (what this page is about) with `k6 cloud script.js`. + +Fundamentally the difference is the machine that the test runs on: + +- `k6 run --out cloud` runs k6 locally and streams the results to the cloud. +- `k6 cloud`, on the other hand, uploads your script to the cloud solution and runs the test on the cloud infrastructure. In this case you'll only see status updates in your CLI. + +In all cases you'll be able to see your test results at [k6 Cloud](https://app.k6.io) or [Grafana Cloud](https://grafana.com/products/cloud/). + +{{% admonition type="caution" %}} + +Data storage and processing are primary cloud costs, +so `k6 run --out cloud` will consume VUh or test runs from your subscription. + +{{% /admonition %}} + +## Instructions + +1. (Optional) - Log in to the cloud + + Assuming you have installed k6, the first step is to log in to the cloud service. + + With the `k6 login cloud` command, you can set up your API token on the k6 machine to authenticate against the cloud service. + + Copy your token from [k6 Cloud](https://app.k6.io/account/api-token) or [Grafana Cloud k6](https://grafana.com/docs/grafana-cloud/k6/author-run/tokens-and-cli-authentication/) and pass it as: + + {{< code >}} + + ```bash + $ k6 login cloud --token + ``` + + {{< /code >}} + +1. Run the tests and upload the results + + Now, k6 will authenticate you against the cloud service, and you can use the `--out` option to send the k6 results to the cloud as: + + {{< code >}} + + ```bash + $ k6 run --out cloud script.js + ``` + + {{< /code >}} + + Alternatively, you could skip the `k6 login cloud` command when passing your API token to the `k6 run` command as: + + {{< code >}} + + ```bash + $ K6_CLOUD_TOKEN= k6 run --out cloud script.js + ``` + + {{< /code >}} + + After running the command, the console shows an URL. Copy this URL and paste it in your browser's address bar to visualize the test results. + + {{< code >}} + + ```bash + execution: local + output: cloud (https://acmecorp.grafana.net/a/k6-app/runs/123456) + script: script.js + ``` + + {{< /code >}} + +![Grafana Cloud k6 Test Results](/media/docs/k6-oss/screenshot-stream-k6-results-to-grafana-cloud-k6.png) + +When you send the results to the k6 Cloud with `k6 run --out`, data will be +continuously streamed to the cloud. While this happens the state of the test run +will be marked as `Running`. A test run that ran its course will be marked +`Finished`. The run state has nothing to do with the test passing any +thresholds, only that the test itself is operating correctly. + +If you deliberately abort your test (e.g. by pressing _Ctrl-C_), your test will +end up as `Aborted by User`. You can still look and analyze the test data you +streamed so far. The test will just have run shorter than originally planned. + +Another possibility would be if you lose network connection with the cloud service +while your test is running. In that case the cloud service will patiently wait for +you to reconnect. In the meanwhile your test's run state will continue to +appear as `Running` on the web app. + +If no reconnection happens, the cloud service will time out after two minutes of no +data, setting the run state to `Timed out`. You can still analyze a timed out +test but you'll of course only have access to as much data as was streamed +before the network issue. + +## Advanced settings + +A few [environment variables](https://grafana.com/docs/k6//using-k6/environment-variables) can control how k6 streams results with `-o cloud`. + +When streaming, k6 will collect all data and send it to the cloud in batches. + +| Name | Description | +| ------------------------------- | ----------------------------------------------------- | +| `K6_CLOUD_METRIC_PUSH_INTERVAL` | How often to send data to the cloud (default `'1s'`). | + +k6 can also _aggregate_ the data it sends to the cloud each batch. This +reduces the amount of data sent to the cloud. Aggregation is disabled by +default. + +When using aggregation, k6 will collect incoming test data into time-buckets. +For each data-type collected in such a bucket, it will figure out the dispersion +(by default the [interquartile range][iqr]) around the median value. +Outlier data—far outside the lower and upper quartiles— is not aggregated, preventing the loss of potentially important testing information. + +| Name | Description | +| --------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | +| `K6_CLOUD_AGGREGATION_PERIOD` | >0s to activate aggregation (default disabled, `'3s'` is a good example to try) | +| `K6_CLOUD_AGGREGATION_CALC_INTERVAL` | How often new data will be aggregated (default `'3s'`). | +| `K6_CLOUD_AGGREGATION_WAIT_PERIOD` | How long to wait for period samples to accumulate before aggregating them (default `'5s'`). | +| `K6_CLOUD_AGGREGATION_MIN_SAMPLES` | If fewer samples than this arrived in interval, skip aggregation (default `100`). | +| `K6_CLOUD_AGGREGATION_OUTLIER_IQR_RADIUS` | Outlier sampling from median to use for Q1 and Q3 quartiles (fraction) (default `0.25` (i.e. [IQR][iqr])). | +| `K6_CLOUD_AGGREGATION_OUTLIER_IQR_COEF_LOWER` | How many quartiles below the lower quartile are treated as non-aggregatable outliers (default `1.5`) | +| `K6_CLOUD_AGGREGATION_OUTLIER_IQR_COEF_UPPER` | How many quartiles above the upper quartile are treated as non-aggregatable outliers (default `1.3`) | + +> When running a test entirely in the cloud with `k6 cloud`, `k6` will always +> aggregate. For that case the aggregation settings are however set by the +> cloud infrastructure and are not controllable from the CLI. + +[iqr]: https://en.wikipedia.org/wiki/Interquartile_range diff --git a/docs/sources/v0.50.x/results-output/real-time/csv.md b/docs/sources/v0.50.x/results-output/real-time/csv.md new file mode 100644 index 000000000..df883e105 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/csv.md @@ -0,0 +1,85 @@ +--- +title: 'CSV' +description: 'You can also make k6 output detailed statistics in a CSV format by using the --out option.' +weight: 00 +--- + +# CSV + +You can output granular data points in CSV format. +To do so, use `k6 run` with the `--out` flag. +Pass the path for your CSV file as the flag argument: + +{{< code >}} + +```bash +$ k6 run --out csv=test_results.csv script.js +``` + +{{< /code >}} + +You can also get the results gzipped, like this: + +{{< code >}} + +```bash +$ k6 run --out csv=test_results.gz script.js +``` + +{{< /code >}} + +To inspect the output in real time, you can use a command like `tail -f` on the file you save: + +```bash +$ tail -f test_results.csv +``` + +## CSV format + +The CSV result file will look something like this: + +{{< code >}} + +```plain +metric_name,timestamp,metric_value,check,error,error_code,group,method,name,proto,scenario,status,subproto,tls_version,url,extra_tags +http_reqs,1595325560,1.000000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +http_req_duration,1595325560,221.899000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +http_req_blocked,1595325560,225.275000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +http_req_connecting,1595325560,217.680000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +http_req_tls_handshaking,1595325560,0.000000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +http_req_sending,1595325560,0.112000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +http_req_waiting,1595325560,220.280000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +http_req_receiving,1595325560,1.507000,,,,,GET,http://test.k6.io,HTTP/1.1,default,200,,,http://test.k6.io, +vus,1595325560,1.000000,,,,,,,,,,,,, +vus_max,1595325560,20.000000,,,,,,,,,,,,, +checks,1595325561,1.000000,status is 200,,,,,,,default,,,,, +checks,1595325561,0.000000,response body,,,,,,,default,,,,, +data_sent,1595325561,76.000000,,,,,,,,default,,,,, +data_received,1595325561,11045.000000,,,,,,,,default,,,,, +iteration_duration,1595325561,1449.049580,,,,,,,,default,,,,, +iterations,1595325561,1.000000,,,,,,,,default,,,,, +``` + +{{< /code >}} + +Each entry in the report represents a metric, `metric_name`, along with its value, `metric_value`, at time, `timestamp`. +If an error happens, then the `error` along with the `error_code` fields will be populated. + +## CSV options + +k6 provides a few options to help you configure your CSV output: + + + +| option | Configures | Possible values | Default | Env. variable | +| -------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ---------- | ---------------------- | +| `saveInterval` | The time intervals at which k6 writes to the CSV | Either a string with time units(`"1m45s"`), or a number of milliseconds | `"1s"` | `K6_CSV_SAVE_INTERVAL` | +| `timeFormat` | The timestamp format | unix, unix_nano, unix_micro, unix_milli, [rfc3339](https://datatracker.ietf.org/doc/html/rfc3339), rfc3339_nano | unix | `K6_CSV_TIME_FORMAT` | +| `fileName` | The file name and path where output is saved | N/A | `file.csv` | `K6_CSV_FILENAME` | + + + +## Read more + +- [Metrics](https://grafana.com/docs/k6//using-k6/metrics) +- [Error codes](https://grafana.com/docs/k6//javascript-api/error-codes) diff --git a/docs/sources/v0.50.x/results-output/real-time/datadog.md b/docs/sources/v0.50.x/results-output/real-time/datadog.md new file mode 100644 index 000000000..e01735f62 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/datadog.md @@ -0,0 +1,109 @@ +--- +title: 'Datadog' +description: 'Send k6 output to Datadog to visualize load test results and correlate performance testing metrics in Datadog.' +weight: 00 +--- + +# Datadog + +{{% admonition type="warning" %}} + +The built-in StatsD output has been deprecated on k6 v0.47.0. You can continue to use this feature by using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd), and this guide has been updated to include instructions for how to use it. + +For more information on the reason behind this change, you can follow [this issue](https://github.com/grafana/k6/issues/2982) in the k6 repository. + +{{% /admonition %}} + +k6 can send metrics to [Datadog](https://www.datadoghq.com/). That allows visualizing and correlating performance testing metrics with other monitored metrics in Datadog. + +This guide covers how to: + +- Run the Datadog Agent +- Run the k6 test +- Visualize in Datadog + +## Before you begin + +To use the StatsD output option, you have to build a k6 binary using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd). For more details, refer to [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd). + +## Run the Datadog Agent + +To get k6 metrics into Datadog, k6 sends metrics through the Datadog Agent, which collects, aggregates, and forwards the metrics to the Datadog platform. + +Run the Datadog Agent service as a Docker container with this command: + +{{< code >}} + +```bash +DOCKER_CONTENT_TRUST=1 \ +docker run --rm -d \ + --name datadog \ + -v /var/run/docker.sock:/var/run/docker.sock:ro \ + -v /proc/:/host/proc/:ro \ + -v /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \ + -e DD_SITE="datadoghq.com" \ + -e DD_API_KEY= \ + -e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \ + -p 8125:8125/udp \ + datadog/agent:latest +``` + +{{< /code >}} + +Replace `` with your [Datadog API key](https://app.datadoghq.com/account/settings#api). + +If your account is registered with Datadog EU, change the value of `DD_SITE` to `datadoghq.eu`. + +{{% admonition type="note" %}} + +For additional information, refer to the Datadog Docker Agent documentation. + +{{% /admonition %}} + +### DogStatsD + +The Datadog agent includes the [DogStatsD](https://docs.datadoghq.com/developers/dogstatsd/) service to collect and aggregate metrics. DogStatsD implements the [StatsD](https://github.com/etsy/statsd) protocol with some extensions. For example, [DogStatsD tagging](https://docs.datadoghq.com/tagging/) allows to collect k6 metrics with tags to distinguish between requests for different URLs, response statuses, groups, etc. + +The instruction above runs the `DogStatsD` service in a [Docker container](https://docs.datadoghq.com/developers/dogstatsd/?tab=containeragent#agent), but it's also possible to run it either as [Host Agent](https://docs.datadoghq.com/developers/dogstatsd/?tab=hostagent#agent), [Kubernetes](https://docs.datadoghq.com/developers/dogstatsd/?tab=kubernetes#agent), and [Helm](https://docs.datadoghq.com/developers/dogstatsd/?tab=helm#agent). + +## Run the k6 test + +Once the Datadog Agent service is running, run the k6 test and send the metrics to the Agent with: + +{{< code >}} + +```bash +$ K6_STATSD_ENABLE_TAGS=true k6 run --out output-statsd script.js +``` + +{{< /code >}} + +Make sure you're using the k6 binary you built with the xk6-output-statsd extension. + +You can look at the [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd) output page for configuration options. + +## Visualize in Datadog + +While running the test, k6 sends metrics periodically to Datadog. By default, these metrics have `k6.` as the name prefix. + +You can visualize k6 metrics in real-time with the [metrics explorer](https://docs.datadoghq.com/metrics/explorer/), [monitors](https://docs.datadoghq.com/monitors/), or [custom dashboards](https://docs.datadoghq.com/graphing/dashboards/). + +![Datadog visualizing performance testing metrics](/media/docs/k6-oss/datadog-performance-testing-metrics.png) + +
+ +To learn more about all the types of k6 metrics, read the [k6 Metrics guide](https://grafana.com/docs/k6//using-k6/metrics) + +
+ +The first time Datadog detects the `k6.http_reqs` metric, the k6 integration tile is installed automatically, and the default k6 dashboard is added to your dashboard list. + +![k6 Datadog Dashboard](/media/docs/k6-oss/datadog-k6-dashboard.png) + +Optionally, you can install the k6 integration tile following these instructions: + +1. Log in to `Datadog`. +2. From the sidebar menu, choose `Integrations` > `Integrations`. +3. Search for `k6`, then select the `k6` integration. +4. Click on the `Configuration` tab option. +5. Scroll down and click on the `Install integration` button. diff --git a/docs/sources/v0.50.x/results-output/real-time/dynatrace.md b/docs/sources/v0.50.x/results-output/real-time/dynatrace.md new file mode 100644 index 000000000..81b2dda16 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/dynatrace.md @@ -0,0 +1,83 @@ +--- +title: 'Dynatrace' +description: Send k6 output to Dynatrace to visualize load test results and correlate performance testing metrics in Dynatrace. +weight: 00 +--- + +# Dynatrace + +With the [Dynatrace k6 extension](https://github.com/Dynatrace/xk6-output-dynatrace), +you can send visualize and correlate performance testing metrics with the other metrics that you monitor in Dynatrace. + +## Build the k6 version + +To build a k6 binary with the extension, first, make sure you have [Go](https://golang.org/doc/install) and [Git](https://git-scm.com/) installed on your machine. + +Then, open your terminal and run the following commands: + +```bash +# Install xk6 +go install go.k6.io/xk6/cmd/xk6@latest + +# Build the k6 binary +xk6 build --with github.com/Dynatrace/xk6-output-dynatrace + +... [INFO] Build environment ready +... [INFO] Building k6 +... [INFO] Build complete: ./k6 +``` + +xk6 will create the new k6 binary in the local folder. + +{{% admonition type="note" %}} + +To learn more about how to build custom k6 versions, check out [xk6](https://github.com/grafana/xk6). + +{{% /admonition %}} + +## Run the test + +Create a Dynatrace API token to send the data. + +{{% admonition type="caution" %}} +The Dynatrace API Token must have the scope name "metrics.ingest" (scope type `API v2`). +{{% /admonition %}} + +You can use the Dynatrace UI: + +![Dynatrace API token](/media/docs/k6-oss/dynatrace-api-token.png) + +Or a `curl` command (replace `` and the `Api-Token`): + +```bash +curl -X POST "https://.live.dynatrace.com/api/v2/apiTokens" -H "accept: application/json; charset=utf-8" -H "Content-Type: application/json; charset=utf-8" -d "{\"name\":\"\",\"scopes\":[\"metrics.ingest\"]}" -H "Authorization: Api-Token XXXXXXXX" +``` + +Use the previously built k6 binary and run the test passing the Dynatrace URL and API token as follows: + +```bash +# export dynatrace variables +export K6_DYNATRACE_URL=https://.live.dynatrace.com +export K6_DYNATRACE_APITOKEN= + +# run the test +./k6 run script.js -o output-dynatrace +``` + +Check the metrics in your Dynatrace environment, filtering for `k6`: + +![Dynatrace Test result](/media/docs/k6-oss/dynatrace-k6-metrics.png) + +![Dynatrace Test result](/media/docs/k6-oss/dynatrace-k6-test-result.png) + +### Options + +When streaming the k6 results to Dynatrace, you can configure the following Dynatrace options: + +| Name | Value | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `K6_DYNATRACE_APITOKEN` | Dynatrace API token to write the metrics. The token must have the scope `metrics.ingest API v2`. | +| `K6_DYNATRACE_FLUSH_PERIOD` | Define how often metrics are sent to Dynatrace. The default value is 1 second. | +| `K6_DYNATRACE_URL` | Dynatrace URL. The default value is `https://dynatrace.live.com`. | +| `K6_DYNATRACE_INSECURE_SKIP_TLS_VERIFY` | If `true`, the HTTP client skips TLS verification on the endpoint. The default value is `true`. | +| `K6_DYNATRACE_HEADER_` | Additional headers to include in the HTTP requests. `K6_DYNATRACE_HEADER_COOL_HEADER="cool value of the header"` | diff --git a/docs/sources/v0.50.x/results-output/real-time/elasticsearch.md b/docs/sources/v0.50.x/results-output/real-time/elasticsearch.md new file mode 100644 index 000000000..160e9afb6 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/elasticsearch.md @@ -0,0 +1,100 @@ +--- +title: 'Elasticsearch' +description: k6 has an output extension to store k6 metrics in Elasticsearch. This document shows you how to configure the k6 Elasticsearch integration. +weight: 00 +--- + +# Elasticsearch + +Using the [Elasticsearch k6 extension](https://github.com/elastic/xk6-output-elasticsearch), you can store k6 metrics in [Elasticsearch](https://github.com/elastic/elasticsearch) and analyze your performance results with Kibana or Grafana. + +## Build the k6 version + +To build a k6 binary with the extension, first, make sure you have [Go](https://golang.org/doc/install) and [Git](https://git-scm.com/) installed on your machine. + +Then, open your terminal and run the following commands: + +```bash +# Install xk6 +go install go.k6.io/xk6/cmd/xk6@latest + +# Build the k6 binary +xk6 build --with github.com/elastic/xk6-output-elasticsearch + +... [INFO] Build environment ready +... [INFO] Building k6 +... [INFO] Build complete: ./k6 +``` + +xk6 will create the new k6 binary in the local folder. + +{{% admonition type="note" %}} + +To learn more about how to build custom k6 versions, check out [xk6](https://github.com/grafana/xk6). + +{{% /admonition %}} + +## Run the test + +Check that the Elasticsearch instance to store the k6 metrics is running. + +If you're running on Elasticsearch cloud, use the previous k6 binary and run the test passing the cloud credentials as follows: + +```bash +# export cloud configuration +export K6_ELASTICSEARCH_CLOUD_ID=your-cloud-id-here +export K6_ELASTICSEARCH_USER=your-user-here +export K6_ELASTICSEARCH_PASSWORD=your-password-here + +# run the test +./k6 run script.js -o output-elasticsearch +``` + +k6 runs the test script and sends the metrics in real-time to Elasticsearch. + +You can also send the metrics to a local Elasticsearch cluster: + +```bash +# export local url +export K6_ELASTICSEARCH_URL=http://localhost:9200 + +# run the test +./k6 run script.js -o output-elasticsearch +``` + +{{% admonition type="caution" %}} +Security and self-signed certificates for non-cloud clusters are not yet supported. +{{% /admonition %}} + +You can now connect to Elasticsearch and query the [k6 metrics](https://grafana.com/docs/k6//using-k6/metrics) stored in the `k6-metrics` index. +The following example uses an unsecured local Elasticsearch, version `7.17.9`: + +```bash +curl -XGET 'http://localhost:9200/k6-metrics/_search?pretty' -H 'Content-Type: application/json' -d' +{ + "sort": [ + { + "Time": { + "order": "desc" + } + } + ], + "size": 10 +}' +``` + +Or use [Kibana Discover](https://www.elastic.co/guide/en/kibana/7.17/discover.html): + +![Kibana Discover k6 results](/media/docs/k6-oss/kibana-discover-test-result.png) + +### Options + +Here is the full list of options that can be configured and passed to the extension: + +| Name | Value | +| ------------------------------- | ---------------------------------------------------------------------------------- | +| `K6_ELASTICSEARCH_CLOUD_ID` | Elasticsearch `cloud.id`, which can be found in the Elastic Cloud web console. | +| `K6_ELASTICSEARCH_FLUSH_PERIOD` | Define how often metrics are sent to Elasticsearch. The default value is 1 second. | +| `K6_ELASTICSEARCH_URL` | Elasticsearch URL. | +| `K6_ELASTICSEARCH_USER` | Elasticsearch username. | +| `K6_ELASTICSEARCH_PASSWORD` | Elasticsearch password. | diff --git a/docs/sources/v0.50.x/results-output/real-time/grafana-cloud-prometheus.md b/docs/sources/v0.50.x/results-output/real-time/grafana-cloud-prometheus.md new file mode 100644 index 000000000..5b628db32 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/grafana-cloud-prometheus.md @@ -0,0 +1,94 @@ +--- +title: 'Grafana Cloud Prometheus' +description: > + How to upload the test result metrics to Grafana Cloud using Grafana Cloud Prometheus and the k6 output for Prometheus remote write' +weight: 00 +--- + +# Grafana Cloud Prometheus + +{{% admonition type="caution" %}} + +This page includes instructions for running a local test that sends the test results to a Prometheus instance in Grafana Cloud. + +For running and managing cloud tests in Grafana Cloud, check out [Grafana Cloud k6](https://grafana.com/docs/grafana-cloud/k6/). + +{{% /admonition %}} + +With Grafana Cloud Prometheus and the [k6 output for Prometheus remote write](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write), you can send your k6 results output to [Grafana Cloud](https://grafana.com/products/cloud) to visualize your testing results. +With k6 metrics in Grafana, you can correlate k6 metrics with other metrics of your observability stack. + +While this topic uses Grafana Cloud as an example, this approach is compatible with any remote write capable Prometheus installation. + +## Set up Grafana Cloud Prometheus + +Before you start, you need the following: + +- A Grafana Cloud account ([sign up](https://grafana.com/products/cloud/)). + The free plan includes 10,000 Prometheus series. +- The URL, username, and password of your Grafana Cloud Prometheus instance to configure the integration. + +After you've set up your account, follow these steps: + +1. Log in to `Grafana.com` and visit the [Cloud Portal](https://grafana.com/docs/grafana-cloud/fundamentals/cloud-portal/). + Select the **Details** of your Prometheus service. + + ![Grafana Cloud Portal](/media/docs/k6-oss/grafana_cloud_portal.png) + +1. Copy the URL of the Remote Write Endpoint, along with the Username and Instance ID. + + ![Grafana Cloud Prometheus Configuration](/media/docs/k6-oss/grafana_cloud_prometheus_configuration.png) + +1. In the **Password / API Key** section, create and copy an API key of the `MetricsPublisher` role. This will be used as a password. + + ![Create API Key](/media/docs/k6-oss/grafana_cloud_create_api_key_metrics_publisher.png) + +## Run the test + +Now, pass the Username, API key, and Remote Write Endpoint of the Grafana Cloud Prometheus Configuration to the k6 binary: + +```bash +K6_PROMETHEUS_RW_USERNAME=USERNAME \ +K6_PROMETHEUS_RW_PASSWORD=API_KEY \ +K6_PROMETHEUS_RW_SERVER_URL=REMOTE_WRITE_ENDPOINT \ +k6 run -o experimental-prometheus-rw script.js +``` + +## Visualize test results + +To visualize test results with Grafana, you can import the [k6 Prometheus dashboard by Grafana k6](https://grafana.com/grafana/dashboards/19665-k6-prometheus/). + +On the Dashboards UI: + +- Click `New` and select `Import`. +- Paste the Grafana URL or ID of the dashboard, and click `Load`. +- Select the Prometheus data source, and click `Import`. + +![k6 Prometheus Dashboard](/media/docs/k6-oss/k6-prometheus-dashboard-part1.png) + +Optionally, when running the test, you can set the `testid` tag as a [wide test tag](https://grafana.com/docs/k6//using-k6/tags-and-groups/#test-wide-tags) to filter results of a particular test run on this dashboard (or in PromQL queries). `testid` can be any unique string that allows you to identify the test run. + +{{< code >}} + +```bash +K6_PROMETHEUS_RW_USERNAME=USERNAME \ +K6_PROMETHEUS_RW_PASSWORD=API_KEY \ +K6_PROMETHEUS_RW_SERVER_URL=REMOTE_WRITE_ENDPOINT \ +k6 run -o experimental-prometheus-rw --tag testid=TEST_ID script.js +``` + +{{< /code >}} + +Additionally, you can also use the [Explore UI](https://grafana.com/docs/grafana/latest/explore/) to query k6 time series, design your visualization panels, and add them to any of your existing dashboards. + +![Explore k6 metrics in Grafana Cloud](/media/docs/k6-oss/grafana_cloud_explore_k6_metrics_from_extension.png) + +All the k6 time series have a **k6\_** prefix. +For more details, refer to the documentation on the [mapping of k6 metrics with Prometheus metrics](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write#metrics-mapping). + +It's also important to understand the default [Trend metric conversion](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write#trend-metric-conversions) process and the format and querying limitations. The [`K6_PROMETHEUS_RW_TREND_STATS` option](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write#options) allows you to convert trend metrics to multiple Prometheus time series. For instance, `K6_PROMETHEUS_RW_TREND_STATS=p(95),p(99),max,min` transforms each k6 trend metric into four Prometheus metrics as follows: + +- `k6_*_p95` +- `k6_*_p99` +- `k6_*_max` +- `k6_*_min` diff --git a/docs/sources/v0.50.x/results-output/real-time/influxdb.md b/docs/sources/v0.50.x/results-output/real-time/influxdb.md new file mode 100644 index 000000000..308eb8be7 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/influxdb.md @@ -0,0 +1,80 @@ +--- +title: 'InfluxDB' +description: 'k6 has an output extension to store k6 metrics in InfluxDB v2. This document shows you how to configure this integration.' +weight: 00 +--- + +# InfluxDB + +Using the [InfluxDB extension](https://github.com/grafana/xk6-output-influxdb), you can store k6 metrics in [InfluxDB v2.0](https://docs.influxdata.com/influxdb/v2.0/) and analyze your performance results with Grafana or [other tools](https://docs.influxdata.com/influxdb/cloud-serverless/query-data/tools/). + +## Build the k6 version + +To build a k6 binary with the extension, first, make sure you have [Go](https://golang.org/doc/install) and [Git](https://git-scm.com/) installed on your machine. + +Then, open your terminal and run the following commands: + +```bash +# Install xk6 +go install go.k6.io/xk6/cmd/xk6@latest + +# Build the k6 binary +xk6 build --with github.com/grafana/xk6-output-influxdb + +... [INFO] Build environment ready +... [INFO] Building k6 +... [INFO] Build complete: ./k6 +``` + +xk6 will create the new k6 binary in the local folder. + +{{% admonition type="note" %}} + +To learn more about how to build custom k6 versions, check out [xk6](https://github.com/grafana/xk6). + +{{% /admonition %}} + +## Run the test + +Check that the InfluxDB instance to store the k6 metrics is running. + +Use the previous k6 binary and run the test passing the following [options](#options): + +```bash +K6_INFLUXDB_ORGANIZATION="" \ +K6_INFLUXDB_BUCKET="" \ +K6_INFLUXDB_TOKEN="" \ +K6_INFLUXDB_ADDR="" \ +./k6 run script.js -o xk6-influxdb +``` + +k6 runs the test script and sends the [k6 metrics](https://grafana.com/docs/k6//using-k6/metrics) in real-time to the InfluxDB instance. You can now select the bucket to [query](https://docs.influxdata.com/influxdb/v2.7/query-data/) and [visualize](https://docs.influxdata.com/influxdb/v2.7/visualize-data/) the stored k6 metrics, for example, using the [InfluxDB Data Explorer](https://docs.influxdata.com/influxdb/v2.7/query-data/execute-queries/data-explorer/). + +
+ +![InfluxDB Data Explorer / k6 bucket](/media/docs/k6-oss/influxdb-data-explorer-k6-bucket.png) + +## Options + +Here is the full list of options that can be configured and passed to the extension: + +| ENV | Default | Description | +| ----------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| K6_INFLUXDB_ORGANIZATION | | Your InfluxDB organization name. [View organizations](https://docs.influxdata.com/influxdb/v2.7/organizations/). | +| K6_INFLUXDB_BUCKET | | The bucket name to store k6 metrics data. [Manage buckets](https://docs.influxdata.com/influxdb/v2.7/organizations/buckets/). | +| K6_INFLUXDB_TOKEN | | An API token that provides authorized access to store data. [Manage API tokens](https://docs.influxdata.com/influxdb/v2.7/security/tokens/). | +| K6_INFLUXDB_ADDR | http://localhost:8086 | The address of the InfluxDB instance. | +| K6_INFLUXDB_PUSH_INTERVAL | 1s | The flush's frequency of the `k6` metrics. | +| K6_INFLUXDB_CONCURRENT_WRITES | 4 | Number of concurrent requests for flushing data. It is useful when a request takes more than the expected time (more than flush interval). | +| K6_INFLUXDB_TAGS_AS_FIELDS | vu:int,iter:int,url | A comma-separated string to set `k6` metrics as non-indexable fields (instead of tags). An optional type can be specified using :type as in vu:int will make the field integer. The possible field types are int, bool, float and string, which is the default. Example: vu:int,iter:int,url:string,event_time:int. | +| K6_INFLUXDB_INSECURE | false | When `true`, it will skip `https` certificate verification. | +| K6_INFLUXDB_PRECISION | 1ns | The timestamp [Precision](https://docs.influxdata.com/influxdb/v2.7/reference/glossary/#precision). | +| K6_INFLUXDB_HTTP_PROXY | | Sets an HTTP proxy for the InfluxDB output. | + +## Grafana Dashboards + +You can use Grafana to query and visualize data from an InfluxDB instance. The instructions are available on [InfluxDB](https://docs.influxdata.com/influxdb/v2.7/tools/grafana/) and [Grafana](https://grafana.com/docs/grafana/latest/datasources/influxdb/). + +You can also build a [custom Grafana dashboard](https://grafana.com/docs/k6//results-output/grafana-dashboards) to visualize the testing results in your own way. + +For testing purposes, the [influxdb extension](https://github.com/grafana/xk6-output-influxdb) repository includes a [docker-compose setup](https://github.com/grafana/xk6-output-influxdb#docker-compose) with two basic dashboards. diff --git a/docs/sources/v0.50.x/results-output/real-time/json.md b/docs/sources/v0.50.x/results-output/real-time/json.md new file mode 100644 index 000000000..1febba1a6 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/json.md @@ -0,0 +1,148 @@ +--- +title: 'JSON' +description: 'You can also make k6 output detailed statistics in JSON format by using the --out option.' +weight: 00 +--- + +# JSON + +You can output granular data points in JSON format. +To do so, use `k6 run` with the `--out` flag. +Pass the path for your JSON file as the flag argument: + +{{< code >}} + +```bash +$ k6 run --out json=test_results.json script.js +``` + +```docker +$ docker run -it --rm \ + -v :/scripts \ + -v :/jsonoutput \ + grafana/k6 run --out json=/jsonoutput/my_test_result.json /scripts/script.js + +# Note that the docker user must have permission to write to ! +``` + +{{< /code >}} + +Or if you want to get the result gzipped, like this: + +{{< code >}} + +```bash +$ k6 run --out json=test_results.gz script.js +``` + +```docker +$ docker run -it --rm \ + -v :/scripts \ + -v :/jsonoutput \ + grafana/k6 run --out json=/jsonoutput/my_test_result.gz /scripts/script.js + +# Note that the docker user must have permission to write to ! +``` + +{{< /code >}} + +To inspect the output in real time, you can use a command like `tail -f` on the file you save: + +```bash +$ tail -f test_results.json +``` + +## JSON format + +The JSON output has lines as follows: + +{{< code >}} + +```json +{"type":"Metric","data":{"type":"gauge","contains":"default","tainted":null,"thresholds":[],"submetrics":null},"metric":"vus"} +{"type":"Point","data":{"time":"2017-05-09T14:34:45.625742514+02:00","value":5,"tags":null},"metric":"vus"} +{"type":"Metric","data":{"type":"trend","contains":"time","tainted":null,"thresholds":["avg<1000"],"submetrics":null},"metric":"http_req_duration"} +{"type":"Point","data":{"time":"2017-05-09T14:34:45.239531499+02:00","value":459.865729,"tags":{"group":"::my group::json","method":"GET","status":"200","url":"https://httpbin.test.k6.io/get"}},"metric":"http_req_duration"} +``` + +{{< /code >}} + +Each line either has information about a metric, or logs a data point (sample) for a metric. +Lines consist of three items: + +- `type` - can have the values [Metric](#metric) or [Point](#point) where `Metric` means the line is declaring a metric, and `Point` is an actual data point (sample) for a metric. +- `data` - is a dictionary that contains lots of stuff, varying depending on the `"type"` above. +- `metric` - the name of the metric. + +### Metric + +This line has metadata about a metric. Here, `"data"` contains the following: + +- `"type"` - the metric type ("gauge", "rate", "counter" or "trend") +- `"contains"` - information on the type of data collected (can e.g. be "time" for timing metrics) +- `"tainted"` - has this metric caused a threshold to fail? +- `"threshold"` - are there any thresholds attached to this metric? +- `"submetrics"` - any derived metrics created as a result of adding a threshold using tags. + +### Point + +This line has actual data samples. Here, `"data"` contains these fields: + +- `"time"` - timestamp when the sample was collected +- `"value"` - the actual data sample; time values are in milliseconds +- `"tags"` - dictionary with tagname-tagvalue pairs that can be used when filtering results data + +## Processing JSON output + +You can use [jq][jq_url] to process the k6 JSON output. + +You can quickly create [filters][jq_filters_url] to return a particular metric of the JSON file: + +{{< code >}} + +```bash +$ jq '. | select(.type=="Point" and .metric == "http_req_duration" and .data.tags.status >= "200")' myscript-output.json +``` + +{{< /code >}} + +And calculate an aggregated value of any metric: + +{{< code >}} + +```bash +$ jq '. | select(.type=="Point" and .metric == "http_req_duration" and .data.tags.status >= "200") | .data.value' myscript-output.json | jq -s 'add/length' +``` + +{{< /code >}} + +{{< code >}} + +```bash +$ jq '. | select(.type=="Point" and .metric == "http_req_duration" and .data.tags.status >= "200") | .data.value' myscript-output.json | jq -s min +``` + +{{< /code >}} + +{{< code >}} + +```bash +$ jq '. | select(.type=="Point" and .metric == "http_req_duration" and .data.tags.status >= "200") | .data.value' myscript-output.json | jq -s max +``` + +{{< /code >}} + +For more advanced cases, check out the [jq Manual][jq_manual_url] + +[jq_url]: https://stedolan.github.io/jq/ 'jq_url' +[jq_filters_url]: https://stedolan.github.io/jq/manual/#Basicfilters 'jq_filters_url' +[jq_manual_url]: https://stedolan.github.io/jq/manual/ 'jq_manual_url' + +## Summary export + +If you want to see only the aggregated data, you can export the end-of-test summary to a JSON file. +For more details, refer to the `handleSummary()` topic in the [end-of-test summary docs](https://grafana.com/docs/k6//results-output/end-of-test). + +## Read more + +- [Metrics](https://grafana.com/docs/k6//using-k6/metrics) diff --git a/docs/sources/v0.50.x/results-output/real-time/netdata.md b/docs/sources/v0.50.x/results-output/real-time/netdata.md new file mode 100644 index 000000000..5ca2d81ba --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/netdata.md @@ -0,0 +1,60 @@ +--- +title: 'Netdata' +description: 'You can send k6 output to Netdata. With this integration, visualize test results with zero configuration, in seconds' +weight: 00 +--- + +# Netdata + +{{% admonition type="warning" %}} + +The built-in StatsD output has been deprecated on k6 v0.47.0. You can continue to use this feature by using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd), and this guide has been updated to include instructions for how to use it. + +For more information on the reason behind this change, you can follow [this issue](https://github.com/grafana/k6/issues/2982) in the k6 repository. + +{{% /admonition %}} + +k6 can send performance testing metrics to [Netdata](https://netdata.cloud). This enables the user to start monitoring their k6 experiments right away, as Netdata is a monitoring tool with: + +- Auto-configuration and auto-detection of data sources +- Automatic organization of metrics into **meaningful** charts and visualization +- Per-second metric granularity + +## Before you begin + +To use the StatsD output option, you have to build a k6 binary using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd). For more details, refer to [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd). + +## Run Netdata + +Netdata runs on many different systems and platforms. The easiest way to download and run Netdata is through the `kickstart` script: + +```bash +bash <(curl -Ss https://my-netdata.io/kickstart.sh) +``` + +Alternatively, you can read more about installing and running Netdata in their [documentation](https://learn.netdata.cloud/docs/get-started/). + +## Setup Netdata for K6 + +Netdata runs a fully functional [StatsD](https://github.com/netdata/netdata/blob/master/collectors/statsd.plugin/README.md) server by default and we have included a default configuration file for k6 metrics. + +## Run the k6 test + +```bash +k6 run --out output-statsd script.js +``` + +Make sure you're using the k6 binary you built with the xk6-output-statsd extension. + +**Caveat**: By default, Netdata binds the StatsD server to `localhost`. That means that if Netdata and k6 are in different hosts, you will need to edit the configuration file of Netdata. + +1. Visit the [StatsD documentation](https://github.com/netdata/netdata/blob/master/collectors/statsd.plugin/README.md) for a reference on the configuration options. We are interested in the `#bind to` option. +2. Use `sudo ./edit-config netdata.conf` from inside the directory where Netdata stores its configuration files (e.g `/etc/netdata/`) and add `bind to=udp:0.0.0.0:8125`. + +## Visualize in Netdata + +Netdata will automatically create charts for your application, as illustrated in the [documentation](https://github.com/netdata/netdata/blob/master/collectors/statsd.plugin/k6.md). + +Simply head over to `localhost:19999` (assuming that you are running Netdata on the same machine) and find the k6 section. If you had opened Netdata before running the experiment, you will need to refresh the dashboard page. + +![Netdata k6 dashboard](/media/docs/k6-oss/netdata-k6-dashboard.png) diff --git a/docs/sources/v0.50.x/results-output/real-time/newrelic.md b/docs/sources/v0.50.x/results-output/real-time/newrelic.md new file mode 100644 index 000000000..1cf082940 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/newrelic.md @@ -0,0 +1,182 @@ +--- +title: 'New Relic' +description: 'You can send k6 output to New Relic. With this integration visualize load test results and correlate them your New Relic telemetry data, create and share reports, and alert on k6 telemetry.' +weight: 00 +slug: 'new-relic' +--- + +# New Relic + +{{% admonition type="warning" %}} + +The built-in StatsD output has been deprecated on k6 v0.47.0. You can continue to use this feature by using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd), and this guide has been updated to include instructions for how to use it. + +For more information on the reason behind this change, you can follow [this issue](https://github.com/grafana/k6/issues/2982) in the k6 repository. + +{{% /admonition %}} + +k6 can send telemetry data to [New Relic](https://newrelic.com/) through the New Relic [StatsD integration](https://docs.newrelic.com/docs/integrations/host-integrations/host-integrations-list/statsd-monitoring-integration-version-2). Within New Relic you can find your k6 performance data alongside your real users data and server side performance. This data can be visualized in dashboards and shared with others, used to compare load impact with system performance, and alert on metrics too. + +This guide covers running the New Relic integration: + +- Run the New Relic StatsD integration +- Run the k6 test +- Visualize k6 telemetry in New Relic + +## Before you begin + +To use the StatsD output option, you have to build a k6 binary using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd). For more details, refer to [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd). + +## Run the New Relic StatsD integration + +To get k6 metrics into New Relic, k6 sends metrics to the New Relic StatsD integration which will take care of collecting, aggregate, format and send the telemetry to the New Relic Telemetry Data Platform. You can run this with or without a New Relic agent. + +Run the New Relic integration as a Docker container with this command: + +{{< code >}} + +```bash +docker run --rm \ + -d --restart unless-stopped \ + --name newrelic-statsd \ + -h $(hostname) \ + -e NR_ACCOUNT_ID= \ + -e NR_API_KEY="" \ + -p 8125:8125/udp \ + newrelic/nri-statsd:latest +``` + +{{< /code >}} + +Replace `` with your [New Relic Account ID](https://docs.newrelic.com/docs/accounts/accounts-billing/account-setup/account-id#:~:text=If%20you%20have%20a%20single,account%20ID%20is%20displayed%20there.) and `` with your [New Relic Insert API Key](https://docs.newrelic.com/docs/insights/insights-data-sources/custom-data/introduction-event-api#register). + +If your account is hosted in the New Relic EU region, then also add this to the above command: `-e NR_EU_REGION=true \` + +The _required_ environment variables used in the above command are: + +| Name | Value | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NR_ACCOUNT_ID` | The Account ID used in New Relic You can find your account ID [here](https://docs.newrelic.com/docs/accounts/accounts-billing/account-setup/account-id#:~:text=If%20you%20have%20a%20single,account%20ID%20is%20displayed%20there.). | +| `NR_API_KEY` | The Insert API Key for your New Relic account to send k6 telemetry to the account ID specified above. You can generate an Insert API key [here](https://docs.newrelic.com/docs/insights/insights-data-sources/custom-data/introduction-event-api#register). | + +_Optional_ environment variables you can use: + +| Name | Value | +| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NR_EU_REGION` | Setting this to `true` tells the integration your account is housed in the New Relic EU region. | +| `TAGS` | Setting tags in key:value format separated by a space lets you further understand your data in New Relic. For example identifying different test runs or machines running the tests. In the docker command add: `-e TAGS="k6Test:myExampleTest someKey:someValue" \`. | +| `NR_LOG_METRICS` | Setting this to `true` activates verbose logging for the integration. | + +### About the New Relic integration + +The New Relic StatsD integration installed above can run standalone. Installing a New Relic agent is optional. + +Everything provided in the command above is enough to send k6 performance metrics to New Relic. You can optionally however [add further configuration](https://docs.newrelic.com/docs/integrations/host-integrations/host-integrations-list/statsd-monitoring-integration-version-2#configure), [further define metrics and their formats](https://docs.newrelic.com/docs/integrations/host-integrations/host-integrations-list/statsd-monitoring-integration-version-2#metric-format) (you can however do this on the New Relic side configuration), [add custom tags](https://docs.newrelic.com/docs/integrations/host-integrations/host-integrations-list/statsd-monitoring-integration-version-2#add-tags), and [create alerts](https://docs.newrelic.com/docs/integrations/host-integrations/host-integrations-list/statsd-monitoring-integration-version-2#alerts). This is covered in the optional table below. + +## Run the k6 test + +Once the integration is running, run the k6 test and send the metrics to the integration with: + +{{< code >}} + +```bash +$ K6_STATSD_ENABLE_TAGS=true k6 run --out output-statsd script.js +``` + +{{< /code >}} + +Make sure you're using the k6 binary you built with the xk6-output-statsd extension. + +You can look at the [StatsD](https://grafana.com/docs/k6//results-output/real-time/statsd) output page for configuration options. + +## Visualisation in New Relic + +As your k6 test is running, k6 is sending performance metrics to the New Relic StatsD integration which in turn is sending these metrics to the New Relic Telemetry Data Platform. These will be prefixed with `k6.` so you can identify them. + +![k6 metrics as seen in the New Relic data explorer](/media/docs/k6-oss/new-relic-data-explorer.png) + +You can visualize the metrics sent from this integration in the [data explorer](https://docs.newrelic.com/docs/insights/use-insights-ui/explore-data/metric-explorer-search-chart-metrics-sent-new-relic-agents) in the top right of New Relic (_query your data_). + +![Sample New Relic k6 dashboard](/media/docs/k6-oss/new-relic-dashboard.png) + +You can also add these metrics to [dashboards](https://docs.newrelic.com/docs/query-your-data/explore-query-data/dashboards/introduction-new-relic-one-dashboards) and [alert on k6 metrics](https://docs.newrelic.com/docs/alerts-applied-intelligence/new-relic-alerts/alert-conditions/create-nrql-alert-conditions). + +### Example NRQL Queries + +{{% admonition type="note" %}} + +New Relic doesn't support calculating percentiles from metric data, which is the data format sent by this k6 output. See [this New Relic forum post](https://discuss.newrelic.com/t/percentiles-of-values-from-metrics-api-with-nrql-not-working/95832) and [the documentation about the metric data type](https://docs.newrelic.com/docs/data-apis/understand-data/metric-data/query-metric-data-type/) for details. + +{{% /admonition %}} + +Here are some example NRQL queries you can easily copy and paste into widgets in a New Relic dashboard, you can however stick with the [chart builder](https://docs.newrelic.com/docs/query-your-data/explore-query-data/query-builder/introduction-query-builder). Find all your k6 Metrics under the metrics tab, prefixed with `k6.` + +**Number of Virtual Users** + +{{< code >}} + +```plain +SELECT latest(k6.vus) FROM Metric TIMESERIES +``` + +{{< /code >}} + +**Max, Median, and Average Request Duration** + +{{< code >}} + +```plain +SELECT max(k6.http_req_duration.summary) AS 'Max Duration', average(k6.http_req_duration.median) AS 'Median', average(k6.http_req_duration.mean) AS 'Avg' FROM Metric TIMESERIES +``` + +{{< /code >}} + +**Rate of Requests** + +{{< code >}} + +```plain +SELECT rate(max(k6.http_reqs), 1 seconds) FROM Metric TIMESERIES +``` + +{{< /code >}} + +**Data Sent and Data Received** + +{{< code >}} + +```plain +SELECT sum(k6.data_received) as 'Data Received', max(k6.data_sent) AS 'Data Sent' FROM Metric TIMESERIES +``` + +{{< /code >}} + +**Histogram bucketing Requests** + +{{< code >}} + +```plain +SELECT histogram(`k6.http_reqs`, 80, 20) FROM Metric +``` + +{{< /code >}} + +**Change in the number of Requests** + +{{< code >}} + +```plain +SELECT derivative(k6.http_reqs, 30 seconds) AS 'Rate /reqs' FROM Metric TIMESERIES +``` + +{{< /code >}} + +**Scrolling List of all k6 Performance Metrics** + +{{< code >}} + +```plain +SELECT uniques(metricName) FROM Metric WHERE metricName LIKE 'k6%' LIMIT MAX +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/results-output/real-time/prometheus-remote-write.md b/docs/sources/v0.50.x/results-output/real-time/prometheus-remote-write.md new file mode 100644 index 000000000..7e7520196 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/prometheus-remote-write.md @@ -0,0 +1,255 @@ +--- +title: 'Prometheus remote write' +description: 'Use the Prometheus remote write output to send test results to any Prometheus remote write endpoint.' +weight: 00 +--- + +# Prometheus remote write + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +[Prometheus remote write](https://prometheus.io/docs/concepts/remote_write_spec/) is a protocol that makes it possible to reliably propagate data in real-time from a sender to a receiver. +It has multiple compatible [implementations](https://prometheus.io/docs/concepts/remote_write_spec/#compatible-senders-and-receivers) and [storage integrations](https://prometheus.io/docs/prometheus/latest/storage/#remote-storage-integrations). + +For instance, when using the `experimental-prometheus-rw` output, k6 can send test-result metrics to the remote-write endpoint and store them in Prometheus. + +The output, during the `k6 run` execution, gets all the generated time-series data points for the [k6 metrics](https://grafana.com/docs/k6//using-k6/metrics). +It then generates the equivalent Prometheus time series and sends them to the Prometheus remote write endpoint. + +## Metrics mapping + +All [k6 metric types](https://grafana.com/docs/k6//using-k6/metrics) are converted into an equivalent [Prometheus metric type](https://prometheus.io/docs/concepts/metric_types/). +The output maps the metrics into time series with Name labels. +As much as possible, k6 respects the [naming best practices](https://prometheus.io/docs/practices/naming) that the Prometheus project defines: + +- All time series are prefixed with the `k6_` namespace. +- All time series are suffixed with the base unit of the sample value (if k6 knows what the base unit is). +- Trends and rates have the relative suffixes, to make them more discoverable. + +| k6 | Prometheus | Name label | +| ------- | ----------------------------------------------------------------------------------------------------------- | -------------------- | +| Counter | Counter | `k6_*_total` | +| Gauge | Gauge | `k6_*_` | +| Rate | Gauge | `k6_*_rate` | +| Trend | [Counter and Gauges (default)](#1-counter-and-gauges) or [Native Histogram](#2-prometheus-native-histogram) | `k6_*_` | + +## Trend metric conversions + +This output provides two distinct mechanisms to send [k6 Trend metrics](https://grafana.com/docs/k6//using-k6/metrics) to Prometheus: + +1. [Counter and Gauge metrics](#1-counter-and-gauges) (default) +1. [Prometheus Native histogram](#2-prometheus-native-histogram) + +Both options provide efficient storage of test results while providing high-precision queries. + +Note that k6 aggregates trend metric data before sending it to Prometheus in both options. The reasons for aggregating data are: + +- Prometheus stores data in a millisecond precision (`ms`), but k6 metrics collect data points with higher accuracy, nanosecond (`ns`). +- A load test could generate vast amounts of data points. High-precision raw data could quickly become expensive and complex to scale and is unnecessary when analyzing performance trends. + +### 1. Counter and gauges + +By default, Prometheus supports [Counter and Gauge Metric types](https://prometheus.io/docs/concepts/metric_types/). Therefore, this option is the default of this output and converts all the k6 `Trend` metrics to Counter and Gauges Prometheus metrics. + +You can configure how to convert all the k6 trend metrics with the [`K6_PROMETHEUS_RW_TREND_STATS` option](#options) that accepts a comma-separated list of stats functions: `count`, `sum`, `min`, `max`, `avg`, `med`, `p(x)`. The default is `p(99)`. + +Given the list of stats functions, k6 converts all trend metrics to the respective math functions as Prometheus metrics. + +For example, `K6_PROMETHEUS_RW_TREND_STATS=p(90),p(95),max` transforms each trend metric into three Prometheus metrics as follows: + +- `k6_*_p90` +- `k6_*_p95` +- `k6_*_max` + +This option provides a configurable solution to represent `Trend` metrics in Prometheus but has the following drawbacks: + +- Convert a k6 `Trend` metric to several Prometheus metrics. +- It is impossible to aggregate some gauge values (especially percentiles). +- It uses a memory-expensive k6 data structure. + +### 2. Prometheus native histogram + +To address the limitations of the previous option, you can convert k6 trend metrics to high-fidelity histograms enabling [Prometheus native histograms](https://prometheus.io/docs/concepts/metric_types/#histogram). + +With this option, each k6 trend metric maps to its corresponding Prometheus histogram metric: `k6_*`. You can then query them using Prometheus histogram functions, such as [histogram_quantile()](https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile). + +{{% admonition type="note" %}} + +🌟 To learn the benefits and outcomes of using Histograms, watch [High-resolution Histograms in Prometheus](https://www.youtube.com/watch?v=F72Tk8iaWeA). + +⚠️ Note that Native Histogram is an experimental feature released in Prometheus v2.40.0, and other remote write implementations might not support it yet. In the future, when Prometheus makes this feature stable, k6 will consider using it as the default conversion method for Trend metrics. + +{{% /admonition %}} + +## Send test metrics to a remote write endpoint + +To use remote write in Prometheus 2.x, enable the feature flag [--web.enable-remote-write-receiver](https://prometheus.io/docs/prometheus/latest/feature_flags/#remote-write-receiver). For remote write storage options, refer to the [Prometheus docs](https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write). + +1. To send k6 metrics to a **remote write endpoint without native histograms**: + + - Set up a running remote write endpoint and ensure k6 can reach it. + + - Run your k6 script with the `--out` flag and the URL of the RW endpoint as follows: + + **Trend stats** + + ```bash + K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \ + k6 run -o experimental-prometheus-rw script.js + ``` + + **HTTP Basic Authentication** + + ```bash + K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \ + K6_PROMETHEUS_RW_USERNAME=USERNAME \ + K6_PROMETHEUS_RW_PASSWORD=PASSWORD \ + k6 run -o experimental-prometheus-rw script.js + ``` + + - Optionally, pass the `K6_PROMETHEUS_RW_TREND_STATS` to gain the ability to query additional stats for trend metrics. The default is `p(99)`. + + {{< code >}} + + ```bash + K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \ + K6_PROMETHEUS_RW_TREND_STATS=p(95),p(99),min,max \ + k6 run -o experimental-prometheus-rw script.js + ``` + + {{< /code >}} + +1. To send k6 metrics to a **remote write endpoint with native histograms**: + + - Enable the feature flag [--enable-feature=native-histograms](https://prometheus.io/docs/prometheus/latest/feature_flags/#native-histograms) in Prometheus 2.40.0 or higher. Set up a running remote write endpoint and ensure k6 can reach it. + + - Run your k6 script with the `--out` flag, enabling the `K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM` option, and the URL of the RW endpoint as follows: + + **Native Histogram** + + ```bash + K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \ + K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true \ + k6 run -o experimental-prometheus-rw script.js + ``` + + **HTTP Basic Authentication** + + ```bash + K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \ + K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true \ + K6_PROMETHEUS_RW_USERNAME=USERNAME \ + K6_PROMETHEUS_RW_PASSWORD=PASSWORD \ + k6 run -o experimental-prometheus-rw script.js + ``` + +When running the previous `k6 run` commands, k6 starts sending time-series metrics to Prometheus. +All the time series have a [`k6_` prefix](#metrics-mapping). +In the Prometheus Web UI, they appear like this: + +![k6 metrics as seen in the Prometheus UI](/media/docs/k6-oss/query-k6-metrics-in-the-prometheus-web-ui.png) + +## Options + +k6 has special options for remote write output. + +| Name | Type | Description | +| -------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `K6_PROMETHEUS_RW_SERVER_URL` | `string` | URL of the Prometheus remote write implementation's endpoint. Default is `http://localhost:9090/api/v1/write` | +| `K6_PROMETHEUS_RW_HEADERS_` | `string` | Additional header to include in the HTTP requests. It can be set using the described format, for example `K6_PROMETHEUS_RW_HEADERS_CUSTOM-HEADER-KEY=custom-header-value`. | +| `K6_PROMETHEUS_RW_HTTP_HEADERS` | A comma-separated list of key-values | Additional headers to include in the HTTP requests. `K6_PROMETHEUS_RW_HTTP_HEADERS=key1:value1,key2:value2`. | +| `K6_PROMETHEUS_RW_PUSH_INTERVAL` | `string` | Interval between the metrics' aggregation and upload to the endpoint. Default is `5s`. | +| `K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM` | `boolean` | If true, maps all the defined trend metrics as [Native Histograms](#2-prometheus-native-histogram). Default is `false`. | +| `K6_PROMETHEUS_RW_TREND_STATS` | list of `string` | If Native Histogram is not enabled, then it defines the stats functions to map for all of the defined trend metrics. It's a comma-separated list of stats functions to include (e.g. `p(90),avg,sum`). Check the trend section to see the list of supported stats. Default is `p(99)`. | +| `K6_PROMETHEUS_RW_INSECURE_SKIP_TLS_VERIFY` | `boolean` | If true, the HTTP client skips TLS verification on the endpoint. Default is `false`. | +| `K6_PROMETHEUS_RW_STALE_MARKERS` | `boolean` | If true, the output at the end of the test marks all the seen time series as stale. Default is `false`. | +| `K6_PROMETHEUS_RW_USERNAME` | `string` | Username for the HTTP Basic authentication at the Prometheus remote write endpoint. | +| `K6_PROMETHEUS_RW_PASSWORD` | `string` | Password for the HTTP Basic authentication at the Prometheus remote write endpoint. | +| `K6_PROMETHEUS_RW_CLIENT_CERTIFICATE` | `string` | A path to the PEM (Privacy-Enhanced Mail) formatted client certificate. | +| `K6_PROMETHEUS_RW_CLIENT_CERTIFICATE_KEY` | `string` | A path to the PEM formatted client private key. | +| `K6_PROMETHEUS_RW_BEARER_TOKEN` | `string` | Sets the Authorization Bearer Token Header. | + +### Stale trend metrics + +This k6 output can mark the time series at the end of the test as stale. +To enable the stale marker option, set the `K6_PROMETHEUS_RW_STALE_MARKERS` environment variable to `true`. + +By default, the metrics are active for 5 minutes after the last flushed sample. +They are automatically marked as stale after. +For details about staleness, refer to the [Prometheus docs](https://prometheus.io/docs/prometheus/latest/querying/basics/#staleness). + +## Time series visualization + +To visualize time series with Grafana, you can use the [Explore UI](https://grafana.com/docs/grafana/latest/explore/) or import any of the existing pre-built dashboards: + +- [k6 Prometheus dashboard by Grafana k6](https://grafana.com/grafana/dashboards/19665-k6-prometheus/) +- [k6 Prometheus (Native Histograms) dashboard by Grafana k6](https://grafana.com/grafana/dashboards/18030-k6-prometheus-native-histograms/) +- [Other public dashboards available from the community](https://grafana.com/grafana/dashboards/?search=k6&dataSource=prometheus) + +If you are a Grafana Cloud user, please refer to the [Grafana Cloud Prometheus docs](https://grafana.com/docs/k6//results-output/real-time/grafana-cloud-prometheus). + +For a local environment, the [`xk6-output-prometheus-remote` repository](https://github.com/grafana/xk6-output-prometheus-remote) includes a docker-compose setup that provisions the `k6 Prometheus` and `k6 Prometheus (Native Histograms)` dashboards: + +![Provisioned k6 Prometheus Dashboards](/media/docs/k6-oss/list-provisioned-prometheus-dashboards.png) + +### Docker compose example + +Clone the repository to get started and follow these steps for using the [docker-compose.yml](https://github.com/grafana/xk6-output-prometheus-remote/blob/main/docker-compose.yml) file that starts _Prometheus_ and _Grafana_: + +1. Start the docker compose environment. + + {{< code >}} + + ```shell + docker compose up -d prometheus grafana + ``` + + {{< /code >}} + + {{< code >}} + + ```shell + # Output + Creating xk6-output-prometheus-remote_grafana_1 ... done + Creating xk6-output-prometheus-remote_prometheus_1 ... done + ``` + + {{< /code >}} + + Prometheus is started with Native Histogram enabled. You can use the same Prometheus instance to receive k6 trend metrics as native histograms or multiple metric stats. + +1. Run the k6 test with one of the options detailed on [Send test metrics to a remote write endpoint](#send-test-metrics-to-a-remote-write-endpoint). + + **Trend stats** + + ```bash + K6_PROMETHEUS_RW_TREND_STATS=p(95),p(99),min,max \ + k6 run -o experimental-prometheus-rw script.js + ``` + + **Native Histograms** + + ```bash + K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true \ + k6 run -o experimental-prometheus-rw script.js + ``` + + Optionally, you can set the `testid` tag as a [wide test tag](https://grafana.com/docs/k6//using-k6/tags-and-groups/#test-wide-tags) to segment metrics into discrete test runs and filter specific test results on the pre-built Grafana dashboards or in PromQL queries. `testid` can be any unique string that let you clearly identify the test run. + + **Trend stats** + + ```bash + K6_PROMETHEUS_RW_TREND_STATS=p(95),p(99),min,max \ + k6 run -o experimental-prometheus-rw --tag testid= script.js + ``` + + **Native Histograms** + + ```bash + K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true \ + k6 run -o experimental-prometheus-rw --tag testid= script.js + ``` + +1. After running the test, visit [http://localhost:3000](http://localhost:3000). If you enabled native histograms, select the **k6 Prometheus (Native Histograms)** dashboard; otherwise, select the **k6 Prometheus** Dashboard. + + ![k6 Prometheus Dashboard](/media/docs/k6-oss/k6-prometheus-dashboard.png) diff --git a/docs/sources/v0.50.x/results-output/real-time/statsd.md b/docs/sources/v0.50.x/results-output/real-time/statsd.md new file mode 100644 index 000000000..9e6fba0c8 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/statsd.md @@ -0,0 +1,66 @@ +--- +title: 'StatsD' +description: 'k6 has a built-in output to a StatsD service.' +weight: 00 +--- + +# StatsD + +{{% admonition type="warning" %}} + +The built-in StatsD output has been deprecated on k6 v0.47.0. You can continue to use this feature by using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd), and this guide has been updated to include instructions for how to use it. + +For more information on the reason behind this change, you can follow [this issue](https://github.com/grafana/k6/issues/2982) in the k6 repository. + +{{% /admonition %}} + +k6 can push test metrics to a [StatsD](https://github.com/statsd/statsd) service by using the [xk6-output-statsd extension](https://github.com/LeonAdato/xk6-output-statsd). + +## Build the k6 version + +To build a k6 binary with the extension, first, make sure you have [Go](https://golang.org/doc/install) and [Git](https://git-scm.com/) installed on your machine. + +Then, open your terminal and run the following commands: + +```bash +# Install xk6 +go install go.k6.io/xk6/cmd/xk6@latest + +# Build the k6 binary +xk6 build --with github.com/grafana/xk6-output-statsd + +... [INFO] Build environment ready +... [INFO] Building k6 +... [INFO] Build complete: ./k6 +``` + +xk6 will create the new k6 binary in the local folder. + +{{% admonition type="note" %}} + +To learn more about how to build custom k6 versions, check out [xk6](https://github.com/grafana/xk6). + +{{% /admonition %}} + +## Run the k6 test + +Using the k6 binary you built in the previous step, you can use the `--out output-statsd` option when running your tests to use this extension: + +{{< code >}} + +```bash +$ ./k6 run --out output-statsd script.js +``` + +{{< /code >}} + +The following options can be configured: + +| Name | Value | +| ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| `K6_STATSD_ADDR` | Address of the statsd service, currently only UDP is supported. The default value is `localhost:8125`. | +| `K6_STATSD_NAMESPACE` | The namespace used as a prefix for all the metric names. The default value is `k6`. | +| `K6_STATSD_PUSH_INTERVAL` | Configure how often data batches are sent. The default value is `1s`. | +| `K6_STATSD_BUFFER_SIZE` | The buffer size. The default value is `20`. | +| `K6_STATSD_ENABLE_TAGS` | If `true` enables sending tags. `false` by default as old versions of statsd, prior to v0.9.0 did not support tags. | +| `K6_STATSD_TAG_BLOCKLIST` | This is a comma-separated list of tags that should NOT be sent to statsd. For example, "tag1,tag2". The default value is `vu,iter,url`. | diff --git a/docs/sources/v0.50.x/results-output/real-time/timescaledb.md b/docs/sources/v0.50.x/results-output/real-time/timescaledb.md new file mode 100644 index 000000000..658abb03e --- /dev/null +++ b/docs/sources/v0.50.x/results-output/real-time/timescaledb.md @@ -0,0 +1,74 @@ +--- +title: 'TimescaleDB' +description: k6 has an output extension to store k6 metrics in TimescaleDB. This document shows you how to configure the k6 TimescaleDB integration. +weight: 00 +--- + +# TimescaleDB + +Using the [TimescaleDB k6 extension](https://github.com/grafana/xk6-output-timescaledb), you can store k6 metrics in [TimescaleDB](https://www.timescale.com/) and analyze your performance results with SQL and dashboards. The extension repository includes two Grafana dashboards. + +## Build the k6 version + +To build a k6 binary with the extension, first, make sure you have [Go](https://golang.org/doc/install) and [Git](https://git-scm.com/) installed on your machine. + +Then, open your terminal and run the following commands: + +```bash +# Install xk6 +go install go.k6.io/xk6/cmd/xk6@latest + +# Build the k6 binary +xk6 build --with github.com/grafana/xk6-output-timescaledb + +... [INFO] Build environment ready +... [INFO] Building k6 +... [INFO] Build complete: ./k6 +``` + +xk6 will create the new k6 binary in the local folder. + +{{% admonition type="note" %}} + +To learn more about how to build custom k6 versions, check out [xk6](https://github.com/grafana/xk6). + +{{% /admonition %}} + +## Run the test + +Check that the TimescaleDB instance is running. Copy the Postgres connection string of the database, which will store the k6 metrics. + +Use the previous k6 binary and run the test passing the Postgres connection string to the [output option](https://grafana.com/docs/k6//using-k6/k6-options/reference#results-output) as follows: + +```bash +k6 run script.js -o timescaledb=postgresql://:@:/ +``` + +k6 runs the test script and sends the metrics in real-time to the TimescaleDB instance. You can now connect to TimescaleDB and query the [k6 metrics](https://grafana.com/docs/k6//using-k6/metrics). + +```bash +k6=# SELECT metric,AVG (value) FROM samples GROUP BY metric; +``` + +### Options + +Here is the full list of options that can be configured and passed to the extension: + +| Name | Value | +| ------------------------------ | ------------------------------------------------------------------------------------- | +| `K6_TIMESCALEDB_PUSH_INTERVAL` | Define how often metrics are sent to TimescaleDB. The default value is 1s (1 second). | + +## Grafana Dashboards + +The extension repository includes a [docker-compose setup](https://github.com/grafana/xk6-output-timescaledb/#docker-compose) with two pre-built dashboards to: + +- list test runs +- visualize the results of a test run + +![TimescaleDB list test runs](/media/docs/k6-oss/timescaledb-dashboard-test-runs.png) + +![TimescaleDB k6 results](/media/docs/k6-oss/timescaledb-dashboard-test-result.png) + +## Read more + +- [Store k6 metrics in TimescaleDB and visualize with Grafana](https://k6.io/blog/store-k6-metrics-in-timescaledb-and-visualize-with-grafana/) diff --git a/docs/sources/v0.50.x/results-output/web-dashboard/_index.md b/docs/sources/v0.50.x/results-output/web-dashboard/_index.md new file mode 100644 index 000000000..0b396a5e9 --- /dev/null +++ b/docs/sources/v0.50.x/results-output/web-dashboard/_index.md @@ -0,0 +1,85 @@ +--- +title: Web dashboard +description: Track test results in real-time with the web-dashboard and generate HTML test reports directly from your web browser. +weight: 200 +--- + +# Web dashboard + +k6 provides a built-in web dashboard that you can enable to visualize and monitor your tests results in real-time. + +![Web dashboard screenshot](/media/docs/k6-oss/web-dashboard-overview.png) + +The dashboard provides a real-time overview of the performance observed by k6 while a test is running, and can help you identify potential reliability issues as they occur. + +## How to use + +The web dashboard is a built-in feature of k6. You can enable it by setting the `K6_WEB_DASHBOARD` environment variable to `true` when running your test script, for example: + +```shell +K6_WEB_DASHBOARD=true k6 run script.js +``` + +```shell +K6_WEB_DASHBOARD=true ./k6 run script.js + + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: ../extensions/xk6-dashboard/script.js + web dashboard: http://127.0.0.1:5665 + output: - +``` + +By default, the web dashboard is available on localhost port `5665`. You can change the host and port by using the [dashboard options](#dashboard-options). + +{{% admonition type="note" %}} + +The k6 process waits to exit as long as there's at least one open browser window for the dashboard extension. + +In certain environments, such as a CI/CD pipeline, the k6 process has to exit after the test run completes. In that case, it's advisable to disable the HTTP port by setting it to `-1`. + +{{% /admonition %}} + +## Generate HTML test reports + +You can generate detailed, downloadable HTML reports directly from the web dashboard or the command line. These reports are self-contained, making them ideal for sharing with your team. + +![HTML test report screenshot](/media/docs/k6-oss/web-dashboard-report.png) + +### Generate report from web dashboard + +To generate a report from the web dashboard, click **Report** on the dashboard's menu. + +![HTML test report generation button](/media/docs/k6-oss/web-dashboard-report-button.png) + +### Generate report from the command line + +To automatically generate a report from the command line once the test finishes running, use the `K6_WEB_DASHBOARD_EXPORT` option. For example: + +```shell +K6_WEB_DASHBOARD=true K6_WEB_DASHBOARD_EXPORT=html-report.html k6 run script.js +``` + +{{< admonition type="note" >}} + +The report only includes graphs if the test duration is greater than three times the aggregation period value, set by the `K6_WEB_DASHBOARD_PERIOD` variable. + +{{< /admonition >}} + +## Dashboard options + +The web dashboard can be configured using environment variables: + +| Environment variable | Description | Default value | +| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `K6_WEB_DASHBOARD` | Enable the web dashboard | `false` | +| `K6_WEB_DASHBOARD_HOST` | Host to bind the web dashboard to | `localhost` | +| `K6_WEB_DASHBOARD_PORT` | Port to bind the web dashboard to | `5665` | +| `K6_WEB_DASHBOARD_PERIOD` | Period in seconds to update the web dashboard | `10s` | +| `K6_WEB_DASHBOARD_OPEN` | Open the web dashboard in the default browser | `false` | +| `K6_WEB_DASHBOARD_EXPORT ` | Filename to automatically export the HTML test report to at the end of the test run. By default, the value is empty and the report isn't exported. | `` | diff --git a/docs/sources/v0.50.x/shared/blocking-aws-blockquote.md b/docs/sources/v0.50.x/shared/blocking-aws-blockquote.md new file mode 100644 index 000000000..ccb5a9445 --- /dev/null +++ b/docs/sources/v0.50.x/shared/blocking-aws-blockquote.md @@ -0,0 +1,12 @@ +--- +title: jslib/aws module blocking admonition +--- + +{{% admonition type="caution" %}} + +In some cases, using this library's operations might impact performance and skew your test results. +
+
+To ensure accurate results, consider executing these operations in the `setup` and `teardown` [lifecycle functions]({{< relref "../using-k6/test-lifecycle" >}}). These functions run before and after the test run and have no impact on the test results. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/shared/browser-module-wip.md b/docs/sources/v0.50.x/shared/browser-module-wip.md new file mode 100644 index 000000000..29dcaac09 --- /dev/null +++ b/docs/sources/v0.50.x/shared/browser-module-wip.md @@ -0,0 +1,9 @@ +--- +title: k6/experimental/browser module admonition +--- + +{{% admonition type="caution" %}} + +This API is a work in progress. Some of the following functionalities might behave unexpectedly. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/shared/crypto-module.md b/docs/sources/v0.50.x/shared/crypto-module.md new file mode 100644 index 000000000..48b7c92a2 --- /dev/null +++ b/docs/sources/v0.50.x/shared/crypto-module.md @@ -0,0 +1,12 @@ +--- +title: k6/crypto module admonition +--- + +{{% admonition type="note" %}} + +A module with a better and standard API exists. +
+
+The new [k6/experimental/webcrypto API]({{< relref "../javascript-api/k6-experimental/webcrypto" >}}) partially implements the [WebCryptoAPI](https://www.w3.org/TR/WebCryptoAPI/), supporting more features than [k6/crypto]({{< relref "../javascript-api/k6-crypto" >}}). + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/shared/experimental-grpc-module.md b/docs/sources/v0.50.x/shared/experimental-grpc-module.md new file mode 100644 index 000000000..91baf5bbf --- /dev/null +++ b/docs/sources/v0.50.x/shared/experimental-grpc-module.md @@ -0,0 +1,11 @@ +--- +title: Experimental grpc module admonition +--- + +{{% admonition type="caution" %}} + +Starting on k6 `v0.49`, the experimental module `k6/experimental/grpc` has been graduated, and its functionality is now available in the [`k6/net/grpc` module](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/). The `k6/experimental/grpc` is deprecated and will be removed in `v0.51.0`. + +To migrate your scripts, replace all `k6/experimental/grpc` imports with `k6/net/grpc`. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/shared/experimental-module.md b/docs/sources/v0.50.x/shared/experimental-module.md new file mode 100644 index 000000000..ad8c800e4 --- /dev/null +++ b/docs/sources/v0.50.x/shared/experimental-module.md @@ -0,0 +1,15 @@ +--- +title: Experimental module admonition +--- + +{{% admonition type="caution" %}} + +This is an experimental module. +
+
+While we intend to keep experimental modules as stable as possible, we may need to introduce breaking changes. This could happen at future k6 releases until the module becomes fully stable and graduates as a k6 core module. For more information, refer to the [extension graduation process]({{< relref "../extensions/explanations/extension-graduation" >}}). +
+
+Experimental modules maintain a high level of stability and follow regular maintenance and security measures. Feel free to [open an issue](https://github.com/grafana/k6/issues) if you have any feedback or suggestions. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/shared/experimental-timers-module.md b/docs/sources/v0.50.x/shared/experimental-timers-module.md new file mode 100644 index 000000000..35e7ad0bd --- /dev/null +++ b/docs/sources/v0.50.x/shared/experimental-timers-module.md @@ -0,0 +1,11 @@ +--- +title: Experimental timers module admonition +--- + +{{% admonition type="caution" %}} + +Starting on k6 `v0.50`, the experimental module `k6/experimental/timers` has been graduated, and its functionality is now available in the [`k6/net/timers` module](https://grafana.com/docs/k6//javascript-api/k6-timers/). The `k6/experimental/timers` is deprecated and will be removed in `v0.52.0`. + +To migrate your scripts, replace all `k6/experimental/timers` imports with `k6/net/timers`. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/shared/index.md b/docs/sources/v0.50.x/shared/index.md new file mode 100644 index 000000000..ca03031f1 --- /dev/null +++ b/docs/sources/v0.50.x/shared/index.md @@ -0,0 +1,3 @@ +--- +headless: true +--- diff --git a/docs/sources/v0.50.x/shared/ws-module.md b/docs/sources/v0.50.x/shared/ws-module.md new file mode 100644 index 000000000..24874ed4a --- /dev/null +++ b/docs/sources/v0.50.x/shared/ws-module.md @@ -0,0 +1,15 @@ +--- +title: k6/ws module admonition +--- + +{{% admonition type="note" %}} + +A module with a better and standard API exists. +
+
+The new [k6/experimental/websockets API]({{< relref "../javascript-api/k6-experimental/websockets" >}}) partially implements the [WebSockets API living standard](https://websockets.spec.whatwg.org/). +
+
+When possible, we recommend using the new API. It uses a global event loop for consistency with other k6 APIs and better performance. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/testing-guides/_index.md b/docs/sources/v0.50.x/testing-guides/_index.md new file mode 100644 index 000000000..49de7c56c --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/_index.md @@ -0,0 +1,11 @@ +--- +title: 'Testing guides' +description: 'A series of guides to help you defining your load testing strategies.' +weight: 07 +--- + +# Testing guides + +This section provides a list of frequently asked guides to help you set your testing strategy for common cases. + +{{< section >}} diff --git a/docs/sources/v0.50.x/testing-guides/api-load-testing.md b/docs/sources/v0.50.x/testing-guides/api-load-testing.md new file mode 100644 index 000000000..b29d5e8ef --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/api-load-testing.md @@ -0,0 +1,629 @@ +--- +title: 'API load testing' +head_title: 'Intro to API Load Testing: The k6 Guide' +description: 'Load testing APIs has many facets. This guide introduces you to performance testing and provides best practices to load test your APIs with k6.' +weight: 01 +noindex: true +--- + +# API load testing + +An API load test generally starts with small loads on isolated components. +As your testing matures, your strategy can consider how to test the API more completely. +In this process, you'll test your API with more requests, longer durations, and on a wider test scope—from isolated components to complete end-to-end workflows. + +When you design your API tests, first consider _why_ you want to test the API at all: + +- What flows or components do you want to test? +- How will you run the test? +- What criteria determine acceptable performance? + +Once you can answer these questions, your API testing strategy will likely follow something like this procedure: + +1. **Script the test.** Write user flows, parameterize test data, and group URLs. +1. **Assert performance and correctness.** Use Checks to assert system responses and use Thresholds to ensure that the system performs within your SLOs. +1. **Model and generate load.** Choose the executors to correctly model the workload that's appropriate to your test goals. Make sure the load generators are located where they should be. +1. **Iterate over your test suite.** Over time, you'll be able to reuse script logic (e.g., a user log-in flow or a throughput configuration). You'll also be able to run tests with a wider scope or as a part of your automated testing suite. + +The following sections provide specific explanations and examples of the steps in this process. + +## Identify the components to test + +Before you start testing, identify the components you want to test. +Do you want to test a single endpoint or an entire flow? + +The following script uses the [k6 HTTP module](https://grafana.com/docs/k6//javascript-api/k6-http/) to test a single endpoint. + +```javascript +import http from 'k6/http'; + +export default function () { + const payload = JSON.stringify({ + name: 'lorem', + surname: 'ipsum', + }); + const headers = { 'Content-Type': 'application/json' }; + http.post('https://httpbin.test.k6.io/post', payload, { headers }); +} +``` + +This is a minimal test, with one call to one component. +Generally, your test suite will progress from scripts like this to more complex and complete workflows. +In this process, your test suite will advance through the [testing pyramid](https://martinfowler.com/articles/practical-test-pyramid.html) as follows: + +- **Testing an isolated API**. Hammering an API endpoint like [ab](https://httpd.apache.org/docs/2.4/programs/ab.html) to test the baseline performance, breaking point, or availability. If a component doesn’t meet performance requirements, it is a bottleneck. Generally, the load is set in requests per second. +- **Testing integrated APIs**. Testing one or multiple APIs that interact with other internal or external APIs. Your focus might be on testing one system or various. +- **Testing end-to-end API flows**. Simulating realistic interactions between APIs to test the system as a whole. The focus is often on frequent and critical user scenarios. + +Your load test suite should include a wide range of tests. +But, when you start, start small and simple, +testing individual APIs and uncomplicated integration tests. + +## Determined the reason for the test + +Before you configure test load, you should know what traffic patterns you want to test the API for. +A load test typically aims to do one of two things: + +- Validate reliability under expected traffic +- Discover problems and system limits under unusual traffic. + +For example, your team might create one set of tests for frequent user flows on average traffic, and another set to find breaking points in the API. +Even if the test logic stays the same, its load might change. + +The test goal determines the test type, which in turn determines the test load. +Consider the following test types, which correspond to different goals load profiles: + +- [Smoke test](https://grafana.com/docs/k6//testing-guides/test-types/smoke-testing). Verify the system functions with minimal load. +- [“Average” load test](https://grafana.com/docs/k6//testing-guides/test-types/load-testing). Discover how the system functions with typical traffic. +- [Stress test](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing). Discover how the system functions with the load of peak traffic. +- [Spike test](https://grafana.com/docs/k6//testing-guides/test-types/spike-testing). Discover how the system functions with sudden and massive increases in traffic. +- [Breakpoint test](https://grafana.com/docs/k6//testing-guides/test-types/breakpoint-testing). Progressively ramp traffic to discover system breaking points. +- [Soak test](https://grafana.com/docs/k6//testing-guides/test-types/soak-testing). Discover whether or when the system degrades under loads of longer duration. + +The test types that you choose inform how you plan and structure your test. +But each application, organization, and testing project differs. +Our recommendation is always: + +> **"Start simple and test frequently. Iterate and grow the test suite".** + +Once you've decided on the load profile, you can schedule it with k6 options. + +## Model the workload + +To configure the workload, use [test options](https://grafana.com/docs/k6//using-k6/k6-options/). +The test load configures the traffic generated by the test. k6 provides two broad ways to model load: + +- Through _virtual users_ (VUs), to simulate concurrent users +- Through _requests per second_, to simulate raw, real-world throughput + +{{% admonition type="note" %}} + +Generally, your load tests should add [sleep time](https://grafana.com/docs/k6//javascript-api/k6/sleep). +Sleep time helps control the load generator and better simulates the traffic patterns of human users. + +However, when it comes to API load tests, these recommendations about sleep come with a few qualifications. +If testing an isolated component, you might care only about performance under a pre-determined throughput. +But, even in this case, sleep can help you avoid overworking the load generator, and including a few randomized milliseconds of sleep can avoid accidental concurrency. + +When testing the API against normal, human-run workflows, add sleep as in a normal test. + +{{% /admonition %}} + +### Virtual users + +When you model load according to VUs, the basic load options are: + +- [`vus`](https://grafana.com/docs/k6//using-k6/k6-options/reference/#vus) +- [`duration`](https://grafana.com/docs/k6//using-k6/k6-options/reference/#duration) +- [`iterations`](https://grafana.com/docs/k6//using-k6/k6-options/reference/#iterations) + +You can define these options in the test script. In the following test, 50 concurrent users continuously run the `default` flow for 30 seconds. + +```javascript +import http from 'k6/http'; + +export const options = { + vus: 50, + duration: '30s', +}; + +export default function () { + const payload = JSON.stringify({ + name: 'lorem', + surname: 'ipsum', + }); + const headers = { 'Content-Type': 'application/json' }; + http.post('https://httpbin.test.k6.io/post', payload, { headers }); +} +``` + +### Request rate + +When analyzing API endpoint performance, the load is generally reported by request rate—either requests per second or per minute. + +To configure workloads according to a target request rate, use the [constant arrival rate executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-arrival-rate). + +`constant-arrival-rate` sets a constant rate of iterations that execute the script function. +Each iteration can generate one or multiple requests. + +To reach a request-rate target (`RequestsRate`), follow this approach: + +1. Set the rate frequency to the time unit of the target. Per second or per minute. +1. Get the number of requests per iteration (`RequestsPerIteration`). +1. Set the iteration rate to the requests per second target divided by the number of requests per iteration. + `rate` = `RequestsRate ÷ RequestsPerIteration`. + +To reach target of 50 reqs/s with the previous example: + +1. Set the `timeUnit` options to `1s`. +1. The number of requests per iteration is 1. +1. Set the `rate` option to 50/1 (so it equals 50). + +```javascript +import http from 'k6/http'; + +export const options = { + scenarios: { + my_scenario1: { + executor: 'constant-arrival-rate', + duration: '30s', // total duration + preAllocatedVUs: 50, // to allocate runtime resources preAll + + rate: 50, // number of constant iterations given `timeUnit` + timeUnit: '1s', + }, + }, +}; + +export default function () { + const payload = JSON.stringify({ + name: 'lorem', + surname: 'ipsum', + }); + const headers = { 'Content-Type': 'application/json' }; + http.post('https://httpbin.test.k6.io/post', payload, { headers }); +} +``` + +This test outputs the total number of HTTP requests and RPS on the `http_reqs` metric: + +```bash +# the reported value is close to the 50 RPS target + http_reqs......................: 1501 49.84156/s + +# the iteration rate is the same as rps, because each iteration runs only one request +iterations.....................: 1501 49.84156/s +``` + +For a more extensive example, refer to this post about [generating a constant request rate](https://k6.io/blog/how-to-generate-a-constant-request-rate-with-the-new-scenarios-api/) + +With the `constant-arrival-rate` executor, load is constant through the test. +To ramp the request rate up or down, use the [`ramping-arrival-rate`](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-arrival-rate) executor instead. + +For all ways to model the load in k6, refer to [Scenarios](https://grafana.com/docs/k6//using-k6/scenarios). + +## Verify functionality with Checks + +Traditionally, performance tests care most about: + +- _Latency_, how fast the system responds +- _Availability_, how often the system returns errors. + +The `http_req_duration` metric reports the latency, +and `http_req_failed` reports the error rate for HTTP requests. +The previous test run provided the following results: + +```bash +http_req_duration..............: avg=106.14ms min=102.54ms med=104.66ms max=198.93ms p(90)=113.78ms p(95)=114.58ms + { expected_response:true }...: avg=106.14ms min=102.54ms med=104.66ms max=198.93ms p(90)=113.78ms p(95)=114.58ms +http_req_failed................: 0.00% ✓ 0 ✗ 1501 +``` + +Your test analysis might need to go beyond what's available with default metrics. +For more meaningful results analysis, you might also want to validate functionalities and report errors. + +Some application failures happen only under certain load conditions, such as high traffic. +These errors are hard to find. +To find the cause of failures more quickly, instrument your APIs and verify that requests get the expected responses. +To verify application logic in k6, you can use _Checks_. + +[Checks](https://grafana.com/docs/k6//using-k6/checks) validate conditions during the test execution. +For example, you can use checks verify and track API responses. +With checks, you can confirm expected API responses, such as the HTTP status or any returned data. + +Our script now verifies the HTTP response status, headers, and payload. + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +export const options = { + scenarios: { + my_scenario1: { + executor: 'constant-arrival-rate', + duration: '30s', // total duration + preAllocatedVUs: 50, // to allocate runtime resources + + rate: 50, // number of constant iterations given `timeUnit` + timeUnit: '1s', + }, + }, +}; + +export default function () { + const payload = JSON.stringify({ + name: 'lorem', + surname: 'ipsum', + }); + const headers = { 'Content-Type': 'application/json' }; + const res = http.post('https://httpbin.test.k6.io/post', payload, { headers }); + + check(res, { + 'Post status is 200': (r) => res.status === 200, + 'Post Content-Type header': (r) => res.headers['Content-Type'] === 'application/json', + 'Post response name': (r) => res.status === 200 && res.json().json.name === 'lorem', + }); +} +``` + +In this snippet, all checks succeeded: + +```bash +my_scenario1 ✓ [======================================] 00/50 VUs 30s 50.00 iters/s + ✓ Post status is 200 + ✓ Post Content-Type header + ✓ Post response name +``` + +After the load increased to 300 requests per second, the results returned 8811 successful requests and 7 failures: + +```bash +my_scenario1 ✓ [======================================] 000/300 VUs 30s 300.00 iters/s + ✗ Post status is 200 + ↳ 99% — ✓ 8811 / ✗ 7 + ✗ Post Content-Type header + ↳ 99% — ✓ 8811 / ✗ 7 + ✗ Post response name + ↳ 99% — ✓ 8811 / ✗ 7 +``` + +By default, a failed check doesn't fail or abort the test. +In this regard, a check differs from how assertions work for other types of testing. +A load test can run thousands or millions of script iterations, each with dozens of assertions. + +**Some rate of failure is acceptable**, as determined by your SLO's "number of nines" or your organization's error budget. + +## Test your reliability goals with Thresholds + +Every test should have a goal. +Engineering organizations set their reliability goals using [Service Level Objectives](https://en.wikipedia.org/wiki/Service-level_objective) (SLOs) to validate availability, performance, or any performance requirements. + +SLOs maybe defined at distinct scopes, such as on the level of an infrastructure component, of an API, or of the entire application. +Some example SLOs could be: + +- That 99% of APIs returning product information respond in less than 600ms. +- That 99.99% of failed log-in requests respond in less than 1000ms. + +**Design your load tests with pass/fail criteria to validate SLOs**, reliability goals, or other important metrics. +To ensure your system achieves its SLOs, test them frequently, both in pre-production and production environments. + +In k6, you can use [Thresholds](https://grafana.com/docs/k6//using-k6/thresholds) to set the test pass/fail criteria. +This script codifies two SLOs in the `thresholds` object, one about error rate (availability) and one about request duration (latency). + +```javascript +export const options = { + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms + }, + scenarios: { + my_scenario1: { + executor: 'constant-arrival-rate', + duration: '30s', // total duration + preAllocatedVUs: 50, // to allocate runtime resources + + rate: 50, // number of constant iterations given `timeUnit` + timeUnit: '1s', + }, + }, +}; +``` + +When k6 runs a test, the test output indicates whether the metrics were within the thresholds, ✅, or whether they crossed them, ❌. +In this output, the test met both thresholds. + +```bash +✓ http_req_duration..............: avg=104.7ms min=101.87ms med=103.92ms max=120.68ms p(90)=107.2ms p(95)=111.38ms + { expected_response:true }...: avg=104.7ms min=101.87ms med=103.92ms max=120.68ms p(90)=107.2ms p(95)=111.38ms +✓ http_req_failed................: 0.00% ✓ 0 ✗ 1501 +``` + +When the test fails, the k6 CLI returns a non-zero exit code—a necessary condition for test automation. +As an example of a failed test, here's the output for a test with a threshold that 95 percent of requests finish in under 50ms, `http_req_duration:["p(95)<50"]`: + +```bash +running (0m30.1s), 00/50 VUs, 1501 complete and 0 interrupted iterations +my_scenario1 ✓ [======================================] 00/50 VUs 30s 50.00 iters/s + + ✓ Post status is 200 + ✓ Post Content-Type header + ✓ Post response name + + checks.........................: 100.00% ✓ 4503 ✗ 0 + data_received..................: 1.3 MB 45 kB/s + data_sent......................: 313 kB 10 kB/s + http_req_blocked...............: avg=9.26ms min=2µs med=14µs max=557.32ms p(90)=25µs p(95)=46µs + http_req_connecting............: avg=3.5ms min=0s med=0s max=113.46ms p(90)=0s p(95)=0s + ✗ http_req_duration..............: avg=105.14ms min=102.01ms med=103.86ms max=171.56ms p(90)=112.4ms p(95)=113.18ms + { expected_response:true }...: avg=105.14ms min=102.01ms med=103.86ms max=171.56ms p(90)=112.4ms p(95)=113.18ms + ✓ http_req_failed................: 0.00% ✓ 0 ✗ 1501 + http_req_receiving.............: avg=202.86µs min=17µs med=170µs max=4.69ms p(90)=264µs p(95)=341µs + http_req_sending...............: avg=97.56µs min=11µs med=63µs max=5.56ms p(90)=98µs p(95)=133µs + http_req_tls_handshaking.......: avg=4.14ms min=0s med=0s max=169.35ms p(90)=0s p(95)=0s + http_req_waiting...............: avg=104.84ms min=101.88ms med=103.6ms max=171.52ms p(90)=112.18ms p(95)=112.85ms + http_reqs......................: 1501 49.834813/s + iteration_duration.............: avg=115.18ms min=102.51ms med=104.66ms max=704.99ms p(90)=113.68ms p(95)=115.54ms + iterations.....................: 1501 49.834813/s + vus............................: 50 min=50 max=50 + vus_max........................: 50 min=50 max=50 + +ERRO[0030] some thresholds have failed +``` + +## Scripting considerations + +If you have scripted tests before, implementing k6 scripts should seem familiar. +k6 tests are written in JavaScript, and the design of the k6 API has similarities with other testing frameworks. + +But, unlike other tests, load tests run their scripts hundreds, thousands, or millions of times. +The presence of load creates a few specific concerns. +When you load test APIs with k6, consider the following aspects of your script design. + +### Data parameterization + +_Data parameterization_ happens when you replace hard-coded test data with dynamic values. +Parameterization makes it easier to manage a load test with varied users and API calls. +A common case for parameterization happens when you want to use different `userID` and `password` values for each virtual user or iteration. + +For example, consider a JSON file with a list of user info such as: + +{{< code >}} + +```json +{ + "users": [ + { "username": "lorem", "surname": "ipsum" }, + { "username": "dolorem", "surname": "ipsum" } + ] +} +``` + +{{< /code >}} + +You can parameterize the users with the [`SharedArray`](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) object as follows: + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; +import { SharedArray } from 'k6/data'; + +const users = new SharedArray('users.json', function () { + return JSON.parse(open('./users.json')).users; +}); + +export const options = {}; + +export default function () { + // now, user data is not the same for all the iterations + const user = users[Math.floor(Math.random() * users.length)]; + const payload = JSON.stringify({ + name: user.username, + surname: user.surname, + }); + + const headers = { 'Content-Type': 'application/json' }; + const res = http.post('https://httpbin.test.k6.io/post', payload, { + headers, + }); + + check(res, { + 'Post status is 200': (r) => res.status === 200, + 'Post Content-Type header': (r) => res.headers['Content-Type'] === 'application/json', + 'Post response name': (r) => res.status === 200 && res.json().json.name === user.username, + }); +} +``` + +To read more about data parameterization, check out the [parameterization examples](https://grafana.com/docs/k6//examples/data-parameterization) and [Execution context variables](https://grafana.com/docs/k6//using-k6/execution-context-variables). + +### Error handling and acceptance of failures + +**Remember to implement error handling in the test logic**. +Under sufficiently heavy load, the SUT fails and starts to respond with errors. +Though a test might be designed to induce failures, sometimes we focus on only the best-case scenario and forget the importance of accounting for errors. + +The test script must handle API errors to avoid runtime exceptions and to ensure that it tests how the SUT behaves under saturation according to the test goals. +For example, we could extend our script to do some operation that depends on the result of the previous request: + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; +import { SharedArray } from 'k6/data'; + +const users = new SharedArray('users.json', function () { + return JSON.parse(open('./users.json')).users; +}); + +export const options = {}; + +export default function () { + const user = users[Math.floor(Math.random() * users.length)]; + const payload = JSON.stringify({ + name: user.username, + surname: user.surname, + }); + const headers = { 'Content-Type': 'application/json' }; + const res = http.post('https://httpbin.test.k6.io/post', payload, { + headers, + }); + + check(res, { + 'Post status is 200': (r) => res.status === 200, + 'Post Content-Type header': (r) => res.headers['Content-Type'] === 'application/json', + 'Post response name': (r) => res.status === 200 && res.json().json.name === user.username, + }); + + if (res.status === 200) { + // enters only successful responses + // otherwise, it triggers an exception + const delPayload = JSON.stringify({ name: res.json().json.name }); + http.patch('https://httpbin.test.k6.io/patch', delPayload, { headers }); + } +} +``` + +### Test reuse and modularization + +Load testing can be vast in scope, and it may involve different types of tests. +Generally, teams start with simple or critical load tests and continue adding tests for new use cases, user flows, traffic patterns, features, systems, etc. + +In this process, load testing suites grow over time. +To minimize repetitive work, try to **reuse test scripts early** and to **modularize test functions and logic.** +If you script common scenarios in reusable modules, it's easier to create different types of load tests. +The process of creating a new load test goes like this: + +1. Create a new test file. +1. Configure the specific load and other options. +1. Import the scenario. + +As your testing matures, consider creating tests that [combine multiple scenarios](https://grafana.com/docs/k6//using-k6/scenarios/advanced-examples/#combine-scenarios) to simulate more diverse traffic. + +### Dynamic URLs for one endpoint + +By default, when you access the same API endpoint with different URLs―for example, `http://example.com/posts/${id}`―k6 reports the endpoint results separately. +This may create an unnecessary amount of metrics. + +To group the results of the endpoint, use [URL grouping](https://grafana.com/docs/k6//using-k6/http-requests/#url-grouping). + +## Load generator locations + +When you plan the test, consider the locations of your _load generators_, the machines that run the test. +Sometimes, running the test from a specific location is a test requirement. +Other times, you might just choose the location based on convenience or practicality. +Either way, when you set the location of the load generator, keep the following in mind: + +- **Required locations.** To compare performance or ensure accurate results, some load tests need to measure the latency from specific locations. These tests launch the load generators from locations that match their user's region. +- **Optional locations.** Other tests try to measure against a performance baseline—how the system performance changes from a particular performance status or time. To avoid skewed latency results, ensure that the location of the load generator is constant across test tuns, and avoid running the tests from locations that are too close to the SUT. + +### Internal APIs + +End-to-end API tests try to replicate real-world user flows, which access public APIs from external systems. +Other APIs are internal and unreachable from outside. +The need to run internal tests is common when testing API integrations and isolated endpoints. + +If the API is in an internal or restricted environment, you can use k6 to test it in a few different ways: + +- Run the test from your private network using the k6 run command or the [Kubernetes operator](https://github.com/grafana/k6-operator). Optionally, store the test results in [k6 Cloud](https://grafana.com/docs/k6//results-output/real-time/cloud) or other [external services](https://grafana.com/docs/k6//results-output/real-time/). +- For cloud tests: + - [Open your firewall](https://grafana.com/docs/grafana-cloud/k6/reference/cloud-ips/) for cloud test traffic. + - Run the cloud test from your [Kubernetes clusters](https://grafana.com/docs/grafana-cloud/k6/author-run/private-load-zone-v2/). + +## Supplementary tools + +You might want to use k6 in conjunction with other API tools. + +### Integrate with API tools + +The tooling around REST APIs is vast, but there's not much focus on performance testing. +k6 provides a few converters to +help you incorporate the wider API tooling ecosystem into your load tests: + +- [Postman-to-k6 converter](https://github.com/apideck-libraries/postman-to-k6): to create a k6 test from a Postman collection. + + ```bash + postman-to-k6 collection.json -o k6-script.js + ``` + +- [OpenAPI k6 generator](https://k6.io/blog/load-testing-your-api-with-swagger-openapi-and-k6/#api-load-testing-with-swaggeropenapi-specification): to create a k6 test from an Open API (formerly Swagger) definition. + + ```bash + openapi-generator-cli generate -i my-api-spec.json -g k6 + ``` + +These tools generate a k6 test that you can edit and run as usual: + +```bash +k6 run k6-script.js +``` + +Depending on the test type, the converters could help you quickly create your first tests or help onboard new users to k6. +Even so, we recommend you get familiar with the [k6 Javascript API](https://grafana.com/docs/k6//javascript-api) and script your own tests. + +### Using proxy recorders + +Another option is to auto-generate a k6 test from a recorded session. These scripts might help you start building more complex end-to-end and integration tests. + +The [har-to-k6 converter](https://github.com/grafana/har-to-k6) creates the k6 test from a recorded session in HAR format which collects HTTP traffic. + +```bash +har-to-k6 archive.tar -o k6-script.js +``` + +The generated k6 test can be edited and run as usual: + +```bash +k6 run k6-script.js +``` + +To export a recorded session to HAR format, use a proxy recorder such as [Fiddler proxy](https://www.telerik.com/fiddler/fiddler-everywhere) or [GitLab HAR recorder](https://gitlab.com/gitlab-org/security-products/har-recorder/). + +As with the previous converters, the recorder can help prototype tests. +Again, we recommend learning to write your test scripts. + +## Beyond HTTP APIs + +Due to the popularity of the web and REST APIs, this guide has used the term focused on HTTP APIs. But APIs are not restricted to the HTTP protocol. + +By default, k6 supports testing the following protocols: + +- [HTTP/1.1, HTTP/2](https://grafana.com/docs/k6//javascript-api/k6-http/) +- [WebSockets](https://grafana.com/docs/k6//javascript-api/k6-ws/) +- [Redis (experimental)](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis/) +- [gRPC](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/) + +```javascript +import grpc from 'k6/net/grpc'; +import { check, sleep } from 'k6'; + +const client = new grpc.Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('grpcbin.test.k6.io:9001'); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); + + client.close(); + sleep(1); +}; +``` + +But modern software is not built only based on these protocols. Modern infrastructure and applications rely on other API protocols to provide new capabilities or improve their performance, throughput, and reliability. + +To test the performance and capacity of these systems, the testing tool should be able to generate protocol-specific requests against their APIs. + +If k6 doesn't support a protocol you need, you can use (or create) [extensions](https://grafana.com/docs/k6//extensions). +The list of extensions is long: + +- Avro +- ZeroMQ +- Ethereum +- STOMP +- MLLP +- NATS +- and [more](https://grafana.com/docs/k6//extensions/explore). diff --git a/docs/sources/v0.50.x/testing-guides/automated-performance-testing.md b/docs/sources/v0.50.x/testing-guides/automated-performance-testing.md new file mode 100644 index 000000000..741e9036b --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/automated-performance-testing.md @@ -0,0 +1,326 @@ +--- +title: 'Automated performance testing' +head_title: 'How to Automate Performance Testing: The k6 Guide' +description: 'Performance testing automation is about establishing a repeatable and consistent process that checks reliability issues at different stages of the development and release cycle.' +weight: 02 +--- + +# Automated performance testing + +Performance testing automation is about establishing **a repeatable and consistent process that checks reliability issues** at different stages of the development and release cycle. For instance, you could run performance tests from CI/CD pipelines and nightly jobs, or manually trigger load tests and monitor their impact in real-time. + +In performance testing, automation does not remove the need to run tests manually. It’s about planning performance tests as part of your Software Development Life Cycle (SDLC) for **continuous performance testing**. + +This guide provides general recommendations to help you plan and define a strategy for running automated performance tests: + +- Which tests to automate? +- Which environment to test? +- What frequency and how to run tests? +- How to analyze performance results? + +Please note that this guide assumes you are familiar with k6 and already have performance tests. If you are new to performance testing or k6, we recommend looking at our [get-started resources](https://grafana.com/docs/k6//get-started/resources#learning). + +Before we dive in, let's consider the "why" behind automation and how it unlocks the full benefits of your performance testing efforts. + +## Why automate performance tests + +Whether it’s a website loading in under a second, API responses in milliseconds, or instantaneous fault responses, performance is critical as it directly impacts the end-user experience. However, an organizational challenge is that performance may often not receive the recognition of a feature or requirement. + +Performance is still intangible in many organizations, which react only when bad things happen. Automation changes this approach - **from reactive to proactive**. + +In performance testing, it's crucial to establish routines to be consistent in our practices. Automation is necessary to create a performance testing habit, and boost some of its [benefits](https://k6.io/why-your-organization-should-perform-load-testing/), including: + +- **Improve testing coverage, confidence, and maintenance**: Automation creates a constant and iterative process for various types of testing. This continuous effort in performance testing leads to expanded test coverage, enhanced test maintenance, and increased confidence in testing outcomes. +- **Detect issues earlier**: Automating performance tests as part of the software delivery process can ensure applications meet reliability goals while catching issues earlier in the SDLC. +- **Collaborate across teams**: Automation prompts teams to outline a strategy and plan across the SDLC and departments. It fosters engineering leaders to advocate for reliability and implement shared practices. + +Without automation, the lack of a shared framework often leads to isolated and sporadic activities. Automation helps drive continuous performance and reliability testing, introducing a **more efficient and effective testing process**. + +### More than CI/CD + +Automation often refers to running tests with pass/fail conditions as part of the release process within CI/CD pipelines. However, not all performance tests are suited for CI/CD workflows, nor are they solely about providing a Pass/Fail (green/red) status and acting as a release gatekeeper. + +[Automation into CI/CD pipelines](/integrations/#continuous-integration-and-continuous-delivery) is an option, but it's not the only method to schedule the execution of performance tests. When creating a performance testing plan, it’s important to remember that there are different ways to run performance tests in a frequent basis: + +- Cron and cron job runners. +- Cloud testing tools, such as [scheduling in Grafana Cloud k6](https://grafana.com/docs/grafana-cloud/k6/author-run/schedule-a-test/). +- Test management tools with automation capabilities. +- Trigger manual tests. Include this as a step in your release checklist process. + +Running tests only from CI/CD tools on software changes limits the objectives of our performance tests. A complete performance testing strategy can include CI/CD tests, cron-based tests, or even manual tests to address various testing purposes: + +## Determine the purpose of the tests + +The first step in the process is reviewing your existing or planned tests and understanding each test's purpose. Can the test serve additional purposes if executed regularly? Some common goals are: + +- Comparing current performance against an existing performance baseline. +- Understanding variances over time in key performance metrics. Observing flat or changing trends. +- Detecting regressions of new releases. +- Testing Service Level Objectives (SLOs) on a regular basis. +- Testing critical areas during the release process. +- Setting quality gates in the CI/CD pipelines. + +When considering a consistent and ongoing purpose for each test, you discover which tests to automate, any lacking functionality, and missing tests in your test suite. It also guides you in determining the best time to run each test and how. + +## Choose which tests to automate + +Performance tests can generally be divided into two aspects: + +- Test scenario (test case): What is the test verifying? +- Test workload (test load): How much traffic and which traffic pattern? + +Your test suite should incorporate a diverse range of tests that can verify critical areas of your system using distinct [load test types](https://grafana.com/docs/k6//testing-guides/test-types/). + +Any existing test that you wish to run on a frequent basis is a candidate for automation. Fundamentally, automation is about running tests frequently and consistently, whether that's daily, weekly, or annually. + +When designing your performance test suite for automation, consider two key points: start simple and modularize your tests. + +- **Start simple and iterate**: Your test suite, and consequently test coverage, will expand as the team learns and encounters reliability issues to investigate. +- **Modularize your test suite**: In k6, you can separate the scenario and workload logic and reuse them across different tests. That simplifies the process of creating tests with various traffic patterns for different purposes. Modularization also allows reusing common logic across multiple tests. + +When planning test coverage or automation, consider starting with tests that: + +- Verify the core functionality crucial to the product and business. +- Evaluate the performance in scenarios with high traffic. +- Track key performance metrics to observe their trends and compare against their baselines. +- Validate reliability goals or SLOs with Pass/Fail criteria. + +## Model the scenarios and workload + +Once one or multiple tests have been selected, you should determine the various types of traffic that need to be tested. + +Let’s illustrate an example with two simple tests: one test to assess the performance of a GET endpoint and one test to verify a checkout process. + +The next step is to identify the traffic the system under test (SUT) handles for these tests. In this case, we could utilize our analytics and monitoring tools to find the typical traffic patterns for the GET endpoint and checkout flow. + +Depending on the type of traffic, we can create different kinds of tests: + +- [Smoke](https://grafana.com/docs/k6//testing-guides/test-types/smoke-testing): Test for script errors and verify SUT with minimal traffic. +- [Average-Load](https://grafana.com/docs/k6//testing-guides/test-types/load-testing): Test for regular/normal traffic. +- [Stress](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing): Test for maximum expected traffic. +- [Spike](https://grafana.com/docs/k6//testing-guides/test-types/spike-testing): Test for a surge of traffic. +- [Soak](https://grafana.com/docs/k6//testing-guides/test-types/soak-testing): Test for a prolonged period of traffic. + +In our example, we decided on the following workload for the two scenarios: + +| Test scenario | Smoke | Average | Stress | Spike | Soak | +| ---------------- | ------------ | --------------- | ---------------- | ------------ | ---- | +| GET endpoint | 1 iteration | 100 reqs/s - 3m | 1500 reqs/s - 5m | | | +| Checkout process | 3 iterations | 50 VUs - 5m | | 200 VUs - 1m | | + +> We recommend always creating average-load tests for baseline comparisons and smoke tests to validate test script errors before executing larger tests. + +In our example, we have tests that use the same test scenario with distinct workloads. This pattern is extremely common. In this case, the ability to reuse the scenario logic across tests simplifies both test creation and maintenance. A common pattern for organizing tests is prefixing them with the type of workload: + +- `smoke-get-api.js`:   imports the common scenario and set 1 iteration. +- `load-get-api.js`:     imports the common scenario and set 100 reqs/s during 3m. +- `stress-get-api.js`: imports the common scenario and set 1500 reqs/s during 3m. + +_To learn more about configuring workloads in k6, check out [Scenarios](https://grafana.com/docs/k6//using-k6/scenarios#scenario-executors)._ + +## Decide the testing frequency for each environment + +The next step is to decide which environment to test and its frequency. Each organization has different environments, and their purpose might also vary from one organization to another. + +Here are some common environments found at organizations, and general guidelines of what kind of testing to use them for. + +### Development environment + +This environment, whether the personal machine or its dedicated environment, might not include all the components of the system. It is commonly used for preliminary testing before deploying the application to a more comprehensive environment. + +This environment is great for verifying the basic functionality of our tests by running smoke tests. + +In this type of environment, debugging and building our performance tests is more common than any type of automation. However, if your project structure permits, you can also schedule the execution of smoke tests on project changes. + +### QA environment + +This environment often deploys the entire application but with minimal infrastructure resources. It’s like a low-scale staging environment that all teams can use to test functional aspects and find regressions for new features. + +Given the infrastructure does not closely match the production environment, this type of QA environment is unsuitable for assessing the performance and scalability of the application. + +However, validating the functional aspects of our testing with smoke tests can help to catch errors earlier in this environment. Additionally, it verifies that the same script can run in larger load tests later. + +Run all the available smoke tests: end-to-end, integration, and unit test types. Schedule these tests as part of the suite of automated tests executed in the CI flow. + +### Pre-release and ephemeral environments + +These environments are available to test upcoming releases, with each organization using them differently as part of their unique release process. + +As a general rule on pre-release environments, we should run our larger tests with quality gates, Pass/Fail criteria that validate SLOs or reliability goals. In k6, you can do that by using [Thresholds](https://grafana.com/docs/k6//using-k6/thresholds) in `options` as follows: + +```javascript +export const options = { + thresholds: { + // http errors should be less than 1% + 'http_req_failed': ['rate<0.01'], + // 90% of requests should be below 600ms + 'http_req_duration': ['p(90)<600'], + // 95% of requests tagged as static content should be below 200ms + 'http_req_duration{type:staticContent}': ['p(99)<250'], + // the error rate of my custom metric should be below 5% + 'my_custom_metric': ['rate<0.05'], + }, +}; +``` + +However, it can be challenging to effectively assess all reliability goals. Frequently, you’ll encounter “false positives” and “true negatives” when testing with distinct types of load. + +For larger tests, verifying the release based “only” on a Pass/Fail status can create a false sense of security in your performance testing and release process. + +We recommend keeping the pre-release environment available for a few hours or days to thoroughly test the entire system. Our recommendations include: + +- Allocating a period of one to several days for validating the release. +- Executing all the existing average-load, stress, and spike tests. +- Executing each test at least twice consecutively. +- Scheduling all tests to run periodically, for instance, every 4 hours or 8 hours. + +### Staging/pre-production + +In some cases, the staging environment acts like the “Pre-release” environment. If so, follow the strategy mentioned in the previous section. + +The staging environment is always available and consistently updated with the latest changes. It’s generally suitable for assessing performance changes like performance trends, regressions, or improvements. + +In this case, we should choose the tests that assess key performance indicators and schedule them for consistent execution to collect metrics over a period. Start by selecting a few tests and scheduling their runs two to three times per week. + +Like in the pre-release environment, we suggest executing each test at least twice consecutively, allowing us to ignore unreliable tests. + +As we aim to find performance changes, consider scaling the workload of the test according to the staging infrastructure, which often does not match the scale of the production environment. + +### Production + +Typically, the previous testing environments do not perfectly mirror the production environment, with differences in test data, infrastructure resources, and scalability policies. + +Testing in production provides real-world insights that cannot be achieved in other environments. However, production testing requires a careful approach to handling and storing test data in production and avoiding impacting real users. + +A low-risk common practice is to utilize smoke tests for synthetic testing, also called synthetic monitoring. Testing production with minimal load is safe. Schedule smoke tests every five minutes, establishing Pass/Fail test conditions and an effective alerting mechanism. For instance, if six consecutive test runs fail, send an alert. + +If release strategies like Blue/Green or Canary deployments are in place, run load tests against the Green or new version to validate the release. It's an ideal moment to see how SLOs behave in production. + +Also, consider scheduling nightly tests or when the system handles less traffic. The goal is not to stress the system, but to consistently gather performance results to compare changes and analyze performance trends. For instance, schedule tests with half of the average traffic level on a weekly basis. + +### Example plan + +| Test | Deployment Env. | Type | Workload | Automation | Frequency | +| ---------------- | --------------- | -------- | ---------------- | -------------------------------------- | ----------------------------------------- | +| Checkout process | QA | Smoke | 1 iteration | CI flow | Branch changes | +| Checkout process | Pre-release | Average | 50 VUs - 5m | Scheduled during QA/Pre-release period | 3 times per day during pre-release period | +| Checkout process | Pre-release | Spike | 200 VUs - 1m | Scheduled during QA/Pre-release period | 3 times per day during pre-release period | +| Checkout process | Staging | Average | 50 VUs - 5m | Schedule | 2 times per week | +| | | | | | | +| GET endpoint | QA | Smoke | 1 iteration | CI flow | Branch changes | +| GET endpoint | Pre-release | Average | 100 reqs/s - 3m | Scheduled during QA/Pre-release period | 3 times per day during pre-release period | +| GET endpoint | Pre-release | Stress | 1500 reqs/s - 5m | Scheduled during QA/Pre-release period | 3 times per day during pre-release period | +| GET endpoint | Staging | Average | 100 reqs/s - 3m | Schedule | 2 times per week | +| GET endpoint | Production | 50% Avg. | 50 reqs/s - 3m | Schedule on minimal traffic | Weekly | + +## Plan the result analysis process + +Following the previous steps, you should now have an initial performance testing plan. Now, let’s see how we can analyze and interpret performance results. + +The first step is learning what options you have for outputting performance results. If you’re using k6, there are a few [options you can choose from](https://grafana.com/docs/k6//results-output/). You can review those options and the [k6 metrics](https://grafana.com/docs/k6//using-k6/metrics) to decide on a long-term solution to analyze the results of your test automation plan. + +Here are some questions to consider when creating your result analysis process. + +### How to store your performance results + +In k6, you can get the [aggregated results](https://grafana.com/docs/k6//results-output/end-of-test) at the end of a test, or [time series metrics in real-time](https://grafana.com/docs/k6//results-output/real-time). Both options allow you to customize the output. + +The process we recommend is: + +- Select a storage backend. +- Understand how it stores your test data and its particular capabilities. +- Learn how to query and visualize results and any limitations. +- Establish a policy for deleting old test results, and make sure to retain key performance results for future comparisons like baseline performance data. +- Test the solution and decide on a long-term storage choice to avoid frequent changes to this critical component. + +### Which key performance metrics will be your focus + +Think about the goal of each particular test, and make sure you track the metrics that depend on your test goals. + +k6 provides [built-in metrics](https://grafana.com/docs/k6//using-k6/metrics/reference) that aggregate all the interactions against the SUT. You can also utilize [tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#user-defined-tags) and custom metrics to categorize and filter results for one interaction or particular types. + +Consider defining your own performance criteria and their visualization. For instance, set different colors for what is good, acceptable, slightly concerning, or wrong so you can quickly visualize if a particular set of performance results are ok. + +Think about performance changes. Is there any particular metric to compare changes or track its trend over time? Most of the test visualization choices focus on the results of individual test runs. Consider implementing a way to visualize the result of a critical performance metric over time so you can identify any changes and trends. + +### How often will you analyze results + +Consider creating dashboards and custom notifications that can quickly provide an overview of the latest results of any automated tests. These dashboards are the first line to indicate issues requiring investigation. + +Additionally, we recommend setting up alerts for important issues. Think about priority and non-priority levels and follow-up actions. Consider these [tips to design alerts](https://grafana.com/docs/grafana/latest/alerting/#design-your-alerting-system). + +### Correlate testing and observability data + +Last but not least, set up proper instrumentation of the SUT and understand the monitoring and observability in place for system and application data. + +Performance testing results can highlight poor performance, such as slow responses. However, it does not show what happens internally on the SUT, such as a slow SQL query or CPU and memory saturation. + +To bridge the gap, work out a way to correlate testing results with how you instrument your infrastructure and application code. For instance, connecting or building custom dashboards with test results or using [trace data from your tests](https://grafana.com/docs/k6//javascript-api/k6-experimental/tracing). + +Continuous testing helps detect issues and performance degradations, whether from test results or system data. Proper observability will help to find the root cause. + +### Example plan + +As we finalize our planning, we can organize our test plan considering how often tests are run, the options to analyze their results, and follow-up actions. For example: + +| Deployment Env. | Test type | Frequency | Alerts | Test overview | Action | +| --------------- | ---------------------------------------- | --------------------------------------------------- | -------------------------------- | ----------------------------------- | -------------------------------------------------------- | +| QA | Smoke tests | CI on Branch changes | CI | | Fix broken tests | +| Pre-release | All performance tests except smoke tests | 2 or 3 times daily during the QA/pre-release period | | | Validate the release after assessing performance results | +| Staging | Baseline performance tests | Schedule 2 times per week | Only for critical issues | Custom dashboard | Oversee test results | +| Production | Baseline performance tests | Weekly schedule | Priority and Non-priority issues | Custom dashboards and notifications | Oversee test results | +| Production | Synthetic tests | Hourly schedule | Custom alerts | | Respond to alerts | + +## Considerations + +### Start simple and then iterate + +When starting your performance test plan or automation, it is common to think about having a few dozen scenarios to test. Start small to avoid planning paralysis. + +We recommend beginning with a few distinct tests across testing environments. + +Over time, you can add more tests, and your performance test suite will gradually increase its test coverage. + +Focus on proving your test automation plan and solution across the software release process. A successful implementation will pave the way for collaborating with other teams and promoting the value of continuous performance testing. + +### Test consistency is critical + +One of the primary objectives of continuous performance testing is assessing changes in the key metrics that define reliability and performance goals. To achieve this, we need to compare the value of these metrics between test runs over a period. + +It’s critical to compare test run results of the same test. Otherwise, you’re comparing apples with oranges. Compare identical test runs, the same workload, running the same scenario with the same test data against the same environment. + +Make sure not to introduce variance between test runs. If changes are necessary, rename or create a new test and start comparing test results from scratch. + +Additionally, we recommend scheduling the execution of the same test twice and almost consecutively. This collects one extra test run result for better comparison and allows us to ignore a potentially unreliable test. + +### Establish how to stop automated and manual tests + +Certain performance tests, especially those involving heavy-load tests, might cause outages. Automating the execution of risky tests without supervision may not be desirable, but that doesn't mean you should avoid them. + +These tests require controlled test execution and real-time analysis of test results, allowing them to be stopped before the system becomes unresponsive. + +Similarly, you might want to stop a test when the system begins to produce a flood of errors. When a system becomes completely overloaded, continuing the test execution often doesn't provide more meaningful insights and merely consumes resources. + +To stop a k6 test, learn how to use the [`abortOnFail` threshold option](https://grafana.com/docs/k6//using-k6/thresholds#abort) or integrate with the k6 CLI or Grafana Cloud k6. + +### Complement automation with a repeatable QA process + +We mentioned this at the beginning of the guide: automation in performance testing is about establishing a repeatable and consistent testing process. + +You should also plan the frequency of tests that are manually triggered and require supervision of the system during their execution. To ensure these different cases are consistently tested, set reminders and document them as part of the QA process and release checklists. Common examples are: + +- Running soak tests quarterly. +- Running heavy-stress tests 2 months before an important seasonal event. +- Running heavy-load tests for major releases in a pre-release environment. + +### Quality gates in CI/CD may result in false assurance + +Quality gates in performance tests are often defined as [Pass/Fail criteria](https://grafana.com/docs/k6//using-k6/thresholds) that verify the release meets its reliability goals. + +However, setting up reliable quality gates is challenging when testing thousands or millions of interactions. The test script, the SLO for that particular environment, and the Pass/Fail criteria could easily be wrong. + +Assume reliability checks may have false negatives (and vice versa); ensure performance tests don't block releases wrongly. + +Unless your verification process is mature, do not rely entirely on Pass/Fail results to guarantee the reliability of releases. If unsure, start utilizing Pass/Fail results to warn about possible issues for deeper investigation, and continuously tweak the criteria until becoming confident. + +Moreover, note that the load test duration often takes between 3 to 15 minutes or more; thus, introducing performance testing into CI/CD significantly increases the time of the release process. This is another reason we advise not to run larger tests in pipelines meant for automatic deployment. Instead, plan one or more days for performance testing pre-releases in a dedicated environment. diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/_index.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/_index.md new file mode 100644 index 000000000..a88ddb894 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/_index.md @@ -0,0 +1,57 @@ +--- +title: 'Injecting faults with xk6-disruptor' +description: 'xk6-disruptor is a k6 extension providing fault injection capabilities to test system reliability under turbulent conditions.' +weight: 06 +--- + +# Injecting faults with xk6-disruptor + +[xk6-disruptor](https://github.com/grafana/xk6-disruptor) is an extension that adds fault injection capabilities to k6. It implements the principles of the Chaos Engineering discipline to test the reliability of our applications under turbulent conditions such as delays and response errors. + +Key features include: + +- Everything as code. Facilitate test reuse and collaboration between teams without learning a new DSL. +- Fast to adopt with no day-two surprises. [No need to deploy and maintain](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/how--it-works) a fleet of agents or operators. +- Easy to extend and integrate with other types of k6 tests. No need to try to glue multiple tools together to get the job done. + +## Capabilities + +Currently, the disruptor is intended to test applications running in Kubernetes. Other platforms are not supported at this time. + +It provides a Javascript API to inject different [faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults/) in HTTP and gRPC requests, such as errors and delays, into the selected Kubernetes [Pods](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/) or [Services](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor/). + +Other types of faults and disruptors will be introduced in the future. The [Roadmap](https://github.com/grafana/xk6-disruptor/blob/main/ROADMAP.md) presents the project's goals for the coming months regarding new functionalities and enhancements. + +## Use cases + +The disruptor lets you test the resiliency of distributed applications by introducing errors in the requests served by your services. + +The disruptor does not try to reproduce root causes, such as failed application instances or degraded computing or network resources. +It focuses on reproducing the side effects of such failures, so you can focus on understanding the propagation of errors between internal and public services and improving the error handling in your application. + +This way, the disruptor makes reliability tests repeatable and predictable while limiting their blast radius. +These are essentials to test applications deployed on shared infrastructures such as pre-production and testing environments. + +Common use cases are: + +- Test resilient policies such as backoff, timeouts, retries, etc. +- Test the fallback functionality when internal failures arise. +- Test SLOs under common internal failures. +- Test application performance when experiencing delays. +- Add fault injection to existing performance tests. + +## Learn more + +Lear more about [Fault injection](https://k6.io/blog/democratize-chaos-testing/) and [Building Resilience early in the development cycle](https://k6.io/blog/building-resilience-early-in-the-development-cycle/). + +Check the [first steps](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/first-steps) to get started with the disruptor. + +Follow the [examples of injecting faults in different scenarios](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/examples). + +Visit the [interactive demo environment in Killercoda](https://killercoda.com/grafana-xk6-disruptor/scenario/killercoda) and try the disruptor in a demo application without having to do any setup. + +## Contributing + +For any unexpected behavior, please search the [GitHub issues](https://github.com/grafana/xk6-disruptor/issues) first. + +And if you are interested in contributing to the development of this project, check the [contributing guide](https://github.com/grafana/xk6-disruptor/blob/main/docs/01-development/01-contributing.md). diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/_index.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/_index.md new file mode 100644 index 000000000..4b414d3d1 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/_index.md @@ -0,0 +1,18 @@ +--- +title: 'xk6-disruptor examples' +description: 'Examples of how to use the xk6-disruptor extension to introduce faults in k6 tests.' +weight: 06 +--- + +# xk6-disruptor examples + +In this section, we present some examples of using the `xk6-disruptor` extension to introduce faults in `k6` tests. + +- [Injecting gRPC faults into a Service](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-grpc-faults-into-service) +- [Injecting HTTP faults into a Pod](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-http-faults-into-pod) +- [Interactive demo](https://killercoda.com/grafana-xk6-disruptor/scenario/killercoda) (Killercoda) + +To follow the instructions of the examples, check first the system under test meets the [requirements](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/requirements) to receive faults, in particular: + +- You have configured the credentials to access the Kubernetes cluster. +- This cluster exposes the service using an external IP. diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/demo-environment.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/demo-environment.md new file mode 100644 index 000000000..6b907d123 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/demo-environment.md @@ -0,0 +1,8 @@ +--- +title: 'Interactive demo' +weight: 03 +--- + +# Interactive demo + +Visit the [interactive demo environment in Killercoda](https://killercoda.com/grafana-xk6-disruptor/scenario/killercoda) and try the disruptor in a demo application without having to do any setup. diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-grpc-faults-into-service.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-grpc-faults-into-service.md new file mode 100644 index 000000000..b7462ccee --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-grpc-faults-into-service.md @@ -0,0 +1,437 @@ +--- +title: 'Inject gRPC faults into Service' +description: 'This example shows how to test the effect of faults injected in the gRPC requests served by a service.' +weight: 01 +--- + +# Inject gRPC faults into Service + +This example shows a way to use the [ServiceDisruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor) to test the effect of faults injected in the gRPC requests served by a service. + +The complete [source code](#source-code) is at the end of this document. The next sections examine the code in detail. + +The example uses [grpcpbin](https://grpcbin.test.k6.io), a simple request/response application that offers endpoints for testing different gRPC requests. + +The test requires `grpcpbin` to be deployed in a cluster in the namespace `grpcbin` and exposed with an external IP. The IP address is expected in the environment variable `GRPC_HOST`. + +For the Kubernetes manifests and the instructions on how to deploy it to a cluster, refer to the [test setup](#test-setup) section at the end of this document. To learn more about how to get the external IP address, refer to [Expose your application](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/expose--your-application). + +## Initialization + +The initialization code imports the external dependencies required by the test. The [ServiceDisruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor) class imported from the `xk6-disruptor` extension provides functions for injecting faults in services. The [k6/net/grpc](https://grafana.com/docs/k6//javascript-api/k6-net-grpc) module provides functions for executing gRPC requests. The [check](https://grafana.com/docs/k6//using-k6/checks) function verifies the results from the requests. + +```javascript +import { ServiceDisruptor } from 'k6/x/disruptor'; +import grpc from 'k6/net/grpc'; +import { check } from 'k6'; +``` + +We also create a grpc client and load the protobufs definitions for the [HelloService](https://github.com/moul/pb/blob/master/hello/hello.proto) service. + +```javascript +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); + +client.load(['pb'], 'hello.proto'); +``` + +## Test Load + +The test load is generated by the `default` function, which connects to the `grpcbin` service using the IP and port obtained from the environment variable `GRPC_HOST` and invokes the `SayHello` method of the `hello.HelloService` service. Finally, The status code of the response is checked. When faults are injected, this check should fail. + +```javascript +import { check } from 'k6'; +import grpc from 'k6/net/grpc'; + +const client = new grpc.Client(); + +export default function () { + client.connect(__ENV.GRPC_HOST, { + plaintext: true, + timeout: '1s', + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data, { + timeout: '1s', + }); + client.close(); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); +} +``` + +## Fault injection + +The `disrupt` function creates a `ServiceDisruptor` for the `grpcbin` service in the namespace `grpcbin`. + +The gRPC faults are injected by calling the [ServiceDisruptor.injectGrpcFaults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor/injectgrpcfaults) method using a fault definition that introduces a delay of `300ms` on each request and an error with status code `13` ("Internal error") in `10%` of the requests. + +```javascript +import grpc from 'k6/net/grpc'; +import { ServiceDisruptor } from 'k6/x/disruptor'; + +export function disrupt() { + if (__ENV.SKIP_FAULTS == '1') { + return; + } + + const fault = { + averageDelay: '300ms', + statusCode: grpc.StatusInternal, + errorRate: 0.1, + port: 9000, + }; + const disruptor = new ServiceDisruptor('grpcbin', 'grpcbin'); + disruptor.injectGrpcFaults(fault, '30s'); +} +``` + +Notice the following code snippet in the `injectFaults` function above: + +```javascript +export default function () { + if (__ENV.SKIP_FAULTS == '1') { + return; + } +} +``` + +This code makes the function return without injecting faults if the `SKIP_FAULTS` environment variable is passed to the execution of the test with a value of "1". We will use this option to obtain a [baseline execution](#baseline-execution) without faults. + +## Scenarios + +This test defines two [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) to be executed. The `load` scenario applies the test load to the `grpcpbin` application for `30s` invoking the `default` function. The `disrupt` scenario invokes the `disrupt` function to inject a fault in the gRPC requests to the target application. + +```javascript +export const options = { + scenarios: { + load: { + executor: 'constant-arrival-rate', + rate: 100, + preAllocatedVUs: 10, + maxVUs: 100, + exec: 'default', + startTime: '0s', + duration: '30s', + }, + disrupt: { + executor: 'shared-iterations', + iterations: 1, + vus: 1, + exec: 'disrupt', + startTime: '0s', + }, + }, +}; +``` + +{{% admonition type="note" %}} + +The `disrupt` scenario uses a `shared-iterations` executor with one iteration and one `VU`. This setting ensures the `disrupt` function is executed only once. Executing this function multiples times concurrently may have unpredictable results. + +{{% /admonition %}} + +## Executions + +{{% admonition type="note" %}} + +The commands in this section assume the `xk6-disruptor` binary is available in your current directory. This location can change depending on the installation process and the platform. Refer to [Installation](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/installation) for details on how to install it in your environment. + +{{% /admonition %}} + +### Baseline execution + +We will first execute the test without introducing faults to have an baseline using the following command: + +{{< code >}} + +```bash +xk6-disruptor --env SKIP_FAULTS=1 --env GRPC_HOST=$GRPC_HOST run grpc-faults.js +``` + +```windows-powershell +xk6-disruptor --env SKIP_FAULTS=1 --env "GRPC_HOST=$Env:GRPC_HOST" run grpc-faults.js +``` + +{{< /code >}} + +Notice the argument `--env SKIP_FAULT=1`, which makes the `disrupt` function return without injecting any fault as explained in the [fault injection](#fault-injection) section. Also notice the `--env GRPC_HOST` argument, which passes the external IP used to access the `grpcbin` application. + +You should get an output similar to the following (use the `Expand` button to see all output). + +{{< code >}} + +``` + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: scripts/grpc-faults.js + output: - + + scenarios: (100.00%) 2 scenarios, 101 max VUs, 10m30s max duration (incl. graceful stop): + * disrupt: 1 iterations shared among 1 VUs (maxDuration: 10m0s, exec: disrupt, gracefulStop: 30s) + * load: 100.00 iterations/s for 30s (maxVUs: 10-100, exec: default, gracefulStop: 30s) + + +running (00m30.0s), 000/011 VUs, 3001 complete and 0 interrupted iterations +disrupt ✓ [======================================] 1 VUs 00m00.0s/10m0s 1/1 shared iters +load ✓ [======================================] 000/010 VUs 30s 100.00 iters/s + + ✓ status is OK + + checks...............: 100.00% ✓ 3000 ✗ 0 + data_received........: 416 kB 14 kB/s + data_sent............: 630 kB 21 kB/s + grpc_req_duration....: avg=1.36ms min=512.43µs med=1.25ms max=26.62ms p(90)=1.75ms p(95)=2.09ms + iteration_duration...: avg=4.4ms min=52.86µs med=4.01ms max=66.26ms p(90)=5.32ms p(95)=6.35ms + iterations...........: 3001 100.028406/s + vus..................: 10 min=10 max=10 + vus_max..............: 11 min=11 max=11 +``` + +{{< /code >}} + +### Fault injection + +We repeat the execution injecting the faults. Notice we have removed the `--env SKIP_FAULTS=1` argument. + +{{< code >}} + +```bash +xk6-disruptor --env GRPC_HOST=$GRPC_HOST run grpc-faults.js +``` + +```windows-powershell +xk6-disruptor --env "GRPC_HOST=$Env:GRPC_HOST" run grpc-faults.js +``` + +{{< /code >}} + +{{< code >}} + +``` + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: scripts/grpc-faults.js + output: - + + scenarios: (100.00%) 2 scenarios, 101 max VUs, 10m30s max duration (incl. graceful stop): + * disrupt: 1 iterations shared among 1 VUs (maxDuration: 10m0s, exec: disrupt, gracefulStop: 30s) + * load: 100.00 iterations/s for 30s (maxVUs: 10-100, exec: default, gracefulStop: 30s) + + +running (00m50.6s), 000/033 VUs, 2975 complete and 0 interrupted iterations +disrupt ✓ [======================================] 1 VUs 00m50.6s/10m0s 1/1 shared iters +load ✓ [======================================] 000/032 VUs 30s 100.00 iters/s + + ✗ status is OK + ↳ 89% — ✓ 2668 / ✗ 306 + + checks...............: 89.71% ✓ 2668 ✗ 306 + data_received........: 405 kB 8.0 kB/s + data_sent............: 615 kB 12 kB/s + dropped_iterations...: 26 0.513628/s + grpc_req_duration....: avg=270.51ms min=502.12µs med=302.34ms max=310.48ms p(90)=303.25ms p(95)=303.57ms + iteration_duration...: avg=290.05ms min=1.96ms med=304.86ms max=50.61s p(90)=306.36ms p(95)=306.89ms + iterations...........: 2975 58.770842/s + vus..................: 1 min=1 max=33 + vus_max..............: 33 min=25 max=33 +``` + +{{< /code >}} + +### Comparison + +Let's take a closer look at the results for the requests on each scenario. We can observe that in the `base` scenario requests duration has an percentile 95 of `2.09ms` and `100%` of requests pass the check. The `faults` scenario has a percentile 96 of `303.57ms` and only `89%` of requests pass the check (or put in another way, `11%` of requests failed), closely matching the fault definition. + +| Execution | P95 Req. Duration | Passed Checks | +| --------------- | ----------------- | ------------- | +| Baseline | 2.09ms | 100% | +| Fault injection | 303.57ms | 89% | + +## Source Code + +{{< collapse title="grpc-faults.js" >}} + +```javascript +import { ServiceDisruptor } from 'k6/x/disruptor'; +import grpc from 'k6/net/grpc'; +import { check } from 'k6'; + +const client = new grpc.Client(); +client.load(['pb'], 'hello.proto'); + +export default function () { + client.connect(__ENV.GRPC_HOST, { + plaintext: true, + timeout: '1s', + }); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data, { + timeout: '1s', + }); + client.close(); + + check(response, { + 'status is OK': (r) => r && r.status === grpc.StatusOK, + }); +} + +export function disrupt() { + if (__ENV.SKIP_FAULTS == '1') { + return; + } + + const disruptor = new ServiceDisruptor('grpcbin', 'grpcbin'); + + // inject errors in requests + const fault = { + averageDelay: '300ms', + statusCode: grpc.StatusInternal, + errorRate: 0.1, + port: 9000, + }; + disruptor.injectGrpcFaults(fault, '30s'); +} + +export const options = { + scenarios: { + load: { + executor: 'constant-arrival-rate', + rate: 100, + preAllocatedVUs: 10, + maxVUs: 100, + exec: 'default', + startTime: '0s', + duration: '30s', + }, + disrupt: { + executor: 'shared-iterations', + iterations: 1, + vus: 1, + exec: 'disrupt', + startTime: '0s', + }, + }, +}; +``` + +{{< /collapse >}} + +{{< collapse title="pb/hello.proto" >}} + +``` +syntax = "proto2"; + +package hello; + +service HelloService { + rpc SayHello(HelloRequest) returns (HelloResponse); + rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); + rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse); + rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); +} + +message HelloRequest { + optional string greeting = 1; +} + +message HelloResponse { + required string reply = 1; +} +``` + +{{< /collapse >}} + +## Test setup + +The tests requires the deployment of the `grpcbin` application. The application must also be accessible using an external IP available in the `GRPC_HOST` environment variable. + +The following [manifests](#manifests) define the resources required for deploying the application and exposing it as a LoadBalancer service. + +You can deploy the application using the following commands: + +```bash +# Create Namespace +kubectl apply -f namespace.yaml +namespace/grpcbin created + +# Deploy Pod +kubectl apply -f pod.yaml +pod/grpcbin created + +# Expose Pod as a Service +kubectl apply -f service.yaml +service/grpcbin created +``` + +You must set the environment variable `GRPC_HOST` with the external IP address and port used to access the `grpcbin` service from the test script. + +Learn more about how to get the external IP address in the [Expose your application](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/expose--your-application). + +### Manifests + +{{< collapse title="namespace.yaml" >}} + +```yaml +apiVersion: 'v1' +kind: Namespace +metadata: + name: grpcbin +``` + +{{< /collapse >}} + +{{< collapse title="pod.yaml" >}} + +```yaml +kind: 'Pod' +apiVersion: 'v1' +metadata: + name: grpcbin + namespace: grpcbin + labels: + app: grpcbin +spec: + containers: + - name: grpcbin + image: moul/grpcbin + ports: + - name: http + containerPort: 9000 +``` + +{{< /collapse >}} + +{{< collapse title="service.yaml" >}} + +```yaml +apiVersion: 'v1' +kind: 'Service' +metadata: + name: grpcbin + namespace: grpcbin +spec: + selector: + app: grpcbin + type: 'NodePort' + ports: + - port: 9000 + targetPort: 9000 +``` + +{{< /collapse >}} diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-http-faults-into-pod.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-http-faults-into-pod.md new file mode 100644 index 000000000..3e16ae657 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/examples/inject-http-faults-into-pod.md @@ -0,0 +1,424 @@ +--- +title: 'Inject HTTP faults into Pod' +description: 'This example shows how to test the effect of faults injected in the HTTP requests served by a pod.' +weight: 02 +--- + +# Inject HTTP faults into Pod + +This example shows how [PodDisruptor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor) can be used for testing the effect of faults injected in the HTTP requests served by a pod. + +You will find the complete [source code](#source-code) at the end of this document. Next sections examine the code in detail. + +The example uses [httpbin](https://httpbin.org), a simple request/response application that offers endpoints for testing different HTTP requests. + +The test requires `httpbin` to be deployed in a cluster in the namespace `httpbin` and exposed with an external IP. the IP address is expected in the environment variable `SVC_IP`. + +You will find the Kubernetes manifests and the instructions of how to deploy it to a cluster in the [test setup](#test-setup) section at the end of this document. You can learn more about how to get the external IP address in the [expose your application](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/expose--your-application) section. + +## Initialization + +The initialization code imports the external dependencies required by the test. The `PodDisruptor` class imported from the `xk6-disruptor` extension provides functions for injecting faults in pods. The [k6/http](https://grafana.com/docs/k6//javascript-api/k6-http) module provides functions for executing HTTP requests. + +```javascript +import { PodDisruptor } from 'k6/x/disruptor'; +import http from 'k6/http'; +``` + +## Test Load + +The test load is generated by the `default` function, which executes a request to the `httpbin` pod using the IP obtained from the environment variable `SVC_IP`. The test makes requests to the endpoint `delay/0.1` which will return after `0.1` seconds (`100ms`). + +```javascript +import http from 'k6/http'; + +export default function (data) { + http.get(`http://${data.SVC_IP}/delay/0.1`); +} +``` + +{{% admonition type="note" %}} + +The test uses the `delay` endpoint which return after the requested delay. It requests a `0.1s` (`100ms`) delay to ensure the baseline scenario (see scenarios below) has meaningful statistics for the request duration. If we were simply calling a locally deployed http server (for example `nginx`), the response time would exhibit a large variation between a few microseconds to a few milliseconds. Having `100ms` as baseline response time has proved to offer more consistent results. + +{{% /admonition %}} + +## Fault injection + +The `disrupt` function creates a `PodDisruptor` using a selector that matches pods in the namespace `httpbin` with the label `app: httpbin`. + +The http faults are injected by calling the [PodDisruptor.injectHTTPFaults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/injecthttpfaults) method using a fault definition that introduces a delay of `50ms` on each request and an error code `500` in `10%` of the requests. + +```javascript +import { PodDisruptor } from 'k6/x/disruptor'; + +export function disrupt(data) { + if (__ENV.SKIP_FAULTS == '1') { + return; + } + + const namespace = 'default'; + + const selector = { + namespace: namespace, + select: { + labels: { + app: 'httpbin', + }, + }, + }; + + const podDisruptor = new PodDisruptor(selector); + + // delay traffic from one random replica of the deployment + const fault = { + averageDelay: '50ms', + errorCode: 500, + errorRate: 0.1, + }; + + podDisruptor.injectHTTPFaults(fault, '30s'); +} +``` + +Notice the following code snippet in the `injectFaults` function above: + +```javascript +export default function () { + if (__ENV.SKIP_FAULTS == '1') { + return; + } +} +``` + +This code makes the function return without injecting faults if the `SKIP_FAULTS` environment variable is passed to the execution of the test with a value of "1". We will use this option to obtain a [baseline execution](#baseline-execution) without faults. + +## Scenarios + +This test defines two [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) to be executed. The `load` scenario applies the test load to the `httpbin` application for `30s` invoking the `default` function. The `disrupt` scenario invokes the `disrupt` function to inject a fault in the HTTP requests of the target application. + +```javascript +export const options = { + scenarios: { + load: { + executor: 'constant-arrival-rate', + rate: 100, + preAllocatedVUs: 10, + maxVUs: 100, + exec: 'default', + startTime: '0s', + duration: '30s', + }, + disrupt: { + executor: 'shared-iterations', + iterations: 1, + vus: 1, + exec: 'disrupt', + startTime: '0s', + }, + }, +}; +``` + +{{% admonition type="note" %}} + +Notice that the `disrupt` scenario uses a `shared-iterations` executor with one iteration and one `VU`. This setting ensures the `disrupt` function is executed only once. Executing this function multiples times concurrently may have unpredictable results. + +{{% /admonition %}} + +## Executions + +{{% admonition type="note" %}} + +The commands in this section assume the `xk6-disruptor` binary is available in your current directory. This location can change depending on the installation process and the platform. Refer to the [installation section](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/installation) for details on how to install it in your environment. + +{{% /admonition %}} + +### Baseline execution + +We will first execute the test without introducing faults to have an baseline using the following command: + +{{< code >}} + +```bash +xk6-disruptor run --env SKIP_FAULTS=1 --env SVC_IP=$SVC_IP disrupt-pod.js +``` + +```windows-powershell +xk6-disruptor run --env SKIP_FAULTS=1 --env "SVC_IP=$Env:SVC_IP" disrupt-pod.js +``` + +{{< /code >}} + +Notice the argument `--env SKIP_FAULT=1`, which makes the `disrupt` function return without injecting any fault as explained in the [fault injection](#fault-injection) section. Also notice the `--env SVC_IP` argument, which passes the external IP used to access the `httpbin` application. + +You should get an output similar to the one shown below (click `Expand` button to see all output). + +{{< code >}} + +``` + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: test.js + output: - + + scenarios: (100.00%) 2 scenarios, 101 max VUs, 10m30s max duration (incl. graceful stop): + * disrupt: 1 iterations shared among 1 VUs (maxDuration: 10m0s, exec: disrupt, gracefulStop: 30s) + * load: 100.00 iterations/s for 30s (maxVUs: 10-100, exec: default, gracefulStop: 30s) + + +running (00m30.1s), 000/014 VUs, 2998 complete and 0 interrupted iterations +disrupt ✓ [======================================] 1 VUs 00m00.0s/10m0s 1/1 shared iters +load ✓ [======================================] 000/013 VUs 30s 100.00 iters/s + + data_received..................: 1.4 MB 46 kB/s + data_sent......................: 267 kB 8.9 kB/s + dropped_iterations.............: 4 0.132766/s + http_req_blocked...............: avg=8.08µs min=2.36µs med=5.8µs max=543.79µs p(90)=8.68µs p(95)=10.5µs + http_req_connecting............: avg=1.25µs min=0s med=0s max=418.63µs p(90)=0s p(95)=0s + http_req_duration..............: avg=103.22ms min=101.65ms med=103.13ms max=121.7ms p(90)=104.01ms p(95)=104.4ms + { expected_response:true }...: avg=103.22ms min=101.65ms med=103.13ms max=121.7ms p(90)=104.01ms p(95)=104.4ms + http_req_failed................: 0.00% ✓ 0 ✗ 2997 + http_req_receiving.............: avg=133.5µs min=45.06µs med=131.23µs max=879.43µs p(90)=193.48µs p(95)=223.04µs + http_req_sending...............: avg=31.14µs min=11.46µs med=29.37µs max=171.68µs p(90)=40.48µs p(95)=47.53µs + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=103.05ms min=101.54ms med=102.98ms max=121.56ms p(90)=103.82ms p(95)=104.2ms + http_reqs......................: 2997 99.474844/s + iteration_duration.............: avg=103.34ms min=109.86µs med=103.28ms max=121.92ms p(90)=104.19ms p(95)=104.63ms + iterations.....................: 2998 99.508035/s + vus............................: 13 min=11 max=13 + vus_max........................: 14 min=12 max=14 +``` + +{{< /code >}} + +### Fault injection + +We repeat the execution injecting the faults. Notice we have removed the `--env SKIP_FAULTS=1` argument. + +{{< code >}} + +```bash +xk6-disruptor run --env SVC_IP=$SVC_IP disrupt-pod.js +``` + +```windows-powershell +xk6-disruptor run --env "SVC_IP=$Env:SVC_IP" disrupt-pod.js +``` + +{{< /code >}} + +{{< code >}} + +``` + + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: disrupt-pod.js + output: - + + scenarios: (100.00%) 2 scenarios, 101 max VUs, 10m30s max duration (incl. graceful stop): + * disrupt: 1 iterations shared among 1 VUs (maxDuration: 10m0s, exec: disrupt, gracefulStop: 30s) + * load: 100.00 iterations/s for 30s (maxVUs: 10-100, exec: default, gracefulStop: 30s) + + +running (00m31.1s), 000/018 VUs, 2995 complete and 0 interrupted iterations +disrupt ✓ [======================================] 1 VUs 00m31.1s/10m0s 1/1 shared iters +load ✓ [======================================] 000/017 VUs 30s 100.00 iters/s + + data_received..................: 1.1 MB 34 kB/s + data_sent......................: 267 kB 8.6 kB/s + dropped_iterations.............: 7 0.224798/s + http_req_blocked...............: avg=9.81µs min=2.59µs med=5.93µs max=489.67µs p(90)=7.88µs p(95)=9.5µs + http_req_connecting............: avg=2.48µs min=0s med=0s max=367.63µs p(90)=0s p(95)=0s + http_req_duration..............: avg=142.15ms min=50.33ms med=153.79ms max=165.85ms p(90)=154.8ms p(95)=155.12ms + { expected_response:true }...: avg=151.9ms min=101.81ms med=153.9ms max=165.85ms p(90)=154.86ms p(95)=155.17ms + http_req_failed................: 9.65% ✓ 289 ✗ 2705 + http_req_receiving.............: avg=80.92µs min=28.32µs med=77.33µs max=352.19µs p(90)=105.09µs p(95)=123.68µs + http_req_sending...............: avg=30.43µs min=11.27µs med=29.37µs max=287.71µs p(90)=37.42µs p(95)=41.84µs + http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + http_req_waiting...............: avg=142.04ms min=50.25ms med=153.68ms max=165.76ms p(90)=154.69ms p(95)=155.01ms + http_reqs......................: 2994 96.149356/s + iteration_duration.............: avg=152.64ms min=50.43ms med=153.93ms max=31.12s p(90)=154.97ms p(95)=155.29ms + iterations.....................: 2995 96.18147/s + vus............................: 1 min=1 max=18 + vus_max........................: 18 min=12 max=18 + +``` + +{{< /code >}} + +### Comparison + +Let's take a closer look at the results for the requests on each scenario. We can observe that he `base` scenario has an average of `103ms` and an error rate of `0%` while the `faults` scenario has a median around `151.9ms` and an error rate of nearly `10%`, matching the definition of the faults defined in the disruptor. + +| Execution | Avg. Response | Failed requests | +| --------------- | ------------- | --------------- | +| Baseline | 103.22ms | 0.00% | +| Fault injection | 151.9 ms | 9.65% | + +{{% admonition type="note" %}} + +Notice we have used the average response time reported as `expected_response:true` because this metric only consider successful requests while `http_req_duration` considers all requests, including those returning a fault. + +{{% /admonition %}} + +## Source Code + +{{< collapse title="disrupt-pod.js" >}} + +```javascript +import { PodDisruptor } from 'k6/x/disruptor'; +import http from 'k6/http'; + +export default function (data) { + http.get(`http://${__ENV.SVC_IP}/delay/0.1`); +} + +export function disrupt(data) { + if (__ENV.SKIP_FAULTS == '1') { + return; + } + + const selector = { + namespace: 'httpbin', + select: { + labels: { + app: 'httpbin', + }, + }, + }; + const podDisruptor = new PodDisruptor(selector); + + // delay traffic from one random replica of the deployment + const fault = { + averageDelay: '50ms', + errorCode: 500, + errorRate: 0.1, + }; + podDisruptor.injectHTTPFaults(fault, '30s'); +} + +export const options = { + scenarios: { + load: { + executor: 'constant-arrival-rate', + rate: 100, + preAllocatedVUs: 10, + maxVUs: 100, + exec: 'default', + startTime: '0s', + duration: '30s', + }, + disrupt: { + executor: 'shared-iterations', + iterations: 1, + vus: 1, + exec: 'disrupt', + startTime: '0s', + }, + }, +}; +``` + +{{< /collapse >}} + +## Test setup + +The tests requires the deployment of the `httpbin` application. The application must also be accessible using an external IP available in the `SVC_IP` environment variable. + +The [manifests](#manifests) below define the resources required for deploying the application and exposing it as a LoadBalancer service. + +You can deploy the application using the following commands: + +```bash +# Create Namespace +kubectl apply -f namespace.yaml +namespace/httpbin created + +# Deploy Pod +kubectl apply -f pod.yaml +pod/httpbin created + +# Expose Pod as a Service +kubectl apply -f service.yaml +service/httpbin created +``` + +You can retrieve the resources using the following command: + +```bash +kubectl -n httpbin get all +NAME READY STATUS RESTARTS AGE +pod/httpbin 1/1 Running 0 1m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/httpbin LoadBalancer 10.96.169.78 172.18.255.200 80:31224/TCP 1m +``` + +You must set the environment variable `SVC_IP` with the external IP address and port used to access the `httpbin` service from the test script. + +You can learn more about how to get the external IP address in the [expose your application](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/expose--your-application) section. + +### Manifests + +{{< collapse title="namespace.yaml" >}} + +```yaml +apiVersion: 'v1' +kind: Namespace +metadata: + name: httpbin +``` + +{{< /collapse >}} + +{{< collapse title="pod.yaml" >}} + +```yaml +kind: 'Pod' +apiVersion: 'v1' +metadata: + name: httpbin + namespace: httpbin + labels: + app: httpbin +spec: + containers: + - name: httpbin + image: kennethreitz/httpbin + ports: + - name: http + containerPort: 80 +``` + +{{< /collapse >}} + +{{< collapse title="service.yaml" >}} + +```yaml +apiVersion: 'v1' +kind: 'Service' +metadata: + name: httpbin + namespace: httpbin +spec: + selector: + app: httpbin + type: 'LoadBalancer' + ports: + - port: 80 + targetPort: 80 +``` + +{{< /collapse >}} diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/expose-your-application.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/expose-your-application.md new file mode 100644 index 000000000..35cdc9790 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/expose-your-application.md @@ -0,0 +1,111 @@ +--- +title: 'Expose your application' +description: 'How to make your applications accessible from the test scripts.' +weight: 04 +aliases: + - ./expose--your-application/ +--- + +# Expose your application + +To access your application from the test scripts, you must assign it an external IP in the cluster where it's running. +How you do this depends on the platform you use to deploy the application. +The following sections explain the different approaches. + +## Using port-forwarding + +`kubectl`'s `port-forward` command allows accessing applications running in a Kubernetes cluster using a local port. + +For example, the following command will make `my-service`'s port `80` accessible as `localhost:8080`: + +```bash +kubectl port-forward svc/my-service 8080:80 +Forwarding from 127.0.0.1:8080 -> 80 +``` + +### Limitations using port forwarding + +To be able to inject faults, `xk6-disruptor` must [install an agent on each target](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/how--it-works) that intercepts the requests and applies the desired disruptions. This process requires any existing connection to the targets to be redirected to the agent. + +Due to an existing bug in `kubectl`, the process of installing the disruptor can potentially [break the port forwarding](https://github.com/grafana/xk6-disruptor/issues/254). Notice that this issue happens only if the faults are injected in the service that is exposed using port forward. If the faults are injected in another service not exposed by port-forwarding, there shouldn't be any issue. + +Until this issue is solved in `kubectl`, tests using port forwarding to access a service should ensure the agent is installed in the targets before any traffic is sent by the test. + +The simplest way to accomplish this is to ensure the scenario that executes the load (#2) starts after the scenario that injects the faults (#1): + +```js +export const options = { + scenarios: { + disrupt: { + // #1 inject faults + executor: 'shared-iterations', + iterations: 1, + vus: 1, + exec: 'disrupt', + startTime: '0s', + }, + load: { + // #2 execute load + executor: 'constant-arrival-rate', + rate: 100, + preAllocatedVUs: 10, + maxVUs: 100, + exec: 'default', + startTime: '20s', // give time for the agents to be installed + duration: '30s', + }, + }, +}; +``` + +## As a LoadBalancer service + +A service of type [`LoadBalancer`](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/) receives an external IP from an external load-balancer provider. +How you configure the load balancer depends on the your cluster's deployment platform and configuration. +The following sections provide guidelines to expose your application when running in common development environments. +If your cluster is deployed in a public cloud, check your cloud provider documentation. + +If the service that you want your tests to access is not defined as a load balancer, you can change the service type with the following command. The service will then receive an external IP. + +{{< code >}} + +```bash +kubectl -n patch svc -p '{"spec": {"type": "LoadBalancer"}}' +``` + +```windows-powershell +kubectl -n patch svc -p '{\"spec\": {\"type\": \"LoadBalancer\"}}' +``` + +{{< /code >}} + +You can retrieve the external IP address and store it in an environment variable (`SVC_IP` in this example) using the following command: + +{{< code >}} + +```bash +SVC_IP=$(kubectl -n get svc --output jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +```windows-powershell +$Env:SVC_IP=$(kubectl -n get svc --output jsonpath='{.status.loadBalancer.ingress[0].ip}') +``` + +{{< /code >}} + +### Configuring a LoadBalancer in Kind + +[Kind](https://kind.sigs.k8s.io/) is a tool to run local Kubernetes clusters using a Docker container to emulate nodes. +You can use kind for local development or CI. +Services deployed in a kind cluster can be exposed to be accessed from the host machine [using metallb as a load balancer](https://kind.sigs.k8s.io/docs/user/loadbalancer). + +### Configuring a LoadBalancer in Minikube + +[Minikube](https://github.com/kubernetes/minikube) implements a local Kubernetes cluster that supports different technologies to virtualize the cluster's infrastructure, such as containers, VMs or bare metal. + +Minikube's tunnel command runs as a process, creating a network route on the host to the service CIDR of the cluster. +It uses the cluster’s IP address as a gateway. The tunnel command exposes the external IP directly to any program that is running on the host operating system. + +```bash +minikube tunnel +``` diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/first-steps.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/first-steps.md new file mode 100644 index 000000000..dc546de59 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/first-steps.md @@ -0,0 +1,42 @@ +--- +title: 'xk6-disruptor first steps' +description: 'xk6-disruptor is a k6 extension providing fault injection capabilities to k6.' +weight: 01 +--- + +# xk6-disruptor first steps + +[xk6-disruptor](https://github.com/grafana/xk6-disruptor) is an extension that adds fault injection capabilities to k6. + +It provides a Javascript [API](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/) to inject [faults](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/faults) such as errors and delays into HTTP and gRPC requests served by selected Kubernetes [Pods](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor) or [Services](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor). + +```javascript +import { ServiceDisruptor } from 'k6/x/disruptor'; + +export default function () { + // Create a new disruptor that targets a service + const disruptor = new ServiceDisruptor('app-service', 'app-namespace'); + + // Disrupt the targets by injecting delays and faults into HTTP request for 30 seconds + const fault = { + averageDelay: '500ms', + errorRate: 0.1, + errorCode: 500, + }; + disruptor.injectHTTPFaults(fault, '30s'); +} +``` + +## Next steps + +Explore the fault injection [API](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/) + +See [step-by-step examples](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/examples). + +Visit the [interactive demo environment](https://killercoda.com/grafana-xk6-disruptor/scenario/killercoda). + +Learn the basics of using the disruptor in your test project: + +- [Requirements](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/requirements) + +- [Installation](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/installation) diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/how-it-works.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/how-it-works.md new file mode 100644 index 000000000..3a236fc12 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/how-it-works.md @@ -0,0 +1,38 @@ +--- +title: 'How xk6-disruptor works' +description: 'A brief description of the components of the xk6-disruptor and how they work when inject faults in a target system.' +weight: 05 +aliases: + - ./how--it-works/ +--- + +# How xk6-disruptor works + +xk6-disruptor consists of two main components: + +- **The xk6-disruptor extension** provides a Javascript API for injecting faults into a target system using the xk6-disruptor-agent as a backend. This API is built around a collection of **disruptors**. Each disruptor targets a type of component in the system (for example Pods or cluster Nodes). +- **The xk6-disruptor-agent** injects faults into the target system where it runs. It is provided as an Docker image that you can pull from the [xk6-disruptor repository](https://github.com/grafana/xk6-disruptor/pkgs/container/xk6-disruptor-agent). + +The xk6-disruptor extension installs the agent in the target and sends commands to inject the desired faults. How this happens depends on the type of disruptor, as described in the following sections. + +## PodDisruptor + +The following diagram shows how PodDisruptor works: + +1. The PodDisruptor selects the target pods based on the selector attributes defined in the [constructor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/constructor) +2. The PodDisruptor attaches the xk6-disruptor-agent to each of the target pods +3. When a fault is injected (e.g. calling the [injectHTTTFault](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/poddisruptor/injecthttpfaults)) the PodDisruptor sends a command to the agents to inject the fault in their respective pods + +![How PodDisruptor works](/media/docs/k6-oss/xk6-disruptor-how-pod-disruptor-works.png) + +## ServiceDisruptor + +The ServiceDisruptor works as a wrapper around a PodDisruptor, which targets the pods that back the service. + +1. The ServiceDisruptor uses the definition of the service specified in the [constructor](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/servicedisruptor/constructor) to create a pod selector that matches the pods that back the service. +2. The ServiceDisruptor creates a PodDisruptor using this pod selector. +3. The PodDisruptor installs the agent in the target pods. + +From this point, the PodDisruptor works as described before. + +![How ServiceDisruptor works](/media/docs/k6-oss/xk6-disruptor-how-service-disruptor-works.png) diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/installation.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/installation.md new file mode 100644 index 000000000..87a33c3c5 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/installation.md @@ -0,0 +1,44 @@ +--- +title: 'Installation' +description: 'A step-by-step guide on how to install xk6-disruptor.' +weight: 03 +--- + +# Installation + +xk6-disruptor is a [k6 extension](https://grafana.com/docs/k6//extensions). You have to run a k6 version built with the disruptor extension to use the [disruptor APIs](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/) in your k6 tests. + +The following sections explain the different options to get this custom binary. + +## Download a release binary + +The quickest way to get started is to [download a release binary from GitHub](https://github.com/grafana/xk6-disruptor/releases). + +## Build from source + +You can also use [xk6](https://github.com/grafana/xk6) to build a k6 binary. + +To learn more about xk6, refer to [Build a k6 binary using Go](https://grafana.com/docs/k6//extensions/build-k6-binary-using-go). + +To build the k6 binary with the xk6-disruptor extension: + +1. Ensure you have [Go 1.19](https://golang.org/doc/install) and [Git](https://git-scm.com/) installed. +2. Run the following commands in a terminal: + +{{< code >}} + +```bash +# Install xk6 +go install go.k6.io/xk6/cmd/xk6@latest + +# Clone the xk6-disruptor code +git clone https://github.com/grafana/xk6-disruptor.git +cd xk6-disruptor + +# Build the custom binary +xk6 build --output xk6-disruptor --with github.com/grafana/xk6-disruptor=. +``` + +{{< /code >}} + +xk6 will create the `xk6-disruptor` binary in the current working directory. diff --git a/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/requirements.md b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/requirements.md new file mode 100644 index 000000000..294ee053c --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/injecting-faults-with-xk6-disruptor/requirements.md @@ -0,0 +1,29 @@ +--- +title: 'Requirements' +description: 'Requirements for using xk6-disruptor in your test scripts' +weight: 02 +--- + +# Requirements + +The xk6-disruptor is a k6 extension. +To use it in a k6 test script, you need to bundle a k6 extension that includes the disruptor. +Refer to the [Installation](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/installation) section for instructions on how to get this custom build. + +xk6-disruptor needs to interact with the Kubernetes cluster on which the application under test is running. +To do so, you must have the credentials to access the cluster in a [kubeconfig](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) file. +Ensure that this file is pointed to by the `KUBECONFIG` environment variable or that it is located at the default location, `$HOME/.kube/config`. + +{{% admonition type="note" %}} + +xk6-disruptor requires Kubernetes version 1.23 or higher + +{{% /admonition %}} + +`xk6-disruptor` requires the [grafana/xk6-disruptor-agent](https://github.com/grafana/xk6-disruptor/pkgs/container/xk6-disruptor-agent) image for injecting the [disruptor agent](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/how--it-works) into the disruption targets. Kubernetes clusters can be configured to restrict the download of images from public repositories. You need to ensure this image is available in the cluster where the application under test is running. Additionally, the xk6-disruptor-agent must run with network access privileges. Kubernetes clusters [can be configured to restrict the privileges of containers](https://kubernetes.io/docs/concepts/security/pod-security-admission/). +If you find an error similar to the following when using the xk6-disruptor, contact your cluster administrator and request the necessary privileges. + +> ERROR\[0000\] error creating PodDisruptor: pods "nginx" is forbidden: violates PodSecurity "baseline:latest": non-default capabilities (container "xk6-agent" must not include "NET_ADMIN", "NET_RAW" in securityContext.capabilities.add) + +You also need to ensure your test application is accessible from the machine where the tests run. +Refer to [Expose your application](https://grafana.com/docs/k6//testing-guides/injecting-faults-with-xk6-disruptor/expose--your-application) section for instructions on how to make your application available from outside the Kubernetes cluster.. diff --git a/docs/sources/v0.50.x/testing-guides/load-testing-websites.md b/docs/sources/v0.50.x/testing-guides/load-testing-websites.md new file mode 100644 index 000000000..e85fc3119 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/load-testing-websites.md @@ -0,0 +1,346 @@ +--- +title: 'Load testing websites' +head_title: 'How to Load Test a Website: The k6 Guide' +description: 'Do you know how many users your site can handle? This guide answers the WHY and WHEN you should load test your website and gives you the best practices for load testing websites or web apps with k6. Let’s get started.' +weight: 03 +noindex: true +--- + +# Load testing websites + +This doc explains some key concepts about load testing websites, including: + +- The difference between backend and frontend performance testing +- When to choose between protocol-based, browser-based, or hybrid scripts +- Recommended practices about how to load test websites + +All load tests try to simulate real user traffic to prevent failures, improve reliability, and release new code with confidence. +But your approach to load testing must adapt to the type of application you want to test. + +In this guide, learn about strategies to test websites, including specific recommendations for complex scenario scripting and test execution. + +## Load testing approaches + +When you approach the load test, first consider the following perspectives: + +- Backend vs. frontend performance +- Protocol-based, browser-based, or hybrid load testing +- Component testing vs. end-to-end testing + +## Backend vs. frontend performance + +Performance has a significant influence on the user experience of a website. + +For example, when users want to visualize or interact with some information, they expect the website to react quickly. + +To measure performance, testers often look at response times, which are affected by two main factors: + +- Frontend performance +- Backend performance + +### Frontend performance + +Frontend performance testing verifies application performance on the interface level, measuring round-trip metrics that consider how and when page elements appear on the screen. It's concerned with the end-user experience of an application, usually involving a browser. + +![Snapshots over time of webpage elements rendering on a browser](/media/docs/k6-oss/frontend-rendering.png 'Snapshots over time of webpage elements rendering on a browser') + +Frontend performance testing excels at identifying issues on a micro level but does not expose issues in the underlying architecture of a system. + +Because it primarily measures a single user's experience of the system, frontend performance testing tends to be easier to run on a small scale. +Frontend performance testing has metrics that are distinct from backend performance testing. Frontend performance tests for things like: + +- Whether the pages of the application are optimized to render quickly on a user's screen +- How long it takes a user to interact with the UI elements of the application. + +Some concerns when doing this type of performance testing are its dependency on fully integrated environments and the cost of scaling. You can test frontend performance only once the application code and infrastructure have been integrated with a user interface. Tools to automate frontend testing are also inherently more resource-intensive, so they can be costly to run at scale and are not suitable for high load tests. + +### Backend performance + +Backend performance testing targets the underlying application servers to see how they behave under production-like conditions. When it comes to websites, while frontend performance involves how assets included in the page are rendered, backend performance focuses on how those assets are processed by the application servers, served to users, and downloaded by browsers. + +![Backend components](/media/docs/k6-oss/backend-performance.png 'Backend components of an application') + +Backend testing is broader in scope than frontend performance testing. API testing can be used to target specific components or integrated components, meaning that application teams have more flexibility and higher chances of finding performance issues earlier. Backend testing is less resource-intensive than frontend performance testing and is thus more suitable for generating high load. + +### Which one should you test? + +It depends! Ideally, both. + +Frontend testing tools are executed on the client side and are limited in scope: they do not provide enough information about backend components for fine-tuning beyond the user interface. + +This limitation can lead to false confidence in overall application performance when the amount of traffic against an application increases. While the frontend component of response time remains more or less constant, the backend component of response time increases exponentially with the number of concurrent users: + +![Backend performance often contributes significantly to overall user experience](/media/docs/k6-oss/frontend-backend-load-testing.png 'Backend performance often contributes significantly to overall user experience') + +Testing _only_ frontend performance ignores a large part of the application, one more susceptible to increased failures and performance bottlenecks at higher levels of load. + +Testing _only_ backend performance, on the other hand, ignores "the first mile" of user experience and breadth. Backend testing involves messaging at the protocol level rather than interacting with page elements the way a real user would. It verifies the foundation of an application rather than the highest layer of it that a user ultimately sees. + +Testing both frontend and backend performance leads to the best overall performance and user experience for your application. Ignoring one or the other exposes you to performance bottlenecks that significantly decrease a user's satisfaction with your application. + +However, if your testing is smaller in scope, you can choose to focus on either frontend or backend with the goal of eventually building a test suite that encompasses both. + +## Component testing vs. end-to-end testing + +When you're testing a web app, you may wonder whether how you structure your test scripts. Consider the following two methods. + +### Component testing + +One way you could load test a web app is to load test its components. Maybe you know there are issues with a specific functionality due to previous production issues, or maybe you'd like to target components that are business-critical to reduce risk exposure. + +In these cases, the type of test you write may: + +- be protocol-based +- call only those API endpoints that are relevant +- have smaller or no sleep/think time +- be focused on eventually stress testing a component or service or finding its breaking point + +Testing in this way is more flexible. With protocol testing, you can specify the endpoints or servers you'd like to hit, and narrow your test's target that way. You can test specific functionalities while skipping others that come chronologically before it in a standard user flow. You can more finely control the type of traffic that is generated. If you're recreating a specific mix of requests, you can script those requests and reproduce them in a repeatable manner. + +Doing component testing for load may not always require that your script behave like an end user. In fact, it may be necessary to artificially inflate traffic to more quickly reproduce issues, or to deflate traffic to reduce the noise in the logs. By the nature of this type of testing, scripts don't contain the full flow of a request that you would expect to see in production, so realism is not a priority. + +### End-to-end testing + +You could also do end-to-end testing against a web app. End-to-end testing seeks to replicate real user behaviour and track its effects across the entire stack. When doing end-to-end testing, you might: + +- do protocol-level, browser-level, or hybrid load testing +- replicate the actions in a typical user flow +- think about the performance of the entire workflow as well as how long a request took to be processed by each component +- write scripts that access the application the same way users would, such as by browsing to the homepage before finding their way to other parts of the website + +This type of load testing is broader in scope than component testing, but shallower in terms of depth. With end-to-end testing, you get a better idea of the full user experience of the application as a whole. However, it can also be more complex to troubleshoot, as you have more components to monitor and more places to look for issues that are found. + +End-to-end testing is based on real user behavior, so it's often important that end-to-end test scripts also be realistic. + +## Protocol-based, browser-based, or hybrid load testing + +The decision of whether to test the frontend, backend, or both will also affect the type of load testing you should carry out and the kind of scripts you should write. + +### Protocol-based load testing + +Protocol-based load testing verifies the backend performance of an application by simulating the requests underlying user actions. For websites, this commonly involves HTTP requests that bypass the user interface of your application and are sent directly to a server or application component. + +For example, a protocol-based load testing script might request all the resources on a webpage from the application servers, but those resources are merely downloaded. The response times reported by a purely protocol-based script do not include frontend metrics such as the time taken for images to render on a browser. The load is generated by simulating many requests sent to application servers. + +While protocol-based load testing may seem to lend itself better to component testing, you can also do end-to-end website testing with protocol-level scripts. + +#### Sample protocol-based test script + +The following is an example of a protocol-based load testing script in k6 that fetches the homepage, along with resources embedded into the page. + +```javascript +import http from 'k6/http'; +import { sleep, check } from 'k6'; + +export function Homepage() { + const params = { + 'sec-ch-ua': '"Chromium";v="94", "Google Chrome";v="94", ";Not A Brand";v="99"', + 'accept-encoding': 'gzip, deflate, br', + 'accept-language': 'en-GB,en;q=0.9', + }; + + // 01. Go to the homepage + let responses = http.batch([ + ['GET', 'https://mywebsite.com/', params], + ['GET', 'https://mywebsite.com/style.min.css', params], + ['GET', 'https://website.com/header.png', params], + ['GET', 'https://website.com/polyfill.min.js', params], + ]); + check(responses, { + 'Homepage loaded': (r) => JSON.stringify(r).includes('Welcome to my site'), + }); + + sleep(4); + + // 02. View products + responses = http.batch([ + ['GET', 'https://mywebsite.com/products', params], + ['GET', 'https://mywebsite.com/style.css', params], + ['GET', 'https://website.com/product1.jpg', params], + ['GET', 'https://website.com/product2.jpg', params], + ['GET', 'https://website.com/displaylist.js', params], + ]); + check(responses, { + 'Products loaded': (r) => JSON.stringify(r).includes('Add to Cart'), + }); + + sleep(1); +} +``` + +[Recording browser traffic](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder) helps you prototype to test websites on the protocol level. + +### Browser-based load testing + +Browser-based load testing verifies the frontend performance of an application by simulating real users using a browser to access your website. + +For example, a browser-based load testing script might include instructions to navigate to a page, click on a button, and type out text on a form. Those user actions then trigger underlying requests on the protocol layer, but only user actions are scripted in browser-based testing. + +Unlike protocol-based load testing, browser-based load testing scripts generate load by starting multiple instances of browsers and interacting with your application the way real users would. Testing at the browser level can also be the only option for testing Single-Page Applications where a lot of the application logic is executed by client-side scripts. + +Scripting on the browser level usually requires the use of different tools from the ones used to test at the protocol level. +However, k6 now has an experimental module called [k6 browser](https://grafana.com/docs/k6//using-k6-browser/) that allows the creation of browser-based test scripts alongside protocol-based ones. + +#### Sample browser-based test script + +The following is an example of a browser-based load testing script in k6 using the browser module on a dummy website. Instead of making an HTTP request, the script views the homepage, then looks for and clicks on a link to the product page. + +{{< code >}} + + + +```javascript +import { browser } from 'k6/experimental/browser'; +import { sleep } from 'k6'; + +export default async function () { + const page = browser.newPage(); + + // 01. Go to the homepage + try { + await page.goto('https://mywebsite.com'); + + page.waitForSelector('p[class="woocommerce-result-count"]"]'); + page.screenshot({ path: 'screenshots/01_homepage.png' }); + + sleep(4); + + // 02. View products + const element = page.locator( + 'a[class="woocommerce-LoopProduct-link woocommerce-loop-product__link"]' + ); + await element.click(); + page.waitForSelector('button[name="add-to-cart"]'); + page.screenshot({ path: 'screenshots/02_view-product.png' }); + + sleep(1); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +#### Tips for writing browser-level scripts + +The following steps can help you get started with a browser-level test script. + +**Script user actions, not requests.** Determine what a user might do for a particular task, and script interactions with elements on the browser level. For example, script what buttons the user clicks on. + +**Identify unique selectors.** Once you have identified which page elements a user interacts with, use the Element Inspector for DevTools in your browser to find a unique, static, and simple way to identify each element. The script needs selectors to find the right element to interact with. + +**Use elements to verify responses.** After every action, use [locators](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator) to search for elements on the page that you would expect to find. This verification helps ensure that the script has reached the expected page. + +**Take screenshots for every action while debugging.** One of the advantages of browser-based testing is the ability to take screenshots. After every user interaction the script simulates, use [page.screenshot](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) to save a visual image of what the script encountered for later troubleshooting. + +### Hybrid load testing + +Hybrid load testing is a combination of protocol-based and browser-based load testing. While you can use two tools or two scripts to execute different types of load tests (one protocol-based and one browser-based), having both types of tests executed by the same script and the same tool is ideal. Aggregating results between different tools can be difficult at best and inconsistent at worst. + +![Hybrid of frontend and backend performance testing](/media/docs/k6-oss/hybrid-testing.png 'In hybrid load testing, both the frontend and backend are tested') + +A best practice in hybrid load testing is to generate most of the load using the protocol-level test, and then to run a smaller number of the browser-level testing scripts. This approach: + +- reduces the number of load generators needed, since protocol-level testing requires fewer machines to generate the same load +- measures backend and frontend performance in the same test execution +- provides a single source of aggregated output at the end +- reduces complexity in the creation and maintenance of scripts + +## Scripting considerations + +When you script a test for a website, consider these recommendations. + +### Consider factors that affect script realism + +**Record your user journey.** Using the browser recorder can facilitate initial test script creation by capturing all the embedded resources on webpages. Check out the [Session Recording guide](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/) to learn more about how to auto-generate your load test from a user session. + +**Correlate data.** Recordings often don't take into account dynamic values that are generated anew each time a request is made. Go through the recorded requests and determine whether you need to [extract values from previous responses](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data) and use parameters in subsequent requests. This practice ensures your VUs behave more like real users would. + +**Include or exclude static resources.** Determine whether you should include or exclude static resources on pages such as images, JavaScript, etc. Consider including them if you want to measure overall user experience. Consider excluding them if you are using a Content Delivery Network (CDN) that is under a separate Service Level Agreement (SLA). + +**Exclude third-party requests.** Don't load test servers that you don't own. Many applications make calls to third-party providers for authentication, social sharing, and marketing analytics. Disable these requests unless you have permission to include them in your tests. + +**Use concurrent requests.** To mimic the way modern browsers download some requests in parallel, use [batching](https://grafana.com/docs/k6//javascript-api/k6-http/batch). + +**Determine cache and cookie behaviour.** k6 automatically resets cookies between iterations, but you can also [change this behavior](https://grafana.com/docs/k6//using-k6/k6-options/reference/#no-cookies-reset) if maintaining cookies would be more realistic. + +**Use dynamic think time and pacing.** Consider adding varying [delays](https://grafana.com/docs/k6//javascript-api/k6/sleep) so you don't artificially stagger a script with completely uniform delays. + +**Use test data.** Real users typically don't search for or submit the same data repeatedly. Consider adding a [test data file](https://grafana.com/docs/k6//examples/data-parameterization) for the script to iterate through. + +**Model test parameters and load profile after production.** In k6, you can use [test options](https://grafana.com/docs/k6//using-k6/k6-options/how-to) to determine the exact shape and profile of your load test script. Select the appropriate [executors](https://grafana.com/docs/k6//using-k6/scenarios/executors) for the job. + +### Create a reusable framework + +**Use tags and groups.** Organizing requests by [tagging and grouping them](https://grafana.com/docs/k6//using-k6/tags-and-groups) helps you consolidate like metrics and makes your test scripts more understandable by others. + +**Use scenarios.** When combining protocol-based and browser-based tests, use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) to independently control their test parameters and [executors](https://grafana.com/docs/k6//using-k6/scenarios/executors). + +**Modularize scripts.** Use [modules](https://grafana.com/docs/k6//using-k6/modules) to separate and organize functions for protocol-level testing and browser-level testing, and then use a test runner script to execute them. This approach means different scripts can be versioned and changed without affecting each other. + +**Integrate your tests into your CI pipeline.** Adopting a "tests as code" approach enables you to tie your load tests more closely into your project's existing CI/CD processes, helping you get the most value out of every test. + +### Test with thresholds in mind + +**Create thresholds for both types of testing.** Some browser-level and protocol-level metrics cannot be combined because they don't measure the same thing. Set [thresholds](https://grafana.com/docs/k6//using-k6/thresholds) for relevant metrics from both the browser-level script and the protocol-level one. + +### When possible, use hybrid load testing + +**Use protocol-based scripts to generate majority of the load.** When writing the test scenario, use the protocol-based requests to simulate most of the traffic and use fewer VUs for the browser-based requests. Relying on the protocol-level traffic helps keep resource utilization on load generators down. + +## Execution considerations + +When you run tests, consider the test environment and the load generator location. + +### Run your tests in the appropriate environment + +Testing in pre-production environments and in production environments both add value. + +**Testing in pre-production environments** (staging, test, system integration testing, user acceptance testing, and production replica environments) enables you to identify performance defects early, which can save a lot of time and effort (and reputation) later on. Running in test environments also means you can often afford to be more aggressive with your tests. However, it's also more critical to get your load profiles right, and the test results you get may not necessarily apply to production. + +**Testing in production** yields the most accurate results, but it's also more risky. Often, testing production is the only feasible alternative. You can reduce the risk of impact to real customers while testing in production by using lower levels of load when running load tests during peak hours, schedule tests for off-peak hours, choosing load test types that are less risky, using techniques like synthetic monitoring that generate less traffic, using real user monitoring tools to get snapshots of user performance at load, and ensuring that your observability stack is working at peak efficiency. + +### Run tests where your customers are + +The location of the load generator(s), where the traffic is coming _from_, can also have an impact on your test results. The question is: where are your end users located? + +**On-premises load testing** can be ideal when testing early on in the development a website, or when there are machines that can be repurposed as load generators. However, testing entirely from within a corporate network may also yield false positives, in that response times reported are significantly lower than if the same application servers were accessed from across the country. + +**Load testing on the cloud** is an essential part of the testing strategy for many public-facing websites. Using load generators on the cloud gives you access to test in different states and geographical countries, creating a mix of load generators proportional to your users' locations. Cloud load generators are easier to provision and cheaper to maintain in the long run than on-premise ones. Load testing on the cloud can help you include the effects of network latency in your tests and yield results that are more realistic. + +## Recommendations + +Here are some recommendations to help you plan, script, and execute your load tests for websites. + +If you want to test the last-mile user experience of your website: + +- focus on **frontend performance**, +- write **browser-based test scripts**, +- and consider doing more realistic **end-to-end** tests of the user flow. + +If you want to test the underlying infrastructure of your website: + +- focus on **backend performance**, +- write **protocol-based test scripts**, +- and consider starting with **component testing** and then gradually increasing the scope. + +If your website is meant for internal or has limited access: + +- Use **on-premise load generators** located within the network that most of your users access the website from. + +If your website is external and public-facing: + +- Use **cloud load generators** in the load zones where your users reside. + +Test in **pre-production environments** when you can, but also consider **testing in production** in a limited capacity. + +Load testing websites can be complex due to the number of viable testing approaches available, the scope for performance testing, and the potential effects of releasing undertested code. By following the recommendations we've put forward here, you can tailor your testing more closely to your objectives. + +## Read more + +- [Browser testing with k6 browser](https://grafana.com/docs/k6//using-k6-browser/) +- [Load test types](https://grafana.com/docs/k6//testing-guides/test-types/) +- [Session recording guide](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/) +- [Determining concurrent users in your load tests](https://k6.io/blog/monthly-visits-concurrent-users) +- [Data correlation in your test script](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data) diff --git a/docs/sources/v0.50.x/testing-guides/running-distributed-tests.md b/docs/sources/v0.50.x/testing-guides/running-distributed-tests.md new file mode 100644 index 000000000..1c8ef2dc8 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/running-distributed-tests.md @@ -0,0 +1,416 @@ +--- +title: 'Running distributed tests' +description: 'How to run distributed tests in Kubernetes' +weight: 05 +--- + +# Running distributed tests + +It has already been established that k6 can [run large load tests](https://grafana.com/docs/k6//testing-guides/running-large-tests) from a single instance, but what about _multiple instances running a single test_? + +Several reasons why you may wish to run a distributed test include: + +- Your [system under test](https://grafana.com/docs/k6//misc/glossary#system-under-test) (SUT) should be accessed from multiple IP addresses. +- A fully optimized node cannot produce the load required by your extremely large test. +- Kubernetes is already your preferred operations environment. + +For scenarios such as these, we've created the [k6-operator](https://github.com/grafana/k6-operator). + +## Introducing k6-operator + +[k6-operator](https://github.com/grafana/k6-operator) is an implementation of the [operator pattern](https://grafana.com/docs/k6//misc/glossary#operator-pattern) in Kubernetes, defining [custom resources](https://grafana.com/docs/k6//misc/glossary#custom-resource) in Kubernetes. +The intent is to automate tasks that a _human operator_ would normally do; tasks like provisioning new application components, changing configurations, or resolving run-time issues. + +The k6-operator defines the custom `TestRun` resource type and listens for changes to, or creation of, `TestRun` objects. +Each `TestRun` object references a k6 test script, configures the environment, and specifies the number of instances, as `parallelism`, for a test run. +Once a change is detected, the operator will react by modifying the cluster state, spinning up k6 test jobs as needed. + +## Get started with k6-operator + +Let's walk through the process for getting started with the k6-operator. +The only requirement being access to a Kubernetes cluster and having the appropriate access and tooling. + +{{% admonition type="note" %}} + +This process can be performed on a local Kubernetes cluster running within [Docker](https://docs.docker.com/get-docker/)! +Using [kind](https://kind.sigs.k8s.io/) or [k3d](https://k3d.io/) are awesome options to experiment with the process. + +{{% /admonition %}} + +- [Introducing k6-operator](#introducing-k6-operator) +- [Get started with k6-operator](#get-started-with-k6-operator) +- [1. Install the operator](#1-install-the-operator) +- [2. Create a test script](#2-create-a-test-script) +- [3. Add test scripts](#3-add-test-scripts) + - [Add as a ConfigMap](#add-as-a-configmap) + - [Add inside a PersistentVolume](#add-inside-a-persistentvolume) +- [4. Create a custom resource](#4-create-a-custom-resource) + - [Script in a ConfigMap](#script-in-a-configmap) + - [Script in a PersistentVolume](#script-in-a-persistentvolume) + - [Configure the environment](#configure-the-environment) + - [Change command-line arguments](#change-command-line-arguments) +- [5. Run your test](#5-run-your-test) +- [6. When things go wrong](#6-when-things-go-wrong) +- [See also](#see-also) + +## 1. Install the operator + +The first step to running distributed tests in Kubernetes is to install the operator if not already installed in the cluster. +At this time, installation does require having the project source code downloaded onto your system. +Installation commands must be run from the source directory. + +{{% admonition type="note" %}} + +Besides privileged access to a Kubernetes cluster, installation will require that the system performing the installation has the following tools installed: + +- [Git](https://git-scm.com/downloads) +- [Go](https://go.dev/doc/install) +- [Kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) +- [Kustomize](https://kubectl.docs.kubernetes.io/installation/kustomize/) +- [Make](https://www.gnu.org/software/make/) + +{{% /admonition %}} + +From your command-line, execute the following: + +```shell +git clone https://github.com/grafana/k6-operator && cd k6-operator +``` + +Ensure that your `kubectl` tool is set for the appropriate Kubernetes cluster. +Then, from the `k6-operator` directory, you may now perform the installation: + +```shell +make deploy +``` + +By default, the operator will be installed into a new namespace, `k6-operator-system`. +You can verify the successful installation by listing available resources within the namespace: + +```shell +kubectl get pod -n k6-operator-system +``` + +After a few moments, your resulting status should become `Running` as shown below: + +{{< code >}} + +```bash +NAME READY STATUS RESTARTS AGE +k6-operator-controller-manager-7664957cf7-llw54 2/2 Running 0 160m +``` + +{{< /code >}} + +You are now ready to start create and execute test scripts! + +## 2. Create a test script + +Creating k6 test scripts for Kubernetes is no different from creating the script for the command-line. +If you haven’t already created test cases for your system, then we suggest having a read through one of our guides for creating tests for websites and APIs/microservices: + +- [Website testing guide](https://grafana.com/docs/k6//testing-guides/load-testing-websites) +- [API testing guide](https://grafana.com/docs/k6//testing-guides/api-load-testing) + +In general, it is advised to start small and expand on your scripts over iterations. +So let's start simple and create a `test.js` with the following content: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + vus: 10, + duration: '10s', +}; + +export default function () { + http.get('https://test.k6.io/'); + sleep(1); +} +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +While creating scripts, [run them locally](https://grafana.com/docs/k6//get-started/running-k6#running-local-tests) before publishing to your cluster. +This can give you immediate feedback if you have errors in your script. + +{{% /admonition %}} + +Let's go ahead and verify our script is valid by performing a brief test: + +```shell +k6 run test.js +``` + +We should see a successful execution and resulting output summary. + +## 3. Add test scripts + +In order for your test scripts to be run, they must be published into your cluster. +Two main methods for this are using [ConfigMap](https://kubernetes.io/docs/concepts/configuration/configmap/) or [PersistentVolume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) resources. + +### Add as a ConfigMap + +Using a `ConfigMap` is a quick and straightforward mechanism for adding your test scripts to Kubernetes. +The `kubectl` tool provides a convenient method to create a new `ConfigMap` from a local script. + +Let's create our ConfigMap as `my-test` with the content of our `test.js` script we created in the previous step: + +```shell +kubectl create configmap my-test --from-file test.js +``` + +{{% admonition type="caution" %}} + +Limitations exist on how large your test script can be when deployed within a `ConfigMap`. +Kubernetes imposes a size limit of 1,048,576 bytes (1 MiB) for the data, therefore if your test scripts exceed this limit, you'll need to mount a `PersistentVolume`. + +Check the [motivations](https://kubernetes.io/docs/concepts/configuration/configmap/#motivation) for when you should use a `ConfigMap` versus a `PersistentVolume`. + +{{% /admonition %}} + +You should see confirmation with `configmap/my-test created`. + +### Add inside a PersistentVolume + +Setting up a `PersistentVolume` is beyond the scope of this guide, but enables access to a shared filesystem from your Kubernetes cluster via `PersistentVolumeClaim`. + +When using this option, organize your test scripts in the applicable filesystem just as you would locally. +This mechanism is ideal when breaking up monolithic scripts into reusable [modules](https://grafana.com/docs/k6//using-k6/modules). + +{{% admonition type="note" %}} + +Organizing your test scripts was part of the discussion during [episode #76](https://www.youtube.com/watch?v=zDtEzp_JUOE&utm=k6-guides) of k6 Office Hours. + +{{% /admonition %}} + +{{% admonition type="caution" %}} + +When using a `PersistentVolume`, the operator will expect all test scripts to be contained within a directory named `/test/`. + +{{% /admonition %}} + +To learn more about creating `PersistentVolume` and `PersistentVolumeClaim` resources, review the [Kubernetes documentation](https://kubernetes.io/docs/concepts/storage/persistent-volumes/). + +## 4. Create a custom resource + +During [installation](#1-install-the-operator), the `TestRun` [Custom Resource definition](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) was added to the Kubernetes API. +The data we provide in the custom resource `TestRun` object should contain all the information necessary for the k6-operator to start a distributed load test. + +Specifically, the main elements defined within the `TestRun` object relate to the name and location of the test script to run, and the amount of [parallelism](https://grafana.com/docs/k6//misc/glossary#parallelism) to utilize. + +{{% admonition type="note" %}} + +The `TestRun` custom resource provides many configuration options to control the initialization and execution of tests. +For the full listing of possible options, please refer to the project [source](https://github.com/grafana/k6-operator/blob/main/config/crd/bases/k6.io_testruns.yaml) and [README](https://github.com/grafana/k6-operator/blob/main/README.md). + +{{% /admonition %}} + +The following examples will show some common variations for the custom resource: + +### Script in a ConfigMap + +When the test script to be executed is contained within a `ConfigMap` resource, we specify the script details within the `configMap` block of [YAML](https://grafana.com/docs/k6//misc/glossary#yaml). +The `name` is the name of the ConfigMap and the `file` is the key-value for the entry. + +Let's create the file `run-k6-from-configmap.yaml` with the following content: + +{{< code >}} + +```yaml +apiVersion: k6.io/v1alpha1 +kind: TestRun +metadata: + name: run-k6-from-configmap +spec: + parallelism: 4 + script: + configMap: + name: my-test + file: test.js +``` + +{{< /code >}} + +Recall when the script was [added as a ConfigMap](#add-as-a-configmap) for our configuration values. +We created the ConfigMap named `my-test`. +The test script content was added to the map using the filename as the key-value, therefore the `file` value is `test.js`. + +The amount of `parallelism` is up to you; how many pods do you want to split the test amongst? +The operator will split the workload between the pods using [execution segments](https://grafana.com/docs/k6//misc/glossary#execution-segment). + +{{% admonition type="caution" %}} + +It is important that the `ConfigMap` and `CustomResource` are created in the same [Namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). + +{{% /admonition %}} + +### Script in a PersistentVolume + +If the test script to be executed is contained within a `PersistentVolume`, creation of a [PersistentVolumeClaim](https://kubernetes.io/docs/concepts/storage/persistent-volumes/#persistentvolumeclaims) will be required. +We won't go into the details of PersistentVolumes and PersistentVolumeClaims, but to learn more, you should review the [Kubernetes documentation](https://kubernetes.io/docs/concepts/storage/persistent-volumes/). + +Assume we've created a `PersistentVolumeClaim` named `my-volume-claim` against a `PersistentVolume` containing the test script `/test/test.js`, we can create the file `run-k6-from-volume.yaml` with the following content: + +{{< code >}} + +```yaml +apiVersion: k6.io/v1alpha1 +kind: TestRun +metadata: + name: run-k6-from-volume +spec: + parallelism: 4 + script: + volumeClaim: + name: my-volume-claim + # File is relative to /test/ directory within volume + file: test.js +``` + +{{< /code >}} + +{{% admonition type="caution" %}} + +It is important that the `PersistentVolumeClaim` and `CustomResource` are created in the same [Namespace](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/). + +{{% /admonition %}} + +### Configure the environment + +Not everything should be included directly in your scripts. +Well written scripts will allow for variability to support multiple scenarios and to avoid hard-coding values that tend to change. +These could be anything from passwords to target urls, in addition to system options. + +We can pass this data as [environment variables](https://grafana.com/docs/k6//misc/glossary#environment-variables) for use with each pod executing your script. +This can be defined explicitly within the `TestRun` resource, or by referencing a `ConfigMap` or `Secret`. + +{{< code >}} + +```yaml +apiVersion: k6.io/v1alpha1 +kind: TestRun +metadata: + name: run-k6-with-vars +spec: + parallelism: 4 + script: + configMap: + name: my-test + file: test.js + runner: + env: + - name: MY_CUSTOM_VARIABLE + value: 'this is my variable value' + envFrom: + - configMapRef: + name: my-config-vars + - secretRef: + name: my-secrets-vars +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +The above YAML introduces the `runner` section. This section applies to each pod that will be running a portion of your test, based upon the desired `parallelism`. + +{{% /admonition %}} + +Now, with the referenced resources, our test scripts can [use environment variables](https://grafana.com/docs/k6//using-k6/environment-variables) as in the following: + +```javascript +export function setup() { + console.log(`Variable is set as: ${__ENV.MY_CUSTOM_VARIABLE}`); +} +``` + +### Change command-line arguments + +[k6 options](https://grafana.com/docs/k6//using-k6/k6-options/) can be specified in many ways, one being the command-line. +Specifying options via command-line can still be accomplished when using the operator as shown with the following example: + +{{< code >}} + +```yaml +apiVersion: k6.io/v1alpha1 +kind: TestRun +metadata: + name: run-k6-with-args +spec: + parallelism: 4 + script: + configMap: + name: my-test + file: test.js + arguments: --tag testid=run-k6-with-args --log-format json +``` + +{{< /code >}} + +With the above arguments, we're adding a [test-wide custom tag](https://grafana.com/docs/k6//using-k6/tags-and-groups#test-wide-tags) to metrics and changing the output format of logs to [JSON](https://grafana.com/docs/k6//misc/glossary#json). + +{{% admonition type="note" %}} + +Be sure to visit the [options reference](https://grafana.com/docs/k6//using-k6/k6-options/reference) for a listing of available options. + +{{% /admonition %}} + +## 5. Run your test + +Tests are executed by applying the custom resource `TestRun` to a cluster where the operator is running. +The test configuration is applied as in the following: + +```shell +kubectl apply -f /path/to/your/k6-resource.yaml +``` + +After completing a test run, you need to clean up the test jobs created. +This is done by running the following command: + +```shell +kubectl delete -f /path/to/your/k6-resource.yaml +``` + +{{% admonition type="note" %}} + +If you make use of [real-time results output](https://grafana.com/docs/k6//results-output/real-time), e.g. [Prometheus Remote Write](https://grafana.com/docs/k6//results-output/real-time/prometheus-remote-write), use the `cleanup` option to automatically remove resources upon test completion as with the following example: + +```yaml +apiVersion: k6.io/v1alpha1 +kind: TestRun +metadata: + name: run-k6-with-realtime +spec: + parallelism: 4 + # Removes all resources upon completion + cleanup: post + script: + configMap: + name: my-test + file: test.js + arguments: -o experimental-prometheus-rw +``` + +{{% /admonition %}} + +## 6. When things go wrong + +Sadly nothing works perfectly all the time, so knowing where you can go for help is important. + +Be sure to search the [k6-operator category in the community forum](https://community.grafana.com/c/grafana-k6/k6-operator/73). +k6 has a growing and helpful community of engineers working with k6-operator, so there's a good chance your issue has already been discussed and overcome. +It's also in these forums where you'll be able to get help from members of the k6 development team. + +## See also + +Here are some additional resources to help on your learning journey: + +- [Distributed load testing using Kubernetes with k6 (k6 Office Hours #72)](https://www.youtube.com/watch?v=5d5zxsGz8L4) +- [Demo for k6-operator](https://github.com/javaducky/demo-k6-operator) +- [Blog: Running distributed k6 tests on Kubernetes](https://k6.io/blog/running-distributed-tests-on-k8s/) diff --git a/docs/sources/v0.50.x/testing-guides/running-large-tests.md b/docs/sources/v0.50.x/testing-guides/running-large-tests.md new file mode 100644 index 000000000..48c0430de --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/running-large-tests.md @@ -0,0 +1,369 @@ +--- +title: 'Running large tests' +description: 'How to run large-scale k6 tests without distributed-execution' +weight: 04 +--- + +# Running large tests + +k6 can generate a lot of load from a single machine. With proper monitoring and script optimization, you might be able to run a rather large load test without needing [distributed execution](#distributed-execution). This document explains how to launch such a test, and some of the aspects you should be aware of. + +Maximizing the load a machine generates is a multi-faceted process, which includes: + +- Changing operating system settings to increase the default network and user limits. +- Monitoring the load-generator machine to ensure adequate resource usage. +- Designing efficient tests, with attention to scripting, k6 options, and file uploads. +- Monitoring the test run to detect errors logged by k6, which could indicate limitations of the load generator machine or the [system under test](https://grafana.com/docs/k6//misc/glossary#system-under-test) (SUT). + +A single k6 process efficiently uses all CPU cores on a load generator machine. Depending on the available resources, and with the guidelines described in this document, a single instance of k6 can run 30,000-40,000 simultaneous users (VUs). +In some cases, this number of VUs can generate up to 300,000 HTTP [requests per second](https://grafana.com/docs/k6//misc/glossary#requests-per-second) (RPS). + +Unless you need more than 100,000-300,000 requests per second (6-12M requests per minute), a single instance of k6 is likely sufficient for your needs. +Read on to learn about how to get the most load from a single machine. + +## OS fine-tuning + +Modern operating systems are configured with a fairly low limit of the number of concurrent network connections an application can create. This is a safe default since most programs don't need to open thousands of concurrent TCP connections like k6. However, if we want to use the full network capacity and achieve maximum performance, we need to change some of these default settings. + +On a GNU/Linux machine, run the following commands as the `root` user: + +```bash +sysctl -w net.ipv4.ip_local_port_range="1024 65535" +sysctl -w net.ipv4.tcp_tw_reuse=1 +sysctl -w net.ipv4.tcp_timestamps=1 +ulimit -n 250000 +``` + +These commands enable reusing network connections, increase the limit of network connections, and range of local ports. + +The `sysctl` commands apply immediately for the entire system, and will reset to their default values if you restart the network service or reboot the machine. The `ulimit` command applies only for the current shell session, and you'll need to run it again for it to be set in another shell instance. + +For detailed information about these settings, how to make them permanent, and instructions for macOS, check out our ["Fine tuning OS" article](https://grafana.com/docs/k6//misc/fine-tuning-os). + +## Hardware considerations + +As load increases, you need to pay attention to hardware constraints. + + + +### Network + +Network throughput of the machine is an important consideration when running large tests. Many AWS EC2 machines come with connections of 1Gbit/s, which may limit the load k6 can generate. + +When running the test, use a tool like `iftop` in the terminal to view the amount of network traffic generated in real time. +If the traffic is constant at 1Gbit/s, your test is probably limited by the network card. Consider upgrading to a different EC2 instance. + +### CPU + +k6 is heavily multi-threaded, and will effectively utilize all available CPU cores. + +The amount of CPU you need depends on your test script and associated files. +Regardless of the test file, you can assume that large tests require a significant amount of CPU power. +We recommend that you size the machine to have at least 20% idle cycles (up to 80% used by k6, 20% idle). +If k6 uses 100% of the CPU to generate load, the test will experience throttling limits, and this may +cause the result metrics to have a much larger response time than in reality. + +### Memory + +k6 can use a lot of memory, though [more efficiently than some other load testing tools](https://k6.io/blog/comparing-best-open-source-load-testing-tools#memory-usage). +Memory consumption heavily depends on your test scenarios. To estimate the memory requirement of your test, +run the test on your development machine with 100VUs and multiply the consumed memory by the target number of VUs. + +Simple tests use ~1-5MB per VU. (1000VUs = 1-5GB). +Tests that use file uploads, or load large JS modules, can consume tens of megabytes per VU. +Note that each VU has a copy of all JS modules your test uses. +To share memory between VUs, consider using [SharedArray](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray), or an external data store, such as [Redis](https://grafana.com/docs/k6//javascript-api/k6-experimental/redis). + +If you're using [swap space](https://en.wikipedia.org/wiki/Memory_paging), consider disabling it. If the system runs out of physical memory, the process of swapping memory to much slower secondary storage will have erratic effects on performance and system stability. Which likely will invalidate any test results as the load generator had different performance in different parts of the test. Instead, plan ahead for the memory usage you expect your tests to achieve, and ensure that you have enough physical RAM for the usage to not exceed 90%. + +## Monitoring the load generator + +The previous section described how hardware limits the load your machine can generate. +Keeping this in mind, you should monitor resources while the test runs—especially when running tests for the first time. + +The easiest way to monitor the load generator is to open multiple terminals on the machine: one to run k6, and others to monitor the CPU, memory, and network. +We recommend the following tools: + +- CPU and memory: [htop](https://en.wikipedia.org/wiki/Htop) or [nmon](https://nmon.sourceforge.net/). +- Network: [iftop](https://en.wikipedia.org/wiki/Iftop) or [nmon](https://nmon.sourceforge.net/). + +If you prefer graphical applications, use the system monitoring tool from your OS (e.g. GNOME System +Monitor or Plasma System Monitor), or standalone tools like [SysMonTask](https://github.com/KrispyCamel4u/SysMonTask). + +Here's a screenshot of 3 terminal sessions showing k6, iftop and htop. +![k6 iftop htop](/media/docs/k6-oss/large-scale-testing-3-terminals.png) + +### Monitoring guidelines + +As the test runs, these are good indicators to monitor: + +- **CPU utilization stays within 80%.** + If all CPU cores are 100% utilized during the test run, you might notice performance degradation in your test results. + +- **Network utilization is at an acceptable level.** + Depending on your test, you might expect to fully saturate the network bandwidth, or this might be a signal that your test is bound by the available bandwidth. In other scenarios, you might want to minimize network usage to keep costs down. + +- **Memory utilization doesn't exceed 90%.** + If you're close to exhausting available physical RAM, the system might start swapping memory to disk, which will affect performance and system stability (note that [we recommend disabling swap space altogether](#memory)). In extreme cases, running out of memory on Linux will cause the system to end the k6 process. + +## Error handling should be resilient + +When running [large stress tests](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing), your script shouldn't assume anything about the HTTP response. +An oversight of some scripts is to test with only the [happy path](https://grafana.com/docs/k6//misc/glossary#happy-path 'The default behavior that happens when the system returns no errors') in mind. + +For example, in k6 scripts, we often see something like this _happy path_ check: + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +const res = http.get('https://test.k6.io'); +const checkRes = check(res, { + 'Homepage body size is 11026 bytes': (r) => r.body.length === 11026, +}); +``` + +Code like this runs fine when the SUT is not overloaded and returns proper responses. +When the system starts to fail, this check won't work as expected. + +The issue is that the check assumes that the response always has a body. +But if the server is failing, `r.body` might not exist. +In such cases, the check itself won't work as expected, and an error such as the following will be returned: + +```bash +ERRO[0625] TypeError: Cannot read property 'length' of undefined +``` + +To fix this issue, make your checks resilient to any response type. + +The following change will handle the exception. + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +const res = http.get('https://test.k6.io'); +const checkRes = check(res, { + 'Homepage body size is 11026 bytes': (r) => r.body && r.body.length === 11026, +}); +``` + +{{< /code >}} + +Refer to subsequent sections for a list of [common errors](#common-errors). + +## k6 options to reduce resource use + +The following k6 settings can reduce the performance costs of running large tests. + +### Save memory with `discardResponseBodies` + +By default, k6 loads the response body of the request into memory. This causes much higher memory consumption and is often unnecessary. + +To instruct k6 to not process all response bodies, set `discardResponseBodies` in the options object like this: + +```javascript +export const options = { + discardResponseBodies: true, +}; +``` + +If you need the response body for some requests, override the option with [Params.responseType](https://grafana.com/docs/k6//javascript-api/k6-http/params). + +### When streaming, use `--no-thresholds` and `--no-summary` + +If you're running a local test and streaming results to the cloud (`k6 run -o cloud`), you might want to disable the terminal summary +and local threshold calculation, because the cloud service will display the summary and calculate the thresholds. + +Without these options, the operations will be duplicated by both the local machine and the cloud servers. +This will save some memory and CPU cycles. + +Here are all the mentioned flags, all in one: + +```bash +k6 run scripts/website.js \ + -o cloud \ + --vus=20000 \ + --duration=10m \ + --no-thresholds \ + --no-summary +``` + +## Script optimizations + +To squeeze more performance out of the hardware, consider optimizing the code of the test script itself. +Some of these optimizations involve limiting how you use certain k6 features. +Other optimizations are general to all JavaScript programming. + +### Limit resource-intensive k6 operations + +Some features of the k6 API entail more computation to execute. +To maximize load generation, you might need to limit how your script uses the following. + + + +Checks and groups +: k6 records the result of every individual check and group separately. If you are using many checks and groups, you may consider removing them to boost performance. + +Custom metrics +: Similar to checks, values for custom metrics (Trend, Counter, Gauge and Rate) are recorded separately. Consider minimizing the usage of custom metrics. + +Thresholds with abortOnFail +: If you have configured [abortOnFail thresholds](https://grafana.com/docs/k6//using-k6/thresholds#aborting-a-test-when-a-threshold-is-crossed), k6 needs to evaluate the result constantly to verify that the threshold wasn't crossed. Consider removing this setting. + +URL grouping +: k6 v0.41.0 introduced a change to support time-series metrics. A side effect of this is that every unique URL creates a new time-series object, which may consume more RAM than expected. +: To solve this, use the [URL grouping](https://grafana.com/docs/k6//using-k6/http-requests#url-grouping) feature. + + + +### JavaScript optimizations + +Finally, if the preceding suggestions are insufficient, you might be able to do some general JavaScript optimizations: + +- Avoid deeply nested `for` loops. +- Avoid references to large objects in memory as much as possible. +- Keep external JS dependencies to a minimum. +- Perform tree shaking of the k6 script if you have a build pipeline, and so on. + +Refer to this article about [garbage collection](https://javascript.info/garbage-collection) in the V8 runtime. While the JavaScript VM that k6 uses is very different and runs on Go, the general principles apply. Note that memory leaks are still possible in k6 scripts, and, if not fixed, they might exhaust RAM much more quickly. + +## File upload considerations + +File uploads can affect both resource usage and cloud costs. + + + +Network throughput +: The network throughput of the load generator machine and of the SUT are likely bottlenecks for file uploads. + +Memory +: k6 needs a significant amount of memory when uploading files. Every VU is independent and has its own memory, and files will be copied in all VUs that upload them. You can follow [issue #1931](https://github.com/grafana/k6/issues/1931) that aims to improve this situation. + +Data transfer costs +: k6 can upload a large amount of data in a very short period of time. Make sure you understand the data transfer costs before commencing a large scale test. +: [Outbound Data Transfer is expensive in AWS EC2](https://www.cloudmanagementinsider.com/data-transfer-costs-everything-you-need-to-know/). The price ranges between $0.08 to $0.20 per GB depending on the region. +If you use the cheapest region, the cost is about $0.08 per GB. Uploading 1TB, therefore, costs about $80. A long-running test can cost several hundreds of dollars in data transfer alone. +: If your infrastructure is already hosted on AWS, consider running your load generator machine within the same AWS region and availability zone. In some cases, this traffic will be much cheaper or even free. For additional data cost-saving tips, check this [article on how to reduce data transfer costs on AWS](https://www.stormit.cloud/blog/aws-data-transfer-pricing-how-to-reduce-costs/). Our examples are made with AWS in mind. However, the same suggestions also apply to other cloud providers. + +Virtual server costs +: The AWS EC2 instances are relatively cheap. Even the largest instance we have used in this benchmark (m5.24xlarge) costs only $4.6 per hour. +: Make sure you turn off the load generator servers once you are done with your testing. A forgotten EC2 server might cost thousands of dollars per month. +**Tip:** it's often possible to launch "spot instances" of the same hardware for 10-20% of the cost. + + + +## Common errors + +If you run into errors during the execution, it helps to know whether they were caused by the load generator or by the failing SUT. + +### read: connection reset by peer + +This is caused by the target system resetting the TCP connection. It happens when the Load Balancer or the server itself isn't able to handle the traffic. + +```bash +WARN[0013] Request Failed error="Get http://test.k6.io: read tcp 172.31.72.209:35288->63.32.205.136:80: read: connection reset by peer" +``` + +### context deadline exceeded + +This happens when k6 can send a request, but the target system doesn't respond in time. The default timeout in k6 is 60 seconds. If your system doesn't produce the response in this time frame, this error will appear. + +```bash +WARN[0064] Request Failed error="Get http://test.k6.io: context deadline exceeded" +``` + +### dial tcp 52.18.24.222:80: i/o timeout + +This error is similar to `context deadline exceeded`, but in this case, k6 wasn't even able to make an HTTP request. The target system can't establish a TCP connection. + +```bash +WARN[0057] Request Failed error="Get http://test.k6.io/: dial tcp 52.18.24.222:80: i/o timeout" +``` + +### socket: too many open files + +This error means that the load generator can't open TCP sockets because it reached the limit of open file descriptors. +Make sure that your limit is set sufficiently high. +Refer to the ["Fine tuning OS" article](https://grafana.com/docs/k6//misc/fine-tuning-os#viewing-limits-configuration). + +```bash +WARN[0034] Request Failed error="Get http://test.k6.io/: dial tcp 99.81.83.131:80: socket: too many open files" +``` + +{{% admonition type="note" %}} + +Decide what level of errors is acceptable. At large scale, some errors are always present. +If you make 50M requests with 100 failures, this is generally a good result (0.00002% errors). + +{{% /admonition %}} + +## Benchmarking k6 + +We've executed a few large tests on different EC2 machines to see how much load k6 can generate. Our general observation is that k6 scales proportionally to the hardware. A machine with 2x more resources can generate roughly 2x more traffic. The limit to this scalability is the number of open connections. A single Linux machine can open up to 65,535 sockets per IP address (refer to [RFC 6056](https://www.rfc-editor.org/rfc/rfc6056#section-2) for details). This means that a maximum of 65k requests can be sent simultaneously on a single machine. This is a theoretical limit that will depend on the [configured ephemeral port range](https://en.wikipedia.org/wiki/Ephemeral_port), whether HTTP/2 is in use, which uses request multiplexing and can achieve higher throughput, and other factors. + +Primarily, though, the RPS limit depends on the response time of the SUT. If responses are delivered in 100ms, the theoretical RPS limit is 650,000 for HTTP/1 requests to a single remote address. + +We maintain a [repository](https://github.com/grafana/k6-benchmarks) with some scripts used to benchmark k6 and create reports. These tests are run for every new k6 version, and you can see the results in the [`results/` directory](https://github.com/grafana/k6-benchmarks/tree/master/results). + +## Distributed execution + +In load testing, _distributed execution_ happens when a load is distributed across multiple machines. + +Users often look for the distributed execution mode to run large-scale tests. Although a single k6 instance can generate enormous load, distributed execution is necessary to: + +- Simulate load from multiple locations simultaneously. +- Scale the load of your test beyond what a single machine can handle. + +In k6, you can split the load of a test across multiple k6 instances using the [`execution-segment`](https://grafana.com/docs/k6//using-k6/k6-options/reference#execution-segment) option. For example: + +{{< code >}} + +```bash +## split the load of my-script.js across two machines +k6 run --execution-segment "0:1/2" --execution-segment-sequence "0,1/2,1" my-script.js +k6 run --execution-segment "1/2:1" --execution-segment-sequence "0,1/2,1" my-script.js +``` + +```bash +## split the load of my-script.js across three machines +k6 run --execution-segment "0:1/3" --execution-segment-sequence "0,1/3,2/3,1" my-script.js +k6 run --execution-segment "1/3:2/3" --execution-segment-sequence "0,1/3,2/3,1" my-script.js +k6 run --execution-segment "2/3:1" --execution-segment-sequence "0,1/3,2/3,1" my-script.js +``` + +```bash +## split the load of my-script.js across four machines +k6 run --execution-segment "0:1/4" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js +k6 run --execution-segment "1/4:2/4" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js +k6 run --execution-segment "2/4:3/4" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js +k6 run --execution-segment "3/4:1" --execution-segment-sequence "0,1/4,2/4,3/4,1" my-script.js +``` + +{{< /code >}} + +However, at this moment, the distributed execution mode of k6 is not entirely functional. The current limitations are: + +- k6 does not provide the functionality of a "primary" instance to coordinate the distributed execution of the test. Alternatively, you can use the [k6 REST API](https://grafana.com/docs/k6//misc/k6-rest-api) and [`--paused`](https://grafana.com/docs/k6//using-k6/k6-options/reference#paused) to synchronize the multiple k6 instances' execution. +- Each k6 instance evaluates [Thresholds](https://grafana.com/docs/k6//using-k6/thresholds) independently - excluding the results of the other k6 instances. If you want to disable the threshold execution, use [`--no-thresholds`](https://grafana.com/docs/k6//using-k6/k6-options/reference#no-thresholds). +- k6 reports the metrics individually for each instance. Depending on how you store the load test results, you'll have to aggregate some metrics to calculate them correctly. + +With the limitations mentioned above, we built a [Kubernetes operator](https://github.com/grafana/k6-operator) to distribute the load of a k6 test across a **Kubernetes cluster**. For further instructions, check out [the tutorial for running distributed k6 tests on Kubernetes](https://k6.io/blog/running-distributed-tests-on-k8s/). + +> The k6 goal is to support a native open-source solution for distributed execution. If you want to follow the progress, subscribe to the [distributed execution issue](https://github.com/grafana/k6/issues/140) on GitHub. + +## Large-scale tests in the cloud + +[Grafana Cloud k6](https://grafana.com/products/cloud/k6/), our commercial offering, provides an instant solution for running large-scale tests, among other [benefits](https://grafana.com/docs/grafana-cloud/k6/). + +If you aren't sure whether OSS or Cloud is a better fit for your project, we recommend reading this [white paper](https://k6.io/what-to-consider-when-building-or-buying-a-load-testing-solution) to learn more about the risks and features to consider when building a scalable solution. + +## Read more + +- [Fine tuning OS](https://grafana.com/docs/k6//misc/fine-tuning-os) +- [JavaScript Compatibility Mode](https://grafana.com/docs/k6//using-k6/javascript-compatibility-mode) + +- [A biased comparison of the best open source load testing tools](https://k6.io/blog/comparing-best-open-source-load-testing-tools) +- [White paper: what to consider when building or buying a load testing solution](https://k6.io/what-to-consider-when-building-or-buying-a-load-testing-solution) diff --git a/docs/sources/v0.50.x/testing-guides/test-types/_index.md b/docs/sources/v0.50.x/testing-guides/test-types/_index.md new file mode 100644 index 000000000..48a81518e --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/test-types/_index.md @@ -0,0 +1,95 @@ +--- +title: 'Load test types' +description: 'A series of conceptual articles explaining the different types of load tests. Learn about planning, running, and interpreting different tests for different performance goals.' +weight: -10 +cascade: + noindex: true +--- + +# Load test types + +Many things can go wrong when a system is under load. +The system must run numerous operations simultaneously and respond to different requests from a variable number of users. +To prepare for these performance risks, teams use load testing. + +But a good load-testing strategy requires more than just executing a single script. +Different patterns of traffic create different risk profiles for the application. +For comprehensive preparation, teams must test the system against different _test types_. + +![Overview of load test shapes](/media/docs/k6-oss/chart-load-test-types-overview.png) + +## Different tests for different goals + +Start with smoke tests, then progress to higher loads and longer durations. + +The main types are as follows. Each type has its own article outlining its essential concepts. + +- [**Smoke tests**](https://grafana.com/docs/k6//testing-guides/test-types/smoke-testing) validate that your script works and that the system performs adequately under minimal load. + +- [**Average-load test**](https://grafana.com/docs/k6//testing-guides/test-types/load-testing) assess how your system performs under expected normal conditions. + +- [**Stress tests**](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing) assess how a system performs at its limits when load exceeds the expected average. + +- [**Soak tests**](https://grafana.com/docs/k6//testing-guides/test-types/soak-testing) assess the reliability and performance of your system over extended periods. + +- [**Spike tests**](https://grafana.com/docs/k6//testing-guides/test-types/spike-testing) validate the behavior and survival of your system in cases of sudden, short, and massive increases in activity. + +- [**Breakpoint tests**](https://grafana.com/docs/k6//testing-guides/test-types/breakpoint-testing) gradually increase load to identify the capacity limits of the system. + +{{% admonition type="note" %}} + +In k6 scripts, configure the load configuration using [`options`](https://grafana.com/docs/k6//get-started/running-k6#using-options) or [`scenarios`](https://grafana.com/docs/k6//using-k6/scenarios). This separates workload configuration from iteration logic. + +{{% /admonition %}} + +## Test-type cheat sheet + +The following table provides some broad comparisons. + +| Type | VUs/Throughput | Duration | When? | +| --------------------------------------------------------------------------------------------------- | --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| [Smoke](https://grafana.com/docs/k6//testing-guides/test-types/smoke-testing) | Low | Short (seconds or minutes) | When the relevant system or application code changes. It checks functional logic, baseline metrics, and deviations | +| [Load](https://grafana.com/docs/k6//testing-guides/test-types/load-testing) | Average production | Mid (5-60 minutes) | Often to check system maintains performance with average use | +| [Stress](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing) | High (above average) | Mid (5-60 minutes) | When system may receive above-average loads to check how it manages | +| [Soak](https://grafana.com/docs/k6//testing-guides/test-types/soak-testing) | Average | Long (hours) | After changes to check system under prolonged continuous use | +| [Spike](https://grafana.com/docs/k6//testing-guides/test-types/spike-testing) | Very high | Short (a few minutes) | When the system prepares for seasonal events or receives frequent traffic peaks | +| [Breakpoint](https://grafana.com/docs/k6//testing-guides/test-types/breakpoint-testing) | Increases until break | As long as necessary | A few times to find the upper limits of the system | + +## General recommendations + +When you write and run different test types in k6, consider the following. + +### Start with a smoke test + +Start with a [smoke test](https://grafana.com/docs/k6//testing-guides/test-types/smoke-testing). +Before beginning larger tests, validate that your scripts work as expected and that your system performs well with a few users. + +After you know that the script works and the system responds correctly to minimal load, +you can move on to average-load tests. +From there, you can progress to more complex load patterns. + +### The specifics depend on your use case + +Systems have different architectures and different user bases. As a result, the correct load testing strategy is highly dependent on the risk profile for your organization. Avoid thinking in absolutes. + +For example, k6 can model load by either number of VUs or by number of iterations per second ([open vs. closed](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed)). +When you design your test, consider which pattern makes sense for the type. + +What's more, **no single test type eliminates all risk.** +To assess different failure modes of your system, incorporate multiple test types. +The risk profile of your system determines what test types to emphasize: + +- Some systems are more at risk of longer use, in which case soaks should be prioritized. +- Others are more at risk of intensive use, in which case stress tests should take precedence. + +In any case, **no single test can uncover all issues**. + +What's more, the categories themselves are relative to use cases. A stress test for one application is an average-load test for another. Indeed, no consensus even exists about the names of these test types (each of the following topics provides alternative names). + +### Aim for simple designs and reproducible results + +While the specifics are greatly context-dependent, what's constant is that you want to make results that you can compare and interpret. + +Stick to simple load patterns. For all test types, directions is enough: ramp-up, plateau, ramp-down. + +Avoid "rollercoaster" series where load increases and decreases multiple times. These will waste resources and make it hard to isolate issues. diff --git a/docs/sources/v0.50.x/testing-guides/test-types/breakpoint-testing.md b/docs/sources/v0.50.x/testing-guides/test-types/breakpoint-testing.md new file mode 100644 index 000000000..b213d535a --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/test-types/breakpoint-testing.md @@ -0,0 +1,130 @@ +--- +title: 'Breakpoint testing' +description: 'Breakpoint tests aim to find system limits. They increase load until the system fails.' +weight: 07 +--- + +# Breakpoint testing + +Breakpoint testing aims to find system limits. Reasons you might want to know the limits include: + +- To tune or care for the system's weak spots to relocate those higher limits at higher levels. +- To help plan remediation steps in those cases and prepare for when the system nears those limits. + +In other words, knowing where and how a system starts to fail helps prepare for such limits. + +A breakpoint ramps to unrealistically high numbers. +This test commonly has to be stopped manually or automatically as thresholds start to fail. When these problems appear, the system has reached its limits. + +![Overview of a breakpoint test](/media/docs/k6-oss/chart-breakpoint-test-overview.png) + +The breakpoint test is another test type with no clear naming consensus. +In some testing conversation, it's also known as capacity, point load, and limit testing. + +## When to run a breakpoint test + +Teams execute a breakpoint test whenever they must know their system's diverse limits. Some conditions that may warrant a breakpoint test include the following: + +- The need to know if the system's load expects to grow continuously +- If current resource consumption is considered high +- After significant changes to the code-base or infrastructure. + +How often to run this test type depends on the risk of reaching the system limits and the number of changes to provision infrastructure components. + +Once the breakpoint runs and the system limits have been identified, you can repeat the test after the tuning exercise to validate how it impacted limits. Repeat the test-tune cycle until the team is satisfied. + +## Considerations + +- **Avoid breakpoint tests in elastic cloud environments.** + + The elastic environment may grow as the test moves further, finding only the limit of your cloud account bill. If this test runs on a cloud environment, **turning off elasticity on all the affected components is strongly recommended**. + +- **Increase the load gradually.** + + A sudden increase may make it difficult to pinpoint why and when the system starts to fail. + +- **System failure could mean different things to different teams** + + You might want to identify each of the following failure points: + + - Degraded performance. The response times increased, and user experience decreased. + - Troublesome performance. The response times get to a point where the user experience severely degrades. + - Timeouts. Processes are failing due to extremely high response times. + - Errors. The system starts responding with HTTP error codes. + - System failure. The system collapsed. + +- **You can repeat this test several times** + + Repeating after each tuning might let the you push the system further. + +- **Run breakpoints only when the system is known to perform under all other test types.** + + The breakpoint test might go far if the system performs poorly with the previous testing types. + +## Breakpoint testing in k6 + +The breakpoint test is straightforward. Load slowly ramps up to a considerably high level. +It has no plateau, ramp-down, or other steps. And it generally fails before reaching the indicated point. + +k6 offers two ways to increase the activity: increasing VUs or increasing throughput ([open and closed models](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed)). +Different from other load test types, which should be stopped when the system degrades to a certain point, breakpoint load increases even as the system starts to degrade. +That makes it recommendable to use [ramping-arrival-rate](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-arrival-rate) for a breakpoint test. + +The test keeps increasing load or VUs until it reaches the defined breaking point or system limits, at which point the test stops or is aborted. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + // Key configurations for breakpoint in this section + executor: 'ramping-arrival-rate', //Assure load increase if the system slows + stages: [ + { duration: '2h', target: 20000 }, // just slowly ramp-up to a HUGE load + ], +}; + +export default () => { + const urlRes = http.get('https://test-api.k6.io'); + sleep(1); + // MORE STEPS + // Here you can have more steps or complex script + // Step1 + // Step2 + // etc. +}; +``` + +{{< /code >}} + +![The shape of the breakpoint test as configured in the preceding script](/media/docs/k6-oss/chart-breakpoint-test-k6-script-example.png) + +The test must be stopped before it completes the scheduled execution. +You can stop the test manually or with a threshold: + +- To stop k6 manually in the CLI, press `Ctrl+C` in Linux or Windows, and `Command .` in Mac. +- To stop the test using a threshold, you must define `abortOnFail` as true. + +For details, refer to [Thresholds](https://grafana.com/docs/k6//using-k6/thresholds). + +## Results analysis + +A breakpoint test must cause system failure. +The test helps identify the failure points of our system and how the system behaves once it reaches its limits. + +Once the system limits are identified, the team has two choices: accept them or tune the system. + +If the decision is to accept the limits, the test results help teams prepare and act when the system is nearing such limits. + +These actions could be: + +- Prevent reaching such limits +- Grow system resources +- Implement corrective actions for the system behavior at its limit +- Tune the system to stretch its limits + +If the action taken is to tune the system, tune, then repeat the breakpoint test to find where and whether the system limits moved. + +A team must determine the number of repetitions of the breakpoint test, how much the system can be tuned, and how far its limits can be tuned after each exercise. diff --git a/docs/sources/v0.50.x/testing-guides/test-types/load-testing.md b/docs/sources/v0.50.x/testing-guides/test-types/load-testing.md new file mode 100644 index 000000000..9e57b38f8 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/test-types/load-testing.md @@ -0,0 +1,109 @@ +--- +title: 'Average-load testing' +head_title: 'What is Load Testing? How to create a Load Test in k6' +description: 'An average-load test assesses the performance of your system in terms of concurrent users or requests per second.' +weight: 02 +--- + +# Average-load testing + +An average-load test assesses how the system performs under typical load. Typical load might be a regular day in production or an average moment. + +Average-load tests simulate the number of concurrent users and requests per second that reflect average behaviors in the production environment. This type of test typically increases the throughput or VUs gradually and keeps that average load for some time. Depending on the system's characteristics, the test may stop suddenly or have a short ramp-down period. + +![Overview of an average-load test](/media/docs/k6-oss/chart-average-load-test-overview.png) + +Since “load test” might refer to all types of tests that simulate traffic, this guide uses the name _average-load test_ to avoid confusion. +In some testing conversation, this test also might be called a day-in-life test or volume test. + +## When to run an average-load test + +Average-Load testing helps understand whether a system meets performance goals on a typical day (commonplace load). _Typical day_ here means when an average number of users access the application at the same time, doing normal, average work. + +You should run an average-load test to: + +- Assess the performance of your system under a typical load. +- Identify early degradation signs during the ramp-up or full load periods. +- Assure that the system still meets the performance standards after system changes (code and infrastructure). + +## Considerations + +When you prepare an average-load test, consider the following: + +- **Know the specific number of users and the typical throughput per process in the system.** + + To find this, look through APMs or analytic tools that provide information from the production environment. If you can't access such tools, the business must provide these estimations. + +- **Gradually increase load to the target average.** + + That is, use a _ramp-up period_. This period usually lasts between 5% and 15% of the total test duration. A ramp-up period has many essential uses: + + - It gives your system time to warm up or auto-scale to handle the traffic. + - It lets you compare response times between the low-load and average-load stages. + - If you run tests using our cloud service, a ramp up lets the automated performance alerts understand the expected behavior of your system. + +- **Maintain average for a period longer than the ramp up.** + + Aim for an average duration at least five times longer than the ramp-up to assess the performance trend over a significant period of time. + +- **Consider a ramp-down period.** + + The ramp down is when virtual user activity gradually decreases. The ramp down usually lasts as long as the ramp up or a bit less. + +## Average-load testing in k6 + +{{% admonition type="note" %}} + +If this is your first time running load tests, we recommend starting small or configuring the ramp-up to be slow. Your application and infrastructure might not be as rock solid as you think. We've had thousands of users run load tests that quickly crash their applications (or staging environments). + +{{% /admonition %}} + +The goal of an average-load test is to simulate the average amount of activity on a typical day in production. The pattern follows this sequence: + +1. Increase the script's activity until it reaches the desired number of users and throughput. +1. Maintain that load for a while +1. Depending on the test case, stop the test or let it ramp down gradually. + +Configure load in the `options` object: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + // Key configurations for avg load test in this section + stages: [ + { duration: '5m', target: 100 }, // traffic ramp-up from 1 to 100 users over 5 minutes. + { duration: '30m', target: 100 }, // stay at 100 users for 30 minutes + { duration: '5m', target: 0 }, // ramp-down to 0 users + ], +}; + +export default () => { + const urlRes = http.get('https://test-api.k6.io'); + sleep(1); + // MORE STEPS + // Here you can have more steps or complex script + // Step1 + // Step2 + // etc. +}; +``` + +{{< /code >}} + +This script logic has only one request (to open a web page). Your test behavior likely has more steps. If you would like to see more complex tests that use groups, checks, thresholds, and helper functions, refer to [Examples](https://grafana.com/docs/k6//examples). + +The VU or throughput chart of an average-load test looks similar to this: + +![The shape of the average-load test as configured in the preceding script](/media/docs/k6-oss/chart-average-load-test-k6-script-example.png 'Note that the number of users or throughput starts at 0, gradually ramps up to the desired value, and stays there for the indicated period. Then load ramps down for a short period.') + +## Results analysis + +An initial outcome for the average-load test appears during the ramp-up period to find whether the response time degrades as the load increases. Some systems might even fail during the ramp-up period. + +The test validates if the system's performance and resource consumption stay stable during the period of full load, as some systems may display erratic behavior in this period. + +Once you know your system performs well and survives a typical load, you may need to push it further to determine how it behaves at above-average conditions. Some of these above-average conditions are known as [Stress tests](https://grafana.com/docs/k6//testing-guides/test-types/stress-testing). diff --git a/docs/sources/v0.50.x/testing-guides/test-types/smoke-testing.md b/docs/sources/v0.50.x/testing-guides/test-types/smoke-testing.md new file mode 100644 index 000000000..256e29868 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/test-types/smoke-testing.md @@ -0,0 +1,81 @@ +--- +title: 'Smoke testing' +head_title: 'What is Smoke Testing? How to create a Smoke Test in k6' +description: 'A Smoke test is a minimal load test to run when you create or modify a script.' +weight: 01 +--- + +# Smoke testing + +Smoke tests have a minimal load. +Run them to verify that the system works well under minimal load and to gather baseline performance values. + +This test type consists of running tests with a few VUs — more than 5 VUs could be considered a mini-load test. + +Similarly, the test should execute for a short period, either a low number of [iterations](https://grafana.com/docs/k6//using-k6/k6-options/reference#iterations) or a [duration](https://grafana.com/docs/k6//using-k6/k6-options/reference#duration) from seconds to a few minutes maximum. + +![Overview of a smoke test](/media/docs/k6-oss/chart-smoke-test-overview.png) + +In some testing conversation, smoke tests are also called shakeout tests. + +## When to run a Smoke test + +Teams should run smoke tests whenever a test script is created or updated. Smoke testing should also be done whenever the relevant application code is updated. + +It's a good practice to run a smoke test as a first step, with the following goals: + +- Verify that your test script doesn't have errors. +- Verify that your system doesn't throw any errors (performance or system related) when under minimal load. +- Gather baseline performance metrics of your system’s response under minimal load. +- With simple logic, to serve as a synthetic test to monitor the performance and availability of production environments. + +## Considerations + +When you prepare a smoke test, consider the following: + +- **Each time you create or update a script, run a smoke test** + + Because smoke tests verify test scripts, try to run one every time you create or update a script. Avoid running other test types with untested scripts. + +- **Keep throughput small and duration short** + + Configure your test script to be executed by a small number of VUs (from 2 to 20) with few iterations or brief durations (30 seconds to 3 minutes). + +## Smoke testing in k6 + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 3, // Key for Smoke test. Keep it at 2, 3, max 5 VUs + duration: '1m', // This can be shorter or just a few iterations +}; + +export default () => { + const urlRes = http.get('https://test-api.k6.io'); + sleep(1); + // MORE STEPS + // Here you can have more steps or complex script + // Step1 + // Step2 + // etc. +}; +``` + +{{< /code >}} + +The following script is an example smoke test. You can copy it, change the endpoints, and start testing. For more comprehensive test logic, refer to [Examples](https://grafana.com/docs/k6//examples). +The VU chart of a smoke test should look similar to this. + +![The shape of the smoke test as configured in the preceding script](/media/docs/k6-oss/chart-smoke-test-k6-script-example.png) + +## Results analysis + +The smoke test initially validates that your script runs without errors. If any script-related errors appear, correct the script before trying any more extensive tests. + +On the other hand, if you notice poor performance with these low VU numbers, report it, fix your environment, and try again with a smoke test before any further tests. + +Once your smoke test shows zero errors and the performance results seem acceptable, you can proceed to other test types. diff --git a/docs/sources/v0.50.x/testing-guides/test-types/soak-testing.md b/docs/sources/v0.50.x/testing-guides/test-types/soak-testing.md new file mode 100644 index 000000000..9b5e89342 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/test-types/soak-testing.md @@ -0,0 +1,101 @@ +--- +title: 'Soak testing' +head_title: 'What is Soak Testing? How to create a Soak Test in k6' +description: 'A Soak Test tests the reliability and performance of your system over extended periods of use.' +weight: 05 +--- + +# Soak testing + +Soak testing is another variation of the Average-Load test. It focuses on extended periods, analyzing the following: + +- The system's degradation of performance and resource consumption over extended periods. +- The system's availability and stability during extended periods. + +The soak test differs from an average-load test in test duration. In a soak test, the peak load duration (usually an average amount) extends several hours and even days. +Though the duration is considerably longer, the ramp-up and ramp-down periods of a soak test are the same as an average-load test. + +![Overview of a soak test](/media/docs/k6-oss/chart-soak-test-overview.png) + +In some testing conversation, a soak test might be called an endurance, constant high load, or stamina test. + +## When to perform a Soak test + +Most systems must stay turned on and keep working for days, weeks, and months without intervention. This test verifies the system stability and reliability over extended periods of use. + +This test type checks for common performance defects that show only after extended use. Those problems include response time degradation, memory or other resource leaks, data saturation, and storage depletion. + +## Considerations + +When you prepare to run a soak test, consider the following: + +- **Configure the duration to be considerably longer than any other test.** + + Some typical values are 3, 4, 8, 12, 24, and 48 to 72 hours. + +- **If possible, re-use the average-load test script** + + Changing only the peak durations for the aforementioned values. + +- **Don't run soak tests before running smoke and average-load tests.** + + Each test uncovers different problems. + Running this first may cause confusion and resource waste. + +- **Monitor the backend resources and code efficiency.** + Since we are checking for system degradation, monitoring the backend resources and code efficiency is highly recommended. + Of all test types, backend monitoring is especially important for soak tests. + +## Soak testing in k6 + +The soak test is almost the same as the average-load test. The only difference is the increased duration of the load plateau. + +1. Increase the load until it reaches an average number of users or throughput. +1. Maintain that load for **a considerably longer time.** +1. Finally, depending on the test case, stop or ramp down gradually. + +Configure the load duration in the `options` object: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + // Key configurations for Soak test in this section + stages: [ + { duration: '5m', target: 100 }, // traffic ramp-up from 1 to 100 users over 5 minutes. + { duration: '8h', target: 100 }, // stay at 100 users for 8 hours!!! + { duration: '5m', target: 0 }, // ramp-down to 0 users + ], +}; + +export default () => { + const urlRes = http.get('https://test-api.k6.io'); + sleep(1); + // MORE STEPS + // Here you can have more steps or complex script + // Step1 + // Step2 + // etc. +}; +``` + +{{< /code >}} + +For more complex behavior, refer to [Examples](https://grafana.com/docs/k6//examples). + +Notice that, as in an average-load test, peak load plateaus at 100. VUs. +The difference is in duration. +In this soak, peak load maintains for 8 hours rather than some minutes. + +![The shape of the soak test as configured in the preceding script](/media/docs/k6-oss/chart-soak-test-k6-script-example.png) + +## Results analysis + +If we execute this test after the previous types, we should have a system performing well under previous scenarios. In this test, monitor for changes in any performance metric as time passes. Try to correlate any impact with backend measurement changes that indicate degradation over time. Such changes can be gradual degradations, as mentioned, and sudden changes (improvements, too) in response time and backend hardware resources. Backend resources to check are RAM consumed, CPU, Network, and growth of cloud resources, among others. + +The expected outcome is that the performance and resource utilization of the backend stays stable or within expected variations. + +After you run all the previous test types, you know your system performs well at many different loads: small, average, high, and extended. diff --git a/docs/sources/v0.50.x/testing-guides/test-types/spike-testing.md b/docs/sources/v0.50.x/testing-guides/test-types/spike-testing.md new file mode 100644 index 000000000..fa2e29a3a --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/test-types/spike-testing.md @@ -0,0 +1,94 @@ +--- +title: 'Spike testing' +description: 'Spike tests simulate moments of short, extreme load' +weight: 06 +--- + +# Spike testing + +A spike test verifies whether the system survives and performs under sudden and massive rushes of utilization. + +Spike tests are useful when the system may experience events of sudden and massive traffic. +Examples of such events include ticket sales (Taylor Swift), product launches (PS5), broadcast ads (Super Bowl), process deadlines (tax declaration), and seasonal sales (Black Friday). Also, spikes in traffic could be caused by more frequent events such as rush hours, a particular task, or a use case. + +Spike testing increases to extremely high loads in a very short or non-existent ramp-up time. +Usually, it has no plateau period or is very brief, as real users generally do not stick around doing extra steps in these situations. In the same way, the ramp-down is very fast or non-existent, letting the process iterate only once. + +This test might include different processes than the previous test types, as spikes often aren't part of an average day in production. It may also require adding, removing, or modifying processes on the script that were not in the average-load tests. + +Occasionally, teams should revamp the system to allow or prioritize resources for the high-demand processes during the event. + +![Overview of a spike test](/media/docs/k6-oss/chart-spike-test-overview.png) + +## When to perform a spike test + +This test must be executed when the system expects to receive a sudden rush of activity. + +When the system expects this type of behavior, the spike test helps identify how the system will behave and if it will survive the sudden rush of load. The load is considerably above the average and might focus on a different set of processes than the other test types. + +## Considerations + +When preparing for a spike test, consider the following: + +- **Focus on key processes in this test type.** + + Assess whether the spike in traffic triggers the same or different processes from the other test types. Create test logic accordingly. + +- **The test often won't finish.** + + Errors are common under these scenarios. + +- **Run, tune, repeat.** + + When your system is at risk of spike events, the team must run a spikes test and tune the system several times. + +- **Monitor.** + + Backend monitoring is a must for successful outcomes of this test. + +## Spike testing in k6 + +The key differentiators of the spike test are the simulation of sudden and very high loads. It lacks a plateau (full load) duration or is usually brief. + +Sometimes, the test may require a load plateau for some time. If a plateau is needed, it's generally short. A ramp-down can also be quick or unnecessary as the goal is to suddenly increase the system's load. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + // Key configurations for spike in this section + stages: [ + { duration: '2m', target: 2000 }, // fast ramp-up to a high point + // No plateau + { duration: '1m', target: 0 }, // quick ramp-down to 0 users + ], +}; + +export default () => { + const urlRes = http.get('https://test-api.k6.io'); + sleep(1); + // MORE STEPS + // Add only the processes that will be on high demand + // Step1 + // Step2 + // etc. +}; +``` + +{{< /code >}} + +In a spike test, load quickly increases to an extreme level. +The ramp-down period follows when the test reaches the maximum, returning to 0 quickly. + +A spike test gets its name from the shape of its load when represented graphically. + +![The shape of the spike test as configured in the preceding script](/media/docs/k6-oss/chart-spike-test-k6-script-example.png 'Note that the load goes from 0 to peak in three minutes: an abrupt increase.') + +## Results analysis + +Some performance metrics to assess in spike tests include pod speeds, recovery times after the load rush, time to return to normal, or the behavior on crucial system processes during the overload. + +Finding how the system responds (if it survives) to the sudden rush helps to optimize it to guarantee that it can perform during a real event. In some events, the load is so high that the whole system may have to be optimized to deal with the key processes. In these cases, repeat the test until the system confidence is high. diff --git a/docs/sources/v0.50.x/testing-guides/test-types/stress-testing.md b/docs/sources/v0.50.x/testing-guides/test-types/stress-testing.md new file mode 100644 index 000000000..447399014 --- /dev/null +++ b/docs/sources/v0.50.x/testing-guides/test-types/stress-testing.md @@ -0,0 +1,108 @@ +--- +title: 'Stress testing' +head_title: 'What is Stress Testing? How to create a Stress Test in k6' +description: 'Stress tests assess the limits of your system and stability under extreme conditions.' +weight: 03 +--- + +# Stress testing + +Stress testing assesses how the system performs when loads are heavier than usual. + +The load pattern of a stress test resembles that of an average-load test. The main difference is higher load. +To account for higher load, the ramp-up period takes longer in proportion to the load increase. +Similarly, after the test reaches the desired load, it might last for slightly longer than it would in the average-load test. + +![Overview of a stress test](/media/docs/k6-oss/chart-stress-test-overview.png) + +In some testing conversation, stress tests might also be called rush-hour, surge, or scale tests. + +## When to perform a Stress test + +Stress tests verify the stability and reliability of the system under conditions of heavy use. +Systems may receive higher than usual workloads on unusual moments such as process deadlines, paydays, rush hours, ends of the workweek, and many other behaviors that might cause frequent higher-than-average traffic. + +## Considerations + +When you run a stress test, consider the following: + +- **Load should be higher than what the system experiences on average.** + + Some testers might have default targets for stress tests—say an increase upon average load by 50 or 100 percent—there's no fixed percentage. + + The load simulated in a Stress test depends on the stressful situations that the system may be subject to. Sometimes this may be just a few percentage points above that average. Other times, it can be 50 to 100% higher, as mentioned. Some stressful situations can be twice, triple, or even orders of magnitude higher. + + Define load according to the risk load patterns that the system may receive. + +* **Only run stress tests after running average-load tests.** + + Identify performance issues under average-load tests before trying anything more challenging. + This sequence is essential. + +- **Re-use the Average-Load test script.** + + Modify the parameters to have higher load or VUs. + +- **Expect worse performance compared to average load.** + + This test determines how much the performance degrades with the extra load and whether the system survives it. A well-performant system should respond with consistent response times when handling a constant workload for an extended period. + +## Stress testing in k6 + +The load in a stress test resembles load in an average-load test. +The difference is that it reaches a higher level of load. + +1. Increase the script's activity further in a slower ramp-up until it reaches an above-average number of users or throughput. +1. Maintain that load for a while. +1. Depending on the test case, stop or ramp down gradually. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + // Key configurations for Stress in this section + stages: [ + { duration: '10m', target: 200 }, // traffic ramp-up from 1 to a higher 200 users over 10 minutes. + { duration: '30m', target: 200 }, // stay at higher 200 users for 30 minutes + { duration: '5m', target: 0 }, // ramp-down to 0 users + ], +}; + +export default () => { + const urlRes = http.get('https://test-api.k6.io'); + sleep(1); + // MORE STEPS + // Here you can have more steps or complex script + // Step1 + // Step2 + // etc. +}; +``` + +{{< /code >}} + +For more complex behavior, refer to [Examples](https://grafana.com/docs/k6//examples). + +The VU or throughput chart of a Stress test looks similar to this: + +![The shape of the stress test as configured in the preceding script](/media/docs/k6-oss/chart-stress-test-k6-script-example.png) + +Note that in the same way as the average-load test, the Stress test starts at 0 and increases beyond the point tested in the average-load type. The ramp-up and ramp-down periods are longer to allow a more realistic response. + +{{% admonition type="note" %}} + +Run stress tests only after smoke and average-load tests. Running this test type earlier may be wasteful and make it hard to pinpoint problems if they appear at low volumes or at loads under the average utilization. + +{{% /admonition %}} + +## Results analysis + +Like the average-load test, an initial outcome for the Stress test shows up during the ramp-up period to identify response time degradation as the load increases further than the average utilization. Commonly, the performance degrades, and even the system's stability crashes as we push the system further than the average-load test. + +During the full load period, verification is vital if the system's performance and resource consumption stays stable with a higher load. + +Now that you know that your system can handle outstanding load events, the teams generally check if the system performs well over extended periods. +That is, they run a [Soak test](https://grafana.com/docs/k6//testing-guides/test-types/soak-testing). diff --git a/docs/sources/v0.50.x/using-k6-browser/_index.md b/docs/sources/v0.50.x/using-k6-browser/_index.md new file mode 100644 index 000000000..a39b35f86 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/_index.md @@ -0,0 +1,132 @@ +--- +title: Using k6 browser +heading: 'Browser Module Documentation' +head_title: 'Browser Module Documentation' +description: 'The browser module brings browser automation and end-to-end testing to k6 while supporting core k6 features. Interact with real browsers and collect frontend metrics as part of your k6 tests.' +weight: 03 +--- + +# Using k6 browser + +{{< docs/shared source="k6" lookup="experimental-module.md" version="" >}} + +The [Browser module](https://github.com/grafana/xk6-browser) brings browser automation and end-to-end web testing to k6 while supporting core k6 features. It adds browser-level APIs to interact with browsers and collect frontend performance metrics as part of your k6 tests. + +This module aims to provide rough compatibility with the Playwright API, so you don’t need to learn a completely new API. + +{{% admonition type="note" %}} + +To work with the browser module, make sure you are using the latest [k6 version](https://github.com/grafana/k6/releases), and install a Chromium-based browser on your machine (such as [Google Chrome](https://www.google.com/chrome/)). + +{{% /admonition %}} + +Watch the video below to learn more about k6 browser. + +{{< youtube id="N7VJ9X5yAKo" >}} + +## Use case for browser testing + +The main use case for the browser module is to test performance on the browser level. Browser-level testing provides a way to measure user experience and find issues that are difficult to catch on the protocol level. Browser-level testing can help you answer questions like: + +- When my application is receiving thousands of simultaneous requests from the protocol-level, what happens to the frontend? +- How can I get metrics specific to browsers, like total page load time? +- Are all my elements interactive on the frontend? +- Are there any loading spinners that take a long time to disappear? + +## A simple browser test + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import { check } from 'k6'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const context = browser.newContext(); + const page = context.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + page.locator('input[name="login"]').type('admin'); + page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + + await Promise.all([page.waitForNavigation(), submitButton.click()]); + + check(page, { + header: (p) => p.locator('h2').textContent() == 'Welcome, admin!', + }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +The preceding code launches a Chromium-based browser, visits the application and mimics a user logging in to the application. Once submitted, it checks if the text of the header matches what is expected. + +After running the test, the following [browser metrics](https://grafana.com/docs/k6//using-k6-browser/metrics) will be reported. + +{{< code >}} + +```bash + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: test.js + output: - + + scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): + * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) + + +running (00m01.3s), 0/1 VUs, 1 complete and 0 interrupted iterations +ui ✓ [======================================] 1 VUs 00m01.3s/10m0s 1/1 shared iters + + ✓ header + + browser_data_received.......: 2.6 kB 2.0 kB/s + browser_data_sent...........: 1.9 kB 1.5 kB/s + browser_http_req_duration...: avg=215.4ms min=124.9ms med=126.65ms max=394.64ms p(90)=341.04ms p(95)=367.84ms + browser_http_req_failed.....: 0.00% ✓ 0 ✗ 3 + browser_web_vital_cls.......: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 + browser_web_vital_fcp.......: avg=344.15ms min=269.2ms med=344.15ms max=419.1ms p(90)=404.11ms p(95)=411.6ms + browser_web_vital_fid.......: avg=200µs min=200µs med=200µs max=200µs p(90)=200µs p(95)=200µs + browser_web_vital_inp.......: avg=8ms min=8ms med=8ms max=8ms p(90)=8ms p(95)=8ms + browser_web_vital_lcp.......: avg=419.1ms min=419.1ms med=419.1ms max=419.1ms p(90)=419.1ms p(95)=419.1ms + browser_web_vital_ttfb......: avg=322.4ms min=251ms med=322.4ms max=393.8ms p(90)=379.52ms p(95)=386.66ms + ✓ checks......................: 100.00% ✓ 1 ✗ 0 + data_received...............: 0 B 0 B/s + data_sent...................: 0 B 0 B/s + iteration_duration..........: avg=1.28s min=1.28s med=1.28s max=1.28s p(90)=1.28s p(95)=1.28s + iterations..................: 1 0.777541/s + vus.........................: 1 min=1 max=1 + vus_max.....................: 1 min=1 max=1 +``` + +{{< /code >}} + +This gives you a representation of browser performance, via the web vitals, as well as the HTTP requests that came from the browser. diff --git a/docs/sources/v0.50.x/using-k6-browser/metrics.md b/docs/sources/v0.50.x/using-k6-browser/metrics.md new file mode 100644 index 000000000..83112b2c2 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/metrics.md @@ -0,0 +1,167 @@ +--- +title: 'Browser metrics' +description: 'An overview of the different browser performance metrics that the browser module tracks.' +weight: 03 +--- + +# Browser metrics + +Follow along to learn about: + +- Google's Core Web Vitals and why they are important +- How to analyze the browser metrics output +- How to set thresholds for your browser metrics + +## Google's Core Web Vitals + +The k6 browser module emits metrics based on the [Core Web Vitals](https://web.dev/vitals/#core-web-vitals). +This section provides some conceptual background about the core vitals. +To review the complete list of browser metrics, refer to the section in the [Metrics reference](https://grafana.com/docs/k6//using-k6/metrics/reference#browser). + +Google introduced these metrics to provided unified signals to assess user experience on the web. +The vitals are composed of three important metrics to help user experience when using your web application. + +- Loading performance +- Interactivity +- And visual stability + +### Why web vitals + +The Core Web Vitals are one of [Google's Page Experience Signals](https://developers.google.com/search/docs/appearance/page-experience). A positive page experience naturally leads to better quality and better search engine rankings. These golden metrics help you understand which areas of your frontend application need optimization so your pages can rank higher than similar content. + +Existing browser measures, such as `Load` and `DOMContentLoaded` times, no longer accurately reflect user experience very well. +Relying on these load events does not give the correct metric to analyze critical performance bottlenecks that your page might have. Google's Web Vitals is a better measure of your page performance and its user experience. + +## Understanding the browser metrics output + +When a browser test finishes, k6 reports a top-level overview of the aggregated browser metrics output. +The following snippet is an example: + +{{% admonition type="note" %}} + +As Google also recommends measuring the 75th percentile for each web vital metric, there will still be future tweaks to improve the summary output. + +{{% /admonition %}} + +```bash + browser_data_received.......: 2.6 kB 2.0 kB/s + browser_data_sent...........: 1.9 kB 1.5 kB/s + browser_http_req_duration...: avg=215.4ms min=124.9ms med=126.65ms max=394.64ms p(90)=341.04ms p(95)=367.84ms + browser_http_req_failed.....: 0.00% ✓ 0 ✗ 3 + browser_web_vital_cls.......: avg=0 min=0 med=0 max=0 p(90)=0 p(95)=0 + browser_web_vital_fcp.......: avg=344.15ms min=269.2ms med=344.15ms max=419.1ms p(90)=404.11ms p(95)=411.6ms + browser_web_vital_fid.......: avg=200µs min=200µs med=200µs max=200µs p(90)=200µs p(95)=200µs + browser_web_vital_inp.......: avg=8ms min=8ms med=8ms max=8ms p(90)=8ms p(95)=8ms + browser_web_vital_lcp.......: avg=419.1ms min=419.1ms med=419.1ms max=419.1ms p(90)=419.1ms p(95)=419.1ms + browser_web_vital_ttfb......: avg=322.4ms min=251ms med=322.4ms max=393.8ms p(90)=379.52ms p(95)=386.66ms +``` + +You can also visualize these results in different ways depending on your team's needs. For more information, check out our blog post on [visualizing k6 results](https://k6.io/blog/ways-to-visualize-k6-results/). + +## Set thresholds for your browser metrics + +The browser module can use all key k6 functionalities, such as [Thresholds](https://grafana.com/docs/k6//using-k6/thresholds). + +To set thresholds for your browser metrics: + +1. Add the metric you want to check. +1. Specify its threshold value. + +As the following example shows, you can also pass in different URLs if you're going to set a threshold for other pages, especially when your script contains page navigations. + +{{% admonition type="caution" %}} + +Currently, you can only use URLs to specify thresholds for different pages. If you use [Groups](https://grafana.com/docs/k6//using-k6/tags-and-groups/#groups), the metrics are not correctly grouped as described in [#721](https://github.com/grafana/xk6-browser/issues/721). + +{{% /admonition %}} + +{{< code >}} + +```javascript +export const options = { + thresholds: { + 'browser_web_vital_lcp': ['p(90) < 1000'], + 'browser_web_vital_inp{url:https://test.k6.io/}': ['p(90) < 80'], + 'browser_web_vital_inp{url:https://test.k6.io/my_messages.php}': ['p(90) < 100'], + }, +}; +``` + +{{< /code >}} + +When the test is run, you should see a similar output as the one below. + +```bash + browser_web_vital_inp..........................: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + ✓ { url:https://test.k6.io/ }..................: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s + ✓ { url:https://test.k6.io/my_messages.php }...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s +✓ browser_web_vital_lcp..........................: avg=460.1ms min=460.1ms med=460.1ms max=460.1ms p(90)=460.1ms p(95)=460.1ms + browser_web_vital_ttfb.........................: avg=339.3ms min=258.9ms med=339.3ms max=419.7ms p(90)=403.62ms p(95)=411.66ms +``` + +## Measure custom metrics + +When using the k6 browser `page.evaluate` function, you can call the [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API) to measure the performance of web applications. For example, if you want to measure the time it takes for your users to complete actions, such as a search feature, you can use the [`performance.mark`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/mark) method to add a timestamp in your browser's performance timeline. +Using the [`performance.measure`](https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure) method, you can also measure the time difference between two performance markers. The time duration that `performance.measure` returns can be added as a custom metric in k6 browser using [Trends](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend/). + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import { Trend } from 'k6/metrics'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +const myTrend = new Trend('total_action_time', true); + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/browser.php'); + page.evaluate(() => window.performance.mark('page-visit')); + + page.locator('#checkbox1').check(); + page.locator('#counter-button"]').click(); + page.locator('#text1').fill('This is a test'); + + page.evaluate(() => window.performance.mark('action-completed')); + + // Get time difference between visiting the page and completing the actions + page.evaluate(() => + window.performance.measure('total-action-time', 'page-visit', 'action-completed') + ); + + const totalActionTime = page.evaluate( + () => + JSON.parse(JSON.stringify(window.performance.getEntriesByName('total-action-time')))[0] + .duration + ); + + myTrend.add(totalActionTime); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +After you run the test, you should see a similar output as the one below. + +```bash + iteration_duration..........: avg=1.06s min=1.06s med=1.06s max=1.06s p(90)=1.06s p(95)=1.06s + iterations..................: 1 0.70866/s + total_action_time.............: avg=295.3ms min=295.3ms med=295.3ms max=295.3ms p(90)=295.3ms p(95)=295.3ms +``` diff --git a/docs/sources/v0.50.x/using-k6-browser/migrating-to-k6-v0-46.md b/docs/sources/v0.50.x/using-k6-browser/migrating-to-k6-v0-46.md new file mode 100644 index 000000000..3f9f35ef6 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/migrating-to-k6-v0-46.md @@ -0,0 +1,336 @@ +--- +title: 'Migrating to k6 v0.46' +description: 'A migration guide to ease the process of transitioning to the new k6 browser module version' +weight: 04 +--- + +# Migrating to k6 v0.46 + +This guide outlines the key changes you will need to make when moving your existing k6 browser test scripts to the new [k6 browser module](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser) (bundled with [k6 version 0.46](https://github.com/grafana/k6/releases/tag/v0.46.0)). + +## Key changes + +The updated version introduces notable structural changes in its operation and API, including breaking changes: + +- The [import path](#import-path) for the browser module has switched from `chromium` to [`browser`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-api). +- [Simplified resource management](#simplified-resource-management). The browser module now handles the startup and shutdown of browser processes automatically. The `chromium.launch()`, `chromium.connect()`, and `browser.close()` methods are no longer necessary and have been removed. +- [Browser options](#browser-options) can now only be set using [environment variables](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-options). +- [Scenario options](#scenario-options) must now be defined for running browser tests. +- [Single browser context per iteration](#browser-context-limit). You can now only run a single [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) at a time in the same iteration. + +{{% admonition type="note" %}} + +You no longer need to use the `K6_BROWSER_ENABLED` flag when running browser tests with the `k6` command. + +{{% /admonition %}} + +## Before and after comparison + +Let's start with an overview of the main differences between the previous and new versions of the k6 browser API. + +{{< code >}} + +```javascript +import { chromium } from 'k6/experimental/browser'; + +export default async function () { + const browser = chromium.launch({ + headless: false, + timeout: '60s', + }); + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/'); + page.screenshot({ path: 'screenshot.png' }); + } finally { + page.close(); + browser.close(); + } +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/'); + page.screenshot({ path: 'screenshot.png' }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +## Import path + +With the browser type (specifically `chromium`) now set in [scenario options](#scenario-options), you should directly import the [browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-api) object from the [browser module](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser). + +{{< code >}} + +```javascript +import { chromium } from 'k6/experimental/browser'; +``` + +{{< /code >}} + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +``` + +{{< /code >}} + +## Simplified resource management + +The browser lifecycle is now automatically managed by the browser module, and so the `chromium.launch()`, `chromium.connect()`, and `browser.close()` methods are no longer necessary and have been removed. + +Now, all that is needed is to specify the `browser.type` within the [scenario options](#scenario-options). If the option is set, a browser instance will automatically start at the beginning and close at the end of each test iteration. + +## Browser options + +With the removal of the `chromium.launch()` and `chromium.connect()` methods, setting browsers options is now done by using environment variables. For more information, refer to [Browser Module Options](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-options). + +### Launching a browser + +Before: + +{{< code >}} + + + +```javascript +export default async function () { + const browser = chromium.launch({ + headless: false, + timeout: '60s', + }); +} +``` + +{{< /code >}} + +After: + +{{< code >}} + +```bash +$ K6_BROWSER_HEADLESS=false K6_BROWSER_TIMEOUT='60s' k6 run script.js +``` + +```docker +# WARNING! +# The grafana/k6:master-with-browser image launches a Chrome browser by setting the +# 'no-sandbox' argument. Only use it with trustworthy websites. +# +# As an alternative, you can use a Docker SECCOMP profile instead, and overwrite the +# Chrome arguments to not use 'no-sandbox' such as: +# docker container run --rm -i -e K6_BROWSER_ARGS='' --security-opt seccomp=$(pwd)/chrome.json grafana/k6:master-with-browser run - set "K6_BROWSER_HEADLESS=false" && set "K6_BROWSER_TIMEOUT='60s' " && k6 run script.js +``` + +```powershell +PS C:\k6> $env:K6_BROWSER_HEADLESS="false" ; $env:K6_BROWSER_TIMEOUT='60s' ; k6 run script.js +``` + +{{< /code >}} + +### Connecting to a remote browser + +Before: + +{{< code >}} + + + +```javascript +export default async function () { + const remoteURL = 'REMOTE_URL'; + const browser = chromium.connect(remoteURL); + const page = browser.newPage(); +} +``` + +{{< /code >}} + +After: + +{{< code >}} + +```bash +$ K6_BROWSER_WS_URL='REMOTE_URL' k6 run script.js +``` + +```docker +# WARNING! +# The grafana/k6:master-with-browser image launches a Chrome browser by setting the +# 'no-sandbox' argument. Only use it with trustworthy websites. +# +# As an alternative, you can use a Docker SECCOMP profile instead, and overwrite the +# Chrome arguments to not use 'no-sandbox' such as: +# docker container run --rm -i -e K6_BROWSER_ARGS='' --security-opt seccomp=$(pwd)/chrome.json grafana/k6:master-with-browser run - set "K6_BROWSER_WS_URL='REMOTE_URL'" && set "K6_BROWSER_TIMEOUT='60s' && k6 run script.js +``` + +```powershell +PS C:\k6> $env:K6_BROWSER_WS_URL='REMOTE_URL' ; k6 run script.js +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +The following browser options are no longer supported: `devtools`, `env`, and `proxy` since they weren't providing much value. `slowMo` has been temporarily removed, and we're working on reintroducing it. + +{{% /admonition %}} + +## Scenario options + +You must now set the [executor](https://grafana.com/docs/k6//using-k6/scenarios/executors) and browser type as options in a [scenario](https://grafana.com/docs/k6//using-k6/scenarios) definition. Specifically, the `browser.type` option should be set to `chromium`, as Chromium is the only browser supported. + +{{< code >}} + + + +```javascript +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, +}; +``` + +{{< /code >}} + +## Opening and closing a page + +You can open a new page by using the imported [browser](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-api) object's [browser.newPage()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/newpage) method. You can still use the [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) object as before. + +{{< code >}} + + + +```javascript +export default async function () { + const browser = chromium.launch(); + const page = browser.newPage(); + // ... + page.close(); + browser.close(); +} +``` + +{{< /code >}} + +{{< code >}} + + + +```javascript +export default async function () { + const page = browser.newPage(); + // ... + page.close(); +} +``` + +{{< /code >}} + +The `browser.close()` method has been removed, so you can remove that from your scripts and use [`page.close()`](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page/close) once you're done using the page object. + +{{% admonition type="note" %}} + +Closing of the page is critical for the calculation of accurate Web Vital metrics. See the [browser metrics](https://grafana.com/docs/k6//using-k6-browser/metrics) for more details. + +{{% /admonition %}} + +## Browser context limit + +The new browser implementation limits the usage to a single active [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) per iteration. This change enhances the prediction of resource requirements for a test run and promotes the use of [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) to separate independent browser sessions. + +- A new [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) can be created either with the [browser.newContext()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/newcontext) or [browser.newPage()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/newpage) methods. +- If a new [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) needs to be created, the existing one must be closed first using the [browserContext.close()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext/close) method. +- Alongside these changes, the `browser.contexts()` method has been altered to [browser.context()](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/context) to retrieve the current [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext). + +For instance, the code below will not function as intended, as it attempts to execute two simultaneous [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext)s within the same iteration. + +{{< code >}} + + + +```javascript +export default async function () { + const context1 = browser.newContext(); + // Simultaneous browser contexts are not permitted! + const context2 = browser.newContext(); +} +``` + +{{< /code >}} + +On the other hand, the next example will function correctly by closing the initial [BrowserContext](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/browsercontext) prior to establishing a new one. + +{{< code >}} + + + +```javascript +export default async function () { + const context1 = browser.newContext(); + context1.close(); + + // Since the first browser context is closed, a new browser context can be created. + const context2 = browser.newContext(); + context2.close(); +} +``` + +{{< /code >}} + +These updates make the usage of the API more straightforward for users, aiding in more consistent and automatic resource management. + +For all the details, make sure to review the complete changelog for [k6 version 0.46](https://github.com/grafana/k6/releases/tag/v0.46.0). For more information watch [k6 Office Hours #98](https://www.youtube.com/watch?v=fK6Hpvt0pY0), where we discuss the latest changes in k6 browser, and, as always, ask in [the community forum](https://community.grafana.com/c/grafana-k6/k6-browser/79) if you need help! diff --git a/docs/sources/v0.50.x/using-k6-browser/recommended-practices/_index.md b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/_index.md new file mode 100644 index 000000000..cf40e339b --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/_index.md @@ -0,0 +1,14 @@ +--- +title: 'Recommended practices' +description: 'A list of different examples and recommended practices when working with the k6 browser module' +weight: 100 +weight: 100 +--- + +# Recommended practices + +This section presents some examples and recommended practices when working with the `k6 browser` module to leverage browser automation as part of your k6 tests. + +- [Hybrid approach to performance](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/hybrid-approach-to-performance) +- [Page object model pattern](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/page-object-model-pattern) +- [Selecting elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements) diff --git a/docs/sources/v0.50.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md new file mode 100644 index 000000000..1c71ef1a4 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/hybrid-approach-to-performance.md @@ -0,0 +1,198 @@ +--- +title: 'Hybrid approach to performance' +heading: 'Hybrid performance with k6 browser' +head_title: 'Hybrid performance with k6 browser' +description: 'An example on how to implement a hybrid approach to performance with k6 browser' +weight: 01 +--- + +# Hybrid performance with k6 browser + +An alternative approach to [browser-based load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites/#browser-based-load-testing) that's much less resource-intensive is combining a small number of virtual users for a browser test with a large number of virtual users for a protocol-level test. + +You can achieve hybrid performance in multiple ways, often by using different tools. To simplify the developer experience, you can combine k6 browser with core k6 features to write hybrid tests in a single script. + +## Browser and HTTP test + +The code below shows an example of combining a browser and HTTP test in a single script. While the script exposes the backend to the typical load, it also checks the frontend for any unexpected issues. It also defines thresholds to check both HTTP and browser metrics against pre-defined SLOs. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; +import { browser } from 'k6/experimental/browser'; + +const BASE_URL = __ENV.BASE_URL; + +export const options = { + scenarios: { + load: { + exec: 'getPizza', + executor: 'ramping-vus', + stages: [ + { duration: '5s', target: 5 }, + { duration: '10s', target: 5 }, + { duration: '5s', target: 0 }, + ], + startTime: '10s', + }, + browser: { + exec: 'checkFrontend', + executor: 'constant-vus', + vus: 1, + duration: '30s', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + http_req_failed: ['rate<0.01'], + http_req_duration: ['p(95)<500', 'p(99)<1000'], + browser_web_vital_fcp: ['p(95) < 1000'], + browser_web_vital_lcp: ['p(95) < 2000'], + }, +}; + +export function getPizza() { + const customers = ['123', '456', '789']; + + const restrictions = { + maxCaloriesPerSlice: 500, + mustBeVegetarian: false, + excludedIngredients: ['pepperoni'], + excludedTools: ['knife'], + maxNumberOfToppings: 6, + minNumberOfToppings: 2, + }; + + const res = http.post(`${BASE_URL}/api/pizza`, JSON.stringify(restrictions), { + headers: { + 'Content-Type': 'application/json', + 'X-User-ID': customers[Math.floor(Math.random() * customers.length)], + }, + }); + + check(res, { + 'status is 200': (res) => res.status === 200, + }); +} + +export async function checkFrontend() { + const page = browser.newPage(); + + try { + await page.goto(BASE_URL); + check(page, { + header: page.locator('h1').textContent() == 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + page.waitForTimeout(500); + page.screenshot({ path: `screenshots/${__ITER}.png` }); + + check(page, { + recommendation: page.locator('div#recommendations').textContent() != '', + }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +## Browser and failure injection test + +You can also run a browser test together with a failure injection test by using the [xk6-disruptor](https://github.com/grafana/xk6-disruptor) extension. This approach lets you find issues in your front end if any services it depends on are suddenly injected with failures, such as delays or server errors. + +The following code shows an example of how to introduce faults to a Kubernetes service. At the same time, the `browser` scenario runs to ensure the frontend application is free of any unexpected errors that may not have been handled properly. + +To find out more information about injecting faults to your service, check out the [Get started with xk6-disruptor guide](https://grafana.com/docs/k6//javascript-api/xk6-disruptor/get-started/). + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; +import { browser } from 'k6/experimental/browser'; +import { ServiceDisruptor } from 'k6/x/disruptor'; + +const BASE_URL = __ENV.BASE_URL; + +export const options = { + scenarios: { + disrupt: { + executor: 'shared-iterations', + iterations: 1, + vus: 1, + exec: 'disrupt', + }, + browser: { + executor: 'constant-vus', + vus: 1, + duration: '10s', + startTime: '10s', + exec: 'browser', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + browser_web_vital_fcp: ['p(95) < 1000'], + browser_web_vital_lcp: ['p(95) < 2000'], + }, +}; + +// Add faults to the service by introducing a delay of 1s and 503 errors to 10% of the requests. +const fault = { + averageDelay: '1000ms', + errorRate: 0.1, + errorCode: 503, +}; + +export function disrupt() { + const disruptor = new ServiceDisruptor('pizza-info', 'pizza-ns'); + const targets = disruptor.targets(); + if (targets.length == 0) { + throw new Error('expected list to have one target'); + } + + disruptor.injectHTTPFaults(fault, '20s'); +} + +export async function checkFrontend() { + const page = browser.newPage(); + + try { + await page.goto(BASE_URL); + check(page, { + header: page.locator('h1').textContent() == 'Looking to break out of your pizza routine?', + }); + + await page.locator('//button[. = "Pizza, Please!"]').click(); + page.waitForTimeout(500); + page.screenshot({ path: `screenshots/${__ITER}.png` }); + + check(page, { + recommendation: page.locator('div#recommendations').textContent() != '', + }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +## Recommended practices + +- **Start small**. Start with a small number of browser-based virtual users. A good starting point is to have 10% virtual users or less to monitor the user experience for your end-users, while the script emulates around 90% of traffic from the protocol level. +- **Combine browser test with different load testing types**. To fully understand the impact of different traffic patterns on your end-user experience, experiment with running your browser test with different [load testing types](https://grafana.com/docs/k6//testing-guides/test-types/). +- **Focus on high-risk user journeys as a start**. Identify the high-risk user journeys first so you can start monitoring the web performance metrics for them while your backend applications are being exposed to high traffic or service faults. diff --git a/docs/sources/v0.50.x/using-k6-browser/recommended-practices/page-object-model-pattern.md b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/page-object-model-pattern.md new file mode 100644 index 000000000..7609e35e5 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/page-object-model-pattern.md @@ -0,0 +1,97 @@ +--- +title: 'Page object model' +heading: 'Page object model with k6 browser' +head_title: 'Page object model with k6 browser' +description: 'An example on how to implement page object model design pattern with k6 browser' +weight: 02 +--- + +# Page object model + +When working with large test suites, a popular design pattern to improve your code’s maintainability and readability is the [page object model](https://martinfowler.com/bliki/PageObject.html). + +A page object commonly represents an HTML page or significant elements/components within a page, such as a header or a footer. It is a form of encapsulation that hides the details of the UI structure from other places, such as your test files. Through page object models, any changes you need to make on a specific page or element within a page are constrained into a single place, resulting in ease of maintenance and avoiding code duplication. + +Since k6 browser aims to provide rough compatibility with the Playwright API, you can leverage any existing page objects you have and easily re-use them with your k6 browser tests. + +## Implementation + +Let's take an example of a website with a booking form added to the homepage. Imagine you want to write a test that checks that a user can fill out the booking form successfully. + +To model a page object for the homepage, we've created a page object class called `homepage.js`. Different locators are created inside the constructor so that when the homepage class is instantiated, the page locator elements are ready to be used. + +The `homepage.js` class also contains different methods for: + +- Navigating to the homepage +- Submitting the form +- Getting the verification message + +When locators need to be updated or other specific changes related to the homepage are made, you only need to update the `homepage.js` class. + +{{< code >}} + +```javascript +import { bookingData } from '../data/booking-data.js'; + +export class Homepage { + constructor(page) { + this.page = page; + this.nameField = page.locator('[data-testid="ContactName"]'); + this.emailField = page.locator('[data-testid="ContactEmail"]'); + this.phoneField = page.locator('[data-testid="ContactPhone"]'); + this.subjectField = page.locator('[data-testid="ContactSubject"]'); + this.descField = page.locator('[data-testid="ContactDescription"]'); + this.submitButton = page.locator('#submitContact'); + this.verificationMessage = page.locator('.row.contact h2'); + } + + async goto() { + await this.page.goto('https://myexamplewebsite/'); + } + + async submitForm() { + const { name, email, phone, subject, description } = bookingData; + + this.nameField.type(name); + this.emailField.type(email); + this.phoneField.type(phone); + this.subjectField.type(subject); + this.descField.type(description); + await this.submitButton.click(); + } + + getVerificationMessage() { + return this.verificationMessage.innerText(); + } +} +``` + +{{< /code >}} + +You can import the `Homepage` class within your test class and invoke the methods you need. This makes the code easier to understand and enforces the separation between your test and business logic. + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import { expect } from 'https://jslib.k6.io/k6chaijs/4.3.4.0/index.js'; + +import { Homepage } from '../pages/homepage.js'; +import { bookingData } from '../data/booking-data.js'; + +export default async function () { + const page = browser.newPage(); + + const { name } = bookingData; + + const homepage = new Homepage(page); + await homepage.goto(); + await homepage.submitForm(); + + expect(homepage.getVerificationMessage()).to.contain(name); + + page.close(); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6-browser/recommended-practices/selecting-elements.md b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/selecting-elements.md new file mode 100644 index 000000000..4d4caaa19 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/recommended-practices/selecting-elements.md @@ -0,0 +1,31 @@ +--- +title: 'Selecting elements' +description: 'A guide on how to select elements with the browser module.' +weight: 03 +--- + +# Selecting elements + +Selectors are strings that represents a specific DOM element on the page. When writing browser-level tests, it's recommended to use selectors that are robust to avoid test flakiness when the DOM structure changes. + +Currently, the browser module supports the standard **CSS and XPath selectors**. + +{{% admonition type="note" %}} + +Text-based selectors are currently not supported. This will be supported in future releases. + +{{% /admonition %}} + +## Recommended practices + +The selectors that you choose should not be tightly coupled to any behaviour or styling changes. If your application is prone to changes frequently, it's recommended to use user-facing attributes or custom data attributes as these are not tightly coupled to the element. + +| Selector | Notes | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ✅ `page.locator('[aria-label="Login"]')` | User-facing attributes such as ARIA labels are rarely changed so these are great candidates. | +| ✅ `page.locator('[data-test="login"]')` | Custom data attributes are not tightly coupled to the element. | +| ✅ `page.locator('//button[text()="Submit"]')` | Text selectors are also great as text content rarely changes. While we don't support text-based selectors as of yet, xpath can be used as a workaround. | +| ⚠️ `page.locator('#login-btn')` | Selecting an element via its ID is also recommended if the ID doesn't change. | +| ⚠️ `page.locator('.login-btn')` | Selecting an element via its class name should be kept to a minimum as class names can be duplicated. | +| ❌ `page.locator('button')` | Generic elements are not recommended because this has no context. | +| ❌ `page.locator('/html[1]/body[1]/main[1]` | Absolute paths are not recommended as these are tightly coupled to the DOM structure. | diff --git a/docs/sources/v0.50.x/using-k6-browser/running-browser-tests.md b/docs/sources/v0.50.x/using-k6-browser/running-browser-tests.md new file mode 100644 index 000000000..f5c0a2b0e --- /dev/null +++ b/docs/sources/v0.50.x/using-k6-browser/running-browser-tests.md @@ -0,0 +1,359 @@ +--- +title: 'Running browser tests' +description: 'Follow along to learn how to run a browser test, interact with elements on the page, wait for page navigation, write assertions and run both browser-level and protocol-level tests in a single script.' +weight: 02 +--- + +# Running browser tests + +Follow along to learn how to: + +1. Run a test +2. Interact with elements on your webpage +3. Wait for page navigation +4. Run both browser-level and protocol-level tests in a single script + +{{% admonition type="note" %}} + +With these example snippets, you'll run the test locally with your machine's resources. The browser module is not available within k6 cloud as of yet. + +{{% /admonition %}} + +## Run a test + +To run a simple local script: + +1. Copy the following code, paste it into your favorite editor, and save it as `script.js`. + + Note that providing an `executor` and setting the `browser` scenario option's `type` to `chromium` is mandatory. Please see the [options](https://grafana.com/docs/k6//using-k6/k6-options) and [scenarios](https://grafana.com/docs/k6//using-k6/scenarios) documentation for more information. + + {{< code >}} + + ```javascript + import { browser } from 'k6/experimental/browser'; + + export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, + }; + + export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/'); + page.screenshot({ path: 'screenshots/screenshot.png' }); + } finally { + page.close(); + } + } + ``` + + {{< /code >}} + + The preceding code imports the `browser` [the browser module](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser)), and uses its `newPage` method to open a new page. + + After getting the page, you can interact with it using the [Page](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/page) methods. This example visits a test URL and takes a screenshot of the page. + + Subsequently, the page is closed. This allows for the freeing up of allocated resources and enables the accurate calculation of [Web Vital metrics](https://grafana.com/docs/k6//using-k6-browser/metrics). + + {{% admonition type="note" %}} + + To provide rough compatibility with the Playwright API, the browser module API is also being converted from synchronous to asynchronous. `page.goto()` is now asynchronous so `await` keyword is used to deal with the asynchronous nature of the operation. + + {{% /admonition %}} + +1. Then, run the test on your terminal with this command: + + {{< code >}} + + ```bash + $ k6 run script.js + ``` + + ```docker + # WARNING! + # The grafana/k6:master-with-browser image launches a Chrome browser by setting the + # 'no-sandbox' argument. Only use it with trustworthy websites. + # + # As an alternative, you can use a Docker SECCOMP profile instead, and overwrite the + # Chrome arguments to not use 'no-sandbox' such as: + # docker container run --rm -i -e K6_BROWSER_ARGS='' --security-opt seccomp=$(pwd)/chrome.json grafana/k6:master-with-browser run - k6 run script.js + ``` + + ```powershell + PS C:\k6> k6 run script.js + ``` + + {{< /code >}} + + You can also use [the browser module options](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser#browser-module-options) to customize the launching of a browser process. For instance, you can start a headful browser using the previous test script with this command. + + {{< code >}} + + ```bash + $ K6_BROWSER_HEADLESS=false k6 run script.js + ``` + + ```docker + # WARNING! + # The grafana/k6:master-with-browser image launches a Chrome browser by setting the + # 'no-sandbox' argument. Only use it with trustworthy websites. + # + # As an alternative, you can use a Docker SECCOMP profile instead, and overwrite the + # Chrome arguments to not use 'no-sandbox' such as: + # docker container run --rm -i -e K6_BROWSER_ARGS='' --security-opt seccomp=$(pwd)/chrome.json grafana/k6:master-with-browser run - set "K6_BROWSER_HEADLESS=false" && k6 run script.js + ``` + + ```powershell + PS C:\k6> $env:K6_BROWSER_HEADLESS=false ; k6 run script.js + ``` + + {{< /code >}} + + {{% admonition type="note" %}} + + When using Docker to run k6 browser tests, make sure you have pulled the correct image with Chromium built-in. See [k6 Installation via Docker](https://grafana.com/docs/k6//get-started/installation#docker) for more information. + + {{% /admonition %}} + +1. Optional step: running browser tests in Docker on M1/M2 Macs + + 1. Make sure you’re running [the latest Docker](https://docs.docker.com/engine/install/) version. + + 2. Update [Rosetta]() and export an environment variable with the following: + + ```bash + $ softwareupdate --install-rosetta + $ export DOCKER_DEFAULT_PLATFORM=linux/amd64 + ``` + + 3. Select VirtuoFS in **Settings** > **General** > **VirtuoFS**. + + 4. Enable the Rosetta emulation in **Settings** > **Features in development** > **Use Rosetta for x86/amd64 emulation on Apple Silicon**. + + 5. Restart Docker. + + 6. Run the browser image with the following command (adds the `--platform` flag): + + ```bash + $ docker run --rm -i --platform linux/amd64 -v $(pwd):/home/k6/screenshots -e K6_BROWSER_HEADLESS=false grafana/k6:master-with-browser run - /javascript-api/k6-experimental/browser/locator) object, which you can later use to interact with the element. + +To find out which selectors the browser module supports, check out [Selecting Elements](https://grafana.com/docs/k6//using-k6-browser/recommended-practices/selecting-elements). + +{{% admonition type="note" %}} + +You can also use `page.$()` instead of `page.locator()`. You can find the differences between `page.locator()` and `page.$` in the [Locator API documentation](https://grafana.com/docs/k6//javascript-api/k6-experimental/browser/locator). + +{{% /admonition %}} + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + // Enter login credentials + page.locator('input[name="login"]').type('admin'); + page.locator('input[name="password"]').type('123'); + + page.screenshot({ path: 'screenshots/screenshot.png' }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +The preceding code creates and returns a Locator object with the selectors for both login and password passed as arguments. + +Within the Locator API, various methods such as `type()` can be used to interact with the elements. The `type()` method types a text to an input field. + +## Asynchronous operations + +Since many browser operations happen asynchronously, and to follow the Playwright API more closely, we are working on migrating most of the browser module methods to be asynchronous as well. + +At the moment, methods such as `page.goto()`, `page.waitForNavigation()` and `Element.click()` return [JavaScript promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises), and scripts must be written to handle this properly. + +To avoid timing errors or other race conditions in your script, if you have actions that load up a different page, you need to make sure that you wait for that action to finish before continuing. + +{{< code >}} + +```javascript +import { check } from 'k6'; +import { browser } from 'k6/experimental/browser'; + +export const options = { + scenarios: { + ui: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + thresholds: { + checks: ['rate==1.0'], + }, +}; + +export default async function () { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/my_messages.php'); + + page.locator('input[name="login"]').type('admin'); + page.locator('input[name="password"]').type('123'); + + const submitButton = page.locator('input[type="submit"]'); + + await Promise.all([page.waitForNavigation(), submitButton.click()]); + + check(page, { + header: (p) => p.locator('h2').textContent() == 'Welcome, admin!', + }); + } finally { + page.close(); + } +} +``` + +{{< /code >}} + +The preceding code uses `Promise.all([])` to wait for the two promises to be resolved before continuing. Since clicking the submit button causes page navigation, `page.waitForNavigation()` is needed because the page won't be ready until the navigation completes. This is required because there can be a race condition if these two actions don't happen simultaneously. + +Then, you can use [`check`](https://grafana.com/docs/k6//javascript-api/k6/check) from the k6 API to assert the text content of a specific element. Finally, you close the page and the browser. + +## Run both browser-level and protocol-level tests in a single script + +The real power of the browser module shines when it’s combined with the existing features of k6. A common scenario that you can try is to mix a smaller subset of browser-level tests with a larger protocol-level test which can simulate how your website responds to various performance events. This approach is what we refer to as [hybrid load testing](https://grafana.com/docs/k6//testing-guides/load-testing-websites#hybrid-load-testing) and provides advantages such as: + +- testing real user flows on the frontend while generating a higher load in the backend +- measuring backend and frontend performance in the same test execution +- increased collaboration between backend and frontend teams since the same tool can be used + +To run a browser-level and protocol-level test concurrently, you can use [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). + +{{% admonition type="note" %}} + +Keep in mind that there is an additional performance overhead when it comes to spinning up a browser VU and that the resource usage will depend on the system under test. + +{{% /admonition %}} + +{{< code >}} + +```javascript +import { browser } from 'k6/experimental/browser'; +import { check } from 'k6'; +import http from 'k6/http'; + +export const options = { + scenarios: { + browser: { + executor: 'constant-vus', + exec: 'browserTest', + vus: 1, + duration: '10s', + options: { + browser: { + type: 'chromium', + }, + }, + }, + news: { + executor: 'constant-vus', + exec: 'news', + vus: 20, + duration: '1m', + }, + }, +}; + +export async function browserTest() { + const page = browser.newPage(); + + try { + await page.goto('https://test.k6.io/browser.php'); + + page.locator('#checkbox1').check(); + + check(page, { + 'checkbox is checked': + page.locator('#checkbox-info-display').textContent() === 'Thanks for checking the box', + }); + } finally { + page.close(); + } +} + +export function news() { + const res = http.get('https://test.k6.io/news.php'); + + check(res, { + 'status is 200': (r) => r.status === 200, + }); +} +``` + +{{< /code >}} + +The preceding code contains two scenarios. One for the browser-level test called `browser` and one for the protocol-level test called `news`. Both scenarios are using the [constant-vus executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus) which introduces a constant number of virtual users to execute as many iterations as possible for a specified amount of time. + +Since it's all in one script, this allows for greater collaboration amongst teams. diff --git a/docs/sources/v0.50.x/using-k6/_index.md b/docs/sources/v0.50.x/using-k6/_index.md new file mode 100644 index 000000000..640e2ff3d --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/_index.md @@ -0,0 +1,10 @@ +--- +weight: 02 +title: Using k6 +--- + +# Using k6 + + + +{{< section >}} diff --git a/docs/sources/v0.50.x/using-k6/checks.md b/docs/sources/v0.50.x/using-k6/checks.md new file mode 100644 index 000000000..a341357cf --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/checks.md @@ -0,0 +1,154 @@ +--- +title: 'Checks' +description: 'Checks are like asserts but differ in that they do not halt the execution, instead, they just store the result of the check, pass or fail, and let the script execution continue.' +weight: 03 +--- + +# Checks + +Checks validate boolean conditions in your test. +Testers often use checks to validate that the system is responding with the expected content. +For example, a check could validate that a POST request has a `response.status == 201`, or that the body is of a certain size. + +Checks are similar to what many testing frameworks call an _assert_, but **failed checks do not cause the test to abort or finish with a failed status**. +Instead, k6 keeps track of the rate of failed checks as the test continues to run + +Each check creates a [rate metric](https://grafana.com/docs/k6//using-k6/metrics). +To make a check abort or fail a test, you can combine it with a [Threshold](https://grafana.com/docs/k6//using-k6/thresholds). + +## Check for HTTP response code + +Checks are great for codifying assertions relating to HTTP requests and responses. +For example, this snippet makes sure the HTTP response code is a 200: + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + const res = http.get('http://test.k6.io/'); + check(res, { + 'is status 200': (r) => r.status === 200, + }); +} +``` + +{{< /code >}} + +## Check for text in response body + +Sometimes, even an HTTP 200 response contains an error message. +In these situations, consider adding a check to verify the response body, like this: + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + const res = http.get('http://test.k6.io/'); + check(res, { + 'verify homepage text': (r) => + r.body.includes('Collection of simple web-pages suitable for load testing'), + }); +} +``` + +{{< /code >}} + +## Check for response body size + +To verify the size of the response body, you can use a check like this: + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + const res = http.get('http://test.k6.io/'); + check(res, { + 'body size is 11,105 bytes': (r) => r.body.length == 11105, + }); +} +``` + +{{< /code >}} + +## See percentage of checks that passed + +When a script includes checks, the summary report shows how many of the tests' checks passed: + +{{< code >}} + +```bash +$ k6 run script.js + + ... + ✓ is status 200 + + ... + checks.........................: 100.00% ✓ 1 ✗ 0 + data_received..................: 11 kB 12 kB/s +``` + +{{< /code >}} + +In this example, note that the check "is status 200" succeeded 100% of the times it was called. + +## Add multiple checks + +You can also add multiple checks within a single [check()](https://grafana.com/docs/k6//javascript-api/k6/check) statement: + +{{< code >}} + +```javascript +import { check } from 'k6'; +import http from 'k6/http'; + +export default function () { + const res = http.get('http://test.k6.io/'); + check(res, { + 'is status 200': (r) => r.status === 200, + 'body size is 11,105 bytes': (r) => r.body.length == 11105, + }); +} +``` + +{{< /code >}} + +When this test executes, the output will look something like this: + +{{< code >}} + +```bash +$ k6 run checks.js + + ... + ✓ is status 200 + ✓ body size is 11,105 bytes + + ... + checks.........................: 100.00% ✓ 2 ✗ 0 + data_received..................: 11 kB 20 kB/s +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +When a check fails, the script will continue executing successfully and will not return a 'failed' exit status. +If you need the whole test to fail based on the results of a check, you have to [combine checks with thresholds](https://grafana.com/docs/k6//using-k6/thresholds#fail-a-load-test-using-checks). +This is particularly useful in specific contexts, such as integrating k6 into your CI pipelines or receiving alerts when scheduling your performance tests. + +{{% /admonition %}} + +## Read more + +- [Check Javascript API](https://grafana.com/docs/k6//javascript-api/k6/check) +- [Failing a load test using checks](https://grafana.com/docs/k6//using-k6/thresholds#fail-a-load-test-using-checks) +- [k6chaijs](https://grafana.com/docs/k6//javascript-api/jslib/k6chaijs): use BDD assertions in k6 diff --git a/docs/sources/v0.50.x/using-k6/cookies.md b/docs/sources/v0.50.x/using-k6/cookies.md new file mode 100644 index 000000000..ff31a9eaa --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/cookies.md @@ -0,0 +1,284 @@ +--- +title: 'Cookies' +description: 'k6 will transparently manage the receiving, storage and sending of cookies as described above, so that testing of your cookie-based web site or app will just work.' +weight: 09 +--- + +# Cookies + +HTTP Cookies are used by web sites and apps to store pieces of stateful information on user devices. +Through the `Set-Cookie` HTTP header, a server tells a client what information it wants stored on the user machine. + +The user's browser stores the cookie data and associates it with the hostname of the server. +For each subsequent request to that hostname, it includes the stored cookie data in a +`Cookie` header. + +You can then control more specific rules for whether to send the cookie data or not, +including limiting it to specific subdomains or paths. +You can also set an expiry date on the cookie and tell the browser to send it only over encrypted (SSL/TLS) connections. + +## Cookies with k6 + +For most purposes, k6 transparently manages the reception, storage, and transmission of cookies as described. +Testing of your cookie-based web site or app will _just work_ without requiring any special action of you. + +In some cases, though, you might want more control over cookies. +k6 provides multiple options to do this. +You can: + +- [Directly manipulate HTTP headers](https://grafana.com/docs/k6//javascript-api/k6-http/params), +- Use the more ergonomic cookie API. + +The following section shows how to use the Cookie API. + +## Setting simple cookies + +To simulate that a cookie has previously been set by a browser and is now supposed to be included +in subsequent requests to the server, include the cookie in the `cookies` request parameter: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + http.get('https://httpbin.test.k6.io/cookies', { + cookies: { + my_cookie: 'hello world', + }, + }); +} +``` + +{{< /code >}} + +This applies only to the cookie for the request in question. +It isn't sent for any subsequent requests. +To send the cookie for subsequent requests, add it to a cookie jar. +By default, k6 has a cookie jar for each VU, which you can interact with to set and inspect cookies: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie', 'hello world'); + http.get('https://httpbin.test.k6.io/cookies'); +} +``` + +{{< /code >}} + +The per-VU cookie jar stores all cookies received from the server in a `Set-Cookie` header. +You can also create "local cookie jars" that override the per-VU cookie jar (shown in a subsequent section). + +You can also override a cookie that is already part of the per-VU cookie jar: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie', 'hello world'); + + const cookies = { + my_cookie: { + value: 'hello world 2', + replace: true, + }, + }; + + const res = http.get('https://httpbin.test.k6.io/cookies', { + cookies, + }); + + check(res.json(), { + 'cookie has correct value': (b) => b.cookies.my_cookie == 'hello world 2', + }); +} +``` + +{{< /code >}} + +## Accessing cookies + +To see which cookies were set for a particular response, look in the `cookies` property of the response object: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res = http.get('https://httpbin.test.k6.io/cookies/set?my_cookie=hello%20world', { + redirects: 0, + }); + check(res, { + "has cookie 'my_cookie'": (r) => r.cookies.my_cookie.length > 0, + 'cookie has correct value': (r) => r.cookies.my_cookie[0].value === 'hello world', + }); +} +``` + +{{< /code >}} + +The response object's `cookies` property is a map where the key is the cookie name and the value +is an array of response cookie objects. +This array can support multiple cookies that have the same name but different `domain` or `path` attributes, as specified in [RFC6265](https://tools.ietf.org/html/rfc6265#section-5.3). + +## Properties of a response cookie object + +A response cookie object contains the following properties: + +| Property | Type | Description | +| --------- | --------- | ------------------------------------------------------------------------------------------------------------- | +| name | `string` | the name of the cookie | +| value | `string` | the value of the cookie | +| domain | `string` | domain deciding what hostnames this cookie should be sent to | +| path | `string` | limiting the cookie to only be sent if the path of the request matches this value | +| expires | `string` | when the cookie expires, this needs to be in the RFC1123 format looking like: `Mon, 02 Jan 2006 15:04:05 MST` | +| max_age | `number` | used for the same purpose as expires but defined as the number of seconds a cookie will be valid | +| secure | `boolean` | if true, the cookie will only be sent over an encrypted (SSL/TLS) connection | +| http_only | `boolean` | if true, the cookie would not be exposed to JavaScript in a browser environment | + +## Inspecting a cookie in the jar + +To see which cookies are set and stored in the cookie jar for a particular URL, +use the `cookieForURL()` method of the cookie jar object: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res = http.get('https://httpbin.test.k6.io/cookies/set?my_cookie=hello%20world', { + redirects: 0, + }); + const jar = http.cookieJar(); + const cookies = jar.cookiesForURL('https://httpbin.test.k6.io/'); + check(res, { + "has cookie 'my_cookie'": (r) => cookies.my_cookie.length > 0, + 'cookie has correct value': (r) => cookies.my_cookie[0] === 'hello world', + }); +} +``` + +{{< /code >}} + +The `cookies` object returned by the jar's `cookiesForURL()` method is a map where the key is the +cookie name and the value is an array of cookie values (strings). It is an array to support +multiple cookies having the same name (but different `domain` and/or `path` attributes), which +is part of [RFC6265](https://tools.ietf.org/html/rfc6265#section-5.3). + +## Setting advanced cookies with attributes + +To set cookies that more tightly controls the behavior of the cookie we must add the cookie to a +cookie jar. An example: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const jar = http.cookieJar(); + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie', 'hello world', { + domain: 'httpbin.test.k6.io', + path: '/cookies', + secure: true, + max_age: 600, + }); + const res = http.get('https://httpbin.test.k6.io/cookies'); + check(res, { + 'has status 200': (r) => r.status === 200, + "has cookie 'my_cookie'": (r) => r.cookies.my_cookie[0] !== null, + 'cookie has correct value': (r) => r.cookies.my_cookie[0].value == 'hello world', + }); +} +``` + +{{< /code >}} + +## Local cookie jars + +Besides the per-VU cookie jar, you can also create local cookie jars to override the per-VU +cookie jar on a per-request basis: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const jar = new http.CookieJar(); + + // Add cookie to local jar + const cookieOptions = { + domain: 'httpbin.test.k6.io', + path: '/cookies', + secure: true, + max_age: 600, + }; + jar.set('https://httpbin.test.k6.io/cookies', 'my_cookie', 'hello world', cookieOptions); + + // Override per-VU jar with local jar for the following request + const res = http.get('https://httpbin.test.k6.io/cookies', { jar }); + check(res, { + 'has status 200': (r) => r.status === 200, + "has cookie 'my_cookie'": (r) => r.cookies.my_cookie[0] !== null, + 'cookie has correct value': (r) => r.cookies.my_cookie[0].value == 'hello world', + }); +} +``` + +{{< /code >}} + +## Examples + +{{< code >}} + +```javascript +// Example showing two methods how to log all cookies (with attributes) from a HTTP response. +import http from 'k6/http'; + +function logCookie(c) { + // Here we log the name and value of the cookie along with additional attributes. + // For full list of attributes see: + // https://grafana.com/docs/k6/using-k6/cookies#properties-of-a-response-cookie-object + const output = ` + ${c.name}: ${c.value} + tdomain: ${c.domain} + tpath: ${c.path} + texpires: ${c.expires} + thttpOnly: ${c.http_only} + `; + console.log(output); +} +export default function () { + const res = http.get('https://www.google.com/'); + + // Method 1: Use for-loop and check for non-inherited properties + for (const name in res.cookies) { + if (res.cookies.hasOwnProperty(name) !== undefined) { + logCookie(res.cookies[name][0]); + } + } + + // Method 2: Use ES6 Map to loop over Object entries + new Map(Object.entries(res.cookies)).forEach((v, k) => { + logCookie(v[0]); + }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/environment-variables.md b/docs/sources/v0.50.x/using-k6/environment-variables.md new file mode 100644 index 000000000..350a9986f --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/environment-variables.md @@ -0,0 +1,126 @@ +--- +title: 'Environment variables' +description: 'You can access any environment variables from your k6 script code and use this to supply your VUs with configuration information.' +weight: 11 +--- + +# Environment variables + +Often, scripts need only minor tweaks to be reusable in different contexts. +Rather than creating several separate scripts for these different contexts or environments, you can use [environment variables](https://grafana.com/docs/k6//misc/glossary#environment-variables) to make parts of your script tweakable. + +You can use environment variables for two main purposes: + +1. Passing environment variables to the k6 Script +2. Configuring [k6 Options](https://grafana.com/docs/k6//using-k6/k6-options/how-to) with environment variables + +## Passing environment variables to the k6 script + +In k6, the environment variables are exposed through a global `__ENV` variable, a JS object. +For reference, see the script example below: + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + const res = http.get(`http://${__ENV.MY_HOSTNAME}/`); + sleep(1); +} +``` + +The recommended option to pass environment variables to your testing script is to use one or more [`-e` / `--env` CLI flags](https://grafana.com/docs/k6//using-k6/k6-options/reference#supply-environment-variables) +(this command works the same for all platforms): + +{{< code >}} + +```bash +$ k6 run -e MY_HOSTNAME=test.k6.io script.js +``` + +{{< /code >}} + +> #### ⚠ The `-e` flag does not configure options +> +> This flag just provides variables to the script, which the script can use or ignore. +> For example, `-e K6_ITERATIONS=120` does _not_ configure the script iterations. +> +> Compare this behavior with `K6_ITERATIONS=120 k6 run script.js`, which _does_ set iterations. + +{{< collapse title="Using System Environment Variables" >}} + +A second option to pass environment variables is to source them from the local system. + +```bash +$ MY_HOSTNAME=test.k6.io k6 run script.js +``` + +```windows +C:\k6> set "MY_HOSTNAME=test.k6.io" && k6 run script.js +``` + +```powershell +PS C:\k6> $env:MY_HOSTNAME="test.k6.io"; k6 run script.js +``` + +#### ⚠️ Warning + +By default, passing system environment variables doesn't work for `k6 archive`, `k6 cloud`, and `k6 inspect`. +This is a security measure to avoid the risk of uploading sensitive data to k6 Cloud. +To override this mode, specify [--include-system-env-vars](https://grafana.com/docs/k6//using-k6/k6-options/reference#include-system-env-vars). + +{{< /collapse >}} + +## Configure k6 options with environment variables + +You can also configure k6 [options](https://grafana.com/docs/k6//using-k6/k6-options/how-to) with environment variables. +Consider this script: + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export default function () { + const res = http.get('https://test.k6.io'); + sleep(1); +} +``` + +By default, a local run of this script will execute a single iteration with one virtual user(VU). +To modify the default behavior, pass [k6 options](https://grafana.com/docs/k6//using-k6/k6-options/how-to) as environment variables. +For example, this snippet configures the script to run 10 virtual users for a duration of 10 seconds: + +{{< code >}} + +```bash +$ K6_VUS=10 K6_DURATION=10s k6 run script.js +``` + +```windows +C:\k6> set "K6_VUS=10 K6_DURATION=10s" && k6 run script.js +``` + +```powershell +PS C:\k6> $env:K6_VUS=10 ; $env:K6_DURATION="10s" ; k6 run script.js +``` + +{{< /code >}} + +As the preceding example shows, you need to prefix `K6_` in the environment variable name for k6 to evaluate it as an option parameter. +However, be aware that not all options are supported as environment variables. +You can confirm whether one is by checking the [documentation for each option](https://grafana.com/docs/k6//using-k6/k6-options/reference). + +Note that when you define options in multiple places, there's an [order of precedence](https://grafana.com/docs/k6//using-k6/k6-options/how-to) that determines the option to use. +To ensure you're always working with the highest precedence, use command-line flags instead of environment variables: + +{{< code >}} + +```bash +$ k6 run -e MY_HOSTNAME=test.k6.io --duration 10s --vus 10 script.js +``` + +{{< /code >}} + +## Read more + +- [Manage environment variables in k6 Cloud](https://grafana.com/docs/grafana-cloud/k6/author-run/cloud-scripting-extras/cloud-environment-variables/) diff --git a/docs/sources/v0.50.x/using-k6/execution-context-variables.md b/docs/sources/v0.50.x/using-k6/execution-context-variables.md new file mode 100644 index 000000000..451b1c962 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/execution-context-variables.md @@ -0,0 +1,142 @@ +--- +title: 'Execution context variables' +description: 'k6/execution module provides the capability to get information about the current test execution state inside the test script' +weight: 12 +--- + +# Execution context variables + +In some cases, it's really useful to have information about the script's current test-execution state. For example, you might want to + +- Have different VUs run different test logic +- Use different data for each VU and iteration +- Figure out the stage that a test is in + +To solve these issues, you can use _execution context variables_. + +## k6/execution + +The [k6/execution](https://grafana.com/docs/k6//javascript-api/k6-execution) module exposes details about the current execution state, such as _the name of the currently executed scenario_, _how many VUs are currently active_, and more. +The module provides test-execution information via three properties: + +| Property | Meta-information and execution details about | +| ----------------------------------------------------------------------------------------- | -------------------------------------------- | +| [instance](https://grafana.com/docs/k6//javascript-api/k6-execution#instance) | The current running k6 instance | +| [scenario](https://grafana.com/docs/k6//javascript-api/k6-execution#scenario) | The current running scenario | +| [vu](https://grafana.com/docs/k6//javascript-api/k6-execution#vu) | The current VU and iteration | + +> k6 v0.34.0 introduced the **k6/execution** module. +> If you are using a version k6 that does not have this module, +> refer to the [\_\_VU and \_\_ITER](https://grafana.com/docs/k6//using-k6/execution-context-variables#__vu-and-__iter-discouraged) section. + +## Example: log all context variables + +If you want to experiment with what each context variable looks like as a test runs, +you can copy this template literal into one of your test scripts. + +Note that this omits the `abort` variable, since that function would abort the test. + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +export default function () { + console.log(`Execution context + +Instance info +------------- +Vus active: ${exec.instance.vusActive} +Iterations completed: ${exec.instance.iterationsCompleted} +Iterations interrupted: ${exec.instance.iterationsInterrupted} +Iterations completed: ${exec.instance.iterationsCompleted} +Iterations active: ${exec.instance.vusActive} +Initialized vus: ${exec.instance.vusInitialized} +Time passed from start of run(ms): ${exec.instance.currentTestRunDuration} + +Scenario info +------------- +Name of the running scenario: ${exec.scenario.name} +Executor type: ${exec.scenario.executor} +Scenario start timestamp: ${exec.scenario.startTime} +Percenatage complete: ${exec.scenario.progress} +Iteration in instance: ${exec.scenario.iterationInInstance} +Iteration in test: ${exec.scenario.iterationInTest} + +Test info +--------- +All test options: ${exec.test.options} + +VU info +------- +Iteration id: ${exec.vu.iterationInInstance} +Iteration in scenario: ${exec.vu.iterationInScenario} +VU ID in instance: ${exec.vu.idInInstance} +VU ID in test: ${exec.vu.idInTest} +VU tags: ${exec.vu.tags}`); +} +``` + +{{< /code >}} + +For detailed reference, refer to the [k6/execution module](https://grafana.com/docs/k6//javascript-api/k6-execution). + +## Examples and use cases + +- [Getting unique data once](https://grafana.com/docs/k6//examples/data-parameterization#retrieving-unique-data) +- [Timing operations](https://grafana.com/docs/k6//javascript-api/k6-execution#timing-operations) +- [Executing different code blocks](https://grafana.com/docs/k6//javascript-api/k6-execution#script-naming) + +{{< collapse title="_VU and _ITER (discouraged)" >}} + +⚠️ **\_\_VU** and **\_\_ITER** are both global variables with execution-context information that k6 makes available to the test script. + +### \_\_ITER + +A numeric counter with the current iteration number for a specific VU. Zero-based. + +### \_\_VU + +Current VU number in use. k6 assigns the value incrementally for each new VU instance, starting from one. +The variable is 0 when executing the setup and teardown functions. + +### Running in k6 Cloud + +When you run tests in [k6 Cloud](https://grafana.com/docs/grafana-cloud/k6/), the **\_\_VU** value is per server/load generator. +Read the details in the [cloud docs](https://grafana.com/docs/grafana-cloud/k6/reference/cloud-ips/). + +### Examples + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; +export default function () { + http.get('http://test.k6.io'); + console.log(`VU: ${__VU} - ITER: ${__ITER}`); + sleep(1); +} +``` + +You can use execution-context variables to configure different test behaviors and parameterizations. +A typical use case is a load test that simulates different users performing a login flow. + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; +export default function () { + const email = `user+${__VU}@mail.com`; + const payload = JSON.stringify({ email: email, password: 'test' }); + const params = { headers: { 'Content-Type': 'application/json' } }; + http.post('http://test.k6.io/login', payload, params); + console.log(email); + // .. continue the user flow + sleep(1); +} +``` + +{{< /collapse >}} + +## Grafana Cloud k6 environment variables + +If you run tests in k6 Cloud, you have additional environment variables that tell you the server, load zone, and distribution of the currently running test. +[Read about cloud environment variables](https://grafana.com/docs/grafana-cloud/k6/author-run/cloud-scripting-extras/cloud-environment-variables/). diff --git a/docs/sources/v0.50.x/using-k6/http-debugging.md b/docs/sources/v0.50.x/using-k6/http-debugging.md new file mode 100644 index 000000000..8878c51be --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/http-debugging.md @@ -0,0 +1,102 @@ +--- +title: 'HTTP debugging' +description: "Things don't always work as expected. For those cases there is a handy CLI flag, --http-debug, that is good to be aware of." +_build: + list: false +weight: 17 +--- + +# HTTP debugging + +Things don't always work as expected. For those cases, enabling the [--http-debug](https://grafana.com/docs/k6//using-k6/k6-options/reference#http-debug) option will log HTTP requests and responses to help you debugging the script. + +- `--http-debug` logs the HTTP requests and responses, skipping the body. +- `--http-debug="full"` logs the HTTP requests and responses, including the full body. + +## Example + +Given the following script: + +{{< code >}} + +```bash +import http from "k6/http"; + +export default function() { + http.get("https://google.com/"); +} +``` + +{{< /code >}} + +If we run it using `k6 run --http-debug script.js` we get output that looks like this: + +{{< code >}} + +```bash + /\ |‾‾| /‾‾/ /‾/ + /\ / \ | |_/ / / / + / \/ \ | | / ‾‾\ + / \ | |‾\ \ | (_) | + / __________ \ |__| \__\ \___/ .io + + execution: local + output: - + script: script.js + + duration: -, iterations: 1 + vus: 1, max: 1 + +Request: [----------------------------------------------------------] starting + +GET / HTTP/1.1 +Host: google.com +User-Agent: k6/0.20.0 (https://k6.io/); +Accept-Encoding: gzip + + +RedirectResponse: +HTTP/2.0 302 Found +Content-Length: 267 +Alt-Svc: hq=":443"; ma=2592000; quic=51303432; quic=51303431; quic=51303339; quic=51303335,quic=":443"; ma=2592000; v="42,41,39,35" +Cache-Control: private +Content-Type: text/html; charset=UTF-8 +Date: Thu, 22 Mar 2018 13:39:44 GMT +Location: https://www.google.se/?gfe_rd=cr&dcr=0&ei=ILKzWqiODOaxX_LBi5AG +Referrer-Policy: no-referrer + + +RedirectRequest: +GET /?gfe_rd=cr&dcr=0&ei=ILKzWqiODOaxX_LBi5AG HTTP/1.1 +Host: www.google.se +User-Agent: k6/0.19.0 (https://k6.io/); +Referer: https://google.com/ +Accept-Encoding: gzip + + +Response: +HTTP/2.0 200 OK +Connection: close +Accept-Ranges: none +Alt-Svc: hq=":443"; ma=2592000; quic=51303432; quic=51303431; quic=51303339; quic=51303335,quic=":443"; ma=2592000; v="42,41,39,35" +Cache-Control: private, max-age=0 +Content-Type: text/html; charset=ISO-8859-1 +Date: Thu, 22 Mar 2018 13:39:44 GMT +Expires: -1 +P3p: CP="This is not a P3P policy! See g.co/p3phelp for more info." +Server: gws +Set-Cookie: 1P_JAR=2018-03-22-13; expires=Sat, 21-Apr-2018 13:39:44 GMT; path=/; domain=.google.se +Set-Cookie: NID=126=asTcm5s3-jMa1-pquG09m9lpUrowy-OtzZ9cUomBuKeJePbvwJAZe5wCtiyLITj9_RrlWLf6DTQ8ufpdB39MCRV-zUpfXAUw8XUVMWtgdU1gbtnQ9rssin56333g9Hyo; expires=Fri, 21-Sep-2018 13:39:44 GMT; path=/; domain=.google.se; HttpOnly +Vary: Accept-Encoding +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 1; mode=block + +... + +``` + +{{< /code >}} + +## Read more + +- [Debugging Using a Web Proxy](https://k6.io/blog/k6-load-testing-debugging-using-a-web-proxy/) diff --git a/docs/sources/v0.50.x/using-k6/http-requests.md b/docs/sources/v0.50.x/using-k6/http-requests.md new file mode 100644 index 000000000..500c2d610 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/http-requests.md @@ -0,0 +1,215 @@ +--- +title: 'HTTP Requests' +description: 'Define the HTTP requests and methods you want to use. k6 adds tags to the requests, making it easier to filter results. You can customize tags as you wish.' +weight: 01 +--- + +# HTTP Requests + +When you create a new load test, one of the first steps is to define the HTTP requests that you would like to test. + +## Make HTTP Requests {#making-http-requests} + +A GET request looks like this: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + http.get('http://test.k6.io'); +} +``` + +{{< /code >}} + +For something slightly more complex, this example shows a POST request with an email/password authentication payload: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + const url = 'http://test.k6.io/login'; + const payload = JSON.stringify({ + email: 'aaa', + password: 'bbb', + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + }, + }; + + http.post(url, payload, params); +} +``` + +{{< /code >}} + +## Available methods + +The [http module](https://grafana.com/docs/k6//javascript-api/k6-http) handles all kinds of HTTP requests and methods. + +| Name | Value | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | +| [batch()](https://grafana.com/docs/k6//javascript-api/k6-http/batch) | Issue multiple HTTP requests in parallel (like e.g. browsers tend to do). | +| [del()](https://grafana.com/docs/k6//javascript-api/k6-http/del) | Issue an HTTP DELETE request. | +| [get()](https://grafana.com/docs/k6//javascript-api/k6-http/get) | Issue an HTTP GET request. | +| [head()](https://grafana.com/docs/k6//javascript-api/k6-http/head) | Issue an HTTP HEAD request. | +| [options()](https://grafana.com/docs/k6//javascript-api/k6-http/options) | Issue an HTTP OPTIONS request. | +| [patch()](https://grafana.com/docs/k6//javascript-api/k6-http/patch) | Issue an HTTP PATCH request. | +| [post()](https://grafana.com/docs/k6//javascript-api/k6-http/post) | Issue an HTTP POST request. | +| [put()](https://grafana.com/docs/k6//javascript-api/k6-http/put) | Issue an HTTP PUT request. | +| [request()](https://grafana.com/docs/k6//javascript-api/k6-http/request) | Issue any type of HTTP request. | + +## HTTP Request Tags + +k6 automatically applies [tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#section-tags) to your HTTP requests. +You can use these tags to filter your results and organize your analysis. + +| Name | Description | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| expected_response | By default, response statuses between 200 and 399 are `true`. Change the default behavior with [setResponseCallback](https://grafana.com/docs/k6//javascript-api/k6-http/set-response-callback). | +| group | When the request runs inside a [group](https://grafana.com/docs/k6//javascript-api/k6/group), the tag value is the group name. Default is empty. | +| name | Defaults to URL requested | +| method | Request method (`GET`, `POST`, `PUT` etc.) | +| scenario | When the request runs inside a [scenario](https://grafana.com/docs/k6//using-k6/scenarios), the tag value is the scenario name. Default is `default`. | +| status | response status | +| url | defaults to URL requested | + +The following snippet shows a JSON example of how a test-result data point is logged. +In this example, the metric is the duration of an HTTP request. + +Note how the `tags` object groups data. + +{{< code >}} + +```json +{ + "type": "Point", + "metric": "http_req_duration", + "data": { + "time": "2017-06-02T23:10:29.52444541+02:00", + "value": 586.831127, + "tags": { + "expected_response": "true", + "group": "", + "method": "GET", + "name": "http://test.k6.io", + "scenario": "default", + "status": "200", + "url": "http://test.k6.io" + } + } +} +``` + +{{< /code >}} + +## Group URLs under one tag {#url-grouping} + +By default, tags have a `name` field that holds the value of the request URL. +If your test has dynamic URL paths, you might not want this behavior, which could bring a large number of unique URLs into the metrics stream. +For example, the following code accesses 100 different URLs: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + for (let id = 1; id <= 100; id++) { + http.get(`http://example.com/posts/${id}`); + } +} +// tags.name=\"http://example.com/posts/1\", +// tags.name=\"http://example.com/posts/2\", +``` + +{{< /code >}} + +You might prefer to report this data in a single metric: +To aggregate data from dynamic URLs, explicitly set a `name` tag: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + for (let id = 1; id <= 100; id++) { + http.get(`http://example.com/posts/${id}`, { + tags: { name: 'PostsItemURL' }, + }); + } +} +// tags.name=\"PostsItemURL\", +// tags.name=\"PostsItemURL\", +``` + +{{< /code >}} + +That code would produce JSON output like this: + +{{< code >}} + +```json +{ + "type":"Point", + "metric":"http_req_duration", + "data": { + "time":"2017-06-02T23:10:29.52444541+02:00", + "value":586.831127, + "tags": { + "method":"GET", + "name":"PostsItemURL", + "status":"200", + "url":"http://example.com/1" + } + } +} + +// and + +{ + "type":"Point", + "metric":"http_req_duration", + "data": { + "time":"2017-06-02T23:10:29.58582529+02:00", + "value":580.839273, + "tags": { + "method":"GET", + "name":"PostsItemURL", + "status":"200", + "url":"http://example.com/2" + } + } +} +``` + +{{< /code >}} + +Note how these two objects have the same `name`, despite having different URLs. +If you filter the results for the tag `name: PostsItemURL`, the results include all data points from all 100 URLs. + +As an alternative, you can also use the `http.url` wrapper to set the `name` tag with a string template value: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + for (let id = 1; id <= 100; id++) { + http.get(http.url`http://example.com/posts/${id}`); + } +} +// tags.name="http://example.com/posts/${}", +// tags.name="http://example.com/posts/${}", +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/javascript-compatibility-mode.md b/docs/sources/v0.50.x/using-k6/javascript-compatibility-mode.md new file mode 100644 index 000000000..fa0d8ac68 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/javascript-compatibility-mode.md @@ -0,0 +1,111 @@ +--- +title: JavaScript compatibility mode +menuTitle: JavaScript mode +excerpt: 'k6 supports running test scripts with different ECMAScript compatibility modes using --compatibility-mode' +_build: + list: false +weight: 19 +--- + +# JavaScript compatibility mode + +You can write k6 scripts in various ECMAScript versions: + +- ES6+ JavaScript with ES modules (ESM). +- ES6+ JavaScript with CommonJS modules. + +k6 supports both module types and most ES6+ features in all k6 execution modes: local, distributed, and cloud. + +To enable ES module support, k6 uses [Babel](https://babeljs.io/) internally to transform ESM to CommonJS. The process is as follows: + +![Babel transformation in k6](/media/docs/k6-oss/diagram-grafana-k6-babel-pipeline.png) + +Some users prefer to bundle their test code outside k6. For this reason, k6 offers two JavaScript compatibility modes: + +- [Extended mode](#extended-mode): The default option, supporting ESM and most ES6+ features. +- [Base mode](#base-mode): Limited to CommonJS, excluding the Babel step. + +When running tests, you can change the mode by using the `--compatibility-mode` option: + +| Env | CLI | Code / Config file | Default | +| ----------------------- | ---------------------- | ------------------ | ------------ | +| `K6_COMPATIBILITY_MODE` | `--compatibility-mode` | N/A | `"extended"` | + +## Extended mode + +By default, k6 uses the `--compatibility-mode=extended` mode: + +{{< code >}} + +```bash +$ k6 run script.js +``` + +{{< /code >}} + +As illustrated in the previous diagram, if k6 detects unsupported ES+ features while parsing the test script, it then transforms the script with Babel to polyfill the unsupported features. + +Currently, the k6 Babel transformation only adds ESM support and sets `global` (node's global variable) with the value of `globalThis`. + +## Base mode + +{{< code >}} + +```cli +$ k6 run --compatibility-mode=base script.js +``` + +```env +$ K6_COMPATIBILITY_MODE=base k6 run script.js +``` + +{{< /code >}} + +The base mode omits the Babel transformation step, supporting only ES5.1+ code. You may want to enable this mode if your scripts are already written using only ES5.1 features or were previously transformed by Babel. + +Generally, this mode is not recommended as it offers minor benefits in reducing startup time. + +### CommonJS Example + +{{< code >}} + +```javascript +const http = require('k6/http'); +const k6 = require('k6'); + +module.exports.options = { + vus: 10, + duration: '30s', +}; + +module.exports.default = function () { + http.get('http://test.k6.io'); + k6.sleep(1); +}; +``` + +{{< /code >}} + +> ### ⚠️ About require() +> +> Note that `require()` is a custom k6 implementation of module +> loading, which doesn't behave in the same way as the +> [require() call in Node.js](https://nodejs.org/api/modules.html#modules_require_id). +> Specifically, it only handles loading of built-in k6 modules, +> scripts on the local filesystem, and remote scripts over HTTP(S), +> but it does _not_ support the +> [Node.js module resolution algorithm](https://nodejs.org/api/modules.html#modules_all_together). + +## Bundling with Babel outside of k6 + +The examples below demonstrate the use of Babel with bundlers like [Webpack](https://webpack.js.org/) and [Rollup](https://rollupjs.org/): + +- [k6-template-es6](https://github.com/grafana/k6-template-es6): Template using Webpack and Babel to bundle k6 tests. +- [k6-rollup-example](https://github.com/grafana/k6-rollup-example): Example using Rollup and Babel to bundle a testing project. + +## Read more + +- [Native ESM support](https://github.com/grafana/k6/issues/3265): GitHub issue for native ESM support in k6. This feature aims to eliminate the Babel transformation step within k6. +- [Running large tests](https://grafana.com/docs/k6//testing-guides/running-large-tests): Optimize k6 for better performance. +- [k6 Modules](https://grafana.com/docs/k6//using-k6/modules): Different options to import modules in k6. +- [k6 Archive Command](https://grafana.com/docs/k6//misc/archive): The `k6 archive` command bundles all k6 test dependencies into a `tar` file, which can then be used for execution. It may also reduce the execution startup time. diff --git a/docs/sources/v0.50.x/using-k6/k6-options/_index.md b/docs/sources/v0.50.x/using-k6/k6-options/_index.md new file mode 100644 index 000000000..d1f313d6b --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/k6-options/_index.md @@ -0,0 +1,16 @@ +--- +title: 'Options' +slug: '/k6-options' +descriptiontion: 'Options configure test-run behavior. You can set options in multiple locations. Examples for how to use options, and a complete reference.' +weight: 05 +--- + +# Options + +Options configure test-run behavior. +For example, options are how you define test tags, thresholds, user agents, and the number of virtual users and iterations. + +| On this page | Read about... | +| ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| [How to use options](https://grafana.com/docs/k6//using-k6/k6-options/how-to) | How to set options in different places (with examples), how to override options, and how to access the value of an option as the test runs. | +| [Options reference](https://grafana.com/docs/k6//using-k6/k6-options/reference) | Every option, with examples, defaults, and descriptions. | diff --git a/docs/sources/v0.50.x/using-k6/k6-options/how-to.md b/docs/sources/v0.50.x/using-k6/k6-options/how-to.md new file mode 100644 index 000000000..4e8122426 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/k6-options/how-to.md @@ -0,0 +1,229 @@ +--- +title: 'How to use options' +slug: '/how-to' +descriptiontiontion: 'How to set options in different places (with examples), how to override options, and how to access the value of an option as the test runs.' +weight: 01 +--- + +# How to use options + +k6 provides multiple places to set options: + +- In CLI flags +- In environment variables +- In the script `options` object +- In a configuration file + +Most likely, your use case will determine where you want to set the particular options for a particular test. +You can also access option values as your test runs. + +## Order of precedence + +![Options passed as command-line flags override all other options: defaults < script options < environment variables < command-line flags](/media/docs/k6-oss/order-of-precedence.png) + +You can set options in multiple places. +If there are conflicts, k6 uses the option from the place with the highest _order of precedence_. + +1. First, k6 uses the option's default value. +1. Next, k6 uses the options set in a configuration file via the `--config` flag. +1. Then, k6 uses the script value (if set). +1. After, k6 uses the environment variable (if set). +1. Finally, k6 takes the value from the CLI flag (if set). + +That is, **command-line flags have the highest order of precedence**. + +## Where to set options + +Sometimes, how you set options is a matter of personal preference. +Other times, the context of your test dictates the most sensible place to put your options. + +- **Options in the script to version control and keep tests tidy** + + The script `options` object is generally the best place to put your options. + This provides automatic version control, allows for easy reuse, and lets you modularize your script. + +- **CLI flags to set options on the fly** + + When you want to run a quick test, command-line flags are convenient. + + You can also use command-line flags to override files in your script (as determined by the [order of precedence](#order-of-precedence)). + For example, if your script file sets the test duration at 60 seconds, you could use a CLI flag to run a one-time shorter test. + With a flag like `--duration 30s`, the test would be half as long but otherwise identical. + +- **Environment variables to set options from your build chain** + + For example, you could derive the option from a variable in your Docker container definition, CI UI, or vault—wherever you declare environment variables. + + The [block hostnames](https://grafana.com/docs/k6//using-k6/k6-options/reference#block-hostnames) option is an example of an option that works well with environment variables. + +## Examples of setting options + +The following JS snippets show some examples of how you can set options. + +### Set options in the script + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + hosts: { 'test.k6.io': '1.2.3.4' }, + stages: [ + { duration: '1m', target: 10 }, + { duration: '1m', target: 20 }, + { duration: '1m', target: 0 }, + ], + thresholds: { http_req_duration: ['avg<100', 'p(95)<200'] }, + noConnectionReuse: true, + userAgent: 'MyK6UserAgentString/1.0', +}; + +export default function () { + http.get('http://test.k6.io/'); +} +``` + +{{< /code >}} + +### Set options with environment variables + +You can also set the options from the previous example through environment variables and command-line flags: + +{{< code >}} + +```bash +$ K6_NO_CONNECTION_REUSE=true K6_USER_AGENT="MyK6UserAgentString/1.0" k6 run script.js + +$ k6 run --no-connection-reuse --user-agent "MyK6UserAgentString/1.0" script.js +``` + +```windows +C:\k6> set "K6_NO_CONNECTION_REUSE=true" && set "K6_USER_AGENT=MyK6UserAgentString/1.0" && k6 run script.js + +C:\k6> k6 run --no-connection-reuse --user-agent "MyK6UserAgentString/1.0" script.js +``` + +```powershell +PS C:\k6> $env:K6_NO_CONNECTION_REUSE=true; $env:K6_USER_AGENT="MyK6UserAgentString/1.0"; k6 run script.js + +PS C:\k6> k6 run --no-connection-reuse --user-agent "MyK6UserAgentString/1.0" script.js +``` + +{{< /code >}} + +### Set options from k6 variables + +With the `--env` flag, you can use the CLI to define k6 variables. +Then, you can use the variable to dynamically define an option's value in the script file. + +For example, you could define a variable for your user agent like this: + +```bash +k6 run script.js --env MY_USER_AGENT="hello" +``` + +Then, your script could then set the `userAgent` option based on the variable's value. +This allows for quick configuration. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + userAgent: __ENV.MY_USER_AGENT, +}; + +export default function () { + http.get('http://test.k6.io/'); +} +``` + +{{< /code >}} + +> **Note**: Though this method uses the `--env` flag, this is not the same as using an environment variable. +> For an explanation, refer to the [environment variables document](https://grafana.com/docs/k6//using-k6/environment-variables). + +### Set options with the --config flag + +k6 includes a [default configuration file](https://grafana.com/docs/k6//using-k6/k6-options/reference#config) that you can edit, or you can create a new file and then use a CLI flag to point to that file. +If you use it, the options take the _second lowest order of precedence_ (after defaults). +If you set options anywhere else, they will override the `--config` flag options. + +Use the `--config` flag to declare the file path to your options. + +```bash +k6 run --config options.json script.js +``` + +This command would set test options according to the values in the `options.json` file. + +{{< code >}} + +```json +{ + "hosts": { + "test.k6.io": "1.2.3.4" + }, + "stages": [ + { + "duration": "1m", + "target": 10 + }, + { + "duration": "1m", + "target": 30 + }, + { + "duration": "1m", + "target": 0 + } + ], + "thresholds": { + "http_req_duration": ["avg<100", "p(95)<200"] + }, + "noConnectionReuse": true, + "userAgent": "MyK6UserAgentString/1.0" +} +``` + +{{< /code >}} + +For an alternative way to separate configuration from logic, you can use the `JSON.parse()` method in your script file: + +```javascript +// load test config, used to populate exported options object: +const testConfig = JSON.parse(open('./config/test.json')); +// combine the above with options set directly: +export const options = testConfig; +``` + +## Get an option value from the script + +The `k6/execution` API provides a [test.options](https://grafana.com/docs/k6//javascript-api/k6-execution#test) object. +With `test.options`, you can access the consolidated and derived options of your script as the test runs. + +A common use of this feature is to log the value of a tag, but there are many possibilities. +For example, this script accesses the value of the test's current stage: + +{{< code >}} + +```javascript +import exec from 'k6/execution'; + +export const options = { + stages: [ + { duration: '5s', target: 100 }, + { duration: '5s', target: 50 }, + ], +}; + +export default function () { + console.log(exec.test.options.scenarios.default.stages[0].target); // 100 +} +``` + +{{< /code >}} + +
diff --git a/docs/sources/v0.50.x/using-k6/k6-options/reference.md b/docs/sources/v0.50.x/using-k6/k6-options/reference.md new file mode 100644 index 000000000..cf2da4da4 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/k6-options/reference.md @@ -0,0 +1,1553 @@ +--- +title: 'Options reference' +slug: '/reference' +description: 'A complete list of all k6 options, with descriptions, defaults, and examples of how to set the option in your script, config files, environment variables, or CLI.' +weight: 02 +--- + +# Options reference + +Options define test-run behavior. +Most options can be passed in multiple places. +If an option is defined in multiple places, k6 chooses the value from the highest [order of precedence](https://grafana.com/docs/k6//using-k6/k6-options/how-to#order-of-precedence). + +## Quick reference of options + +Each option has its own detailed reference in a separate section. + +| Option | Description | +| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [Address](#address) | Address of the REST API server | +| [Batch](#batch) | Max number of simultaneous connections of a `http.batch()` call | +| [Batch per host](#batch-per-host) | Max number of simultaneous connections of a `http.batch()` call for a host | +| [Blacklist IP](#blacklist-ip) | Blacklist IP ranges from being called | +| [Block hostnames](#block-hostnames) | Block any requests to specific hostnames | +| [Compatibility mode](#compatibility-mode) | Support running scripts with different ECMAScript modes | +| [Config](#config) | Specify the config file in JSON format to read the options values | +| [Console output](#console-output) | Redirects logs logged by `console` methods to the provided output file | +| [Discard response bodies](#discard-response-bodies) | Specify whether response bodies should be discarded | +| [DNS](#dns) | Configure DNS resolution behavior | +| [Duration](#duration) | A string specifying the total duration of the test run; together with the [vus option](#vus), it's a shortcut for a single [scenario](https://grafana.com/docs/k6//using-k6/scenarios) with a [constant VUs executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus) | +| [Execution segment](#execution-segment) | Limit execution to a segment of the total test | +| [Exit on running](#exit-on-running) | Exits when test reaches the running status | +| [Cloud options](#cloud-options) | An object used to set configuration options for cloud parameters. | +| [Hosts](#hosts) | An object with overrides to DNS resolution | +| [HTTP debug](#http-debug) | Log all HTTP requests and responses | +| [Include system Env vars](#include-system-env-vars) | Pass the real system environment variables to the runtime | +| [Insecure skip TLS verify](#insecure-skip-tls-verify) | A boolean specifying whether k6 should ignore TLS verifications for connections established from code | +| [Iterations](#iterations) | A number specifying a fixed number of iterations to execute of the script; together with the [vus option](#vus), it's a shortcut for a single [scenario](https://grafana.com/docs/k6//using-k6/scenarios) with a [shared iterations executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/shared-iterations) | +| [Linger](#linger) | A boolean specifying whether k6 should linger around after test run completion | +| [Local IPs](#local-ips) | A list of local IPs, IP ranges, and CIDRs from which VUs will make requests | +| [Log output](#log-output) | Configuration about where logs from k6 should be send | +| [LogFormat](#logformat) | Specify the format of the log output | +| [Max redirects](#max-redirects) | The maximum number of HTTP redirects that k6 will follow | +| [Minimum iteration duration](#minimum-iteration-duration) | Specify the minimum duration for every single execution | +| [No color](#no-color) | A boolean specifying whether colored output is disabled | +| [No connection reuse](#no-connection-reuse) | A boolean specifying whether k6 should disable keep-alive connections | +| [No cookies reset](#no-cookies-reset) | This disables resetting the cookie jar after each VU iteration | +| [No summary](#no-summary) | disables the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test) | +| [No setup](#no-setup) | A boolean specifying whether `setup()` function should be run | +| [No teardown](#no-teardown) | A boolean specifying whether `teardown()` function should be run | +| [No thresholds](#no-thresholds) | Disables threshold execution | +| [No usage report](#no-usage-report) | A boolean specifying whether k6 should send a usage report | +| [No VU connection reuse](#no-vu-connection-reuse) | A boolean specifying whether k6 should reuse TCP connections | +| [Paused](#paused) | A boolean specifying whether the test should start in a paused state | +| [Profiling Enabled](#profiling-enabled) | Enables profiling endpoints | +| [Quiet](#quiet) | A boolean specifying whether to show the progress update in the console or not | +| [Results output](#results-output) | Specify the results output | +| [RPS](#rps) | The maximum number of requests to make per second globally (discouraged, use [arrival-rate executors](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed) instead) | +| [Scenarios](#scenarios) | Define advanced execution scenarios | +| [Setup timeout](#setup-timeout) | Specify how long the `setup()` function is allow to run before it's terminated | +| [Show logs](#show-logs) | A boolean specifying whether the cloud logs are printed out to the terminal | +| [Stages](#stages) | A list of objects that specify the target number of VUs to ramp up or down; shortcut option for a single [scenario](https://grafana.com/docs/k6//using-k6/scenarios) with a [ramping VUs executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus) | +| [Supply environment variable](#supply-environment-variables) | Add/override environment variable with `VAR=value` | +| [System tags](#system-tags) | Specify which System Tags will be in the collected metrics | +| [Summary export](#summary-export) | Output the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test) report to a JSON file (discouraged, use [handleSummary()](https://grafana.com/docs/k6//results-output/end-of-test/custom-summary) instead) | +| [Summary trend stats](#summary-trend-stats) | Define stats for trend metrics in the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test) | +| [Summary time unit](#summary-time-unit) | Time unit to be used for _all_ time values in the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test) | +| [Tags](#tags) | Specify tags that should be set test-wide across all metrics | +| [Teardown timeout](#teardown-timeout) | Specify how long the teardown() function is allowed to run before it's terminated | +| [Thresholds](#thresholds) | Configure under what conditions a test is successful or not | +| [Throw](#throw) | A boolean specifying whether to throw errors on failed HTTP requests | +| [TLS auth](#tls-auth) | A list of TLS client certificate configuration objects | +| [TLS cipher suites](#tls-cipher-suites) | A list of cipher suites allowed to be used by in SSL/TLS interactions with a server | +| [TLS version](#tls-version) | String or object representing the only SSL/TLS version allowed | +| [Traces output](#traces-output) | Configuration about where traces from k6 should be sent | +| [User agent](#user-agent) | A string specifying the User-Agent header when sending HTTP requests | +| [Verbose](#verbose) | A boolean specifying whether verbose logging is enabled | +| [VUs](#vus) | A number specifying the number of VUs to run concurrently | + +The following sections detail all available options that you can be specify within a script. + +It also documents the equivalent command line flag, environment variables or option when executing `k6 run ...` +and `k6 cloud ...`, which you can use to override options specified in the code. + +## Address + +Address of the API server. When executing scripts with `k6 run` an HTTP server with a REST API is spun up, +which can be used to control some of the parameters of the test execution. +By default, the server listens on `localhost:6565`. Read more on [k6 REST API](https://grafana.com/docs/k6//misc/k6-rest-api). + +| Env | CLI | Code / Config file | Default | +| --- | ----------------- | ------------------ | ---------------- | +| N/A | `--address`, `-a` | N/A | `localhost:6565` | + +{{< code >}} + +```bash +$ k6 run --address "localhost:3000" script.js +``` + +{{< /code >}} + +## Batch + +The maximum number of simultaneous/parallel connections in total that an +[`http.batch()`](https://grafana.com/docs/k6//javascript-api/k6-http/batch) call in a VU can make. If you have a +`batch()` call that you've given 20 URLs to and `--batch` is set to 15, then the VU will make 15 +requests right away in parallel and queue the rest, executing them as soon as a previous request is +done and a slot opens. Available in both the `k6 run` and the `k6 cloud` commands + +| Env | CLI | Code / Config file | Default | +| ---------- | --------- | ------------------ | ------- | +| `K6_BATCH` | `--batch` | `batch` | `20` | + +{{< code >}} + +```javascript +export const options = { + batch: 15, +}; +``` + +{{< /code >}} + +## Batch per host + +The maximum number of simultaneous/parallel connections for the same hostname that an +[`http.batch()`](https://grafana.com/docs/k6//javascript-api/k6-http/batch) call in a VU can make. If you have a +`batch()` call that you've given 20 URLs to the _same_ hostname and `--batch-per-host` is set to 5, then the VU will make 5 +requests right away in parallel and queue the rest, executing them as soon as a previous request is +done and a slot opens. This will not run more request in parallel then the value of `batch`. Available in both the `k6 run` and the `k6 cloud` commands + +| Env | CLI | Code / Config file | Default | +| ------------------- | ------------------ | ------------------ | ------- | +| `K6_BATCH_PER_HOST` | `--batch-per-host` | `batchPerHost` | `6` | + +{{< code >}} + +```javascript +export const options = { + batchPerHost: 5, +}; +``` + +{{< /code >}} + +## Blacklist IP + +Blacklist IP ranges from being called. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| ------------------ | ---------------- | ------------------ | ------- | +| `K6_BLACKLIST_IPS` | `--blacklist-ip` | `blacklistIPs` | `null` | + +{{< code >}} + +```javascript +export const options = { + blacklistIPs: ['10.0.0.0/8'], +}; +``` + +{{< /code >}} + +## Block hostnames + +Blocks hostnames based on a list of glob match strings. The pattern matching string can have a single +`*` at the beginning such as `*.example.com` that will match anything before that such as +`test.example.com` and `test.test.example.com`. +Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| -------------------- | ------------------- | ------------------ | ------- | +| `K6_BLOCK_HOSTNAMES` | `--block-hostnames` | `blockHostnames` | `null` | + +{{< code >}} + +```javascript +export const options = { + blockHostnames: ['test.k6.io', '*.example.com'], +}; +``` + +{{< /code >}} + +{{< code >}} + +```bash +$ k6 run --block-hostnames="test.k6.io,*.example.com" script.js +``` + +{{< /code >}} + +## Compatibility mode + +Support running scripts with different ECMAScript compatibility modes. + +Read about the different modes on the [JavaScript Compatibility Mode documentation](https://grafana.com/docs/k6//using-k6/javascript-compatibility-mode). + +| Env | CLI | Code / Config file | Default | +| ----------------------- | ---------------------- | ------------------ | ------------ | +| `K6_COMPATIBILITY_MODE` | `--compatibility-mode` | N/A | `"extended"` | + +{{< code >}} + +```bash +$ k6 run --compatibility-mode=base script.js +``` + +{{< /code >}} + +## Config + +Specify the config file in JSON format. +If the config file is not specified, k6 looks for `config.json` in the `loadimpact/k6` directory inside the regular directory for configuration files on the operating system. +Default config locations on different operating systems are as follows: + +| OS | Default Config Path | +| ---------- | --------------------------------------------------------------- | +| Unix-based | `${HOME}/.config/loadimpact/k6/config.json` | +| macOS | `${HOME}/Library/Application Support/loadimpact/k6/config.json` | +| Windows | `%AppData%/loadimpact/k6/config.json` | + +Available in `k6 run` and `k6 cloud` commands: + +| Env | CLI | Code / Config file | Default | +| --- | ------------------------------ | ------------------ | ------- | +| N/A | `--config `, `-c ` | N/A | `null` | + +{{% admonition type="note" %}} + +When running tests in k6 Cloud and using a non-default config.json file, +specify the cloud token inside your config file to authenticate. + +{{% /admonition %}} + +## Console output + +Redirects logs logged by `console` methods to the provided output file. Available in `k6 cloud` and `k6 run` commands. + +| Env | CLI | Code / Config file | Default | +| ------------------- | ------------------ | ------------------ | ------- | +| `K6_CONSOLE_OUTPUT` | `--console-output` | N/A | `null` | + +{{< code >}} + +```bash +$ k6 run --console-output "loadtest.log" script.js +``` + +{{< /code >}} + +## Discard response bodies + +Specify if response bodies should be discarded by changing the default value of +[responseType](https://grafana.com/docs/k6//javascript-api/k6-http/params) to `none` for all HTTP requests. Highly recommended to be set +to `true` and then only for the requests where the response body is needed for scripting +to set [responseType](https://grafana.com/docs/k6//javascript-api/k6-http/params) to `text` or `binary`. Lessens the amount of memory +required and the amount of GC - reducing the load on the testing machine, and probably producing +more reliable test results. + +| Env | CLI | Code / Config file | Default | +| ---------------------------- | --------------------------- | ----------------------- | ------- | +| `K6_DISCARD_RESPONSE_BODIES` | `--discard-response-bodies` | `discardResponseBodies` | `false` | + +{{< code >}} + +```javascript +export const options = { + discardResponseBodies: true, +}; +``` + +{{< /code >}} + +## DNS + +This is a composite option that provides control of DNS resolution behavior with +configuration for cache expiration (TTL), IP selection strategy and IP version +preference. The TTL field in the DNS record is currently not read by k6, so the +`ttl` option allows manual control over this behavior, albeit as a fixed value +for the duration of the test run. + +Note that DNS resolution is done only on new HTTP connections, and by default k6 +will try to reuse connections if HTTP keep-alive is supported. To force a certain +DNS behavior consider enabling the [`noConnectionReuse`](#no-connection-reuse) +option in your tests. + +| Env | CLI | Code / Config file | Default | +| -------- | ------- | ------------------ | ---------------------------------------- | +| `K6_DNS` | `--dns` | `dns` | `ttl=5m,select=random,policy=preferIPv4` | + +Possible `ttl` values are: + +- `0`: no caching at all - each request will trigger a new DNS lookup. +- `inf`: cache any resolved IPs for the duration of the test run. +- any time duration like `60s`, `5m30s`, `10m`, `2h`, etc.; if no unit is specified (e.g. `ttl=3000`), k6 assumes milliseconds. + +Possible `select` values are: + +- `first`: always pick the first resolved IP. +- `random`: pick a random IP for every new connection. +- `roundRobin`: iterate sequentially over the resolved IPs. + +Possible `policy` values are: + +- `preferIPv4`: use IPv4 addresses if available, otherwise fall back to IPv6. +- `preferIPv6`: use IPv6 addresses if available, otherwise fall back to IPv4. +- `onlyIPv4`: only use IPv4 addresses, ignore any IPv6 ones. +- `onlyIPv6`: only use IPv6 addresses, ignore any IPv4 ones. +- `any`: no preference, use all addresses. + +Here are some configuration examples: + +```bash +K6_DNS="ttl=5m,select=random,policy=preferIPv4" k6 cloud script.js +``` + +{{< code >}} + +```javascript +export const options = { + dns: { + ttl: '1m', + select: 'roundRobin', + policy: 'any', + }, +}; +``` + +{{< /code >}} + +## Duration + +A string specifying the total duration a test run should be run for. During this time each +VU will execute the script in a loop. Available in `k6 run` and `k6 cloud` commands. + +Together with the [`vus` option](#vus), `duration` is a shortcut for a single [scenario](https://grafana.com/docs/k6//using-k6/scenarios) with a [constant VUs executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus). + +| Env | CLI | Code / Config file | Default | +| ------------- | ------------------ | ------------------ | ------- | +| `K6_DURATION` | `--duration`, `-d` | `duration` | `null` | + +{{< code >}} + +```javascript +export const options = { + vus: 100, + duration: '3m', +}; +``` + +{{< /code >}} + +## Cloud options + +An object used to set configuration options for Grafana Cloud k6. For more information about available parameters, refer to [Cloud options](https://grafana.com/docs/grafana-cloud/k6/author-run/cloud-scripting-extras/cloud-options/). + +| Env | CLI | Code / Config file | Default | +| --- | --- | ------------------ | ------- | +| N/A | N/A | `cloud` | `null` | + +This is an example of how to specify the test name (test runs/executions with the same name will be +logically grouped for trending and comparison) when streaming results to +[Grafana Cloud k6 Performance Insights](https://grafana.com/docs/grafana-cloud/k6/analyze-results/get-performance-insights/). + +{{< code >}} + +```javascript +export const options = { + cloud: { + name: 'My test name', + }, +}; +``` + +Previously, the `cloud` object was known as `ext.loadimpact`. + +{{< /code >}} + +## Execution segment + +These options specify how to partition the test run and which segment to run. +If defined, k6 will scale the number of VUs and iterations to be run for that +segment, which is useful in distributed execution. Available in `k6 run` and +`k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --- | ------------------------------ | -------------------------- | ------- | +| N/A | `--execution-segment` | `executionSegment` | `"0:1"` | +| N/A | `--execution-segment-sequence` | `executionSegmentSequence` | `"0,1"` | + +For example, to run 25% of a test, you would specify `--execution-segment '25%'`, +which would be equivalent to `--execution-segment '0:1/4'`, i.e. run the first +1/4 of the test. +To ensure that each instance executes a specific segment, also specify the full +segment sequence, e.g. `--execution-segment-sequence '0,1/4,1/2,1'`. +This way one instance could run with `--execution-segment '0:1/4'`, another with +`--execution-segment '1/4:1/2'`, etc. and there would be no overlap between them. + + + +## Exit on running + +A boolean, specifying whether the script should exit once the test status reaches `running`. +When running scripts with `k6 cloud` by default scripts will run until the test reaches a finalized status. +This could be problematic in certain environments (think of Continuous Integration and Delivery pipelines), +since you'd need to wait until the test ends up in a finalized state. + +With this option, you can exit early and let the script run in the background. Available in `k6 cloud` command. + +| Env | CLI | Code / Config file | Default | +| -------------------- | ------------------- | ------------------ | ------- | +| `K6_EXIT_ON_RUNNING` | `--exit-on-running` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 cloud --exit-on-running script.js +``` + +{{< /code >}} + +## Hosts + +An object with overrides to DNS resolution, similar to what you can do with `/etc/hosts` on +Linux/Unix or `C:\Windows\System32\drivers\etc\hosts` on Windows. For instance, you could set +up an override which routes all requests for `test.k6.io` to `1.2.3.4`. + +k6 also supports ways to narrow or widen the scope of your redirects: + +- You can redirect only from or to certain ports. +- Starting from v0.42.0, you can use an asterisk (`*`) as a wild card at the start of the host name to avoid repetition. For example, `*.k6.io` would apply the override for all subdomains of `k6.io`. + +{{% admonition type="note" %}} + +This does not modify the actual HTTP `Host` header, but rather where it will be routed. + +{{% /admonition %}} + +| Env | CLI | Code / Config file | Default | +| --- | --- | ------------------ | ------- | +| N/A | N/A | `hosts` | `null` | + +{{< code >}} + +```javascript +export const options = { + hosts: { + 'test.k6.io': '1.2.3.4', + 'test.k6.io:443': '1.2.3.4:8443', + '*.grafana.com': '1.2.3.4', + }, +}; +``` + +{{< /code >}} + +The preceding code will redirect requests made to `test.k6.io` to `1.2.3.4`, keeping the same port. If the request is done to port `443`, it will redirect it to port `8443` instead. It will also redirect requests to any subdomain of `grafana.com` to `1.2.3.4`. + +## HTTP debug + +Log all HTTP requests and responses. Excludes body by default, to include body use +`--http-debug=full`. Available in `k6 run` and `k6 cloud` commands. + +Read more [here](https://grafana.com/docs/k6//using-k6/http-debugging). + +| Env | CLI | Code / Config file | Default | +| --------------- | --------------------------------------- | ------------------ | ------- | +| `K6_HTTP_DEBUG` | `--http-debug`,
`--http-debug=full` | `httpDebug` | `false` | + +{{< code >}} + +```javascript +export const options = { + httpDebug: 'full', +}; +``` + +{{< /code >}} + +## Include system env vars + +Pass the real system [environment variables](https://grafana.com/docs/k6//using-k6/environment-variables) to the runtime. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --- | --------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------- | +| N/A | `--include-system-env-vars` | N/A | `true` for `k6 run`, but `false` for all other commands to prevent inadvertent sensitive data leaks. | + +{{< code >}} + +```bash +$ k6 run --include-system-env-vars ~/script.js +``` + +{{< /code >}} + +## Insecure skip TLS verify + +A boolean, true or false. When this option is enabled (set to true), all of the verifications that +would otherwise be done to establish trust in a server provided TLS certificate will be ignored. +This only applies to connections created from code, such as HTTP requests. +Available in `k6 run` and `k6 cloud` commands + +| Env | CLI | Code / Config file | Default | +| ----------------------------- | ---------------------------- | ----------------------- | ------- | +| `K6_INSECURE_SKIP_TLS_VERIFY` | `--insecure-skip-tls-verify` | `insecureSkipTLSVerify` | `false` | + +{{< code >}} + +```javascript +export const options = { + insecureSkipTLSVerify: true, +}; +``` + +{{< /code >}} + +## Iterations + +An integer value, specifying the total number of iterations of the `default` function to execute in the test run, as opposed to specifying a duration of time during which the script would run in a loop. Available both in the `k6 run` and `k6 cloud` commands. + +Together with the [`vus` option](#vus), `iterations` is a shortcut for a single [scenario](https://grafana.com/docs/k6//using-k6/scenarios) with a [shared iterations executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/shared-iterations). + +By default, the maximum duration of a `shared-iterations` scenario is 10 minutes. You can adjust that time via the `maxDuration` option of the scenario, or by also specifying the [`duration` global shortcut option](#duration). + +**Note that iterations aren't fairly distributed with this option, and a VU that executes faster will complete more iterations than others.** Each VU will try to complete as many iterations as possible, ["stealing"](https://en.wikipedia.org/wiki/Work_stealing) them from the total number of iterations for the test. So, depending on iteration times, some VUs may complete more iterations than others. If you want guarantees that every VU will complete a specific, fixed number of iterations, [use the per-VU iterations executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/per-vu-iterations). + +| Env | CLI | Code / Config file | Default | +| --------------- | -------------------- | ------------------ | ------- | +| `K6_ITERATIONS` | `--iterations`, `-i` | `iterations` | `1` | + +{{< code >}} + +```javascript +export const options = { + vus: 5, + iterations: 10, +}; +``` + +{{< /code >}} + +Or, to run 10 VUs 10 times each: + +{{< code >}} + +```javascript +export const options = { + vus: 10, + iterations: 100, +}; +``` + +{{< /code >}} + +## Linger + +A boolean, true or false, specifying whether the k6 process should linger around after test +run completion. Available in the `k6 run` command. + +| Env | CLI | Code / Config file | Default | +| ----------- | ---------------- | ------------------ | ------- | +| `K6_LINGER` | `--linger`, `-l` | `linger` | `false` | + +{{< code >}} + +```javascript +export const options = { + linger: true, +}; +``` + +{{< /code >}} + +## Local IPs + +A list of IPs, IP ranges and CIDRs from which VUs will make requests. The IPs will be sequentially +given out to VUs. This option doesn't change anything on the OS level so the IPs need to be already +configured on the OS level for k6 to use them. Also IPv4 CIDRs with more than 2 +IPs don't include the first and last IP as they are reserved for referring to the network itself and +the broadcast address respectively. + +This option can be used for splitting the network traffic from k6 between multiple network cards, thus potentially increasing the available network throughput. For example, if you have 2 NICs, you can run k6 with `--local-ips=","` to balance the traffic equally between them - half of the VUs will use the first IP and the other half will use the second. This can scale to any number of NICs, and you can repeat some local IPs to give them more traffic. For example, `--local-ips=",,,"` will split VUs between 3 different source IPs in a 25%:25%:50% ratio. + +Available in the `k6 run` command. + +| Env | CLI | Code / Config file | Default | +| -------------- | ------------- | ------------------ | ------- | +| `K6_LOCAL_IPS` | `--local-ips` | N/A | N/A | + +{{< code >}} + +```bash +$ k6 run --local-ips=192.168.20.12-192.168.20.15,192.168.10.0/27 script.js +``` + +{{< /code >}} + +## Log output + +This option specifies where to send logs to and another configuration connected to it. Available in the `k6 run` command. + +| Env | CLI | Code / Config file | Default | +| --------------- | -------------- | ------------------ | -------- | +| `K6_LOG_OUTPUT` | `--log-output` | N/A | `stderr` | + +{{< code >}} + +```bash +$ k6 run --log-output=stdout script.js +``` + +{{< /code >}} + +Possible values are: + +- none - disable +- stdout - send to the standard output +- stderr - send to the standard error output (this is the default) +- loki - send logs to a loki server +- file - write logs to a file + +### Loki + +Use the `log-output` option to configure [Loki](https://grafana.com/oss/loki/) as follows. +For additional instructions and a step-by-step guide, check out the [Loki tutorial](https://k6.io/blog/using-loki-to-store-and-query-k6-logs/). + +{{< code >}} + +```bash +$ k6 run --log-output=loki=http://127.0.0.1:3100/loki/api/v1/push,label.something=else,label.foo=bar,limit=32,level=info,pushPeriod=5m32s,msgMaxSize=1231 script.js +``` + +{{< /code >}} + +Where all but the url in the beginning are not required. +The possible keys with their meanings and default values: + +| key | description | default value | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| `nothing` | the endpoint to which to send logs | `http://127.0.0.1:3100/loki/api/v1/push` | +| allowedLabels | if set k6 will send only the provided labels as such and all others will be appended to the message in the form `key=value`. The value of the option is in the form `[label1,label2]` | N/A | +| label.`labelName` | adds an additional label with the provided key and value to each message | N/A | +| header.`headerName` | adds an additional HTTP header with the provided header name and value to each HTTP request made to Loki | N/A | +| limit | the limit of message per pushPeriod, an additional log is send when the limit is reached, logging how many logs were dropped | 100 | +| level | the minimal level of a message so it's send to loki | all | +| pushPeriod | at what period to send log lines | 1s | +| profile | whether to print some info about performance of the sending to loki | false | +| msgMaxSize | how many symbols can there be at most in a message. Messages bigger will miss the middle of the message with an additional few characters explaining how many characters were dropped. | 1048576 | + +### File + +The file can be configured as below, where an explicit file path is required: + +{{< code >}} + +```bash +$ k6 run --log-output=file=./k6.log script.js +``` + +{{< /code >}} + +A valid file path is the unique mandatory field, the other optional fields listed below: + +| key | description | default value | +| ----- | --------------------------------------------------------------------------------------------------------------------- | ------------- | +| level | the minimal level of a message to write out of (in ascending order): trace, debug, info, warning, error, fatal, panic | trace | + +## LogFormat + +A value specifying the log format. By default, k6 includes extra debug information like date and log level. The other options available are: + +- `json`: print all the debug information in JSON format. + +- `raw`: print only the log message. + +| Env | CLI | Code / Config file | Default | +| --------------- | -------------------- | ------------------ | ------- | +| `K6_LOG_FORMAT` | `--log-format`, `-f` | N/A | | + +{{< code >}} + +```bash +$ k6 run --log-format raw test.js +``` + +{{< /code >}} + +## Max redirects + +The maximum number of HTTP redirects that k6 will follow before giving up on a request and +erroring out. Available in both the `k6 run` and the `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| ------------------ | ----------------- | ------------------ | ------- | +| `K6_MAX_REDIRECTS` | `--max-redirects` | `maxRedirects` | `10` | + +{{< code >}} + +```javascript +export const options = { + maxRedirects: 10, +}; +``` + +{{< /code >}} + +## Minimum iteration duration + +Specifies the minimum duration of every single execution (i.e. iteration) of the `default` +function. Any iterations that are shorter than this value will cause that VU to sleep for +the remainder of the time until the specified minimum duration is reached. + +| Env | CLI | Code / Config file | Default | +| --------------------------- | -------------------------- | ---------------------- | -------------- | +| `K6_MIN_ITERATION_DURATION` | `--min-iteration-duration` | `minIterationDuration` | `0` (disabled) | + +{{< code >}} + +```javascript +export const options = { + minIterationDuration: '10s', +}; +``` + +{{< /code >}} + +## No color + +A boolean specifying whether colored output is disabled. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --- | ------------ | ------------------ | ------- | +| N/A | `--no-color` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 run --no-color script.js +``` + +{{< /code >}} + +## No connection reuse + +A boolean, true or false, specifying whether k6 should disable keep-alive connections. +Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| ------------------------ | ----------------------- | ------------------- | ------- | +| `K6_NO_CONNECTION_REUSE` | `--no-connection-reuse` | `noConnectionReuse` | `false` | + +{{< code >}} + +```javascript +export const options = { + noConnectionReuse: true, +}; +``` + +{{< /code >}} + +## No cookies reset + +This disables the default behavior of resetting the cookie jar after each VU iteration. If +it's enabled, saved cookies will be persisted across VU iterations. + +| Env | CLI | Code / Config file | Default | +| --------------------- | --- | ------------------ | ------- | +| `K6_NO_COOKIES_RESET` | N/A | `noCookiesReset` | `false` | + +{{< code >}} + +```javascript +export const options = { + noCookiesReset: true, +}; +``` + +{{< /code >}} + +## No summary + +Disables [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test) generation, +including calls to [`handleSummary()`](https://grafana.com/docs/k6//results-output/end-of-test/custom-summary) and `--summary-export`. + +Available in the `k6 run` command. + +| Env | CLI | Code / Config file | Default | +| --------------- | -------------- | ------------------ | ------- | +| `K6_NO_SUMMARY` | `--no-summary` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 run --no-summary ~/script.js +``` + +{{< /code >}} + +## No setup + +A boolean specifying whether `setup()` function should be run. Available in `k6 cloud` and `k6 run` commands. + +| Env | CLI | Code / Config file | Default | +| ------------- | ------------ | ------------------ | ------- | +| `K6_NO_SETUP` | `--no-setup` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 run --no-setup script.js +``` + +{{< /code >}} + +## No teardown + +A boolean specifying whether `teardown()` function should be run. Available in `k6 cloud` and `k6 run` commands. + +| Env | CLI | Code / Config file | Default | +| ---------------- | --------------- | ------------------ | ------- | +| `K6_NO_TEARDOWN` | `--no-teardown` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 run --no-teardown script.js +``` + +{{< /code >}} + +## No thresholds + +Disables threshold execution. Available in the `k6 run` command. + +| Env | CLI | Code / Config file | Default | +| ------------------ | ----------------- | ------------------ | ------- | +| `K6_NO_THRESHOLDS` | `--no-thresholds` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 run --no-thresholds ~/script.js +``` + +{{< /code >}} + +## No usage report + +A boolean, true or false. By default, k6 sends a usage report each time it is run, so that we can +track how often people use it. If this option is set to true, no usage report will be made. To +learn more, have a look at the [Usage reports](https://grafana.com/docs/k6//misc/usage-collection) documentation. Available in +`k6 run` commands. + +| Env | CLI | Config file | Default | +| -------------------- | ------------------- | ----------------- | ------- | +| `K6_NO_USAGE_REPORT` | `--no-usage-report` | `noUsageReport`\* | `false` | + +{{< code >}} + +```bash +$ k6 run --no-usage-report ~/script.js +``` + +{{< /code >}} + +\* Note that this option is not supported in the exported script options, but can be specified in a configuration file. + +## No VU connection reuse + +A boolean, true or false, specifying whether k6 should reuse TCP connections between iterations +of a VU. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --------------------------- | -------------------------- | --------------------- | ------- | +| `K6_NO_VU_CONNECTION_REUSE` | `--no-vu-connection-reuse` | `noVUConnectionReuse` | `false` | + +{{< code >}} + +```javascript +export const options = { + noVUConnectionReuse: true, +}; +``` + +{{< /code >}} + +## Paused + +A boolean, true or false, specifying whether the test should start in a paused state. To resume +a paused state you'd use the `k6 resume` command. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| ----------- | ---------------- | ------------------ | ------- | +| `K6_PAUSED` | `--paused`, `-p` | `paused` | `false` | + +{{< code >}} + +```javascript +export const options = { + paused: true, +}; +``` + +{{< /code >}} + +## Profiling Enabled + +Enables [pprof](https://pkg.go.dev/net/http/pprof) profiling endpoints under the k6's REST API [address](#address). These endpoints help debug and profile k6 itself. k6's REST API should be enabled as well. + +| Env | CLI | Code / Config file | Default | +| --- | --------------------- | ------------------ | ------------------------------------ | +| N/A | `--profiling-enabled` | N/A | `http://localhost:6565/debug/pprof/` | + +{{< code >}} + +```bash +$ k6 run --profiling-enabled script.js +``` + +{{< /code >}} + +## Quiet + +A boolean, true or false, that disables the progress update bar on the console output. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --- | --------------- | ------------------ | ------- | +| N/A | `--quiet`, `-q` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 run script.js -d 20s --quiet +``` + +{{< /code >}} + +## Results output + +Specify the results output. +For information on all available outputs and how to configure them, +refer to [Results output](https://grafana.com/docs/k6//results-output/). + +| Env | CLI | Code / Config file | Default | +| -------- | ------------- | ------------------ | ------- | +| `K6_OUT` | `--out`, `-o` | N/A | `null` | + +{{< code >}} + +```bash +$ k6 run --out influxdb=http://localhost:8086/k6 script.js +``` + +{{< /code >}} + +## RPS + +The maximum number of requests to make per second, in total across all VUs. Available in `k6 run` and `k6 cloud` commands. + +{{% admonition type="caution" %}} + +**This option is discouraged.** +
+
+The `--rps` option has caveats and is difficult to use correctly. +
+
+For example, in the cloud or distributed execution, this option affects every k6 instance independently. +That is, it is not sharded like VUs are. +
+
+We strongly recommend the [arrival-rate executors](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed) to simulate constant RPS instead of this option. + +{{% /admonition %}} + +| Env | CLI | Code / Config file | Default | +| -------- | ------- | ------------------ | --------------- | +| `K6_RPS` | `--rps` | `rps` | `0` (unlimited) | + +{{< code >}} + +```javascript +export const options = { + rps: 500, +}; +``` + +{{< /code >}} + +> ### Considerations when running in the cloud +> +> The option is set per load generator which means that the value you set in the options object of your test script will be multiplied by the number of load generators your test run is using. At the moment we are hosting 300 VUs per load generator instance. In practice that means that if you set the option for 100 rps, and run a test with 1000 VUs, you will spin up 4 load gen instances and effective rps limit of your test run will be 400 + +## Scenarios + +Define one or more execution patterns, with various VU and iteration scheduling +settings, running different exported functions (besides `default`!), using different +environment variables, tags, and more. + +See the [Scenarios](https://grafana.com/docs/k6//using-k6/scenarios) article for details and more examples. + +Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --- | --- | ------------------ | ------- | +| N/A | N/A | `scenarios` | `null` | + +{{< code >}} + +```javascript +export const options = { + scenarios: { + my_api_scenario: { + // arbitrary scenario name + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '5s', target: 100 }, + { duration: '5s', target: 0 }, + ], + gracefulRampDown: '10s', + env: { MYVAR: 'example' }, + tags: { my_tag: 'example' }, + }, + }, +}; +``` + +{{< /code >}} + +## Setup timeout + +Specify how long the `setup()` function is allow to run before it's terminated and the test fails. + +| Env | CLI | Code / Config file | Default | +| ------------------ | --- | ------------------ | ------- | +| `K6_SETUP_TIMEOUT` | N/A | `setupTimeout` | `"60s"` | + +{{< code >}} + +```javascript +export const options = { + setupTimeout: '30s', +}; +``` + +{{< /code >}} + +## Show logs + +A boolean specifying whether the cloud logs are printed out to the terminal. Available in `k6 cloud` command. + +| Env | CLI | Code / Config file | Default | +| --- | ------------- | ------------------ | ------- | +| N/A | `--show-logs` | N/A | `true` | + +{{< code >}} + +```bash +$ k6 cloud --show-logs=false script.js +``` + +{{< /code >}} + +## Stages + +A list of VU `{ target: ..., duration: ... }` objects that specify the target number of VUs to +ramp up or down to for a specific period. Available in `k6 run` and `k6 cloud` commands. + +It is a shortcut option for a single [scenario](https://grafana.com/docs/k6//using-k6/scenarios) with a [ramping VUs executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus). If used together with the [VUs](#vus) option, the `vus` value is used as the `startVUs` option of the executor. + +| Env | CLI | Code / Config file | Default | +| ----------- | ------------------------------------------------------- | ------------------ | ------------------------------ | +| `K6_STAGES` | `--stage :`, `-s :` | `stages` | Based on `vus` and `duration`. | + +{{< code >}} + +```javascript +// The following config would have k6 ramping up from 1 to 10 VUs for 3 minutes, +// then staying flat at 10 VUs for 5 minutes, then ramping up from 10 to 35 VUs +// over the next 10 minutes before finally ramping down to 0 VUs for another +// 3 minutes. + +export const options = { + stages: [ + { duration: '3m', target: 10 }, + { duration: '5m', target: 10 }, + { duration: '10m', target: 35 }, + { duration: '3m', target: 0 }, + ], +}; +``` + +```bash +$ k6 run --stage 5s:10,5m:20,10s:5 script.js + +# or... + +$ K6_STAGES="5s:10,5m:20,10s:5" k6 run script.js +``` + +```windows +C:\k6> k6 run --stage 5s:10,5m:20,10s:5 script.js + +# or... + +C:\k6> set "K6_STAGES=5s:10,5m:20,10s:5" && k6 run script.js + +``` + +```powershell +C:\k6> k6 run --stage 5s:10,5m:20,10s:5 script.js + +# or... + +C:\k6> $env:K6_STAGES="5s:10,5m:20,10s:5"; k6 run script.js + +``` + +{{< /code >}} + +## Summary export + +Save the end-of-test summary report to a JSON file that includes data for all test metrics, checks and thresholds. +This is useful to get the aggregated test results in a machine-readable format, for integration with dashboards, external alerts, CI pipelines, etc. + +While this feature is not deprecated yet, [we now discourage it](https://grafana.com/docs/k6//results-output/end-of-test#summary-export-to-a-json-file). +For a better, more flexible JSON export, as well as export of the summary data to different formats (e.g. JUnit/XUnit/etc. XML, HTML, .txt) and complete summary customization, use the [`handleSummary()` function](https://grafana.com/docs/k6//results-output/end-of-test/custom-summary). + +Available in the `k6 run` command. + +| Env | CLI | Code / Config file | Default | +| ------------------- | ----------------------------- | ------------------ | ------- | +| `K6_SUMMARY_EXPORT` | `--summary-export ` | N/A | `null` | + +{{< code >}} + +```bash +$ k6 run --summary-export export.json script.js + +# or... + +$ K6_SUMMARY_EXPORT="export.json" k6 run script.js +``` + +```windows +C:\k6> k6 run --summary-export export.json script.js + +# or... + +C:\k6> set "K6_SUMMARY_EXPORT=export.json" && k6 run script.js + +``` + +```powershell +C:\k6> k6 run --summary-export export.json script.js + +# or... + +C:\k6> $env:K6_SUMMARY_EXPORT="export.json"; k6 run script.js + +``` + +{{< /code >}} + +See an example file on the [Results Output](https://grafana.com/docs/k6//get-started/results-output#summary-export) page. + +## Supply environment variables + +Add/override an [environment variable](https://grafana.com/docs/k6//using-k6/environment-variables) with `VAR=value`in a k6 script. Available in `k6 run` and `k6 cloud` commands. + +To make the system environment variables available in the k6 script via `__ENV`, use the [`--include-system-env-vars` option](#include-system-env-vars). + +{{% admonition type="note" %}} + +**The `-e` flag does not configure options.** + +This flag just provides variables to the script, which the script can use or ignore. +For example, `-e K6_ITERATIONS=120` does _not_ configure the script iterations. + +Compare this behavior with `K6_ITERATIONS=120 k6 run script.js`, which _does_ set iterations. + +{{% /admonition %}} + +| Env | CLI | Code / Config file | Default | +| --- | ------------- | ------------------ | ------- | +| N/A | `--env`, `-e` | N/A | `null` | + +{{< code >}} + +```bash +$ k6 run -e FOO=bar ~/script.js +``` + +{{< /code >}} + +## System tags + +Specify which [system tags](https://grafana.com/docs/k6//using-k6/tags-and-groups#system-tags) will be in the collected +metrics. Some collectors like the `cloud` one may require that certain system tags be used. +You can specify the tags as an array from the JS scripts or as a comma-separated list via the +CLI. Available in `k6 run` and `k6 cloud` commands + +| Env | CLI | Code / Config file | Default | +| ---------------- | --------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| `K6_SYSTEM_TAGS` | `--system-tags` | `systemTags` | `proto`,`subproto`,`status`,`method`,`url`,`name`,`group`,`check`,`error`,`error_code`,`tls_version`,`scenario`,`service`,`expected_response` | + +{{< code >}} + +```javascript +export const options = { + systemTags: ['status', 'method', 'url'], +}; +``` + +{{< /code >}} + +## Summary time unit + +Define which time unit will be used for _all_ time values in the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test). Possible values are `s` (seconds), `ms` (milliseconds) and `us` (microseconds). If no value is specified, k6 will use mixed time units, choosing the most appropriate unit for each value. + +| Env | CLI | Code / Config file | Default | +| ---------------------- | --------------------- | ------------------ | ------- | +| `K6_SUMMARY_TIME_UNIT` | `--summary-time-unit` | `summaryTimeUnit` | `null` | + +{{< code >}} + +```javascript +export const options = { + summaryTimeUnit: 'ms', +}; +``` + +{{< /code >}} + +## Summary trend stats + +Define which stats for [`Trend` metrics](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend) (e.g. response times, group/iteration durations, etc.) will be shown in the [end-of-test summary](https://grafana.com/docs/k6//results-output/end-of-test). Possible values include `avg` (average), `med` (median), `min`, `max`, `count`, as well as arbitrary percentile values (e.g. `p(95)`, `p(99)`, `p(99.99)`, etc.). + +For further summary customization and exporting the summary in various formats (e.g. JSON, JUnit/XUnit/etc. XML, HTML, .txt, etc.), use the [`handleSummary()` function](https://grafana.com/docs/k6//results-output/end-of-test/custom-summary). + +| Env | CLI | Code / Config file | Default | +| ------------------------ | ----------------------- | ------------------- | ----------------------------- | +| `K6_SUMMARY_TREND_STATS` | `--summary-trend-stats` | `summaryTrendStats` | `avg,min,med,max,p(90),p(95)` | + +{{< code >}} + +```javascript +export const options = { + summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(95)', 'p(99)', 'p(99.99)', 'count'], +}; +``` + +{{< /code >}} + +{{< code >}} + +```bash +$ k6 run --summary-trend-stats="avg,min,med,max,p(90),p(99.9),p(99.99),count" ./script.js +``` + +{{< /code >}} + +## Tags + +Specify tags that should be set test wide across all metrics. If a tag with the same name has +been specified on a request, check or custom metrics it will have precedence over a test wide +tag. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --- | ------------------ | ------------------ | ------- | +| N/A | `--tag NAME=VALUE` | `tags` | `null` | + +{{< code >}} + +```javascript +export const options = { + tags: { + name: 'value', + }, +}; +``` + +{{< /code >}} + +## Teardown timeout + +Specify how long the `teardown()` function is allowed to run before it's terminated and the test +fails. + +| Env | CLI | Code / Config file | Default | +| --------------------- | --- | ------------------ | ------- | +| `K6_TEARDOWN_TIMEOUT` | N/A | `teardownTimeout` | `"60s"` | + +{{< code >}} + +```javascript +export const options = { + teardownTimeout: '30s', +}; +``` + +{{< /code >}} + +## Thresholds + +A collection of threshold specifications to configure under what condition(s) a test is considered +successful or not, when it has passed or failed, based on metric data. To learn more, have a look +at the [Thresholds](https://grafana.com/docs/k6//using-k6/thresholds) documentation. Available in `k6 run` commands. + +| Env | CLI | Code / Config file | Default | +| --- | --- | ------------------ | ------- | +| N/A | N/A | `thresholds` | `null` | + +{{< code >}} + +```javascript +export const options = { + thresholds: { + 'http_req_duration': ['avg<100', 'p(95)<200'], + 'http_req_connecting{cdnAsset:true}': ['p(95)<100'], + }, +}; +``` + +{{< /code >}} + +## Throw + +A boolean, true or false, specifying whether k6 should throw exceptions when certain errors occur, or if it should just log them with a warning. Behaviors that currently depend on this option: + +- Failing to make [HTTP requests](https://grafana.com/docs/k6//javascript-api/k6-http), e.g. due to a network error. +- Adding invalid values to [custom metrics](https://grafana.com/docs/k6//using-k6/metrics#custom-metrics). +- Setting invalid [per-VU metric tags](https://grafana.com/docs/k6//javascript-api/k6-execution#tags). + +Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| ---------- | --------------- | ------------------ | ------- | +| `K6_THROW` | `--throw`, `-w` | `throw` | `false` | + +{{< code >}} + +```javascript +export const options = { + throw: true, +}; +``` + +{{< /code >}} + +## TLS auth + +A list of TLS client certificate configuration objects. `domains` and `password` are optional, but `cert` and `key` are required. + +| Env | CLI | Code / Config file | Default | +| --- | --- | ------------------ | ------- | +| N/A | N/A | `tlsAuth` | `null` | + +{{< code >}} + +```javascript +export const options = { + tlsAuth: [ + { + domains: ['example.com'], + cert: open('mycert.pem'), + key: open('mycert-key.pem'), + password: 'mycert-passphrase', + }, + ], +}; +``` + +{{< /code >}} + +## TLS cipher suites + +A list of cipher suites allowed to be used by in SSL/TLS interactions with a server. +For a full listing of available ciphers go [here](https://golang.org/pkg/crypto/tls/#pkg-constants). + +{{% admonition type="caution" %}} + +Due to limitations in the underlying [go implementation](https://github.com/golang/go/issues/29349), changing the ciphers for TLS 1.3 is _not_ supported and will do nothing. + +{{% /admonition %}} + +| Env | CLI | Code / Config file | Default | +| --- | --- | ------------------ | ------------------------- | +| N/A | N/A | `tlsCipherSuites` | `null` (Allow all suites) | + +{{< code >}} + +```javascript +export const options = { + tlsCipherSuites: ['TLS_RSA_WITH_RC4_128_SHA', 'TLS_RSA_WITH_AES_128_GCM_SHA256'], +}; +``` + +{{< /code >}} + +## TLS version + +Either a string representing the only SSL/TLS version allowed to be used in interactions with a +server, or an object specifying the "min" and "max" versions allowed to be used. + +| Env | CLI | Code / Config file | Default | +| --- | --- | ------------------ | --------------------------- | +| N/A | N/A | `tlsVersion` | `null` (Allow all versions) | + +{{< code >}} + +```javascript +export const options = { + tlsVersion: 'tls1.2', +}; +``` + +{{< /code >}} + +{{< code >}} + +```javascript +export const options = { + tlsVersion: { + min: 'ssl3.0', + max: 'tls1.2', + }, +}; +``` + +{{< /code >}} + +## Traces output + +This option specifies where to send traces to. Available in the `k6 run` command. + +| Env | CLI | Code / Config file | Default | +| ------------------ | ----------------- | ------------------ | ------- | +| `K6_TRACES_OUTPUT` | `--traces-output` | N/A | `none` | + +{{< code >}} + +```bash +$ k6 run --traces-output=otel script.js +``` + +{{< /code >}} + +Possible values are: + +- none - disable +- otel - send traces to an OTEL compatible backend + +### OTEL + +Use the `traces-output` option to configure [Open Telemetry](https://opentelemetry.io/) compatible output as follows. + +{{< code >}} + +```bash +$ k6 run --traces-output=otel=http://127.0.0.1:4318,proto=http,header.AdditionalHeader=example script.js +``` + +{{< /code >}} + +Where none of the options are required. +The possible keys with their meanings and default values: + +| key | description | default value | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| `nothing` | the endpoint to which send traces to | `http://127.0.0.1:4317` | +| proto | the protocol to use when connecting with the traces backend | `grpc` | +| header.`headerName` | adds an additional HTTP header with the provided header name and value to each HTTP request made to the traces backend | N/A | + +## Upload Only + +A boolean specifying whether the test should just be uploaded to the cloud, but not run it. Available in `k6 cloud` command. + +This would be useful if you would like to update a given test and run it later. For example, updating test scripts of a scheduled test from the CI pipelines. + +| Env | CLI | Code / Config file | Default | +| ---------------------- | --------------- | ------------------ | ------- | +| `K6_CLOUD_UPLOAD_ONLY` | `--upload-only` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 cloud --upload-only script.js +``` + +{{< /code >}} + +## User agent + +A string specifying the user-agent string to use in `User-Agent` headers when sending HTTP +requests. +If you pass an empty string, no `User-Agent` header is sent. +Available in `k6 run` and `k6 cloud` commands + +| Env | CLI | Code / Config file | Default | +| --------------- | -------------- | ------------------ | --------------------------------------------------------------------- | +| `K6_USER_AGENT` | `--user-agent` | `userAgent` | `k6/0.27.0 (https://k6.io/)` (depending on the version you're using)` | + +{{< code >}} + +```javascript +export const options = { + userAgent: 'MyK6UserAgentString/1.0', +}; +``` + +{{< /code >}} + +## Verbose + +A boolean specifying whether verbose logging is enabled. Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| --- | ----------------- | ------------------ | ------- | +| N/A | `--verbose`, `-v` | N/A | `false` | + +{{< code >}} + +```bash +$ k6 run --verbose script.js +``` + +{{< /code >}} + +## VUs + +An integer value specifying the number of VUs to run concurrently, used together with the [iterations](#iterations) or [duration](#duration) options. If you'd like more control look at the [`stages`](#stages) option or [scenarios](https://grafana.com/docs/k6//using-k6/scenarios). + +Available in `k6 run` and `k6 cloud` commands. + +| Env | CLI | Code / Config file | Default | +| -------- | ------------- | ------------------ | ------- | +| `K6_VUS` | `--vus`, `-u` | `vus` | `1` | + +{{< code >}} + +```javascript +export const options = { + vus: 10, + duration: '1h', +}; +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/metrics/_index.md b/docs/sources/v0.50.x/using-k6/metrics/_index.md new file mode 100644 index 000000000..99ab4f2b2 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/metrics/_index.md @@ -0,0 +1,117 @@ +--- +title: 'Metrics' +description: 'This section covers the important aspect of metrics management in k6. How and what kind of metrics k6 collects automatically (_built-in_ metrics), and what custom metrics you can make k6 collect.' +weight: 02 +--- + +# Metrics + +_Metrics_ measure how a system performs under test conditions. +By default, k6 automatically collects built-in metrics. +Besides built-ins, you can also make custom metrics. + +Metrics fall into four broad types: + +- **Counters** sum values. +- **Gauges** track the smallest, largest, and latest values. +- **Rates** track how frequently a non-zero value occurs. +- **Trends** calculates statistics for multiple values (like mean, mode or percentile). + +To make a test fail a certain criteria, you can write a [Threshold](https://grafana.com/docs/k6//using-k6/thresholds) based on the metric criteria (the specifics of the expression depend on the metric type). +To filter metrics, you can use [Tags and groups](https://grafana.com/docs/k6//using-k6/tags-and-groups). +You can also export metrics in various summary and granular formats, as documented in [Results output](https://grafana.com/docs/k6//results-output). + +| On this page... | Read about... | +| -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | +| [Built-in metrics](https://grafana.com/docs/k6//using-k6/metrics/reference) | Each built-in metric for each supported [protocol](https://grafana.com/docs/k6//using-k6/protocols) | +| [Create custom metrics](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics) | How to build your own metric for each metric type | + +## What metrics to look at? + +Each metric provides a different perspective on performance. +So the best metric for your analysis depends on your goals. + +However, if you're unsure about the metrics to focus on, you can start with the metrics that measure the requests, errors, and duration (the criteria of the [RED method](https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/)). + +- `http_reqs`, to measure requests +- `http_req_failed`, to measure error rate +- `http_req_duration`, to measure duration + +{{% admonition type="note" %}} + +In other terminology, these metrics measure traffic (in requests), availability (in error rate), and latency (in request duration). +SREs might recognize these metrics as three of the [four Golden Signals](https://sre.google/sre-book/monitoring-distributed-systems/#xref_monitoring_golden-signals). + +{{% /admonition %}} + +## Example output + +An aggregated summary of all _built-in_ and custom metrics outputs to `stdout` when you run a test: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export default function () { + http.get('https://test-api.k6.io/'); +} +``` + +{{< /code >}} + +The preceding script outputs something like this: + +{{< code >}} + +```bash +$ k6 run script.js + + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: http_get.js + output: - + + scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop): + * default: 1 iterations for each of 1 VUs (maxDuration: 10m0s, gracefulStop: 30s) + + +running (00m03.8s), 0/1 VUs, 1 complete and 0 interrupted iterations +default ✓ [======================================] 1 VUs 00m03.8s/10m0s 1/1 iters, 1 per VU + + data_received..................: 22 kB 5.7 kB/s + data_sent......................: 742 B 198 B/s + http_req_blocked...............: avg=1.05s min=1.05s med=1.05s max=1.05s p(90)=1.05s p(95)=1.05s + http_req_connecting............: avg=334.26ms min=334.26ms med=334.26ms max=334.26ms p(90)=334.26ms p(95)=334.26ms + http_req_duration..............: avg=2.7s min=2.7s med=2.7s max=2.7s p(90)=2.7s p(95)=2.7s + { expected_response:true }...: avg=2.7s min=2.7s med=2.7s max=2.7s p(90)=2.7s p(95)=2.7s + http_req_failed................: 0.00% ✓ 0 ✗ 1 + http_req_receiving.............: avg=112.41µs min=112.41µs med=112.41µs max=112.41µs p(90)=112.41µs p(95)=112.41µs + http_req_sending...............: avg=294.48µs min=294.48µs med=294.48µs max=294.48µs p(90)=294.48µs p(95)=294.48µs + http_req_tls_handshaking.......: avg=700.6ms min=700.6ms med=700.6ms max=700.6ms p(90)=700.6ms p(95)=700.6ms + http_req_waiting...............: avg=2.7s min=2.7s med=2.7s max=2.7s p(90)=2.7s p(95)=2.7s + http_reqs......................: 1 0.266167/s + iteration_duration.............: avg=3.75s min=3.75s med=3.75s max=3.75s p(90)=3.75s p(95)=3.75s + iterations.....................: 1 0.266167/s + vus............................: 1 min=1 max=1 + vus_max........................: 1 min=1 max=1 +``` + +{{< /code >}} + +In that output, all the metrics that start with `http`, `iteration`, and `vu` are _built-in_ metrics, which get written to stdout at the end of a test. +For details of all metrics, refer to the [Metrics reference](https://grafana.com/docs/k6//using-k6/metrics/reference). + +## Metric name restrictions + +Metric names must comply with OpenTelemetry and [Prometheus limitations](https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). The character limit is the same limit k6 had before restricting the character set down. + +That means metrics names must: + +- Include up to 128 symbols (ASCII letters, numbers, or underscores). +- Start with a letter or an underscore. diff --git a/docs/sources/v0.50.x/using-k6/metrics/create-custom-metrics.md b/docs/sources/v0.50.x/using-k6/metrics/create-custom-metrics.md new file mode 100644 index 000000000..0cc1571a6 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/metrics/create-custom-metrics.md @@ -0,0 +1,108 @@ +--- +title: Create custom metrics +description: How to build custom k6 metrics for each metric type. +weight: 200 +--- + +# Create custom metrics + +Besides the [built-in metrics](https://grafana.com/docs/k6//using-k6/metrics/reference), you can create custom metrics. +For example, you can compute a metric for your business logic, or use the [Response.timings](https://grafana.com/docs/k6//javascript-api/k6-http/response) object to create a metric for a specific set of endpoints. + +Each metric type has a [constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor) to create a custom metric. +The constructor creates a metric object of the declared type. Each type has an `add` method to take metric measurements. + +If you need help constructing a custom metric, read the following sections of this document. +They document the procedure and provide examples. +If you're comfortable with these already, you might prefer to read the [reference documentation for each metric constructor](https://grafana.com/docs/k6//javascript-api/k6-metrics). +Each topic has examples to make a custom metric and create [thresholds](https://grafana.com/docs/k6//using-k6/thresholds) from it. + +## Create a custom metric + +{{% admonition type="note" %}} + +Custom metrics must be created in [init context](https://grafana.com/docs/k6//using-k6/test-lifecycle). +This limits memory and ensures that k6 can validate that all thresholds are evaluating defined metrics. + +{{% /admonition %}} + +The generic procedure to create a custom metric is as follows: + +1. Import the [`k6/metrics`](https://grafana.com/docs/k6//javascript-api/k6-metrics) module. Optionally, specify the type of metrics you want to create with a named import: + + ```javascript + import { Trend } from 'k6/metrics'; + ``` + +1. In init context, construct a new custom-metric object. + + For example, the following creates a custom trend. The object in the script is called `myTrend`, and its metric appears in the results output as `waiting_time`. + + ```javascript + const myTrend = new Trend('waiting_time'); + ``` + +1. In the VU iteration code, use the `add` method to take a measurement. + +## Example: create a trend metric for waiting time + +This VU code makes a request then adds the timing value of the request to the `myTrend` object. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { Trend } from 'k6/metrics'; + +const myTrend = new Trend('waiting_time'); + +export default function () { + const r = http.get('https://httpbin.test.k6.io'); + myTrend.add(r.timings.waiting); + console.log(myTrend.name); // waiting_time +} +``` + +{{< /code >}} + +## View custom metric results + +Custom metrics appear in [Results output](https://grafana.com/docs/k6//results-output) in both the end-of-test summary and in the granular data points. +Each metric type has specific aggregation methods. +You can also optionally [tag](https://grafana.com/docs/k6//using-k6/tags-and-groups) any value for a custom metric. +You can use these tags to filter test results. + +Here's how the output of the preceding script might look in the end-of-test summary. +Since the metric is a trend, k6 calculates various trends based on the number of values and their summation. + +{{< code >}} + +```bash +$ k6 run script.js + + ... + INFO[0001] waiting_time source=console + + ... + iteration_duration.............: avg=1.15s min=1.15s med=1.15s max=1.15s p(90)=1.15s p(95)=1.15s + iterations.....................: 1 0.864973/s + waiting_time...................: avg=265.245396 min=265.245396 med=265.245396 max=265.245396 p(90)=265.245396 p(95)=265.245396 +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +Custom metrics are collected from VU threads only at the end of a VU iteration. +For long-running scripts, custom metrics might appear only after the test runs a while. + +{{% /admonition %}} + +## Read more + +The [`k6/metrics`](https://grafana.com/docs/k6//javascript-api/k6-metrics) has examples for defining each type of custom metric, and for defining a threshold on the metric. + +- [Counter](https://grafana.com/docs/k6//javascript-api/k6-metrics/counter) +- [Gauge](https://grafana.com/docs/k6//javascript-api/k6-metrics/gauge) +- [Rate](https://grafana.com/docs/k6//javascript-api/k6-metrics/rate) +- [Trend](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend) diff --git a/docs/sources/v0.50.x/using-k6/metrics/reference.md b/docs/sources/v0.50.x/using-k6/metrics/reference.md new file mode 100644 index 000000000..f074b58fe --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/metrics/reference.md @@ -0,0 +1,108 @@ +--- +title: Built-in metrics +description: A reference of built-in metrics for different supported protocols. +weight: 100 +--- + +# Built-in metrics + +Every k6 test emits built-in and [custom metrics](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics). +Each supported protocol also has its specific metrics. + +## Standard built-in metrics + +k6 always collects the following metrics, no matter what protocol the test uses: + +| Metric Name | Type | Description | +| ------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| checks | Rate | The rate of successful checks. | +| data_received | Counter | The amount of received data. [This example covers how to track data for an individual URL](https://grafana.com/docs/k6//examples/track-transmitted-data-per-url). | +| data_sent | Counter | The amount of data sent. [Track data for an individual URL](https://grafana.com/docs/k6//examples/track-transmitted-data-per-url) to track data for an individual URL. | +| dropped_iterations | Counter | The number of iterations that weren't started due to lack of VUs (for the arrival-rate executors) or lack of time (expired maxDuration in the iteration-based executors). Refer to [Dropped iterations](https://grafana.com/docs/k6//using-k6/scenarios/concepts/dropped-iterations) for more details. | +| iteration_duration | Trend | The time to complete one full iteration, including time spent in `setup` and `teardown`. To calculate the duration of the iteration's function for the specific scenario, [try this workaround](https://grafana.com/docs/k6//using-k6/workaround-iteration-duration). | +| iterations | Counter | The aggregate number of times the VUs execute the JS script (the `default` function). | +| vus | Gauge | Current number of active virtual users | +| vus_max | Gauge | Max possible number of virtual users (VU resources are [pre-allocated](https://grafana.com/docs/k6//using-k6/scenarios/concepts/arrival-rate-vu-allocation), to avoid affecting performance when scaling up load). | + +## HTTP-specific built-in metrics {#http} + +These metrics are generated only when the test makes HTTP requests. + +{{% admonition type="note" %}} + +For all `http_req_*` metrics, **the timestamp is emitted the end of the request.** +In other words, the timestamp happens when k6 receives the end of the response body, or the request times out. + +{{% /admonition %}} + +| Metric Name | Type | Description | +| ------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| http_req_blocked | Trend | Time spent blocked (waiting for a free TCP connection slot) before initiating the request. `float` | +| http_req_connecting | Trend | Time spent establishing TCP connection to the remote host. `float` | +| http_req_duration | Trend | Total time for the request. It's equal to `http_req_sending + http_req_waiting + http_req_receiving` (i.e. how long did the remote server take to process the request and respond, without the initial DNS lookup/connection times). `float` | +| http_req_failed | Rate | The rate of failed requests according to [setResponseCallback](https://grafana.com/docs/k6//javascript-api/k6-http/set-response-callback). | +| http_req_receiving | Trend | Time spent receiving response data from the remote host. `float` | +| http_req_sending | Trend | Time spent sending data to the remote host. `float` | +| http_req_tls_handshaking | Trend | Time spent handshaking TLS session with remote host | +| http_req_waiting | Trend | Time spent waiting for response from remote host (a.k.a. “time to first byte”, or “TTFB”). `float` | +| http_reqs | Counter | How many total HTTP requests k6 generated. | + +## Browser metrics {#browser} + +The [k6 browser module](https://grafana.com/docs/k6//using-k6-browser) emits its own metrics based on the [Core Web Vitals](https://web.dev/vitals/#core-web-vitals). + +These core metrics will evolve over time when technology changes, but for now, k6 tracks the following core web vitals: + + + +| Core Web Vital | Description | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| browser_web_vital_cls | Measures the visual stability on a webpage by quantifying the amount of unexpected layout shift of visible page content. Refer to [Cumulative Layout Shift](https://web.dev/cls/) for more information. | +| browser_web_vital_fid | Measures the responsiveness of a web page by quantifying the delay between a user’s first interaction, such as clicking a button, and the browser’s response. Refer to [First Input Delay](https://web.dev/fid/) for more information. | +| browser_web_vital_lcp | Measures the time it takes for the largest content element on a page to become visible. Refer to [Largest Contentful Paint](https://web.dev/lcp/) for more information. | + + + +### Other Web Vitals + +Apart from the Core Web Vitals, the browser module also reports [Other Web Vitals](https://web.dev/vitals/#other-web-vitals). + + + +| Other Web Vital | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| browser_web_vital_fcp | Measures the time it takes for the browser to render the first DOM element on the page, whether that's a text, image or header. Refer to [First Contentful Paint](https://web.dev/fcp/) for more information. | +| browser_web_vital_inp | An experimental metric that measures a page's responsiveness. Refer to [Interaction to Next Paint](https://web.dev/inp/) for more information. | +| browser_web_vital_ttfb | Measures the time it takes between the browser request and the start of the response from a server. Refer to [Time to First Byte](https://web.dev/ttfb/) for more information. | + + + +## Built-in WebSocket metrics {#websockets} + +`k6` emits the following metrics when interacting with a WebSocket service through the [`experimental`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets) or legacy websockets API. + +| Metric name | Type | Description | +| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------ | +| ws_connecting | Trend | Total duration for the WebSocket connection request. | +| ws_msgs_received | Counter | Total number of messages received | +| ws_msgs_sent | Counter | Total number of messages sent | +| ws_ping | Trend | Duration between a ping request and its pong reception | +| ws_session_duration | Trend | Duration of the WebSocket session. Time between the start of the connection and the end of the VU execution. | +| ws_sessions | Counter | Total number of started WebSocket sessions. | + +## Built-in gRPC metrics {#grpc} + +k6 emits the following metrics when it interacts with a service through the [`gRPC`](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/) API. + +| Metric Name | Type | Description | +| -------------------------- | ------- | ----------------------------------------- | +| grpc_req_duration | Trend | Time to receive response from remote host | +| grpc_streams | Counter | Total number of started streams | +| grpc_streams_msgs_received | Counter | Total number of messages received | +| grpc_streams_msgs_sent | Counter | Total number of messages sent | + +{{% admonition type="note" %}} + +Steams-related metrics (`grpc_streams*`) are available only on `k6` version `0.49.0` or higher or when using the `k6/experimental/grpc` module, which is available on `k6` version `0.45.0`. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/using-k6/modules.md b/docs/sources/v0.50.x/using-k6/modules.md new file mode 100644 index 000000000..c7f7b6b1d --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/modules.md @@ -0,0 +1,236 @@ +--- +title: 'Modules' +description: 'While writing test scripts, it is common to import different modules, or part of modules, for +usage throughout the script. In k6, it is possible to import three different kinds of modules.' +weight: 07 +--- + +# Modules + +## Importing modules + +It's common to import modules, or parts of modules, to use in your test scripts. +In k6, you can import different kinds of modules: + +- [Built-in modules](#built-in-modules) +- [Local filesystem modules](#local-filesystem-modules) +- [Remote HTTP(S) modules](#remote-https-modules) +- [Extension modules](#extension-modules) + +### Built-in modules + +k6 provides many built-in modules for core functionalities. +For example, the `http` client make requests against the +system under test. +For the full list of built-in modules, refer to the [API documentation](https://grafana.com/docs/k6//javascript-api). + +```javascript +import http from 'k6/http'; +``` + +### Local modules + +These modules are stored on the local filesystem, and accessed either through relative or absolute filesystem paths. + +k6 adopts a **browser-like module resolution** and doesn't support [Node.js module resolution](https://nodejs.org/api/modules.html#modules_all_together). File names for `imports` must be fully specified, such as `./helpers.js`. + +```javascript +//my-test.js +import { someHelper } from './helpers.js'; + +export default function () { + someHelper(); +} +``` + +```javascript +//helpers.js +export function someHelper() { + // ... +} +``` + +### Remote modules + +These modules are accessed over HTTP(S), from a public source like GitHub, any CDN, or +from any publicly accessible web server. The imported modules are downloaded and executed at +runtime, making it extremely important to **make sure you trust the code before including +it in a test script**. + +For example, [jslib](https://grafana.com/docs/k6//javascript-api/jslib) is a set of k6 JavaScript libraries available as remote HTTPS modules. They can be downloaded and imported as local modules or directly imported as remote modules. + +```javascript +import { randomItem } from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; + +export default function () { + randomItem(); +} +``` + +You can also build your custom Javascript libraries and distribute them via a public web hosting. For reference, [k6-jslib-aws](https://github.com/grafana/k6-jslib-aws) and [k6-rollup-example](https://github.com/grafana/k6-rollup-example) host their modules as GitHub release assets. + +### Extension modules + +Like the [k6 APIs](https://grafana.com/docs/k6//javascript-api), you can build custom modules in Go code and expose them as JavaScript modules. These custom Go-to-JS modules are known as [k6 extensions](https://grafana.com/docs/k6//extensions). + +Below is an example that imports the `k6/x/kubernetes` module from the [xk6-kubernetes](https://github.com/grafana/xk6-kubernetes) extension. + +```javascript +import { Kubernetes } from 'k6/x/kubernetes'; + +const podSpec = { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'busybox', namespace: 'testns' }, + spec: { + containers: [ + { + name: 'busybox', + image: 'busybox', + command: ['sh', '-c', 'sleep 30'], + }, + ], + }, +}; +export default function () { + const kubernetes = new Kubernetes(); + kubernetes.create(podSpec); + const pods = kubernetes.list('Pod', 'testns'); + pods.map(function (pod) { + console.log(pod.metadata.name); + }); +} +``` + +How do k6 extensions (Go-to-JS modules) work? For enhanced performance, the k6 engine is written in Go and embeds a JavaScript VM ([goja](https://github.com/dop251/goja)) to execute JavaScript test code. That allows you to build your modules in Go code and import them as JavaScript as usual. + +To learn more about using or creating k6 extensions, refer to the [Extension documentation](https://grafana.com/docs/k6//extensions). + +## Sharing JavaScript modules + +As mentioned previously, users can import custom JavaScript libraries by loading either local or remote modules. Because of that, we have two options to import JavaScript modules, along with various methods to distribute them. + +{{< admonition type="note" >}} + +The following options for distributing and sharing JavaScript libraries are available for both custom and other public libraries. + +{{< /admonition >}} + +**As remote modules** + +You can host your modules in a public webserver like GitHub and any CDN and be imported remotely. + +```javascript +// As GitHub release assets +import { + WorkloadConfig, + sayHello, +} from 'https://github.com/grafana/k6-rollup-example/releases/download/v0.0.2/index.js'; + +// or hosted in a CDN +import { randomIntBetween, randomItem } from 'https://jslib.k6.io/k6-utils/1.4.0/index.js'; +``` + +When the library consists of multiple files and modules, you may want to bundle these modules to create public releases. Here are some examples for reference: + +- Using Webpack: [k6-jslib-utils](https://github.com/grafana/k6-jslib-utils) and [k6-jslib-aws](https://github.com/grafana/k6-jslib-aws). +- Using Rollup: [test-commons](https://github.com/grafana/k6-rollup-example/). + +Be aware that k6 automatically executes remote modules, making it crucial to trust the source code of these remote modules. There is a **risk of altering the remote modules with certain hosting mechanisms**. To mitigate this security risk, some users prefer to download and import the modules locally to ensure full control of the source code. + +**As local modules** + +In this example, the previous remote modules have been downloaded to the `lib` folder of the testing project and imported as follows: + +```javascript +import { WorkloadConfig, sayHello } from './libs/test-commons.js'; + +import { randomIntBetween, randomItem } from './libs/k6-utils.js'; +``` + +Another option to distribute libraries is to use a package manager tool like npm, which enables version locking and the linking of local libraries. The latter can be useful during development. + +Although k6 does not resolve node modules, you can utilize a Bundler to load npm dependencies, as shown in the [k6-rollup-example](https://github.com/grafana/k6-rollup-example). + +## Using TypeScript + +k6 does not natively support TypeScript. If you wish to write k6 tests in Typescript, you will need a bundler, as demonstrated in the previous examples: + +- Using Webpack: Refer to [k6-template-typescript](https://github.com/grafana/k6-template-typescript) and [k6-jslib-aws](https://github.com/grafana/k6-jslib-aws). +- Using Rollup: Apply the [@rollup/plugin-typescript](https://github.com/rollup/plugins/tree/master/packages/typescript) to the [k6-rollup-example](https://github.com/grafana/k6-rollup-example). + +## Using modules with Docker + +Built-in and remote modules work out of the box when running k6 in a Docker container like the [Grafana k6 Docker image](https://hub.docker.com/r/grafana/k6). + +### Local modules + +To run k6 with Docker and import a local module, you must make sure to mount the necessary folders from the host into the container, using [Docker volumes](https://docs.docker.com/engine/admin/volumes/volumes/). Thus, k6 can see all the JS modules it needs to import. + +For example, say you have the following structure on your host machine: + +- `/home/k6/example/src/index.js` +- `/home/k6/example/src/modules/module.js` + +{{< code >}} + +```javascript +import { hello_world } from './modules/module.js'; + +export default function () { + hello_world(); +} +``` + +{{< /code >}} + +{{< code >}} + +```javascript +export function hello_world() { + console.log('Hello world'); +} +``` + +{{< /code >}} + +To run index.js and make the modules available for import we execute the following Docker command with the `/home/k6/example/src` host folder mounted at `/src` in the container: + +{{< code >}} + +```bash +$ docker run --rm -v /home/k6/example/src:/src -i grafana/k6 run /src/index.js +``` + +{{< /code >}} + +Note that on Windows, you also need to make sure that your drive in question, say `C:\`, +has been marked for sharing in the Docker Desktop settings. + +### Extension modules + +The official [Grafana k6 Docker image](https://hub.docker.com/r/grafana/k6) includes the k6 release binary but lacks additional k6 extensions. Therefore, using the official Docker container to run a k6 test that requires an extension will fail. + +To run k6 with extensions in Docker, create a Docker image that includes the k6 binary with any extension you may want to use. Define a `Dockerfile` with the necessary [xk6 build](https://grafana.com/docs/k6//extensions/build-k6-binary-using-go#breaking-down-the-xk6-command) instructions as follows: + +```bash +FROM grafana/xk6:latest + +RUN GCO_ENABLED=0 xk6 build \ + --with github.com/grafana/xk6-kubernetes@latest + +ENTRYPOINT ["./k6"] +``` + +After building your custom k6 Docker image, you can [run k6 with Docker](https://grafana.com/docs/k6//get-started/running-k6/) as usual. + +Alternatively, you can implement a multistage Dockerfile build such as shown on this [Dockerfile example](https://github.com/grafana/xk6-output-prometheus-remote/blob/main/Dockerfile). + +## Read more + +- [JSLib](https://grafana.com/docs/k6//javascript-api/jslib): A collection of k6 JavaScript libraries maintained by Grafana Labs and available as remote modules. +- [Extensions directory](https://grafana.com/docs/k6//extensions/explore): A collection of k6 extensions maintained by Grafana Labs and the community. +- [k6-rollup-example](https://github.com/grafana/k6-rollup-example): Example using Rollup and Babel to bundle a common library and testing suite. +- [k6-template-es6](https://github.com/grafana/k6-template-es6): Template using Webpack and Babel to bundle k6 tests into CommonJS modules and polyfill ES+ features. +- [k6-template-typescript](https://github.com/grafana/k6-template-typescript): Template using Webpack and Babel to use TypeScript in your k6 scripts. +- [JavaScript Compatibility Mode](https://grafana.com/docs/k6//using-k6/javascript-compatibility-mode): An option to change the ECMAScript version supported by k6. diff --git a/docs/sources/v0.50.x/using-k6/protocols/_index.md b/docs/sources/v0.50.x/using-k6/protocols/_index.md new file mode 100644 index 000000000..5396ee1f9 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/_index.md @@ -0,0 +1,39 @@ +--- +title: 'Protocols' +description: 'Out of the box k6 comes with support for a few protocols: HTTP / WebSockets / gRPC / ...' +weight: 10 +--- + +# Protocols + +Out of the box, k6 supports the following protocols: + +- HTTP/1.1 +- [HTTP/2](https://grafana.com/docs/k6//using-k6/protocols/http-2) +- [WebSockets](https://grafana.com/docs/k6//using-k6/protocols/websockets) +- [gRPC](https://grafana.com/docs/k6//using-k6/protocols/grpc) + +You can use k6 on more protocols with xk6. + +## Upgrade to HTTP/2 is automatic + +By default, k6 uses HTTP/1.1 when it contacts a server. +If the server reports to k6 that it supports [HTTP/2](https://grafana.com/docs/k6//using-k6/protocols/http-2), k6 upgrades the connection to HTTP/2 instead. + +This is all automatic: +you don't need to do anything special for either the initial use of HTTP/1.1 or the potential protocol upgrade. +However, you might want to verify which protocol is actually being +used for a transaction. +This verification requires an extra step. + +Using [WebSockets](https://grafana.com/docs/k6//using-k6/protocols/websockets) is a bit different, for both the test structure and the VU lifecycle. + +## Extend protocol support with xk6 + +xk6 is a separate CLI tool that lets you build custom k6 binaries. +Community contributors have already added support for additional protocols, +with extensions for SQL, Kafka, ZeroMQ, Redis, and more. +Refer to the [full list of extensions](https://grafana.com/docs/k6//extensions/explore) and find the ones you want to build. Then follow the [xk6 guide](https://grafana.com/docs/k6//extensions/build-k6-binary-using-go). + +You can build your own extension, too. +See the [tutorials](https://grafana.com/docs/k6//extensions/create) to get started. diff --git a/docs/sources/v0.50.x/using-k6/protocols/grpc.md b/docs/sources/v0.50.x/using-k6/protocols/grpc.md new file mode 100644 index 000000000..611f59eaf --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/grpc.md @@ -0,0 +1,342 @@ +--- +title: 'gRPC' +description: 'gRPC is a lightweight open-source RPC framework. Starting with k6 v0.29.0, we support unary gRPC requests.' +weight: 03 +--- + +# gRPC + +## Overview + +[gRPC](https://grpc.io/) is a lightweight, open-source RPC framework. +It was originally developed by Google, with version 1.0 released in August 2016. +Since then, it's gained much attention and wide adoption. + +Whereas JSON transmits as human-readable text, gRPC is binary. +The binary format makes data transfer faster and more compact. +In the benchmarks we've seen, gRPC has proved much faster than REST, gRPC's more traditional, JSON-based counterpart. +The messages and services used for gRPC are described in `.proto` files, containing definitions for [Protocol Buffers](https://en.wikipedia.org/wiki/Protocol_Buffers) (protobuf). + +## Load testing gRPC services with k6 + +Starting on k6 v0.49.0, k6 supports unary gRPC requests and streaming as part of the `k6/net/grpc` core module. + +### gRPC definitions + +Before interacting with a gRPC service, k6 needs to learn the definitions of the messages and services. + +One way to do that is to explicitly use the `Client.load()` method and load the client definitions from the local file system. The method accepts a list of import paths and a list of `.proto` files. k6 then loads all the definitions from the files and their dependencies. + +{{< code >}} + +```javascript +import { Client } from 'k6/net/grpc'; + +const client = new Client(); +client.load(['definitions'], 'hello.proto'); +``` + +{{< /code >}} + +Alternatively, you can dynamically load the definitions by using the gRPC reflection protocol. To enable reflection, you can pass the `reflect: true` option to `Client.connect()`. k6 then loads all the definitions from the server and their dependencies. + +This option is only possible if the server has been instrumented with reflection support. + +{{< code >}} + +```javascript +import { Client } from 'k6/net/grpc'; + +const client = new Client(); +client.connect('127.0.0.1:10000', { reflect: true }); +``` + +{{< /code >}} + +### Unary gRPC requests + +Unary calls work the same way as regular HTTP requests. A single request is sent to a server, and the server replies with a single response. + +{{< code >}} + +```javascript +import { Client, StatusOK } from 'k6/net/grpc'; +import { check, sleep } from 'k6'; + +const client = new Client(); +client.load(['definitions'], 'hello.proto'); + +export default () => { + client.connect('127.0.0.1:10000', {}); + + const data = { greeting: 'Bert' }; + const response = client.invoke('hello.HelloService/SayHello', data); + + check(response, { + 'status is OK': (r) => r && r.status === StatusOK, + }); + + console.log(JSON.stringify(response.message)); + + client.close(); + sleep(1); +}; +``` + +{{< /code >}} + +### Server gRPC streaming + +In server streaming mode, the client sends a single request to the server, and the server replies with multiple responses. + +The example below demonstrates server streaming. + +{{< code >}} + +```javascript +import { Client, Stream } from 'k6/net/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; + +const client = new Client(); + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true, reflect: true }); + } + + const stream = new Stream(client, 'main.FeatureExplorer/ListFeatures', null); + + stream.on('data', function (feature) { + console.log( + `Found feature called "${feature.name}" at ${feature.location.latitude / COORD_FACTOR}, ${ + feature.location.longitude / COORD_FACTOR + }` + ); + }); + + stream.on('end', function () { + // The server has finished sending + client.close(); + console.log('All done'); + }); + + // send a message to the server + stream.write({ + lo: { + latitude: 400000000, + longitude: -750000000, + }, + hi: { + latitude: 420000000, + longitude: -730000000, + }, + }); + + sleep(0.5); +}; +``` + +{{< /code >}} + +In the example script, k6 connects to a gRPC server, creates a stream, and sends a message to the server with latitude and longitude coordinates. When the server sends data back, it logs the feature name and its location. When the server finishes sending data, it closes the client connection and logs a completion message. + +### Client gRPC streaming + +The client streaming mode is the opposite of the server streaming mode. The client sends multiple requests to the server, and the server replies with a single response. + +The example below demonstrates client streaming. + +{{< code >}} + +```javascript +import { Client, Stream } from 'k6/net/grpc'; +import { sleep } from 'k6'; + +const COORD_FACTOR = 1e7; +const client = new Client(); + +// a sample points collection +const points = [ + { + location: { latitude: 407838351, longitude: -746143763 }, + name: 'Patriots Path, Mendham, NJ 07945, USA', + }, + { + location: { latitude: 408122808, longitude: -743999179 }, + name: '101 New Jersey 10, Whippany, NJ 07981, USA', + }, + { + location: { latitude: 413628156, longitude: -749015468 }, + name: 'U.S. 6, Shohola, PA 18458, USA', + }, + { + location: { latitude: 419999544, longitude: -740371136 }, + name: '5 Conners Road, Kingston, NY 12401, USA', + }, + { + location: { latitude: 414008389, longitude: -743951297 }, + name: 'Mid Hudson Psychiatric Center, New Hampton, NY 10958, USA', + }, +]; + +export default () => { + if (__ITER == 0) { + client.connect('127.0.0.1:10000', { plaintext: true, reflect: true }); + } + + const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + + stream.on('data', (stats) => { + console.log(`Finished trip with ${stats.pointCount} points`); + console.log(`Passed ${stats.featureCount} features`); + console.log(`Travelled ${stats.distance} meters`); + console.log(`It took ${stats.elapsedTime} seconds`); + }); + + stream.on('end', () => { + client.close(); + console.log('All done'); + }); + + // send 3 random points + for (let i = 0; i < 3; i++) { + const point = points[Math.floor(Math.random() * points.length)]; + pointSender(stream, point); + } + + // close the client stream + stream.end(); +}; + +const pointSender = (stream, point) => { + console.log( + `Visiting point ${point.name} ${point.location.latitude / COORD_FACTOR}, ${ + point.location.longitude / COORD_FACTOR + }` + ); + + // send the location to the server + stream.write(point.location); + + sleep(0.5); +}; +``` + +{{< /code >}} + +In the example script, k6 establishes a connection to a gRPC server, creates a stream, and sends three random points. The server responds with statistics about the trip, which are logged to the console. The code also handles the end of the stream, closing the client and logging a completion message. + +### Bidirectional gRPC streaming + +In bi-directional streaming mode, the client and the server may send multiple messages. + +From the API perspective, it combines the client and server streaming modes, so the code is similar to the examples above. + +### Streaming error handling + +To catch errors that occur during streaming, you can use the `error` event handler. + +The handler receives [an error object](https://grafana.com/docs/k6//javascript-api/k6-net-grpc/stream/stream-error/). + +{{< code >}} + +```javascript +import { Client, Stream } from 'k6/net/grpc'; + +const client = new Client(); +const stream = new Stream(client, 'main.RouteGuide/RecordRoute'); + +stream.on('error', function (e) { + // An error has occurred and the stream has been closed. + console.log('Error: ' + JSON.stringify(e)); +}); +``` + +{{< /code >}} + +### Protocol Buffers JSON Mapping + +It's important to note how k6 handles requests and messages. First, it tries to marshal the request/message in JSON format. Then, k6 uses the [protojson](https://pkg.go.dev/google.golang.org/protobuf/encoding/protojson) package to encode or decode to a Protobuf message. + +A limitation during this process is that the object you pass as a request/message must be serializable. That means structs like `Map` don't work. + +The benefit of using `protojson` is the canonical JSON encoding support. The [Protocol Buffers documentation](https://protobuf.dev/programming-guides/proto3/#json) describes this mapping. + +#### Examples + +For instance, if you import `"google/protobuf/wrappers.proto"` and your proto-definitions look like this: + +```proto +syntax = "proto3"; + +package testing; + +import "google/protobuf/wrappers.proto"; + +service Service { + rpc SayHey(google.protobuf.StringValue) returns (google.protobuf.StringValue); + rpc DoubleInteger(google.protobuf.Int64Value) returns (google.protobuf.Int64Value); +} + +``` + +When passing a message, you should use a string or an integer, not an object. As a result, you will receive a type that has already been marshaled. + +{{< code >}} + +```javascript +import { Client } from 'k6/net/grpc'; +const client = new Client(); + +// an example of passing a string +const respString = client.invoke('testing.Service/SayHey', 'John'); +if (respString.message !== 'hey John') { + throw new Error("expected to get 'hey John', but got a " + respString.message); +} + +// an example of passing an integer +const respInt = client.invoke('testing.Service/DoubleInteger', '3'); +if (respInt.message !== '6') { + throw new Error("expected to get '6', but got a " + respInt.message); +} +``` + +{{< /code >}} + +Another example could be usage of `oneof`. Let's say you have a proto-definition like this: + +```proto +syntax = "proto3"; + +package testing; + +service Service { + rpc Test(Foo) returns (Foo) {} +} + +message Foo { + oneof Bar { + string code = 1; + uint32 id = 2; + } +} +``` + +In this case, you should pass an object either with `code` or `id` fields. + +{{< code >}} + +```javascript +import { Client } from 'k6/net/grpc'; +const client = new Client(); + +// calling RPC with filled code field +const respWithCode = client.invoke('testing.Service/Test', { code: 'abc-123' }); + +// calling RPC with filled id field +const respWithID = client.invoke('testing.Service/Test', { id: 123 }); +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/protocols/http-2.md b/docs/sources/v0.50.x/using-k6/protocols/http-2.md new file mode 100644 index 000000000..0c603cd50 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/http-2.md @@ -0,0 +1,50 @@ +--- +title: 'HTTP/2' +descriptiontiontiontiontion: 'When you make HTTP requests in k6 it will automatically upgrade the connection to HTTP/2.0 if the server supports it, just like your web browser would.' +weight: 01 +--- + +# HTTP/2 + +## Overview + +HTTP/2.0 is the latest version of the HTTP protocol. +It improves significantly upon HTTP/1. +Most importantly, it introduces a binary wire protocol with multiplexed streams over a single TCP connection. +This solves a long-standing performance issue with HTTP/1.1: [head-of-line blocking](https://en.wikipedia.org/wiki/Head-of-line_blocking). + +Well, it at least _partially_ solves it. +The TCP congestion control mechanisms can interfere with the intended independent nature of the multiplexed streams in cases of lost/dropped packets and retransmission/reassembly. +The full solution is to run HTTP/2.0 over UDP, as Google implemented with [QUIC](https://en.wikipedia.org/wiki/QUIC). + +## Additional features of HTTP/2.0 + +- Builtin compression of HTTP headers +- Server push +- Pipelining of requests +- Prioritization of requests + +## Load testing HTTP/2 with k6 + +When you make HTTP requests in k6, k6 automatically upgrades the connection to HTTP/2.0 if the server supports it, just like your web browser would. + +To check what protocol was used for a particular request, refer to the `proto` property of the response object. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export default function () { + const res = http.get('https://test-api.k6.io/'); + check(res, { + 'protocol is HTTP/2': (r) => r.proto === 'HTTP/2.0', + }); + sleep(1); +} +``` + +{{< /code >}} + +To see the values that the `r.proto` field can have, refer to the documentation for [k6 HTTP](https://grafana.com/docs/k6//javascript-api/k6-http/response). diff --git a/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/_index.md b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/_index.md new file mode 100644 index 000000000..e741a92dc --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/_index.md @@ -0,0 +1,38 @@ +--- +title: 'SSL/TLS' +description: 'By default and without any special configuration, k6 will connect and talk to servers over TLS. You just need to make sure to specify your request URLs with the https scheme.' +weight: 04 +weight: 04 +--- + +# SSL/TLS + +Transport Layer Security (TLS), the successor of Secure Socket Layer (SSL), is the mechanism through +which encrypted connections can be established between clients and servers on the web and through +which data can flow with integrity intact. + +Simplified, TLS is made possible by using a Public Key Infrastructure (PKI) involving private +cryptographic keys, certificates of ownership (public, signed with private key), and certificate +authorities (CAs, issuing and asserting the validity of issued certificates). + +The entire system depends on the functioning operation of the CAs. +The system trusts that the cryptographic keys that CAs use to sign certificates of ownership to site/domain owners are kept secret. +It also depends on the domain owners' ability to keep their private cryptographic keys secret. + +If they fail to do so, they have to report the leak to the CA that issued the certificate. +After this disclosure, the CA can revoke the certificate and, in doing so, let browsers and other clients know of the changes through +the Online Certificate Status Protocol (OCSP). + +## TLS in k6 + +By default and without any special configuration, k6 connects and talks to servers over TLS. +You just need to make sure that you specify your request URLs with the `https` scheme. + +K6 supports the following TLS functionalities. +Each is worth discussing in more detail: + +- [TLS client certificates](https://grafana.com/docs/k6//using-k6/protocols/ssl-tls/ssl-tls-client-certificates) +- [TLS version and ciphers](https://grafana.com/docs/k6//using-k6/protocols/ssl-tls/ssl-tls-version-and-ciphers) (restricting as + well as checking what was used for a HTTP request by checking response object properties) +- [Online Certificate Status Protocol (OCSP)](https://grafana.com/docs/k6//using-k6/protocols/ssl-tls/online-certificate-status-protocol--ocsp) +- [Response.timings.tls_handshaking](https://grafana.com/docs/k6//javascript-api/k6-http/response) diff --git a/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/online-certificate-status-protocol-ocsp.md b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/online-certificate-status-protocol-ocsp.md new file mode 100644 index 000000000..659b02e9a --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/online-certificate-status-protocol-ocsp.md @@ -0,0 +1,79 @@ +--- +title: 'Online Certificate Status Protocol (OCSP)' +description: 'k6 supports OCSP stapling, receiving and parsing a stapled response as part of +the TLS connection setup.' +aliases: + - ./online-certificate-status-protocol--ocsp/ +--- + +## What is OCSP? + +The Online Certificate Status Protocol (OCSP) lets web browsers and clients check the status of an issued TLS certificate with a Certificate Authority (CA), ensuring that the certificate has not been revoked. + +It exists different ways to check whether the certificate has been revoked. +Each way places the burden on different parties: + +- The browser/client: talk to the CA (or through a CA–entrusted OCSP responder) with OCSP. One downside + with this approach is that the CA's servers need to be available. + +- The browser vendor: maintain an up-to-date list of certificate revocations by talking to + the CAs (or through a CA–entrusted OCSP responder) and distributing this list to the browsers + running on users' machines. +- The server side: the server handles the interaction with the CA (or through a CA–entrusted OCSP + responder), caching the results of the periodic updates and including a "stapled response" + (referred to as OCSP stapling) in the TLS connection setup with the browser/client. + +## OCSP with k6 + +k6 supports OCSP stapling. +The application can receive and parse a stapled response as part of the TLS connection setup. +The OCSP response information is available in the `ocsp.stapled_response` property of the response object. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res = http.get('https://stackoverflow.com'); + check(res, { + 'is OCSP response good': (r) => r.ocsp.status === http.OCSP_STATUS_GOOD, + }); +} +``` + +{{< /code >}} + +## Properties of an OCSP object + +The OCSP `ocsp` object contains the following properties: + +| Key | Type | Description | +| ------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `status` | string | the status of the certificate, see possible values below | +| `revocation_reason` | string | the reason for revocation of the certificate (if that is the status), see possible values below | +| `produced_at` | number | number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time when this OCSP stapled response was signed by the CA (or CA–entrusted OCSP responder) | +| `this_update` | number | number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time when the status being indicated was known to be correct | +| `next_update` | number | number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time when this OCSP stapled response will be refreshed with CA (or by CA entrusted OCSP responder) | +| `revoked_at` | number | number of milliseconds elapsed since 1 January 1970 00:00:00 UTC, representing the time when this certificate was revoked (if that is the status) | + +### Possible values for `status`: + +- `http.OCSP_STATUS_GOOD` +- `http.OCSP_STATUS_REVOKED` +- `http.OCSP_STATUS_UNKNOWN` +- `http.OCSP_STATUS_SERVER_FAILED` + +### Possible values for `revocation_reason`: + +- `http.OCSP_REASON_UNSPECIFIED` +- `http.OCSP_REASON_KEY_COMPROMISE` +- `http.OCSP_REASON_CA_COMPROMISE` +- `http.OCSP_REASON_AFFILIATION_CHANGED` +- `http.OCSP_REASON_SUPERSEDED` +- `http.OCSP_REASON_CESSATION_OF_OPERATION` +- `http.OCSP_REASON_CERTIFICATE_HOLD` +- `http.OCSP_REASON_REMOVE_FROM_CRL` +- `http.OCSP_REASON_PRIVILEGE_WITHDRAWN` +- `http.OCSP_REASON_AA_COMPROMISE` diff --git a/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/ssl-tls-client-certificates.md b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/ssl-tls-client-certificates.md new file mode 100644 index 000000000..e3434548e --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/ssl-tls-client-certificates.md @@ -0,0 +1,90 @@ +--- +title: 'SSL/TLS client certificates' +descriptiontiontion: 'To use client certificates, you specify global that tell k6 how to map a public certificate and private key to the domains they are valid for.' +--- + +# SSL/TLS client certificates + +Discussion about TLS certificates is usually about how clients authenticate servers. +However, both TLS and k6 also support the reverse process, in which servers authenticate clients. + +To use client certificates, specify global [configuration options](https://grafana.com/docs/k6//using-k6/k6-options) that tell k6 how to map a public certificate and private key to the domains they are valid for. +You can load the certificate and key from local files or embed them as strings in the script. + +## Loading a certificate and a key from local files + +To load a certificate and a key from local files, use the builtin `open(...)` function: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + tlsAuth: [ + { + domains: ['example.com'], + cert: open('./mycert.pem'), + key: open('./mycert-key.pem'), + }, + ], +}; + +export default function () { + http.get('https://example.com/'); +} +``` + +{{< /code >}} + +## Loading certificate and key from embedded strings + +To load the certificate and key from embedded strings, use this snippet. +Note the use of +[template literals](https://developer.mozilla.org/en-US/Web/JavaScript/Reference/Template_literals) for multi-line strings): + +{{% admonition type="note" %}} + +These are just example keys. + +{{% /admonition %}} + +{{< code >}} + +```javascript +import http from 'k6/http'; + +const CERT = `-----BEGIN CERTIFICATE----- +MIIFgTCCA2kCAQEwDQYJKoZIhvcNAQEFBQAwgYExCzAJBgNVBAYTAlNFMRcwFQYD +VQQIEw5TdG9ja2hvbG1zIExhbjESMBAGA1UEBxMJU3RvY2tob2xtMRcwFQYDVQQK +... +/n5QrTGhP51P9Q1THzRfn6cNCDwzSTMVEJr40QhuTJQWASe3miuFmZoG5ykmGqVm +fWQRiQyM330s9vTwFy14J2Bxe4px6cyy7rVXvYL2LvfA4L0T7/x1nUULw+Mpqun1 +R3XRJWqGDjBKXr5q8VatdQO1QLgr +-----END CERTIFICATE-----`; + +const KEY = `-----BEGIN RSA PRIVATE KEY----- +KsZVVI1FTX+F959vqu1S02T+R1JM29PkIfJILIXapKQfb0FWrALU5xpipdPYBWp7 +j5iSp06/7H8ms87Uz9BrOA6rytoRSE0/wEe5WkWdBBgLLPpfOSWZsAA5RGCB2n+N +... +Dk+frzKuiErHFN7HOHAQannui4eLsY0ehYMByowgJIUGzIJyXR6O19hVhV7Py66u +X7/Jy01JXn83LuWdpaPAKU+B42BLP0IGXt5CocPms07HOdtJ/wm2zwHTyfjn9vu+ +HO/dQr6a7DhRu2lLI9Sc983NwRqDKICZQQ/+gqWk8BgQZ1yI9O4AYkzywzAEk3py +-----END RSA PRIVATE KEY-----`; + +export const options = { + tlsAuth: [ + { + domains: ['example.com'], + cert: CERT, + key: KEY, + }, + ], +}; + +export default function () { + http.get('https://example.com/'); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/ssl-tls-version-and-ciphers.md b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/ssl-tls-version-and-ciphers.md new file mode 100644 index 000000000..b7d4033bf --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/ssl-tls/ssl-tls-version-and-ciphers.md @@ -0,0 +1,137 @@ +--- +title: 'SSL/TLS version and ciphers' +description: 'To support testing specific client configurations, you can set a specific version or range +of versions of SSL/TLS that should be allowed for a connection.' +--- + +# SSL/TLS version and ciphers + +To support testing specific client configurations, you can specify a version or range of versions of SSL/TLS that are allowed for a connection. +You can as also specify which cipher suites are allowed for that connection. + +> #### ⚠️ Reg. ciphers and TLS 1.3 +> +> Due to limitations in the underlying [go implementation](https://github.com/golang/go/issues/29349), changing the ciphers for TLS 1.3 is _not_ supported and will do nothing. + +## Limiting SSL/TLS version + +To limit the k6 to a specific SSL/TLS version, use a global +[configuration option](https://grafana.com/docs/k6//using-k6/k6-options): + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + tlsVersion: http.TLS_1_2, +}; + +export default function () { + http.get('https://badssl.com'); +} +``` + +{{< /code >}} + +You can also accept a range of SSL/TLS versions: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + tlsVersion: { + min: http.SSL_3_0, + max: http.TLS_1_2, + }, +}; + +export default function () { + http.get('https://badssl.com'); +} +``` + +{{< /code >}} + +## Versions available to choose from + +Here's the list of available SSL/TLS versions that you can choose from, ordered from oldest version to latest. + +- `http.SSL_3_0` +- `http.TLS_1_0` +- `http.TLS_1_1` +- `http.TLS_1_2` +- `http.TLS_1_3` + +## Limiting cipher suites + +To limit the cipher suites that k6 is allowed to use, there's a global +[configuration option](https://grafana.com/docs/k6//using-k6/k6-options). +You choose a list of allowed ciphers: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + tlsCipherSuites: ['TLS_RSA_WITH_RC4_128_SHA', 'TLS_RSA_WITH_AES_128_GCM_SHA256'], +}; + +export default function () { + http.get('https://badssl.com'); +} +``` + +{{< /code >}} + +## Checking SSL/TLS version and cipher suite used in requests + +You can also check which SSL/TLS version and ciphers were used. +To do so, look at the `tls_version` and `tls_cipher_suite` response object properties. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check } from 'k6'; + +export default function () { + const res = http.get('https://sha256.badssl.com'); + check(res, { + 'is TLSv1.2': (r) => r.tls_version === http.TLS_1_2, + 'is sha256 cipher suite': (r) => r.tls_cipher_suite === 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', + }); +} +``` + +{{< /code >}} + +## Cipher suites available to choose from + +Here's a list of available SSL/TLS cipher suites: + +- `TLS_RSA_WITH_RC4_128_SHA` +- `TLS_RSA_WITH_3DES_EDE_CBC_SHA` +- `TLS_RSA_WITH_AES_128_CBC_SHA` +- `TLS_RSA_WITH_AES_256_CBC_SHA` +- `TLS_RSA_WITH_AES_128_GCM_SHA256` +- `TLS_RSA_WITH_AES_256_GCM_SHA384` +- `TLS_ECDHE_ECDSA_WITH_RC4_128_SHA` +- `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA` +- `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` +- `TLS_ECDHE_RSA_WITH_RC4_128_SHA` +- `TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA` +- `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA` +- `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` +- `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` +- `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` +- `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` +- `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` + +> ### ⚠️ Differences depending on k6 build +> +> This list reflects the available cipher suites in the latest official build. +> If you are using a custom-built k6, the available cipher suites will depend on the Go version you compiled it with, see [https://golang.org/pkg/crypto/tls/#pkg-constants](https://golang.org/pkg/crypto/tls/#pkg-constants). diff --git a/docs/sources/v0.50.x/using-k6/protocols/websockets.md b/docs/sources/v0.50.x/using-k6/protocols/websockets.md new file mode 100644 index 000000000..ca649afb7 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/protocols/websockets.md @@ -0,0 +1,206 @@ +--- +title: 'WebSockets' +description: 'Comparing HTTP based tests to WebSocket ones, there are some differences in the structure and inner workings of k6.' +weight: 02 +--- + +# WebSockets + +## Overview + +[WebSocket](https://en.wikipedia.org/wiki/WebSocket) is a protocol that provides full-duplex communication channels over a single TCP connection. +It is commonly used by single-page apps (SPAs) and mobile apps, to add server-push based functionality, which usually improves performance in polling-based solutions. + +## Load testing WebSockets with k6 + +{{% admonition type="note" %}} + +[xk6-websockets](https://github.com/grafana/xk6-websockets) is an experimental module with a more standard API than `k6/ws`. It implements [the WebSockets API living standard](https://websockets.spec.whatwg.org/). While the implementation isn't complete, it uses a global event loop instead of local one. + +Currently, it's available as an experimental module [`k6/experimental/websockets`](https://grafana.com/docs/k6//javascript-api/k6-experimental/websockets). It's also likely that it will become part of the core of k6 in the future. + +{{% /admonition %}} + +Comparing HTTP-based tests to WebSocket ones, you'll find differences in both structure and inner workings. +The primary difference is that instead of continuously looping the main function (`export default function() { ... }`) over and over, each VU is now runs an asynchronous event loop. + +The basic structure of a WebSocket test looks like this: + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const res = ws.connect(url, params, function (socket) { + socket.on('open', () => console.log('connected')); + socket.on('message', (data) => console.log('Message received: ', data)); + socket.on('close', () => console.log('disconnected')); + }); + + check(res, { 'status is 101': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} + +In this example, the [connect()](https://grafana.com/docs/k6//javascript-api/k6-ws/connect) method takes a "run" function as its third parameter. +That function should accept a [Socket](https://grafana.com/docs/k6//javascript-api/k6-ws/socket) object as its only parameter. +The run function forms the basis of the asynchronous event loop. + +When the WebSocket connection is created, the run function will be immediately called, all code inside it will be executed (usually code to set up event handlers), and then blocked until the WebSocket connection is closed (by the remote host or by using [socket.close()](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-close)). + +## Error handling + +To catch errors happen during the life of a WebSocket connection, attach a handler to the "error" event: + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const res = ws.connect(url, params, function (socket) { + socket.on('open', function open() { + // ... + }); + + socket.on('error', function (e) { + if (e.error() != 'websocket: close sent') { + console.log('An unexpected error occured: ', e.error()); + } + }); + }); + + check(res, { 'status is 101': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} + +## Timers + +To schedule a recurring action, use the [socket.setInterval](https://grafana.com/docs/k6//javascript-api/k6-ws/socket#section-socketsetinterval) to specify a function to call at a particular interval. + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const res = ws.connect(url, params, function (socket) { + socket.on('open', function open() { + console.log('connected'); + + socket.setInterval(function timeout() { + socket.ping(); + console.log('Pinging every 1sec (setInterval test)'); + }, 1000); + }); + + socket.on('ping', () => console.log('PING!')); + socket.on('pong', () => console.log('PONG!')); + socket.on('close', () => console.log('disconnected')); + }); + + check(res, { 'status is 101': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} + +## Timeouts + +To add a timeout to the WebSocket connection, pass both a handler function and a timeout value (in milliseconds) to the [socket.setTimeout](https://grafana.com/docs/k6//javascript-api/k6-ws/socket/socket-settimeout) function. + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const res = ws.connect(url, params, function (socket) { + socket.on('open', () => console.log('connected')); + socket.on('close', () => console.log('disconnected')); + + socket.setTimeout(function () { + console.log('2 seconds passed, closing the socket'); + socket.close(); + }, 2000); + }); + + check(res, { 'status is 101': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} + +In the preceding example, the timeout will close the WebSocket connection after 2 seconds. + +## Multiple event handlers + +You can attach multiple handler functions to an event: + +{{< code >}} + +```javascript +import ws from 'k6/ws'; +import { check } from 'k6'; + +export default function () { + const url = 'ws://echo.websocket.org'; + const params = { tags: { my_tag: 'hello' } }; + + const response = ws.connect(url, params, function (socket) { + socket.on('open', function open() { + console.log('connected'); + socket.send(Date.now()); + + socket.setInterval(function timeout() { + socket.ping(); + console.log('Pinging every 1sec (setInterval test)'); + }, 1000); + }); + + socket.on('ping', () => console.log('PING!')); + socket.on('pong', () => console.log('PONG!')); + socket.on('pong', () => { + // Multiple event handlers on the same event + console.log('OTHER PONG!'); + }); + + socket.on('close', () => console.log('disconnected')); + + socket.on('error', (e) => { + if (e.error() != 'websocket: close sent') { + console.log('An unexpected error occurred: ', e.error()); + } + }); + + socket.setTimeout(function () { + console.log('2 seconds passed, closing the socket'); + socket.close(); + }, 2000); + }); + + check(response, { 'status is 101': (r) => r && r.status === 101 }); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/scenarios/_index.md b/docs/sources/v0.50.x/using-k6/scenarios/_index.md new file mode 100644 index 000000000..8763b77b7 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/_index.md @@ -0,0 +1,207 @@ +--- +title: Scenarios +description: 'Scenarios allow us to make in-depth configurations to how VUs and iterations are scheduled. This makes it possible to model diverse traffic patterns in load tests.' +weight: 13 +--- + +# Scenarios + +Scenarios configure how VUs and iteration schedules in granular detail. +With scenarios, you can model diverse _workloads_, or traffic patterns in load tests. + +Benefits of using scenarios include: + +- **Easier, more flexible test organization.** You can declare multiple scenarios in the same script, + and each one can independently execute a different JavaScript function. +- **Simulate more realistic traffic.** + Every scenario can use a distinct VU and iteration scheduling pattern, + powered by a purpose-built [executor](#scenario-executors). +- **Parallel or sequential workloads.** Scenarios are independent from each other and run in parallel, though they can be made to appear sequential by setting the `startTime` property of each carefully. +- **Granular results analysis.** Different environment variables and metric tags can be set per scenario. + +## Configure scenarios + +To configure scenarios, use the `scenarios` key in the `options` object. +You can give the scenario any name, as long as each scenario name in the script is unique. + +The scenario name appears in the result summary, tags, and so on. + +{{< code >}} + +```javascript +export const options = { + scenarios: { + example_scenario: { + // name of the executor to use + executor: 'shared-iterations', + + // common scenario configuration + startTime: '10s', + gracefulStop: '5s', + env: { EXAMPLEVAR: 'testing' }, + tags: { example_tag: 'testing' }, + + // executor-specific configuration + vus: 10, + iterations: 200, + maxDuration: '10s', + }, + another_scenario: { + /*...*/ + }, + }, +}; +``` + +{{< /code >}} + +## Scenario executors + +For each k6 scenario, the VU workload is scheduled by an _executor_. +Executors configure how long the test runs, whether traffic stays constant or changes, and whether the workload is modeled by VUs or by arrival rate (that is, [open or closed models](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed)). + +Your scenario object must define the `executor` property with one of the predefined executor names. +Your choice of executor determines how k6 models load. +Choices include: + +- **By number of iterations.** + + - [`shared-iterations`](https://grafana.com/docs/k6//using-k6/scenarios/executors/shared-iterations) shares iterations between VUs. + - [`per-vu-iterations`](https://grafana.com/docs/k6//using-k6/scenarios/executors/per-vu-iterations) has each VU run the configured iterations. + +- **By number of VUs.** + + - [`constant-VUs`](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus) sends VUs at a constant number. + - [`ramping-vus`](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus) ramps the number of VUs according to your configured stages. + +- **By iteration rate.** + + - [`constant-arrival-rate`](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-arrival-rate) starts iterations at a constant rate. + - [`ramping-arrival-rate`](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-arrival-rate) ramps the iteration rate according to your configured stages. + +Along with the generic scenario options, each executor object has additional options specific to its workload. +For the full list, refer to [Executors](https://grafana.com/docs/k6//using-k6/scenarios/executors). + +## Scenario options {#options} + +| Option | Type | Description | Default | +| ------------------------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | +| executor(required) ️ | string | Unique executor name. See the list of possible values in the [executors](https://grafana.com/docs/k6//using-k6/scenarios/executors) section. | - | +| startTime | string | Time offset since the start of the test, at which point this scenario should begin execution. | `"0s"` | +| gracefulStop | string | Time to wait for iterations to finish executing before stopping them forcefully. To learn more, read [Graceful stop](https://grafana.com/docs/k6//using-k6/scenarios/concepts/graceful-stop). | `"30s"` | +| exec | string | Name of exported JS function to execute. | `"default"` | +| env | object | Environment variables specific to this scenario. | `{}` | +| tags | object | [Tags](https://grafana.com/docs/k6//using-k6/tags-and-groups) specific to this scenario. | `{}` | + +## Scenario example + +This script combines two scenarios, with sequencing: + +- The `shared_iter_scenario` starts immediately. Ten VUs try to use 100 iterations as quickly as possible (some VUs may use more iterations than others). +- The `per_vu_scenario` starts after 10s. In this case, ten VUs each run ten iterations. + +Which scenario takes longer? +You can run to discover. +You can also add a `maxDuration` property to one or both scenarios. + +```javascript +import http from 'k6/http'; + +export const options = { + scenarios: { + shared_iter_scenario: { + executor: 'shared-iterations', + vus: 10, + iterations: 100, + startTime: '0s', + }, + per_vu_scenario: { + executor: 'per-vu-iterations', + vus: 10, + iterations: 10, + startTime: '10s', + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/'); +} +``` + +If you run a script with scenarios, k6 output includes high-level information about each one. +For example, if you run the preceding script, `k6 run scenario-example.js`, +then k6 reports the scenarios as follows: + +{{< code >}} + +```bash + execution: local + script: scenario-example.js + output: - + + scenarios: (100.00%) 2 scenarios, 20 max VUs, 10m40s max duration (incl. grace +ful stop): + * shared_iter_scenario: 100 iterations shared among 10 VUs (maxDurati +on: 10m0s, gracefulStop: 30s) + * per_vu_scenario: 10 iterations for each of 10 VUs (maxDuration: 10m +0s, startTime: 10s, gracefulStop: 30s) +``` + +{{< /code >}} + +The full output includes the summary metrics, like any default end-of-test summary: + +{{< collapse title="full scenario-example.js output" >}} + +```bash + /\ |‾‾| /‾‾/ /‾‾/ + /\ / \ | |/ / / / + / \/ \ | ( / ‾‾\ + / \ | |\ \ | (‾) | + / __________ \ |__| \__\ \_____/ .io + + execution: local + script: scenario-example.js + output: - + + scenarios: (100.00%) 2 scenarios, 20 max VUs, 10m40s max duration (incl. grace +ful stop): + * shared_iter_scenario: 100 iterations shared among 10 VUs (maxDurati +on: 10m0s, gracefulStop: 30s) + * per_vu_scenario: 10 iterations for each of 10 VUs (maxDuration: 10m +0s, startTime: 10s, gracefulStop: 30s) + + +running (00m12.8s), 00/20 VUs, 200 complete and 0 interrupted iterations +shared_iter_scenario ✓ [ 100% ] 10 VUs 00m02.7s/10m0s 100/100 shared iters +per_vu_scenario ✓ [ 100% ] 10 VUs 00m02.8s/10m0s 100/100 iters, 10 per V + + data_received..................: 2.4 MB 188 kB/s + data_sent......................: 26 kB 2.1 kB/s + http_req_blocked...............: avg=64.26ms min=1.56µs med=8.28µs max +=710.86ms p(90)=57.63ms p(95)=582.36ms + http_req_connecting............: avg=28.6ms min=0s med=0s max +=365.92ms p(90)=20.38ms p(95)=224.44ms + http_req_duration..............: avg=204.25ms min=169.55ms med=204.36ms max +=407.95ms p(90)=205.96ms p(95)=239.32ms + { expected_response:true }...: avg=204.25ms min=169.55ms med=204.36ms max +=407.95ms p(90)=205.96ms p(95)=239.32ms + http_req_failed................: 0.00% ✓ 0 ✗ 200 + http_req_receiving.............: avg=195.54µs min=53.87µs med=162.55µs max +=1.52ms p(90)=260.6µs p(95)=317.89µs + http_req_sending...............: avg=38.16µs min=7.61µs med=37.99µs max +=167.54µs p(90)=50.16µs p(95)=60.18µs + http_req_tls_handshaking.......: avg=24.03ms min=0s med=0s max +=274.05ms p(90)=20.81ms p(95)=212.74ms + http_req_waiting...............: avg=204.01ms min=169.32ms med=204.11ms max +=407.82ms p(90)=205.74ms p(95)=239.01ms + http_reqs......................: 200 15.593759/s + iteration_duration.............: avg=268.83ms min=169.78ms med=204.67ms max +=952.85ms p(90)=444.97ms p(95)=786.52ms + iterations.....................: 200 15.593759/s + vus............................: 10 min=0 max=10 + vus_max........................: 20 min=20 max=20 +``` + +{{< /collapse >}} diff --git a/docs/sources/v0.50.x/using-k6/scenarios/advanced-examples.md b/docs/sources/v0.50.x/using-k6/scenarios/advanced-examples.md new file mode 100644 index 000000000..188f8e5c9 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/advanced-examples.md @@ -0,0 +1,203 @@ +--- +title: 'Advanced Examples' +description: 'Advanced Examples using the k6 Scenario API - Using multiple scenarios, different environment variables and tags per scenario.' +weight: 02 +--- + +# Advanced Examples + +You can use multiple scenarios in one script, and these scenarios can be run in sequence or in parallel. +Some ways that you can combine scenarios include the following: + +- Have different start times to sequence workloads +- Add per-scenario tags and environment variables +- Make scenario-specific thresholds. +- Use multiple scenarios to run different test logic, so that VUs don't run only the [`default` function](https://grafana.com/docs/k6//using-k6/test-lifecycle). + +## Combine scenarios + +With the `startTime` property, you can configure your script to start some scenarios later than others. +To sequence your scenarios, you can combine `startTime` with the duration options specific to the executor. +(this is easiest to do with executors with set durations, like the arrival-rate executors). + +This script has two scenarios, `contacts` and `news`, which run in sequence: + +1. At the beginning of the test, k6 starts the `contacts` scenario. 50 VUs try to run as many iterations as possible for 30 seconds. +1. After 30 seconds, k6 starts the `news` scenario. 50 VUs each try to run 100 iterations in one minute. + +Along with `startTime`, `duration`, and `maxDuration`, note the different test logic for each scenario. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'constant-vus', + exec: 'contacts', + vus: 50, + duration: '30s', + }, + news: { + executor: 'per-vu-iterations', + exec: 'news', + vus: 50, + iterations: 100, + startTime: '30s', + maxDuration: '1m', + }, + }, +}; + +export function contacts() { + http.get('https://test.k6.io/contacts.php', { + tags: { my_custom_tag: 'contacts' }, + }); +} + +export function news() { + http.get('https://test.k6.io/news.php', { tags: { my_custom_tag: 'news' } }); +} +``` + +{{< /code >}} + +## Use different environment variables and tags per scenario. + +The previous example sets tags on individual HTTP request metrics. +But, you can also set tags per scenario, which applies them to other +[taggable](https://grafana.com/docs/k6//using-k6/tags-and-groups#tags) objects as well. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { fail } from 'k6'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'constant-vus', + exec: 'contacts', + vus: 50, + duration: '30s', + tags: { my_custom_tag: 'contacts' }, + env: { MYVAR: 'contacts' }, + }, + news: { + executor: 'per-vu-iterations', + exec: 'news', + vus: 50, + iterations: 100, + startTime: '30s', + maxDuration: '1m', + tags: { my_custom_tag: 'news' }, + env: { MYVAR: 'news' }, + }, + }, +}; + +export function contacts() { + if (__ENV.MYVAR != 'contacts') fail(); + http.get('https://test.k6.io/contacts.php'); +} + +export function news() { + if (__ENV.MYVAR != 'news') fail(); + http.get('https://test.k6.io/news.php'); +} +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +By default, k6 applies a `scenario` tag to all metrics in each scenario, whose value is the scenario name. +You can combine these tags with thresholds, or use them to simplify results filtering. + +To disable scenario tags, use the [`--system-tags` option](https://grafana.com/docs/k6//using-k6/k6-options#system-tags). + +{{% /admonition %}} + +## Run multiple scenario functions, with different thresholds + +You can also set different thresholds for different scenario functions. +To do this: + +1. Set scenario-specific tags +1. Set thresholds for these tags. + +This test has 3 scenarios, each with different `exec` functions, tags and environment variables, and thresholds: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + scenarios: { + my_web_test: { + // some arbitrary scenario name + executor: 'constant-vus', + vus: 50, + duration: '5m', + gracefulStop: '0s', // do not wait for iterations to finish in the end + tags: { test_type: 'website' }, // extra tags for the metrics generated by this scenario + exec: 'webtest', // the function this scenario will execute + }, + my_api_test_1: { + executor: 'constant-arrival-rate', + rate: 90, + timeUnit: '1m', // 90 iterations per minute, i.e. 1.5 RPS + duration: '5m', + preAllocatedVUs: 10, // the size of the VU (i.e. worker) pool for this scenario + tags: { test_type: 'api' }, // different extra metric tags for this scenario + env: { MY_CROC_ID: '1' }, // and we can specify extra environment variables as well! + exec: 'apitest', // this scenario is executing different code than the one above! + }, + my_api_test_2: { + executor: 'ramping-arrival-rate', + startTime: '30s', // the ramping API test starts a little later + startRate: 50, + timeUnit: '1s', // we start at 50 iterations per second + stages: [ + { target: 200, duration: '30s' }, // go from 50 to 200 iters/s in the first 30 seconds + { target: 200, duration: '3m30s' }, // hold at 200 iters/s for 3.5 minutes + { target: 0, duration: '30s' }, // ramp down back to 0 iters/s over the last 30 second + ], + preAllocatedVUs: 50, // how large the initial pool of VUs would be + maxVUs: 100, // if the preAllocatedVUs are not enough, we can initialize more + tags: { test_type: 'api' }, // different extra metric tags for this scenario + env: { MY_CROC_ID: '2' }, // same function, different environment variables + exec: 'apitest', // same function as the scenario above, but with different env vars + }, + }, + discardResponseBodies: true, + thresholds: { + // we can set different thresholds for the different scenarios because + // of the extra metric tags we set! + 'http_req_duration{test_type:api}': ['p(95)<250', 'p(99)<350'], + 'http_req_duration{test_type:website}': ['p(99)<500'], + // we can reference the scenario names as well + 'http_req_duration{scenario:my_api_test_2}': ['p(99)<300'], + }, +}; + +export function webtest() { + http.get('https://test.k6.io/contacts.php'); + sleep(Math.random() * 2); +} + +export function apitest() { + http.get(`https://test-api.k6.io/public/crocodiles/${__ENV.MY_CROC_ID}/`); + // no need for sleep() here, the iteration pacing will be controlled by the + // arrival-rate executors above! +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/scenarios/concepts/_index.md b/docs/sources/v0.50.x/using-k6/scenarios/concepts/_index.md new file mode 100644 index 000000000..ce2ce5c0f --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/concepts/_index.md @@ -0,0 +1,20 @@ +--- +title: 'Concepts' +description: High-level explanations about how your executor configuration can change the test execution and test results +weight: -10 +--- + +# Concepts + +These topics explain the essential concepts of how scenarios and their executors work. + +Different scenario configurations can affect many different aspects of your system, +including the generated load, utilized resources, and emitted metrics. +If you know a bit about how scenarios work, you'll design better tests and interpret test results with more understanding. + +| On this page | Read about | +| ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| [Open and closed models](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed) | Different ways k6 can schedule VUs, their affects on test results, and how k6 implements the open model in its arrival-rate executors | +| [Graceful Stop](https://grafana.com/docs/k6//using-k6/scenarios/concepts/graceful-stop) | A configurable period for iterations to finish or ramp down after the test reaches its scheduled duration | +| [Arrival-rate VU allocation](https://grafana.com/docs/k6//using-k6/scenarios/concepts/arrival-rate-vu-allocation) | How k6 allocates VUs in arrival-rate executors | +| [Dropped iterations](https://grafana.com/docs/k6//using-k6/scenarios/concepts/dropped-iterations) | Possible reasons k6 might drop a scheduled iteration | diff --git a/docs/sources/v0.50.x/using-k6/scenarios/concepts/arrival-rate-vu-allocation.md b/docs/sources/v0.50.x/using-k6/scenarios/concepts/arrival-rate-vu-allocation.md new file mode 100644 index 000000000..f6b167199 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/concepts/arrival-rate-vu-allocation.md @@ -0,0 +1,124 @@ +--- +title: Arrival-rate VU allocation +description: How k6 allocates VUs in the open-model, arrival-rate executors +weight: 20 +--- + +# Arrival-rate VU allocation + +In arrival-rate executors, as long as k6 has VUs available, it starts iterations according to your target rate. +The ability to set iteration rate comes with a bit more configuration complexity: you must pre-allocate a sufficient number of VUs. +In other words, before the tests runs, you must both: + +- Configure load (as new iterations per unit of time) +- Ensure that you've allocated enough VUs. + +Read on to learn about how k6 allocates VUs in the arrival-rate executors. + +{{% admonition type="caution" %}} + +In cloud tests, **`preAllocatedVUs` count against your subscription.** + +When planning a test, consider doing a trial initialization on a local machine to ensure you're allocating VUs efficiently. + +{{% /admonition %}} + +## Pre-allocation in arrival-rate executors + +As [open-model](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed#open-model) scenarios, arrival-rate executors start iterations according to a configured rate. +For example, you can configure arrival-rate executors to start 10 iterations each second, or minute, or hour. +This behavior is opposed to the closed-model scenarios, in which VUs wait for one iteration to finish before starting another + +Each iteration needs a VU to run it. +Because k6 VUs are single threaded, like other JavaScript runtimes, a VU can only run a single iteration (and its event loop) at a time. +To ensure you have enough, you must pre-allocate a sufficient number. + +In your arrival-rate configuration, three properties determine the iteration rate: + +- k6 starts `rate` number of iterations evenly across the `timeUnit` (default 1s). +- `preAllocatedVUs` sets the number of VUs to initialize to meet the target iteration rate. + +For example, with a `constant-arrival-rate` executor and `rate: 10`, k6 tries to start a new iteration every 100 milliseconds. If the scenario has `rate: 10, timeUnit: '1m'`, k6 tries to start a new iteration every 6 seconds. +Whether it _can_ start the target iteration rate depends on whether the number of allocated VUs is sufficient. + +```javascript +export const options = { + scenarios: { + constant_load: { + executor: 'constant-arrival-rate', + preAllocatedVUs: 10, + rate: 10, + timeUnit: '1m', + }, + }, +}; +``` + +In practice, determining the right number of VUs to be able to reach the target iteration rate might take some trial and error, +as the necessary VUs entirely depends on what your iteration code looks like and how quickly the system under test can process requests. + +## How k6 uses allocated VUs + +Before an arrival-rate scenario starts, k6 first initializes the number of `preAllocatedVUs`. +When the test runs, +the number of available `preAllocatedVUs` determines how many iterations k6 can start. +k6 tries to reach the target iterations per second, +and one of two things can happen: + +| If the executor | Then.. | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Has enough VUs | the extra VUs are "idle," ready to be used when needed. | +| Has insufficient VUs. | k6 emits a [`dropped_iterations` metric](https://grafana.com/docs/k6//using-k6/scenarios/concepts/dropped-iterations) for each iteration that it can't run. | + +## Iteration duration affects the necessary allocation + +The necessary allocation depends on the iteration duration: +longer durations need more VUs. + +In a perfect world, you could estimate the number of pre-allocated VUs with this formula: + +``` +preAllocatedVUs = [median_iteration_duration * rate] + constant_for_variance +``` + +In the real world, if you know _exactly_ how long an iteration takes, you likely don't need to run a test. +What's more, as the test goes on, iteration duration likely increases. +If response times slow so much that k6 lacks the VUs to start iterations at the desired rate, +the allocation might be insufficient and k6 will drop iterations. + +To determine your strategy, you can run tests locally and gradually add more pre-allocated VUs. +As dropped iterations can also indicate that the system performance is degrading, this early experimentation can provide useful data on its own. + +## You probably don't need `maxVUs` + +{{% admonition type="caution" %}} + +In cloud tests, the number of `maxVUs` counts against your subscription, +**overriding the number set by `preAllocatedVUs`**. + +{{% /admonition %}} + +The arrival-rate executors also have a `maxVUs` property. +If you set it, k6 runs in this sequence: + +1. Pre-allocate the `preAllocatedVUs`. +1. Run the test, trying to reach the target iteration rate. +1. If the target exceeds the available VUs, allocate another VU. +1. If the target still exceeds available VUs, continue allocating VUs until reaching the number set by `maxVUs`. + +Though it seems convenient, you should avoid using `maxVUs` in most cases. +Allocating VUs has CPU and memory costs, and allocating VUs as the test runs **can overload the load generator and skew results**. +In Cloud tests, the number of `maxVUs` count against your subscription. +This is because k6 must allocate sufficient resources for `maxVUs` to be initialized, even if they never are. +In almost all cases, the best thing to do is to pre-allocate the number of VUs you need beforehand. + +Some of the times it might make sense to use `maxVUs` include: + +- To determine necessary allocation in first-time tests +- To add a little "cushion" to the pre-allocated VUs that you expect the test needs +- In huge, highly distributed tests, in which you need to carefully scale load generators as you increment VUs. + +## You can't guarantee which VU runs an iteration + +As with all executors, you can't predict the specific VU that an arrival-rate executor uses for a specific iteration. +As the test runs on, the executor might use some or all of the allocated VUs, even if it never needs the entire allocated number to reach the iteration rate. diff --git a/docs/sources/v0.50.x/using-k6/scenarios/concepts/dropped-iterations.md b/docs/sources/v0.50.x/using-k6/scenarios/concepts/dropped-iterations.md new file mode 100644 index 000000000..66e80376c --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/concepts/dropped-iterations.md @@ -0,0 +1,44 @@ +--- +title: Dropped iterations +description: Explanations about how your scenario configuration or SUT performance can lead to dropped iterations +weight: 30 +--- + +# Dropped iterations + +Sometimes, a scenario can't run the expected number of iterations. +k6 tracks the number of unsent iterations in a counter metric, `dropped_iterations`. +The number of dropped iterations can be valuable data when you debug executors or analyze results. + +Dropped iterations usually happen for one of two reasons: + +- The executor configuration is insufficient. +- The SUT can't handle the configured VU arrival rate. + +### Configuration-related iteration drops + +Dropped iterations happen for different reasons in different types of executors. + +With `shared-iterations` and `per-vu-iterations`, iterations drop if the scenario reaches its `maxDuration` before all iterations finish. +To mitigate this, you likely need to increase the value of the duration. + +With `constant-arrival-rate` and `ramping-arrival-rate`, iterations drop if there are no free VUs. +**If it happens at the beginning of the test, you likely just need to allocate more VUs.** +If this happens later in the test, the dropped iterations might happen because SUT performance is degrading and iterations are taking longer to finish. + +### SUT-related iteration drops + +At a certain point of high latency or longer iteration durations, k6 will no longer have free VUs to start iterations with at the configured rate. +As a result, the executor will drop iterations. + +The reasons for these dropped iterations vary: + +- The SUT response has become so long that k6 starts dropping scheduled iterations from the queue. +- The SUT iteration duration has become so long that k6 needs to schedule more VUs to reach the target arrival rate, exceeding the number of scheduled iterations. + +As the causes vary, dropped iterations might mean different things. +A few dropped iterations might indicate a quick network error. +Many dropped iterations might indicate that your SUT has completely stopped responding. + +When you design your test, consider what an acceptable rate of dropped iterations is (the _error budget_). +To assert that the SUT responds within this error budget, you can use the `dropped_iterations` metric in a [Threshold](https://grafana.com/docs/k6//using-k6/thresholds). diff --git a/docs/sources/v0.50.x/using-k6/scenarios/concepts/graceful-stop.md b/docs/sources/v0.50.x/using-k6/scenarios/concepts/graceful-stop.md new file mode 100644 index 000000000..b828ac939 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/concepts/graceful-stop.md @@ -0,0 +1,97 @@ +--- +title: 'Graceful stop' +description: 'This option is available for all executors except externally-controlled and allows the user to specify a duration to wait before forcefully interrupting them.' +weight: 10 +--- + +# Graceful stop + +The `gracefulStop` is a period at the end of the test in which k6 lets iterations in progress finish. + +If a test has a set duration or ramp down, its possible that k6 could interrupt iterations in progress. +These interruptions can lead to skewed metrics and unexpected test results. +To deal with this, k6 scenarios have a `gracefulStop`. +For the `ramping-vus` executor, a related option, `gracefulRampDown`, exists to let VUs finish as their number ramps down. + +## Graceful stop + +The `gracefulStop` option is available for all executors except `externally-controlled`. +It specifies a duration that k6 will wait before forcefully interrupting an iteration. +The default value is `30s`. + +### Graceful stop example + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'constant-vus', + vus: 100, + duration: '10s', + gracefulStop: '3s', + }, + }, +}; + +export default function () { + const delay = Math.floor(Math.random() * 5) + 1; + http.get(`https://httpbin.test.k6.io/delay/${delay}`); +} +``` + +{{< /code >}} + +Running this script would result in something like: + +```bash +running (13.0s), 000/100 VUs, 349 complete and 23 interrupted iterations +contacts ✓ [======================================] 100 VUs 10s +``` + +Notice that even though the total test duration is 10s, the actual execution time was 13s +because of `gracefulStop`, giving the VUs a 3s additional time to complete iterations in progress. 23 +of the iterations currently in progress did not complete within this window and was therefore interrupted. + +## The `gracefulRampDown` + +In addition to `gracefulStop`, the [ramping-vus](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus) executor also has a `gracefulRampDown`. + +When the target value for a stage is lower than the target for the previous stage, k6 might need to stop some VUs that were started during the previous stages. +The `gracefulRampDown` option controls how long these VUs have to finish currently before k6 interrupts them. + +To get an idea of how `gracefulRampDown` works, you can run the following script with +`k6 run --verbose`. +In this script, the iteration `sleep` time exceeds the `gracefulRampdown` time. +So, as k6 ramps down to reach the target of the second stage, it must forcibly interrupt VUs. +The `--verbose` flag will log to your console when VUs start, enter the grace period, and are forcibly interrupted. + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + discardresponsebodies: true, + scenarios: { + contacts: { + executor: 'ramping-vus', + startvus: 0, + stages: [ + { duration: '10s', target: 10 }, + { duration: '10s', target: 0 }, + ], + gracefulRampDown: '1s', + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); + // adding sleep beyond so that iterations are longer than rampdown + sleep(5); +} +``` diff --git a/docs/sources/v0.50.x/using-k6/scenarios/concepts/open-vs-closed.md b/docs/sources/v0.50.x/using-k6/scenarios/concepts/open-vs-closed.md new file mode 100644 index 000000000..1fc96de47 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/concepts/open-vs-closed.md @@ -0,0 +1,127 @@ +--- +title: 'Open and closed models' +description: 'k6 has two ways to schedule VUs, which can affect test results. k6 implements the open model in its arrival-rate executors.' +weight: 00 +--- + +# Open and closed models + +Different k6 executors have different ways of scheduling VUs. +Some executors use the _closed model_, while the arrival-rate executors use the _open model_. + +In short, in the closed model, VU iterations start only when the last iteration finishes. +In the open model, on the other hand, VUs arrive independently of iteration completion. +Different models suit different test aims. + +## Closed Model + +In a closed model, the execution time of each iteration dictates the +number of iterations executed in your test. +The next iteration doesn't start until the previous one finishes. + +Thus, in a closed model, the start or arrival rate of +new VU iterations is tightly coupled with the iteration duration (that is, time from start +to finish of the VU's `exec` function, by default the `export default function`): + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + scenarios: { + closed_model: { + executor: 'constant-vus', + vus: 1, + duration: '1m', + }, + }, +}; + +export default function () { + // The following request will take roughly 6s to complete, + // resulting in an iteration duration of 6s. + + // As a result, New VU iterations will start at a rate of 1 per 6s, + // and we can expect to get 10 iterations completed in total for the + // full 1m test duration. + + http.get('https://httpbin.test.k6.io/delay/6'); +} +``` + +{{< /code >}} + +Running this script would result in something like: + +```bash + +running (1m01.5s), 0/1 VUs, 10 complete and 0 interrupted iterations +closed_model ✓ [======================================] 1 VUs 1m0s + +``` + +### Drawbacks of using the closed model + +When the duration of the VU iteration is tightly coupled to the start of new VU iterations, +the target system's response time can influence the throughput of the test. +Slower response times means longer iterations and a lower arrival rate of new iterations―and vice versa for faster response times. +In some testing literature, this problem is known as _coordinated omission._ + +In other words, when the target system is stressed and starts to respond more +slowly, a closed model load test will wait, resulting in increased +iteration durations and a tapering off of the arrival rate of new VU iterations. + +This effect is not ideal when the goal is to simulate a certain arrival rate of new VUs, +or more generally throughput (e.g. requests per second). + +## Open model + +Compared to the closed model, the open model decouples VU iterations from +the iteration duration. +The response times of the target system no longer +influence the load on the target system. + +To fix this problem of coordination, you can use an open model, +which decouples the start of new VU iterations from the iteration duration. +This reduces the influence of the target system's response time. + +![Arrival rate closed/open models](/media/docs/k6-oss/arrival-rate-open-closed-model.png) + +k6 implements the open model with two _arrival rate_ executors: +[constant-arrival-rate](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-arrival-rate) and [ramping-arrival-rate](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-arrival-rate): + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + scenarios: { + open_model: { + executor: 'constant-arrival-rate', + rate: 1, + timeUnit: '1s', + duration: '1m', + preAllocatedVUs: 20, + }, + }, +}; + +export default function () { + // With the open model arrival rate executor config above, + // new VU iterations will start at a rate of 1 every second, + // and we can thus expect to get 60 iterations completed + // for the full 1m test duration. + http.get('https://httpbin.test.k6.io/delay/6'); +} +``` + +{{< /code >}} + +Running this script would result in something like: + +```bash +running (1m09.3s), 000/011 VUs, 60 complete and 0 interrupted iterations +open_model ✓ [======================================] 011/011 VUs 1m0s 1 iters/s +``` diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/_index.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/_index.md new file mode 100644 index 000000000..443ea6fb2 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/_index.md @@ -0,0 +1,50 @@ +--- +title: 'Executors' +description: 'Executors control how k6 schedules VUs and iterations. Choose the executor to model traffic you want to model to test your services' +weight: 01 +--- + +# Executors + +**Executors** control how k6 schedules VUs and iterations. +The executor that you choose depends on the goals of your test and the type of traffic you want to model. + +Define the executor in `executor` key of the scenario object. +The value is the executor name separated by hyphens. + +```javascript +export const options = { + scenarios: { + arbitrary_scenario_name: { + //Name of executor + executor: 'ramping-vus', + // more configuration here + }, + }, +}; +``` + +## All executors + +The following table lists all k6 executors and links to their documentation. + +| Name | Value | Description | +| -------------------------------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [Shared iterations](https://grafana.com/docs/k6//using-k6/scenarios/executors/shared-iterations) | `shared-iterations` | A fixed amount of iterations are
shared between a number of VUs. | +| [Per VU iterations](https://grafana.com/docs/k6//using-k6/scenarios/executors/per-vu-iterations) | `per-vu-iterations` | Each VU executes an exact number of iterations. | +| [Constant VUs](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-vus) | `constant-vus` | A fixed number of VUs execute as many
iterations as possible for a specified amount of time. | +| [Ramping VUs](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus) | `ramping-vus` | A variable number of VUs execute as many
iterations as possible for a specified amount of time. | +| [Constant Arrival Rate](https://grafana.com/docs/k6//using-k6/scenarios/executors/constant-arrival-rate) | `constant-arrival-rate` | A fixed number of iterations are executed
in a specified period of time. | +| [Ramping Arrival Rate](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-arrival-rate) | `ramping-arrival-rate` | A variable number of iterations are
executed in a specified period of time. | +| [Externally Controlled](https://grafana.com/docs/k6//using-k6/scenarios/executors/externally-controlled) | `externally-controlled` | Control and scale execution at runtime
via [k6's REST API](https://grafana.com/docs/k6//misc/k6-rest-api) or the [CLI](https://k6.io/blog/how-to-control-a-live-k6-test). | + +{{% admonition type="note" %}} + +For any given scenario, you can't guarantee that a specific VU can run a specific iteration. + +With [`SharedArray`](https://grafana.com/docs/k6//javascript-api/k6-data/sharedarray) and [execution context variables](https://grafana.com/docs/k6//using-k6/execution-context-variables), you can map a specific VU to a specific value in your test data. +So the tenth VU could use the tenth item in your array (or the sixth iteration to the sixth item). + +But, you _cannot_ reliably map, for example, the tenth VU to the tenth iteration. + +{{% /admonition %}} diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/constant-arrival-rate.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/constant-arrival-rate.md new file mode 100644 index 000000000..de4b2e8c0 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/constant-arrival-rate.md @@ -0,0 +1,108 @@ +--- +title: 'Constant arrival rate' +description: 'A fixed number of iterations are started in a specified period of time.' +weight: 05 +--- + +# Constant arrival rate + +With the `constant-arrival-rate` executor, k6 starts a fixed number of iterations over a specified period of time. +It is an open-model executor, meaning iterations start independently of system response (for details, read +[Open and Closed models](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed)). + +This executor continues to start iterations at the given rate as long as VUs are available. +The time to execute an iteration can vary with test logic or the system-under-test response time. +To compensate for this, the executor starts a varied number of VUs to meet the configured iteration rate. +For explanations of how allocation works, read [Arrival-rate VU allocation](https://grafana.com/docs/k6//using-k6/scenarios/concepts/arrival-rate-vu-allocation). + +{{% admonition type="note" %}} + +**Iteration starts are spaced fractionally.** +Iterations **do not** start at exactly the same time. +At a `rate` of `10` with a `timeUnit` of `1s`, each iteration starts about every tenth of a second (that is, each 100ms). + +{{% /admonition %}} + +## Options + +Besides the [common configuration options](https://grafana.com/docs/k6//using-k6/scenarios#options), +this executor has the following options: + +| Option | Type | Description | Default | +| ------------------------------------ | ------- | ------------------------------------------------------------------------------ | ----------------------------------- | +| duration(required) | string | Total scenario duration (excluding `gracefulStop`). | - | +| rate(required) | integer | Number of iterations to start during each `timeUnit` period. | - | +| preAllocatedVUs(required) | integer | Number of VUs to pre-allocate before test start to preserve runtime resources. | - | +| timeUnit | string | Period of time to apply the `rate` value. | `"1s"` | +| maxVUs | integer | Maximum number of VUs to allow during the test run. | If unset, same as `preAllocatedVUs` | + +## When to use + +When you want iterations to remain constant, independent of the performance of the system under test. +This approach is useful for a more accurate representation of RPS, for example. + +{{% admonition type="note" %}} + +**Don't put sleep at the end of an iteration.** + +The arrival-rate executors already pace the iteration rate through the `rate` and `timeUnit` properties. +So it's unnecessary to use a `sleep()` function at the end of the VU code. + +{{% /admonition %}} + +## Example + +This example schedules a constant rate of 30 iterations per second for 30 seconds. +It pre-allocates 2 VUs, and allows k6 to dynamically schedule up to 50 VUs as needed. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'constant-arrival-rate', + + // How long the test lasts + duration: '30s', + + // How many iterations per timeUnit + rate: 30, + + // Start `rate` iterations per second + timeUnit: '1s', + + // Pre-allocate 2 VUs before starting the test + preAllocatedVUs: 2, + + // Spin up a maximum of 50 VUs to sustain the defined + // constant arrival rate. + maxVUs: 50, + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); +} +``` + +{{< /code >}} + +## Observations + +The following graph depicts the performance of the [example](#example) script: + +![Ramping VUs](/media/docs/k6-oss/constant-arrival-rate.png) + +Based upon our test scenario inputs and results: + +- The desired rate of 30 iterations started every 1 second is achieved and maintained for the majority of the test. +- The test scenario runs for the specified 30 second duration. +- Having started with 2 VUs (as specified by the `preAllocatedVUs` option), k6 automatically adjusts the number of VUs to achieve the desired rate, up to the `maxVUs`. For this test, this ended up as 17 VUs. +- The number of VUs to achieve the desired rate varies depending on how long each iteration takes to execute. For this test definition, if it would take exactly 1 second, then 30 VUs would be needed. However, as it takes less than 1 second, then less VUs are needed. + +> Using too low of a `preAllocatedVUs` setting will reduce the test duration at the desired rate, as resources need to continually be allocated to achieve the rate. diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/constant-vus.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/constant-vus.md new file mode 100644 index 000000000..fa84a2aae --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/constant-vus.md @@ -0,0 +1,71 @@ +--- +title: 'Constant VUs' +description: 'A fixed number of VUs execute as many iterations as possible for a specified amount of time.' +weight: 03 +--- + +# Constant VUs + +With the `constant-vus` executor, a fixed number of VUs execute as many iterations as possible for a specified amount of time. + +For a shortcut to this executor, use the [VUs](https://grafana.com/docs/k6//using-k6/k6-options/reference#vus) and [duration](https://grafana.com/docs/k6//using-k6/k6-options/reference#duration) options. + +## Options + +Besides the [common configuration options](https://grafana.com/docs/k6//using-k6/scenarios#options), +this executor has the following options: + +| Option | Type | Description | Default | +| ----------------------------- | ------- | --------------------------------------------------- | ------- | +| duration(required) | string | Total scenario duration (excluding `gracefulStop`). | - | +| vus | integer | Number of VUs to run concurrently. | `1` | + +## When to use + +Use this executor if you need a specific number of VUs to run for a certain amount of time. + +## Example + +This examples schedules 10 VUs to run constantly for a duration 30 seconds. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'constant-vus', + vus: 10, + duration: '30s', + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); + // Injecting sleep + // Total iteration time is sleep + time to finish request. + sleep(0.5); +} +``` + +{{< /code >}} + +## Observations + +The following graph depicts the performance of the [example](#example) script: + +![Constant VUs](/media/docs/k6-oss/constant-vus.png) + +Based upon our test scenario inputs and results: + +- The number of VUs is fixed at 10, and are initialized before the test begins; +- Overall test duration is fixed at the configured 30 second duration; +- Each _iteration_ of the `default` function is expected to be roughly 515ms, or ~2/s; +- Maximum throughput (highest efficiency) is therefore expected to be ~20 iters/s, `2 iters/s * 10 VUs`; +- We see that the maximum throughput is reached and maintained for the majority of the test; +- Approximately 600 iterations are therefore performed in total, `30 seconds * 20 iters/s`. diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/externally-controlled.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/externally-controlled.md new file mode 100644 index 000000000..4299f1f0d --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/externally-controlled.md @@ -0,0 +1,68 @@ +--- +title: 'Externally controlled' +description: 'Control and scale execution at runtime via k6 REST API or the CLI.' +weight: 07 +--- + +# Externally controlled + +Control and scale execution at runtime via [k6's REST API](https://grafana.com/docs/k6//misc/k6-rest-api) or +the [CLI](https://k6.io/blog/how-to-control-a-live-k6-test). + +Previously, the `pause`, `resume`, and `scale` CLI commands were used to globally control +k6 execution. This executor does the same job by providing a better API that can be used to +control k6 execution at runtime. + +Note that, passing arguments to the `scale` CLI command for changing the amount of active or +maximum VUs will only affect the externally controlled executor. + +## Options + +**The `externally-controlled` executor has no graceful stop**. + +Besides that, this executor has all the [common configuration options](https://grafana.com/docs/k6//using-k6/scenarios#options), +and these particular ones: + +| Option | Type | Description | Default | +| ----------------------------- | ------- | --------------------------------------------------- | ------- | +| duration(required) | string | Total test duration. | - | +| vus | integer | Number of VUs to run concurrently. | - | +| maxVUs | integer | Maximum number of VUs to allow during the test run. | - | + +## When to use + +If you want to control the number of VUs while the test is running. + +Important: this is the only executor that is not supported in `k6 cloud`, it can only be used +locally with `k6 run`. + +## Example + +In this example, we'll execute a test controllable at runtime, starting with 10 VUs up to +a maximum of 50, and a total duration of 10 minutes. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'externally-controlled', + vus: 10, + maxVUs: 50, + duration: '10m', + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); +} +``` + +Once the test has started, it can be externally controlled with the `pause`, `resume`, and `scale` CLI commands. + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/per-vu-iterations.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/per-vu-iterations.md new file mode 100644 index 000000000..21ad9c8ef --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/per-vu-iterations.md @@ -0,0 +1,77 @@ +--- +title: 'Per VU iterations' +description: 'Each VU executes an exact number of iterations.' +weight: 02 +--- + +# Per VU iterations + +With the `per-vu-iterations` executor, each VU executes an exact number of iterations. +The total number of completed iterations equals `vus * iterations`. + +## Options + +Besides the [common configuration options](https://grafana.com/docs/k6//using-k6/scenarios#options), +this executor has the following options: + +| Option | Type | Description | Default | +| ----------- | ------- | ---------------------------------------------------------------------------------- | ------- | +| vus | integer | Number of VUs to run concurrently. | `1` | +| iterations | integer | Number of `exec` function iterations to be executed by each VU. | `1` | +| maxDuration | string | Maximum scenario duration before it's forcibly stopped (excluding `gracefulStop`). | `"10m"` | + +## When to use + +Use this executor if you need a specific number of VUs to complete the same number of +iterations. This can be useful when you have fixed sets of test data that you want to +partition between VUs. + +## Example + +The following example schedules 10 VUs to execute 20 iterations _each_. +The test runs 200 total iterations and has a maximum duration of 30 seconds. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'per-vu-iterations', + vus: 10, + iterations: 20, + maxDuration: '30s', + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); + // Injecting sleep + // Sleep time is 500ms. Total iteration time is sleep + time to finish request. + sleep(0.5); +} +``` + +{{< /code >}} + +## Observations + +The following graph depicts the performance of the [example](#example) script: + +![Per VU Iterations](/media/docs/k6-oss/per-vu-iterations.png) + +Based upon our test scenario inputs and results: + +- The number of VUs is fixed at 10, and are initialized before the test begins; +- Total iterations are fixed at 20 iterations per VU, i.e. 200 iterations, `10 VUs * 20 iters each`; +- Each _iteration_ of the `default` function is expected to be roughly 515ms, or ~2/s; +- Maximum throughput (highest efficiency) is therefore expected to be ~20 iters/s, `2 iters/s * 10 VUs`; +- The maximum throughput is reached, but not maintained; +- Because the distribution of iterations is even among VUs, a _fast_ VU may finish early and be idle for the remainder of the test, thereby lowering _efficiency_; +- Total duration of 9 seconds is slightly longer than [shared iterations](https://grafana.com/docs/k6//using-k6/scenarios/executors/shared-iterations) due to lower efficiency; +- Overall test duration lasts as long as the _slowest_ VU takes to complete 20 requests. diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/ramping-arrival-rate.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/ramping-arrival-rate.md new file mode 100644 index 000000000..9725aa072 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/ramping-arrival-rate.md @@ -0,0 +1,144 @@ +--- +title: 'Ramping arrival rate' +description: 'A variable number of iterations are started in a specified period of time.' +weight: 06 +--- + +# Ramping arrival rate + +With the `ramping-arrival-rate` executor, k6 starts iterations at a variable rate. +It is an open-model executor, meaning iterations start independently of system response (for details, read +[Open and Closed models](https://grafana.com/docs/k6//using-k6/scenarios/concepts/open-vs-closed)). + +This executor has _stages_ that configure target number of iterations and the time k6 takes to reach or stay at this target. +Unlike the [ramping VUs executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/ramping-vus), which configures VUs, +this executor dynamically changes the number of iterations to start, and starts these iterations as long as the test has enough allocated VUs. +To learn how allocation works, read [Arrival-rate VU allocation](https://grafana.com/docs/k6//using-k6/scenarios/concepts/arrival-rate-vu-allocation). + +{{% admonition type="note" %}} + +**Iteration starts are spaced fractionally.** +Iterations **do not** start at exactly the same time. +At a `rate` of `10` with a `timeUnit` of `1s`, each iteration starts about every tenth of a second (that is, each 100ms). + +{{% /admonition %}} + +## Options + +Besides the [common configuration options](https://grafana.com/docs/k6//using-k6/scenarios#options), +this executor has the following options: + +| Option | Type | Description | Default | +| ------------------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | +| stages(required) | array | Array of objects that specify the target number of iterations to ramp up or down to. | `[]` | +| preAllocatedVUs(required) | integer | Number of VUs to pre-allocate before test start to preserve runtime resources. | - | +| startRate | integer | Number of iterations to execute each `timeUnit` period at test start. | `0` | +| timeUnit | string | Period of time to apply the `startRate` to the `stages`' `target` value. Its value is constant for the whole duration of the scenario, it is not possible to change it for a specific stage. | `"1s"` | +| maxVUs | integer | Maximum number of VUs to allow during the test run. | If unset, same as `preAllocatedVUs` | + +## When to use + +If you need start iterations independent of system-under-test performance, and +want to ramp the number of iterations up or down during specific periods of time. + +{{% admonition type="note" %}} + +**Don't put sleep at the end of an iteration.** +The arrival-rate executors already pace the iteration rate through the `rate` and `timeUnit` properties. +It's unnecessary to use a `sleep()` function at the end of the VU code. + +{{% /admonition %}} + +## Example + +This is an example of a four-stage test. + +It starts at the defined `startRate`, 300 iterations per minute over a one minute period. +After one minute, the iteration rate ramps to 600 iterations started per minute over the next two minutes, and stays at this rate for four more minutes. +In the last two minutes, it ramps down to a target of 60 iterations per minute. + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + discardResponseBodies: true, + + scenarios: { + contacts: { + executor: 'ramping-arrival-rate', + + // Start iterations per `timeUnit` + startRate: 300, + + // Start `startRate` iterations per minute + timeUnit: '1m', + + // Pre-allocate necessary VUs. + preAllocatedVUs: 50, + + stages: [ + // Start 300 iterations per `timeUnit` for the first minute. + { target: 300, duration: '1m' }, + + // Linearly ramp-up to starting 600 iterations per `timeUnit` over the following two minutes. + { target: 600, duration: '2m' }, + + // Continue starting 600 iterations per `timeUnit` for the following four minutes. + { target: 600, duration: '4m' }, + + // Linearly ramp-down to starting 60 iterations per `timeUnit` over the last two minutes. + { target: 60, duration: '2m' }, + ], + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); +} +``` + +{{< /code >}} + +## Observations + +The following graph depicts the performance of the [example](#example) script: + +![Ramping Arrival Rate](/media/docs/k6-oss/ramping-arrival-rate.png) + +Based upon our test scenario inputs and results: + +- The configuration defines 4 stages for a total test duration of 9 minutes. +- Stage 1 maintains the `startRate` iteration rate at 300 iterations started per minute for 1 minute. +- Stage 2 ramps _up_ the iteration rate linearly from the _stage 1_ of 300 iterations started per minute, to the target of 600 iterations started per minute over a 2-minute duration. +- Stage 3 maintains the _stage 2_ iteration rate at 600 iterations started per minute over a 4-minute duration. +- Stage 4 ramps _down_ the iteration rate linearly to the target rate of 60 iterations started per minute over the last two minutes duration. +- Changes to the iteration rate are performed by k6, adjusting the number of VUs +- The script waits for a period of time (defined by the `gracefulStop` option) for iterations to finish. It doesn't start new iterations during the `gracefulStop` period. +- Our example performed, 4020 iterations over the course of the test. + +## Get the stage index + +To get the current running stage index, use the `getCurrentStageIndex` helper function from the [k6-jslib-utils](https://grafana.com/docs/k6//javascript-api/jslib/utils) library. It returns a zero-based number equal to the position in the shortcut `stages` array or in the executor's `stages` array. + +```javascript +import { getCurrentStageIndex } from 'https://jslib.k6.io/k6-utils/1.3.0/index.js'; + +export const options = { + stages: [ + { target: 10, duration: '30s' }, + { target: 50, duration: '1m' }, + { target: 10, duration: '30s' }, + ], +}; + +export default function () { + if (getCurrentStageIndex() === 1) { + console.log('Running the second stage where the expected target is 50'); + } +} +``` + +Using this feature, it is possible to automatically tag using the current running stage. Check the [Tagging stages](https://grafana.com/docs/k6//using-k6/tags-and-groups#tagging-stages) section for more details. diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/ramping-vus.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/ramping-vus.md new file mode 100644 index 000000000..76f0fee82 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/ramping-vus.md @@ -0,0 +1,109 @@ +--- +title: 'Ramping VUs' +description: 'A variable number of VUs execute as many iterations as possible for a specified amount of time.' +weight: 04 +--- + +# Ramping VUs + +With the `ramping-vus` executor, a variable number of VUs executes as many iterations as possible for a specified amount of time. + +For a shortcut to this executor, use the [stages](https://grafana.com/docs/k6//using-k6/k6-options#stages) option. + +## Options + +Besides the [common configuration options](https://grafana.com/docs/k6//using-k6/scenarios#options), +this executor has the following options: + +| Option | Type | Description | Default | +| --------------------------- | ------- | ---------------------------------------------------------------------------------------------- | ------- | +| stages(required) | array | Array of objects that specify the target number of VUs to ramp up or down to. | `[]` | +| startVUs | integer | Number of VUs to run at test start. | `1` | +| gracefulRampDown | string | Time to wait for an already started iteration to finish before stopping it during a ramp down. | `"30s"` | + +## When to use + +This executor is a good fit if you need VUs to ramp up or down during specific periods +of time. + +## Example + +This example schedules a two-stage test, ramping up from 0 to 10 VUs over 20 seconds, then down +to 0 VUs over 10 seconds. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'ramping-vus', + startVUs: 0, + stages: [ + { duration: '20s', target: 10 }, + { duration: '10s', target: 0 }, + ], + gracefulRampDown: '0s', + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); + // Injecting sleep + // Sleep time is 500ms. Total iteration time is sleep + time to finish request. + sleep(0.5); +} +``` + +{{< /code >}} + +{{% admonition type="note" %}} + +With [`gracefulRampDown`](https://grafana.com/docs/k6//using-k6/scenarios/concepts/graceful-stop#the-gracefulrampdown) set to 0 seconds, some iterations might be +interrupted during the ramp down stage. + +{{% /admonition %}} + +## Observations + +The following graph depicts the performance of the [example](#example) script: + +![Ramping VUs](/media/docs/k6-oss/ramping-vus.png) + +Based upon our test scenario inputs and results: + +- The configuration defines 2 stages for a total test duration of 30 seconds; +- Stage 1 ramps _up_ VUs linearly from the `startVUs` of 0 to the target of 10 over a 20 second duration; +- From the 10 VUs at the end of stage 1, stage 2 then ramps _down_ VUs linearly to the target of 0 over a 10 second duration; +- Each _iteration_ of the `default` function is expected to be roughly 515ms, or ~2/s; +- As the number of VUs changes, the iteration rate directly correlates; each addition of a VU increases the rate by about 2 iters/s, whereas each subtraction of a VU reduces by about 2 iters/s; +- The example performed ~300 iterations over the course of the test. + +## Get the stage index + +To get the current running stage index, use the `getCurrentStageIndex` helper function from the [k6-jslib-utils](https://grafana.com/docs/k6//javascript-api/jslib/utils) library. It returns a zero-based number equal to the position in the shortcut `stages` array or in the executor's `stages` array. + +```javascript +import { getCurrentStageIndex } from 'https://jslib.k6.io/k6-utils/1.3.0/index.js'; + +export const options = { + stages: [ + { target: 10, duration: '30s' }, + { target: 50, duration: '1m' }, + { target: 10, duration: '30s' }, + ], +}; + +export default function () { + if (getCurrentStageIndex() === 1) { + console.log('Running the second stage where the expected target is 50'); + } +} +``` + +Using this feature, it is possible to automatically tag using the current running stage. Check the [Tagging stages](https://grafana.com/docs/k6//using-k6/tags-and-groups#tagging-stages) section for more details. diff --git a/docs/sources/v0.50.x/using-k6/scenarios/executors/shared-iterations.md b/docs/sources/v0.50.x/using-k6/scenarios/executors/shared-iterations.md new file mode 100644 index 000000000..4ea66f6b0 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/scenarios/executors/shared-iterations.md @@ -0,0 +1,89 @@ +--- +title: 'Shared iterations' +description: 'A fixed number of iterations are "shared" between a number of VUs, and the test ends once all iterations are executed.' +weight: 01 +--- + +# Shared iterations + +The `shared-iterations` executor shares iterations between the number of VUs. +The test ends once k6 executes all iterations. + +For a shortcut to this executor, use the [`vus`](https://grafana.com/docs/k6//using-k6/k6-options/reference#vus) and [`iterations`](https://grafana.com/docs/k6//using-k6/k6-options/reference#iterations) options. + +{{% admonition type="note" %}} + +Iterations **are not guaranteed to be evenly distributed** with this executor. +VU that executes faster will complete more iterations than slower VUs. + +To guarantee that every VU completes a specific, fixed number of iterations, [use the per-VU iterations executor](https://grafana.com/docs/k6//using-k6/scenarios/executors/per-vu-iterations). + +{{% /admonition %}} + +## Options + +Besides the [common configuration options](https://grafana.com/docs/k6//using-k6/scenarios#options), +this executor has the following options: + +| Option | Type | Description | Default | +| ----------- | ------- | ---------------------------------------------------------------------------------- | ------- | +| vus | integer | Number of VUs to run concurrently. | `1` | +| iterations | integer | Total number of script iterations to execute across all VUs. | `1` | +| maxDuration | string | Maximum scenario duration before it's forcibly stopped (excluding `gracefulStop`). | `"10m"` | + +## When to use + +This executor is suitable when you want a specific number of VUs to complete a fixed +number of total iterations, and the amount of iterations per VU is unimportant. +If the **time to complete** a number of test iterations is your concern, this executor should perform best. + +An example use case is for quick performance tests in the developement build cycle. +As developers make changes, they might run the test against the local code to test for performance regressions. +Thus the executor works well with a _shift-left_ policy, where emphasizes testing performance early in the development cycle, when the cost of a fix is lowest. + +## Example + +The following example schedules 200 total iterations shared by 10 VUs with a maximum test duration of 30 seconds. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + discardResponseBodies: true, + scenarios: { + contacts: { + executor: 'shared-iterations', + vus: 10, + iterations: 200, + maxDuration: '30s', + }, + }, +}; + +export default function () { + http.get('https://test.k6.io/contacts.php'); + // Injecting sleep + // Sleep time is 500ms. Total iteration time is sleep + time to finish request. + sleep(0.5); +} +``` + +{{< /code >}} + +## Observations + +The following graph depicts the performance of the [example](#example) script: + +![Shared Iterations](/media/docs/k6-oss/shared-iterations.png) + +Based upon our test scenario inputs and results: + +- Test is limited to a fixed number of 200 iterations of the `default` function; +- The number of VUs is fixed to 10, and are initialized before the test begins; +- Each _iteration_ of the `default` function is expected to be roughly 515ms, or ~2/s; +- Maximum throughput (highest efficiency) is therefore expected to be ~20 iters/s, `2 iters/s * 10 VUs`; +- The maximum throughput is maintained for a larger portion of the test; +- The distribution of iterations may be skewed: one VU may have performed 50 iterations, another only 10. diff --git a/docs/sources/v0.50.x/using-k6/tags-and-groups.md b/docs/sources/v0.50.x/using-k6/tags-and-groups.md new file mode 100644 index 000000000..77f3d96c5 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/tags-and-groups.md @@ -0,0 +1,341 @@ +--- +title: 'Tags and Groups' +description: 'k6 provides the Tags and Groups APIs to help you during the analysis and easily visualize, sort and +filter your test results.' +weight: 08 +--- + +# Tags and Groups + +A load test usually targets a service with different subsystems and resources. +This can make it hard to pinpoint the issues that are degrading performance. + +To help you visualize, sort, and filter your test results, k6 adds the following to your results. + +- _Tags_ categorize your checks, thresholds, custom metrics, and requests for in-depth filtering. +- _Groups_ apply tags to the script's functions. + +Besides these granular tags, you can also use options to set test-wide tags. +You can use these tags to compare results from multiple tests. + +In addition to filtering results, you can also [use tags to limit the operations that your thresholds analyze](https://grafana.com/docs/k6//using-k6/thresholds#thresholds-on-tags). + +## Tags + +Tags are a powerful way to categorize your k6 entities and filter test results. + +k6 provides two types of tags: + +- **System tags.** Tags that k6 automatically assigns. +- **User-defined tags.** Tags that you add when you write your script. + +### System tags + +Currently, k6 automatically creates the following tags by default: + +| Tag | Description | +| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `proto` | the name of the protocol used (e.g. `HTTP/1.1`) | +| `subproto` | the subprotocol name (used by websockets) | +| `status` | the HTTP status code (e.g. `200`, `404`, etc.) | +| `method` | the HTTP method name (e.g. `GET`, `POST`, etc.) or the RPC method name for gRPC | +| `url` | the HTTP request URL | +| `name` | the HTTP [request name](https://grafana.com/docs/k6//using-k6/http-requests#url-grouping) | +| `group` | the full [group](#groups) path, see the preceding explanation for details about its value | +| `check` | the [Check](https://grafana.com/docs/k6//using-k6/checks) name | +| `error` | a string with a non-HTTP error message (e.g. network or DNS error) | +| `error_code` | A number specifying an error types; a list of current error codes can be found at the [Error Codes](https://grafana.com/docs/k6//javascript-api/error-codes) page | +| `tls_version` | the [TLS](https://grafana.com/docs/k6//using-k6/protocols/ssl-tls) version | +| `scenario` | the name of the scenario where the metric was emitted | +| `service` | the RPC service name for gRPC | +| `expected_response` | `true` or `false` based on the [responseCallback](https://grafana.com/docs/k6//javascript-api/k6-http/set-response-callback); by default checks whether the status is 2xx or 3xx | + +To disable some of the preceding tags, use the [`systemTags` option](https://grafana.com/docs/k6//using-k6/k6-options/reference#system-tags). +Note that some data collectors, for example `cloud` runs, may require certain tags. + +The following system tags are optional. Enable them as needed: + +| Tag | Description | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `vu` | the ID of the virtual user that executed the request | +| `iter` | the iteration number | +| `ip` | The IP address of the remote server | +| `ocsp_status` | the [Online Certificate Status Protocol (OCSP)](https://grafana.com/docs/k6//using-k6/protocols/ssl-tls/online-certificate-status-protocol--ocsp) HTTPS status | + +## User-defined tags + +Define your own tags to categorize k6 entities based on your test logic. +You can tag the following entities: + +- requests +- checks +- thresholds +- custom metrics + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { Trend } from 'k6/metrics'; +import { check } from 'k6'; + +const myTrend = new Trend('my_trend'); + +export default function () { + // Add tag to request metric data + const res = http.get('https://httpbin.test.k6.io/', { + tags: { + my_tag: "I'm a tag", + }, + }); + + // Add tag to check + check(res, { 'status is 200': (r) => r.status === 200 }, { my_tag: "I'm a tag" }); + + // Add tag to custom metric + myTrend.add(res.timings.connecting, { my_tag: "I'm a tag" }); +} +``` + +{{< /code >}} + +## Test-wide tags + +Besides attaching tags to requests, checks, and custom metrics, you can set test-wide tags across all metrics. +You can set these tags in two ways: + +- In the CLI, using one or more `--tag NAME=VALUE` flags + +- In the script itself: + + {{< code >}} + + ```javascript + export const options = { + tags: { + name: 'value', + }, + }; + ``` + + {{< /code >}} + +## Code-defined tags + +In the case, a user-defined tag with advanced logic for handling which tag to set is required then it's possible doing it by defining the tag from the code. + +To support advanced tagging workflows, it is also possible to directly set and get them from scripts' code. + +[k6/execution.vu.tags](https://grafana.com/docs/k6//javascript-api/k6-execution/#vu) object's properties can indeed be directly assigned new key/value pairs to define new tags dynamically. This can prove useful, as demonstrated in the following example, to track a container's group from nested groups, and aggregating nested group's sub-metrics. + +```javascript +import http from 'k6/http'; +import exec from 'k6/execution'; +import { group } from 'k6'; + +export const options = { + thresholds: { + 'http_reqs{container_group:main}': ['count==3'], + 'http_req_duration{container_group:main}': ['max<1000'], + }, +}; + +export default function () { + exec.vu.tags.containerGroup = 'main'; + + group('main', function () { + http.get('https://test.k6.io'); + group('sub', function () { + http.get('https://httpbin.test.k6.io/anything'); + }); + http.get('https://test-api.k6.io'); + }); + + delete exec.vu.tags.containerGroup; + + http.get('https://httpbin.test.k6.io/delay/3'); +} +``` + +Using the same API, you can also retrieve any already set user-defined or system-defined tag: + +```javascript +import exec from 'k6/execution'; + +export default function () { + const tag = exec.vu.tags['scenario']; + console.log(tag); // default +} +``` + +## Tagging stages + +Thanks to some helper functions in the [k6-jslib-utils](https://grafana.com/docs/k6//javascript-api/jslib/utils) project, if an executor supports the `stages` option, you can add tags with the current ongoing stage. +Similar to other tags tag, the tag is added to all samples collected during the iteration. + +One way to tag the executed operations is to invoke the `tagWithCurrentStageIndex` function for setting a `stage` tag for identifying the stage that has executed them: + +```javascript +import http from 'k6/http'; +import exec from 'k6/execution'; +import { tagWithCurrentStageIndex } from 'https://jslib.k6.io/k6-utils/1.3.0/index.js'; + +export const options = { + stages: [ + { target: 5, duration: '5s' }, + { target: 10, duration: '10s' }, + ], +}; + +export default function () { + tagWithCurrentStageIndex(); + + // all the requests will have a `stage` tag + // with its value equal to the index of the stage + http.get('https://test.k6.io'); // e.g. {stage: "1"} +} +``` + +Additionally, a profiling function `tagWithCurrentStageProfile` can add a tag with a computed profile of the current running stage: + +```javascript +import http from 'k6/http'; +import exec from 'k6/execution'; +import { tagWithCurrentStageProfile } from 'https://jslib.k6.io/k6-utils/1.3.0/index.js'; + +export const options = { + stages: [{ target: 10, duration: '10s' }], +}; + +export default function () { + tagWithCurrentStageProfile(); + + // all the requests are tagged with a `stage` tag + // with the index of the stage as value + http.get('https://test.k6.io'); // {stage_profile: ramp-up} +} +``` + +The profile value based on the current stage can be one of the following options: + +| Profile | Description | +| ----------- | ----------------------------------------------------------------------- | +| `ramp-up` | The current stage has a target greater than the previous stage's target | +| `steady` | The current stage has a target equal to the previous stage's target | +| `ramp-down` | The current stage has a target less than the previous stage's target | + +## Tags in results output + +{{< code >}} + +```json +{ + "type ": "Point ", + "data ": { + "time ": "2017-05-09T14:34:45.239531499+02:00 ", + "value ": 459.865729, + "tags ": { + "group ": "::my group::json ", + "method ": "GET ", + "status ": "200 ", + "url ": "https://httpbin.test.k6.io/get " + } + }, + "metric ": "http_req_duration " +} +``` + +```json +{ + "type ": "Point ", + "data ": { + "time ": "2017-05-09T14:34:45.625742514+02:00 ", + "value ": 5, + "tags ": null + }, + "metric ": "vus " +} +``` + +{{< /code >}} + +To see how tags affect your test-result output, refer to the [k6 results output syntax](https://grafana.com/docs/k6//results-output/real-time/json). + +## Groups + +For extra organization, use _groups_ to organize a load script by functions. +You can also nest groups for BDD-style testing. + +All metrics emitted in a [group](https://grafana.com/docs/k6//javascript-api/k6/group) have the tag `group` with a value of all wrapping group names separated by `::` (two colons). +The root group uses the name '' (empty string). +If you have a single group named `cool requests`, the actual value of the `group` is `::cool requests`. + +For example, you could use groups to organize multiple requests by page loads or user actions. + +{{< code >}} + +```javascript +import { group } from 'k6'; + +export default function () { + group('visit product listing page', function () { + // ... + }); + group('add several products to the shopping cart', function () { + // ... + }); + group('visit login page', function () { + // ... + }); + group('authenticate', function () { + // ... + }); + group('checkout process', function () { + // ... + }); +} +``` + +{{< /code >}} + +Groups do the following tasks internally: + +- For each `group()` function, k6 emits a [group_duration metric](https://grafana.com/docs/k6//using-k6/metrics), which contains the total time to execute the group function. + +- When a taggable resource—a check, request, or custom metric—runs within a group, k6 sets the tag `group` with the current group name. + For more info, refer to the [Tags section](https://grafana.com/docs/k6//using-k6/tags-and-groups#tags). + +Both options, the `group_duration` metric and `group tagging`, could help you analyze and visualize complex test results. Check out how they work in your [k6 result output](https://grafana.com/docs/k6//misc/integrations#result-store-and-visualization). + +### Discouraged: one group per request + +Wrapping each request within a group might add unnecessary boilerplate. + +{{< code >}} + +```javascript +import { group, check } from 'k6'; +import http from 'k6/http'; + +const id = 5; + +// reconsider this type of code +group('get post', function () { + http.get(`http://example.com/posts/${id}`); +}); +group('list posts', function () { + const res = http.get(`http://example.com/posts`); + check(res, { + 'is status 200': (r) => r.status === 200, + }); +}); +``` + +{{< /code >}} + +If your code looks like the preceding snippet, consider the following strategies to write cleaner code: + +- For dynamic URLs, use the [URL grouping feature](https://grafana.com/docs/k6//using-k6/http-requests#url-grouping). +- To provide a meaningful name to your request, set the value of [tags.name](https://grafana.com/docs/k6//using-k6/http-requests#http-request-tags). +- To reuse common logic or organize your code better, group logic in functions, or create a [local JavaScript module](https://grafana.com/docs/k6//using-k6/modules#local-filesystem-modules) and import it into the test script. +- To model advanced user patterns, check out [Scenarios](https://grafana.com/docs/k6//using-k6/scenarios). diff --git a/docs/sources/v0.50.x/using-k6/test-authoring/_index.md b/docs/sources/v0.50.x/using-k6/test-authoring/_index.md new file mode 100644 index 000000000..43d554462 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/test-authoring/_index.md @@ -0,0 +1,10 @@ +--- +title: Test authoring +weight: 14 +--- + +# Test authoring + + + +{{< section >}} diff --git a/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/_index.md b/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/_index.md new file mode 100644 index 000000000..a86553556 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/_index.md @@ -0,0 +1,65 @@ +--- +title: 'Create tests from recordings' +description: 'In load testing, recording usually refers to the process of creating a load test from the recording of a user session.' +weight: 02 +--- + +# Create tests from recordings + +A recording stores the sequence of requests and parameters of a user session or API interaction. +You can use this recording to auto-generate your test logic. + +Testers commonly use recordings to avoid writing complex tests from scratch. +For example, testing advanced scenarios on websites or mobile applications, such as end-to-end (E2E) tests with dozens or hundreds of requests. + +k6 provides two tools that can directly convert a recording into k6 script: + +- [Browser recorder](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder) generates a k6 script from a browser session. +- [HAR converter](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-har-converter) generates a k6 script from the requests included in a HAR file. + +## Steps + +The steps to create a load test from a recording are as follows: + +1. Record a user or API session. +2. Convert the recorded session into a test. +3. Set up the load and test options. +4. Handle [correlation and dynamic data](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data). + +You can then debug or run the load test. + +## Be sure to record realistically + +If you use a browser to simulate a user session and generate its recording, consider the following dos and don'ts. + +It's a good idea to: + +- Browse as a user would. +- Take natural pauses that users would take to consume page content. +- Focus on the most common use cases, rather than all the possible use cases. +- Take note of pages where forms/logins occur. You will need to edit the script to [handle correlation](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data). + +You probably _do not_ want to: + +- Visit every page in one journey. +- Click every possible option. +- Navigate as fast as you can. +- Navigate out of your actual site or application. + +## Consider hybrid approach for load testing websites + +When you start the recording and navigate as a user, the recorder captures every HTTP(s) request loaded into the browser as you click. This includes all the requests for third-party services, ads, images, documents, etc. + +When you finish the recording, the converter generates the k6 script from all the recorded requests and assets. +The script could include **dozens or hundreds of requests for each page visit or interaction**. + +These types of recorded tests are difficult to maintain. As the website changes, these tests must be updated to reflect assets and API changes. + +An alternative approach to load test websites is to run a [hybrid load test](https://grafana.com/docs/k6//testing-guides/load-testing-websites/#hybrid-load-testing) which: + +- Runs a [browser test](https://grafana.com/docs/k6//using-k6-browser/running-browser-tests) to validate the frontend. +- While simultaneously running API tests to inject load to the backend. + +As the browser test automatically handles website assets, these tests require fewer updates. + +To learn more about this approach, check out the [example mixing browser-level and protocol-level tests](https://grafana.com/docs/k6//using-k6-browser/running-browser-tests#run-both-browser-level-and-protocol-level-tests-in-a-single-script). diff --git a/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder.md b/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder.md new file mode 100644 index 000000000..e9566ba1e --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder.md @@ -0,0 +1,44 @@ +--- +title: 'Using the browser recorder' +description: 'The browser recorder allows generating a k6 script based on a web session. It is available as extensions for Chrome and Firefox.' +weight: 01 +--- + +# Using the browser recorder + +The browser recorder lets you generate a k6 script based on a browser session. +It's available as an extension for [Chrome](https://chrome.google.com/webstore/detail/grafana-k6-browser-record/fbanjfonbcedhifbgikmjelkkckhhidl) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/grafana-k6-browser-recorder/). + +## Before you start + +Before you start, consider the following: + +- [Be sure to record realistically](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings#be-sure-to-record-realistically) +- [A hybrid approach for load testing websites](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings#consider-hybrid-approach-for-load-testing-websites) + +> Note that the browser recorders **do not require a cloud account**. For cloud users, check out the [Grafana Cloud](https://grafana.com/docs/grafana-cloud/k6/author-run/browser-recorder/) instructions. + +## How to record + +1. Install the [Chrome](https://chrome.google.com/webstore/detail/grafana-k6-browser-record/fbanjfonbcedhifbgikmjelkkckhhidl) or [Firefox](https://addons.mozilla.org/en-US/firefox/addon/grafana-k6-browser-recorder/) extension. +1. Open the extension by clicking the k6 logo. +1. Choose where to save the auto-generated script. + - To save it on your local machine, select **I don't want to save tests in the cloud**. + - To save it on any of your Grafana Cloud k6 projects, select **Sign In**. +1. Select **Start recording** to begin recording the current browser tab. +1. When done, select **Stop recording**. +1. Save the recorded script locally or in any of your cloud projects. +1. Edit your script as necessary. Depending on the [type of load test](https://grafana.com/docs/k6//testing-guides/test-types/), you might need to change different aspects of the script. + Typical changes are for [load options](https://grafana.com/docs/k6//using-k6/k6-options) and to handle [correlation and dynamic data](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data). +1. Run the test from the CLI or Grafana Cloud k6. For more about running k6, refer to the [Running k6 guide](https://grafana.com/docs/k6//get-started/running-k6). + +## Troubleshooting. Try the HAR converter + +If you experience problems recording a request, try the [HAR converter](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-har-converter). + +The Browser recorder uses the HAR converter under the hood. +Like the Browser Recorder, the HAR converter creates a k6 script from the HTTP requests included in a HAR file. + +The HAR converter can also record other tabs or pop-up windows, which the Browser Recorder cannot. + +If the error persists with the HAR converter, please provide detailed information about the problem [in a new issue](https://github.com/k6io/har-to-k6/issues). diff --git a/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/using-the-har-converter.md b/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/using-the-har-converter.md new file mode 100644 index 000000000..d03adbbf2 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/test-authoring/create-tests-from-recordings/using-the-har-converter.md @@ -0,0 +1,163 @@ +--- +title: 'Using the HAR converter' +description: 'The HAR converter is an alternative to the Browser recorder. It generates a k6 script based on the HTTP requests included on a HAR file.' +weight: 02 +--- + +# Using the HAR converter + +The [har-to-k6 converter](https://github.com/k6io/har-to-k6) is a NodeJS tool that generates a k6 script based on the HTTP requests included in a [HAR file](). +It is an alternative to the [Browser recorder](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings/using-the-browser-recorder). + +{{% admonition type="note" %}} + +HAR is a file format used by all major browsers and various other tools to export recorded HTTP requests. + +{{% /admonition %}} + +## Before you start + +Before you start, consider the following: + +- [Be sure to record realistically](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings#be-sure-to-record-realistically) +- [A hybrid approach for load testing websites](https://grafana.com/docs/k6//using-k6/test-authoring/create-tests-from-recordings#consider-hybrid-approach-for-load-testing-websites) + +You'll need to choose a tool to record your HAR file. +Multiple browsers and tools can export HTTP traffic in a HAR format. +A few popular ones are: + +- [Chrome](https://www.google.com/chrome/) +- [Firefox](https://www.mozilla.org/en-US/firefox/) +- [Microsoft Edge](https://www.microsoft.com/en-us/edge) +- [Charles recording proxy](http://www.charlesproxy.com/)(HTTP proxy/recorder) +- [Fiddler](http://www.telerik.com/fiddler) (HTTP proxy/recorder) + +## 1. Record a HAR file + +Here are the basic steps you need to record in Chrome: + +1. Open a new incognito window in Chrome. (This is optional, but it means you won't send things like cookies, which your browser might have saved). +1. Open up Chrome developer tools (press F12). +1. Select the **Network** tab. +1. Check that the recording button (round button) is activated (red color). +1. If you want to make a recording of several successive page loads, select the **Preserve log** checkbox. +1. Enter the URL of your site and start doing whatever you want your simulated load-test users to do. +1. When done, in Chrome developer tools, right-click the URLs and choose **Save as HAR with content**. + +![Save HAR for load testing](/media/docs/k6-oss/session_recorder_save_as_har.png) + +## 2. Convert with `har-to-k6` + +The [har-to-k6 converter](https://github.com/k6io/har-to-k6) is a NodeJS tool that can convert a HAR file (browser session) into a k6 script. + +1. Make sure that you have installed NodeJS (version >=11.0.0). +1. Install the converter. You can use `npm`: + + ```bash + $ npm install -g har-to-k6 + ``` + + For other installation options, check out the [har-to-k6 installation instructions](https://github.com/k6io/har-to-k6#installation). + +1. Generate a k6 script from a HAR file with the convert command: + + ```bash + $ har-to-k6 myfile.har -o loadtest.js + ``` + + This command auto-generates a k6 script for you. + It reads the HAR file (_myfile.har_) and converts it into a k6 test (_loadtest.js_). + +## 3. Modify the auto-generated k6 script + +In the previous step, the converter created a k6 script for testing. +Now, you should evaluate whether you have to change any part of the k6 script. + +Depending on your use case, you might need to: + +- Configure the load options +- Remove third-party content +- Correlate dynamic data + +### Configure the load options + +Now, k6 has auto-generated a "functional" test. +By default, this test runs with one virtual user and for one iteration. + +It's time for you to configure the load options of your performance tests. +k6 lets you configure this in several ways: + +- As CLI arguments while running the test: + + ```bash + k6 run --vus 10 --duration 30s loadtest.js + ``` + +- As options in the script file. + + ```javascript + export const options = { + vus: 10, + duration: '30s', + }; + ``` + +To learn more about how to configure the load options, read the [Adding more VUs guide](https://grafana.com/docs/k6//get-started/running-k6#adding-more-vus) and the [Options guide](https://grafana.com/docs/k6//using-k6/k6-options). + +### Remove third-party content + +If you are recording a user session of a website, by default, you'll record all the HTTP requests that your website uses. +This includes requests from the third-party tools that your site uses, +e.g. analytics tools, Facebook, Twitter, Support Widgets, CDNs, etc. + +You should remove these third party requests: + +- They will skew the percentiles of your performance results. +- You may be unable to affect the performance of the third-party service. +- The load test may violate the terms-of-service contract that you have with the provider. + +Your k6 script can skip third-party requests in a few ways: + +- Edit the auto-generated k6 script, removing the requests one-by-one +- Download a HAR file with only requests to the selected domains. + +In Chrome, you can use the DevTools Network Filter to select only particular domains. +The Filter input accepts a Regex to match multiple domains. + +```bash +/loadimpact.com|cloudfront.net/ +``` + +![Save HAR filter domain using regex](/media/docs/k6-oss/session_recorder_filter_domain.png) + +After filtering your selected domains, you can download the HAR file as described in the first step of this tutorial. +The HAR file will include only the requests to the selected domains. + +If you don't know all the domains to filter, it helps to use the query language of the Network Filter. +Just input `domain:` in the filter to see all the different domains recorded by the Network Panel. + +![Save HAR filter domain list](/media/docs/k6-oss/session_recorder_filter_domain_list.png) + +### Correlate dynamic data + +In load testing, _correlation_ is when you extract the value from the response of one request and reuse it in a subsequent request. +Often, the correlation would be for a token or ID that is needed to run a sequence of steps in a user journey. + +The recorded HAR file may include dynamic data used on your site - `IDs`, `CSRF tokens`, `VIEWSTATE`, `wpnonce`, and other `dynamic values` - that will be converted into the k6 script. + +To run your load test correctly, you may need to replace some recorded data with dynamic data that k6 gets from previous requests. +For example, tokens expire quickly, and they are one of the most common things that users will correlate from a recorded session. + +[Here](https://grafana.com/docs/k6//examples/correlation-and-dynamic-data) are a few examples using the k6 API to correlate dynamic data. + +## 4. Run the test + +Now, you can run your load test with k6. If you have not installed k6 yet, please, follow the [k6 installation instructions](https://grafana.com/docs/k6//get-started/installation). + +Execute the `k6 run` command to run your k6 script: + +```bash +$ k6 run loadtest.js +``` + +To learn about running k6, check out the [Running k6 tutorial](https://grafana.com/docs/k6//get-started/running-k6). diff --git a/docs/sources/v0.50.x/using-k6/test-authoring/test-builder.md b/docs/sources/v0.50.x/using-k6/test-authoring/test-builder.md new file mode 100644 index 000000000..fa48f2ba3 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/test-authoring/test-builder.md @@ -0,0 +1,21 @@ +--- +title: 'Test builder' +description: 'Use a graphical interface to create a k6 test.' +weight: 01 +--- + +# Test builder + +The k6 Test Builder provides a graphical interface to generate a k6 test script based on your input. Then, you can copy the test script and [run the test from the CLI](https://grafana.com/docs/k6//get-started/running-k6). + +Though we strongly believe that scriptable, code-based tools will help you get the most out of your performance-testing efforts, a GUI-based tool like the test builder could help you: + +- Speed up the test creation. +- Learn the [k6 API](https://grafana.com/docs/k6//javascript-api) quickly. +- Collaborate with non-coders to build tests. + +The k6 Test Builder is **free to use** and is available in: + +- [Grafana Cloud k6](https://grafana.com/docs/grafana-cloud/k6/author-run/test-builder/) + +![k6 Test Builder](/media/docs/k6-oss/grafana-cloud-k6-test-builder.png) diff --git a/docs/sources/v0.50.x/using-k6/test-lifecycle.md b/docs/sources/v0.50.x/using-k6/test-lifecycle.md new file mode 100644 index 000000000..a2b4928a5 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/test-lifecycle.md @@ -0,0 +1,284 @@ +--- +title: 'Test lifecycle' +description: 'The four distinct lifecycle stages in a k6 test are "init", "setup", "VU", and "teardown".' +weight: 06 +--- + +# Test lifecycle + +In the lifecycle of a k6 test, +a script always runs through these stages in the same order: + +1. Code in the `init` context prepares the script, loading files, importing modules, and defining the test _lifecycle functions_. **Required**. +2. The `setup` function runs, setting up the test environment and generating data. _Optional._ +3. VU code runs in the `default` or scenario function, running for as long and as many times as the `options` define. **Required**. +4. The `teardown` function runs, postprocessing data and closing the test environment. _Optional._ + +{{% admonition type="note" %}} + +**Lifecycle functions** + +Except for init code, each stage occurs in a _lifecycle function_, +a function called in a specific sequence in the k6 runtime. + +{{% /admonition %}} + +{{< code >}} + +```javascript +// 1. init code + +export function setup() { + // 2. setup code +} + +export default function (data) { + // 3. VU code +} + +export function teardown(data) { + // 4. teardown code +} +``` + +{{< /code >}} + +## Overview of the lifecycle stages + +For examples and implementation details of each stage, refer to the subsequent sections. + +| Test stage | Purpose | Example | Called | +| --------------- | ------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------- | +| **1. init** | Load local files, import modules, declare lifecycle functions | Open JSON file, Import module | Once per VU\* | +| **2. Setup** | Set up data for processing, share data among VUs | Call API to start test environment | Once | +| **3. VU code** | Run the test function, usually `default` | Make https requests, validate responses | Once per iteration, as many times as the test options require | +| **4. Teardown** | Process result of setup code, stop test environment | Validate that setup had a certain result, send webhook notifying that test has finished | Once \*\* | + +\* In cloud scripts, init code might be called more often. + +\*\* If the `Setup` function ends abnormally (e.g throws an error), the `teardown()` function isn't called. Consider adding logic to the `setup()` function to handle errors and ensure proper cleanup. + +## The init stage + +**The init stage is required**. +Before the test runs, k6 needs to initialize the test conditions. +To prepare the test, code in the `init` context runs once per VU. + +Some operations that might happen in `init` include the following: + +- Import modules +- Load files from the local file system +- Configure the test for all `options` +- Define lifecycle functions for the VU, `setup`, and `teardown` stages (and for custom or `handleSummary()` functions, too). + +**All code that is outside of a lifecycle function is code in the `init` context**. +Code in the `init` context _always executes first_. + +{{< code >}} + +```javascript +// init context: importing modules +import http from 'k6/http'; +import { Trend } from 'k6/metrics'; + +// init context: define k6 options +export const options = { + vus: 10, + duration: '30s', +}; + +// init context: global variables +const customTrend = new Trend('oneCustomMetric'); + +// init context: define custom function +function myCustomFunction() { + // ... +} +``` + +{{< /code >}} + +Separating the `init` stage from the VU stage removes irrelevant computation from VU code, which improves k6 performance and makes test results more reliable. +One limitation of `init` code is that it **cannot** make HTTP requests. +This limitation ensures that the `init` stage is reproducible across tests (the response from protocol requests is dynamic and unpredictable) + +## The VU stage + +Scripts must contain, at least, a _scenario function_ that defines the logic of the VUs. +The code inside this function is _VU code_. +Typically, VU code is inside the `default` function, but it can also be inside the function defined by a scenario (see subsequent section for an example). + +{{< code >}} + +```javascript +export default function () { + // do things here... +} +``` + +{{< /code >}} + +**VU code runs over and over through the test duration.** +VU code can make HTTP requests, emit metrics, and generally do everything you'd expect a load test to do. +The only exceptions are the jobs that happen in the `init` context. + +- VU code _does not_ load files from your local filesystem. +- VU code _does not_ import any other modules. + +Again, instead of VU code, init code does these jobs. + +### The default function life-cycle + +A VU executes the `default()` function from start to end in sequence. +Once the VU reaches the end of the function, it loops back to the start and executes the code all over. + +As part of this "restart" process, k6 resets the VU. +Cookies are cleared, and TCP connections might be torn down (depending on your test configuration options). + +## Setup and teardown stages + +Like `default`, `setup` and `teardown` functions must be exported functions. +But unlike the `default` function, k6 calls `setup` and `teardown` only once per test. + +- `setup` is called at the beginning of the test, after the init stage but before the VU stage. +- `teardown` is called at the end of a test, after the VU stage (`default` function). + +You can call the full k6 API in the setup and teardown stages, unlike the init stage. +For example, you can make HTTP requests: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export function setup() { + const res = http.get('https://httpbin.test.k6.io/get'); + return { data: res.json() }; +} + +export function teardown(data) { + console.log(JSON.stringify(data)); +} + +export default function (data) { + console.log(JSON.stringify(data)); +} +``` + +{{< /code >}} + +### Skip setup and teardown execution + +You can skip the execution of setup and teardown stages using the options `--no-setup` and +`--no-teardown`. + +{{< code >}} + +```bash +$ k6 run --no-setup --no-teardown ... +``` + +{{< /code >}} + +### Use data from setup in default and teardown + +Again, let's have a look at the basic structure of a k6 test: + +{{< code >}} + +```javascript +// 1. init code + +export function setup() { + // 2. setup code +} + +export default function (data) { + // 3. VU code +} + +export function teardown(data) { + // 4. teardown code +} +``` + +{{< /code >}} + +You might have noticed the function signatures of the `default()` and `teardown()` functions take an argument, referred to here as `data`. + +Here's an example of passing some data from the setup code to the VU and teardown stages: + +{{< code >}} + +```javascript +export function setup() { + return { v: 1 }; +} + +export default function (data) { + console.log(JSON.stringify(data)); +} + +export function teardown(data) { + if (data.v != 1) { + throw new Error('incorrect data: ' + JSON.stringify(data)); + } +} +``` + +{{< /code >}} + +For example, with the data returned by the `setup()` function, you can: + +- Give each VU access to an identical copy of the data +- Postprocess the data in `teardown` code + +However, there are some restrictions. + +- You can pass only data (i.e. JSON) between `setup` and the other stages. + You cannot pass functions. +- If the data returned by the `setup()` function is large, it will consume more memory. +- You cannot manipulate data in the `default()` function, then pass it to the `teardown()` function. + +It's best to think that each stage and each VU has access to a fresh "copy" of whatever data the `setup()` function returns. + +![Diagram showing data getting returned by setup, then used (separately) by default and teardown functions](/media/docs/k6-oss/lifecycle.png) + +It would be extremely complicated and computationally intensive to pass mutable data between all VUs and then to teardown, especially in distributed setups. +This would go against a core k6 goal: the same script should be executable in multiple modes. + +## Additional lifecycle functions + +k6 has a few additional ways to use lifecycle functions: + +- **`handleSummary()`**. If you want to make a custom summary, k6 calls one more lifecycle function at the very end of the test. + + For details, refer to [Custom summary](https://grafana.com/docs/k6//results-output/end-of-test/custom-summary). + +- **Scenario functions**. Instead of the `default` function, you can also run VU code in scenario functions. + + {{< code >}} + + ```javascript + import http from 'k6/http'; + import { sleep } from 'k6'; + + export const options = { + scenarios: { + my_web_test: { + // the function this scenario will execute + exec: 'webtest', + executor: 'constant-vus', + vus: 50, + duration: '1m', + }, + }, + }; + + export function webtest() { + http.get('https://test.k6.io/contacts.php'); + sleep(Math.random() * 2); + } + ``` + + {{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/thresholds.md b/docs/sources/v0.50.x/using-k6/thresholds.md new file mode 100644 index 000000000..1f49bfe8c --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/thresholds.md @@ -0,0 +1,530 @@ +--- +title: 'Thresholds' +description: 'Thresholds are a pass/fail criteria used to specify the performance expectations of the system under test.' +weight: 04 +--- + +# Thresholds + +Thresholds are the pass/fail criteria that you define for your test metrics. +If the performance of the system under test (SUT) does not meet the conditions of your threshold, +**the test finishes with a failed status.** + +Often, testers use thresholds to codify their SLOs. +For example, you can create thresholds for any combination of the following expectations: + +- Less than 1% of requests return an error. +- 95% of requests have a response time below 200ms. +- 99% of requests have a response time below 400ms. +- A specific endpoint always responds within 300ms. +- Any conditions for a [custom metric](https://grafana.com/docs/k6//using-k6/metrics/create-custom-metrics). + +Thresholds are also essential for [load-testing automation](https://grafana.com/docs/k6//testing-guides/automated-performance-testing): + +1. Give your test a threshold. +1. Automate your execution +1. Set up alerts for test failures. + +After that, you need to worry about the test only after your SUT fails to meet its performance expectations. + +## Thresholds example for HTTP errors and response duration + +This sample script specifies two thresholds. +One threshold evaluates the rate of HTTP errors (`http_req_failed` metric). +The other evaluates whether 95 percent of responses happen within a certain duration (the `http_req_duration` metric). + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + thresholds: { + http_req_failed: ['rate<0.01'], // http errors should be less than 1% + http_req_duration: ['p(95)<200'], // 95% of requests should be below 200ms + }, +}; + +export default function () { + http.get('https://test-api.k6.io/public/crocodiles/1/'); +} +``` + +{{< /code >}} + +In other words, when you define your threshold, specify an expression for a `pass` criteria. +If that expression evaluates to `false` at the end of the test, k6 considers the whole test a `fail`. + +After executing that script, k6 outputs something similar to this: + +{{< code >}} + +```bash + ✓ http_req_duration..............: avg=151.06ms min=151.06ms med=151.06ms max=151.06ms p(90)=151.06ms p(95)=151.06ms + { expected_response:true }...: avg=151.06ms min=151.06ms med=151.06ms max=151.06ms p(90)=151.06ms p(95)=151.06ms + ✓ http_req_failed................: 0.00% ✓ 0 ✗ 1 +``` + +{{< /code >}} + +In this case, the test met the criteria for both thresholds. +k6 considers this test a `pass` and exits with an exit code `0`. + +If any of the thresholds had failed, the little green checkmark next to the threshold name +(`http_req_failed`, `http_req_duration`) would be a red cross +and k6 would exit with a non-zero exit code. + +## Threshold Syntax + +To use a threshold, follow these steps: + +1. In the `thresholds` property of the `options` object, set a key using the name of the metric you want the threshold for: + + ```javascript + export const options = { + thresholds: { + /* ... */ + }, + }; + ``` + +2. Define at least one threshold expression. You can do this in two ways: + + - The short format puts all threshold expressions as strings in an array. + - The long format puts each threshold in an object, with extra properties to [abort on failure](#abort-a-test-when-a-threshold-is-crossed). + + ```javascript + export const options = { + thresholds: { + //short format + METRIC_NAME1: ['THRESHOLD_EXPRESSION', `...`], + //long format + METRIC_NAME2: [ + { + threshold: 'THRESHOLD_EXPRESSION', + abortOnFail: true, // boolean + delayAbortEval: '10s', // string + }, + ], // full format + }, + }; + ``` + + Note that `METRIC_NAME1` and `THRESHOLD_EXPRESSION` are placeholders. + The real text must be the name of the metric and the threshold expression. + +This declaration configures thresholds for the metrics `metric_name1` and `metric_name2`. +To determine whether the threshold passes or fails, the script evaluates the `'threshold_expression'.` + +### Threshold expression syntax + +A threshold expression evaluates to `true` or `false`. +The threshold expression must be in the following format: + +``` + +``` + +Some examples of threshold expressions are as follows: + +- `avg < 200` // average duration must be less than 200ms +- `count >= 500` // count must be larger than or equal to 500 +- `p(90) < 300` // 90% of samples must be below 300 + +### Aggregation methods by type + +k6 aggregates metrics according to [their type](https://grafana.com/docs/k6//using-k6/metrics). +These aggregation methods form part of the threshold expressions. + +| Metric type | Aggregation methods | +| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Counter | `count` and `rate` | +| Gauge | `value` | +| Rate | `rate` | +| Trend | `avg`, `min`, `max`, `med` and `p(N)` where `N` specifies the threshold percentile value, expressed as a number between 0.0 and 100. E.g. `p(99.99)` means the 99.99th percentile. The values are in milliseconds. | + +This (slightly contrived) sample script uses all different types of metrics, +setting different types of thresholds for each: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { Trend, Rate, Counter, Gauge } from 'k6/metrics'; +import { sleep } from 'k6'; + +export const TrendRTT = new Trend('RTT'); +export const RateContentOK = new Rate('Content OK'); +export const GaugeContentSize = new Gauge('ContentSize'); +export const CounterErrors = new Counter('Errors'); +export const options = { + thresholds: { + // Count: Incorrect content cannot be returned more than 99 times. + 'Errors': ['count<100'], + // Gauge: returned content must be smaller than 4000 bytes + 'ContentSize': ['value<4000'], + // Rate: content must be OK more than 95 times + 'Content OK': ['rate>0.95'], + // Trend: Percentiles, averages, medians, and minimums + // must be within specified milliseconds. + 'RTT': ['p(99)<300', 'p(70)<250', 'avg<200', 'med<150', 'min<100'], + }, +}; + +export default function () { + const res = http.get('https://test-api.k6.io/public/crocodiles/1/'); + const contentOK = res.json('name') === 'Bert'; + + TrendRTT.add(res.timings.duration); + RateContentOK.add(contentOK); + GaugeContentSize.add(res.body.length); + CounterErrors.add(!contentOK); + + sleep(1); +} +``` + +{{< /code >}} + +{{% admonition type="caution" %}} + +Do not specify multiple thresholds for the same metric by repeating the same object key. + +{{% /admonition %}} + +Since thresholds are defined as the properties of a JavaScript object, you can't specify multiple ones with the same property name. + +{{< code >}} + +```javascript +export const options = { + thresholds: { + // don't use the same metric more than once here + metric_name: ['count<100'], + metric_name: ['rate<50'], + }, +}; +``` + +{{< /code >}} + +The rest will be **silently** ignored. +If you want to set multiple thresholds for a metric, specify them with an [array for the same key](https://grafana.com/docs/k6//using-k6/thresholds#multiple-thresholds-on-a-single-metric). + +## Threshold examples to copy and paste + +The quickest way to start with thresholds is to use the [built-in metrics](https://grafana.com/docs/k6//using-k6/metrics/reference). +Here are a few copy-paste examples that you can start using right away. + +For more specific threshold examples, refer to the [Counter](https://grafana.com/docs/k6//javascript-api/k6-metrics/counter#counter-usage-in-thresholds), [Gauge](https://grafana.com/docs/k6//javascript-api/k6-metrics/gauge#gauge-usage-in-thresholds), [Trend](https://grafana.com/docs/k6//javascript-api/k6-metrics/trend#trend-usage-in-thresholds) and [Rate](https://grafana.com/docs/k6//javascript-api/k6-metrics/rate#rate-usage-in-thresholds) pages. + +### A percentile of requests finishes in a specified duration + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + thresholds: { + // 90% of requests must finish within 400ms. + http_req_duration: ['p(90) < 400'], + }, +}; + +export default function () { + http.get('https://test-api.k6.io/public/crocodiles/1/'); + sleep(1); +} +``` + +{{< /code >}} + +### Error rate is lower than 1 percent + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + thresholds: { + // During the whole test execution, the error rate must be lower than 1%. + http_req_failed: ['rate<0.01'], + }, +}; + +export default function () { + http.get('https://test-api.k6.io/public/crocodiles/1/'); + sleep(1); +} +``` + +{{< /code >}} + +### Multiple thresholds on a single metric + +You can also apply multiple thresholds for one metric. +This threshold has different duration requirements for different request percentiles. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; + +export const options = { + thresholds: { + // 90% of requests must finish within 400ms, 95% within 800, and 99.9% within 2s. + http_req_duration: ['p(90) < 400', 'p(95) < 800', 'p(99.9) < 2000'], + }, +}; + +export default function () { + const res1 = http.get('https://test-api.k6.io/public/crocodiles/1/'); + sleep(1); +} +``` + +{{< /code >}} + +### Threshold on group duration + +You can set thresholds per [Group](https://grafana.com/docs/k6//using-k6/tags-and-groups#groups). +This code has groups for individual requests and batch requests. +For each group, there are different thresholds. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { group, sleep } from 'k6'; + +export const options = { + thresholds: { + 'group_duration{group:::individualRequests}': ['avg < 400'], + 'group_duration{group:::batchRequests}': ['avg < 200'], + }, + vus: 1, + duration: '10s', +}; + +export default function () { + group('individualRequests', function () { + http.get('https://test-api.k6.io/public/crocodiles/1/'); + http.get('https://test-api.k6.io/public/crocodiles/2/'); + http.get('https://test-api.k6.io/public/crocodiles/3/'); + }); + + group('batchRequests', function () { + http.batch([ + ['GET', `https://test-api.k6.io/public/crocodiles/1/`], + ['GET', `https://test-api.k6.io/public/crocodiles/2/`], + ['GET', `https://test-api.k6.io/public/crocodiles/3/`], + ]); + }); + + sleep(1); +} +``` + +{{< /code >}} + +## Set thresholds for specific tags + +It's often useful to specify thresholds on a single URL or specific tag. +In k6, tagged requests create sub-metrics that you can use in thresholds: + +```javascript +export const options = { + thresholds: { + 'metric_name{tag_name:tag_value}': ['threshold_expression'], + }, +}; +``` + +And here's a full example. + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { sleep } from 'k6'; +import { Rate } from 'k6/metrics'; + +export const options = { + thresholds: { + 'http_req_duration{type:API}': ['p(95)<500'], // threshold on API requests only + 'http_req_duration{type:staticContent}': ['p(95)<200'], // threshold on static content only + }, +}; + +export default function () { + const res1 = http.get('https://test-api.k6.io/public/crocodiles/1/', { + tags: { type: 'API' }, + }); + const res2 = http.get('https://test-api.k6.io/public/crocodiles/2/', { + tags: { type: 'API' }, + }); + + const responses = http.batch([ + ['GET', 'https://test-api.k6.io/static/favicon.ico', null, { tags: { type: 'staticContent' } }], + [ + 'GET', + 'https://test-api.k6.io/static/css/site.css', + null, + { tags: { type: 'staticContent' } }, + ], + ]); + + sleep(1); +} +``` + +{{< /code >}} + +## Abort a test when a threshold is crossed + +If you want to abort a test as soon as a threshold is crossed, +set the `abortOnFail` property to `true`. +When you set `abortOnFail`, the test run stops _as soon as the threshold fails_. + +Sometimes, though, a test might fail a threshold early and abort before the test generates significant data. +To prevent these cases, you can delay `abortOnFail` with `delayAbortEval`. +In this script, `abortOnFail` is delayed ten seconds. +After ten seconds, the test aborts if it fails the `p(99) < 10` threshold. + +{{< code >}} + +```javascript +export const options = { + thresholds: { + metric_name: [ + { + threshold: 'p(99) < 10', // string + abortOnFail: true, // boolean + delayAbortEval: '10s', // string + /*...*/ + }, + ], + }, +}; +``` + +{{< /code >}} + +The fields are as follows: + +| Name | Type | Description | +| -------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| threshold | string | This is the threshold expression string specifying the threshold condition to evaluate. | +| abortOnFail | boolean | Whether to abort the test if the threshold is evaluated to false before the test has completed. | +| delayAbortEval | string | If you want to delay the evaluation of the threshold to let some metric samples to be collected, you can specify the amount of time to delay using relative time strings like `10s`, `1m` and so on. | + +Here is an example: + +{{< code >}} + +```javascript +import http from 'k6/http'; + +export const options = { + vus: 30, + duration: '2m', + thresholds: { + http_req_duration: [{ threshold: 'p(99) < 10', abortOnFail: true }], + }, +}; + +export default function () { + http.get('https://test-api.k6.io/public/crocodiles/1/'); +} +``` + +{{< /code >}} + +{{% admonition type="caution" %}} + +When k6 runs in the cloud, thresholds are evaluated every 60 seconds. +Therefore, the `abortOnFail` feature may be delayed by up to 60 seconds. + +{{% /admonition %}} + +## Fail a load test using checks + +[Checks](https://grafana.com/docs/k6//using-k6/checks) are nice for codifying assertions, but unlike `thresholds`, `checks` do not affect the exit status of k6. + +If you use only `checks` to verify that things work as expected, you can't fail the whole test run based on the `check` results. + +It's often useful to combine `checks` and `thresholds`, to get the best of both: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 50, + duration: '10s', + thresholds: { + // the rate of successful checks should be higher than 90% + checks: ['rate>0.9'], + }, +}; + +export default function () { + const res = http.get('https://httpbin.test.k6.io'); + + check(res, { + 'status is 500': (r) => r.status == 500, + }); + + sleep(1); +} +``` + +{{< /code >}} + +In this example, the `threshold` is configured on the [checks metric](https://grafana.com/docs/k6//using-k6/metrics/reference), establishing that the rate of successful checks is higher than 90%. + +Additionally, you can use `tags` on checks if you want to define a threshold based on a particular check or group of checks. For example: + +{{< code >}} + +```javascript +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 50, + duration: '10s', + thresholds: { + 'checks{myTag:hola}': ['rate>0.9'], + }, +}; + +export default function () { + let res; + + res = http.get('https://httpbin.test.k6.io'); + check(res, { + 'status is 500': (r) => r.status == 500, + }); + + res = http.get('https://httpbin.test.k6.io'); + check( + res, + { + 'status is 200': (r) => r.status == 200, + }, + { myTag: 'hola' } + ); + + sleep(1); +} +``` + +{{< /code >}} diff --git a/docs/sources/v0.50.x/using-k6/workaround-iteration-duration.md b/docs/sources/v0.50.x/using-k6/workaround-iteration-duration.md new file mode 100644 index 000000000..afcdec629 --- /dev/null +++ b/docs/sources/v0.50.x/using-k6/workaround-iteration-duration.md @@ -0,0 +1,69 @@ +--- +title: Workaround to calculate iteration_duration +description: 'A threshold can calculate the value of a metric excluding the results of the setup and teardown functions' +_build: + list: false +weight: 20 +--- + +# Workaround to calculate iteration_duration + +A common requested case is to track the `iteration_duration` metric without including time spent for `setup` and `teardown` functions. +This feature is not yet available but a threshold on `iteration_duration` or any pre-existing metrics can be used as a workaround. + +It's based on the concept of creating thresholds for sub-metrics created by tags for the required scope and setting the criteria that always pass. It works with any enabled tags that already works with threshold, for example: + +- `iteration_duration{scenario:default}` generates a sub-metric collecting samples only for the default scenario's iteration. `scenario:default` is used because that's the internal k6 scenario name when we haven't specified `options. `scenarios` explicitly and are just using the execution shortcuts instead. +- `iteration_duration{group:::setup}` or `iteration_duration{group:::teardown}` create sub-metrics collecting the duration only for `setup` and `teardown`. `k6` implicitly creates [groups](https://grafana.com/docs/k6//using-k6/tags-and-groups#groups) for `setup` and `teardown`, and `::` is the group separator. +- `http_req_duration{scenario:default}` can be useful as well for isolating executed long-running requests. + +{{< code >}} + +```javascript +import { sleep } from 'k6'; +import http from 'k6/http'; + +export const options = { + vus: 1, + iterations: 1, + thresholds: { + 'iteration_duration{scenario:default}': [`max>=0`], + 'iteration_duration{group:::setup}': [`max>=0`], + 'iteration_duration{group:::teardown}': [`max>=0`], + 'http_req_duration{scenario:default}': [`max>=0`], + }, +}; + +export function setup() { + http.get('https://httpbin.test.k6.io/delay/5'); +} + +export default function () { + http.get('http://test.k6.io/?where=default'); + sleep(0.5); +} + +export function teardown() { + http.get('https://httpbin.test.k6.io/delay/3'); + sleep(5); +} +``` + +{{< /code >}} + +Dedicated sub-metrics have been generated collecting samples only for the scope defined by thresholds: + +{{< code >}} + +```bash +http_req_duration..............: avg=1.48s min=101.95ms med=148.4ms max=5.22s p(90)=4.21s p(95)=4.71s + { expected_response:true }...: avg=1.48s min=101.95ms med=148.4ms max=5.22s p(90)=4.21s p(95)=4.71s +✓ { scenario:default }.........: avg=148.4ms min=103.1ms med=148.4ms max=193.7ms p(90)=184.64ms p(95)=189.17ms + +iteration_duration.............: avg=5.51s min=1.61s med=6.13s max=8.81s p(90)=8.27s p(95)=8.54s +✓ { group:::setup }............: avg=6.13s min=6.13s med=6.13s max=6.13s p(90)=6.13s p(95)=6.13s +✓ { group:::teardown }.........: avg=8.81s min=8.81s med=8.81s max=8.81s p(90)=8.81s p(95)=8.81s +✓ { scenario:default }.........: avg=1.61s min=1.61s med=1.61s max=1.61s p(90)=1.61s p(95)=1.61s +``` + +{{< /code >}}