Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/internal/telemetry/destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
)

// DefaultEndpoint is the project default telemetry destination, ruled in
// specs/done_022-default-telemetry-dashboard/OPEN-DECISIONS.md OD-1. It must stay
// specs/022-default-telemetry-dashboard/OPEN-DECISIONS.md OD-1. It must stay
// an https URL (hack/check-telemetry-default.sh enforces this in CI).
const DefaultEndpoint = "https://telemetry.gameplane.net/ingest"

Expand Down
2 changes: 1 addition & 1 deletion api/internal/telemetry/telemetry.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Package telemetry sends the install's usage report to its resolved
// destination (specs/done_022-default-telemetry-dashboard).
// destination (specs/022-default-telemetry-dashboard).
//
// Privacy: the basic report carries only the control-plane version and
// total counts. The extended report adds an install ID and environment
Expand Down
2 changes: 1 addition & 1 deletion assets/design-export/MANIFEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -2502,7 +2502,7 @@ Server Settings gets a "Game configuration" section (edit `spec.config` from the

## Spec 022 telemetry notice and settings (2026-10-07)

T041 and T052 of `specs/done_022-default-telemetry-dashboard/`. New frames sit in an empty region below the Admin Settings row (x 11480–21120, y 25404–28584); no existing frame changed. Destination hosts are samples while OD-1 is unruled: their text nodes are named `host (SAMPLE, OD-1 unruled)` (default) and `host (SAMPLE)` (bundled).
T041 and T052 of `specs/022-default-telemetry-dashboard/`. New frames sit in an empty region below the Admin Settings row (x 11480–21120, y 25404–28584); no existing frame changed. Destination hosts are samples while OD-1 is unruled: their text nodes are named `host (SAMPLE, OD-1 unruled)` (default) and `host (SAMPLE)` (bundled).

| Object ID | Name | Change |
| --- | --- | --- |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Open Decisions

**Status**: 0 open; 6 ruled (OD-2, OD-3 and OD-4 on 2026-10-06, OD-1 and OD-5 on 2026-10-07, OD-6 on 2026-10-08).
**Status**: 0 open; 10 ruled (OD-2, OD-3 and OD-4 on 2026-10-06, OD-1 and OD-5 on 2026-10-07, OD-6 on 2026-10-08, OD-7 to OD-10 on 2026-10-14).

Per CLAUDE.md rule 10, an open value MUST NOT be committed as a settled contract in code, chart defaults or docs until it is ruled here.

Expand Down Expand Up @@ -110,3 +110,59 @@ Per CLAUDE.md rule 10, an open value MUST NOT be committed as a settled contract
**Question**: The receiver dashboard's ranges ended on the latest complete UTC day (yesterday), so a provider saw "No reports yet" for up to a day after its first accepted report, and today's reports never showed until the day closed.

**Ruling**: the dashboard shows all data. Every range (7, 30, 90 or 365 days) ends on today, the current UTC day, which is still in progress and labelled so. The empty state appears only when the range holds no report at all. Today's lapsed-installs value stays `0` until the lifecycle job finalises the day. The public `GET /v1/summary` is unchanged and keeps the latest complete day.

---

## Amendment 2026-10-14: version trend (FR-042)

### OD-7: What "stale" means

**Status**: RULED (2026-10-14, user)

**Question**: The version trend counts installs that upgraded, downgraded or went stale. Upgrades and downgrades are detected when an install reports a different version than last time. What makes an install stale? The receiver doesn't know which release is newest today, and installs that stop reporting are already counted as lapsed.

**Options**:
1. Behind the newest seen: on a day, an install's version is lower than the newest release any install reported in the range.
2. Unchanged for N days.
3. Both: behind the newest seen release for at least N days.

**Ruling**: (1) behind the newest seen. No threshold to tune. Whether a pre-release can be the newest release is OD-10.

### OD-8: Ordering versions that aren't releases

**Status**: RULED (2026-10-14, user)

**Question**: Hand builds (`pr617`), PR images (`dev`) and edge images (`edge-<sha>`) have no order. How does the trend treat them?

**Options**:
1. Semver only: only semver versions are ordered; a change to or from anything else is "changed (unordered)".
2. Semver, with everything else sorting below every release.
3. Leave installs on non-release builds out of the trend.

**Ruling**: (1) semver only. The trend never guesses a direction, and an install on a non-semver build is never stale.

### OD-9: Where the views go

**Status**: RULED (2026-10-14, user)

**Question**: The basic "Versions" panel counts reports by version (FR-024), and "Versions by install" in the extended section already counts each install once on its latest version (FR-025). Where do these and the new trend go?

**Options**:
1. Replace the top panel with the per-install view and the trend.
2. Keep both, rename the reports panel "Reports by version", and put the trend under "Versions by install".
3. Move "Versions by install" and the trend to the top, and the reports panel down.

**Ruling**: (2) keep both and add the trend. The layout is designed first in `telemetry-receiver/telemetry-dashboard.pen`.

### OD-10: Pre-releases and the newest-release bar

**Status**: RULED (2026-10-14, user)

**Question**: Can a pre-release such as `v0.4.0-rc.1` be the "newest release" that makes other installs stale (OD-7)? Release images report their git tag (`v0.3.0`), and the repo has pre-release tags (`v0.3.0-rc.1`). Semver orders `v0.4.0-rc.1` above `v0.3.0`, so one rc tester would make every install on the newest final release stale. Moving to or from a pre-release still counts as an upgrade or downgrade either way.

**Options**:
1. Finals only: only versions without a pre-release suffix set the bar.
2. Any semver: the highest semver seen sets the bar, pre-releases included.

**Ruling**: (2) any semver. The highest semver version seen in the range sets the bar, pre-releases included. One install on `v0.4.0-rc.1` makes every install on `v0.3.0` stale for the days in the range.

Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ An E2E check (R17) compares the unauthenticated response against the login page
"newPerDay": [{"day": "2026-09-06", "count": 12}],
"lapsedPerDay": [{"day": "2026-09-06", "count": 4}],
"versionsByInstall": {"windowDays": 30, "items": [{"label": "0.3.0", "installs": 1011}]},
"versionTrend": {"newestRelease": "v0.3.0", "days": [{"day": "2026-09-06", "upgraded": 14, "downgraded": 1, "unordered": 0, "stale": 212}]},
"env": {"k8s": [{"label": "1.31", "share": 0.42}], "distro": [], "arch": [], "nodes": []},
"games": [{"module": "minecraft-java", "installsShare": 0.63, "serversPerDay": 2140.5}, {"module": "custom", "installsShare": 0.08, "serversPerDay": 77.1}],
"features": [{"feature": "wakeOnConnect", "share": 0.18}],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ Responses inside one attempt:
| `daily_ext` | `day` | `ext_reports`, `active_installs`, `new_installs`, `lapsed_installs` | Ingest for the first three. Rollover for `lapsed_installs`. |
| `daily_dim` | `(day, dim, value)` | `installs` | Ingest of a non-duplicate extended report. See the dimension list below. |
| `daily_game` | `(day, module)` | `installs`, `servers` | Ingest. `module` is a catalog name or `custom`. |
| `daily_install_version` | `(day, version)` | `installs` | Ingest of a non-duplicate extended report (FR-042). `version` is the same bounded label as `daily_version`. |
| `daily_version_change` | `(day, kind)` | `installs` | Ingest of a non-duplicate extended report whose version differs from `activity.last_version` (FR-042). `kind` is `upgraded`, `downgraded` or `unordered` (R22). |
| `activity` | `id_hmac` | `key_fp` (SHA-256 of the claiming public key), `first_seen`, `last_seen`, `last_sent_at`, `last_version` | Ingest: inserting a row *is* the claim (R20). Deleted by expiry, and the claim expires with it (FR-038). |

`daily_dim` dimensions:
Expand All @@ -213,6 +215,7 @@ Every `value` is a `telemetryschema` enumeration member or `other`, so the row c
- **Extended attributes are never keyed by ID.** `activity` has no column for any of them (FR-014).
- **Refused reports leave no trace.** A report refused by the proof-of-work, signature, window, claim or replay checks changes no table (FR-036, FR-039). Only the in-memory limiter and the `rate_limited` and `refused` metrics see it.
- **No full public keys are stored.** `key_fp` is enough to check the claim, because the key arrives with every report.
- **Version changes are counts only.** `daily_version_change` and `daily_install_version` hold no ID, so no version history can be traced back to an install (FR-014, FR-042).
- **One extended contribution per install per day.** At most one extended report per `id_hmac` per day changes any aggregate (R5).
- **The running total never decreases.** `reports_total` is only ever incremented, so retention sweeps don't change the public "total since collection began".

Expand All @@ -235,6 +238,7 @@ The range R is 7, 30, 90 or 365 days and ends on today, the current UTC day, whi
| Unique active installs | `count(activity where last_seen ≥ asOf − {0, 6, 29})` |
| New and lapsed installs | `daily_ext.new_installs` and `lapsed_installs` series over R |
| Version adoption (installs) | `activity.last_version` for records with `last_seen` inside `min(R, ACTIVITY_EXPIRY_DAYS)`. The window used is shown. |
| Version trend (FR-042) | Per day D in R: `upgraded`, `downgraded` and `unordered` from `daily_version_change[D]`; `stale[D]` = Σ `daily_install_version[D].installs` over semver versions lower than N, where N is the newest release in `daily_install_version` over R (OD-7): the highest semver version, pre-releases included (OD-10). Versions that aren't semver are never stale (OD-8). |
| Environment, games, feature shares | Σ `installs` ÷ Σ `daily_ext.ext_reports` over R (install-day weighted, research R4) |
| Extended coverage (FR-026) | Σ `ext_reports` ÷ Σ `reports` over R |
| Public summary (FR-029) | `reports` for asOf; Σ `reports` over the 30 days ending asOf; `meta.reports_total`; `count(activity where last_seen ≥ asOf − 29)`; `asOf` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Branch**: `feat/default-telemetry-dashboard` (not yet created; the spec-kit script derived `022-default-telemetry-dashboard`) | **Date**: 2026-10-06 | **Spec**: [spec.md](spec.md)

**Input**: Feature specification from `specs/done_022-default-telemetry-dashboard/spec.md`
**Input**: Feature specification from `specs/022-default-telemetry-dashboard/spec.md`

## Summary

Expand Down Expand Up @@ -109,7 +109,7 @@ No NEEDS CLARIFICATION remains. Every technical unknown is resolved in [research
### Documentation (this feature)

```text
specs/done_022-default-telemetry-dashboard/
specs/022-default-telemetry-dashboard/
├── spec.md
├── OPEN-DECISIONS.md # OD-1..OD-5 ruled
├── plan.md # this file
Expand Down Expand Up @@ -218,6 +218,16 @@ The slices are implementation waves on **one feature branch and one PR**, in thi

Slices 2 and 3 can run in parallel once slice 1 is committed. Slice 4 needs slice 3's routes. Slice 5 needs everything before it.

### Amendment slice (2026-10-14): version trend (FR-042)

Spec 022 is reopened for one more slice on a new branch and PR; the shipped feature is unchanged until it lands.

6. **Version trend** (US5 scenarios 7–9, FR-042, OD-7–OD-10, research R22):
- receiver storage: `daily_install_version` and `daily_version_change` (created with `CREATE TABLE IF NOT EXISTS`, so existing providers gain them on restart; no schema-version bump), plus their retention sweep
- version ordering through `golang.org/x/mod/semver`, a new direct dependency of `telemetry-receiver`
- classification in the extended ingest transaction, and the `extended.versionTrend` view block
- the design in `telemetry-receiver/telemetry-dashboard.pen` first (rename "Versions" to "Reports by version", add the trend chart under "Versions by install"), done on the local machine where Pencil runs, then the templates

## Cross-spec dependencies

- **019 localization-language-packs** (Draft) owns adding languages to the `telemetryschema` `language` enumeration and wiring the configured default. Until then `language` is always `en` (R19).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,18 @@ No new RBAC is needed; every source is already readable (see the baseline facts)
- *A memory-hard function such as Argon2.* It resists GPUs better, but `telemetryschema` must stay stdlib-only, and a Raspberry Pi install would pay far more.
- *Proof-of-work on the dashboard login.* It needs JavaScript, which the dashboard's CSP forbids (R7), and the 32-character credential (R8) already makes guessing infeasible.
- *A fixed difficulty.* Either every install pays all the time, or the difficulty is too low to matter under attack.

## R22. Version trend: ordering and storage (FR-042, OD-7–OD-10)

**Decision**:
- **Ordering** uses `golang.org/x/mod/semver`, already used in the workspace (`operator/internal/controller/semver.go`, `operator/internal/oci`) at v0.41.0. A label without a leading `v` is compared as `v` + label, so `0.3.0` and `v0.3.0` order alike. A label `semver.IsValid` rejects (`pr617`, `dev`, `edge-<sha>`, `other`, `invalid`) is unordered (OD-8).
- **Classification at ingest**, on a non-duplicate extended report for an existing activity record whose `last_version` differs from the new label: both semver and new > old is `upgraded`, new < old is `downgraded`, otherwise `unordered`. Labels that differ but compare equal under semver (`0.3.0` and `v0.3.0`) are not a change. The write shares the transaction that updates the activity record, so a refused or duplicate report changes nothing.
- **Stale at read time** from `daily_install_version`, so the "newest release in the range" (OD-7) follows the selected range instead of being frozen at ingest time.

**Rationale**: Daily counts keep FR-014 (no attribute linked to an ID) while still giving a per-day trend. Storing installs per version per day costs at most the bounded label set (`maxVersionLabels` = 128, plus `other` and `invalid`) per day.

**Alternatives considered**:
- A per-install version history table: rejected, it would link versions to an ID over time (FR-014).
- Stale at ingest against the newest version seen so far: rejected, the result would depend on arrival order and ignore the selected range.
- A hand-written semver parser in `telemetryschema`: rejected, the `x/mod` package is the reference implementation and is already vetted in this workspace.

Loading
Loading