Skip to content
apsio/ specApsio Mobile Profile, homev0.1.0 · final
GitHub

16 Live inspection (optional)

An app’s team can watch the device of one of its own testers while the app is open: the log lines it writes, its memory and power state, and wireframes of the screen. It can send that device a few commands. Nothing streams to anyone else: live inspection needs two keys, the project’s and the app’s, and the device shows that it is being watched.

This section is optional. An SDK or a backend MAY implement live inspection. One that does MUST follow all of this section. An SDK that does not implement it ignores live in the policy (12.2).

A device takes part only when both keys are turned:

  1. The project marks it as a tester. The policy’s live.testers lists the testers, at most 100. Each entry has an id, a match and an expires_at (Unix seconds); an entry whose expires_at has passed is ignored. As for remote requests (15.5), the policy never names an install or a user, and it never publishes what proves a claim either:

    • an entry’s proof is the lowercase hex SHA-256 of the UTF-8 bytes of its id, a colon, and the install’s app.installation.id or the app user’s user.hash, as set;
    • its match is the lowercase hex SHA-256 of the UTF-8 bytes of that 64-character proof.

    Entry ids are unique in the list, and never another id in the policy (16.7). A device computes the proof of each entry with its own installation id and, while the app has set one, with its user.hash, again whenever the policy changes or the app sets, changes or clears the hash, and takes part when the SHA-256 of a proof equals the entry’s match. Its hello sends that entry’s id and proof (16.4), never the install id it came from or the user.hash, and the backend counts the tester only when the proof checks out (16.7): the same rule for an install and for a user, so the policy never says which an entry is. Knowing the policy, then, is not enough to claim a tester; a leaked proof claims that one entry only. A proof is a secret as the device token is: the SDK keeps it in memory only, MUST NOT write it anywhere, logs and HTTP caches included, and MUST NOT send it anywhere but live.origin. No live object in the policy means live inspection is off.

  2. The app allows it on the device. setLiveInspection(allowed) (section 13):

    • MUST be off until the app turns it on, and MUST be kept across launches until the app turns it off.
    • Nothing else turns it on: not the policy, not a command, not a backend. There is no remote switch.
    • A call made before start MUST be kept, and applies at start: the SDK stores the choice when the app makes it, whether or not it has started.
    • It is meant for an app’s own internal or beta builds, behind its own setting (“Let my team see my activity live”). An app that never calls it never takes part.

The channel (16.4) is open only while all of these hold, paused while the keys hold but the app is interrupted or saving power or data, and closed otherwise:

  • live inspection is allowed on the device;
  • consent is granted (13.5);
  • the kill switch is off (12.3);
  • an unexpired live.testers entry matches the device;
  • the app is in the foreground (16.3): active for open, interrupted for paused;
  • none of Low Power Mode, Low Data Mode or Data Saver is on (4.4), for open; one of them on, for paused.

When the channel closes, the SDK MUST stop at once: it cancels its requests in flight and sends nothing more on it. Withdrawing consent or live inspection, and the kill switch, also end a raised log level (16.6). Live inspection never brings back what consent, an opt-out or the kill switch removed, and never overrides a family the app turned off in code. The function live.open (B.2) pins the gate, the match, the proof and the expiry.

While at least one viewer watches the device, the SDK MUST show it on screen:

  • a native indicator, drawn by the SDK itself (not by a cross-platform layer’s views, which can be torn down), above the app’s own content: on top of the app’s windows in each foreground scene on iOS, and in each window replay walks on Android. System interfaces outside the app (alerts, sheets, the share sheet) may cover it;
  • with a plain text such as “Your team is viewing this app live”, which names the app’s team, never a person: the device does not know who views it;
  • inside the safe area, without taking focus or the keyboard, and letting touches outside it reach the app;
  • with an accessibility label, announced once when viewing starts.

Each command the device runs (16.6) MUST show a short notice of it, such as “Your team took a screenshot”.

