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).
16.1 The two keys
Section titled “16.1 The two keys”A device takes part only when both keys are turned:
-
The project marks it as a tester. The policy’s
live.testerslists the testers, at most 100. Each entry has anid, amatchand anexpires_at(Unix seconds); an entry whoseexpires_athas 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’sapp.installation.idor the app user’suser.hash, as set; - its
matchis 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’smatch. Its hello sends that entry’sidand proof (16.4), never the install id it came from or theuser.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 butlive.origin. Noliveobject in the policy means live inspection is off. - an entry’s proof is the lowercase hex SHA-256 of the UTF-8 bytes of its
-
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
startMUST be kept, and applies atstart: 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.testersentry 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.
16.2 The indicator
Section titled “16.2 The indicator”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.
16.3 When the channel runs
Section titled “16.3 When the channel runs”-
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_sseconds (16.4), except under Low Data Mode or Data Saver, where it sends nothing (4.4). A hello says, with itsreason, 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 aframesbody withstate: "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
willResignActiveanddidBecomeActive; 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, oneframesbody withstate: "paused"and the reason, then nothing,nextincluded, until it resumes, which it says the same way withstate: "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); nextanswers with astop(stop);- the session has stayed paused for 60 seconds (
paused); - the session’s end comes (
until): 30 minutes after its firstframes, unless a viewer renews it. The device keeps that end on its own monotonic clock,next’sremaining_msafter the answer arrived; - the bodies of
framesandresultsthe 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
framesbody withstate: "ended"and that reason, and goes back to hellos while the keys hold, except onclosed, where it sends nothing more (16.1): the backend learns of that end from 60 seconds withoutframesornext(below), or from the next hello. The functionlive.stop(B.2) pins these limits and their order. - the channel closes (16.1), or the app moves to the background
(
-
No restart by itself. A viewer who stays does not start the next session:
- after a session ends at
untilorsession_bytes, the backend MUST answer hellos withviewers0 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’ssetLiveInspectionchoice; live data itself never is (16.4).
- after a session ends at
-
A backend treats a live session with no
framesand nonextfor 60 seconds as ended.
16.4 The wire
Section titled “16.4 The wire”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
2xxanswer to a hello whose tester counts (16.7) carriesdevice_token, 64 lowercase hex characters; a device given any other shape, or none, keeps none. A device that held a token and gets a2xxhello answer without one drops it and ends its live session at once, without anendedbody, and cancelsnext: its tester no longer counts (an entry removed, or expired first on the backend’s clock).next,framesandresultsMUST carry it in theApsio-Live-Tokenheader.- 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, whateverviewerssays;nextcarriesviewersandstopfor 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,framesorresultsthat 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 gets401and starts over. - On a
401fromnext,framesorresults(the token is missing, wrong or replaced), the device ends its live session at once, without anendedbody (its token can no longer send one), cancelsnext, drops the token, and says hello again. That hello goes at once, unless the device already had a401fromnext,framesorresultswithin the lastinterval_s; then it goes at the nextinterval_s. Two senders that share an install therefore take the session from each other at most once everyinterval_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 butlive.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.
- Keep and renew. A hello may carry the current token in
- 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
idat 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,403and503mean what they mean on ingest (1.5): a401there is an unknown app key, and the channel stays closed until the next launch. A backend MUST NOT answer a hello401for its token: a missing or other token replaces it. Onnext,framesandresults, a401is the device token (above), and402,403and503mean what they mean on ingest. Onresults, a409is a result for no command the device was sent, or for one past its 2 minutes: the device treats it as a2xxfor the result it keeps and does not send it again.400is a body the backend cannot read;404and405are an unknown route or method. - No redirects. The SDK MUST NOT follow a redirect on the live channel: a
3xxis a failed request, as a5xxis. Following one would carryApsio-App-KeyandApsio-Live-Tokento another host. Its HTTP session keeps no cache and no cookies. - On
429,5xxor3xx:- the next hello goes after
Retry-After, at leastinterval_slater, backing off exponentially to 120 seconds while it keeps failing; - during a live session, the device pauses
nextandframesuntilRetry-After.
- the next hello goes after
- Nothing is buffered. Live data is never queued, written to disk or retried: a lost
framesbody is lost, and the viewers see a gap. Only aresultsbody is sent once more, after the backoff, and only with the current token: one refused with401is not sent again, since the command comes again in the next session and gets its kept result.
16.5 What the device sends
Section titled “16.5 What the device sends”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 with400) are copies of the log records the SDK writes for thelogsanderrorsfamilies 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, whenreplayis 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_stateandapsio.device.low_power(7.8), from the platform’s change notifications;apsio.battery.levelandapsio.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.
16.6 Commands
Section titled “16.6 Commands”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_ |
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.diagnosticsrecord carriesapsio.live.command.id(the command’sid),apsio.memory.footprint,apsio.storage.app,apsio.storage.dataandapsio.storage.cache(7.9),apsio.device.thermal_state(7.8),network.connection.type(7.10), and the SDK’s queue (section 4):apsio.queue.recordsandapsio.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.
16.7 The backend
Section titled “16.7 The backend”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
testeronly when the app’s policy lists thatid, itsexpires_athas not passed, and the lowercase hex SHA-256 of the hello’sproofequals the entry’smatch(16.1): one rule for both kinds. A hello with noproof, or one that is not 64 lowercase hex characters, has a tester that does not count (not a400). A hello whose tester does not count is answered{ "viewers": 0, "interval_s": s }, with nodevice_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 functionlive.tester(B.2) pins the check. - MUST NOT keep a hello’s
proofbeyond the check: the presence keeps thetesteronly. - MUST give each
live.testersentry anidunique 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’smatchis 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
frameson), end that session for its viewers with the reasonreplaced, so no viewer sees another party’s frames without being told. A token replaced before anyframesused 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=liveas 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 loopbackhttp://an SDK may accept (16.4) is for a local development deployment only, so a served policy never points a device at plain HTTP.