rtb-telemetry 0.7.5

Opt-in anonymous usage telemetry with pluggable sinks. Part of the phpboyscout Rust toolkit.
Documentation
---
title: The consent file
description: On-disk TOML format for recorded consent, the schema version rule, and the read/write/reset functions.
---

# The consent file

The `consent` module persists a user's decision about collection so the
answer survives between runs. It is a small, deliberately boring format.

## On-disk format

```toml
version = 1
state = "disabled"
decided_at = "2026-08-02T20:31:52.918653411Z"
```

| Key | Type | Required | Meaning |
|---|---|---|---|
| `version` | integer | yes | Schema version. Must be exactly `1`. |
| `state` | `"enabled"` \| `"disabled"` \| `"unset"` | yes | The recorded decision. |
| `decided_at` | string | no | RFC 3339 UTC timestamp of the decision. Omitted when absent. |

`decided_at` is written with the sub-second precision the clock reports, as
above, not truncated to whole seconds. It is stored as a plain string
rather than a parsed timestamp so that a future schema version can carry a
richer format without breaking deserialisation of version 1.

## Where the file lives

**This crate does not choose the path.** Every function takes a `&Path`
from the caller. The convention across the toolkit is
`<config_dir>/<tool>/consent.toml`, and the CLI runtime
([rtb-cli](https://cli.rust.phpboyscout.uk)) resolves `<config_dir>` from
`directories::ProjectDirs` and owns the `telemetry status / enable /
disable / reset` commands that write it. A tool not built on rtb-cli picks
its own path and gets to keep both halves.

## `ConsentState` and how it maps to a policy

| `ConsentState` | `CollectionPolicy` |
|---|---|
| `Enabled` | `Enabled` |
| `Disabled` | `Disabled` |
| `Unset` (the `Default`) | `Disabled` |

`Unset` and `Disabled` both mean "do not collect". They are distinct
because a tool usually wants to prompt someone who has never been asked
and never prompt someone who has said no.

The conversion is a `From` impl, so `CollectionPolicy::from(state)` or
`state.into()` both work.

## Constructors

| Constructor | State | `decided_at` |
|---|---|---|
| `Consent::unset()` | `Unset` | `None` |
| `Consent::enabled_now()` | `Enabled` | current UTC time |
| `Consent::disabled_now()` | `Disabled` | current UTC time |

`Consent::SCHEMA_VERSION` is the constant `1`, and every constructor stamps
it.

## Reading, writing and deleting

```rust
consent::read(path)  -> Result<Option<Consent>, TelemetryError>
consent::write(path, &consent) -> Result<(), TelemetryError>
consent::reset(path) -> Result<(), TelemetryError>
```

**`read`** returns `Ok(None)` when the file does not exist. A missing file
is not an error, and callers treat it as `Unset`. It returns
`TelemetryError::Serde` for malformed TOML and for a `version` other than
`1`:

```text
serialisation error: consent: unsupported schema version 99 (expected 1)
```

That is a hard refusal, not a downgrade: a file written by a future version
of the format will stop the tool reading consent rather than being
guessed at. Any other filesystem failure (a permissions problem, say)
comes back as `TelemetryError::Io`.

**`write`** creates parent directories on demand and serialises with
`toml::to_string_pretty`. It is **not atomic**: it writes in place, so a
process killed mid-write can leave a truncated file, which the next `read`
will reject as malformed. If that matters, write to a temporary file
yourself and rename it over the top.

**`reset`** deletes the file and returns `Ok(())` if it was already absent,
so `telemetry reset` is idempotent. It deletes the *decision*, not any
collected data; see
[What this does not do](../explanation/what-this-does-not-do.md#there-is-no-data-deletion-path).

## Reading consent at startup

```rust
let policy = match consent::read(&path)? {
    Some(record) => CollectionPolicy::from(record.state),
    None => CollectionPolicy::Disabled,
};
```

That is the whole integration. Nothing in the crate reads the file for you,
and nothing watches it for changes: a context built from a policy keeps
that policy until it is rebuilt.