Control · vocabulary.yaml
features: - id: members.invite.send surface: workspace.members name: "Send invite" matchers: { testid: [send-invite] } source: "members.tsx:11"
Stop watching replays. Spoiler reads them for you and tells you what each user was trying to do, and where they got stuck.
Try it outYour session replays already know how people use your product. Getting it out means hours of watching, or adding events everywhere. Spoiler turns every recording into tasks, steps and friction you can read, search and query.
From how your product works to how people use it.
spoilervocabbuild
Point it at your source files and a model drafts a YAML feature map: your pages, controls and domain rules, each citing the line it came from. Review it once, reuse it for every session.
spoilercompile
The rrweb recording becomes a trace: one line per action, matched to the feature map, with the server's answer and the timing. Plain code, no AI.
spoileranalyze
A model groups the trace into tasks and friction. Code checks every claim against the trace and drops what it can't back.
What it learns from your code.
vocab build reads the files that define your routes, controls and rules, and a model drafts the
map. Every entry cites its source line. What the sources leave open is listed as a gap, never guessed.
<invite.Form method="post" action="/api/invites"> <input name="email" type="email" data-testid="invite-email" placeholder="name@company.com" /> <button data-testid="send-invite">Send invite</button></invite.Form>
Control · vocabulary.yaml
features: - id: members.invite.send surface: workspace.members name: "Send invite" matchers: { testid: [send-invite] } source: "members.tsx:11"
export async function sendInvite(workspace: Workspace, email: string) { const seatsHeld = await db.member.count({ where: { workspaceId: workspace.id } }); if (seatsHeld >= workspace.seats) { return data({ error: `Couldn't send invite: all ${workspace.seats} seats are in use.` }, { status: 402 }); }
Rule · vocabulary.yaml
terms: - term: seat means: "Every member holds a seat until removed, deactivated members included. Invites fail with 402 while all seats are held." ui_labels: ["Couldn't send invite: all 5 seats are in use."] source: "members.server.ts:4"
Gap
Billing page: billing.tsx is not among the sources, so seat purchases are unnamed. What the sources leave open is listed, never guessed.
Every action, as one line.
compile replays the recording against the map: what was clicked, what the server answered, what
changed on screen, and friction like repeated errors. Plain code, so the same recording always gives the same
trace.
The trace · 22 s recording
| Time | Action | Result |
|---|---|---|
| 1.0 se2 | Typed "priya@example.com" into Invite email | |
| 3.0 se3 | Clicked Send invite | 402 “Couldn't send invite: all 5 seats are in use.” |
| 6.5 se5 | Clicked Send invite | 402 Same message |
| 10.6 se7 | Clicked Deactivate member for Ben Ortiz | 200 Ben Ortiz: Active → Deactivated |
| 13.1 se8 | Clicked Send invite | 402 Same message |
| 17.1 se10 | Clicked Remove member for Ben Ortiz | Asked “Remove Ben Ortiz?” |
| 19.6 se11 | Clicked Confirm remove | 200 Ben Ortiz removed |
| 22.1 se12 | Clicked Send invite | 201 “Invitation sent to priya@example.com” |
What they tried, and where they got stuck.
analyze hands the trace and the map to a model. Code then checks the answer against the trace:
every claim must cite the actions behind it, and whatever the trace can't back is dropped.
What Spoiler wrote
Task: Invite priya@example.com to the workspace.
Outcome: Finished with a workaround · 21.1 s active · 8 actions
Problem 1: Send invite failed three times with 402 and "Couldn't send invite: all 5 seats are in use." e3 e5 e8
Hypothesis: the message doesn't say that deactivated members still hold seats.
Problem 2: Deactivated Ben Ortiz, then retried Send invite, which failed the same way. e7 e8
Hypothesis: the admin expected deactivating a member to free a seat.
Dropped by the check
“Rage-clicked Send invite after the first refusal.” No cited action carries a rage flag.
Let your coding agent set it up, or install it yourself.
Use curl to read https://spoiler.sh/agents.md, then follow it to set up Spoiler in this project.
curl -fsSL https://spoiler.sh/install | sh
uv tool install spoiler
pip install spoiler
cargo install spoiler --locked
Then set it up in your project, or try it on a sample recording with no account or API key.