Files
openobserve/scripts
Omkar Kesarkhaneandgithub-actions[bot] ddb67f3efb fix(synthetics): record from a specific step, and stop silently downgrading recorded actions (#13799)
Supersedes #13777, which carried the record-from-step commits alone.
Closing that in favour of this, because the two changes land in the same
three files (`types/synthetics.ts`, `useSyntheticsRecorder.ts`,
`JourneySteps.vue`) and reviewing them apart means resolving the same
conflict twice.

## Problem

**A recorded hover was stored as a click.**

Playwright 1.56 added `hover` to the recorder's action model, reachable
from its "Choose action" picker. The extension's action mapper seeds
every step with `action: 'click'` as a placeholder for its switch to
overwrite, and returned that placeholder from `default:` — so any action
name the switch did not know became a click the author never performed.

On the usual hover target — a nav trigger, a dropdown opener — clicking
it can navigate or submit. The run still reports green.

There are three separate guards meant to stop exactly this, and none of
them fired, because the relabel happens upstream of all of them:

| Guard | Why it missed |
|---|---|
| `UNSUPPORTED_REPLAY_ACTIONS` (extension) | step already said `click` |
| `RETIRED_ACTIONS` → `isStorableAction` | step already said `click` |
| `V2_STEP_ACTIONS` (Rust validation) | `click` is valid |

The same picker offers **Right click** and **Double click** as explicit
choices. Both were flattened too: `click_count` was dropped by the
mapper and never existed downstream, and the probe's executor called
`locator.click({ timeout })` while reading neither `button` nor
`click_count`. A deliberately recorded right click replayed as a left
click; a double click as a single one.

**Recording could only start from scratch.** Fixing step 12 of a 15-step
journey meant re-recording all 15.

## Change

**Record from a specific step.** Each step gets a record action that
replays the prefix before it, then flips the recorder into insert mode
at that point. A restore that cannot reach the requested step reports
where it stopped (`prefixFailed`) rather than silently recording from
the wrong place.

**Every action Playwright can record is now stored and replayed as
itself.**

- `hover` is a first-class v2 action — mapped, stored, validated, and
executed by `Locator.hover` in the probe and `Frame.hover` in the
extension player.
- `button` and `click_count` travel to storage, so right click and
double click replay as recorded.
- An action name the mapper does not know now **throws** instead of
returning the `click` placeholder. This is the part that is not
hover-specific: upstream added one action in nine minors and will add
more, and the next one must be a deliberate decision rather than a
silent downgrade.

**The extension handshake reports capabilities, not just presence.**
`getStatus` now returns `extVersion` and a `capabilities` array; O2
gates affordances on `hasCapability()` with a defined pre-handshake
fallback, and warns when the installed build is older than the minimum.
Previously the protocol carried no version at all — Chrome updates the
extension independently of when O2 deploys, so the two could disagree
about the wire with no way to say so.

**Two dead message declarations, opposite problems.** `setSources` no
longer exists in playwright-core (sources arrive alongside the actions),
so its declared variant could never fire — while its `generatedCode`
field is exactly what an eject-to-code feature would reach for. Deleted.
`elementPicked` is the reverse: it fires on every "Pick locator" and
every click in inspecting mode, and was being discarded, which made
inspect mode a dead end. Now kept.

### Compatibility

`button` and `click_count` are optional with defined absent-behaviour —
absent means a single left click. Every journey stored today stays
valid, means the same thing, and serialises byte-identically. **No
schema version bump and no migration.** A test pins that rather than
leaving it as an intention.

`hover` is a new vocabulary entry with no default, so the probe must be
able to execute one before O2 is allowed to store one — the probe change
ships first.

## Implementation

| File | |
|---|---|
| `src/config/src/meta/synthetics.rs` | `hover` joins
`V2_STEP_ACTIONS`/`V2_ELEMENT_ACTIONS`; `button` and `click_count` added
to `BrowserStepV2` with validation (`left\|middle\|right`, `1..=3`) |
| `web/src/types/synthetics.ts` | `button`/`clickCount` on both step
types; `extVersion` + `capabilities` on `RecorderStatus`; `setSources`
variant deleted and `elementPicked` typed; `prefixFailed` and
insert-mode fields on `recordingStarted` |
| `web/src/utils/synthetics/mapRecordedStep.ts` | promotes
`button`/`clickCount` from the wire, like every other stored field |
| `web/src/utils/synthetics/buildV2Steps.ts` | `hover` in
`ACTION_TO_V2`; crosses camelCase → snake_case for the two click fields
|
| `web/src/constants/synthetics.ts` | `hover` out of `RETIRED_ACTIONS`;
`MIN_EXTENSION_VERSION` |
| `web/src/composables/useSyntheticsRecorder.ts` | `startRecordingFrom`,
`hasCapability`, `pickedSelector`, `extensionOutdated` |
|
`web/src/components/synthetics/journey/{BrowserJourney,JourneySteps}.vue`
| per-step record action and its wiring |

`click_count` is capped at 3: the recorder only ever emits 1 or 2, and
an unbounded count is a way to hold a page's event loop open.

The two click fields cross from camelCase to snake_case in `buildV2Step`
and nowhere else — reading `step.wire` directly inside it would have
worked and invented a second path into storage.

### Depends on a recorder extension build

`hover`, the click fields and the capability handshake all need the
matching extension build. O2 degrades honestly without it:
`hasCapability()` falls back to the pre-handshake set, and an older
build raises the outdated-extension warning instead of failing on a
message shape it does not recognise.

## Tests

- **Rust** — hover accepted as an element action; `button`/`click_count`
validated at both bounds; a step carrying neither still validates (the
compatibility guarantee). Two existing tests encoded "hover is rejected"
and now say the opposite. 101 pass.
- **Probe** — hover hovers rather than clicks; a right click sends the
recorded button; a double click sends two clicks; a journey stored
before these fields replays unchanged. The action union is asserted
against the Rust vocabulary, so the next divergence fails a test instead
of reaching an executor with no case for it. Also pinned: a selector
carrying a frame hop resolves into the iframe, which had no coverage
anywhere.
- **Web** — 1213 across 60 files. Nine existing tests encoded "hover is
retired" across constants, `buildV2Steps`, `buildPayload` and the
save-gate schema; each now states the opposite deliberately.
- **Extension** — the unmapped-action throw, hover round-tripping both
directions, and a double click surviving the round trip as two clicks.

`vue-tsc`, `cargo clippy -D warnings`, and `prettier --check` are clean.
The last commit is formatting only: the three record-from-step files
arrived failing `format:check`, which is a gate of its own and would not
have been caught by lint.

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-16 17:50:46 +00:00
..
2026-08-15 00:57:20 +00:00