Product analytics
For AI agents: this page is Markdown at https://squiggle.sh/docs/product.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.
See how people use your app: which screens they visit, which features they use, where they drop out and whether they come back. It reads the same sessions as Performance, so a slow checkout and an abandoned one are in the same place.
Name what matters
Screens, taps, renders and requests are recorded without any code. Three optional calls add what only you know: what an action is called, and who did it.
import { defineAction, identify, tag } from '@squiggle.sh/react-native';// Name a user action. Each call is timed, and recorded as ok or error.const addToCart = defineAction('add_to_cart', () => cart.add(product));addToCart();// Say who is using the app. Traits are merged into the session.identify('user-123', { plan: 'pro', appVersion: '1.4.2' });// Add context to the interaction in progress.tag({ experiment: 'checkout-v2', variant: 'A' });defineAction is the one to start with. Call the function it returns instead of the original. One name then works as a funnel step, a feature-usage count, a timing, and a flow assertion such as assert: { action: { add_to_cart: { eq: 1 } } }, which proves a journey did the thing, not only that a tap was sent. See the SDK reference for each call.
Engagement
The Product screen’s first tab:
| Section | What it shows |
|---|---|
| Screens | Visits, average time and exit rate per screen, and the paths people take most. |
| Feature usage | How often each defineAction ran, and how often it failed. Switch to adoption to see the share of sessions that used it instead. |
| Feature affinity | Actions people tend to use together, strongest first. |
| Friction | Rage taps, dead zones and confusion across sessions, by screen, with the worst spots first. |
| State | Pick a store value to see what it held on each screen, with how many sessions. Needs state samples from the app. |
Usage and adoption rank differently on purpose: a feature used ten times in one session is not the same as a feature used once in every session. Friction is explained in Friction and the journey.
From the terminal
squiggle product overview # the headline numberssquiggle product screens # visits, time and exit rate per screensquiggle product usage --adoption # share of sessions that used each actionsquiggle product affinity # actions people use togetherEach report covers every stored session of one platform. Pass --last 20 for the twenty most recent, which is the usual way to ask whether something is getting better or worse, or --session <id> for one. Your agent has a matching product_ tool for each report.
If your app sends state samples, add --where "zustand:useCart.items#len >= 3" to any report to split it in two: sessions that reached that state, and sessions that sent samples but never did. See Compare groups.
If your app builds for iOS and macOS, the two are counted separately: a report reads the platform of the app you ran most recently. Pass --platform ios or --platform macos to choose, or --platform all for both together. In Studio, the iOS, macOS and All switch at the top of the screen does the same. It appears when apps for both platforms are connected, or, with neither connected, when the project has sessions from both. Picking a platform also switches Live to that app; All leaves Live where it is.
See what your app held
If your app sends state samples, list the state paths your sessions have, most widely sampled first:
squiggle product statePass a path to see the values it held on each screen, each with its share of that screen’s samples, its number of sessions and up to three session ids to open. Add --screen checkout to keep one screen, by route name:
squiggle product state "zustand:useCart.items#len" --screen checkoutAn empty answer means no state samples were recorded. See Troubleshooting.
State paths
A path is where a value lives, then # and what is measured about it. A sample holds measurements, never your values, unless you allowed a field’s values with setStateAnalyticsAllow.
| Path | What it holds |
|---|---|
nav#route | The route the user is on, as the route names from the root down. |
nav#depth | How many screens the current stack holds. |
nav#tab | Which tab is selected, counting from 0. |
zustand:useCart.items | A field of the Zustand store declared as useCart. A Legend State store starts with legend:. |
store:zustand-3 | A store the Babel plugin could not name, by the order it was created in. This moves when you add a store. |
#kind | What the value is: null, boolean, number, string, array or object. |
#bool | A boolean: 1 for true, 0 for false. |
#len | An array: how many items it holds. |
#keys | An object: how many keys it has. |
#sign | A number: 1, 0 or -1. |
#mag | A number: its order of magnitude, as a power of two (3 means 8 up to 15). |
#chars | A string: its length, rounded down to a power of two. 0 when empty. |
#value | A string you allowed with setStateAnalyticsAllow: the value itself. |
mmkv:settings#keys | How many keys an MMKV instance holds. Instances whose id looks like data, such as user-48213, are added together as mmkv:other#keys. |
zustand:useCart#skipped | How many values the store held that a sample cannot describe, such as functions and class instances. |
zustand:useCart#capped | 1 when the store has more than 64 paths and some were left out. |
Squiggle reads two levels into each store and sends at most 64 paths per store, the same ones every time. A store name only holds steady from release to release when the Babel plugin names it; see Store names.
Find the states your tests never reach
If your app sends state samples, Squiggle compares what real sessions held on each screen with what your library flows and save slots reach:
squiggle product state --coverageEach row is a state, such as an empty cart on checkout, with the share of that screen’s real sessions that reached it. A session counts as real when no flow ran it. Each row also proposes a save slot that reaches the state: the nearest slot you already saved on that screen, with one field changed. Nothing is saved until you ask:
squiggle product state --coverage --save-gap 1 --as checkout-emptyThen squiggle load checkout-empty puts the app in that state for a flow to start from. A row has no proposal when you have no slot on that screen, or when the state is a count or a length range rather than one value. The row says which.
The report card shows the same gaps: up to three per screen your change touches, each with the --save-gap command for it.
See what changed between releases
With state samples and a release (or appVersion) trait set through identify(), Squiggle compares what sessions held on each screen in your latest release with the one before:
squiggle product state --driftA state is listed when its share of a screen’s sessions moved by 20 points or more, such as an empty cart on checkout going from 25% to 75% of sessions. A screen needs at least three sessions in each release to be compared. Pass --from and --to to pick the releases.
squiggle product state --perf reads performance findings, such as render storms, jank and long tasks, against the state each happened in. It lists the store values that come up more often in findings than in the screen’s samples overall.