Recording a Test Plan from Browser Traffic
The record command launches a real browser, captures the HTTP traffic while you browse your application, and turns it into a starting LPS test plan (.yaml or .json). It is the fastest way to bootstrap a plan: browse the flow you want to load-test, and LPS writes the requests for you to refine.
Recording is powered by Playwright using its native HAR capture. Because the browser hands LPS the already-decrypted traffic, no proxy and no root certificate installation are required.
The generated plan is a starting draft, not a finished test. Review it, parameterize secrets, and adjust the load shape before running at scale.
Prerequisites
The first time you use record, install the browser Playwright drives (a one-time ~150 MB download of Chromium):
lps record --install
Basic Usage
lps record app.yaml --url https://www.example.com
This opens a browser at the given URL. Browse the flow you want to test, then stop the recording (see below). LPS filters the captured traffic and writes the plan to app.yaml.
The output file is optional and defaults to RecordedPlan.yaml:
lps record
The file extension decides the format: use .yaml/.yml for YAML or .json for JSON.
Stopping the Recording
Recording stops on the first of these three signals:
| Signal | How |
|---|---|
| Close the browser | Close the browser window |
| Press Enter | Focus the terminal and press Enter |
| Cancel | Press Ctrl+C |
When recording stops, LPS reads the capture, applies your filters, and writes the plan once.
Options
| Option | Alias | Description | Default |
|---|---|---|---|
<output> | — | Output plan file (.yaml, .yml, or .json) | RecordedPlan.yaml |
--name | -n | Name for the generated plan (defaults to the output file name) | RecordedPlan |
--url | -u | Start URL to open when the browser launches | — |
--headless | — | Run the browser without a visible window | false |
--install | — | Install the Playwright Chromium browser and exit | false |
--prompt-files | -pf | After recording, ask for a local path for each captured file upload | false |
--append | -a | Append captured requests to the existing plan file instead of overwriting it | false |
--update | -up | Update matching requests (by method + URL + body) instead of adding duplicates; new requests are appended | false |
--round | -rn | Round for new requests (with --append or --update); also the round to create | Main |
--number-of-clients | -nc | Virtual clients per round in the generated plan | 1 |
--arrival-delay | -ad | Delay in milliseconds between client arrivals | 0 |
--run-in-parallel | -rip | Run the round's clients in parallel | false |
--request-count | -rc | Requests per client for each iteration (request-count mode) | 1 |
--duration | -d | Run each iteration for this many seconds (switches to duration mode) | — |
--only-host | -oh | Keep only requests to these host(s) | — |
--ignore-host | -ih | Ignore requests to these host(s) | — |
--ignore-path | -ip | Ignore requests whose URL path/query contains any of these substrings | — |
--ignore-content-type | -ict | Ignore responses with these content-type(s) | — |
--ignore-extension | -iext | Ignore URLs ending in these file extension(s) | — |
--ignore-method | -im | Ignore these HTTP method(s) | — |
--ignore-resource-type | -irt | Ignore these Playwright resource type(s) | — |
All --ignore-*, --only-host, and --ignore-host options accept multiple space-separated values, for example -irt image font media.
Shaping the load
By default the plan is generated with a single client and one request per iteration. Pass the load-shape options to bake in your own numbers, so you don't have to hand-edit the plan afterward:
lps record api.yaml -u https://www.example.com --only-host example.com --number-of-clients 5000 --arrival-delay 100 --run-in-parallel
Provide --duration <seconds> to generate duration-based iterations instead of request-count ones.
Appending to an Existing Plan
By default, record overwrites the output file. With --append, it merges the captured requests into an existing plan instead, so you can build a multi-flow plan across several sessions, each flow as its own round.
Use --round <name> to control where the requests land:
| Command | Result |
|---|---|
lps record plan.yaml | Overwrite the file; one round named Main |
lps record plan.yaml --round Login | Overwrite; the round is named Login |
lps record plan.yaml --append --round Checkout (round not in file) | Add a new round Checkout (load-shape flags apply to it) |
lps record plan.yaml --append --round Login (round already exists) | Add the captured requests into the existing Login round |
lps record plan.yaml --append (no --round) | Add a new auto-named round (Round2, Round3, …) |
Example: build a plan flow by flow
lps record app.yaml --round Login --only-host myapp.com
lps record app.yaml --append --round Checkout --only-host myapp.com --number-of-clients 500
lps record app.yaml --append --round Login --only-host myapp.com # adds more requests to Login
Notes:
- Iteration name collisions are resolved automatically (
GET_products→GET_products_2). - When you append into an existing round, that round keeps its own
numberOfClients/arrivalDelay/runInParallel. The load-shape flags are ignored (with a warning), since they would only reshape a round you are adding to. - If
--appendor--updateis set but the existing file cannot be parsed, LPS aborts rather than overwriting it. (An empty or blank file is treated as a fresh start.)
Updating an Existing Plan
--append always adds the captured requests, which can create duplicates if you re-record a flow that revisits the same endpoints. --update instead upserts: if a captured request already exists in the plan, it is refreshed in place; only genuinely new requests are added.
lps record app.yaml --update --only-host myapp.com
A request is considered "the same" when its method, URL, and body all match. So GET /products/1 recorded twice updates one iteration, while two POST /graphql calls with different query bodies stay separate (they are different operations).
| Recorded request | With --append | With --update |
|---|---|---|
| Already in the plan | Added again (duplicate) | Existing iteration refreshed in place |
| New | Added | Added |
| Recorded twice in one session | Both kept | Collapsed to one |
An update keeps the matched iteration's:
- Name, so references and edits you made survive.
- Load shape (
mode,requestCount,duration) and any failure/termination rules. Only the captured request (URL, method, headers, body) is refreshed.
New (unmatched) requests are added to the target round: --round if given, otherwise the first existing round, otherwise a new Main round. Because the match is plan-wide, an existing request is updated wherever it lives, even if it sits in a different round than the target.
Round-level load-shape flags (
--number-of-clients,--arrival-delay,--run-in-parallel) are ignored in--updatemode (with a warning). Updating refreshes requests, not the load shape. If both--appendand--updateare given,--updatewins.
Example: refresh a plan after the app changed
# Original capture
lps record app.yaml --only-host myapp.com
# Later, the app added an endpoint and changed a response - refresh without duplicating
lps record app.yaml --update --only-host myapp.com
Existing requests are refreshed in place, the new endpoint is appended, and your tuned numberOfClients and rules are left intact.
Filtering Out Noise
A few minutes of browsing can generate hundreds of requests: static assets, analytics, and CDN infrastructure. LPS removes most of this for you and lets you trim the rest.
Built-in defaults
Out of the box, LPS drops:
- Static assets by resource type (
image,stylesheet,font,media,manifest) and by extension (.css,.js,.png,.svg,.woff2,.map, …). - Infrastructure paths: anything containing
/cdn-cgi/(Cloudflare challenge/RUM traffic).
Your --ignore-* values are added on top of these defaults.
The most useful filter: --only-host
Third-party analytics and tracking live on other hosts (e.g. analytics.example.com, j.clarity.ms). An allowlist drops them all in one shot:
lps record api.yaml --url https://www.example.com --only-host example.com
Only requests whose host exactly matches are kept, so analytics.example.com and third-party domains are excluded. List every host you want to keep:
lps record api.yaml -u https://www.example.com --only-host example.com api.example.com
--only-hostand--ignore-hostalso accept full URLs:--only-host https://example.com/is normalized to the hostexample.com.
Which signal each filter reads
| Filter | Reads from |
|---|---|
--ignore-method | Request method |
--ignore-extension | URL path |
--ignore-content-type | Response content-type |
--ignore-resource-type | Playwright resource type (document, xhr, fetch, image, …) |
--only-host / --ignore-host | Request host (exact match) |
--ignore-path | URL path/query substring |
Examples
Keep only your API calls and drop CORS preflights:
lps record api.yaml -u https://www.example.com --only-host example.com --ignore-method OPTIONS
Deny specific trackers without an allowlist, and strip infrastructure paths:
lps record api.yaml -u https://www.example.com -ih j.clarity.ms google-analytics.com -ip /cdn-cgi/ /collect
If everything is filtered out, LPS writes nothing and reports how many requests were captured versus kept. Check your filters if you see that message.
What Gets Generated
Each kept request becomes one iteration inside a single round:
name: RecordedPlan
rounds:
- name: Main
numberOfClients: 1
arrivalDelay: 0
iterations:
- name: GET_products
httpRequest:
url: https://api.example.com/products
method: GET
headers:
accept: application/json
mode: R
requestCount: 1
- name: POST_login
httpRequest:
url: https://api.example.com/login
method: POST
headers:
content-type: application/json
payload:
raw: '{"user":"demo","password":"..."}'
mode: R
requestCount: 1
Notes on the output:
- Volatile and pseudo-headers (
host,content-length,connection, HTTP/2:authority, …) are dropped. - Load-shape fields (
numberOfClients,arrivalDelay,mode,requestCount) are kept with their defaults so they are easy to tune. ChangenumberOfClients: 1to the load you want. - Rarely-edited transport defaults (
httpVersion,supportH2C) are omitted for clarity.
Request Bodies and File Uploads
Request bodies are mapped by content type:
Request Content-Type | Generated payload |
|---|---|
application/json, text/*, application/x-www-form-urlencoded | raw |
multipart/form-data | multipart with fields and files |
For multipart forms, text fields are captured in full. Uploaded file bytes are not captured: the browser only exposes the file name, so each file part is written with a placeholder path for you to fill in:
payload:
multipart:
fields:
- name: title
value: My Photo
files:
- name: avatar
contentType: image/png
path: files/me.png # placeholder - set this to a real local file
This is usually what you want anyway: load tests should point at a controlled test file, not whatever was uploaded while browsing.
--prompt-files
To fill the paths interactively at the end of a recording, add --prompt-files:
lps record upload.yaml -u https://www.example.com --prompt-files
1 file upload(s) were captured. Enter a local path for each (blank keeps the placeholder):
[POST_upload] field 'avatar' (was 'me.png'): C:\test-data\avatar.png
Without the flag, LPS prints a summary of every file field that still needs a path.
Running the Recorded Plan
Once you have reviewed the plan and filled in any file paths, run it like any other LPS plan:
lps run app.yaml
Tune the round before running at scale, for example set numberOfClients, arrivalDelay, and iteration mode/requestCount to shape the load.
Tips and Limitations
- Review captured headers before sharing. Recorded requests include cookies and
Authorizationheaders exactly as sent. Treat the generated plan as sensitive, and replace secrets with variables before committing it. - Correlation is not automatic yet. Dynamic values (CSRF tokens, session IDs) captured from one response are not automatically wired into later requests. Use
capturetogether with$find/$findxmlto correlate them by hand. - Re-filtering means re-recording. Filters are applied while generating the plan; to change them, run
recordagain. - Chromium only. Recording uses the Chromium browser provisioned by
--install.