The SDK MUST leave its own indicator and notices out of everything it records: replay and live wireframes (section 10), screen views (8.6), frustration signals (8.7) and touches.

  • Foreground only. The channel runs only while the app is in the foreground. The SDK MUST NOT keep it, or schedule anything for it, in the background: no background task, no background transfer.

  • Hellos. While the keys hold and the app is in the foreground, the device says hello every interval_s seconds (16.4), except under Low Data Mode or Data Saver, where it sends nothing (4.4). A hello says, with its reason, why the device cannot start a live session: the app is interrupted (inactive), Low Power Mode is on (low_power), or the day’s bytes are spent (capped).

  • Start. A hello answered with viewers, and sent with no reason, starts a live session: the device sends at once a frames body with state: "live", which tells the viewers it is live and starts the session’s clock, then waits for commands and sends what 16.5 lists.

  • Pause. While the app is interrupted in the foreground (iOS between willResignActive and didBecomeActive; Android with no activity resumed), or while Low Power Mode, Low Data Mode or Data Saver is on, the live session pauses: the device sends, when the network allows, one frames body with state: "paused" and the reason, then nothing, next included, until it resumes, which it says the same way with state: "live".

  • End. The live session ends when the first of these happens, in this order:

    • the channel closes (16.1), or the app moves to the background (closed);
    • next answers with a stop (stop);
    • the session has stayed paused for 60 seconds (paused);
    • the session’s end comes (until): 30 minutes after its first frames, unless a viewer renews it. The device keeps that end on its own monotonic clock, next’s remaining_ms after the answer arrived;
    • the bodies of frames and results the live session sent, as sent (compressed), reach 10,000,000 bytes (session_bytes), or those the device sent in the calendar day (UTC) reach 50,000,000 (day_bytes).

    The device then sends, if it can, a frames body with state: "ended" and that reason, and goes back to hellos while the keys hold, except on closed, where it sends nothing more (16.1): the backend learns of that end from 60 seconds without frames or next (below), or from the next hello. The function live.stop (B.2) pins these limits and their order.

  • No restart by itself. A viewer who stays does not start the next session:

    • after a session ends at until or session_bytes, the backend MUST answer hellos with viewers 0 until a viewer starts a new session or renews it;
    • once the day’s bytes reach the cap, the device MUST NOT start a live session until the UTC day ends, and says so with reason: "capped". It MUST keep the day’s count across launches, on disk like the app’s setLiveInspection choice; live data itself never is (16.4).
  • A backend treats a live session with no frames and no next for 60 seconds as ended.

The SDK sends to the policy’s live.origin: https://, a lowercase host (letters, digits, . and -) and an optional :port (1 to 65535, in decimal with no leading zero), with no userinfo, path, query or fragment (for example https://live.example.com). An SDK MUST ignore a live object whose origin has any other form, as if the policy had none: https://live.example.com@other.example names other.example. For local testing only, an SDK MAY also accept http:// with a loopback host, 127.0.0.1, [::1] or localhost, and an optional :port as above, in any build: loopback traffic never leaves the device (on the Android emulator, adb reverse makes 127.0.0.1 reach the host). It MUST ignore any other origin that is not https://, http://127.0.0.2 and http://localhost.example.com included. A production backend never serves such an origin (16.7). The function live.origin (B.2) pins these forms, and the live/sdk-* vectors (B.5) pin the wire below: the device token, a hello without one, a 401, no redirects and a 409 on results. Every request carries the app key in Apsio-App-Key, as on ingest (1.2). Requests are JSON, compressed as 1.1 compresses ingest’s. install is the device’s app.installation.id, in every body and in next’s query.

