Skip to main content

Command Palette

Search for a command to run...

Grafana k6 Browser

Updated
12 min read
Grafana k6 Browser

Grafana k6 Browser current version

k6 Browser 在設計時, 大部分 API 跟名詞都遵循 Playwright 的 API 設計, 這樣的設計使得開發者降低從 Playwright 遷移腳本的認知負擔, 還可直接重用現有的元素定位策略. 以及未來會支援 Playwright RPC server .

瀏覽器的部分現在只支援Chromium, 未來才會陸續支援 FirefoxWebKit-based 的瀏覽器.

所以 k6 Browser 若是有看不懂或描述不清楚的文件, 前往 Playwright 官網查看會比較快又豐富.

k6 Browser 常用 API

k6 Browser 提供了很多 API 模組,其中許多都跟 Playwright 的操作方式兼容。以下是一些基本一定會用到的 API 模組。

Browser context

BrowserContext 在 Playwright 中是一個重要的概念,它代表了一個獨立的瀏覽器 session,類似於 private 模式,可以隔離 cookies 和 Local storage 等,使得每個測試案例在獨立的環境中運行,避免相互幹擾。

常見測試目的像是多帳號測試場景不同裝置或效能測試場景的模擬等。

import { browser } from "k6/browser";  

const iphoneX = devices['iPhone X'];
const context = await browser.newContext(iphoneX);

Page

在 Playwright 和 k6 中,**Page**代表一個瀏覽器標籤頁或頁面,使用者透過Page物件與頁面內容交互,例如導覽、元素操作等。需要區分不同框架中的實作差異,例如 k6 可能更注重效能測試,而 Playwright 則更著重功能測試。同一個 BrowserContext 底下能開啟很多 Page 來測試。

// 多頁面管理範例
const context = browser.newContext();
const page1 = await context.newPage();
const page2 = await context.newPage();

// 頁面關係示意
Browser Instance
└── BrowserContext
    ├── Page 1 (Main Frame)
    │   └── Sub Frame (iframe)
    └── Page 2 (SPA)

如果有多個頁面,我們能實踐**跨 Page 通訊**的測試互動

// 主頁面
const [popup] = await Promise.all([
  page.waitForEvent('popup'),
  page.click('#open-window')
]);

// 新頁面操作
await popup.fill('#email', 'test@example.com');

但絕大多數我們幾乎都是在單個 Page 上操作的,畢竟現在大部份網站都是 SPA 架構了。

gotowaitForNavigation 是 page 蠻常會使用的 API。

goto(url[, options])

將該頁導航至對應的 URL。通常測試腳本一開始就會執行這句。

await page.goto(`https://otel-demo.field-eng.grafana.net/`);

waitForNavigation([options])

在很多範例的測試腳本都會看到 page.waitForNavigation(),用來等待頁面導航至新的 URL 或重新載入。當您執行會間接導致頁面導航的程式碼時,此方法非常有用。

如果不知道這隻 API,大家肯定會用 sleep(t) 或者使用 waitForTimeout(t),但這個會是個反模式。最顯著的問題是 life cycle,sleep 本身就是blocking的操作,所以 sleep的時長就會被納入 k6 的統計中。其次是可能會使得測試不夠穩定,也可能 sleep 過了,頁面都還沒載入。而waitForTimeout 跟 sleep 其實是一樣的東西。

 page.waitForNavigation()

頁面載入狀態事件

當一個頁面開始載入時,有三個狀態我們需要了解的。domcontentloadedloadnetworkidle,接下來,我需要分別解釋​​這三個事件:

domcontentloaded:當DOMContentLoaded事件觸發時完成操作。這個事件在HTML文件被完全載入和解析時觸發,無需等待 CSS、image 和框架的載入。適用於需要快速與DOM互動的情況,例如測試頁面結構是否已載入完成。

load(預設值):當頁面所有資源(如圖片、CSS)載入完成後觸發 load 事件。這會等待更長時間,適合需要所有資源就緒的場景,例如測試頁面完全載入後的功能。

networkidle:網路空閒至少500毫秒時認為完成。但用戶指出這在某些情況下可能不可靠,特別是對於持續有網路請求的網站(如聊天應用程式)。 k6 browser可能同樣存在這個問題,因此建議使用明確的斷言來確認頁面狀態。

