287 lines
16 KiB
Markdown
287 lines
16 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. 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.
|