Logopicto

The Error Capture Window

A session that hit an uncaught error uploads a window around that error rather than the whole session — what the window covers, and why the first error also decides when the session ends.

When an uncaught error is recorded during a session, two things change about that session, and both are things you see directly in the dashboard:

  1. The upload is trimmed to a window around the first error — by default roughly 30 seconds before it and up to 30 seconds after — instead of the whole session.
  2. The first error schedules the session's own end. The session uploads at whichever comes first: the app being backgrounded, or the after-window elapsing (30 seconds by default).

Both halves are configurable at Picto.setup. The numbers throughout this page are the defaults.

This is not the same feature as Errors That Start a Recording. That one is about a session beginning when nothing was recording. This one is about what a session keeps and when it ends, and it applies just as much to a session you started yourself with Picto.startRecording().

The thing to know before you file a bug#

Record a five-minute session, throw at the two-minute mark, and the replay you get back is roughly 1:30–2:30 — not 0:00–5:00.

That is working as intended. The replay is not truncated, corrupted, or missing its beginning; it starts near the error on purpose. If you are looking at a short replay of a long session and the session hadError, this page is the explanation.

The "before" half#

The window does not start at exactly 30 seconds back. It starts at the newest window mark at or before error time − 30s, and marks are taken at most once every 5 seconds.

So the real lookback is at least 30 seconds, and a little more depending on where the last mark happened to fall — somewhere in the range of 30 to 35 seconds. A window has to begin somewhere the replay can be reconstructed from, and a mark is that place.

That "and a little more" is the mark interval, not the window, so it stays 5 seconds whatever you configure. If you set the before-window to 10 seconds, the real lookback lands in 10–15 seconds; set it to two minutes and it lands in 2:00–2:05. In general the lookback is somewhere in [before, before + 5s].

Nothing is thrown away from the recorder#

The window is not a buffer size, and the recorder does not record less because of it. Bytes accumulate exactly as they always did — append-only, nothing evicted. The window is an index over those bytes (byte offsets, plus shallow copies of state the recorder already holds), and it is only materialised at the moment the session is flushed to disk, and only when an error was actually recorded.

A session with no error is byte-for-byte the file it would have been before this feature existed.

Taking a mark costs no fidelity and forces no repaint either — it writes nothing to the wire, so it is index memory and not recorded bytes. The number of live marks stays bounded no matter how long the session runs; a ten-minute session holds no more of them than a one-minute one.

When the window is the whole session anyway#

Two common cases collapse to "from the start of the session", which means the upload is the entire recording, unchanged:

  • The error arrived within the first 30 seconds — within your before-window, if you set one. There is nothing before the window to drop.
  • The error arrived before any frame committed — which is every error-initiated session, since nothing was being recorded until the error itself. There is no before-window by construction.

Those two look the same in this list and do not look the same in the dashboard, so it is worth being blunt about the second one.

The one case where you get no lookback at all#

A recording that an uncaught error started has no before-window, whatever you set errorWindowBefore to. It is the same sentence as the bullet above, but the consequence is the opposite of "the upload is the entire recording": for that session the entire recording is everything from the exception onward. The replay opens on the crash. There is nothing in front of it, and there was never going to be.

This is not the window failing. errorWindowBefore is a rule about which already-captured bytes to keep, never a rule about what to capture — so it can only ever give you back time picto was already recording. If nothing was recording when the error fired, there is no earlier footage anywhere to hand you. Picto does not start recording just because it was installed, and that is deliberate.

Since this is indistinguishable from a broken before-window by looking at the replay, the SDK now says so out loud. The first time an error starts a session in a process, it prints:

[Picto] started a recording for an uncaught error, and this one has NO
before-window: nothing was recording when the error fired, so the upload
begins AT the exception instead of 10s in front of it. ...

The number in that line is the value you configured, so it also tells you whether your errorWindowBefore reached the SDK at all. It is printed once per process, not once per error — an app whose build throws on every visit to a screen would otherwise bury it.

If you want lookback on errors this early in the run, call Picto.startRecording() earlier. The reference host app waits five seconds after launch before starting, which is exactly the stretch where launch-time crashes live; a host that starts recording in main() has a before-window from the first frame.

This is now genuinely the early case only. A later error in a run you started recording gets its lookback even if an earlier error already closed a window, because recording restarts when the window closes.

A recording that was running when the error fired always gets at least the lookback you asked for. If you see a short lookback on one of those, that is a bug worth reporting — none of the window's own fallbacks can shorten a window, they can only widen it to the whole session.

The "after" half, and why the session ends#

The first error of a session arms a 30-second timer. The session then ends and uploads at:

min(next time the app is backgrounded, the after-window)

