Skip to content

1. Transport

The profile uses OTLP over HTTP, unchanged. A standard OpenTelemetry exporter that can set headers can send conforming data.

  • MUST send to the standard OTLP/HTTP paths: /v1/logs, /v1/traces and /v1/metrics.
  • SHOULD encode as OTLP/JSON with Content-Type: application/json, compressed with gzip (Content-Encoding: gzip). JSON keeps the SDK free of a protobuf runtime.
  • A backend MUST accept both OTLP/JSON and OTLP/protobuf (application/x-protobuf), with or without gzip.
  • A backend MUST follow the OTLP/JSON rules: 64-bit integers as decimal strings, trace and span ids as hex strings, enum values as integers.
Header Required Value
Apsio-App-Key MUST The app key. It routes data to a project.
Apsio-Batch-Id MUST A unique id for the batch, kept across retries (section 4).
Apsio-Sent-At MUST The device time when the request is sent, in Unix nanoseconds, as a decimal string (section 5). Omitted only when the operating system sends the request on the SDK’s behalf (5.1).
  • The app key ships inside the app binary, so it is not a secret. It travels in its own header, not in Authorization, because proxies and logging tools treat Authorization as a credential. Headers use no X- prefix (RFC 6648).
  • A generic OpenTelemetry exporter can send the key with OTEL_EXPORTER_OTLP_HEADERS=apsio-app-key=<key>. Such an exporter will not send Apsio-Batch-Id or Apsio-Sent-At; a backend MUST still accept its data, without batch deduplication or clock correction for it.
  • Apsio-Batch-Id MUST be 16 to 64 characters from [A-Za-z0-9_-]. A UUID or a ULID without separators both fit. A backend MUST reject a request whose batch id does not match this format with 400.
  • MAY use a host derived from the app key, for example a-k3f9q2x7.ingest.example.com, so a backend can move one app to another region or cluster without an SDK release. The host only routes: a backend MUST still check the key header, and MUST reject a key that does not belong to the host with 403.
  • An app key used in a host name MUST be a valid DNS label: lowercase letters, digits and -, at most 63 characters, starting with a letter.
  • An SDK MUST let the app override the endpoint, for proxies and self-hosting.
  • An SDK SHOULD keep each request at or below 1 MiB after compression. A backend MUST accept requests up to that size.
  • Payloads that do not fit (full crash reports, replay segments) are uploaded separately, as attachments, and referenced from the record by apsio.attachment.id (1.7, section 6, section 10).

A backend answers as OTLP/HTTP specifies, with these cases made explicit:

Status When SDK behavior
200 Accepted, including a batch already received (a duplicate) Delete the batch from the queue
200 with partial_success Some records were rejected Delete the batch; do not resend the rejected records
400 Malformed body, unknown encoding, invalid batch id Delete the batch; resending cannot succeed
401 Apsio-App-Key missing or unknown Keep the batch; stop sending until the next launch
402 The account the key belongs to cannot ingest until it is paid for, such as a trial that ended Keep the batch; stop sending until the next policy fetch
403 The key does not belong to the host, or the app is disabled Keep the batch; stop sending until the next policy fetch
413 The request is larger than the backend accepts Delete the batch
408, 429, 500 to 599 A request timeout, throttling, a server error or a backend temporarily unavailable Retry the same batch later, with backoff
Any other status Not an Apsio answer: a redirect, a proxy with a wrong path (404) or asking for credentials (407) Retry the same batch later, with backoff
  • A retry MUST use exponential backoff with jitter, starting at no less than 5 seconds and capped at no more than 30 minutes. Attachments have a backoff of their own for some answers (1.7).
  • Only 400, 413 and a 2xx delete a batch. Any status this table does not name is retried, so a misconfigured proxy or endpoint cannot delete telemetry; the queue’s size cap and maximum age (4.1) bound what is kept.
  • MUST honor Retry-After on a response that is retried, in both of its forms (RFC 9110, section 10.2.3): delay-seconds and an HTTP-date. The SDK waits the longer of its backoff and the time Retry-After gives, up to the same 30-minute cap: a longer value counts as 30 minutes. A date in the past, or a value that does not parse, leaves the backoff unchanged.
  • SHOULD turn an HTTP-date into a delay against the response’s Date header when it has one, because the device clock can be wrong (section 5).
  • A send that the app asks for (flush(), section 13), or that a move to the background triggers (4.4), MAY skip the backoff, of batches and attachments alike: the app or the user bounds how often it happens. It MUST NOT skip a Retry-After that is still running, nor the wait after an attachment found no route (1.7).
  • A backend MUST answer a duplicate batch with 200, so the SDK deletes it.
  • A backend that answers 402 SHOULD also serve the kill switch in the app’s policy (12.3) for as long as it does, so the SDK stops collecting and drops its queue at the next fetch. Until that fetch the batch is kept: if the account is paid for meanwhile, the next policy has no kill switch and nothing was lost.
  • A rejected request MUST have a body that says why, as an OTLP Status message.

