Files
EscapistsReintroduced/docs/telemetry.md
T
2026-11-01 19:49:47 +01:00

287 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. A deployable Cloudflare Worker/D1 receiver is
provided in [services/telemetry-worker](../services/telemetry-worker/README.md).
It includes automatic server enrollment and protected mission/player query APIs.
The Worker must be deployed separately. There is no statistics website.
## Receiving URL and configuration
The bundled receiving URL in `addons/core/Configs/ERR/Telemetry.conf` is
`https://co10escape-telemetry.zacekjakub.workers.dev/api/mission-events`.
Any host running the updated mission can report without contacting the receiver
administrator or editing its configuration. Mission → Statistics remains the
administrator opt-out. This source change requires a new Workshop release;
0.0.125 does not contain automatic enrollment.
Before the first request, the host saves its own randomly generated credential
in `$profile:EscapistsTelemetryCredentials.json`. The Worker enrolls an unknown
server ID on its first valid event and stores only the credential hash. Later
requests must present the same credential. Enrollment and the initial event are
committed together. Disabling telemetry prevents enrollment and delivery and
discards pending events; the credential remains for continuity when re-enabled.
No shared secret or receiver administrator credential is bundled in the mod.
Keep the credentials file with the profile state when moving a server. Damaged
credentials suspend delivery without silently rotating them. The file retains
up to 16 server/endpoint pairs, so changing receiving URLs does not send one
receiver's secret to another. Explicit operator-issued tokens still override
automatic credentials. The default receiver accepts community reports, not
independently verified gameplay or identities.
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
explicit or automatically generated 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": "Everon",
"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 island name resolved by the reporting game server
from the loaded root scene (for example `Everon`), using the mission title
resolver. It does not use the Game Master scenario filename or the next-round
terrain preference. Reports through 0.0.129 contain world resource paths instead;
existing queued payloads retain their original values and event IDs.
## 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
The default receiver also returns public map aggregates from `GET /api/summary`
(and `GET /api/mission-events`). `terrain` filters the reported island name (or world resource in older reports);
`serverId` adds the installation's record for that map. Summaries expose counts
only; detailed player and mission records still require a read/admin token.
After durably accepting a victory or defeat, the Worker returns a `community`
summary containing matching map/server/mission/event/outcome identifiers plus
`server` and `allServers` records. Each record has completed rounds, wins,
failures, win rate and partial-round coverage. The mission validates these
identifiers and counts before displaying the current map's record on its result
screen. Retries never increase the totals twice. Startup failures, interrupted
sessions and empty restarts are excluded from win/failure totals. If a summary
cannot be delivered before the next mission starts, the screen reports it as
unavailable and keeps local statistics visible.
Once a confirmed record is displayed, failures uploading older queued events
cannot replace it with the unavailable message.
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.
The Worker validates schema and limits and binds each server to its private
credential after the first accepted event. Automatic enrollment proves continuity
of a claimed installation, not its operator's identity or gameplay honesty.
Operator-created server records are labelled separately. These are reports from
community-hosted missions, not independent anti-cheat attestations.
Published clients through 0.0.129 can send fractional `elapsedSeconds`. The
receiver accepts finite, nonnegative durations within the integer range and
rounds down before hashing and storage. This also lets existing saved queues
retry without edits; retries still use the original event IDs and count once.
## 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).
### Round combat counters
Player records additionally include `kills`, `friendlyKills`, `deaths`, `downed`
and `recoveries`, using the same server-owned counters as the after-mission
screen. These fields are additive to schema version 1. Older queued records may
not contain them; treat absent values as unavailable rather than inventing a
historical count. Kills exclude friendly kills, suicides, vehicles and editor
kills. Downed means an unconscious episode, while a death is a fatal loss.
Recoveries include self-recovery and are not healer-attributed revives.
Local result tracking continues when telemetry is disabled; sending still follows
the existing telemetry setting and endpoint configuration.
## Browser dashboard
The receiver serves a private statistics dashboard at
`https://co10escape-telemetry.zacekjakub.workers.dev/dashboard`. Use the receiver’s
read-only token or admin token to view server records and last receipt times,
mission outcomes, player counters, mods, settings and raw event history. Tokens
are held only in browser-tab memory. See the [receiver guide](../services/telemetry-worker/README.md#statistics-dashboard)
for deployment, pagination and freshness semantics. This requires only the Worker
update and receipt index migration; it does not require a new mission release.
### Automatic server names
New events first read `game.name` from `BackendApi.GetRunningDSConfig()`.
If unavailable or blank, they try `ServerLobbyApi.GetServerConfig().GetName()`
and then `GetGame().GetServerInfo().GetName()`. The optional profile `serverName`
remains an explicit override. Names are trimmed and bounded to 120 characters.
Resolution repeats for each new event so startup metadata delays do not permanently
leave a server unnamed. Neither scenario titles nor account identities are used.
The installation UUID and queued historical events remain unchanged.
The receiver updates automatic server labels from nonblank accepted reports,
using server-reported event time to prevent older queued names from replacing a
newer one. As with all telemetry timestamps, bad server clocks can affect that
ordering. Managed labels remain operator-owned. Mission rows use the historical
reported name where present and the current installation label otherwise.
Automatic discovery needs a new Workshop release; existing blank records only
acquire names after an updated server reports.
Validation: [2026-11-01 evidence](evidence/telemetry-server-name-2026-11-01/verification.json)
records 21 receiver/client tests plus two bundled-client checks. Native compilation,
explicit override and absent-metadata fallback pass without script failures; the
13 inherited resource/world errors remain. The local Workbench fixture has no
configured server name, so the real dedicated-server name must still be confirmed
from the first report after publishing.
Dedicated-name follow-up: [validation evidence](evidence/telemetry-dedicated-name-2026-11-01/verification.json)
records the added `GetRunningDSConfig().game.name` priority path. Production-method
tests cover dedicated names and fallback conditions; native Workbench compilation
and available-metadata checks pass. Workbench still has no running dedicated
configuration, so this does not prove recovery on a live dedicated server.