Both halves of that min are load-bearing, and the reasoning is the feature:

  • The backgrounding is the real mechanism. Picto already ends and uploads every session when the app is backgrounded or terminated, and that is what usually closes the window. It fires when the user actually leaves, which is both sooner and more meaningful than a countdown.
  • A pure timer would lose the crash case, because a process that dies never fires one.
  • A pure lifecycle rule would lose the session that errors and then stays in the foreground for an hour — you would not hear about the error until the user got around to leaving.

Whichever of the two loses is a harmless no-op. Picto.endSessionAndUpload() returns false immediately once the recorder is no longer active, and the timer is cancelled when the session ends by any route, so a session never uploads twice.

The min is a real bound, so the id is retired#

A backgrounding ends the session, but it does not by itself end the journey: coming back inside backgroundTimeout normally rejoins the same session_id at the next sequence. For a session that had errored, that would quietly undo the whole ceiling — the resumed segment is a fresh recorder that never saw the error, so no new after-window is armed and the session_id runs on for the rest of the app run.

So a session whose capture window has opened is retired when it ends. If you asked for recording, foregrounding after it starts a genuinely new session at sequence 0 rather than rejoining, and you lose nothing: capture continues either way, and the erroring session is bounded by min(backgrounding, after-window) as documented instead of only appearing to be.

If you never called startRecording() — so the only session you had is one the error itself started — the backgrounding is the end of it, and foregrounding starts nothing. That is the same narrowness the restart below has, applied to the other door out of the session.

Recording carries on afterwards, so the next error has a window too#

Closing the window ends that session; it does not switch picto off for the rest of the app run. The close uploads the errored session and immediately starts a fresh one, so the app is still being recorded when the next error arrives — and that error gets a real before-window like any other.

This matters more than it sounds, because the bug you are chasing is usually the one that happens again. Before this, the second crash of a run was always an error-initiated session: picto had stopped, so the error started a session at the exception and the replay opened on the crash with no lookback at all, however large errorWindowBefore was. If you configured a 10-second before-window and got it on your first error and not your second, that was this.

The restart is on picto's own initiative, so it is deliberately narrow:

  • Only when you asked for recording. It happens because you called Picto.startRecording() and picto is the party that stopped the session. If you ended the session yourself with Picto.endSessionAndUpload(), or never started one, picto stays stopped — an uncaught error buys one session, not a recorder switched on behind your back.
  • Never on a run sampling declined. The same gate startRecording() answers to.
  • Always a new session_id. The errored session's id is retired, exactly as it is after a backgrounding (above). Capture continues, under a new id, at sequence 0.

It also cannot turn a broken build into an upload treadmill: a window is armed by the first error of a session and closes errorWindowAfter later, so an app that throws every single frame still produces at most one session per after-window — two an hour at the default — rather than one per error.

Only the first error arms it#

An error that arrives while the timer is already running does not restart it. That is deliberate: FlutterError.onError fires every frame for a persistent build error, and a design that re-armed on each one would push the upload out indefinitely — for exactly the session you most want off the device.

For the same reason, the window is anchored on the first error rather than the latest. Chasing the newest error would walk the window away from the failure that started the whole thing. This is the same "the first one wins" rule that keeps an error storm from producing hundreds of sessions.

Where it deliberately does nothing#

Under a foreign binding — your own testWidgets test, flutter_driver, or integration_test — no timer is armed at all. See Testing for that stand-down and its siblings.

Choosing your own window#

Both halves are parameters on Picto.setup, alongside maxRecordingDuration:

Picto.setup(
  apiKey: 'your-key',
  appVersion: '1.0.0',
  errorWindowBefore: const Duration(minutes: 2),
  errorWindowAfter: const Duration(seconds: 10),
);

Omit either to keep its 30-second default. They apply to every session of the app run, not just the first — including a session an uncaught error started on its own — so configuring picto once in main() is all there is to do.

A few things worth knowing before you pick numbers:

  • The 5-second mark interval is still fixed, and is not a Picto.setup parameter. It is index granularity rather than window size: it is why the real lookback is [before, before + 5s] rather than exactly before. There is no environment variable for it either — see the Configuration Reference for everything the SDK does read.
  • A longer before-window costs upload size, not recording fidelity. Nothing extra is captured either way (see Nothing is thrown away from the recorder); a longer window simply keeps more of what was already recorded, and holds a few more marks in memory — ceil(before / 5s) + 2 of them, which is why even a very long window is cheap to index.
  • A longer after-window mostly costs latency, not data. It is a ceiling, and the app being backgrounded usually gets there first. Raising it delays the upload of a session that errors and then keeps running; lowering it means a foreground app that recovers gets less of the recovery in the replay.
  • Neither is validated, on purpose. A negative duration is clamped to zero rather than throwing, on the same reasoning as the sampling percentage: a mistyped value computed from remote config should record too much, not stop your app from launching.

If you are building the recorder yourself through PictoService rather than through the facade, CanvasRecorder takes errorWindowBefore directly. There is no errorWindowAfter there, because the "after" half is a timer Picto owns — on that path you decide when to end the session.

See also#