Request When Body Answer
POST /v1/live/hello Every interval_s while the keys hold and the app is in the foreground install; session (session.id); model (device.model.identifier); os (os.name, a space, os.version); release (service.version); tester, the id of the entry that matched; proof, that entry’s proof (16.1), 64 lowercase hex; reason when it cannot start a session (inactive, low_power or capped). Never a user.hash { "viewers": n, "interval_s": s, "device_token": t }: with viewers above 0, and no reason sent, a live session starts. interval_s is 30 by default; the device keeps it within 15 to 120. device_token is 32 random bytes as hex, for next, frames and results
GET /v1/live/next?install=<install> During a live session, one at a time. The backend holds it up to 25 seconds, then answers 204; the device asks again at once None { "commands": [{ "id", "name", "args" }], "viewers": n, "until": <Unix ms>, "remaining_ms": n, "stop": null }. until (for the viewers) and remaining_ms (the time left as the backend answers, for the device) are null until the session’s first frames. stop is viewers_left, ended or tester_removed when the session must end now
POST /v1/live/frames During a live session: at most once a second, only when there is something new (16.5), and once on each change of state install, state (live, paused or ended), reason when paused (inactive, low_power, low_data or data_saver) or ended (as 16.3 names them), lines, and optionally metrics and wireframe 2xx
POST /v1/live/results Once for each command served install, id and name (the command’s), outcome (done, failed or refused), at (Unix ms) and result (16.6) 2xx; 409 for a result whose id and name are not a command sent to that device in the last 2 minutes, which is delivered nowhere
  • The device token. Every 2xx answer to a hello whose tester counts (16.7) carries device_token, 64 lowercase hex characters; a device given any other shape, or none, keeps none. A device that held a token and gets a 2xx hello answer without one drops it and ends its live session at once, without an ended body, and cancels next: its tester no longer counts (an entry removed, or expired first on the backend’s clock). next, frames and results MUST carry it in the Apsio-Live-Token header.
    • Keep and renew. A hello may carry the current token in Apsio-Live-Token, and the SDK MUST send it on every hello while it holds one. A hello with the current token keeps it and renews the presence: its answer returns the same token. During a live session such a hello only renews the presence: it neither starts nor ends a session, whatever viewers says; next carries viewers and stop for that.
    • Replace. A hello whose tester counts, without the current token or with another, from anyone, replaces it, which ends the session that held the old one.
    • Presence. The backend keeps a device’s presence, and its token, at least 2 × interval_s + 25 seconds after the hello that made the token, or the last hello, next, frames or results that carried it, and makes a new token once it has expired. A device paused under Low Data Mode or Data Saver sends nothing, so its presence can expire meanwhile: on resume it gets 401 and starts over.
    • On a 401 from next, frames or results (the token is missing, wrong or replaced), the device ends its live session at once, without an ended body (its token can no longer send one), cancels next, drops the token, and says hello again. That hello goes at once, unless the device already had a 401 from next, frames or results within the last interval_s; then it goes at the next interval_s. Two senders that share an install therefore take the session from each other at most once every interval_s, visibly, never in a loop. The command ids it ran stay kept for 2 minutes, as below.
    • Its life on the device. The SDK keeps the last hello’s token while the channel is not closed (16.1), in memory only, and drops it when the channel closes and on a 401. It MUST NOT write it anywhere else, logs and HTTP caches included, and MUST NOT send it anywhere but live.origin.
    • What it is. The token binds requests to the session the last hello opened. It is not a device credential: the tester’s proof (16.1) stays the secret that lets a device take part, and the two keys still gate everything. A second party that takes the session over shows as the session ending and starting, never as a silent second sender.
  • Commands stay pending on the backend until their result arrives, or for 2 minutes, so one can come twice, in the same live session or the next. The device MUST run each command id at most once: it keeps the ids it ran, with their results, for 2 minutes, in memory, and answers a repeated id by sending the same result again without running it.
  • Statuses. On a hello, 401, 402, 403 and 503 mean what they mean on ingest (1.5): a 401 there is an unknown app key, and the channel stays closed until the next launch. A backend MUST NOT answer a hello 401 for its token: a missing or other token replaces it. On next, frames and results, a 401 is the device token (above), and 402, 403 and 503 mean what they mean on ingest. On results, a 409 is a result for no command the device was sent, or for one past its 2 minutes: the device treats it as a 2xx for the result it keeps and does not send it again. 400 is a body the backend cannot read; 404 and 405 are an unknown route or method.
  • No redirects. The SDK MUST NOT follow a redirect on the live channel: a 3xx is a failed request, as a 5xx is. Following one would carry Apsio-App-Key and Apsio-Live-Token to another host. Its HTTP session keeps no cache and no cookies.
  • On 429, 5xx or 3xx:
    • the next hello goes after Retry-After, at least interval_s later, backing off exponentially to 120 seconds while it keeps failing;
    • during a live session, the device pauses next and frames until Retry-After.
  • Nothing is buffered. Live data is never queued, written to disk or retried: a lost frames body is lost, and the viewers see a gap. Only a results body is sent once more, after the backoff, and only with the current token: one refused with 401 is not sent again, since the command comes again in the next session and gets its kept result.

The live channel sends every record the SDK writes for a family that is not off, after redaction and masking, and never past consent, an opt-out or the kill switch. Sampling, triggers and trigger windows (section 15) do not apply to it. A family is off when the app turned it off in code, when the policy does not collect it (12.2 capture), or when its mode is never (15.3).

  • Log lines (lines, at most 200 a body; a backend MAY refuse more with 400) are copies of the log records the SDK writes for the logs and errors families when they are not off (13.6, 13.7), taken after redaction (11.1): { "t": <Unix ms>, "severity": 1 to 24, "body": <text>, "attributes": { key: string, number or boolean } }. The copy is taken before the log bridges’ limits, which keep applying to the records themselves, and its body is cut to 4096 bytes as 1.6 cuts it.
  • Wireframes (wireframe) are full replay frames (10.1), never deltas, masked as replay masks (10.3, 10.4), from the same capturer, when replay is not off, at most 2 a second. The device MAY send them less often, for example as the live session nears its byte cap. Capturing them MUST NOT change the session’s replay: its segments, its budget and its sampling stay as they are.
  • Metrics (metrics, one sample): t (Unix ms) and the attributes of the samples section 7 defines, by the same names and units: apsio.memory.footprint (7.6), read at most once a second; apsio.device.thermal_state and apsio.device.low_power (7.8), from the platform’s change notifications; apsio.battery.level and apsio.battery.charging (7.9) only when battery monitoring is already on, which live inspection never turns on.
  • Uploads go at most once a second, and only when there is something new: a log line or a changed wireframe. Metrics ride along; an upload of metrics alone goes at most every 5 seconds.