事件狀態比較表

事件類型觸發時機適用場景潛在風險
domcontentloadedHTML 解析完成,DOM 樹構建完畢表單驗證、DOM 操作測試樣式未載入可能影響佈局判斷
load所有資源(圖片/CSS/JS)載入完成完整頁面渲染驗證第三方資源延遲導致超時
networkidle連續 500ms 無網絡活動傳統 SPA 應用長輪詢/WebSocket 導致失效

screenshot([options])

將該頁面給截圖並且儲存在指定路徑,這在 debug 很好用。

await page.screenshot({ path: `./screenshots/order.png` });

Evaluate

evaluate允許在瀏覽器上下文中執行 JavaScript程式碼,通常用於取得或操作頁面元素,或執行特定的客戶端邏輯,如操作 DOM 元素、獲取頁面資料並操作、執行複雜計算「跟頁面中的 JavaScript 互動,甚至能注入 JavaScript 程式等,並且把結果返回 k6 中。且 evaluate 能處理 sync 和 async 函數,並向這些函數傳遞參數。

像是透過 evaluate 執行 window.performance.mark 或是 window.performance.measure,用於精確測量程式碼區塊的效能,幫助開發者分析執行時間和最佳化效能。但其實 k6 browser metrics 有幫忙監測這些數值。

const result = await page.evaluate("([x, y]) => Promise.resolve(x * y)", [7,8]);
console.log(`Result: ${result}`);

await page.evaluate(() => window.performance.mark('page-visit'));

// 假設頁面中有 greet(content) 的 JavaScript 函數
result = await page.evaluate('greet("k6 demo")');

Locator

Locator 絕對是這裡面最需要搞懂的模組,它與document.querySelectorㄧ 樣就是找到需要操作的元素。

locator(selector) 唯一的參數就是 selector,這有常在使用 jQuery 的開發者應該不陌生了 XD

關於 select 的選擇官方有給一份最佳實踐。

  1. 盡可能優先選擇頁面裡唯一的屬性。

  2. 穩定性,抗變更的能力

  3. 語意性,團隊中的規範

還給了評分表當作參考 ❌ = 0, ⚠️= 0.5, ✅ = 1 ,選用的 selector 類型分數越高越好。

Unique to the pageValue is stableConveys intentTotal
autocapitalize0
class⚠️⚠️1
id⚠️⚠️2
name⚠️2.5
placeholder⚠️2.5
data-testid3
// 危險範例
page.locator('.css-8tk2dk-input-input') // 由CSS-in-JS生成
// React產生的易變ID
page.locator('#:r0:'); // 使用useId()生成
// 多語言情境下的問題
page.locator('[placeholder="email or username"]')

selector 若選用 placeholder 的話可能會有以下問題︰

  • 需要與i18n系統整合

  • 內容團隊可能修改文案

  • 無法區分相似元素(如多個email輸入框)

data-testid 這我其實不知道是什麼,我看公司平台的前端內容是有 data-pc-section、data-pc-nam,就加減用了。真的沒招的話,最差就是 XPath 了。

page.locator(`//h2[text()="Product Title"]`)

Check

若是以前有再寫 k6 API 測試的開發者,一定對 check 不陌生。以前的話是這樣寫

check( val, sets, [tags] ),set 則是對 val 做斷言檢查的部份。

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,
  });
}

But ! 瀏覽器的元件都是 Asynchronous 的。原本 k6 內建的 check 沒辦法處理。因此需要 k6 util 提供的 check 來處理非同步事件。將 locator 取得後,來斷言其內容或狀態。

import { check } from 'https://jslib.k6.io/k6-utils/1.6.0/index.js';

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

  try {
    await page.goto('https://test.k6.io/my_messages.php');

    await check(page.locator('h2'), {
      'header': async lo => await lo.textContent() == 'Welcome, admin!'
    });
  } finally {
    await page.close();
  }
}

有 check 的話,都會在 options 的 thresholds 搭配使用 checks 來檢查失敗比例需要高於多少。以下範例就是全部 check 的成功率需要超過 9 成。rate 的範圍值在 [0.0, 1.0] 之間,1.0 等於就是 100% check 都成功。

