Persist a user's consent decision¶
The crate stores the decision; you decide where the file lives, when to ask, and what the prompt says.
Pick a path¶
Every consent function takes a &Path. The toolkit convention is
<config_dir>/<tool>/consent.toml — on Linux that is typically
~/.config/<tool>/consent.toml. Use whatever crate you already have for
platform config directories; this one does not pick for you.
Keep consent in its own file rather than inside your main config. It stays unambiguously tool-managed, "forget my decision" becomes a file deletion rather than a config rewrite, and the two schemas can change independently.
Record the decision¶
use rtb_telemetry::consent::{self, Consent};
// User said yes.
consent::write(&path, &Consent::enabled_now())?;
// User said no.
consent::write(&path, &Consent::disabled_now())?;
// User asked you to forget they were ever asked.
consent::reset(&path)?; // idempotent — Ok(()) when already absent
Both *_now constructors stamp decided_at with the current UTC time, so
you can tell later how old the decision is — for a "you opted in two years
ago, still happy?" prompt, for example.
Parent directories are created for you. The write is not atomic; if a torn file would be a problem for your tool, write to a temporary file and rename it over the top.
Turn the stored decision into a policy at startup¶
use rtb_telemetry::{CollectionPolicy, TelemetryContext};
let policy = match consent::read(&path)? {
Some(record) => CollectionPolicy::from(record.state),
None => CollectionPolicy::Disabled, // never asked
};
let telemetry = TelemetryContext::builder()
.tool(env!("CARGO_PKG_NAME"))
.tool_version(env!("CARGO_PKG_VERSION"))
.salt(concat!(env!("CARGO_PKG_NAME"), ".telemetry.v1"))
.sink(sink)
.policy(policy)
.build();
ConsentState::Unset maps to Disabled too, so a file present with
state = "unset" behaves the same as no file at all for collection
purposes — while still letting you distinguish the two when deciding
whether to prompt.
Tell the difference between "said no" and "never asked"¶
use rtb_telemetry::consent::ConsentState;
match consent::read(&path)? {
None => prompt_for_consent()?, // never asked
Some(c) if c.state == ConsentState::Unset => prompt_for_consent()?,
Some(_) => {} // already decided — don't ask again
}
Re-prompting someone who has already declined is the fastest way to lose the argument about whether your opt-in is genuine.
Handle a consent file you cannot read¶
read returns TelemetryError::Serde for malformed TOML and for a schema
version other than 1 — a file written by a future version of your tool,
for instance. It does not fall back to a default, on purpose: guessing at
an unreadable consent record means guessing about consent.
Treat it as "not opted in", and say something:
let policy = match consent::read(&path) {
Ok(Some(record)) => CollectionPolicy::from(record.state),
Ok(None) => CollectionPolicy::Disabled,
Err(err) => {
tracing::warn!(%err, "unreadable consent file; telemetry stays off");
CollectionPolicy::Disabled
}
};
If you build on rtb-cli¶
You do not need any of the above. The CLI runtime resolves the policy and
ships telemetry status / enable / disable / reset backed by this module —
see rtb-cli.