A viewer can send a watched device one of three commands, each with empty args, and no other. None runs code or changes the app’s state. A device answers an unknown command, or one that does not apply (a log level already raised), with outcome: "refused".

Command On the device result
screenshot One wireframe of the current screen, as 16.5 takes them wireframe. It goes to the viewers only, never in frames
diagnostics Once, off the main thread, the attributes of an apsio.diagnostics log record, which goes through ingest The record’s attributes
raise_log_level For 30 minutes, in memory only: the SDK writes log records from DEBUG up, whatever capture.logs_min_severity says, and releases every record of logs, spans and network through ingest, whatever sampling and triggers say, as a capture request releases them (15.5). The log bridges keep their own floors (13.7) until, the Unix ms when it ends
  • The apsio.diagnostics record carries apsio.live.command.id (the command’s id), apsio.memory.footprint, apsio.storage.app, apsio.storage.data and apsio.storage.cache (7.9), apsio.device.thermal_state (7.8), network.connection.type (7.10), and the SDK’s queue (section 4): apsio.queue.records and apsio.queue.bytes, the records and bytes in its queue not yet sent (section 4), the batches held before the first policy (15.3) included, and uploads already handed to the operating system not.
  • A raised log level ends after 30 minutes, at the next launch (it is kept in memory only), and when consent, live inspection or the kill switch withdraws it (16.1). The live session ending does not end it. It never releases a family that is off (16.5).
  • Records that go through ingest because of a command carry apsio.sampling.released = live (9.3): they are stored, retained and erased like every other record, and never counted in estimates.

A backend that offers live inspection:

  • MUST open a live view only of a device whose install or app user the project marked as a tester, and only for a person it has signed in: never for a project token, an upload token or a service credential.
  • MUST count a hello’s tester only when the app’s policy lists that id, its expires_at has not passed, and the lowercase hex SHA-256 of the hello’s proof equals the entry’s match (16.1): one rule for both kinds. A hello with no proof, or one that is not 64 lowercase hex characters, has a tester that does not count (not a 400). A hello whose tester does not count is answered { "viewers": 0, "interval_s": s }, with no device_token; no viewer sees the device, and the hello neither makes, renews nor replaces the install’s token or presence, so it cannot end another sender’s live session. The function live.tester (B.2) pins the check.
  • MUST NOT keep a hello’s proof beyond the check: the presence keeps the tester only.
  • MUST give each live.testers entry an id unique in the list, and MUST NOT use it as any other id in the policy, a request’s (15.5) or a layer’s: a request’s match is the SHA-256 of its id, a colon and its subject, which is a tester’s proof when the id and the subject are the same, so the policy would publish it.
  • MUST accept commands only from people it allows to change the project’s settings, and only for a device in a live session.
  • MUST, when a hello replaces the device token while a live session runs (from its first frames on), end that session for its viewers with the reason replaced, so no viewer sees another party’s frames without being told. A token replaced before any frames used it (a hello retried after a lost answer) ends nothing. The next session then starts as any other does, with its own 30 minutes and 10,000,000 bytes; the device’s day cap (16.3) still holds, so a replace is no way around the caps.
  • MUST NOT store the live stream: the lines, metrics and wireframes of frames, and a screenshot, go to the viewers watching and stay in memory only, gone when the live session ends.
  • Stores the records that come through ingest with apsio.sampling.released = live as it stores any other.
  • MUST NOT keep a tester’s hash beyond the tester’s entry and the device’s presence on the channel, and MUST delete the tester’s entry when the app user’s data is erased.
  • MUST, when it serves production devices, publish only an https:// live.origin, and refuse any other in its configuration: the loopback http:// an SDK may accept (16.4) is for a local development deployment only, so a served policy never points a device at plain HTTP.