Skip to main content

Crate cratefield_module_telemetry

Crate cratefield_module_telemetry 

Source
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.

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 — What clients did, never who did it.

cratefield-module-telemetry on crates.io cratefield-module-telemetry documentation MIT

§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 an Environment its 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 - retention as 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).