Files
hermes-agent/hermes_cli/web_routers/shared_metrics.py
teknium1 7a06cb50d7 feat(telemetry): shared-metrics opt-in offer on every surface
One answer per profile, asked once, with the Desktop strip's three equal
choices (Send to Nous / Local only / No thanks) now offered by:

- `hermes setup`: at the end of every flow (Quick, Full, Blank Slate,
  Portal, --quick), via the setup-completed record they all funnel into.
- `hermes` / `hermes --tui`: once before an interactive chat on a profile
  that never answered. Skipped for -q, piped/JSON output, spawned actions
  (HERMES_NONINTERACTIVE), Desktop-hosted panes and managed installs.
- Web dashboard: a banner for the profile being managed, backed by
  GET/PUT /api/shared-metrics/consent.

"No thanks" is the terminal default so Enter never opts anyone in; Esc and
the banner's X leave the question open.

Also fixes the answer that could not stick: a CLI "no" equals the shipped
defaults, so save_config stripped it and every surface (Desktop strip
included) kept asking. The answer is now written to config.yaml directly,
and `decided` has one definition shared by the RPC, the CLI and the router.
2026-09-30 21:15:08 -07:00

49 lines
1.6 KiB
Python

"""``/api/shared-metrics/consent``: the dashboard's first-run shared-metrics offer.
The twin of Desktop's composer strip and the terminal's offer: one answer per profile, the same
two config.yaml keys (``telemetry.shared_metrics.enabled`` / ``.send``), read and written through
``hermes_cli.observability.shared_metrics_consent`` for the ``?profile=`` being managed.
"""
from __future__ import annotations
import asyncio
from typing import Optional
from fastapi import APIRouter
from pydantic import BaseModel
from hermes_cli.web_routers._common import config_write_scope, scoped_to_thread
router = APIRouter()
class ConsentAnswer(BaseModel):
enabled: bool
send: bool = False
def _read_consent() -> dict:
"""``{enabled, send, decided, managed}``; a managed install cannot save, so it is never offered."""
from hermes_cli.config import is_managed, read_raw_config
from hermes_cli.observability.shared_metrics_consent import consent_state
return {**consent_state(read_raw_config()), "managed": is_managed()}
@router.get("/api/shared-metrics/consent")
async def get_shared_metrics_consent(profile: Optional[str] = None):
return await scoped_to_thread(profile, _read_consent)
@router.put("/api/shared-metrics/consent")
async def put_shared_metrics_consent(body: ConsentAnswer, profile: Optional[str] = None):
from hermes_cli.observability.shared_metrics_consent import save_consent
def _write() -> dict:
with config_write_scope(profile):
save_consent(body.enabled, body.send)
return _read_consent()
return await asyncio.to_thread(_write)