Skip to content

C. Changelog

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.id on 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-Id header, and no re-batching after the first send;
  • hot starts, 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.fingerprint on handled errors;
  • the language-neutral SDK API;
  • the 30-minute and 4-hour session limits, the 15-minute policy interval and the Apsio-Sent-At header 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.id for 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) omits Apsio-Sent-At; a WorkManager job runs SDK code and sets it (5.1);
  • device.anr, Android’s hang record, is never sampled, like apsio.hang (9.2);
  • 408 and every 5xx are retried with backoff, any other 4xx deletes the batch, and Retry-After is honored as delay-seconds or an HTTP-date, up to the 30-minute backoff cap (1.5); the vector delivery/sdk-retry-server-error checks that a batch answered with 500 is 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.images set to [], and is deobfuscated with the mapping app.build_id names (6.1);
  • the terminate step with how: "normal" on a platform with no termination callback (B.5);
  • apsio.exit.reason is a closed list: a native crash with no crash report is signal, and excessive resource use or any other unlisted reason is unknown (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.