Writing user flows

DevTools Recorder exports

Record a flow in Chrome DevTools, export it as JSON and replay it with measurement steps.

The Chrome DevTools Recorder records clicks, inputs and navigations as you use a page and exports them as a JSON file. user-flow can replay such a recording as a Lighthouse user flow and lets you add measurement steps directly to the JSON. The result is a flow without a single line of hand-written interaction code.

If the Recorder is new to you, these resources cover the basics:

1. Record and export#

  1. Open DevTools, switch to the Recorder panel and start a new recording for your page.

    Start a new recording in the DevTools Recorder

  2. Perform the interactions you want to measure, then stop the recording.

  3. Export the recording as a JSON file.

    Export the recording as JSON

  4. Store the file in your project, for example under recordings:

    ๐Ÿ“ฆ my-app
     โ”ฃ ๐Ÿ“‚ src
     โ”ƒ โ”— ๐Ÿ“‚ my-app-user-flows
     โ”ƒ   โ”ฃ ๐Ÿ“‚ user-flows
     โ”ƒ   โ”— ๐Ÿ“‚ recordings
     โ”ƒ     โ”— ๐Ÿ“„ order-coffee.replay.json
     โ”— ๐Ÿ“„ .user-flowrc.json

2. Replay it inside a flow#

createUserFlowRunner turns a recording into a runner that drives the page from the flow's context. Wrap the run in a timespan so Lighthouse measures the whole recording as one step:

user-flows/order-coffee.uf.mts
import { createUserFlowRunner, UserFlowContext, UserFlowInteractionsFn, UserFlowProvider } from '@push-based/user-flow';

const interactions: UserFlowInteractionsFn = async (ctx: UserFlowContext): Promise<any> => {
  const { flow } = ctx;

  await flow.startTimespan({ name: 'Checkout order' });

  const runner = await createUserFlowRunner('./recordings/order-coffee.replay.json', ctx);
  await runner.run();

  await flow.endTimespan();
};

export default {
  flowOptions: { name: 'Order Coffee' },
  interactions,
} satisfies UserFlowProvider;

Run npx user-flow and the report shows a single timespan covering the entire recording:

Report of a replayed recording wrapped in one timespan

3. Add measurement steps to the recording#

One timespan over everything is a coarse measurement. To see each interaction on its own, add measurement steps to the recording's steps array. user-flow understands the following step types in addition to the regular Recorder steps:

TypeLighthouse call
startNavigation / endNavigationflow.startNavigation() / flow.endNavigation()
startTimespan / endTimespanflow.startTimespan() / flow.endTimespan()
snapshotflow.snapshot()
recordings/order-coffee.replay.json
{
  "title": "Order coffee",
  "steps": [
    { "type": "startTimespan" },
    {
      "type": "click",
      "selectors": [["[data-test=Cappucino]"]],
      "offsetX": 10,
      "offsetY": 10
    },
    { "type": "endTimespan" },
    { "type": "snapshot" }
  ]
}

Because the recording now contains its own measurement steps, remove the surrounding timespan from the flow:

user-flows/order-coffee.uf.mts
const interactions: UserFlowInteractionsFn = async (ctx: UserFlowContext): Promise<any> => {
  const runner = await createUserFlowRunner('./recordings/order-coffee.replay.json', ctx);
  await runner.run();
};

Report of a recording with its own timespans and snapshots

4. Name the steps#

Measurement steps accept the same options as their Lighthouse counterparts through stepOptions. A name makes the report readable:

recordings/order-coffee.replay.json
{
  "steps": [
    {
      "type": "startTimespan",
      "stepOptions": { "name": "Select coffee" }
    },
    {
      "type": "click",
      "selectors": [["[data-test=Cappucino]"]],
      "offsetX": 10,
      "offsetY": 10
    },
    { "type": "endTimespan" },
    {
      "type": "snapshot",
      "stepOptions": { "name": "Coffee selected" }
    }
  ]
}

Report with named steps from the recording

Mixing recordings and code#

A recording and hand-written code are not mutually exclusive. A flow can navigate with flow.navigate, replay a recording for the tricky part of an interaction and finish with a snapshot written by hand. The runner uses the same page as the rest of the flow, so state carries over.

The repository contains complete examples under packages/user-flow-example/replay-examples together with the recordings they use.