# Friction and the journey

Squiggle watches what happens after each tap. When people tap the same spot again and again and nothing changes, it reports where, on which control, and on which screen of their journey.

## What counts as friction

Four detectors look for friction. Rage taps and dead zones need three or more taps on the same spot within three seconds.

| Finding | What happened |
| --- | --- |
| Rage tap | The control reacted, by highlighting or rendering, but nothing changed: a button that looks like it works and doesn’t. Read these first. |
| Dead zone | Nothing reacted at all. People expect a control where there isn’t one, or the control ignores touches. |
| Confusion | Four or more taps on a screen, then the person went back. They couldn’t find what they came for. |
| Abandoned | A drag set off to dismiss, refresh, reorder or scrub on a spot where that works, and nothing happened: the sheet sprang back, the pull refreshed nothing, the row went back where it was. It counts only on a spot where the same drag worked at least once in the session, so a list with no pull to refresh never reports one. |

A finding names the control: its component and its testID, role or label, such as `Pressable “Apply coupon”`, with the components around it. For a dead zone on a simulator Studio launched, it names what the accessibility tree has under the tap, and says so when it can’t tell.

## Which touches count

Every control counts: `Pressable`, tab bars, gesture-handler buttons and empty space. A touch counts as a tap only if the finger lifts where it went down, so scrolling and dragging never look like a rage tap. A press on a gesture-handler control is counted once.

To keep touches for the heatmap only, turn the detectors off:

```ts
<Squiggle navigationRef={ref} touchDetectors={false}>
  <Stack />
</Squiggle>
```

## The Analytics screen

Studio finds friction in the background while your app is connected, whether or not this screen is open. Open it to read a run:

- **Errors**: each JS error and crash, with its stack and the taps and screens that led up to it. An error that made the app reload is still here afterwards.
- **UX insights**: the rage taps, dead zones, confusion and abandoned drags from this run.
- **The journey**: the screens the run went through, in order, each marked with how it was reached and how many findings it has. Click a screen to show only what happened there.

Pick **Live** or an earlier run from the run menu. Flows you run count too, so a run nobody watched still has its findings.

## Across sessions

One session is one person’s bad afternoon. To see whether a spot fails for everyone, roll friction up across sessions:

```shell
squiggle product friction --last 20
```

Output:

```text
friction by screen 1 screen(s)
  index  ████████████████████  4  4 rage · 0 dead · 0 confusion · 0 abandoned · 2 session(s)

WORST SPOTS
  ▌ rage-tap ScreenStackItem — 3 tap(s) across 1 session(s) on index
  ▌ session mv6prs4i-1 · event 1121
```

Each spot names one session and event to start from. Studio shows the same rollup on the Product screen; see [Product analytics](https://squiggle.sh/docs/product). Your agent reads it with `product_friction`, and one session’s findings with `perf_insights`.

Friction is found the same way on a [recording](https://squiggle.sh/docs/replay), so you can look at a session you captured last week.

## Find the state friction happens in

If your app sends [state samples](https://squiggle.sh/docs/reference/sdk), Squiggle can tell you what the app was holding when friction happened. Run `squiggle product friction --state`. For each screen it lists the store values that show up more often in friction moments than in the screen’s samples overall, highest ratio first. For example, a promo code set in every dead-zone moment on checkout, but in only 60% of checkout samples, reads ×1.7.

Each moment gets the last sample taken before it in its session. A moment with no sample before it is counted but not ranked. A value that appears in fewer than two moments isn’t ranked either. Add `--json` for every moment’s state. Your agent passes `withState` to `product_friction`.