export const options = {
  thresholds: {
    // the rate of successful checks should be higher than 90%
    checks: ['rate>0.9'],
  },
};

Executor ︰Shared-iterations

k6 其實提供很多 Executor,但 UI 測試幾乎都選用 Shared-iterations 居多,我們先理解 shared-iterations 是什麼。Executor 的選擇主要會根據我們執行測試腳本時,對於設定好的 VU 數量,想要怎麼調度的策略。k6 目前提供了 7 種VU調度策略。有興趣能自己研究。

shared-iterations executor 主要策略是讓設定好的 VU數量去共享迭代次數。以下範例就是 10 個 VU 去共享200次的腳本迭代,但因為執行時間不是穩定的,每個 VU 實際跑到的次數不一定的。

export const options = {
  scenarios: {
    contacts: {
      executor: 'shared-iterations',
      vus: 10,
      iterations: 200,
    },
  },
};

而在 UI 的測試中,其實就只會設定一個 VU 跑一次迭代而已。除非有必要才會設定其他的參數。

OTel Demo 電商專案 Example

import { browser } from "k6/browser";
import { check } from "https://jslib.k6.io/k6-utils/1.6.0/index.js";
import { sleep, fail } from "k6";

const LESS_IMPORTANT = `info`;
const TWO_SECONDS = 2000;

export const options = {
  scenarios: {
    ui: {
      executor: "shared-iterations",
      options: {
        browser: {
          type: "chromium",
        },
      },
    },
  },
  thresholds: {
    'browser_web_vital_lcp': ['p(90) < 1500'],
    'browser_web_vital_inp': ['p(90) < 1500'],
    //'browser_web_vital_inp{url:https://otel-demo.field-eng.grafana.net/}': ['p(90) < 1500'],
    'browser_http_req_failed': ['rate < 0.3'],
    checks: ['rate==1.0'],
  },
};

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

  const result = await page.evaluate("([x, y]) => Promise.resolve(x * y)", [7,8]);
  console.log(`Result: ${result}`);
 // await page.waitForTimeout(5000);

  await page.goto(`https://otel-demo.field-eng.grafana.net/`);
 // await page.evaluate(() => window.performance.mark('page-visit'));

  // Check homepage title
  check(await page.title(), {
    "Homepage title is correct": (title) => title.includes("OTel demo"),
  });

  await page.locator(`//*[text()="Go Shopping"]`).click();

  // Check product listing page
  check(await page.url(), {
    "URL changed to products page": (url) => url.includes("/#hot-products"),
  });

  await Promise.all([
    page
      .locator(`//*[text()="Starsense Explorer Refractor Telescope"]`)
      .click(),

    // sleep (3000);
    page.waitForNavigation(),
  ]);

   // Check product details page
   check(await page.url(), {
    'URL changed to product detail page': (url) => url.includes('/product/')
  });

   // Check product price is present
   const priceElement = await page.locator('[data-cy="product-price"]');
   check(await priceElement.isVisible(), {
     'Product price is visible': (isVisible) => isVisible === true
   });

  // less important check
  await checkForRecommendedProducts(page, `Product page`);

  await Promise.all([
    page.locator(`//*[text()=" Add To Cart"]`).click(),
    page.waitForNavigation(),
  ]);

    // Check cart page
    check(await page.url(), {
      'URL changed to cart page': (url) => url.includes('/cart')
    });

  await page.locator("#email").fill("nathan@demo.com");
  await page.screenshot({ path: `./screenshots/order.png` });

  await Promise.all([
    page.locator(`//*[text()="Place Order"]`).click(),
    page.waitForNavigation(),
  ]);

    // Check order checkout page
    check(await page.url(), {
      'URL changed to order checkout page': (url) => url.includes('/checkout')
    });

  // less important check
  await checkForRecommendedProducts(page, `Order confirmation`);

  await page.close();
}

