Problem
The tool currently measures the time to receive an HTTP response body, which is correct for API endpoints.
For web pages, the user-perceived load time is longer: the browser must also fetch all the images,
stylesheets, and scripts referenced in the HTML before the page is visually complete.
There is no way today to:
- Detect that a step returned HTML and automatically fetch its embedded resources
- Record the time from request start to first usable paint (HTML received)
- Record the time from request start to full page load (all images loaded)
This blocks realistic end-to-end scenarios like:
steps:
- name: "Login — get JWT"
request:
method: POST
path: "/auth/login"
body: '{"username":"user","password":"pass"}'
extract:
- type: jsonPath
name: jwt_token
jsonPath: "$.access_token"
- name: "Load dashboard page"
request:
method: GET
path: "/dashboard"
headers:
Authorization: "Bearer ${jwt_token}"
simulateBrowser: true # ← new flag
assertions:
- type: pageFirstPaint
max: "1s"
- type: pageLoadComplete
max: "3s"
Proposed Solution
New step option: simulateBrowser: true
When set, the executor:
- Sends the primary request and records
page_first_paint when the HTML response body is received.
- Parses the HTML for sub-resources:
<img src="..."> — images (Phase 1)
<link rel="stylesheet" href="..."> — CSS (Phase 2, optional)
<script src="..."> — JS (Phase 2, optional)
- Fetches all sub-resources in parallel using the same HTTP client (respects
skipTlsVerify, custom headers, etc.). URLs are resolved relative to the page's base URL.
- Records
page_load_complete when the last sub-resource finishes (or times out).
- Reports both timings as step-level metrics and optionally in
GET /health.
Two new timing metrics per step
| Metric |
Browser equivalent |
Description |
page_first_paint_ms |
First Contentful Paint (FCP) |
Time from request start → HTML body received |
page_load_complete_ms |
Load event |
Time from request start → all sub-resources fetched |
These appear alongside the existing response_time metric in Prometheus and in assertion failures.
Two new assertion types
assertions:
- type: pageFirstPaint
max: "800ms"
- type: pageLoadComplete
max: "3s"
Sub-resource behaviour
- Sub-resources are fetched with the same bearer token / custom headers as the parent request (configurable via
inheritHeaders: true, default true).
- Sub-resource failures are counted separately (do not fail the step assertion by default; controlled via
failOnSubResourceError: false).
- The number of sub-resources fetched and the slowest sub-resource URL are logged at
debug level.
bodySize and simulateBrowser are mutually exclusive (validation error if both set).
Scope — Phase 1
- Images only (
<img src>, <source srcset>)
- Inline
<style> blocks ignored (no external fetch needed)
- No JS execution / DOM rendering — this is a network-level simulation, not a headless browser
Example use case (JWT login + page load)
version: "1.0"
config:
baseUrl: "https://app.example.com"
workers: 20
duration: "10m"
timeout: "30s"
load:
model: "rps"
target: 50
scenarios:
- name: "Login and load dashboard"
weight: 100
steps:
- name: "POST /auth/login"
request:
method: POST
path: "/auth/login"
body: '{"username":"loadtest","password":"secret"}'
headers:
Content-Type: "application/json"
extract:
- type: jsonPath
name: jwt_token
jsonPath: "$.access_token"
assertions:
- type: statusCode
expected: 200
- name: "GET /dashboard (with images)"
request:
method: GET
path: "/dashboard"
headers:
Authorization: "Bearer ${jwt_token}"
simulateBrowser: true
assertions:
- type: statusCode
expected: 200
- type: pageFirstPaint # HTML received
max: "800ms"
- type: pageLoadComplete # all images loaded
max: "3s"
Implementation Notes
- HTML parsing:
scraper crate (CSS selector–based, lightweight)
- URL resolution:
url crate (already a transitive dep via reqwest)
- Sub-resource fetching:
tokio::task::JoinSet for bounded parallel fetch
- The
page_first_paint_ms Prometheus histogram label matches existing per-step label scheme
simulateBrowser: false (default) — zero overhead for existing API tests
Acceptance Criteria
Problem
The tool currently measures the time to receive an HTTP response body, which is correct for API endpoints.
For web pages, the user-perceived load time is longer: the browser must also fetch all the images,
stylesheets, and scripts referenced in the HTML before the page is visually complete.
There is no way today to:
This blocks realistic end-to-end scenarios like:
Proposed Solution
New step option:
simulateBrowser: trueWhen set, the executor:
page_first_paintwhen the HTML response body is received.<img src="...">— images (Phase 1)<link rel="stylesheet" href="...">— CSS (Phase 2, optional)<script src="...">— JS (Phase 2, optional)skipTlsVerify, custom headers, etc.). URLs are resolved relative to the page's base URL.page_load_completewhen the last sub-resource finishes (or times out).GET /health.Two new timing metrics per step
page_first_paint_mspage_load_complete_msThese appear alongside the existing
response_timemetric in Prometheus and in assertion failures.Two new assertion types
Sub-resource behaviour
inheritHeaders: true, defaulttrue).failOnSubResourceError: false).debuglevel.bodySizeandsimulateBrowserare mutually exclusive (validation error if both set).Scope — Phase 1
<img src>,<source srcset>)<style>blocks ignored (no external fetch needed)Example use case (JWT login + page load)
Implementation Notes
scrapercrate (CSS selector–based, lightweight)urlcrate (already a transitive dep via reqwest)tokio::task::JoinSetfor bounded parallel fetchpage_first_paint_msPrometheus histogram label matches existing per-step label schemesimulateBrowser: false(default) — zero overhead for existing API testsAcceptance Criteria
simulateBrowser: trueon a step causes HTML to be parsed for<img>sub-resourcespage_load_completerecorded when all finishpageFirstPaintandpageLoadCompleteassertion types work and appear in failure outputstepandscenariolabelssimulateBrowser+bodySizeraises a validation error<img>tags; assertpage_load_complete > page_first_paint