# Telemetry, Build & Data

Five gameplay cells, one shared roster, one online backbone, ten platforms — and
all of it eventually has to answer two unglamorous questions: _what is the live
game telling us, and how does this tree become a signed package inside a console
frame budget?_ This page is V5's production substrate. It covers the **telemetry
spine** (the in-engine `V5Telemetry` module plus the `telemetry` online service
that lands events in ClickHouse), the **build/cook/patch** story (the real
UnrealBuildTool targets and module graph, versus the cook-farm prose the
architecture aspires to), and the **data architecture** (typed event schemas,
JSON manifests, and the honest gap where cooked binaries and a balance ledger
would sit). It is the layer where a privacy bug ships to every player at once
and a missing strip-profile blows a memory budget on Switch 2, so it is
documented the way it actually exists in the repo — real code cited as real,
intent labelled as intent. The orientation hub is
[../V5_ARCHITECTURE.md](../V5_ARCHITECTURE.md).

## What ships, honestly

Three layers, stated plainly.

- **Telemetry is real, and it is the strongest part of this surface.**
  `V5/ue/Source/V5Telemetry` is a genuine UE5 C++ module: three enums, eight
  `USTRUCT`s, and **five** `UBlueprintFunctionLibrary` classes implementing a
  real event batcher, a TypeBox-shaped edge validator, a 32-entry critical-event
  catalog, an opt-out gate, and a DSAR exporter — backed by **three** automation
  specs that assert concrete values. The matching `apps/v5/telemetry` NestJS
  service does **real per-cell ClickHouse routing** and reports **fail-loud
  health** (it serves a 503 outage contract when its backing store is
  unconfigured rather than faking `ok`).
- **The build _targets_ are real; the build _farm_ is not in this tree.** There
  are five genuine `*.Target.cs` rules and **74** `*.Build.cs` `ModuleRules`
  across the source tree, so the module graph and its dependency edges are real
  and compilable. But unlike V4, there is **no BuildGraph script, no
  FASTBuild/SN-DBS config, no Jenkinsfile, and no `build-cook-patch.json`** in
  V5 — the "Build, Cook, Patch" architecture section is specified _intent_
  (UnrealBuildTool + Horde, per-platform cook, UnrealPak iterative patching),
  not an enforced pipeline. It is labelled that way throughout.
- **The data is text-only, and the balance ledger is scaffolding.** Measured
  live in this tree: **0 `.uasset` binaries, exactly 1 `.umap`, 0 `DT_*`
  DataTables**, against **393 `.json`** files under `V5/ue/Content` (manifest
  stand-ins and the performance catalog). The `V5/balance/` directory is **12
  Markdown files** — READMEs and period-authenticity research — with **no
  balance data, no ledger, no CSV**. Any claim that needs cooked content or a
  telemetry-evidenced balance ledger stays open; the typed _event_ schema is the
  one piece of data that is fully concrete, because it is generated in C++.

The honest summary: the **telemetry pipeline is implemented and tested**, the
**build targets are real but the cook farm is not attached**, and the **content
and balance data remain authored-intent**, exactly as with the rest of V5.

## Telemetry: the `V5Telemetry` module

### Event taxonomy and privacy classes

The vocabulary is defined in
`V5/ue/Source/V5Telemetry/Public/V5TelemetryTypes.h`. Every event is an
`FV5TelemetryEvent` carrying a `Sequence`, an `EventName`/`SchemaName` pair, an
`AccountOpaqueId`, a `Region` (defaulting to `"iad"`), the originating
`EV5Cell`, a `UnixSeconds` stamp, a `PayloadBytes` count, and an
`EV5TelemetryPrivacyClass`. That last field is the spine of the privacy model —
three classes, `Anonymous`, `AccountOpaque`, and `ConsentRequired` — and it is
assigned per event type, not per call site, so a designer cannot accidentally
promote an anonymous event to PII.

