C. Changelog
0.1 (draft)
Section titled “0.1 (draft)”2026-10-03. First draft: transport, resource, sessions, offline delivery, device clock, crashes, MetricKit, app start and screens, sampling, replay, privacy, remote configuration.
2026-10-04. Revised:
session.idon records, with the record winning over the Resource;- the session summary, sent for every session and never sampled;
- the batch id in the
Apsio-Batch-Idheader, and no re-batching after the first send; hotstarts, iOS prewarming as its own attribute, and TTFD;- abnormal exits from the platform’s exit records or inference;
- MetricKit through the iOS 27 Swift API, with
MX*before it, and the full-day entry only; - histograms, sums and gauges for MetricKit’s three kinds of value;
app.installation.id, allowed and switchable;- replay masking that fails closed, with overrides and one precedence order;
apsio.error.fingerprinton handled errors;- the language-neutral SDK API;
- the 30-minute and 4-hour session limits, the 15-minute policy interval and the
Apsio-Sent-Atheader as defaults.
2026-10-04, Markdown edition. The draft moved from the page mock to Markdown in
apsio-spec, and gained:
- response codes and SDK behavior per code (1.5);
- the 1 MiB request size and the batch id format;
- the session summary’s attributes and rules (3.4);
- the deterministic sampling hash (9.1) and the weights of records that are never sampled;
app.crash.idfor crash deduplication;- the policy document and its schema (12.2);
- the conformance vector format and the normalized record (appendix B).
2026-10-04, from the first SDK. Uploads handed to the operating system omit
Apsio-Sent-At (5.1); the launch step of SDK vectors
fetches the policy, and sampling/sdk-dropped-session-still-reports relaunches so the
rate applies to a session that starts after it (9.1).
2026-10-04, from the Android SDK. Revised:
- only an upload the operating system sends on the SDK’s behalf (a background
URLSession) omitsApsio-Sent-At; a WorkManager job runs SDK code and sets it (5.1); device.anr, Android’s hang record, is never sampled, likeapsio.hang(9.2);408and every5xxare retried with backoff, any other4xxdeletes the batch, andRetry-Afteris honored as delay-seconds or an HTTP-date, up to the 30-minute backoff cap (1.5); the vectordelivery/sdk-retry-server-errorchecks that a batch answered with500is kept;- the Android build id reaches the app as the manifest entry
io.apsio.build_id, written by the build integration, with<package>@<versionName>+<versionCode>when it is absent (2.1); - a crash in a managed runtime (JVM, ART) carries the runtime’s stack text and
apsio.crash.imagesset to[], and is deobfuscated with the mappingapp.build_idnames (6.1); - the
terminatestep withhow: "normal"on a platform with no termination callback (B.5); apsio.exit.reasonis a closed list: a native crash with no crash report issignal, and excessive resource use or any other unlisted reason isunknown(6.4).
2026-10-04, from the Android SDK’s review. Only 400, 413 and a 2xx delete a
batch; any status the table does not name, such as a redirect, 404 or 407 from a
misconfigured proxy, is retried with backoff (1.5).
2026-10-05, from the Apple SDK’s review. The policy schema gains upper bounds:
fetch_interval_seconds, session.background_timeout_seconds and
session.max_length_seconds at most one day, queue.max_age_seconds at most 30 days, and
version at most 2^53 - 1. An SDK rejects a document outside them and keeps its previous
policy (12.2), so a misconfigured server cannot
make a client overflow its time arithmetic.
2026-10-05, from metering. 402 joins the responses: the account the key belongs to
cannot ingest until it is paid for (a trial that ended). The SDK keeps the batch and stops
sending until the next policy fetch, as for 403; a backend that answers 402 serves the
kill switch in the policy meanwhile (1.5). The vector
delivery/sdk-keep-on-402 checks that the batch is kept.
2026-10-06, from the React Native SDK. A new section,
14. Cross-platform runtimes: JS errors and unhandled rejections as handled
errors, fatal JS errors as crashes recorded synchronously through the native SDK’s
recordHybridCrash before the framework’s handler runs, and the framework’s native fatal
error of the same session (at or after the hybrid crash minus one second) not reported
again; the attributes apsio.js.engine, apsio.js.mechanism, apsio.js.bundle_id and
apsio.js.component_stack; the JS stack format (Hermes frame lines with bundle file names,
causes, 200 frames at most); and how a backend finds a bundle’s source map. The vector
crashes/sdk-hybrid-crash-next-launch checks the crash and its deduplication.
2026-10-05, from the Apple SDK’s review. Attribute values are bounded
(1.6): 4 KiB per string, 64 values per array, with
8 KiB for exception.message and 128 KiB for exception.stacktrace and
apsio.crash.images. A cut value ends in …[truncated] and is never cut inside a
character. Log bodies are not cut. The vector transport/sdk-attribute-limits checks the
cuts. apsio.crash.images is never cut as a string: whole images are left out, the
frames’ images kept first, and apsio.crash.images_truncated says so (vector
crashes/images-limit).
2026-10-05, for performance views. HTTP client spans MAY carry url.template,
the OpenTelemetry attribute for the path’s low-cardinality template; a backend that groups
requests by endpoint uses it without any query or fragment, and when it is absent derives
one from url.full by replacing each percent-decoded path segment that looks like an
identifier or personal data (ids, phone numbers, tokens, email addresses, free text) with
{id} (8.4).
The normalized record (appendix B.3) does not change, so no vector changes.
2026-10-06, from the symbolicator. A JS record with apsio.js.bundle_id is symbolicated
only with that bundle’s map, and waits for it when it has not arrived, never with its build’s
map, which would resolve an over-the-air bundle’s frames wrongly
(14.3).
Rendering errors that no error boundary caught follow the framework’s handling (a crash in React
Native 0.80 and later, which reports them as fatal) with apsio.js.mechanism render, and
apsio.js.component_stack reduces its locations like the frames
(14.1).
2026-10-06, from the replay design (plan step 3.3). Replay frames travel in segments:
about 10 seconds of keyframes and deltas, gzipped JSON
(replay-segment.schema.json),
uploaded as an attachment, with one apsio.replay.segment log record each; a node’s rect
is in its parent’s content coordinates, so scrolling changes one scroll offset
(10.2). apsio.replay.frame, apsio.replay.frame.kind and
apsio.replay.sequence are gone. Attachments get their endpoint, POST /v1/attachments
(1.7), which full crash reports use too. Masked text is
defined exactly (replay.text_mask), so are the masking rules (replay.mask, a neutral
tree each SDK maps its views onto, 10.4), and
touches are shown only over what is shown: never over masked text, masked by default or
not, so a PIN pad of plain buttons does not leak the PIN. The policy gains sampling.replay_rate; replay is kept with the rate
session_rate × replay_rate (9.1). Sections 10.2 to 10.4
become 10.3 to 10.5.
2026-10-06, from the Android replay capture. A touch is recorded only when every node
under it shows its content (10.3): checking only the
topmost node let a shown overlay over masked text reveal the tap, as the Android fixture
showed. Attachments back off on their own for a retried answer without Retry-After, so a
backend without the attachment route never stops the batches; on 404, 405, 410 or
501 the wait starts at one hour and doubles up to 24 hours
(1.7).
2026-10-06, from the replay preview parity review. The debug mask preview is the same
on both platforms (13.2): showReplayMaskPreview(on),
red for masked (containers included), grey for hidden (web views, maps, video and the
keyboard included), a green outline where touches are recorded, 35% fill and a 1 point
stroke, whether or not the session records, and never in a release app.
2026-10-06, for performance views (draft for review). apsio.hang records carry
apsio.hang.source, live or metrickit; hang rates and the screens of a session’s hangs
use live records only, so a hang MetricKit reports a day later is not counted twice, and
Android’s hangs are live (6.3). MetricKit’s hitch time
ratios map to gauges in ms/s (7.3). A new section,
8.5 Frames: slow (over the display’s budget, refresh
rate aware) and frozen (over 700 ms) frames of the current screen’s window, from
FrameMetrics on Android (not a window’s first frame) and CADisplayLink on iOS, sent per
screen visit as the delta sums apsio.frames.total, apsio.frames.slow and
apsio.frames.frozen. iOS counts idle refreshes in the total, so totals and slow shares
compare within one platform only.
2026-10-07, from the review of the iOS hang detector. A live hang the process does not
survive is kept on disk and sent by the next launch with apsio.hang.unfinished = true
and the duration seen until then, a lower bound
(6.3).
2026-10-07, from the symbolicator (draft for review). Each apsio.crash.images entry
SHOULD carry in_app, decided on the device from where the image was loaded: inside the
crashed process’s app bundle on Apple platforms (up to the first .app component, so an
extension’s containing app); on Android, by package
name, a shared library or APK in the package’s install directory or a shared library in
its data directories. A backend uses it as sent, so a bundled vendor
framework without symbols counts as in-app before any upload
(6.5); the vector crashes/image-in-app checks the decision.
2026-10-07, from the Apple frames work. Metric points that belong to a session (they
carry session.id, as the frame counts of 8.5 do) follow the session’s sampling and carry
its rate for reweighting, like spans; MetricKit points, which belong to no session, are still
always sent (9.2, 9.3).
2026-10-07, from the replay player. A delta has one order of application
(10.1): its updates first (a new parent moves the node), then its
removals (each id with what is under it at that point), then its adds. So one delta can move
a child away from a parent it then removes, and remove an id and add it back as a new node.
The function replay.apply_delta and the vector replay/delta-order state it.
2026-10-07, for journeys and funnels. A new section, 8.6 Screen views:
an app.screen.view event each time a screen is shown, back navigation included, with
app.screen.name and app.screen.previous_name; a screen shown again without leaving it
(the background, a system dialog) sends nothing, the screen under a closing sheet is a new
view (sent by the SDK where the platform calls nothing), a session’s first view is always sent, and
it follows session sampling but not logs_min_severity. The function screen.views and the
vector screens/screen-views state which shows send one.
app.screen.load starts only when a screen is created; a screen shown again starts none
(8.1).
2026-10-07, from the Apple frames measurement. An SDK MAY stop counting frames while the screen is idle (no touch, screen change, keyboard or moving scroll) and resume on the next: a frame callback on every refresh of an idle screen cost far more than the rest of the SDK (8.5).
2026-10-07, from the Android replay review. A policy’s masking.text is absent unless
the app’s owner forces it: absent tightens nothing, "all" voids every unmask the app set, so
a backend that sent "all" by default undid every app’s unmask. The schema no longer gives
masking.text a default, the 12.2 example omits it, and an SDK MUST NOT read an absent
value as "all" (10.5,
12.2). The vector replay/mask-precedence gains a
policy that tightens classes only, under which the app’s unmask stands.
2026-10-07, from the Android replay privacy review. The policy’s mask_classes and
hide_classes win over every setting of the app, on the node and everything inside it
(10.4, 10.5):
before, an app’s instance unmask beat them, which let the app loosen what the policy
tightened. replay.mask and the vector replay/mask-precedence (a new case) follow.
2026-10-07, from the Android review of the idle pause. The frame counted when counting pauses is the one that ends then; the time until the first frame after the pause is no frame, so a resume is never frozen (8.5).
2026-10-07, from the SDK audits of 1.5 and 1.7. A send that the app asks for, or that a
move to the background triggers, MAY skip the backoff of batches and attachments, never
a Retry-After still running nor the wait after an attachment found no route: both SDKs
already skipped the backoff, and the app or the user bounds how often it happens
(1.5).
2026-10-07, from the Android replay audit. Attachments MAY have a size cap of their own instead of counting in the queue’s; either way they are dropped before any batch, and the queue’s maximum age applies to them (1.7, 4.1).
2026-10-07, plan step 3.6. Two calls of the SDK API get their behavior written down, as the
Apple and Android SDKs implement them. Feature flags (13.4):
a feature_flag.evaluation record with feature_flag.key and feature_flag.result.variant,
then feature_flag.<name> on every record and in the crash context; the conventions’ own
feature_flag.* names are not flags. Consent (13.5): withdrawing
drops everything not yet sent and holds across launches; nothing is recorded meanwhile, and a
crash of a run that ended withdrawn is never reported; giving it back starts a new session that
names the withdrawn one as session.previous_id, and the withdrawn session reports no end and
no summary. New SDK vectors: privacy/sdk-consent-pause-resume, privacy/sdk-consent-persists
and sessions/sdk-feature-flags.
2026-10-07, APS-167. A replay token in an accessibility identifier or tag counts only as a
whole word (10.4): a word is a maximal run of ASCII
letters, digits and hyphens, and it must equal apsio-mask, apsio-unmask or apsio-hide
exactly, case included. Before, any identifier containing a token counted, so a test id such
as apsio-unmasked-total unmasked its view. Underscores separate words, so a snake_case id
such as card_number_apsio-mask still masks; dotted, slashed and space-separated identifiers
keep working. replay.mask and the vector replay/mask-precedence (a new case) follow.
2026-10-07, from the Android R8 review. A class named by string, by the app or by the
policy, MUST keep that name in the app’s build (Android -keepnames; a Swift class with
its module prefix), because a name R8 renamed matched nothing and showed what it should have
hidden. A hide or mask name that does not resolve now records the session in strict mode
(every text and input masked, every image and self-drawn view hidden), with a warning and an
apsio.replay.strict_mode record listing the names in apsio.replay.unresolved_classes
(10.4). A policy’s class lists name one platform’s
classes (10.5), and a backend SHOULD warn the owner who
names one (12.2). replay.mask takes
unresolved, and the vector replay/mask-unresolved checks it.