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.

Loading the live Studio…
How the app is used. Live, in your browser.Open in Studio

Product analyticsStudio tour · 3 steps

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.

checkout.ts
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:

SectionWhat it shows
ScreensVisits, average time and exit rate per screen, and the paths people take most.
Feature usageHow often each defineAction ran, and how often it failed. Switch to adoption to see the share of sessions that used it instead.
Feature affinityActions people tend to use together, strongest first.
FrictionRage taps, dead zones and confusion across sessions, by screen, with the worst spots first.
StatePick 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

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 together

Each 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:

Terminal
squiggle product state

Pass 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:

Terminal
squiggle product state "zustand:useCart.items#len" --screen checkout

An 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.

PathWhat it holds
nav#routeThe route the user is on, as the route names from the root down.
nav#depthHow many screens the current stack holds.
nav#tabWhich tab is selected, counting from 0.
zustand:useCart.itemsA field of the Zustand store declared as useCart. A Legend State store starts with legend:.
store:zustand-3A store the Babel plugin could not name, by the order it was created in. This moves when you add a store.
#kindWhat the value is: null, boolean, number, string, array or object.
#boolA boolean: 1 for true, 0 for false.
#lenAn array: how many items it holds.
#keysAn object: how many keys it has.
#signA number: 1, 0 or -1.
#magA number: its order of magnitude, as a power of two (3 means 8 up to 15).
#charsA string: its length, rounded down to a power of two. 0 when empty.
#valueA string you allowed with setStateAnalyticsAllow: the value itself.
mmkv:settings#keysHow 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#skippedHow many values the store held that a sample cannot describe, such as functions and class instances.
zustand:useCart#capped1 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:

Terminal
squiggle product state --coverage

Each 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:

Terminal
squiggle product state --coverage --save-gap 1 --as checkout-empty

Then 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:

Terminal
squiggle product state --drift

A 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.

More