The critical-event catalog lives in
`UV5_Telemetry_CriticalEvents::BuildCriticalEventHandlers`
(`V5TelemetrySystems.cpp`) and ships **32 handlers**, broader than the 24 the
architecture doc enumerates. Beyond the session/mission/combat/narrative events
it adds the online and operations surface the
[persistence & online page](./persistence-online-and-crossplay.md) depends on:
`matchmaking_queue_join`/`leave`, `party_ready_check`,
`leaderboard_score_submit`, `replay_upload`, `crash_report_submitted`,
`anti_cheat_signal`, and `currency_balance_change`. Each handler is stamped with
a `Category` (`session`, `combat`, `mindpalace`, `hunter`, `ship`, `online`,
`ops`, `privacy`, …) and its privacy class — notably `privacy_consent_change` is
`Anonymous`, which is what lets it survive opt-out (below). The automation spec
`FV5TelemetrySchemaAndHandlersTest` asserts `Handlers.Num() >= 30` and checks
each of the 24 documented events resolves to a handler, so the catalog cannot
silently shrink below the documented contract.

### Edge validation, TypeBox-shaped

Events are validated _at the client edge_ before they are ever batched.
`UV5_Telemetry_Schema_TypeBox::BuildSchemaCatalog` derives one
`FV5TelemetrySchemaDefinition` per handler, naming each schema
`typebox.v5.telemetry.<event>.v1` and synthesising a TypeBox-shaped JSON blob
via `MakeTypeBoxSchema` (`{"kind":"Type.Object","event":"…","required":[…]}`).
`ValidateEvent` then runs a real four-stage check and returns one of five
`EV5TelemetryValidationStatus` values:

1. **`MissingSchema`** — no schema for the event name, _or_ the event's
   `SchemaName` disagrees with the catalog's.
2. **`PayloadTooLarge`** — `PayloadBytes > MaxPayloadBytes` (2048).
3. **`MissingField`** — any of the required fields (`event`, `account`,
   `region`, `sequence`) is absent from `PayloadJson`.
4. **`Valid`** — all checks pass, `bValid = true`.

`FV5TelemetrySchemaAndHandlersTest` exercises both branches: a well-formed
`session_start` event passes, and an event whose payload is
`{"event": "session_start"}` fails with exactly `MissingField`. This is a domain
assertion that would break if the validator started rubber-stamping — not a
shape-only smoke test.

### Batching: greedy, ordered, three flush reasons

`UV5_Telemetry_EventBatcher::BuildBatches` is the real send-side accumulator.
Its default `FV5TelemetryBatcherConfig` is the published budget — **2048 bytes /
5 seconds / 500 events** — and the batcher greedily packs events into an
`FV5TelemetryBatch`, cutting a new batch whenever the next event _would_ exceed
the byte cap, the count cap, or the age window. Each finalised batch records a
`FlushReason` (`max_bytes`, `max_events`, `max_age`, or `drain` for the tail), a
deterministic `BatchId` of the form `telemetry.batch.<first>.<last>`, and a
`bOrderingPreserved` flag computed by walking the sequence numbers.
`FV5TelemetryBatchOrderingTest` feeds 10 padded events through the batcher,
asserts the stream splits into more than one batch, asserts every batch stays
`<= 2048` bytes and preserves local ordering, and checks that flattening all
batches reproduces the source sequence `0..9` exactly — i.e. no event is dropped
or reordered across the split.

### Opt-out and DSAR: privacy as code

Two libraries make the privacy story executable rather than aspirational.
`UV5_Telemetry_OptOut::ShouldCollectEvent` implements the in-game opt-out: if
the account has not opted out, everything collects; if it has, the function
returns `false` for all events _except_ `privacy_consent_change`, which remains
essential. `UV5_Telemetry_DSAR_Export::BuildExport` builds the data-subject
export — it filters a stream to the requesting `AccountOpaqueId`, emits one
escaped JSON line per matching event, stamps a **30-day SLA**, marks the export
`bRedacted` and `bCompanionAppVerified`. `FV5TelemetryOptOutAndDSARTest`
confirms opt-out suppresses a `mission_complete` but lets
`privacy_consent_change` through, and that a three-event mixed-account stream
exports exactly the **two** events belonging to the requester. The compliance
laws behind this (GDPR/CCPA, COPPA age-gating, EU data residency) are enforced
in the `compliance-dsar` service detailed on the
[persistence & online page](./persistence-online-and-crossplay.md).

### The telemetry service: ClickHouse routing and fail-loud health