async function checkForRecommendedProducts(page, step) {
  try {
    await page
      .locator(
        `[data-cy="recommendation-list"] [data-cy="product-card"]:first-of-type`
      )
      .waitFor({ timeout: TWO_SECONDS });
  } catch (e) {
    await page.screenshot({ path: `./screenshots/${step}.png` });
    fail(
      `Failed to find recommended products on ${step} page within ${TWO_SECONDS}ms timeout`
    );
  } finally {
    const cards = await page.$$(`[data-cy="product-card"]`);
    console.log(step, cards.length);

    check(
      cards.length,
      {
        "4 recommended products are displayed": (length) => length === 4,
      },
      {
        importance: LESS_IMPORTANT,
      }
    );
  }
}

Browser Metrics

> K6_BROWSER_HEADLESS=false k6 run demo1.js

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: demo1.js
        output: -

     scenarios: (100.00%) 1 scenario, 1 max VUs, 10m30s max duration (incl. graceful stop):
              * ui: 1 iterations shared among 1 VUs (maxDuration: 10m0s, gracefulStop: 30s)

INFO[0000] Result: 56                                    source=console
INFO[0004] Product page 4                                source=console
INFO[0006] Order confirmation 4                          source=console

     ✓ Homepage title is correct
     ✓ URL changed to products page
     ✓ URL changed to product detail page
     ✓ Product price is visible
     ✓ 4 recommended products are displayed
     ✓ URL changed to cart page
     ✓ URL changed to order checkout page

     browser_data_received.......: 12 MB   1.9 MB/s
     browser_data_sent...........: 158 kB  26 kB/s
     browser_http_req_duration...: avg=216.35ms min=49.64ms  med=209.6ms  max=392.39ms p(90)=242.57ms p(95)=300.16ms
   ✓ browser_http_req_failed.....: 2.94%   2 out of 68
     browser_web_vital_cls.......: avg=0.027803 min=0.027803 med=0.027803 max=0.027803 p(90)=0.027803 p(95)=0.027803
     browser_web_vital_fcp.......: avg=563ms    min=563ms    med=563ms    max=563ms    p(90)=563ms    p(95)=563ms   
     browser_web_vital_fid.......: avg=199.99µs min=199.99µs med=199.99µs max=199.99µs p(90)=199.99µs p(95)=199.99µs
   ✓ browser_web_vital_inp.......: avg=136ms    min=136ms    med=136ms    max=136ms    p(90)=136ms    p(95)=136ms   
   ✓ browser_web_vital_lcp.......: avg=1.16s    min=1.16s    med=1.16s    max=1.16s    p(90)=1.16s    p(95)=1.16s   
     browser_web_vital_ttfb......: avg=301.5ms  min=301.5ms  med=301.5ms  max=301.5ms  p(90)=301.5ms  p(95)=301.5ms 
   ✓ checks......................: 100.00% 8 out of 8
     data_received...............: 0 B     0 B/s
     data_sent...................: 0 B     0 B/s
     iteration_duration..........: avg=5.81s    min=5.81s    med=5.81s    max=5.81s    p(90)=5.81s    p(95)=5.81s   
     iterations..................: 1       0.164587/s
     vus.........................: 1       min=1       max=1
     vus_max.....................: 1       min=1       max=1


running (00m06.1s), 0/1 VUs, 1 complete and 0 interrupted iterations
ui   ✓ [======================================] 1 VUs  00m06.1s/10m0s  1/1 shared iters

Google Core Web Vitals,包括 CLS、FID、LCP等。這些其實我也沒很懂。
LCP(Largest Contentful Paint)大概是說主要內容加載,然後使用者多快能看到頁面主題。

FID(First Input Delay),使用者首次能互動的時間(能點擊/輸入)。

CLS (Cumulative Layout Shift)頁面視覺穩定性…我不懂。

INP(Interaction to Next Paint)複雜互動的回應速度。

FCP(First Contentful Paint)跟TTDB(Time to First Byte)

我個人主要是檢查 browser_http_req_failedchecks failed rate。Core Web Vitals 我也不怎懂 XD

此外,k6 的這些指標也能通過 OpenTelemetry 輸出到遠端的服務裡來儲存分析。

K6_OTEL_GRPC_EXPORTER_INSECURE=true K6_OTEL_METRIC_PREFIX=k6_ k6 run -o experimental-opentelemetry demo1.js

