nat.utils.telemetry.consent#

First-run consent prompt for NAT telemetry.

Order of precedence for whether telemetry is active:

  1. NAT_TELEMETRY_ENABLED environment variable, if set (any value).

  2. Persisted consent file at ~/.config/nat/telemetry.toml.

  3. Interactive prompt — only if all three of stdin, stdout, and stderr are TTYs (see is_interactive_session()). The prompt itself is rendered to stderr, so a captured stderr (2>/log, journald, Docker / CI log capture) would be invisible-but-effectful; gating on all three streams prevents that footgun.

  4. Default OFF, in non-interactive contexts (CI, cron, daemons).

The prompt is shown at most once per machine: the user’s answer is persisted, and subsequent invocations use the persisted value silently. The prompt explicitly tells the user what is collected, what is not collected, and how to change their decision later.

Attributes#

logger

_CONSENT_FILE_ENV_VAR

TELEMETRY_ENV_VAR

The single env var that, when set, overrides every other consent signal

PROMPT_VERSION

Bump if we materially change the prompt language. The persisted decision

_TRUTHY

Classes#

ConsentState

Enum where members are also (and must be) strings

Functions#

resolve_env_consent(→ bool | None)

Return the env-var-driven consent override, or None if unset.

_consent_file_path(→ pathlib.Path)

Resolve the consent file location.

read_persisted_consent(→ ConsentState)

Read the user's persisted consent decision, if any.

write_persisted_consent(→ None)

Persist the user's consent decision.

is_interactive_session(→ bool)

Whether we should attempt to prompt the user.

render_prompt(→ str)

The user-facing consent prompt.

prompt_user(→ ConsentState)

Display the consent prompt and read the user's answer.

resolve_initial_consent(→ bool)

Resolve telemetry state at module import time, without prompting.

maybe_prompt_for_consent(→ None)

Run the first-run consent prompt if needed.

Module Contents#

logger#
TELEMETRY_ENV_VAR = 'NAT_TELEMETRY_ENABLED'#

The single env var that, when set, overrides every other consent signal (persisted file, first-run prompt). Centralised here so producers and consumers share one canonical name.

PROMPT_VERSION = '1.0'#

Bump if we materially change the prompt language. The persisted decision records which prompt version the user saw, so we can re-prompt on substantive changes (e.g. new categories of data collected).

_TRUTHY = ('1', 'true', 'yes')#
class ConsentState#

Bases: enum.StrEnum

Enum where members are also (and must be) strings

Initialize self. See help(type(self)) for accurate signature.

ENABLED = 'enabled'#
DISABLED = 'disabled'#
NEVER_ASKED = 'never_asked'#

Return the env-var-driven consent override, or None if unset.

Single source of truth for “what does NAT_TELEMETRY_ENABLED say”. Used by:

Returns:

True if the env var is set to a truthy value (case-insensitive 1 / true / yes); False if set to anything else; None if unset (caller should consult the persisted decision and/or prompt).

Resolve the consent file location.

Honors NAT_TELEMETRY_CONSENT_FILE for tests; falls back to ~/.config/nat/telemetry.toml for production use.

Read the user’s persisted consent decision, if any.

Asymmetric prompt_version gating, designed around user trust:

  • Current ``prompt_version``: return the persisted state as-is.

  • Stale or missing ``prompt_version`` with ``consent = “disabled”``: return DISABLED. A user who explicitly opted out under any version of the prompt must remain opted out — we never silently re-enable telemetry for someone who said no, even if we materially change the disclosure.

  • Stale or missing ``prompt_version`` with ``consent = “enabled”``: return NEVER_ASKED to force a re-prompt under the new disclosure. A stale “yes” from a previous prompt version should not silently authorize collection under a new (potentially broader) disclosure.

  • File missing, malformed, or unrecognized consent value: return NEVER_ASKED.

The asymmetry is the key: re-prompting an already-disabled user combined with the default-yes prompt would be a silent opt-in flip — the worst possible privacy regression. Re-prompting an already-enabled user is conservative and respects the new disclosure.

Persist the user’s consent decision.

Writes a small TOML file at the resolved consent path. Silently ignores write failures (filesystem permission errors, full disk, etc.) — the next interactive run will simply re-prompt.

is_interactive_session() bool#

Whether we should attempt to prompt the user.

Requires all three standard streams to be TTYs:

  • stdin — so input() can read the user’s reply.

  • stdout — so any echoed feedback reaches the user.

  • stderr — because prompt_user() writes the prompt itself to stderr; if stderr is captured (2>/some/log, journald, Docker log capture, CI log files), the user would be asked a question they cannot see and a keystroke / Enter would silently change their consent state.

Pipes, redirects, CI runners, daemons, and any other headless context return False — in those cases we never prompt, and telemetry defaults to OFF.

render_prompt() str#

The user-facing consent prompt.

Kept inline (not in a separate file) so tests can assert on its contents and reviewers see any wording change in PR diffs.

prompt_user() ConsentState#

Display the consent prompt and read the user’s answer.

Returns ENABLED on an explicit y / yes or on an empty line (just pressing Enter, matching the [Y/n] default). Returns DISABLED on n / no, on any other input, or on EOF / KeyboardInterrupt. The decision is always persisted by the caller, so a hostile interrupt is treated as “no thanks” rather than re-prompting indefinitely.

Resolve telemetry state at module import time, without prompting.

Used by config.TELEMETRY_ENABLED so a process that imports the telemetry package without going through the CLI entrypoint (e.g. a library user) gets a sensible default. Order:

  1. NAT_TELEMETRY_ENABLED env var, if set.

  2. Persisted consent file.

  3. Default OFF — until the CLI prompt or env var resolves it.

Run the first-run consent prompt if needed.

Called by the CLI entrypoint group callback. No-op when:

  • NAT_TELEMETRY_ENABLED env var is set (the user opted via env).

  • A persisted consent decision already exists.

  • The session is non-interactive — see is_interactive_session(), which requires stdin, stdout, and stderr to all be TTYs.

Otherwise: print the prompt, read the user’s answer, persist the decision, and update the live TELEMETRY_ENABLED flag so the rest of this same invocation honors the choice.