The in-engine module produces validated batches; `apps/v5/telemetry` ingests
them. The contract (`contract.json`) declares a single endpoint —
`POST /v5/telemetry/batch`, JWT-required, offline-queue-capable, regional — with
storage `{ primary: ClickHouse, cache: Kafka, analytics: Grafana }`,
capabilities `batch-limits`, `schema-validation`, `privacy-opt-out`,
`clickhouse-export`, and a `regional-active-active` failover plan across
`iad/fra/sin` with `pdx/dublin/syd` fallbacks. The service is one of **16** in
the `@v5/service-*` catalog and runs on port **4216** (asserted by
`contract.test.ts`).

The routing is real, not decorative. `selectClickHouseTarget(cell, region)` in
`apps/v5/service-shared/src/runtime.ts` returns a target with database
`v5_telemetry`, table `events_<cell>`, and the request region — so each cell's
events land in their own ClickHouse table, which is what makes the per-cell
Grafana dashboards possible. Health is **fail-loud**: `resolveServiceHealthy`
walks every backing dependency the catalog declares (`V5_POSTGRES_URL`,
`V5_REDIS_URL`, and for telemetry specifically `V5_CLICKHOUSE_URL`) and reports
`status: 'ok'` only when all are configured; otherwise it returns
`not_configured` and the service serves its 503 outage contract. The source
comment records that this _replaced_ a hardcoded `serviceHealthy: true` that had
made the outage branch dead code — a deliberate de-fabrication, and the correct
honest-seam pattern for an integration with no live backing store attached.

### Where `@oshun/metrics` is — and isn't

One honest divergence from the V2 bar is worth stating directly. V2's telemetry
page can claim every service composes the shared
`@oshun/metrics`/`@oshun/tracing` observability libraries. **V5 does not.** A
repo-wide search for `@oshun/metrics` or `@oshun/tracing` under `V5/` returns
nothing; the V5 services instead build on their own `@v5/service-shared` runtime
(`buildHealth`, `handleRealRequest`, `resolveBackingDependencies`). The shared
library is real and capable — it lives at `libs/shared/metrics` and exports
Prometheus-compatible `Counter`, `Gauge`, `Histogram`, and `Summary` types with
`HISTOGRAM_BUCKETS`/`SUMMARY_PERCENTILES` presets — but it is **not currently
wired into V5**, and the architecture's Grafana/ClickHouse observability story
is carried by the service contracts and the (intent-level) dashboards, not by a
`@oshun/metrics` binding. Treat any Prometheus-scrape claim for V5 as
not-yet-implemented.

## Build, cook & patch

### Targets and the module graph

V5 is a real UnrealBuildTool project. There are **five** target rules under
`V5/ue/Source`: `V5.Target.cs` (the game client), `V5Editor.Target.cs`,
`V5DedicatedServer.Target.cs`, `V5ListenServer.Target.cs`, and
`V5ArcadeCabinet.Target.cs`. `V5.Target.cs` is a genuine `TargetRules` subclass
— `Type = TargetType.Game`, `DefaultBuildSettings = BuildSettingsVersion.V5`,
`IncludeOrderVersion = EngineIncludeOrderVersion.Latest`, the UE5.5-era
defaults, matching the same posture V4's targets take. Below the targets, the
tree splits into the engine-module set the
[topology page](./topology-layout-module-split.md) enumerates, each with its own
`*.Build.cs` `ModuleRules` — **74** of them in total. The dependency edges are
real and meaningful: `V5Telemetry.Build.cs` declares
`PublicDependencyModuleNames` of `Core`, `CoreUObject`, `Engine`, and `V5Core`,
pulling in the shared core at the module boundary rather than ad hoc, and the
cross-cell-shared modules forbid per-cell dependencies one-way.

### The Arcade Cabinet target

