Friction and the journey
For AI agents: this page is Markdown at https://squiggle.sh/docs/analytics/friction.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.
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:
<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:
squiggle product friction --last 20friction 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 1121Each spot names one session and event to start from. Studio shows the same rollup on the Product screen; see Product analytics. Your agent reads it with product_friction, and one session’s findings with perf_insights.
Friction is found the same way on a recording, so you can look at a session you captured last week.
Find the state friction happens in
If your app sends state samples, 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.