或是參考小弟出版的OpenTelemetry 入門指南︰建立全面可觀測性架構 Ch 13,透過 Prometheus remote write 寫進 Prometheus 中。

Hybrid Testing

Hybrid Performance Testing for Websites | k6

有用過 k6 scenarios 的開發者就知道,其實我們能再 scenarios 中定義多個 scenario 然後指定 exec 的函數。所以也能應用這樣的方式,同時對 UI 和 API 做不同目的的測試。這裡就用到另一種 constant-vus executor,所以你執行後會看到同時有3個瀏覽氣在執行。

import { browser } from "k6/browser"
import http from "k6/http"
import { check } from "https://jslib.k6.io/k6-utils/1.6.0/index.js"

const PRODUCT_IDS = __ENV.PRODUCT_IDS

const HAS_SOME_LEEWAY = `warn`
const SUPER_IMPORTANT_CHECK = `critical`
const LESS_IMPORTANT = `info`

export const options = {
  scenarios: {
    ui: {
      executor: "constant-vus",
      duration: "1m",
      vus: 3,
      options: {
        browser: {
          type: "chromium",
        },
      },
      exec: "checkoutCompletion",
    },
    "spike-api": {
      executor: "ramping-vus",
      startVUs: 0,
      stages: [
        { duration: "10s", target: 10 },
        { duration: "40s", target: 30 },
        { duration: "10s", target: 10 },
      ],
      gracefulRampDown: "10s",
      exec: "spikeApi",
    },
  },
  thresholds: {
    [`checks{importance:${SUPER_IMPORTANT_CHECK}}`]: ["rate==1.0"],
    [`checks{importance:${HAS_SOME_LEEWAY}}`]: ["rate>=0.95"],
    [`checks{importance:${LESS_IMPORTANT}}`]: ["rate>=0.9"],
  },
}

export function spikeApi() {
  const randomProduct =
    PRODUCT_IDS[Math.floor(Math.random() * PRODUCT_IDS.length)]
  const res = http.get(`https://otel-demo.field-eng.grafana.net/api/recommendations?productIds=${randomProduct}`)

  check(
    res,
    {
      "status code is 200": (r) => r.status === 200,
    },
    { importance: HAS_SOME_LEEWAY }
  )
}

export async function checkoutCompletion() {
  const context = await browser.newContext()
  const page = await context.newPage()

  await page.goto(`https://otel-demo.field-eng.grafana.net/`)
  await page.locator(`//*[text()="Go Shopping"]`).click()

  await Promise.all([
    page
      .locator(`//*[text()="Starsense Explorer Refractor Telescope"]`)
      .click(),
    page.waitForNavigation(),
  ])

  // less important check
  await checkForRecommendedProducts(page, `Product page`)

  await Promise.all([
    page.locator(`//*[text()=" Add To Cart"]`).click(),
    page.waitForNavigation(),
  ])

  // less important check
  await checkForRecommendedProducts(page, `Shipping form`)

  await Promise.all([
    page.locator(`//*[text()="Place Order"]`).click(),
    page.waitForNavigation(),
  ])

  // Super important check
  await check(
    page.locator(`h1`),
    {
      "Place order page was reached": async (lo) =>
        (await lo.textContent()) === "Your order is complete!",
    },
    { important: SUPER_IMPORTANT_CHECK }
  )

  // less important check
  await checkForRecommendedProducts(page, `Order confirmation`)
  await page.close()
}

const TWO_SECONDS = 2000

async function checkForRecommendedProducts(page, step) {
  try {
    await page
      .locator(
        `[data-cy="recommendation-list"] [data-cy="product-card"]:first-of-type`
      )
      .waitFor({ timeout: TWO_SECONDS })
  } catch (e) {
    await page.screenshot({ path: `./screenshots/${step}.png` })
  } finally {
    const cards = await page.$$(`[data-cy="product-card"]`)
    console.log(step, cards.length)

    check(
      cards.length,
      {
        "4 recommended products are displayed": (length) => length === 4,
      },
      {
        importance: LESS_IMPORTANT,
      }
    )
  }
}