`V5ArcadeCabinet.Target.cs` is the most distinctive build artifact — a
location-based four-player cabinet SKU. It sets the project-scope compile
definitions `V5_ARCADE_CABINET`, `V5_LOCAL_COOP_CABINET`,
`V5_CABINET_ONLINE_SERVICES=0`, and `V5_CABINET_COIN_OP_ECONOMY=0`, packages
through `Platforms/ArcadeCabinet/Config/ArcadeCabinetEngine.ini`, and cooks only
a curated local-co-op roster (Spec Ops Co-op, Horde Defense, Fleet Co-op, Hunter
Card Game, Training Range). Its profile manifest at
`V5/ue/Content/V5Input/Data/arcade_cabinet_build_manifest.json` binds
four-player `ArcadePanel` input contexts, 4K60 output, offline boot, attract
mode, and a no-coin-op-economy gate — runtime mode gating maps the target to
`EV5ModeBuildTarget::ArcadeCabinet`. It is a real example of the
GameFeature-plugin breadth meeting a build target, and is covered further on the
[modes, streaming & procgen page](./modes-streaming-procgen.md).

### Cook & patch: specified, not yet a farm

Here the honest line matters. The architecture's "Build, Cook, Patch" section
describes UnrealBuildTool + **Horde** for distributed compile, per-platform
parallel cook with iterative dev cook, **UnrealPak Iterative** chunked patching
with per-cell chunk sizes, a single-binary day-one patch policy, and
per-platform signed packages. **None of those pipeline artifacts exist as files
in this tree.** Unlike V4 — which ships a `V4BuildCookPatch.xml` BuildGraph
script, `FASTBuild.bff`/`SNDBS.ps5.xml` distributed-compile configs, a
`Jenkinsfile.pc`, and a `build-cook-patch.json` manifest all enforced by a check
script — V5 has only the target/module rules and a launch-readiness validator.
The cook/patch design is therefore **specified intent**, and the per-platform
strip profiles, chunk sizing, and signed-package flow described in the hub doc
are not yet machine-enforced. Stated plainly so nobody mistakes the prose for a
green build farm.

### The launch-readiness gate

What _is_ enforced as code is the release gate, via
`V5/tools/release/validate-launch-readiness.py`. Its docstring is itself a
de-fabrication record: a prior revision hardcoded "17/17 gates passed,
releaseBlocked false, 99.72% crash-free" and was rewritten on 2026-06-12 to
**validate honest state and fail closed** — every one of the 17 gates may be
`passed`, `pending`, or `failed`; the summary counters must equal what the
per-section data actually says; and a gate may claim `passed` (and the manifest
may claim `releaseBlocked: false`) only when the backing observations actually
exist. The validator cross-checks real artifacts: the
`performance_platform_profiles.json` catalog, the season-1 live-service
manifest, `legal/platform-cert-bans.json`, the
matchmaking/balance-ledger/compliance service contracts, and
`V5Tests/Private/Tests/V5TestsAutomation.cpp`. Its constants encode the launch
contract — 5 cells, 16 languages (12 voiced + 4 text-only), 9 platforms,
packet-loss tolerance at {5, 20}%, validated ping ≤ 80 ms. The broader testing
tiers, feel-test suites, and crash-rate gates this composes belong to the
[observability, performance-testing & content-pipeline page](./observability-performance-testing-content-pipeline.md).

```mermaid
flowchart TD
  subgraph Client [In-engine · V5Telemetry C++]
    EV[FV5TelemetryEvent<br/>privacy class · cell · region]
    VAL[Schema_TypeBox.ValidateEvent<br/>schema · size · required fields]
    OPT[OptOut.ShouldCollectEvent<br/>essential-only on opt-out]
    BATCH[EventBatcher.BuildBatches<br/>2048 B · 5 s · 500 ev]
    EV --> OPT --> VAL --> BATCH
  end
  subgraph Service [services/telemetry · NestJS]
    ING[POST /v5/telemetry/batch<br/>JWT · offline queue]
    ROUTE[selectClickHouseTarget<br/>db v5_telemetry · events_cell]
    HEALTH{resolveServiceHealthy<br/>PG · Redis · ClickHouse?}
    ING --> ROUTE
    ING --> HEALTH
  end
  subgraph Sinks [Storage]
    CH[(ClickHouse<br/>per-cell tables)]
    GRAF[Grafana dashboards]
    DSAR[DSAR_Export · 30-day SLA]
  end
  BATCH --> ING
  ROUTE --> CH --> GRAF
  EV -. account-filtered .-> DSAR
  HEALTH -->|unconfigured| OUT[503 outage contract]
```