One long attribute value (a response body logged as an attribute, a large global attribute) can make a record larger than a request may be (1.4); a backend refuses such a request with 413 and the SDK deletes the whole batch (1.5). An SDK bounds every attribute value before the record is queued:

Attribute Each string at most
exception.stacktrace, apsio.crash.images 128 KiB
exception.message 8 KiB
Any other 4 KiB
  • MUST cut a string value longer than its limit, counted in UTF-8 bytes, to at most the limit. The cut value ends in …[truncated] (U+2026, then [truncated], 14 bytes in all, counted within the limit), so a reader knows it was cut.
  • MUST NOT cut apsio.crash.images as a string: a cut JSON array breaks symbolication. Past its limit, the SDK leaves out whole images instead, as crash.images_kept does (appendix B.2): it orders the images referenced by the crash’s frames first, in the order the stack reaches them, then the others in their order, keeps the longest run from the first that fits, and sets apsio.crash.images_truncated to true.
  • MUST NOT cut inside a character: the cut falls on a code point boundary, or earlier on a grapheme cluster boundary.
  • MUST keep at most the first 64 values of an array, each string in it cut to 4 KiB.
  • Integers, doubles and booleans are not cut. The record’s body (a log message) is not cut either; a record larger than a request may be is sent in a batch of its own (4.2).
  • The limits apply to every record: logs, handled errors, crashes, spans and metric points, with the global attributes the SDK adds to them.

An attachment is a payload too large or too opaque for a log record: a replay segment (10.2), a full crash report (6.1). It is uploaded on its own, and a record points to it with apsio.attachment.id. The backend keeps attachments in object storage, never in the log store.

  • MUST upload with POST /v1/attachments on the ingest host (1.3), with these headers:
Header Required Value
Apsio-App-Key MUST As in 1.2
Apsio-Attachment-Id MUST 16 to 64 characters from [A-Za-z0-9_-], generated once and kept across retries; the value of apsio.attachment.id
Apsio-Attachment-Kind MUST replay.segment or crash.report
Apsio-Session-Id MUST The session.id the attachment belongs to
Apsio-Sent-At SHOULD As in 1.2
Content-Type MUST application/json for replay.segment and crash.report
Content-Encoding SHOULD gzip
  • An attachment MUST be at most 4 MiB as sent. A replay segment is far smaller (10.1).

  • The responses and the retries are those of 1.5, 402 and 403 included, except that a retried answer without Retry-After holds back attachments only, so a backend without the attachment route never stops the batches:

    • on 404, 405, 410 or 501 (no attachment route there), the SDK MUST wait at least one hour before the next attachment, doubling up to 24 hours, with jitter (between 1 and 1.5 times the wait), so such a backend is not called often;
    • on any other retried answer without Retry-After (408, 429, 5xx, a redirect), attachments back off as 1.5 says, on their own;
    • batches keep being sent meanwhile; a 2xx, 400 or 413 for an attachment resets its backoff;
    • 401, 402, 403, a network failure and a Retry-After stop all uploads, batches and attachments, as 1.5 says.

    An attachment is kept on disk until it is accepted or deleted, and dropped once it is older than the queue’s maximum age (4.1). Attachments count in the queue’s size cap, or MAY have a cap of their own. When either cap is reached, the SDK drops the oldest attachments first, and drops a batch only to make room for batches.

  • SHOULD upload an attachment before the record that points to it, so a stored record rarely points to nothing. A backend MUST NOT reject a record whose attachment has not arrived: it may arrive later, or never, and a player shows a gap.

  • A backend MUST store an attachment once per app and id: a second upload with the same id gets 200 and changes nothing.

  • A backend MUST delete an attachment with the records of its session, and keep it no longer than the app’s retention for those records.

  • An attachment is uploaded through the platform’s background transfer when the app moves to the background, like a batch (4.4).