Logopicto

Screen Tracking

Record which screen the user is on by adding one observer to your app's navigator.

Screen tracking records every route change as a timestamped screen, so a replay can tell you where the user was at any moment — not just what the pixels looked like. For an app that navigates with routes it is one line of wiring and no further calls: add Picto.navigatorObserver to your app's navigatorObservers and picto records the rest. Tabs and other screens that aren't routes take one call each.

Adding the observer#

import 'package:picto/picto.dart';

MaterialApp(
  navigatorObservers: [Picto.navigatorObserver],
  home: const HomePage(),
);

That's the whole integration. Picto.navigatorObserver hands back a fresh observer every time you read it — a Navigator asserts that an observer isn't already attached to another one, so a shared instance would crash in debug the moment your app built a second MaterialApp. Reading it twice is safe and doesn't double-record: the "which screen are we on" state the observer deduplicates against is shared across every instance, not held per observer.

This is the one part of the capture surface Picto.setup cannot install for you. A Navigator reports its transitions only to the observers it was handed when it was built, so there is nowhere for setup to hook in from the outside. An app that never adds the observer simply records no screens — everything else about the recording is unaffected.

If you use a router package rather than MaterialApp's own navigator, add the observer wherever that package accepts navigator observers — it is an ordinary NavigatorObserver and nothing about it is picto-specific at the wiring level. What matters is that it reaches the Navigator your app actually pushes routes onto; a Navigator your observer was not handed to reports nothing.

How a screen gets its name#

A screen is recorded under its RouteSettings.name, and picto never invents a better one. Three rules, in order:

The routeRecorded as
Has a non-empty settings.name that name, verbatim — /checkout, orderDetail, whatever you wrote
Is the navigator's first route and has no name/
Has no name unnamed(MaterialPageRoute<void>) — the route's own type

So naming your routes is what makes the Screen lane readable. If you push routes like this:

Navigator.push(
  context,
  MaterialPageRoute(
    settings: const RouteSettings(name: '/checkout'),
    builder: (_) => const CheckoutPage(),
  ),
);

…the lane reads /checkout. If you omit the settings, it reads unnamed(MaterialPageRoute<void>), and it will keep reading that for every unnamed route in the app.

This is deliberate, and worth understanding before you file it as a shortcoming. Picto could guess a name from the page widget's type or the builder that made it, and it refuses to: a guessed name looks exactly as authoritative as a real one, so a lane full of plausible-looking wrong labels is worse than a lane that visibly says nobody named anything. An empty-looking Screen lane always means "this app did not name its routes" — never "picto could not tell."

Named routes (Navigator.pushNamed, a routes: table, or a router package that sets RouteSettings.name for you — most do) get this for free.

Screens vs. custom events#

These are the two things easiest to confuse, and they are different facts:

  • A screen is where the user was. It's emitted by the navigator, automatically, and it describes a span of time.
  • A custom event is something that happened. You call Picto.recordEvent for it yourself, and it describes a point in time.

They are recorded as different event kinds and drawn on two different lanes — the Screen lane and the Events lane — precisely so that a consumer never has to string-match a magic name to tell them apart.

Don't reach for Picto.recordEvent('viewedCheckout') to mark a screen; the observer already did it, and the hand-rolled version lands in the wrong lane. Do use a custom event for the things a route change can't express — "checkout tapped," "payment failed," "onboarding step 3."

Screens that aren't routes#

Bottom-navigation tabs in an IndexedStack, pages of a PageView, any screen your app swaps in with setState: the Navigator never hears about these, so no observer can record them. Report them yourself with Picto.recordScreen:

void _selectTab(int index) {
  setState(() => _tab = index);
  Picto.recordScreen(const ['home', 'discover', 'portfolio'][index]);
}

They land on the same Screen lane as route changes, and the two share one idea of which screen the user is on:

  • Popping back returns to the tab. The name is remembered against the route that was on top when you recorded it. Push a page from the discover tab and pop it again, and the lane reads discover → /details → discover, not discover → /details → /.
  • Repeats are free. Recording the screen that is already recorded does nothing, so it is safe to call from a handler that fires again for the tab you're on.

The same naming rule applies as for routes — see What not to put in a route name.

Dialogs and bottom sheets#

An unnamed dialog or sheet is not a screen. Every PopupRoute you don't name — showDialog's route, showModalBottomSheet's route, a dropdown's or a popup menu's internal route — is ignored, and so is its dismissal. The framework helpers that push these routes never give them a name of their own, so recording them all would fill the Screen lane with unnamed(DialogRoute) and with the internal route of every dropdown your app opened.

Name one, and it is. Both helpers take a routeSettings, and passing a name tells picto the sheet is somewhere the user went:

showModalBottomSheet<void>(
  context: context,
  routeSettings: const RouteSettings(name: 'bills'),
  builder: (_) => const BillsSheet(),
);

The lane then reads / → bills → /: the sheet opening is recorded as a screen, and dismissing it records the screen underneath again. An unnamed dialog opened on top of a named sheet is still ignored, and closing it returns to the sheet without recording anything.

Name the sheets and dialogs that are destinations in your app's flow, and leave confirmations and pickers unnamed. To mark a moment in a sheet rather than the sheet itself, use a custom event.

Where screens show up#

Screens appear in two places in the replay viewer, both in their own color, distinct from custom events:

  • The Screen lane on the session timeline, directly above the Events lane — a run of route names across the length of the recording.
  • The Screen filter chip in the event list, alongside Errors, Tags, Device and Taps.

See Watching a Replay for how to read the timeline.

What not to put in a route name#

Same policy as everywhere else in the recording pipeline: a route name is a screen identifier, never a value read out of the screen. Whatever you pass as RouteSettings.name is written into the recording verbatim — picto does not scan, redact or mask it, exactly as it does not for tags or custom event data.

The trap is the interpolated route:

// Don't — the order id is now in the recording.
settings: RouteSettings(name: '/orders/${order.id}')

// Do — the route pattern identifies the screen; the id belongs nowhere here.
settings: const RouteSettings(name: '/orders/detail')

Use the pattern, not the resolved path: /orders/detail, not /orders/8814; /user/profile, not /user/ada@example.com. Never put user-entered text, an email address, a token, or any other PII into a route name. If you need to know which order a session was about, that is what an identifier-shaped tag or custom event is for — and the same no-PII rule applies there too.

See What We Capture for the rest of the recorder's content policy.