Skip to content

feat(agents): content-free usage telemetry and /debug usage card - #12205

Merged
MarkusNeusinger merged 7 commits into
mainfrom
feat/agent-telemetry
Oct 16, 2026
Merged

MarkusNeusinger merged 7 commits into
mainfrom
feat/agent-telemetry

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR adds content-free usage telemetry for the admin-only agent chat ("Use with my data"), plus a small usage card on /debug. The design is the one reviewed by Fable + Gemini, and the owner decided on 2026-10-16: 180-day retention, Looker Studio on BigQuery, and highlights on /debug.

  • Four events, one wide JSON line each, written by anyplot-agents on its own logger (anyplot.agents.telemetry) with log_type: "agent_telemetry":
    • data_input: format, entry, outcome, cause, size/rows/cols buckets, column counts by type, category and missing-value buckets, warning codes, tables, sheets, spec_fit, judge tokens, latency.
    • chat_turn: kind, action, asked_question (a boolean computed on the server), tools, prep ops, undo, to_long, manual_binding_changed, binding-warning codes, refusal, error, reply re-ask, tokens and model per step, latency.
    • pipeline_run: run_index, attempts, rounds, score buckets for rounds 1 and 2, shipped attempt, forced-repair ids, failed gates, reviewer and residual ids, sandbox failure class, time per stage, tokens per stage.
    • outcome: a new beacon from the page, routed through the BFF.
  • Content-free by construction.
    • Every event is a pydantic model with extra="forbid". Each field is a Literal, a bounded int, a bool, or an id with a fixed pattern. A value outside its set drops the line.
    • Parser and binding warning sentences map to closed codes (agents/anyplot/data/warning_codes.py); an unknown sentence becomes other.
    • A canary test runs a whole session with distinctive column names, cells, notes, a file name and a message, and fails if any of them reaches a line.
    • Telemetry lines never carry the user id.
  • Salted session hashes. The telemetry session is an HMAC under a per-process salt. Per the owner's decision, the attribution lines' session now uses the same salt under another domain. Neither hash can be recomputed from the request URLs Cloud Run logs, and the two kinds of lines cannot be joined to each other.
  • Envelope and dedup. Every line carries schema, service/revision, source (live or harness; the eval harnesses tag theirs), spec_id, library, turn (set once per route on the request ledger) and an idempotency_key.
  • BigQuery views in agents/telemetry/views/*.sql:
    • base view deduplicated with QUALIFY ROW_NUMBER();
    • typed per-event views;
    • sessions: funnel, turns_to_first_plot, and kept / refined / abandoned states under a 30-minute inactivity rule;
    • usage and failures;
    • model_prices (eu list prices, kept equal to agents/evals/pricing.py by a test) and cost.
  • GET /debug/agent-usage?days=7|30 (admin): one query over the views through BigQuery's REST jobs.query via google-auth's AuthorizedSession, so there is no new dependency. It answers aggregates only. While AGENT_TELEMETRY_DATASET is unset it answers not_configured, and on a query failure unavailable.
  • Frontend.
    • Result card copy/download, .save() and feedback send post the fire-and-forget beacon {action, version, reaction?}.
    • DebugPage gets an "agent usage" section: 7d/30d toggle, KPI tiles, top specs, formats and refusal codes, and empty states.
  • Docs:
    • new docs/reference/agent-telemetry.md: events, privacy, the exact sink filter, the one-time setup, IAM;
    • docs/index.md;
    • a "Usage telemetry" section in docs/concepts/agent-network.md;
    • docs/reference/api.md;
    • docs/reference/plausible.md.

Decisions

  • manual_binding_changed. The page PUTs the parse's defaults after every paste. The server therefore counts only a PUT whose bindings differ from the ones the session held, so that echo is not counted as a manual change.
  • Outcome actions. The set is copied_code, copied_image, downloaded_png, downloaded_code, downloaded_data, saved_session, feedback. downloaded_bundle from the brief is the same as saved_session, so it is not a separate action.
  • Transcription rows. A transcription offer and its confirmed paste are two data_input rows (outcome transcribed, then ok).
  • Cost. The cost view prices only chat_turn and data_input steps; pipeline_run.steps repeats the per-run share, so pricing it too would count it twice.
  • Beacon limits. 30 per minute and 2,000 per day per user, as constants.
  • Copilot rounds 1 and 2: all nine findings applied.
    • attempts counts every adapter call.
    • The harness source is restored on every exit.
    • A chat_turn is written for disconnected clients (error: cancelled).
    • The usage view groups each line under its own library, with an input_formats view of its own.
    • not_ready runs are recorded.
    • An outcome carries its version's library.
    • Every click is its own row.
    • The API setting is activated through the deploy pipeline.
  • Warning-code mapping. Warning codes come from a regex table that sits next to the modules that write the sentences, with a test. Rewriting all ~56 append sites was not worth it.

Owner tasks (not done here)

  • Before the chat opens to the public: add a LegalPage entry for the telemetry — purpose, the fields in plain words, 180 days, EU location, and that no content is recorded. LegalPage is unchanged because the chat is admin-only.
  • Google Cloud setup (coordinator), following docs/reference/agent-telemetry.md § "Set up the BigQuery side":
    1. Create the log bucket.
    2. Create the sink.
    3. Add the _Default exclusion.
    4. Create the linked dataset.
    5. Create the views dataset.
    6. Create the views.
    7. Grant the API runtime identity roles/bigquery.jobUser.
    8. Grant it roles/bigquery.dataViewer on agent_telemetry and agent_telemetry_logs.
    9. Set the _AGENT_TELEMETRY_DATASET=anyplot.agent_telemetry substitution on the deploy-api trigger. It takes effect at the next API deploy, which smoke-tests the revision and promotes it (new in api/cloudbuild.yaml; empty by default, so the card shows "not configured" until then).

Sink filter:

resource.type="cloud_run_revision"
resource.labels.service_name="anyplot-agents"
jsonPayload.log_type="agent_telemetry"

Test plan

  • pytest tests/unit tests/integration: 12,121 passed. New tests:
    • test_telemetry.py: canary, enum closure, dedup, beacon, salted hashes, echo vs. manual bindings;
    • test_warning_codes.py;
    • test_telemetry_prices.py;
    • test_agent_usage.py: REST decoding, states, gate, dataset-name guard;
    • the BFF beacon in test_agent_router.py.
  • ruff check, ruff format --check, mypy api core agents
  • yarn lint, fm:check, type-check, test (1,158 passed), build
  • /verify-api: a local API on a free port answers GET /debug/agent-usage with not_configured, 401 without a token and 400 for days=9.
  • /verify-frontend: the DebugPage card against a mock /debug API, desktop light and mobile dark (no horizontal scroll); the 7d/30d toggle refetches; the not_configured empty state shows. The beacons are covered by vitest (exact bodies, errors swallowed), not driven in a browser, because the chat needs a live agents service.
  • Live data in BigQuery after the Google Cloud setup.
  • uv run python -m tools.changelog check --base origin/main

Known gap: there is no live loop for the sink and the views until the Google Cloud resources exist. The SQL is checked only structurally (dataset names, prices).

🤖 Generated with Claude Code

https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M

MarkusNeusinger and others added 2 commits October 16, 2026 15:14
Four events (data_input, chat_turn, pipeline_run, outcome) as strict pydantic
models on their own logger with log_type=agent_telemetry; closed warning codes;
salted per-process session hashes (the attribution lines' too); the outcome
beacon route on the agents service and the BFF; BigQuery views (dedup,
sessions, usage, failures, cost) and the admin GET /debug/agent-usage via the
BigQuery REST API; docs with the one-time setup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
The result card, the session save and the feedback send post the content-free
outcome beacon (action, version, reaction only; fire and forget, keepalive).
DebugPage gets an "agent usage" section (7d/30d, KPI tiles, top specs,
formats and refusal codes, empty states for not_configured/unavailable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Copilot AI balanced review requested due to automatic review settings October 16, 2026 13:17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.40991% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
api/agent_usage.py 92.25% 11 Missing ⚠️
agents/anyplot/telemetry.py 98.12% 9 Missing ⚠️
agents/anyplot/tools/session.py 66.66% 1 Missing ⚠️
api/routers/debug.py 90.90% 1 Missing ⚠️
app/src/sections/debug/AgentUsageSection.tsx 98.57% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Telemetry loss, incorrect usage aggregation, and persistent harness tagging undermine reporting accuracy.

5 open findings
What changed in this PR

Adds content-free usage telemetry for agent chat, with BigQuery analysis and admin-only usage highlights on /debug.

Changes:

  • Records input, turn, pipeline, and outcome events with salted session identifiers.
  • Adds BigQuery views, an aggregate API endpoint, and a usage card.
  • Adds privacy, beacon, and reporting tests plus setup documentation.
File Description
tests/​unit/​api/​test_agent_usage.py Tests usage endpoint and response decoding.
tests/​unit/​api/​test_agent_router.py Tests outcome beacon forwarding.
tests/​unit/​agents/​runtime/​test_telemetry.py Tests privacy, schemas, and telemetry events.
tests/​unit/​agents/​runtime/​test_data_code_tool.py Filters attribution logs in assertions.
tests/​unit/​agents/​evals/​test_telemetry_prices.py Checks SQL pricing consistency.
tests/​unit/​agents/​data/​test_warning_codes.py Tests closed warning codes.
docs/​reference/​plausible.md Distinguishes telemetry from browser analytics.
docs/​reference/​api.md Documents new endpoints and configuration.
docs/​reference/​agent-telemetry.md Documents privacy, events, and cloud setup.
docs/​index.md Links telemetry documentation.
docs/​concepts/​agent-network.md Explains telemetry architecture.
core/​config.py Adds telemetry dataset configuration.
changelog.d/​agent-usage-telemetry.md Records telemetry and dashboard additions.
app/​src/​sections/​debug/​AgentUsageSection.tsx Displays usage metrics and empty states.
app/​src/​sections/​agent-chat/​ResultCard.tsx Reports successful copies and downloads.
app/​src/​sections/​agent-chat/​ResultCard.test.tsx Tests result-action reporting.
app/​src/​sections/​agent-chat/​ChatThread.tsx Passes outcome callbacks to cards.
app/​src/​pages/​DebugPage.tsx Integrates the usage section.
app/​src/​pages/​DebugPage.test.tsx Tests usage displays and window selection.
app/​src/​pages/​AgentChatPage.tsx Connects result outcome reporting.
app/​src/​pages/​AgentChatPage.feedback.test.tsx Tests feedback and outcome beacons.
app/​src/​lib/​agent.ts Defines beacon types and transport.
app/​src/​lib/​agent.test.ts Tests beacon payloads and failure handling.
app/​src/​hooks/​useAgentSession.ts Reports result, save, and feedback outcomes.
app/​src/​hooks/​useAgentSession.save.test.ts Tests save outcome reporting.
api/​routers/​debug.py Exposes admin usage aggregates.
api/​routers/​agent.py Validates and forwards outcome beacons.
api/​agent_usage.py Queries BigQuery and decodes aggregates.
agents/​telemetry/​views/​05_prices_cost.sql Adds model pricing and cost views.
agents/​telemetry/​views/​04_usage_failures.sql Aggregates usage and failure codes.
agents/​telemetry/​views/​03_sessions.sql Defines session funnels and end states.
agents/​telemetry/​views/​02_event_views.sql Adds typed event views.
agents/​telemetry/​views/​01_events.sql Deduplicates telemetry log entries.
agents/​stream.py Records whether replies end with questions.
agents/​main.py Instruments routes and accepts outcome beacons.
agents/​evals/​prep.py Tags preparation evaluations as harness traffic.
agents/​evals/​matrix.py Tags matrix evaluations as harness traffic.
agents/​anyplot/​tools/​session.py Records preparation operations and warning codes.
agents/​anyplot/​plugins/​tool_safety.py Records allowed tool calls.
agents/​anyplot/​plugins/​scope_guard.py Records judge token usage.
agents/​anyplot/​plugins/​ledger.py Adds usage counters and salted hashes.
agents/​anyplot/​plugins/​budget.py Records per-step model usage.
agents/​anyplot/​pipeline.py Records pipeline metrics and timings.
agents/​anyplot/​dataset_guard.py Records dataset judge usage.
agents/​anyplot/​data/​warning_codes.py Maps warning sentences to closed codes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agents/anyplot/pipeline.py Outdated
Comment thread agents/evals/matrix.py Outdated
Comment thread agents/main.py Outdated
Comment thread agents/telemetry/views/04_usage_failures.sql Outdated
Comment thread agents/telemetry/views/04_usage_failures.sql Outdated
…elled turns, usage views)

- pipeline_run.attempts counts every adapter call (run.attempts), rounds stay the page's
- the eval harnesses tag lines harness inside telemetry.harness(), restored on every exit
- a turn whose client went writes its chat_turn once (error cancelled) after the producer stopped;
  the normal line is written before the error event is yielded
- usage groups by library per line; input formats move to their own view

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@MarkusNeusinger

Copy link
Copy Markdown
Owner Author

Copilot round 1: all five findings applied in d880e93 — pipeline_run.attempts now counts every adapter call (rounds stays the page's count); the eval harnesses tag lines inside telemetry.harness(), restored on every exit; a turn whose client disconnects writes its chat_turn once (error: cancelled) after the producer stopped, and the normal line is written before the error event is yielded; the usage view groups each line under its own library, and input formats moved to their own view (input_formats) so session totals are never copied onto every format.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Missing run events and incorrect outcome attribution and deduplication distort the telemetry metrics.

1 open finding
5 resolved since last review
Previously missed (3)

In code that hasn't changed since last review

Medium severity Missing terminal pipeline_run events for not-ready attempts

agents/​anyplot/​pipeline.py:1340

The readiness return at lines 1285–1288 bypasses this telemetry writer. Calls with missing data, incomplete bindings, or unconfirmed data emit no pipeline_run, so the failures view's not_ready branch receives no rows and the session funnel misses these attempted plots. Emit a terminal run event on that path too, and extend PipelineRun.reason to accept the existing closed NotReadyReason values.

Medium severity Include request ID in activity event idempotency keys

agents/​main.py:2143

Separate clicks with the same action, version, and reaction within one turn produce the same idempotency key. The base view keeps only the earliest row, discarding later copies/downloads and preventing that activity from updating sessions.last_at for the inactivity rule. Include the request ID in the discriminator: the BFF generates one per request, while duplicate deliveries of the same log line still collapse.

Low severity Promote configured API revision before serving traffic

docs/​reference/​agent-telemetry.md:202

The API deployment pins traffic to a named revision (api/cloudbuild.yaml:277–288). Updating environment variables creates a revision without moving that pinned traffic, so this procedure leaves the serving API without the setting and the usage card at not_configured. Require a smoke check and explicit promotion of the newly configured revision, or activation through the existing smoke-tested deployment pipeline.

🧠 Review effort: Balanced

Comment thread agents/main.py
…y and keys, API setting via the pipeline)

- a plot asked for before the data is ready writes a not_ready pipeline_run with its reason
- an outcome on a version carries that version's library (404 for an unknown version);
  every click is its own row (request id in the key)
- AGENT_TELEMETRY_DATASET comes from the deploy-api trigger's substitution, like
  AGENT_SERVICE_URL, so the smoke-tested pipeline activates it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@MarkusNeusinger

Copy link
Copy Markdown
Owner Author

Copilot round 2: all four findings applied in ce8b40d — outcomes on a version carry that version's library (unknown version → 404) and every click is its own row (request id in the key); a plot asked for before the data is ready now writes a not_ready pipeline_run with its reason; AGENT_TELEMETRY_DATASET now comes from the deploy-api trigger's _AGENT_TELEMETRY_DATASET substitution (api/cloudbuild.yaml), so the smoke-tested pipeline activates it instead of an unpromoted revision. Field grazed: no further review round requested.

MarkusNeusinger and others added 2 commits October 16, 2026 15:58
- /debug query reads the linked log view with the window filter before the dedupe
  (partition pruning), a 10-minute floored @SInCE parameter (cacheable) and a
  10-minute in-process cache of ok answers; client start failures are never cached
- cost: model ids normalised to the price key (version suffixes, models/ prefix),
  one step entry per (step, model), the judges carry the judge model
- telemetry source is a ContextVar: a harness run cannot tag live requests
- sessions/usage: not_ready asks are no runs, new funnel steps (outcome_only,
  transcription_offered, plot_not_ready), a transcription counts once
- every parser warning in the data test corpus must have a code (conftest)
- a converted file's kind survives a paste refused at the limits
- test: two outcome actions never share a key

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@MarkusNeusinger
MarkusNeusinger merged commit 3b8646d into main Oct 16, 2026
15 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/agent-telemetry branch October 16, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants