Picto#
The public embedding facade — see Quick Start for the usual flow. Everything below is a static method; there's nothing to construct.
Picto.setup#
| Parameter | Required | Meaning |
|---|---|---|
apiKey | Yes | The client app's API key — from the dashboard. |
appVersion |
Yes | Your app's version string, stored on every upload from here on. |
pendingUploadsDir |
No |
Where a failed-after-all-retries upload gets queued for a later retry. Defaults to
picto_pending_uploads/
under the system temp directory.
|
maxRecordingDuration |
No | How long a single session keeps accepting events before it stops recording. Applies to every session of the app run, not just the first. Defaults to 10 minutes — see the duration cap for what raising it costs. |
errorWindowBefore |
No |
How much of a session
before
its first uncaught error the upload keeps. Applies to every session of the app run, not just the first. Defaults to 30 seconds, and the real lookback lands somewhere in
[errorWindowBefore, errorWindowBefore + 5s]
. It keeps bytes that were already captured and never causes any to be captured, so a recording an error
started
has no lookback at all whatever you set here — see
the error capture window
.
|
errorWindowAfter |
No | The ceiling on how much of a session after its first uncaught error the upload keeps, after which the session ends and uploads. A ceiling, not a countdown: the app being backgrounded already ends the session and usually gets there first. Applies to every session of the app run. Defaults to 30 seconds — see the error capture window . |
backgroundTimeout |
No |
How long the app may sit in the background and still come back to the
same
session. Applies to every backgrounding of the app run. Defaults to 30 seconds. Not a delay before uploading — backgrounding still ends and uploads the recording immediately; this governs only whether the next one continues that
session_id
at the next
sequence
or starts a new one. Negative is clamped to zero, which means every resume starts a new session. See
Sessions & Segments
.
|
There is no endpoint parameter: uploads go to https://api.picto.dev, which is compiled into the SDK.
Call once, before runApp(). Does not start recording — see Starting & Stopping.
Picto.startRecording()#
Turns recording on. The only way a session begins deliberately — an uncaught error also starts one (Errors That Start Recording), and foregrounding the app resumes one a backgrounding cut (Sessions & Segments). A no-op if already recording, and a no-op on a run that sampling declined.
"A no-op if already recording" is literal: a call made during a live session does not start a second recording, it does nothing. A session has to have ended first — see
Starting & Stopping, and
restartRecording for ending one and starting the next in a single call.
Picto.restartRecording({metadata})#
Ends the current recording, uploads it, and starts a genuinely new session — its own session_id, its own clock, its own embedded content, its own
hadError. Throws if setup was never called. metadata is passed to the ending session's upload and does not attach to the new one.
Returns the PictoSessionEndOutcome
of the session that ended, or null.
| Return value | Meaning |
|---|---|
uploaded / uploadFailed / recordingPipelineAbsent |
What became of the old recording. A new session is running. |
nothingRecording |
Nothing was recording, so nothing was uploaded. A new session is running — you do not need to check
isRecording
first.
|
null |
This run was not sampled. Nothing ended and nothing started. |
The old session's upload is awaited before the new recorder is installed, and any error-capture window armed by the old session is cancelled so it cannot end the new one early.
Picto.setTag values do not carry across the restart; the user named by Picto.identify
does. See Starting & Stopping.
Picto.isRecording#
Mirrors whether a recording is currently active.
Picto.endSessionAndUpload({metadata})#
Ends the current recording and uploads it — the manual-override path (backgrounding/terminating the app is the automatic one, see
Starting & Stopping). Throws if setup
was never called. metadata is optional free-form tags merged into the upload — see Tags.
Returns true for exactly one thing: the recording reached the backend.
false covers every other case, including ones where nothing went wrong at all — see endSessionAndUploadOutcome
below before logging false as a failure.
Picto.endSessionAndUploadOutcome({metadata})#
The same call, reporting which of the four things happened instead of collapsing them into a bool. Use this when your app logs or reacts to the result.
| Outcome | What happened |
|---|---|
uploaded |
The recording reached the backend. The only outcome
endSessionAndUpload
reports as
true
.
|
uploadFailed |
The upload was attempted and did not land. The recording is
queued on disk
, and a later
retryPendingUploads
— including the ones the SDK runs itself — will re-send it. Nothing is lost yet.
|
nothingRecording |
No session was active, so there was nothing to end. No request, no file, nothing queued. |
recordingPipelineAbsent |
The recording pipeline was never installed, so the session was empty by construction. The session is still ended. This is the outcome in every widget and
integration_test
suite, by design — see
Testing
.
|
nothingRecording is the one worth knowing about, because it is not an error and usually not even a mistake.
A session ends on its own when the app is backgrounded, and the error capture window
ends one roughly 30 seconds after an uncaught error. An app that ends sessions from a button meets this whenever one of those got there first:
switch (await Picto.endSessionAndUploadOutcome()) {
case PictoSessionEndOutcome.uploaded:
debugPrint('session uploaded');
case PictoSessionEndOutcome.uploadFailed:
debugPrint('upload did not land — queued, will retry');
case PictoSessionEndOutcome.nothingRecording:
debugPrint('nothing was recording; a session had already ended');
case PictoSessionEndOutcome.recordingPipelineAbsent:
debugPrint('no recording pipeline — expected under test');
}
Reporting a bare false as "upload failed, queued for retry" is the mistake this exists to prevent: for
nothingRecording both halves of that sentence are untrue. If you only need the yes/no, endSessionAndUpload
is unchanged and still means what it always did.
Picto.setTag / Picto.recordEvent#
See Tags and Custom Events.
Picto.navigatorObserver#
A NavigatorObserver that records every route change as a screen. Takes no arguments and is the only screen-tracking wiring there is:
MaterialApp(navigatorObservers: [Picto.navigatorObserver], home: const HomePage());
Returns a fresh instance per read — Navigator asserts an observer isn't attached to two navigators at once — and the deduplication state is shared across them, so reading it more than once cannot double-record.
setup cannot install this for you. See Screen Tracking.
Picto.retryPendingUploads()#
Retries every recording left over from a previous run that never made it past its own retries (offline, backend down, etc). Takes no arguments — each queued recording is stored alongside the metadata needed to send it, so nothing has to be supplied or remembered by your app.
You do not have to call this. The SDK drains the queue on its own: once during Picto.setup, and again whenever the app is foregrounded, throttled to at most one drain every 5 minutes. Earlier versions of this page told you to call it from
main(); that line is now redundant and can be deleted. See Uploading a Session
for the whole lifecycle this sits inside.
It stays public for the case the automatic triggers don't cover — draining at a moment you know is better than either of them:
// e.g. your own connectivity monitor has just reported the device is back online
unawaited(Picto.retryPendingUploads());
Never runs concurrently with itself. A call made while a drain is already in flight — yours, or one of the SDK's own — joins that drain instead of starting a second pass over the same queue, so a recording is never sent twice.
Never throws on an individual file: a queue that can't fully drain today is retried on the next launch.
Picto.isRecordingBindingInstalled#
Whether the recording binding is actually installed — i.e. whether startRecording() would capture anything. Always
false in a widget test; see Testing.
RecordingUploader#
The lower-level transport Picto wraps — reach for this directly only if you need more control than the facade gives you (e.g. a custom retry policy).
Constructor parameters:
| Parameter | Required | Meaning |
|---|---|---|
apiKey | Yes | The client app's API key — from the dashboard. |
endpoint |
No |
A
base
URL to upload to, overriding the compiled-in one for this instance only.
/db
is appended internally, don't include it yourself. Exists for tests and tooling that hold a handle on a specific server;
Picto.setup
never sets it.
|
client |
No |
Inject your own
http.Client
(tests do this to mock network calls). Defaults to a real one.
|
pendingUploadsDir |
No |
Where a failed-after-all-retries upload gets queued for a later retry. Defaults to
picto_pending_uploads/
under the system temp directory.
|
retryDelays |
No | Backoff delays between retry attempts. Defaults to [1s, 4s, 16s]. |
sendTimeout |
No | How long a single attempt waits for a response before treating it as a transient failure. Defaults to 120 seconds — multi-minute sessions base64 into multi-MB request bodies, and 30 was routinely too short. |
apiKey is sent in the upload body and independently verified against the real client app record on the server — a spoofed or unrecognized value is rejected outright. The constructor takes no organization ID, client app ID, or account ID: all of that is fully determined by
apiKey alone (one client app per key, one organization/owner per client app), so the server resolves and records it for you — nothing to look up in the dashboard, nothing to wire up here.
RecordingUploader.upload#
| Parameter | Required | Meaning |
|---|---|---|
path |
Yes | The flushed recording file to upload — from PictoService.endSession/flush. |
platform | Yes | e.g. defaultTargetPlatform.name. |
appVersion | Yes | Your app's version string. |
formatVersion |
Yes | Supplied for you by Picto.endSessionAndUpload — never hand-pick this. |
durationMs |
Yes |
The recording's length: the shorter of
CanvasRecorder.elapsedMs
(the session's wall clock) and
CanvasRecorder.contentEndMs
(where the last captured event sits) at flush time. A trailing stretch in which nothing at all was captured — no frames, no input — is not part of the recording, so it is not declared and not billed; a screen that goes static while input keeps arriving is still content.
Picto.endSessionAndUpload
computes this for you.
|
sessionId |
Yes | From the RecordingSegment returned by PictoService.endSession. |
metadata |
No | Free-form tags — see Tags. |
| sequence | No | Which segment of sessionId this upload is, counting from 0 — the ordering key for a session that produced more than one. Defaults to
0, which is what a session with a single upload sends. Picto fills this in for you when it continues a session across a backgrounding; a host driving
RecordingUploader itself supplies it or takes the default. Negative values are rejected by the server. See
Sessions & Segments. |
CanvasRecorder#
| Constructor parameter | Default | Meaning |
|---|---|---|
startActive |
true |
Pass
false
to defer recording until you call
startRecording()
explicitly —
Picto.setup
always does this internally, so recording never auto-starts through the facade either way.
|
maxDuration |
package default |
This recording's own duration cap. Omit it to take the 10-minute package default;
Picto.setup
's
maxRecordingDuration
is what sets it for a session the facade starts. See
the duration cap
.
|
errorWindowBefore |
package default |
This recording's own error-window lookback, over bytes this recorder already captured — it cannot reach back past
startRecording
. Omit it to take the 30-second package default;
Picto.setup
's
errorWindowBefore
is what sets it for a session the facade starts. There is no
errorWindowAfter
here — the "after" half is a timer
Picto
owns, not state on a recorder, so a host driving
PictoService
itself decides when to end the session. See
the error capture window
.
|
Reference app --dart-define values#
This repo's own reference app (apps/mobile/lib/main.dart) reads its API key from a --dart-define
at build time rather than hardcoding it — a reasonable pattern to copy, so a credential never ends up committed to source:
| Define | Maps to |
|---|---|
RECORDING_API_KEY |
Picto.setup's apiKey. Empty skips Picto entirely (see below). |
flutter build apk --dart-define=RECORDING_API_KEY=oc_...
If RECORDING_API_KEY is empty, the reference app skips Picto entirely and falls back to the lower-level primitives it wraps — recording still works, it's just written locally with no credentials to upload under.
SDK --dart-define values#
Unlike the value above, this one is read by the picto SDK itself rather than by the reference app, so it applies to any host app that embeds picto:
| Define | Default | Maps to |
|---|---|---|
PICTO_SAMPLING_PERCENTAGE |
100 |
The share of app
runs
this build records — see
Sampling
. Clamped to
0..100
.
|
flutter build apk --dart-define=PICTO_SAMPLING_PERCENTAGE=25
Picto.setup also accepts a samplingPercentage parameter that defaults to whatever this define compiled in, for a host that computes the number at runtime instead. A rate the app's owner has set server-side — from
App settings → Recording in the dashboard — replaces either of them for the rest of the run, so this define is the fallback rather than the last word. See
Sampling for when a change to it takes effect.