A session is an id. A segment is an upload. For most of this SDK's life those were the same thing, and the docs said so — one recording, one
POST, one row. They are not the same thing any more, and the difference is worth two minutes because it changes how many rows your app produces.
The rule, in one line: the app being backgrounded ends a segment, not necessarily a session.
What backgrounding does now#
Picto installs an AppLifecycleListener at Picto.setup. On
paused or detached it ends the live recording and uploads it, exactly as it always has. What is new is the other half: on
resumed it starts recording again, on the session it just left.
So an app that calls Picto.startRecording() once in main() and never calls anything else now keeps capturing across every app switch, notification shade and permission dialog for the whole life of the process. Previously it did not — see
What changed for you, because the difference is visible on your bill.
You do not wire any of this up. There is no listener to install and no callback to implement.
The recording still uploads the moment you leave#
This is the part that surprises people, so it is worth saying plainly: backgroundTimeout
is not a grace period before uploading. Being backgrounded still ends and uploads the recording immediately.
That is deliberate, and it is the safer of the two designs. A grace timer that kept the recorder alive in memory across the background window would make rejoining trivial — and would lose every session the OS reaps while the app is away, which it may do at any moment and without warning. Uploading immediately and rejoining by id means the worst case is a journey that arrives as segments 0..n with the last one missing, rather than one that never arrives at all.
What survives the gap is not the recording. It is the id.
backgroundTimeout: how long the id stays claimable#
Picto.setup(
apiKey: '...',
appVersion: '1.0.0',
backgroundTimeout: const Duration(seconds: 30),
);
Omit it and you get 30 seconds. It governs one decision, made at the instant the app comes back:
| Time away | What the next recording is |
|---|---|
Less than backgroundTimeout |
The
same
session — same
session_id
, at the next
sequence
. A two-second app switch becomes two segments of one journey.
|
backgroundTimeout or longer |
A
genuinely new
session — fresh
session_id
, back to
sequence
0.
|
The gap is measured from the backgrounding to the foregrounding, read at the moment of the resume rather than after the upload settles — an upload that takes two seconds to finish does not count as two seconds of being away.
Past the timeout the id is retired and not reachable again, which is the behaviour you want: an app the user left an hour ago is a new journey, not a continuation of a forgotten one.
Like maxRecordingDuration and the error-window settings, this applies to every
backgrounding of the app run rather than only the first — the value is read fresh on each resume.
A negative duration is clamped to Duration.zero rather than throwing, following the other duration settings.
Duration.zero is legitimate and means "a resume always starts a new session" — it does not mean "never resume".
sequence is what orders the segments#
Every upload carries a sequence alongside its session_id: which segment of that session it is, counting from 0. A session that only ever produced one upload has
sequence: 0, so there is no such thing as a recording that is "not part of a session" — every one of them has both fields.
sequence is the ordering key rather than arrival time because requests do not necessarily land in the order they were sent. A segment that failed to upload and sat in the
pending-upload queue for a day arrives long after the segments that came behind it, and it keeps its own
sequence when it is re-sent, so it still sorts into the right place.
This is not a new column. recordings.sequence has been in the schema, non-nullable and documented, since long before anything could produce a non-zero value for it. What changed is only that the SDK now fills it in.
What carries across the cut, and what does not#
A resumed segment is a new recorder, not the old one turned back on. Almost nothing carries over, and that is the design rather than an oversight: the things that do not carry all describe a recording that has ended, not the user having it.
| Carries into the next segment? | |
|---|---|
session_id | Yes — that is the whole point. |
sequence | Increments by one. |
The user named by Picto.identify |
Yes. A user does not stop being that user because the app was backgrounded. |
Tags set with Picto.setTag |
No. Tags are session-level by design, and a segment is a new recording — set them again if you want them on the next one. |
hadError | No. It describes the segment that errored. |
| The recording clock |
No
— it restarts at zero. Each segment declares its own length in
duration_ms
, not a running total since the session began.
|
| The duration cap | Resets. Each segment gets the full ceiling, so the cap bounds a segment, not a session. |
The tags row is the one that bites. If you setTag once at launch, your tags are on segment 0 and on nothing after it. This is the same rule restartRecording already had, but backgrounding is far more frequent than a deliberate restart — so an app that tagged once and never thought about it again will now see most of its recordings untagged.
Only a session picto itself ended is ever resumed#
Automatic resuming reaches exactly one thing: a session that the backgrounding policy above ended, in an app that asked to be recorded and has not said stop. Nothing else is picked back up, and nothing starts recording behind your back.
-
You called
endSessionAndUpload()— left alone. Your "stop recording" button means stopped. -
You never called
startRecording()at all — nothing is started, and picto goes quiet. The only session such an app can have is one an uncaught error started, and that error bought one session: it ended at the backgrounding, it was uploaded, and the foregrounding does not buy another. This is the same bargain the error capture window keeps when its own timer closes the session, and it holds for every later app switch too — otherwise one throw would leave an app recording and uploading for the rest of the process without its developer ever having switched picto on. -
The error capture window had opened on the session — left alone, and its
session_idis retired. This covers both routes: the window's own timer ending the session, and a backgrounding closing a session that had already errored. The second is the one that matters, because that session was still inside its after-window: rejoining the id would hand it a segment with no window at all, and the id would then run on with no ceiling for the rest of the app run. If you asked for recording, capture still continues after the foregrounding — it just continues as a genuinely new session atsequence0. If you did not, the bullet above is what applies and nothing continues.When the window's own timer is what closed the session, there is nothing to wait for: recording restarts right then, under a new
session_idatsequence0, without the app having to be backgrounded and brought back. Same rule about the id, sooner. -
Sampling declined this run — left alone, and checked again at the resume rather than assumed. This case is reachable in practice, because an uncaught error starts a session even on a run sampling declined — so a sampled-out run genuinely can have a live session to background.
-
Something already started a session while the upload was settling — left alone. It is a live session either way.
-
The app was killed while backgrounded — the next launch is a new session. What continues the id is held in memory only, deliberately: the id exists to stitch a journey the user experienced as continuous, and a cold start is not that.
What changed for you#
If your app is the documented integration — one Picto.startRecording() in main(), nothing else — read this paragraph.
You used to capture from launch until the first app switch, and then nothing at all for the rest of the process. Nothing said so, and nothing looked wrong: the first recording arrived in full, so the dashboard showed a working integration. It was working for the first few minutes of each run.
That is fixed. It is unambiguously the behaviour you wanted, and it is also a real change in volume:
-
More rows. A user who switches apps eight times now produces nine segments where they used to produce one. They share a
session_id, but they are nine uploads. -
More billed duration. Usage is the sum of
duration_msover every recording in the period, with no grouping by session — so every segment's length counts. An app that used to bill for the first two minutes of each run now bills for the parts it was previously dropping on the floor.
Nothing was over-billing before and nothing is over-billing now; you are simply being charged for the recording you are now actually getting. But if you sized your plan against the old numbers, they were the numbers of a bug, and the increase can be large on an app whose users background it often.
The lever, if you need one, is sampling — record a share of runs instead of all of them.
backgroundTimeout is not that lever: lowering it does not reduce how much is captured or billed, it only changes whether the segments share an id.