Skip to content

Verify hybrid replay against PhoenixReplay 0.6.2, fix what it found; release 0.6.1 - #23

Merged
dannote merged 10 commits into
masterfrom
replay-verify
Oct 8, 2026
Merged

dannote merged 10 commits into
masterfrom
replay-verify

Conversation

@dannote

@dannote dannote commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

PhoenixVapor's hybrid replay checked against PhoenixReplay 0.6.2 for the first time with both installed, by recording real sessions of the demo in Chromium and replaying them in PhoenixReplay's player. That found four PhoenixVapor bugs, fixed here, then a first-paint comparison with Vue's server renderer found two more. Ends with the 0.6.1 release commit.

Includes #22's commit (phoenix_test_playwright 0.18, Playwright 1.63), which PhoenixReplay 0.6.x needs through playwright_ex ~> 0.14; merging this supersedes #22.

Replay fixes

  • Every step of a replay showed the session's first moment. The player renders each moment with __changed__: nil and diffs it with change tracking. The renderer then called Map.keys(nil), the frame crashed and restarted at the start. __changed__: nil now renders every slot, as in LiveView.
  • Recorded state named nowhere in Elixir code failed to replay on a server that didn't compile the component. useClipboard's copied is an example: its atom was made only while compiling. The render spec now carries every declared name's atom as a literal of the module. Tested in a fresh VM that never compiled the component.
  • A server action reached the recording before the state it was sent from. A name typed just before Save was recorded after the save. The bridge now flushes pending reports first.
  • Reports could catch a word halfway typed. They went out once per flush interval, and disagreed with PhoenixReplay's own record of the same input. They now wait for the replayer's debounce, as it records form controls, with flush as a ceiling. Re-recorded: both report the same value at the same moment.
  • Removed: translate_changed, the replay-only translation of __changed__, since PhoenixReplay renders replays in full.

First paint, compared with @vue/server-renderer

  • Reserved attributes: a static ref or key rendered as an attribute. vize 0.17.2 leaves both out.
  • <select v-model> showed no option selected. With vize 0.17.2's option slot, the option whose value, bound :value or text matches is now selected, or each one among them for multiple.
  • Still different: is="vue:name" isn't resolved to a component; that's upstream, is="vue:name" on a native element isn't resolved to a component ubugeeei-prod/vize#8328.

Demo

  • Setup: PhoenixReplay 0.6.2 as its README describes, with the recorder on :demo, /dev/replay in dev, PhoenixReplay.Plug, and replayRecorder in app.ts.
  • Test: a browser test records a session on /contacts and checks that the player's replay shows the typed search.
  • README: a section on what a replay shows and what it doesn't.

Also

  • A hybrid LiveView's client module is written again when it's missing. Compiling writes it, so a build restored from CI's cache beside a fresh checkout never wrote it, and building the demo's assets failed; this PR's own CI run found it. The LiveView defines __mix_recompile__?/0, true while the file is missing, and the demo's assets.build compiles first, as Phoenix's does.
  • The demo's test recordings go to tmp/replay_recordings, by a relative path.
  • CI: installs npm packages from the lock with --frozen. Plain mix npm.install re-resolves everything (mix npm.install re-resolves and rewrites npm.lock even when package.json is unchanged npm_ex#6), and moved Playwright to 1.64 mid-check.
  • Docs: the Hybrid guide's Session replay section, ARCHITECTURE and the CHANGELOG are updated for PhoenixReplay 0.6.
  • Upstream reports: PhoenixReplay #40 (a LiveView's layout) and #41 (charlists), both fixed in 0.6.2.

Release

Requires vize ~> 0.17.2. Release PhoenixVapor 0.6.1 sets the version and dates the CHANGELOG section; tagging and publishing follow the merge.

Checks: mix lint clean; mix test --include e2e 383 passed; demo mix ci lint clean, 23 passed.

dannote added 10 commits October 8, 2026 21:52
phoenix_test_playwright 0.14 failed every assertion with a bare
ArgumentError against Playwright 1.63, so the demo pinned Playwright
1.59.1. 0.18 works with it: update phoenix_test_playwright (with
phoenix_test 0.12 and playwright_ex 0.14) and unpin Playwright.
…ecords inputs

Checked against PhoenixReplay 0.6.1 by recording real sessions of the
demo and replaying them in its player, which found:

- Every step of a replay showed the session's first moment. The player
  renders each moment with __changed__: nil and diffs it with change
  tracking, where the renderer called Map.keys(nil), crashing the frame,
  which restarted. __changed__: nil now renders every slot, as in
  LiveView.
- A replay on a server that didn't compile the component failed for
  recorded state named nowhere in Elixir code, such as useClipboard's
  copied: Names.existing/1 found no atom. The render spec now carries
  every declared name's atom as a literal of the compiled module.
- A server action reached the recording before the state it was sent
  from; the bridge now flushes pending reports before sending one.
- Reports went out once per flush interval, so one could catch a word
  halfway typed, disagreeing with PhoenixReplay's own record of the
  input. They now wait for the replayer's debounce, as it records form
  controls, with the flush interval as a ceiling.

PhoenixReplay renders replays in full, so the replay-only translation of
__changed__ is removed. The guide names PhoenixReplay 0.6, links
Replay.View and replayRecorder, and covers what PhoenixReplay records on
its own.
mix npm.install re-resolves to the newest versions the ranges allow
(elixir-volt/npm_ex#6), so CI tested whatever the registry had that day:
Playwright 1.64 appeared and the local demo run had no browser for it.
CI now installs what npm.lock records, with --frozen, as npm_ex
recommends.
PhoenixReplay 0.6.1 set up as its README says: the recorder on the :demo
live session, the dashboard at /dev/replay behind :dev_routes,
PhoenixReplay.Plug, the user agent in connect_info, and replayParams,
replayMetadata and replayRecorder in app.ts. Tests record nothing except
a browser test that records a session on /contacts and checks that the
player's replay shows the typed search. The README says what a replay
shows and what it doesn't. The demo's CI installs npm packages from the
lock.
With vize_ex splitting each <option> of a <select v-model> into a model
slot, the renderer marks the option whose value, bound :value or text is
the select's selected, compared as Vue's looseEqual does, or each one
among them for <select multiple>. vize_ex also stops rendering a static
ref or key. Both match Vue's server renderer; they need the vize_ex
release with the option slot.
0.6.2 replays a LiveView inside the layout its layout: option names, so
the demo's navigation and flash messages now show in a replay, and shows
lists of integers as numbers. Both checked by recording sessions again.
PhoenixReplay's file storage resolves a relative path against the
working directory it started in, the demo's root in tests.
The client module is written while compiling, so with a build that is up
to date, such as one restored from a CI cache beside a fresh checkout,
it was never written again and building the assets failed. The LiveView
defines __mix_recompile__?/0, true while the file is missing. The demo's
assets.build compiles first, as Phoenix's does.
@dannote
dannote merged commit 8a465c1 into master Oct 8, 2026
4 checks passed
@dannote
dannote deleted the replay-verify branch October 8, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant