Logopicto

What We Capture (and What We Don't)

What ends up in a recording's bytes, what never does, and where the line between the two actually sits.

Picto's core design goal is to reconstruct layout and behavior, not to capture a video or a screenshot. That distinction has real privacy consequences worth being precise about, since "session replay" as a category often implies pixel/video capture — picto's on-screen-content channel deliberately doesn't work that way.

On-screen text#

Canvas.drawParagraph — the call Flutter makes for every piece of rendered text, from a button label to a chat message — is never recorded as characters. The recorder only ever captures each line's laid-out bounding box (sampleParagraphLineBoxes in binding.dart): an offset and a list of rectangles, nothing else. dart:ui gives no API to read glyphs back out of a Paragraph at all, so this isn't a policy layered on top of a capability picto has and chooses not to use — there is no code path here that could record real text even by accident. Replay draws each line back as a translucent gray bar, never reconstructed text.

One narrow, deliberate exception: small icon-font glyphs (a back arrow, a bell, a hamburger menu — square-ish, roughly 10–128 logical px) are rasterized as real pixels instead of a gray bar, since they're decorative UI chrome rather than user-authored content (see isIconGlyphBounds/_recordIconGlyph in binding.dart). This check is deliberately conservative and has a known, honestly-documented residual: a genuine single-character user input (someone types just "B") is indistinguishable from a single-glyph icon by this check alone, and also gets rasterized. That residual is far narrower than what the check fixes — every real word, name, or message of two or more characters stays a placeholder — but it means "never a single glyph" isn't quite as absolute a guarantee as "never two-or-more characters" is.

Images and vector graphics#

Same no-content default, different mechanism: images are recorded as a same-sized gray placeholder rect unless they resolve to a known bundled asset (free, no bytes sent — see Uploading Assets) or the containing widget opts into real capture (CaptureRealImages/CaptureVectorGraphics) — an explicit, size-budgeted choice the host app makes per-subtree, not something picto does automatically.

Excluding a subtree entirely#

Wrap anything that should never be captured at all — a payment form, an SSN field, a support-chat transcript — in ExcludeFromRecording:

ExcludeFromRecording(
  child: PaymentCardForm(),
)

This is stronger than the placeholder policies above: nothing about the excluded subtree reaches the recording, not even its position or size. A placeholder box would itself leak "something sensitive is here," which defeats the point for the cases this exists for. Live rendering is completely unaffected — only what gets recorded changes.

What this does not cover#

The policies above are specifically about what a Canvas draws on screen. A few other things you can attach to a session are separate channels — they are not placeholders, and ExcludeFromRecording has no effect on them, since they're not paint-time draw calls at all:

  • Exception messages and stack traces (FlutterError.onError/PlatformDispatcher.instance.onError, captured automatically once you call installErrorHooks()) — if your app embeds real user data into an exception's own message (for example, interpolating an email address into an error string), that text uploads as written. Keep this in mind when writing error messages in your own app.
  • Tags (setTag) — arbitrary key/value strings you choose to attach. See Tags.
  • The end-user identifier (Picto.identify(userId)) — an explicit, self-attested identifier you provide on purpose. See Identifying Users.

None of these are scanned, redacted, or masked by picto today — that's on the integrating app to manage, the same way you'd manage it for any other logging or analytics SDK.

Network requests are the one exception with its own, narrower policy, not the "arbitrary strings you provide" shape above. Capturing them at all is opt-in (PictoHttpClient, see Network Requests) — and once opted in, headers and request/response bodies are never recorded, by the same firm design decision as on-screen content, not something the integrating app has to manage itself.

Where rendering happens#

The backend (apps/server) never decodes a recording's binary format — it stores and serves the byte stream opaquely, the same way it would any other blob. Every render happens client-side: either in-process on the recording device (the standalone replay app), or in a browser via the compiled Flutter-web replay viewer when watching through the dashboard. No code path in apps/server imports the parsing or painting logic (parseRecording/buildFrameSnapshots/ReplayPainter) at all.