artisan-build / telltale-contracts
Versioned wire contracts shared by the Telltale client and server.
Package info
github.com/artisan-build/telltale-contracts
pkg:composer/artisan-build/telltale-contracts
Requires
- php: ^8.4
Requires (Dev)
- laravel/pint: ^1.27
- pestphp/pest: ^4.7
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Versioned wire contracts shared by the Telltale server and NativePHP client.
Envelope v1
EnvelopeV1 serializes this JSON object:
{
"envelope_version": 1,
"client_version": "1.0.0",
"dropped_events_total": 0,
"events": [
{
"event_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"name": "checkout.completed",
"type": "event",
"ts": "2026-10-03T12:34:56.123Z",
"session_id": "session-1",
"props": {}
}
]
}
The envelope version is exactly 1, client_version is non-empty, dropped_events_total is a
non-negative integer, and events is a list. The drop count is cumulative per install rather than
per batch. A server can therefore persist the latest count without counting the same drops again
when a batch is replayed.
Each event has a canonical uppercase ULID event_id, a non-empty name, one of the exact event
types screen, event, error, session_start, session_end, or context, an RFC3339 ts, a
non-empty session_id, and a JSON-object props. An error object is required only when type is
error; other event types reject it. Error details contain a non-empty class, message, non-empty
file, positive integer line, list of string stack frames, and optional non-empty string
fingerprint.
Limits
Limits are measured in bytes and are fixed for v1:
| Value | Maximum |
|---|---|
| Every property-object key, recursively | 128 bytes |
| Every property string value, recursively | 4,096 bytes |
| Error message | 4,096 bytes |
| Error file | 1,024 bytes |
| Error stack frames | 100 |
| Each error stack frame | 2,048 bytes |
Properties may otherwise contain JSON objects, lists, strings, finite numbers, booleans, and null.
Non-finite numbers and PHP values that JSON cannot represent are rejected. Empty props always
serializes as {}.
Compatibility And Identity
Decoding ignores unknown object fields so producers can add fields without breaking a v1 consumer.
Malformed JSON, invalid shapes or values, and unsupported versions throw ContractException.
EnvelopeV1::versionFrom() reads the declared positive version without requiring v1 decoding, so a
server must inspect the version first and can return an actionable response for a newer producer.
Install tokens are header-only credentials. Ingest values or keys, app IDs, and install IDs are not members of the envelope or event body. Unknown fields with those names are ignored during decoding and are never emitted during encoding. Runtime transport, authentication, and persistence are outside this package.
Envelope v2 and mobile crashes
EnvelopeV2 uses the same headers and ordinary event fields as v1. It adds type: "crash"
with required typed CrashDetails in the event's crash field. A batch may mix ordinary and
crash events. Error and crash details are mutually exclusive. V1 rejects crashes, and its
ordinary JSON encoding is unchanged. Existing Event constructor calls still work; crash
is an optional final argument.
use ArtisanBuild\TelltaleContracts\EnvelopeDecoder; use ArtisanBuild\TelltaleContracts\EnvelopeV2; $envelope = EnvelopeDecoder::decode($json); // EnvelopeV1 or EnvelopeV2 if ($envelope instanceof EnvelopeV2) { foreach ($envelope->events as $event) { if ($event->crash !== null) { $fingerprint = $event->crash->fingerprint(); // Recompute on the server. $retryEventId = $event->crash->eventId(); } } }
Crash schema 1 requires a lowercase SHA-256 report_id, platform, kind, source, machine reason,
fatal flag, occurrence timestamp, timestamp precision and positive occurrence count (at most
1,000,000). Event ts equals occurred_at. Window-end timestamps require both reporting-window
endpoints and equal the end. Optional app version/build describe the crashed build; unknown
values are omitted. MetricKit hangs are nonfatal, while Android ApplicationExitInfo ANR deaths
are fatal. Other accepted crash, memory-exit and watchdog-exit reports are fatal.
CrashFrame retains binary UUID/build ID, name, address, offset, load address and symbol inputs.
Addresses are canonical unsigned 64-bit hex strings, with precision-safe subtraction to derive
relative/load addresses where inputs permit. Fingerprints use platform/kind/reason and up to five
frames, preferring UUID plus offset for native frames and class/method symbols for JVM frames.
They exclude absolute ASLR addresses and trace/exception messages. Empty frames use a
platform/kind/reason fallback. eventId() converts the first 128 digest bits to a stable canonical
ULID for retries; it does not encode the occurrence time.
The limits are 64 frames, 255-byte binary text, 1,024-byte symbols, 16,384-byte sanitized UTF-8
trace text and 16 scalar metadata keys (strings at most 1,024 bytes). Unknown crash/frame keys,
opaque tombstone/base64 fields, binary text, invalid types and contradictory values throw
ContractException. Native projection and privacy callbacks must remove sensitive text before
import. This package does not parse native tombstones, upload reports, or trust body app identity.
The mobile crash contract specifies the field rules, native handoff/acknowledgment, retention, byte-bounded delivery and planned server/MCP behavior. Those delivery paths are not implemented by the contracts package.