Recording a transaction
A "transaction" is a standard Playwright test — no PandoraFMS import required. Three native constructs map to plugin behavior:
| You write | Becomes |
|---|---|
test.step('name', ...) |
a monitored phase (status + time) |
test.info().annotations.push({ type: 'pandora.metric', description: 'name=value' }) |
a custom metric module |
| a failing assertion | the test fails; a screenshot is captured automatically |
1. Record the flow with Playwright's recorder (codegen)
Playwright ships its own recorder, codegen: it opens a real browser, and every click, fill, and navigation you perform is turned into Playwright code in real time, plus a Pick Locator / Explore mode to test selectors against the live page. Official documentation: playwright.dev/docs/codegen-intro. General authoring reference: playwright.dev/docs/writing-tests.
On any machine with Node and Playwright installed (this does not need to be the plugin's Docker image):
npm init playwright@latest # first time only, if the project isn't set up yet
npx playwright codegen https://your-app.example.com
Two windows open: the browser you interact with, and the Playwright Inspector, which shows the generated code live and lets you pick/copy a locator for any element on the page. Useful flags:
-
--browser=firefox/--browser=webkit— record against a specific engine (matches the plugin's_browser_setting). -
--viewport-size=1920,1080— record at the same resolution the plugin will run (matches_browserWidth_/_browserHeight_). -
--save-storage=state.json— capture cookies/localStorage after an interactive login, to seed later authenticated recordings with--load-storage=state.json(see Authentication in the official docs if the flow needs a persisted session).
codegen output is flat, ungrouped code — clicks and assertions one after another, with no test.step(...) and no metric annotations. It is a starting point, not the final transaction: copy it into your .ts file and go to step 2.
2. Turn it into phases
Wrap each meaningful part of the recorded flow in test.step('name', async () => { ... }). Every top-level test.step call — one written directly inside the test(...) callback — becomes one phase, with its own Phase <name> status and Phase <name> time module (see Agent and modules generated by the plugin). Official reference: test.step() API.
await test.step('open home', async () => {
await page.goto('https://your-app.example.com');
await expect(page).toHaveTitle(/Shop/);
});
Things to know:
-
Only top-level steps become phases. A
test.step(...)nested inside anothertest.step(...)is not reported as a separate phase — the plugin only reads the test's own top-levelstepsarray from Playwright's JSON reporter. Keep steps flat (one level) for anything you want to see as an independent phase in the console. -
Assertions belong inside the step, not after it —
expect(...)must run while the step is still open so a failure is attributed to that phase (and captured in its status/description), not to a later one or to the test as a whole. -
A phase with no assertion is only a timing box.
test.step('open home', async () => { await page.goto(...); })with noexpectwill basically always reportstatus = 1, since Playwright only marks a step failed when something inside it throws. Add at least one assertion per phase you actually want monitored, not just timed. -
Ordering and duration: the phase order in the console matches the order the steps run in;
Phase <name> timeis that step's own wall-clock duration, not cumulative.
3. Add custom metrics
Push a pandora.metric annotation with test.info() — from anywhere in the test body, including inside a test.step:
const count = await page.locator('.cart-count').innerText();
test.info().annotations.push({ type: 'pandora.metric', description: `cart_items=${count}` });
Official reference for annotations: test.info().annotations.
Parsing rules (exact, from the runner):
-
typemust be the literal stringpandora.metric; anything else is ignored. -
descriptionmust bename=value, split on the first=only — so a value containing=(e.g. a URL query string) is not truncated. -
nameandvalueare trimmed of surrounding whitespace. Ifdescriptionhas no=, ornameis empty after trimming, that annotation is silently skipped — no module, no error. - The module type is inferred from
value: parses as a number →generic_data; anything else →generic_data_string. - The module is named exactly
nameand taggedextra_data = pw:metric:<name>. - Push one annotation per metric name per test run. The annotation list is not deduplicated — pushing the same name twice in one run queues two modules with the same name/
extra_data, which is redundant at best and ambiguous for Pandora to reconcile at worst.
Full example
import { test, expect } from '@playwright/test';
test('checkout flow', async ({ page }) => {
await test.step('open home', async () => {
await page.goto('https://your-app.example.com');
await expect(page).toHaveTitle(/Shop/);
});
await test.step('login', async () => {
await page.fill('#user', 'demo');
await page.fill('#password', 'demo');
await page.click('#submit');
await expect(page.locator('.dashboard')).toBeVisible();
});
await test.step('add to cart', async () => {
await page.click('text=Add to cart');
const count = await page.locator('.cart-count').innerText();
test.info().annotations.push({ type: 'pandora.metric', description: `cart_items=${count}` });
});
});
Notes
-
Naming: module names come from
test.steptitles — keep them descriptive. Renaming a test starts a new agent. -
Continue after a failure: a normal
expectaborts the test on failure, so later phases do not run at all (they simply don't appear in that run). Useexpect.soft(locator)if every phase must be measured even when one fails. -
Multiple transactions: several
test(...)blocks in one.tsfile produce several agents. - Instead of hand-writing or manually recording the transaction, it can also be generated end-to-end by an AI coding agent that drives a real browser (via a Playwright/browser-automation tool) to validate every locator against the live target before handing over the file — catching ambiguous or strict-mode-violating selectors that plain
codegencannot.