A help-only review of tollbit analytics (findings in analytics-cli-review.md, untracked) turned up 31 issues. The command group is agent-facing, so the fixes split into three buckets: what the CLI itself can fix (help text, output, input, error hints), what the bundled skill must teach agents, and what only the server can provide (limits, truncation, descriptions, error codes, data-model cleanup). Server limits (10k rows, ~30 GB scanned, 10 s) are configured server-side and must never be hard-coded in the CLI; the CLI renders whatever the server reports.
Decisions already made:
- JSON stays the default output. Add other formats behind a flag.
- Server limits come from the server, not the CLI. Proposed contract below.
--user-agentis removed from both analytics commands. It only affected token minting and never reached the analytics request. The client still sends the CLI's ownUser-AgentandX-Tollbit-Clientheaders.- Server data-model asks are recorded as a backlog section, not built here.
Schema GET /analytics/agent/v1/query/schema moves from a top-level array to an object:
{
"dialect": "bigquery",
"tables": [
{
"name": "user_agent_aggregate",
"description": "Daily request counts per host, user agent, status class and request kind.",
"columns": [
{"name": "timestamp", "type": "TIMESTAMP", "description": "Day bucket, 00:00 UTC."},
{"name": "type", "type": "STRING", "description": "Request kind.", "values": ["REQUEST", "ROBOT", "SITEMAP", "WELL-KNOWN"]}
]
}
],
"limits": {
"max_rows": {"value": 10000, "unit": "rows", "description": "Result sets are capped at this many rows. Use ORDER BY with LIMIT/OFFSET to page."},
"max_bytes_scanned": {"value": 32212254720, "unit": "bytes", "description": "Queries estimated to scan more than this are rejected. Filter on timestamp and select fewer columns."},
"max_duration": {"value": 10, "unit": "seconds", "description": "Queries running longer than this are cancelled."}
}
}limits is a map of name to {value, unit, description}. The CLI iterates the map and prints every entry it receives, so new limits need no CLI change. description and values are optional everywhere; the CLI omits what is absent.
Query POST /analytics/agent/v1/query adds a meta block:
{"columns": [...], "rows": [...], "meta": {"row_count": 10000, "truncated": true, "bytes_scanned": 1234567, "duration_ms": 850}}Errors keep ProblemJSON and add code values the CLI can map to hints: analytics_unknown_table, analytics_scan_limit_exceeded, analytics_query_timeout, analytics_statement_not_allowed. detail for the scan-limit case should include the estimate and the limit. The unknown-table detail should list available tables and stop referencing the REST path.
All in internal/cli/analytics.go and internal/client/analytics/client.go unless noted.
- Add
analyticsLongHelp,analyticsQueryLongHelp,analyticsSchemaLongHelpconsts following thesearchLongHelppattern (internal/cli/search.go:21-26). Cover: runschemafirst, BigQuery Standard SQL, only SELECT, dailytimestampbuckets, always filter ontimestampfor the per-page and referrer tables, limits are reported byschema, stdout is JSON, truncation warning on stderr. - Replace the broken example with several real ones: a
schemacall, a 7-daySUM(count)grouped byuser_agent, a per-path query with a date filter, and a stdin example. Args: separate messages for zero args ("analytics query requires ") and extra args ("analytics query accepts a single argument"); reject blank SQL likesearch.go:46-48.- Remove the
--user-agentflag fromqueryandschema. Identity resolves from the stored profile with no override.
query -reads SQL from stdin (cmd.InOrStdin(),io.ReadAll, trim). Error if empty. No--fileflag.
- Add
--format json|table|csvonquery(defaultjson) and--format json|tableonschema. Invalid value is aUsageError. table:text/tabwriterwith the same params asauth status(internal/cli/auth.go:564-576), header row fromcolumns, NULL rendered as empty.csv: stdlibencoding/csv, header row, NULL as empty.- JSON output is the raw
QueryResponseincludingmetawhen present (addMeta *QueryMetawithjson:"meta,omitempty"). - Schema
tableview: one block per table (name, description, columns with type and description and values), then a "Limits:" block listing every entry of the map, then "Dialect:". JSON view is the raw object.
- After a successful query, if
meta.truncatedis true, print to stderr viaprintLeadingCommand-style helper:warning: result truncated at <row_count> rows (server limit). Add ORDER BY and LIMIT/OFFSET to page, or narrow the query.Number comes frommeta.row_count, never a constant. - Nothing is printed when
metais absent (old server).
- Client
Schemareturns a newSchemaResponse{Dialect string; Tables []QueryTable; Limits map[string]Limit}. Decode intojson.RawMessage, sniff first non-space byte:[means legacy array of tables,{means the new object. Both paths produceSchemaResponse. QueryTableandQueryColumngainDescription stringandQueryColumngainsValues []string, allomitempty.
- In
runAnalyticsQuery, afterQueryfails, inspect the error witherrors.Asfor*problemjson.Problem. MapCodeto a stderr hint line appended after the error:analytics_unknown_tableandanalytics_statement_not_allowed: "Runtollbit analytics schemato list available tables."analytics_scan_limit_exceededandanalytics_query_timeout: "Runtollbit analytics schemato see query limits, then filter on timestamp or select fewer columns."
- Unknown or absent codes keep today's behavior. No string matching on
detail.
QueryandSchemasetUser-Agent: version.HTTPUserAgent()andX-Tollbit-Client: version.ClientHeader(), matchinginternal/client/tollbit/client.go:355-357. NoTollbit-User-Agentheader and no signature change.- Switch both runners to the
RetryOnOBORequiredbranch used by search/pricing/fetch (internal/cli/search.go:112-122) so behavior matches the rest of the CLI.
- Hiding
--end-user-proximityfrom analytics help. No precedent for hiding flags; it is one line of noise. - Version bump. Release is a separate PR via
make bump. - Removing dead
trim/joinArgsincommon.go.
skill/tollbit-cli/SKILL.md: add an## Analyticssection between Fetch and Auth: purpose (org traffic analytics), always runschemafirst and readlimits, dialect, only SELECT,timestampis a daily bucket and the four log tables need a date filter,typemeaning until the server documents it, JSON shape,--format, stdin, truncation warning, error hints. Include three example queries matching the help. Extend the## For automationbullets:--formaton analytics, and "checkmeta.truncated". Update the frontmatterdescriptionto add "...or query the org's site traffic analytics (bot and referrer logs)". Keepversion: 0.3.2.README.md: add ananalytics query/analytics schemarow to the command table at lines 74-82 and a short section after Feedback with two examples.
Contract (needed for CLI items 4, 5, 6):
- Schema object with
dialect,tables[].description,columns[].description,columns[].values, andlimitsmap as above. - Query
metawithrow_count,truncated,bytes_scanned,duration_ms. - ProblemJSON
codevalues listed above; scan-limitdetailincludes numbers; unknown-tabledetaillists tables; multi-statement error should say "multiple statements are not supported". - Over-limit queries should never surface as 500 (seen once on a 330-day query).
Data model:
5. agent_logs_by_page and page_logs_by_agent are identical. Drop one or document the difference.
6. page_logs_for_referrers and referrer_logs differ only by today's rows. Same ask.
7. normalized_landing_path never differs from landing_path over 90 days. Drop or fix normalization.
8. type = ROBOT means "/robots.txt request", not "is a bot". Rename to request_kind or document via values descriptions.
9. status_code holds only 200/300/400/500. Rename to status_class or document.
10. No bot/human/AI classification column. ip_provider is null for 99% of rows. Add a client_class or similar.
11. timestamp is a day bucket typed TIMESTAMP. Consider a day DATE column or document.
12. Empty-string user_agent vs NULL; user_agent is sometimes a family name and sometimes raw. Document normalization.
13. Edge Health Probe dominates one host. Consider excluding infra probes or tagging them.
14. full_referrer is sometimes an origin and sometimes a full URL. Document.
internal/cli/analytics.go(help, args, stdin, formats, truncation warning, hints, OBO retry, drop--user-agent)internal/cli/analytics_test.go(new cases below)internal/client/analytics/client.goandclient_test.go(headers,SchemaResponse,Meta, legacy array decode)skill/tollbit-cli/SKILL.mdREADME.md
make test(runs both-tags devand release via CI; locallygo test ./... && go test -tags dev ./...).- New CLI tests:
--format tableandcsvrender header plus NULL as empty;query -reads stdin; blank SQL is a usage error; extra args message; truncation warning appears on stderr only whenmeta.truncated; schema decodes both array and object bodies and renders limits generically (a limit name the test invents must print); ProblemJSONcodemaps to the hint on stderr; unknown code prints no hint. - New client tests: request carries
User-AgentandX-Tollbit-Client;Metadecodes; legacy schema array decodes. - CLI test:
--user-agentonanalytics queryoranalytics schemais rejected as an unknown flag (exit 2). - Skill tests:
TestSkillFrontmatterVersionMatchesCLIstill passes; rendered guide has no{{. - Manual:
make build, then./tollbit analytics --help,./tollbit analytics query --help,./tollbit analytics schema --format table,echo 'SELECT 1 AS x' | ./tollbit analytics query -, and a real 7-day query with--format tableagainst the live gateway. The live server still returns the legacy schema array, so limits will print only once the server ships.