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. That is what a "you opted in two
years ago, still happy?" prompt needs.
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.