traffic-replay — captured traffic → non-regression conditions
Shift-right gap this closes: perf-check/security-surface/a11y-audit are active checks run
against a live app; nothing else in QAIA derives test conditions from real traffic that already
happened. It works the way every analysis skill in this plugin works: read an artifact the user
already has and report only what it shows — the same anti-fabrication discipline as ingestion
(honest recall beats fabricated recall: never assert more than the artifact supports), and the
same mask-before-write discipline, extended here from user-story text to HTTP traffic.
Reference fixture: fixture/ in this skill folder — a fully synthetic HAR
(demo-traffic.har) for a fictional app ("TaskFlow"), hand-built for this validation, never
real production data (the issue's own acceptance criterion). It deliberately contains an
Authorization header, a session cookie, a query-string token, an email, a phone number, a
full name and a card number, so the masking step has real sensitive-shaped values to catch.
See fixture/VALIDATION.md for the worked example and the grep proof that none of the
injected values leak into the output artifacts.
Input
- A HAR file (HAR 1.2) the user provides — exported from a browser's DevTools Network
panel, or an equivalent proxy/tool export the user already has. Never captured live by
this skill: no proxy, no MITM, no browser automation to generate new traffic, no network
call of any kind from the skill itself. If asked to "go capture what the app does," say
plainly that is out of scope and point at exporting a HAR from the browser, or
automate for
generating new test traffic.
- Optionally, a target US-ID if the user wants the derived conditions merged into that story's
manifest (see Output). Not required — captured traffic often spans more routes than one US.
Masking (blocking, before any write — the ingestion masking discipline, extended to HTTP traffic)
Applied to every request and response in the HAR before any finding, table, or JSON file
is written — never after the fact, never partially. This parses naturally as a short-lived,
in-session script (skills ship no runtime code: executable code is generated and run in the
user's session, thrown away, never distributed in the plugin) that deterministically applies the rules below; fixture/build-findings.py
is the actual script used to produce this skill's own validation evidence, kept here for
transparency, not as runtime code the plugin auto-executes.
| Category |
Rule |
Placeholder |
| Auth headers |
Authorization (any scheme), and any header name matching /auth|api.?key|x-.*-token|x-.*-secret/i |
[REDACTED:auth-header] |
| Cookies |
Cookie request header and Set-Cookie response header, in full — a session cookie is itself a bearer credential |
[REDACTED:cookie] |
| Query tokens |
Query-string parameter values whose key matches /token|key|secret|session|auth|password/i (the param name is kept — see grouping) |
[REDACTED:query-token] |
| Email |
Regex-matched email addresses anywhere in headers, query values, URLs, or bodies |
[REDACTED:email] |
| Phone |
Regex-matched phone numbers (international/local formats) in bodies |
[REDACTED:phone] |
| Card number |
Digit runs of 13-19 chars (separators stripped) that pass a Luhn checksum — a checksum beats a naive digit-count regex, same logic class oracle-generate's Luhn oracle encodes |
[REDACTED:card] |
| Name (heuristic) |
String values under JSON keys matching /name|assignee|author|owner|contact/i |
[REDACTED:name] |
| Password/secret/token body fields |
String values under JSON keys matching /password|passwd|pwd|secret|token|api.?key/i (a body carrying a login credential or a session/API token, even in a test/demo HAR, is never echoed — this is distinct from the header-level auth rule above, which only covers header values) |
[REDACTED:secret] |
- No redaction ledger (mirrors the ingestion rule exactly): never persist a mapping from an original value
to its placeholder. The only record kept is
type -> placeholder -> count per HAR.
- Independent second layer: the response-shape fingerprint (Method, step 4) records
keys and value types only, never values — so even a value the regex/heuristic list above
misses cannot reach that fingerprint. It can still reach a header value or a body the skill
quotes verbatim nowhere in its output (the skill never quotes raw bodies — see Output).
Method
- Parse the HAR's
log.entries[].
- Apply Masking (above) to every header, query value, and body value on the in-memory parsed
structure before touching anything else — never write an unmasked copy "for reference."
- Group entries into signatures:
method + path (no query string) + the sorted set of
query parameter names present (names only, never values). Two calls to /api/tasks/1 and
/api/tasks/2 are two different signatures — never collapsed into an inferred
/api/tasks/{id} template; that generalization is not something the HAR itself asserts, and
this skill does not make it on the user's behalf. Two calls to the same path with the same
query-param-name-set but different values are the same signature (repeat samples) — a
different param-name-set is a different signature (e.g. GET /api/tasks vs
GET /api/tasks?status&token are two signatures, not one).
- For each signature, derive a condition record:
- Observed status(es): exact HTTP status code(s) seen. A signature seen twice with two
different statuses reports both, split honestly (e.g. "200 in 3/4, 500 in 1/4") — never
averaged, never "the more common one" silently dropping the outlier.
- Response shape fingerprint: for a JSON body, top-level keys and their type
(
string/number/boolean/array/object/null) — values never included, masked or
not. For a non-JSON body: content-type and byte size only, stated as "not a JSON
structure, no fingerprint" rather than guessed.
- Significant headers:
content-type and any cache/security headers actually present
(cache-control, content-security-policy, x-content-type-options, etc.) — presence/
value as observed. Absence in this capture is reported as absence in this capture,
never generalized to "this app lacks X."
- Timing, only if the entry carries
time/timings: the observed total ms as a single
measured sample — explicitly not a p95/budget. A signature with N>1 samples reports each
observed time, never an average presented as a guarantee; point at perf-check for an
actual latency budget (needs repeated, controlled samples, not opportunistic capture).
- Sample count and an explicit
singleSample: true/false.
- Honesty on N=1 (honest recall, applied to traffic). A signature observed once is reported as
"1 sample observed" and documents only what happened that one time — never implies the
status/shape is guaranteed to recur. This is the default for most routes in any real HAR;
say so rather than quietly treating a single capture as a stable contract.
Output
- A findings table (Markdown) + the same data as JSON: one row per condition
(
@QAIA-TRAFFIC-<NNN> ID, method, path, query-param-name-set, sample count, observed
status(es), response shape fingerprint, significant headers, timing if present, single-sample
flag).
piiMasked: type -> placeholder -> count summary for the whole HAR (no ledger).
- If a US-ID is given/confirmed: merge into
.qaia/reports/<US-ID>/manifest.json under a
new trafficReplay section — same discipline flaky-detect already applies (shared output
contract, ../../OUTPUT-CONTRACT.md, rule 2 — merge, never clobber):
merge only this section, append to producers[], extend artifacts[], never touch
design/execution/gate/status. If no US-ID applies (traffic spans routes from
several stories, or none in particular), skip the manifest merge and say so explicitly —
forcing an arbitrary US-ID onto cross-cutting traffic would be a fabricated association, not
a real one; the findings still stand as a self-contained artifact.
- Never a
gate verdict — this skill only surfaces evidence (contract rule 3, same as every
producer in this plugin).
Not a test book — complementary to testbook-generate / automate
This skill's output describes what actually happened on the routes the HAR captured — an
evidentiary record, not a coverage claim. It does not replace qaia-core:testbook-generate
(derives what should be tested from a user story's acceptance criteria) or
qaia-playwright:automate (turns a test book into runnable tests). A route that never appears
in the HAR is not "covered" by this skill in any sense — silence in the traffic is not evidence
of anything, and the output says so rather than implying completeness. Where a derived
condition looks worth locking in as a real regression check, the human's next step is to hand
it to automate or write it as a Playwright request test by hand — this skill neither
generates nor runs that test itself.
Steps
- Get the HAR path from the user; confirm it is a user-provided export, not something the
skill is being asked to go capture.
- Parse
log.entries[]; apply Masking (blocking) before deriving anything else.
- Apply the Method above; produce the findings table + JSON.
- Ask whether to merge into a US-ID's manifest; do so per Output's rule, or skip and say why.
- Present the findings. No test file is generated or run by this skill.
Guardrails
- Never capture live traffic. No proxy, no MITM, no browser automation to generate new
requests, no network call of any kind from this skill. Input is a HAR the user already has —
full stop. Refuse and explain if asked to "watch the app and capture what happens."
- Mask before any write, blocking, no exceptions, no ledger (the ingestion masking rule,
extended to HTTP traffic) — see Masking above. Applies to headers, query values, and bodies in both requests
and responses; the response-shape fingerprint (keys/types only) is an independent second
safety layer on top of the regex/heuristic list.
- Never generalize beyond the sample. No path-template inference, no averaging across
differing statuses, no claim that an absent header means "insecure in general" — the report
says what this capture showed, never what is structurally guaranteed true of the app.
- No auto-replay, no auto-generated test execution. A condition derived here is a candidate
for a human (or a separate, explicit
automate run) to turn into an actual test — this skill
never sends a request, never runs anything against the app that produced the HAR or any other
target.
- Known masking limitations, stated not hidden:
- Name detection is heuristic and key-based only (
name/assignee/author/owner/
contact JSON keys) — a personal name sitting in an unrelated free-text field (a notes or
description string) is not guaranteed to be caught.
- Card-number detection (Luhn checksum + length) can rarely admit a non-card 13-19-digit
number that happens to pass Luhn, or miss a real one stored with unusual separators.
- National ID / SSN formats are locale-specific and not covered by a generic pattern in
v1 — if the traffic is known to carry them, redact the HAR before handing it to this skill.
- These gaps are reported in the findings output itself, not just here — never imply full PII
coverage.
- HAR only, nothing ever contacted. Mirrors
security-surface/perf-check's
"self-hosted/authorized targets only" rule in spirit; this skill goes one step further and contacts
no target at all, live or otherwise.
1---2name: traffic-replay3description: Ingest a user-provided HAR file (or equivalent captured HTTP traffic export) and derive non-regression test conditions -- observed status, response shape, significant headers, timing -- from real request/response pairs, with mandatory PII/secret masking before any write. Never captures live traffic, never runs a proxy, never sends a network request itself. Use when the user has a HAR export and wants what actually happened turned into regression conditions, not a coverage test book.4---56# traffic-replay — captured traffic → non-regression conditions78Shift-right gap this closes: `perf-check`/`security-surface`/`a11y-audit` are active checks run9against a live app; nothing else in QAIA derives test conditions from **real traffic that already10happened**. It works the way every analysis skill in this plugin works: read an artifact the user11already has and report only what it shows — the same anti-fabrication discipline as ingestion12(honest recall beats fabricated recall: never assert more than the artifact supports), and the13same mask-before-write discipline, extended here from user-story text to HTTP traffic.1415Reference fixture: `fixture/` in this skill folder — a fully **synthetic** HAR16(`demo-traffic.har`) for a fictional app ("TaskFlow"), hand-built for this validation, never17real production data (the issue's own acceptance criterion). It deliberately contains an18`Authorization` header, a session cookie, a query-string token, an email, a phone number, a19full name and a card number, so the masking step has real sensitive-shaped values to catch.20See `fixture/VALIDATION.md` for the worked example and the grep proof that none of the21injected values leak into the output artifacts.2223## Input2425- A **HAR file (HAR 1.2)** the user provides — exported from a browser's DevTools Network26 panel, or an equivalent proxy/tool export the user already has. **Never captured live by27 this skill**: no proxy, no MITM, no browser automation to *generate* new traffic, no network28 call of any kind from the skill itself. If asked to "go capture what the app does," say29 plainly that is out of scope and point at exporting a HAR from the browser, or `automate` for30 generating new test traffic.31- Optionally, a target US-ID if the user wants the derived conditions merged into that story's32 manifest (see Output). Not required — captured traffic often spans more routes than one US.3334## Masking (blocking, before any write — the ingestion masking discipline, extended to HTTP traffic)3536Applied to **every** request and response in the HAR before any finding, table, or JSON file37is written — never after the fact, never partially. This parses naturally as a short-lived,38in-session script (skills ship no runtime code: executable code is generated and run in the39user's session, thrown away, never distributed in the plugin) that deterministically applies the rules below; `fixture/build-findings.py`40is the actual script used to produce this skill's own validation evidence, kept here for41transparency, not as runtime code the plugin auto-executes.4243| Category | Rule | Placeholder |44|---|---|---|45| Auth headers | `Authorization` (any scheme), and any header name matching `/auth\|api.?key\|x-.*-token\|x-.*-secret/i` | `[REDACTED:auth-header]` |46| Cookies | `Cookie` request header and `Set-Cookie` response header, in full — a session cookie is itself a bearer credential | `[REDACTED:cookie]` |47| Query tokens | Query-string parameter **values** whose key matches `/token\|key\|secret\|session\|auth\|password/i` (the param *name* is kept — see grouping) | `[REDACTED:query-token]` |48| Email | Regex-matched email addresses anywhere in headers, query values, URLs, or bodies | `[REDACTED:email]` |49| Phone | Regex-matched phone numbers (international/local formats) in bodies | `[REDACTED:phone]` |50| Card number | Digit runs of 13-19 chars (separators stripped) that pass a **Luhn checksum** — a checksum beats a naive digit-count regex, same logic class `oracle-generate`'s Luhn oracle encodes | `[REDACTED:card]` |51| Name (heuristic) | String values under JSON keys matching `/name\|assignee\|author\|owner\|contact/i` | `[REDACTED:name]` |52| Password/secret/token body fields | String values under JSON keys matching `/password\|passwd\|pwd\|secret\|token\|api.?key/i` (a body carrying a login credential or a session/API token, even in a test/demo HAR, is never echoed — this is distinct from the header-level auth rule above, which only covers header values) | `[REDACTED:secret]` |5354- **No redaction ledger** (mirrors the ingestion rule exactly): never persist a mapping from an original value55 to its placeholder. The only record kept is `type -> placeholder -> count` per HAR.56- **Independent second layer**: the response-shape fingerprint (Method, step 4) records57 **keys and value types only, never values** — so even a value the regex/heuristic list above58 misses cannot reach that fingerprint. It can still reach a header value or a body the skill59 quotes verbatim nowhere in its output (the skill never quotes raw bodies — see Output).6061## Method62631. Parse the HAR's `log.entries[]`.642. Apply Masking (above) to every header, query value, and body value on the in-memory parsed65 structure before touching anything else — never write an unmasked copy "for reference."663. Group entries into **signatures**: `method` + `path` (no query string) + the **sorted set of67 query parameter names present** (names only, never values). Two calls to `/api/tasks/1` and68 `/api/tasks/2` are two different signatures — **never** collapsed into an inferred69 `/api/tasks/{id}` template; that generalization is not something the HAR itself asserts, and70 this skill does not make it on the user's behalf. Two calls to the same path with the same71 query-param-name-set but different values are the **same** signature (repeat samples) — a72 different param-name-set is a different signature (e.g. `GET /api/tasks` vs73 `GET /api/tasks?status&token` are two signatures, not one).744. For each signature, derive a condition record:75 - **Observed status(es)**: exact HTTP status code(s) seen. A signature seen twice with two76 different statuses reports both, split honestly (e.g. "200 in 3/4, 500 in 1/4") — never77 averaged, never "the more common one" silently dropping the outlier.78 - **Response shape fingerprint**: for a JSON body, top-level keys and their type79 (`string`/`number`/`boolean`/`array`/`object`/`null`) — values never included, masked or80 not. For a non-JSON body: `content-type` and byte size only, stated as "not a JSON81 structure, no fingerprint" rather than guessed.82 - **Significant headers**: `content-type` and any cache/security headers actually present83 (`cache-control`, `content-security-policy`, `x-content-type-options`, etc.) — presence/84 value as observed. Absence in this capture is reported as absence **in this capture**,85 never generalized to "this app lacks X."86 - **Timing**, only if the entry carries `time`/`timings`: the observed total ms as a single87 measured sample — explicitly not a p95/budget. A signature with N>1 samples reports each88 observed time, never an average presented as a guarantee; point at `perf-check` for an89 actual latency budget (needs repeated, controlled samples, not opportunistic capture).90 - **Sample count** and an explicit `singleSample: true/false`.915. **Honesty on N=1 (honest recall, applied to traffic).** A signature observed once is reported as92 "1 sample observed" and documents only what happened that one time — never implies the93 status/shape is guaranteed to recur. This is the default for most routes in any real HAR;94 say so rather than quietly treating a single capture as a stable contract.9596## Output9798- A findings table (Markdown) + the same data as JSON: one row per condition99 (`@QAIA-TRAFFIC-<NNN>` ID, method, path, query-param-name-set, sample count, observed100 status(es), response shape fingerprint, significant headers, timing if present, single-sample101 flag).102- `piiMasked`: `type -> placeholder -> count` summary for the whole HAR (no ledger).103- **If a US-ID is given/confirmed**: merge into `.qaia/reports/<US-ID>/manifest.json` under a104 new `trafficReplay` section — same discipline `flaky-detect` already applies (shared output105 contract, `../../OUTPUT-CONTRACT.md`, rule 2 — merge, never clobber):106 merge only this section, append to `producers[]`, extend `artifacts[]`, never touch107 `design`/`execution`/`gate`/`status`. **If no US-ID applies** (traffic spans routes from108 several stories, or none in particular), skip the manifest merge and say so explicitly —109 forcing an arbitrary US-ID onto cross-cutting traffic would be a fabricated association, not110 a real one; the findings still stand as a self-contained artifact.111- Never a `gate` verdict — this skill only surfaces evidence (contract rule 3, same as every112 producer in this plugin).113114## Not a test book — complementary to `testbook-generate` / `automate`115116This skill's output describes **what actually happened** on the routes the HAR captured — an117evidentiary record, not a coverage claim. It does not replace `qaia-core:testbook-generate`118(derives *what should be tested* from a user story's acceptance criteria) or119`qaia-playwright:automate` (turns a test book into runnable tests). A route that never appears120in the HAR is not "covered" by this skill in any sense — silence in the traffic is not evidence121of anything, and the output says so rather than implying completeness. Where a derived122condition looks worth locking in as a real regression check, the human's next step is to hand123it to `automate` or write it as a Playwright request test by hand — this skill neither124generates nor runs that test itself.125126## Steps1271281. Get the HAR path from the user; confirm it is a user-provided export, not something the129 skill is being asked to go capture.1302. Parse `log.entries[]`; apply Masking (blocking) before deriving anything else.1313. Apply the Method above; produce the findings table + JSON.1324. Ask whether to merge into a US-ID's manifest; do so per Output's rule, or skip and say why.1335. Present the findings. No test file is generated or run by this skill.134135## Guardrails136137- **Never capture live traffic.** No proxy, no MITM, no browser automation to generate new138 requests, no network call of any kind from this skill. Input is a HAR the user already has —139 full stop. Refuse and explain if asked to "watch the app and capture what happens."140- **Mask before any write, blocking, no exceptions, no ledger** (the ingestion masking rule,141 extended to HTTP traffic) — see Masking above. Applies to headers, query values, and bodies in both requests142 and responses; the response-shape fingerprint (keys/types only) is an independent second143 safety layer on top of the regex/heuristic list.144- **Never generalize beyond the sample.** No path-template inference, no averaging across145 differing statuses, no claim that an absent header means "insecure in general" — the report146 says what this capture showed, never what is structurally guaranteed true of the app.147- **No auto-replay, no auto-generated test execution.** A condition derived here is a candidate148 for a human (or a separate, explicit `automate` run) to turn into an actual test — this skill149 never sends a request, never runs anything against the app that produced the HAR or any other150 target.151- **Known masking limitations, stated not hidden**:152 - Name detection is heuristic and **key-based only** (`name`/`assignee`/`author`/`owner`/153 `contact` JSON keys) — a personal name sitting in an unrelated free-text field (a notes or154 description string) is **not** guaranteed to be caught.155 - Card-number detection (Luhn checksum + length) can rarely admit a non-card 13-19-digit156 number that happens to pass Luhn, or miss a real one stored with unusual separators.157 - National ID / SSN formats are locale-specific and **not** covered by a generic pattern in158 v1 — if the traffic is known to carry them, redact the HAR before handing it to this skill.159 - These gaps are reported in the findings output itself, not just here — never imply full PII160 coverage.161- **HAR only, nothing ever contacted.** Mirrors `security-surface`/`perf-check`'s162 "self-hosted/authorized targets only" rule in spirit; this skill goes one step further and contacts163 no target at all, live or otherwise.