Interaction analytics
A conventional heatmap knows where a finger landed. This one knows which component was under it, whether that component reacted, and what re-rendered as a result — because the graph records causality, not coordinates.
Why this is different
Session-replay products reconstruct what happened from pixels and DOM snapshots, then leave you to infer intent. Squiggle starts from the other end: a session-level interaction id stamps every event a touch set in motion, so the graph carries three distinct edges — targeted (the touch landed on this node), caused_render (something repainted because of it), and triggered (it caused a navigation or a state change).
That distinction is the whole product. “The user tapped and nothing happened” and “the user tapped, the button highlighted, and nothing happened” look identical on video and are completely different bugs.
What the app has to do
Nothing, for the heatmap. The touch probe is part of <Squiggle> and covers every touch on the screen, instrumented control or not.
One opt-in changes what the detectors see. By default they run over gestures that react-native-gesture-handler recognised; touchDetectors routes every touch through the same chain, so a rage tap on a plain Pressable or a bottom tab is caught too:
<Squiggle navigationRef={ref} touchDetectors> <Stack /></Squiggle>It is off by default for an honest reason: it also turns a scroll’s touch-down into a tap, which is noise. Turn it on when whole-app tap analytics matter more than that.
The heatmap
Heatmap in Simulator Studio draws every touch as a soft blob coloured by its local density on an absolute ramp — a lone tap is green, a cluster is red. It is append-only by construction, so adding a touch never re-colours the existing ones and the overlay never flickers.
Four things it does that a pixel heatmap cannot
- Whole-screen coverage
- Points come from recognised gestures and an app-wide passive touch probe — so you see every touch, not only the ones that landed on an instrumented control. That includes touches on a real device, not just the simulator.
- Per screen
- Every touch carries its timestamp, so a screen picker buckets them by the route that was active at the time. A heatmap of “the whole session” averages away exactly what you wanted to see.
- Scroll-aware
- On a scrollable screen, touches are re-placed from where they landed in content to where that content currently sits in the viewport. A hot spot stays glued to its list item as you scroll, and drops out when it scrolls off.
- Swipe vectors on the same layer
- Drags are drawn as arrows from touch-down to finger-lift, coloured and thickened by speed, with a dashed momentum coast to where the content actually settled. Taps and swipes in one view, because they are one behaviour.
Three detectors
How a cluster is classified
The detectors partition clustered taps by what the graph observed, per spot rather than per tap — one frustrated burst on one control reports once, not split across two categories. The distinguishing question is not how many taps there were but what happened after each one: did anything render, did anything get hit, did the route change.
What each one means
- Rage tap
- A burst the UI acknowledged — at least one tap produced a render or hit a node — but which changed nothing. A control that is present and unresponsive. This is the highest-value finding in the set, because the user could see the affordance and it lied to them.
- Dead zone
- A burst with no observable effect at all: nothing reacted, nothing re-rendered, nothing was hit. Either the control is inert or there is no control there. On an injected simulator the accessibility element visually beneath the tap is hit-tested and named, which usually tells you which — and when it cannot, the finding says it is unattributed rather than guessing.
- Confusion
- Many taps on a screen followed by a retreat. The user gave up.
The journey
A vertical timeline of the screens the session reached, labelled by how it got there (push, back, navigate) with revisit ×n markers. Each stop carries a badge counting the analytic items inside its visit window, and tapping a stop filters the errors and insights lists to that visit. So “what went wrong on the checkout screen” is a click rather than a scroll.
The timeline merges the live buffer with the stored navigation rows, so re-entering the screen rehydrates from the database instead of blanking.
Errors
A Sentry-shaped view of the captured exceptions and crashes: the stack, the component stack, and the breadcrumb trail — the interactions leading up to it, sliced from the buffer at the error’s own cursor. It reads from the store rather than from live state, which is the point: an error that triggered a fatal reload is still there afterwards, as a selectable past run.
How to actually use it
- Drive the app like a user would — by hand in Studio, or by running a flow. Both fill the same buffer.
- Look at the heatmap per screen. Clusters away from your primary action are the interesting ones: they are people trying something you did not design for.
- Read the rage taps first. They are the findings where you know the user had an expectation, because the UI gave them one.
- Use the journey to scope. Filter to the screen with the most findings rather than reading a flat list.
- Then check whether it is systemic. One session is an anecdote. The cross-session friction rollup in Product analytics tells you whether the same spot is failing for everyone.
Everywhere else
Findings are written into perf.db with deterministic ids, so re-running the analysis refreshes a finding in place rather than duplicating it — and every surface sees the same rows:
squiggle product friction --last 20 # cross-session rollupsquiggle perf diff --against v2.4.0 # a session vs a baseline, with the findingsAgents read them through squiggle_perf_insights and squiggle_product_friction, each finding carrying a representative session and event id for drill-down. And because the detectors run over the event stream rather than over SQL, they run identically on a recording — you can analyse a session you captured last week.