Expand description
Aggregate usage telemetry for a Cratefield venture (issue #413): the collector, the closed payload grammar and the consent model, in one crate. Clients report counted events; every field is validated against a fixed vocabulary — an unknown field or an undeclared value is a rejection, never a truncation or a coercion — and the counts accumulate into buckets in the venture’s own database, under the same retention and erasure rules as the rest of its tables.
§Composition
use cratefield_module_telemetry::Telemetry;
let module = Telemetry::new()
.events(["run", "report", "upgrade"]) // closed event vocabulary
.modules(["telemetry", "waitlist"]) // closed composed-module vocabulary
.retention_days(180)
.max_events_per_batch(64)
.opt_out_command("fz telemetry off")
.status_command("fz telemetry status");Both vocabularies default to empty, which fails closed: a venture that forgets to declare them runs a collector that rejects every batch naming an event or a module, not one that counts anything it is sent.
§Consent
The consent model lives in consent: a pure decision over the
environment (the local opt-out, DO_NOT_TRACK, CI), a first-run
notice generated from the payload schema so it cannot drift, a status
rendering of the exact bytes that would be sent, and an install id the
caller mints from random bytes and rotates every consent::ROTATION_DAYS
days. The crate itself never generates randomness and never opens a
connection, which is what keeps it wasm-safe and dependency-free.
See docs/TELEMETRY.md for the published client contract this crate
implements.
§cratefield-module-telemetry
Aggregate usage telemetry for a Cratefield venture: clients report
batches of counted events to POST /v1/telemetry/events, every field
is validated against a closed grammar — an unknown field or an undeclared
value is a rejection, never a truncation — and the counts accumulate into
buckets in the venture’s own database. No third-party analytics service,
no vendor SDK, and no field anywhere a sentence could fit.
use cratefield_module_telemetry::Telemetry;
let module = Telemetry::new()
.events(["run", "report", "upgrade"])
.modules(["telemetry", "waitlist"])
.retention_days(180)
.max_events_per_batch(64)
.opt_out_command("fz telemetry off")
.status_command("fz telemetry status");Both vocabularies default to empty, which fails closed: a venture that forgets to declare them runs a collector that rejects every batch naming an event or a module, not one that counts anything it is sent.
Consent ships as a model, not as wiring: consent::decide is the pure
decision over the local opt-out, DO_NOT_TRACK and CI;
consent::notice generates the first-run text from the payload schema, so
it cannot promise less or more than the parser enforces;
consent::status renders the exact bytes a sender would POST; and a
rotation predicate bounds the install id to 30 days. The crate performs
none of the doing — it does not read the environment in production, print
the notice, persist an opt-out switch, provide the fz telemetry off and
fz telemetry status commands, hold a pending batch, or send anything (it
never opens a connection). That half belongs to the client, which must
wire the primitives so reporting is off with no network and off before the
next batch. The install id is 16 random bytes the caller supplies, never
derived from anything about the machine — pseudonymous, with no stored
link to a person.
Routes: POST /v1/telemetry/events (public, unauthenticated — the
caller is a CLI; the guards are the RateLimiter port, the batch ceiling
and the closed grammar), GET /v1/telemetry/notice (the
machine-readable consent notice, unauthenticated like the privacy
manifest), and GET /v1/telemetry/admin/usage (admin-guarded aggregate
rows; the GROUP BY is the privacy control, so no per-install row exists
to leak).
What is never sent: identifiers or addresses, paths, repository or branch names, prompts, model output, commit or issue text — free text of any kind. Rejection bodies never echo what the client sent.
Emits telemetry.recorded after an accepted batch. Both tables are
declared in personal_data() with subject: "install_id" and
Disposition::Erase, so cratefield-module-privacy can erase exactly
one install’s rows from the id its own client prints.
See docs/TELEMETRY.md for the payload contract, the consent model and
the client rules this crate implements.
MIT. Built in the open for Cratefield, a Factory Zero venture.
Modules§
- consent
- Consent for aggregate usage reporting (issue #413): the decision, the
notice and the status rendering, all pure. Nothing here reads
std::env, opens a connection or touches the network — the decision runs over anEnvironmentits caller built, so the same code decides on a laptop, in continuous integration and on wasm, and a rule that never makes a request cannot leak one on the way to deciding not to report. - payload
- The batch payload and its closed grammar (issue #413). Every field is validated against a fixed vocabulary — an enum, a bounded count, a duration bucket, a fixed-width hex id or a numeric version triple — and a value outside any of them is a rejection, never a truncation or a coercion. There is no string anywhere a sentence could fit, which is what makes the consent notice short enough to be true: the collector can list every value it will ever hold, because the client sent it.
Structs§
- Settings
- The builder’s settings, cloned into the router state.
- Telemetry
- Aggregate usage telemetry with a consent model built in (issue #413).
Constants§
- MAX_
RETENTION_ DAYS - The longest retention a venture may configure, in days: 36,500 — about
a century, several times any retention a venture has ever wanted. The
ceiling exists because the purge computes
now - retentionas a real date, and a retention in the millions (a number of days fat-fingered from milliseconds or seconds) walks that date out of the representable calendar; catching it at build time is cheaper than a failed run on every cron fire (issue #413).