feat(analytics): PR-C -- Usage Analytics v1 API (/analytics/*) - #39
Merged
Conversation
Third of four sub-PRs. Wires PR-A's aggregates and PR-B's clients/RBAC
into 8 live endpoints and mounts them on main.py.
Hard verification gate (user-mandated) completed before writing any of
this PR:
- Read TES's routes_runs.py directly: GET /api/runs DOES carry
organization_id/team_id per run (RunRecord), but is scoped to the
*forwarded token's own* identity (store.list(organization_id=identity.
organization_id, team_id=identity.team_id, filtered=True)), not by a
query parameter aimed at an arbitrary org. Consequence: workflows_run
is always null for a platform_admin (can't honestly answer "org X's
workflows" for an identity that may not belong to org X), documented
in service.py's own module docstring rather than guessed.
- Read omnibioai-auth's GET /orgs/{org}/teams/{team}/members: unpaginated
(team_service.list_team_members's own single .all() query), returns
the complete roster in one call. Confirmed safe to use for team-level
attribution via roster intersection -- implemented as planned.
Additions:
- analytics/service.py: orchestrates aggregator/prometheus/billing_client/
tes_client into each endpoint's response. Team-scoped queries/active-
users use roster intersection against analytics:user_activity/
analytics:active_users (server-side only -- never returns raw user IDs).
A failed roster fetch returns null fields + team_scope_available:false,
never silently substitutes the org-level number.
- analytics/router.py: GET /analytics/{overview,queries,users,services,
workflows,performance,usage} + /analytics/export (CSV), all behind
Depends(require_analytics_scope), all cache-aside (5min TTL) via
cache.get_or_set_async, all delegating computation to service.py.
- /analytics/performance is explicitly labeled "scope": "platform" with
org_id/team_id always null, per this feature's hard requirement that
audit:events-derived numbers (no organization_id on that stream) are
never presented as tenant-scoped.
- aggregator.py additions (additive, built on PR-A): a per-org known-
services index (drives /analytics/services) and an internal-only raw
active-user-id reader (drives team-roster intersection, never exposed
via any response).
- cache.py addition: get_or_set_async, the async twin of PR-A's
get_or_set, for compute functions that call other services over http.
- main.py: mounted analytics_router, same per-route (not blanket)
authorization posture routes_dashboard.py already established.
100% coverage on the entire analytics package (all 12 modules); full
existing suite (1151 tests) green at 99.84% overall, above the 98% gate.
man4ish
added a commit
that referenced
this pull request
Aug 12, 2026
Fourth and final sub-PR. Frontend for the /analytics/* API (#39 PR-C). - analytics.ts: data layer, mirroring interactions.ts's own shape -- typed fetch wrappers for all 7 GET endpoints plus exportAnalyticsCsv (fetched, not a bare <a href>, so Authorization can be attached; blob download). /analytics/performance's response type pins org_id/team_id to `null` and scope to 'platform' at the type level, matching the backend's own hard requirement that those numbers are never presented as tenant-scoped. - pages/AnalyticsDashboard.tsx: reuses the existing DashboardGrid/ MetricCard widget family (PR10) and recharts (already a dependency, already used by BillingPage.tsx/EcosystemPage.tsx -- same chart colors, no new visual system). Overview cards, a queries/active-users trend line chart, DAU/WAU/MAU, a service breakdown table, performance cards (explicitly labeled platform-wide), 7d/30d/90d date presets, CSV export, and an organization filter shown only to a platform_admin (an org_admin/team_admin's scope is already fixed server-side to their own org/team). Null values render as "--", never a fabricated number, same convention DashboardPage.tsx already established. - auth.ts: canSeeAnalytics(), a UX-only mirror of the backend's require_analytics_scope matrix (platform_admin/org_admin/team_admin allowed, regular member denied) built from the SessionUser fields Phase 1 PR3/Team Management v0.8.0 already put there (orgRoles, teamRole) -- no new claim, no new auth mechanism. - navigation.ts / AdminApp.tsx: wired in exactly like 'interactions' was -- one NAV entry (Business section, next to Billing) gated by canSeeAnalytics, one switch case rendering AnalyticsDashboard. Test plan: `npm test` -- 474 passed (38 files), no regressions. AnalyticsDashboard.test.tsx (10 tests): loads and renders live data, date-range re-fetch, API failure + retry, 403 permission-denied state, empty-data state, null-value "--" rendering, CSV export (success + failure), organization filter shown only for a platform_admin. `tsc -b` and `vite build --mode admin` both clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Third of four sub-PRs implementing Usage Analytics v1 (see #37 PR-A, #38 PR-B). Wires the merged aggregator/clients/RBAC into 8 live endpoints.
Hard verification gate (user-mandated, completed before any PR-C code)
/api/runs: readroutes_runs.pydirectly. It DOES carryorganization_id/team_idper run, but is scoped to the forwarded token's own identity (store.list(organization_id=identity.organization_id, team_id=identity.team_id, filtered=True)), not by a query parameter. Consequence:workflows_runis alwaysnullfor a platform_admin -- can't honestly answer "org X's workflows" for a token that may not belong to org X. Documented inservice.py, not guessed.GET /orgs/{org}/teams/{team}/members): readteam_service.list_team_membersdirectly -- a single unpaginated.all()query, always returns the complete roster. Confirmed safe for the roster-intersection mechanism -- implemented as planned.What's in this PR
analytics/service.py-- orchestrates aggregator/prometheus/billing_client/tes_client per endpoint. Team-scoped queries/active-users use roster intersection (server-side only, never returns raw user IDs). A failed roster fetch returns null +team_scope_available: false, never silently substitutes org-level numbers.analytics/router.py--GET /analytics/{overview,queries,users,services,workflows,performance,usage}+/analytics/export(CSV), all behindDepends(require_analytics_scope), all cache-aside (5min TTL)./analytics/performanceexplicitly labeled"scope": "platform"withorg_id/team_idalwaysnull--audit:eventshas no organization_id, per this feature's hard requirement these numbers are never presented as tenant-scoped.aggregator.py/cache.pyfrom PR-A (a services index, an internal-only raw active-user-id reader, an async cache helper).main.py: mounted, same per-route (not blanket) authorization posture asroutes_dashboard.py.Test plan
python -m pytest: 1151 passed, 99.84% overall (gate: 98%). Entireanalyticspackage (12 modules): 100% coverage.🤖 Generated with Claude Code