## Data architecture

### The text-only reality

The architecture describes V5 data as `UDataTable`s (characters, weapons,
vehicles, NPCs, ingredients, bestiary, factions, missions), `UCurveTable`s,
asset registries, the Localization Dashboard, and a telemetry schema. Measured
in the tree, the reality is the same text-only posture V4 documents: **0
`.uasset`**, **exactly 1 `.umap`**, **0 `DT_*` DataTables**, and **393 `.json`**
files under `V5/ue/Content`. Those JSON files are the manifest stand-ins
referenced throughout the hub doc — the `year1_*` and `fullvision_*`
live-service manifests, the NG+/save-archaeology manifests, the arcade-cabinet
profile, and the performance catalog. They carry authored intent (typed fields,
references to C++ symbols and other manifests) and are what the
`V5/tools/test_validate_*.py` suite and `tools/validate-v5-docs.py`
referential-integrity checker attest to, but they **cannot be cooked, loaded,
profiled, or played**. The single concrete exception to the "no typed data" rule
is the **telemetry event schema**, which is not a DataTable at all — it is
generated in C++ by `MakeTypeBoxSchema` and declared in the `@v5/service-shared`
service catalog, which is precisely why the event contract is the most testable
data in the project.

### Balance: scaffolding, honestly

V4's data page can point to a publicly published, telemetry-evidenced **balance
ledger** (`public-balance-ledger.json`, a `gadget-balance-ledger.csv`, matchup
matrices with validated win-rate algebra) gated by a real check script. **V5 has
no such ledger.** The `V5/balance/` directory is **12 Markdown files** — a
per-cell `README.md` for urban/period/frontier/hunter/scifi/shared, the
period-authenticity research notes (1930s/1947/1968), and
`hunter/cardgame-rules.md` — with **no data files of any kind**. The
`balance/schema/README.md` states the directory "owns JSON Schema and
spreadsheet contract definitions used by V5 balance linters and cook-time data
validation," but **those schema and ledger files do not yet exist**; the
directory is documentation-of-intent and authoring scaffolding. There _is_ a
`balance-ledger` online service in the catalog (one of the 16, cross-checked by
the launch validator), but the published numbers, telemetry evidence, and tier
lists that V4's ledger enforces are not present in V5 as data. Any
balance-dashboard claim is therefore intent, fed by the telemetry events above
once they run against a live ClickHouse.

### The performance profile catalog

The one substantial typed dataset on this surface is
`V5/ue/Content/V5Performance/Data/performance_platform_profiles.json` — a real
37 KB catalog with `schemaVersion`, `catalogId`, a `contentLock`, a `summary`,
**11 platform `profiles`**, and `regressionSuites`. It binds the per-platform
frame-time budgets from the hub doc (PS5 33.3/16.6 ms, Switch 2 8 GB, Windows
ultra 8.3 ms, …) to engine config sections and to the release-blocking
performance-regression suite, and it is one of the artifacts
`validate-launch-readiness.py` reads to decide whether the performance gate may
claim `passed`. It is the closest thing V5 has to a real, machine-checked data
table, and it is the bridge from this page to the performance budgets enforced
on the
[observability & performance-testing page](./observability-performance-testing-content-pipeline.md).

## Where this connects

- **What the events instrument:** the
  [modes, streaming & procgen page](./modes-streaming-procgen.md) — the
  GameFeature plugins and build targets (Arcade Cabinet included) whose per-mode
  sessions emit the mission/combat/online events the batcher carries.
- **What gates the build:** the
  [observability, performance-testing & content-pipeline page](./observability-performance-testing-content-pipeline.md)
  — the testing tiers, the `performance_platform_profiles.json` regression
  suite, and the crash-rate/feel-test gates the launch-readiness validator
  composes.
- **What the dashboards visualise:** the
  [audio, VFX, cinematics & UI page](./audio-vfx-cinematics-ui.md) — the
  presentation systems (and the replay/photo-mode surfaces) whose usage and
  export events feed the same per-cell ClickHouse tables.
- **The orientation hub:** [../V5_ARCHITECTURE.md](../V5_ARCHITECTURE.md).
