Skip to content

B. Conformance

An implementation conforms to a version of the profile when it passes every vector for that version that applies to it. A vector is a scripted situation and the exact result the implementation must produce. The Apsio SDKs and the Apsio ingestor run the same vectors in CI; any other OpenTelemetry SDK or backend can run them too.

Vectors live in vectors/v0.1/<area>/<name>.json and validate against schemas/v0.1/vector.schema.json. They are published with each version as vectors-0.1.tar.gz, with the schemas. Vectors, schemas and runners are Apache-2.0.

Kind Who runs it Input Expected
function SDKs and backends Values for a pure function of the profile (the sampling decision, clock correction) The function’s output
ingest Backends HTTP requests, each with the time it is received Each response’s status, the normalized records stored and, when relevant, the session states
sdk SDKs Steps: launches, API calls, clock changes, network conditions The requests the SDK sends

Every vector also has given, when and then: the same situation in plain sentences, for people.

Function Input Output
sampling.keep session_id, rate true or false (9.1)
clock.corrected original, sent_at (or null), received_at The corrected time (5.2)
crash.images_kept images (each a name, the bytes of its JSON entry as the SDK encodes it, and frame, the index of the first stack frame in it, or null) and limit kept, the names in the order sent, and truncated (1.6)
crash.image_in_app platform (apple or android), path (the image’s resolved path) and, on Apple platforms, bundle (the crashed process’s main bundle path, an extension’s own), on Android package (the package name) true or false (6.5)
screen.views shows (each a session, the screen name and the screen’s instance) The views sent, in order: each screen and its previous (absent for a session’s first) (8.6)
replay.text_mask text The masked text (10.3)
replay.apply_delta nodes (a frame’s nodes) and delta (its update, remove and add) The nodes after the delta, by ascending id (10.1)
replay.mask options, policy (optional), unresolved (optional) and tree Per node id, in tree order: content (shown, masked or hidden) and touches (true or false) (10.4)

A replay.mask tree is neutral: each SDK maps its views onto it. A node has an id, a kind (container, text, input, image, webview, map, video or keyboard; container when absent), classes (the class names it belongs to), setting (mask, unmask, hide or null, the instance API), identifier (the accessibility identifier or tag), secure and children. options has text (all or inputs), touches (show or hide) and mask_classes, unmask_classes, hide_classes; policy has the policy’s text, mask_classes and hide_classes; unresolved (optional) lists the class names that do not resolve in the app, which start strict mode when a hide or mask list names one (10.4).

Times are decimal strings of Unix nanoseconds.

A backend stores data its own way. To compare backends, an ingest runner converts each stored record to this neutral form:

Field Value
signal log, span or metric
app_key The app key the record arrived with
batch_id The Apsio-Batch-Id of the request, or ""
session_id Per 2.4, or ""
event_name Logs: the event name, or ""
name Spans and metrics: the span or metric name
severity_number Logs
body Logs: the body as a JSON value
time { "original", "observed", "corrected" }: logs use timeUnixNano; spans and metric points use their end time
start_time Spans and metric points: { "original", "corrected" }
resource The Resource attributes as an object, with the overrides of 2.5 applied
attributes The record attributes as sent, as an object
sample_weight Per 9.3
metric Metric points: type (histogram, sum, gauge), temporality (delta, cumulative), and the point’s count, sum, min, max, bucket_counts, explicit_bounds or value, each only when present

Attribute values convert from OTLP AnyValue to JSON: strings, booleans and doubles as themselves, integers as numbers, arrays as arrays, key-value lists as objects, and bytes as base64 strings. Times are decimal strings.

An ingest vector can also expect session states: per session, the outcome the backend holds after all requests (3.4).

A runner compares the actual result with the expected one by these rules:

  • Objects match as a subset. Every key in the expected object must be in the actual one with a matching value; other actual keys are ignored.
  • Arrays match exactly, element by element, with the same length.
  • Expected records in an ingest vector match as a set. The stored records must be exactly as many as the expected ones, and each expected record must match a different stored record, in any order.
  • Expected records in an sdk request match as “contains”. Each expected record must match a different record of the request; the request may carry others (session events, breadcrumbs). No record of the request may match an entry of absent_records.
  • Expected requests in an sdk vector match in order, as a subsequence of the requests the SDK sent: each expected request matches the next sent request that matches it. Requests to /v1/policy are ignored unless the vector expects one.
  • Captures are tried in the order the expected values are written. When a set or “contains” match can pair records in more than one way, the runner tries the pairings until one satisfies every capture and reference.
  • Special values in the expected side:
Value Matches
{ "$absent": true } The key is missing
{ "$type": "string" } Any value of that JSON type (string, number, boolean, object, array)
{ "$capture": "name" } Any value, remembered as name
{ "$ref": "name" } The value remembered as name
{ "$not": "name" } Any value other than the one remembered as name
{ "$gt": "name" } A time (decimal string) later than the one remembered as name

An sdk vector drives the SDK through a test harness that controls the clock, storage, process lifecycle and network. Draft 0.1 defines these steps; each SDK maps them to its platform in its own test driver.

Step Does
{ "do": "install" } A fresh install: storage is empty
{ "do": "launch" } A cold start of the process; the SDK is started with the vector’s start options and fetches the policy before the step ends
{ "do": "terminate", "how": "normal" | "crash" | "kill" } Ends the process: a normal exit, a crash in the app’s code, or a kill without warning
{ "do": "background" } / { "do": "foreground" } Moves the app between states
{ "do": "advance", "seconds": n } Moves the device clock forward
{ "do": "network", "mode": "online" | "offline" | "timeout" | "status", "status": n } How the next requests are answered: 200, no connection, a timeout, or a given status
{ "do": "call", "api": "<name>", "args": { … } } A call of the public API (13), or of recordHybridCrash for a cross-platform layer (14.1); args.attributes, when present, is an object of attribute values: strings, numbers, booleans and arrays of strings
{ "do": "flush" } Asks the SDK to send what is queued, and waits until it has tried

On a platform with no termination callback (Android), the driver models { "do": "terminate", "how": "normal" } as a move to the background followed by the end of the process, with the platform’s record of a normal exit for the next launch to read (on Android, an ApplicationExitInfo such as REASON_EXIT_SELF or REASON_USER_REQUESTED).

The harness answers GET /v1/policy with the vector’s policy document, or with 404 when the vector has none (the SDK then uses the profile’s defaults).

Each expected request has path, headers (matched without regard to case), records and absent_records. A request’s records are flattened from the OTLP body, whatever its grouping into Resources and scopes, to: event_name, severity_number, body, session_id (per 2.4), name (spans and metrics), attributes and resource.

The repository ships a validator for the vector files (pnpm validate). Runners for Rust (the Apsio ingestor), Swift and Kotlin load the vectors of a pinned version and run them in each implementation’s CI.