Logopicto

Sampling

Record a share of app runs instead of all of them — the build-time percentage, the rate you set from the dashboard, why the decision is per run rather than per session, and what happens when the server cannot be reached.

Sampling decides whether a run records at all. A build carries a percentage; each time your app launches, picto flips one weighted coin, and on a run that comes up short Picto.startRecording() does nothing.

The default is 100 — record everything. Every picto build made before sampling existed behaves exactly as it did before, and so does every build that never sets the value.

Setting the percentage at build time#

The percentage is compiled in, via --dart-define:

flutter build apk --dart-define=PICTO_SAMPLING_PERCENTAGE=25

That build records about a quarter of its runs. The value is read as a const, which is what makes it work on web and lets the rest of the machinery tree-shake away — so it is genuinely a build-time input, not something you can change by setting an environment variable on a device.

A value outside 0..100 is clamped, not rejected. A build that mistypes 250 records everything rather than failing to launch, and -1 behaves as 0. The two ends are also special-cased: at 0 and 100 no random number is drawn at all, so "never" and "always" do not depend on a generator being fair.

If you would rather compute the number in Dart — from a debug menu, or a remote config you already ship — Picto.setup takes a samplingPercentage parameter that defaults to the compiled-in value. Most apps should use the --dart-define and pass nothing.

The decision is per run, not per session#

This is the part worth reading twice, because the dashboard will otherwise be confusing.

The decision is made once per app run and never revisited. A run that starts several sessions — you call Picto.startRecording() again later, after an earlier session was uploaded — gets the same answer every time, not a fresh coin flip.

So a build at 25% records 25% of runs, not 25% of sessions. If your app typically records four sessions per launch, you are still sampling one launch in four; you are just getting all four of that launch's sessions, and none of the other three launches'. Sampling is a statement about whether an app is under observation right now, not about individual sessions.

What a sampled-out run looks like#

Picto.startRecording() returns having done nothing, and Picto.isRecording stays false. Nothing is captured, queued or uploaded.

It is not silent: the SDK prints a line to the console once per process saying the run was sampled out and at what percentage. A developer watching their own startRecording() call do nothing has no other thread to pull, and that is the case this message exists for.

The server-side rate, and where you set it#

The percentage is not only compiled in. On launch, the SDK asks the server what the app's owner has set for this app, and a rate that arrives that way replaces the compiled-in one for the rest of the run — it is not a ceiling or a floor on it. An app whose owner has never set a rate is answered 100, the same as the SDK's own default, so a never-configured app behaves identically whether or not that request succeeded.

That division of labour is worth stating plainly, because the two knobs are not alternatives. The --dart-define is the fallback: what a build records before it has ever heard from the server, which makes it the only thing that works on a device's first launch after install, and the right place to put a 0 on a build that should never record. The dashboard is the knob for everything after that, because it changes a rate without shipping a build.

Changing the rate from the dashboard#

The control is in App settings → Recording, as a whole-number percentage from 0 to 100.

Setting it belongs to the owner of the organization the app is in. Everybody else — collaborators on that organization included — sees the current rate as plain text with no way to change it. Administrators are not an exception in the dashboard; the account that owns the organization is the one that gets the input box.

An app nobody has ever configured shows 100 and says as much, rather than an empty box that would read as "off". A never-set rate and a deliberate 100 are the same number to every device, so the distinction only matters when you are deciding whether the value in front of you was chosen or inherited.

What you should not expect is to see the change immediately, and that is by construction rather than lag. Two things sit between pressing Save and a device recording at the new rate:

  • the SDK asks for the rate on launch and never waits for the answer, so a rate you save now applies to a device's next launch, not the run it is in the middle of;
  • the server holds each app's answer for five minutes, so devices launching inside that window can still be handed the previous rate.

The honest expectation after saving, then, is "within about five minutes, from each device's next launch onward" — not "now". A session that arrives recorded at the old rate shortly after a change is the system working.

The request is never waited on#

The fetch is fired and abandoned. Not raced against a short timeout — not awaited at all. Picto.setup returns at the speed of the binding swap whether the network is fast, slow, rate-limiting or entirely absent, so sampling adds nothing to your launch time, by construction rather than by tuning.

What that buys is paid for elsewhere: the answer is remembered on disk and applied to the next launch. The cost of a rate change is therefore one launch of staleness per device, and never a slow launch.

In practice the answer often arrives in time for the current run anyway, because most apps start recording some way into their launch rather than in the same tick as Picto.setup. The order the SDK resolves is:

  1. a value the server answered earlier in this run, if one has arrived by the time you call startRecording();
  2. the value the previous launch wrote down;
  3. the compiled-in percentage from the --dart-define.

Every failure lands in the same place#

Offline, DNS failure, TLS failure, timeout, 404, 429, 503, any other non-200, and a 200 whose body is not a valid percentage are all handled identically: the value is ignored and the SDK falls back to the list above. There is no failure mode that stops your app recording, and none that delays it.

There is deliberately no retry. The server's rate limiter works on a fixed window, so a rate limit clears in whatever is left of that window rather than in a few seconds — retrying inside one launch would buy close to nothing, and a percentage that is one launch out of date is not worth a retry ladder.

What sampling does not gate#

An uncaught error still starts a session on a run that sampling declined. Error-initiated recording does not go through Picto.startRecording() and is not subject to the sampling decision at all, which is deliberate: "the app was not under observation" is exactly the situation that feature exists for, and a crash you only hear about from the 25% of runs you happened to be watching is not much of a crash report.

Choosing a number#

Sampling trades coverage for volume, and the useful question is which one you are short of.

  • Leave it at 100 while you are integrating, on internal and TestFlight builds, and on any app whose traffic you are comfortable recording in full. This is the right default and most apps never move off it.
  • Turn it down on a high-traffic production build where you want a representative sample of ordinary usage rather than all of it — retention is 30 days regardless, so the question is how many sessions you want to be able to search through, not how long they last.
  • 0 switches recording off for a build without removing picto from it. Errors still start their own sessions, so this is closer to "only record when something goes wrong" than to "record nothing".

Remember that the number applies to runs. At 10%, a user who opens your app twice a day is recorded roughly once every five days, and a bug that needs a specific unlucky sequence gets correspondingly harder to catch on video.