能看到以下測試結果,我測試到一半時, server 其實就異常了。但無礙,因為是 hybrid testing,所以 Browser metrics 與 Http metrics 都有被計算出來了。這就是 Playwright 無法提供的,畢竟 k6 能提供很完整且強大的 metrics 資料以及各種負載測試類型的測試策略。

> K6_BROWSER_HEADLESS=false k6 run demo2.js -e PRODUCT_IDS=10

         /\      Grafana   /‾‾/  
    /\  /  \     |\  __   /  /   
   /  \/    \    | |/ /  /   ‾‾\ 
  /          \   |   (  |  (‾)  |
 / __________ \  |_|\_\  \_____/ 

     execution: local
        script: demo5.js
        output: -

     scenarios: (100.00%) 2 scenarios, 33 max VUs, 1m30s max duration (incl. graceful stop):
              * spike-api: Up to 30 looping VUs for 1m0s over 3 stages (gracefulRampDown: 10s, exec: spikeApi, gracefulStop: 30s)
              * ui: 3 looping VUs for 1m0s (exec: checkoutCompletion, gracefulStop: 30s)

INFO[0002] Product page 4                                source=console
INFO[0003] Shipping form 4                               source=console
INFO[0003] Order confirmation 4                          source=console
INFO[0004] Product page 4                                source=console
INFO[0004] Product page 4                                source=console
INFO[0005] Shipping form 4                               source=console
INFO[0005] Shipping form 4                               source=console
INFO[0005] Order confirmation 4                          source=console
INFO[0006] Order confirmation 4                          source=console
INFO[0008] Product page 4                                source=console
INFO[0008] Product page 4                                source=console
INFO[0010] Product page 0                                source=console
INFO[0011] Shipping form 4                               source=console
INFO[0012] Shipping form 4                               source=console
INFO[0012] Order confirmation 4                          source=console
INFO[0013] Order confirmation 4                          source=console
INFO[0015] Product page 4                                source=console
INFO[0016] Shipping form 4                               source=console
INFO[0016] Product page 4                                source=console
INFO[0017] Order confirmation 4                          source=console
INFO[0017] Shipping form 4                               source=console
INFO[0017] Order confirmation 4                          source=console
INFO[0021] Product page 4                                source=console
INFO[0022] Product page 4                                source=console
INFO[0022] Shipping form 4                               source=console
INFO[0022] Shipping form 4                               source=console
INFO[0022] Order confirmation 4                          source=console
INFO[0023] Order confirmation 4                          source=console
INFO[0026] Product page 4                                source=console
INFO[0026] Shipping form 4                               source=console
INFO[0027] Product page 4                                source=console
INFO[0027] Order confirmation 4                          source=console
INFO[0027] Shipping form 4                               source=console
INFO[0028] Order confirmation 4                          source=console
INFO[0029] Product page 4                                source=console
INFO[0030] Shipping form 4                               source=console
INFO[0031] Order confirmation 4                          source=console
INFO[0033] Product page 4                                source=console
INFO[0033] Shipping form 4                               source=console
INFO[0035] Order confirmation 4                          source=console
INFO[0035] Product page 4                                source=console
INFO[0035] Shipping form 4                               source=console
INFO[0036] Order confirmation 4                          source=console
ERRO[0038] Uncaught (in promise) waiting for navigation: timed out after 30s  executor=constant-vus scenario=ui
INFO[0041] Product page 0                                source=console
INFO[0041] Product page 0                                source=console
WARN[0069] Unexpected DevTools server error: context canceled  category="ExecutionContext:eval" elapsed="0 ms" source=browser
ERRO[0069] Uncaught (in promise) clicking on "//*[text()=\"Go Shopping\"]": timed out after 30s  executor=constant-vus scenario=ui
WARN[0071] Unexpected DevTools server error: context canceled  category="ExecutionContext:eval" elapsed="0 ms" source=browser
WARN[0071] Unexpected DevTools server error: context canceled  category="ExecutionContext:eval" elapsed="0 ms" source=browser
ERRO[0071] Uncaught (in promise) clicking on "//*[text()=\" Add To Cart\"]": timed out after 30s  executor=constant-vus scenario=ui
ERRO[0071] Uncaught (in promise) clicking on "//*[text()=\" Add To Cart\"]": timed out after 30s  executor=constant-vus scenario=ui

     ✗ status code is 200
      ↳  75% — ✓ 2059 / ✗ 671
     ✗ 4 recommended products are displayed
      ↳  93% — ✓ 42 / ✗ 3
     ✗ Place order page was reached
      ↳  78% — ✓ 11 / ✗ 3

     browser_data_received..........: 162 MB 2.3 MB/s
     browser_data_sent..............: 2.6 MB 36 kB/s
     browser_http_req_duration......: avg=243.14ms min=50µs     med=218.64ms max=2.5s     p(90)=308.08ms p(95)=390.47ms
     browser_http_req_failed........: 4.31%  48 out of 1113
     browser_web_vital_cls..........: avg=0.052306 min=0.026286 med=0.027803 max=0.23987  p(90)=0.11228  p(95)=0.23917 
     browser_web_vital_fcp..........: avg=592.86ms min=244ms    med=522.5ms  max=2.45s    p(90)=589.2ms  p(95)=663.5ms 
     browser_web_vital_fid..........: avg=1.78ms   min=199.99µs med=1.59ms   max=3.59ms   p(90)=3.43ms   p(95)=3.51ms  
     browser_web_vital_inp..........: avg=107.29ms min=48ms     med=96ms     max=216ms    p(90)=144ms    p(95)=177.59ms
     browser_web_vital_lcp..........: avg=1.15s    min=658.29ms med=1.2s     max=2.45s    p(90)=1.31s    p(95)=1.49s   
     browser_web_vital_ttfb.........: avg=349.74ms min=200.6ms  med=238.59ms max=2.41s    p(90)=293.4ms  p(95)=306.4ms 
     checks.........................: 75.72% 2112 out of 2789
     ✓ { importance:critical }......: 0.00%  0 out of 0
     ✓ { importance:info }..........: 93.33% 42 out of 45
     ✗ { importance:warn }..........: 75.42% 2059 out of 2730
     data_received..................: 5.9 MB 83 kB/s
     data_sent......................: 258 kB 3.6 kB/s
     http_req_blocked...............: avg=117.56µs min=200ns    med=491ns    max=18.19ms  p(90)=671ns    p(95)=772ns   
     http_req_connecting............: avg=32.99µs  min=0s       med=0s       max=3.6ms    p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=385.08ms min=196.1ms  med=246.84ms max=3.3s     p(90)=703.54ms p(95)=991.39ms
       { expected_response:true }...: avg=343.25ms min=208.15ms med=254.68ms max=2.42s    p(90)=536.31ms p(95)=709.43ms
     http_req_failed................: 24.57% 671 out of 2730
     http_req_receiving.............: avg=158.17µs min=11.23µs  med=137.2µs  max=8.76ms   p(90)=238.37µs p(95)=283.66µs
     http_req_sending...............: avg=40.71µs  min=11.82µs  med=38.86µs  max=163.39µs p(90)=56.8µs   p(95)=64.05µs 
     http_req_tls_handshaking.......: avg=79.32µs  min=0s       med=0s       max=14.87ms  p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=384.88ms min=195.99ms med=246.56ms max=3.3s     p(90)=703.43ms p(95)=991.29ms
     http_reqs......................: 2730   38.299487/s
     iteration_duration.............: avg=457.94ms min=196.26ms med=248.53ms max=36.02s   p(90)=725.73ms p(95)=1.18s   
     iterations.....................: 2748   38.552011/s
     vus............................: 2      min=2            max=32
     vus_max........................: 33     min=33           max=33


running (1m11.3s), 00/33 VUs, 2748 complete and 0 interrupted iterations
spike-api ✓ [======================================] 00/30 VUs  1m0s
ui        ✓ [======================================] 3 VUs      1m0s
ERRO[0071] thresholds on metrics 'checks{importance:warn}' have been crossed

References

k6 Browser examples

k6 JsLib

Grafana Blog - 5 tips to write better browser tests for performance testing and synthetic monitoring

Grafana Blog - Frontend vs. backend: How to plan your performance testing strategy

Grafana Blog - How to load test a website: A comprehensive guide

Youtube k6 - Browser Testing with k6

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read173
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read76
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read191
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read481
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts