Logopicto

Errors That Start a Recording

An uncaught error starts a recording session when nothing is recording — why that exists, what the resulting session contains, and the cases where it deliberately does nothing.

There is exactly one thing that begins a recording without your app asking for it: an uncaught error. If an error reaches picto's hooks while nothing is recording, picto starts a session on the spot and records the error into it.

This is the only start that needs no request from your app at all. Picto.startRecording() remains the only deliberate way a session begins — and the two automatic starts that are not on this page, foregrounding and an error capture window closing, both happen only for an app that asked to be recorded and has not said stop.

This page is about a session starting. An error also decides what a session keeps and when it ends — including a session you started yourself — which is a separate feature covered in The Error Capture Window.

Why this exists#

Picto.setup() installs the error hooks and leaves the recorder inactive — recording never starts implicitly. So between Picto.setup() and your own startRecording() call there is a window in which errors are reported by the framework and picto is not listening.

That window is not an edge case. It is the default, and it is precisely where launch-time crashes live. Before this behaviour existed, every error arriving in it was dropped by CanvasRecorder.recordError's internal "am I active?" guard, and dropped silently: no captured exception, no hadError on any session, not even a console line saying something had been discarded. The crashes most worth seeing were the ones least likely to be captured.

What you need to do#

Nothing. Picto.setup() installs the hooks for you — see Quick Start. There is no parameter to turn this on and no separate call to make.

Both hooks do it, not just the framework one#

Picto installs two handlers, and an error arriving at either one can start a session:

  • FlutterError.onError — errors thrown during build, layout and paint.
  • PlatformDispatcher.instance.onError — errors surfacing from an async gap, which never pass through the framework's handler at all.

An error that escapes a Future is no less worth a session than one thrown during build, so both paths go through the same code.

Your own error handling is not disturbed#

Picto chains onto whatever handler was already installed rather than replacing it. Your existing FlutterError.onError, your crash reporter, your logging — all still run. Picto does not swallow errors, does not mark them handled on your behalf, and does not change what the framework does with them afterwards. Installing picto does not make an error disappear from anywhere it used to appear.

A session already running always wins#

If a session is already recording when an error arrives, picto records the error into that session and does not start a second one.

That is not just tidiness — it is the defence against error storms. FlutterError.onError fires every frame for a persistent build error, so a design that started a session per error would turn one bad build into hundreds of sessions and hundreds of uploads. Instead the first error starts one session and every error after it lands in that same session, which is also what makes "an error during an active session does not start a second one" true without a separate rule for it. A storm outliving that session's capture window then costs one more session per errorWindowAfter — 30 seconds by default — and never one per error, whichever of the two doors those sessions come through.

The session it starts is one session#

An uncaught error buys one session, not a recorder switched on for the rest of the run. That session ends the usual way — whichever of its capture window closing or the app being backgrounded comes first — it is uploaded, and then picto goes quiet again.

In particular, bringing the app back to the foreground does not start another one. Foregrounding resumes recording for an app that called Picto.startRecording(); an app that never did is left stopped, on that app switch and on every one after it. Without that rule a single throw in an app whose main() never mentions picto would leave it recording and uploading one session per app switch indefinitely, which is neither what you asked for nor what you would be expecting to pay for.

If you want capture to carry on past the error, that is what Picto.startRecording() is for — and calling it earlier in the run also buys the next error a before-window, which an error-initiated session by definition has none of.

The replay starts with the screen drawn#

The recorder captures changes, not whole frames. A session that simply switches on and then never repaints would record nothing at all — you would get a dashboard row that looks like data and plays back as an empty file, which is worse than no session.

So an error-initiated session forces a full repaint as it starts. The very next frame is a real keyframe, and the replay opens on the app as it actually looked at the moment things went wrong, rather than on nothing.

Sampling does not cost you crash sessions#

An error-initiated session is not subject to the sampling decision. A run that sampling declined is exactly the situation this feature exists for — "the app was not under observation" is the whole premise — so error-initiated recording does not go through Picto.startRecording() and never consults the sampler.

Turning your sampling percentage down therefore reduces how much ordinary usage you record, and does not reduce how many crashes you hear about. At PICTO_SAMPLING_PERCENTAGE=0 this is the only thing that still produces sessions, which makes a 0 build closer to "only record when something goes wrong" than to "record nothing".

Where it deliberately does nothing#

Under a foreign binding. If your app is running under a test binding — your own testWidgets, flutter_driver, or integration_test — nothing is intercepting draw calls, so a session started here could only ever be empty. Picto stands down instead of starting one. This is the same stand-down described in Testing, and it is the answer if you are wondering why a deliberately-thrown error in your integration test produces no session.

When it cannot start one. Starting a session runs real work, and this runs inside the framework's error handler while it is already reporting another error. An exception escaping from there would be a considerably worse failure than a missed recording, so a failure to start is caught. It is swallowed loudly, never silently — picto prints:

[Picto] failed to start a recording session for an uncaught error: ...

If you are looking for a session that you expected an error to produce, that line is the thing to search your console for.

What you get in the dashboard#

A session with hadError set to true, and a captured exception row carrying the type, message and stack trace.

Those two have different lifetimes, which is worth knowing before it surprises you: the recording itself is subject to the 30-day retention sweep, while the captured exception is stored in its own right and outlives it. An error-initiated session from four months ago is still listed as an exception; the footage it points at is gone.