Files
EscapistsReintroduced/docs/telemetry.md
T
2026-10-28 14:43:43 +01:00

187 lines
9.4 KiB
Markdown

# Mission telemetry
Mission → Statistics contains the administrator toggle. `telemetryEnabled` is
saved in the `mission` section of `$profile:Escapists.json` and defaults to `1`,
including when upgrading an older configuration. The page explains the data
collected and shows delivery status. Existing administrator authentication and
save-before-apply behavior also apply to this setting.
Telemetry runs only on the mission authority, never independently on joining
clients. Workbench runs do not collect unless explicitly launched with the
`errTelemetryTest` diagnostic flag. There is no statistics website or production
receiver included in this change.
## Receiving URL and configuration
The bundled receiving URL belongs in `addons/core/Configs/ERR/Telemetry.conf`.
It is currently empty pending the receiver's actual URL. Until configured, no
telemetry is collected or sent, even with the switch enabled. The menu reports
this condition rather than claiming that statistics are being delivered.
An operator can override the URL and optionally name/authenticate their instance
with `$profile:EscapistsTelemetry.json`:
```json
{
"endpoint": "https://YOUR-HOST/api/mission-events",
"serverName": "My Escape server",
"token": "receiver-issued-instance-token"
}
```
Use the full POST endpoint, with its exact trailing-slash convention. HTTPS is
required. URL credentials, fragments and query parameters are rejected. The
optional token goes in the HTTPS JSON body, never in a URL, menu snapshot, log,
Workshop resource, or persisted event queue. Configuration loads when the mission
starts. An empty endpoint overrides the bundled default and prevents sending.
`serverId` is a generated UUID persisted in
`$profile:EscapistsTelemetryState.json`. Keep this file/profile for continuity;
use different profiles for different server instances. Do not clone this state
file into a second server. `serverName` is a display label, not an identity key.
## Wire format
One asynchronous JSON POST per event, `Content-Type: application/json`. The
receiver must accept chunked request bodies (used by the native HTTP client):
```json
{
"token": "receiver-issued-instance-token",
"event": {
"schemaVersion": 1,
"eventId": "3f16ef25-13f7-46ad-967f-5e84f36cb84b",
"missionId": "a82b4f20-8f32-4978-80f6-c54099185c58",
"serverId": "74f74822-cf6d-4fc1-856c-1768afe025f4",
"serverName": "My Escape server",
"type": "mission_finished",
"occurredAt": 1792936800,
"elapsedSeconds": 3521,
"partial": false,
"phase": 5,
"outcome": "victory",
"previousMissionId": "",
"previousOutcome": "",
"terrain": "{RESOURCE_GUID}Worlds/Example.ent",
"scenario": "Escape",
"gameVersion": "engine build version",
"mods": [{"id": "2528466DE220D91C", "name": "Co10 Escape Reintroduced", "version": ""}],
"modsTruncated": false,
"playersTruncated": false,
"connectedPlayers": 2,
"peakPlayers": 4,
"players": [
{"id": "game-account-UUID", "name": "Player", "authenticated": true,
"connected": true, "participated": true, "escaped": true,
"connectedSeconds": 3200}
],
"reason": "Escape complete: 2 escaped; 2 did not escape.",
"preparationError": "",
"settings": {"telemetryEnabled": 1},
"droppedEvents": 0
}
}
```
The example abbreviates the players and settings arrays/maps. Actual `settings`
contains the saved mission setting values; `-1` retains its meaning of Random,
not an observed random roll. `phase` is 0 preparing, 1 prison, 2 radio search,
3 radio defence, 4 extraction, 5 victory, 6 defeat. `mods` describes mounted
non-vanilla addons; version is the locally active Workshop revision when exposed
by the engine, otherwise empty. Never infer the newest Workshop version from an
empty value. Terrain is the actual loaded world resource, including takeover
scenarios, rather than the next-round terrain preference.
## Event and outcome semantics
| Event | Meaning |
| --- | --- |
| `mission_started` | This mission instance began collection, usually during preparation. |
| `mission_restarted` | A subsequent instance started in the same profile; links the previous mission and its last known outcome. Also followed by `mission_started`. |
| `phase_changed` | An actual nonterminal mission transition. Use elapsed times for milestone durations. |
| `heartbeat` | A snapshot every five minutes while unfinished. |
| `telemetry_resumed` | Re-enabled during the same mission; the mission ID stays the same and `partial` is true. |
| `mission_finished` | Exactly one local result snapshot, before normal teardown. |
| Outcome | Count as a gameplay result? |
| --- | --- |
| `victory` | Yes, successful mission. |
| `defeat` | Yes, failed mission, including administrator abandonment. |
| `preparation_failed` | No, startup error; inspect `preparationError`. |
| `restarted_empty` | No, automatic last-disconnect or idle-empty restart. |
| `interrupted` | No, session ended without a gameplay result. |
| `unfinished` in `previousOutcome` | No confirmed ending survived. May be a crash/forced stop. Do not infer a defeat. |
Player names and stable game account UUIDs support history across servers. No IP
addresses, chat, inventories, precise player positions or credentials are in an
event. Names can change; aggregate by authenticated `id`, not `name` or temporary
connection number. Missing backend identities use a mission-local connection key
with `authenticated: false`; exclude those keys from global player rankings.
`players` retains observed participants and spectators, including disconnected
players. `connected` records the end/snapshot connection state, `participated`
records whether the mission admitted a life, and `escaped` uses the authoritative
extraction roster. A connected spectator or a left-behind participant is not an
escapee. Count individual completions using `escaped`, not the mission's victory
alone. `connectedSeconds` is sampled connection time (roughly five-second
precision), not survival time. Reconnects reuse the authenticated identity.
## Reliable ingestion and aggregation
Return any **2xx only after durable storage**, including for an already accepted
event. Other responses/timeouts retry the identical event with the same event ID.
The receiver must enforce uniqueness on `(serverId, eventId)` and aggregate by
`(serverId, missionId)`. Network delivery is at-least-once, not exactly-once.
A crash between acceptance and local acknowledgement can produce a duplicate.
Suggested tables:
- `events`: unique server/event key, mission key, event time, type, JSON payload.
- `missions`: unique server/mission key, terrain, start/end, outcome, peak count,
partial flag; a repeated start/restart event must not reset a terminal outcome.
- `mission_players`: unique server/mission/player key, name, participation,
connected-at-end, escaped, sampled connected duration.
Useful queries from terminal mission snapshots:
- Per-server failures: count distinct missions with `outcome = 'defeat'` grouped
by `serverId`; add terrain for per-map difficulty.
- Global failures/wins: the same counts across all server IDs, without counting
startup errors, empty restarts or unknown/interrupted sessions as defeats.
- Player completions: count distinct missions where an authenticated player's
final `escaped` flag is true. Also expose attempts, left-behind results,
connected-at-end rosters and best successful duration if desired.
- Milestone times: phase-event elapsed time differences for prison, radio and
extraction. Treat missing phases, dropped events and partial runs as unknown.
Validate schema and limits and authenticate operator-issued tokens if statistics
must be trusted. The bundled addon contains no shared secret. These are reports
from community-hosted missions, not independent anti-cheat attestations.
## Buffering, disabling and limits
Requests have a 15-second timeout, with a 30-second callback watchdog. Retries
back off from 10 seconds to five minutes. A five-second dispatch tick sends one
request at a time. Disk writes occur on events and acknowledgement, not every
player sample. Delivery never blocks mission startup or shutdown.
The profile queue holds at most 64 events of 64 KiB each. On overflow it discards
queued heartbeats first, then the oldest non-in-flight event; `droppedEvents`
reports cumulative loss. Players and mods are each capped at 128 with explicit
truncation flags. The previous valid queue generation is retained as `.bak` for
recovery. An unreadable queue suspends telemetry and preserves evidence for
repair instead of silently replacing the server identity.
Unsent final results survive normal scenario reloads and are retried in the next
enabled instance. Abrupt termination can lose the latest event; the next start's
`previousOutcome` remains unfinished if no result was saved. A permanently
stopped host cannot send further data.
Disabling immediately stops collection/new requests and discards queued events
and recovery copies. It cannot recall an HTTP request already submitted or erase
records already accepted by the receiver. Re-enabling does not reconstruct the
disabled interval, and marks subsequent events partial. Changing the receiver URL
discards unsent data instead of forwarding old records to a different recipient.
Native API references: [RestContext](https://community.bistudio.com/wikidata/external-data/arma-reforger/EnfusionScriptAPIPublic/interfaceRestContext.html) and [RestCallback](https://community.bistudio.com/wikidata/external-data/arma-reforger/EnfusionScriptAPIPublic/interfaceRestCallback.html).