Skip to content

feat(analytics): give account owners the traffic explorer - #483

Open
alukach wants to merge 3 commits into
mainfrom
feat/account-analytics
Open

feat(analytics): give account owners the traffic explorer#483
alukach wants to merge 3 commits into
mainfrom
feat/account-analytics

Conversation

@alukach

@alukach alukach commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Repurposes the admin analytics portal for account owners/maintainers.

What changed

Shared view. The admin page's body moves to BreakdownExplorer — same controls, chart, and ranked table. The admin page is now a gate plus one render call.

Account scope. A new /{account}/-/analytics route pins its account through scopeFilters (applied to every query, absent from the UI and URLs) and passes every dimension except account as the offered filter/group-by set. parseState only accepts filters and group-bys for offered dimensions, so ?account=someone-else&groupBy=account can't widen the view — pinned by a unit test.

Reached as a tab. PROFILE | ANALYTICS on the account profile, mirroring the product's PRODUCT | ANALYTICS. Same ?tab=analytics middleware rewrite, whose match now covers one-segment account roots as well as two-segment product roots. ProductTabs.tsx becomes Tabs.tsx with ProductTabs and AccountTabs over one shared strip.

Preview callout. The same blue notice the product analytics page shows.

Authorization is canManageAccount (owners, maintainers, admins) — same 404 for everyone else, from generateMetadata as well as the page body.

Screen.Recording.2026-08-04.at.11.31.31.AM.mov

Notes

  • A GET form's action drops its own query string, so the explorer splits baseUrl into a path (form action, link base) and params re-emitted as hidden inputs — that's what keeps tab=analytics alive across a filter submit.
  • The tab strip appears on the profile only for viewers who can manage the account.

Verification

  • npx jest — 485 passed; the 3 failing suites (5 tests) fail identically on main (recharts/RTL, untouched files).
  • npm run type-check — no new errors; the pre-existing AdminBreakdownChart.tsx / panels.tsx errors are unchanged and also block npm run build on main. Routing was verified with a one-off build that skipped type checking: compiled clean, no route conflict between [account_id]/-/ and [account_id]/[product_id].
  • Not exercised against a live Analytics Engine dataset.

🤖 Generated with Claude Code

The admin traffic explorer becomes a shared BreakdownExplorer: the admin
page keeps every dimension, while an account-scoped tab pins its account
via hidden scope filters and drops "account" from the filter/group-by
chips so the query string can't widen the view.

Reached through a PROFILE | ANALYTICS tab on the account profile, mirroring
the product tab — the same ?tab=analytics middleware rewrite, now matching
one-segment account roots too. Carries the product page's preview callout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
source-cooperative Ready Ready Preview Aug 4, 2026 6:44pm

Request Review

@claude

claude Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 3m 11s —— View job


Reviewing PR #483

  • Gather context (PR body, diff, changed files)
  • Read changed files in detail
  • Check middleware/route matching logic
  • Check auth/scoping logic (canManageAccount, scopeFilters)
  • Check BreakdownExplorer.tsx and test
  • Post final review

✅ No blocking issues — safe to merge.

Went through the diff against origin/main in detail — the scoping mechanism, authorization, and routing changes all hold up:

  • scopeFilters (src/components/features/analytics/BreakdownExplorer.tsx:236) is spread after state.filters, so a scoped view's pinned dimension always wins even if it were somehow duplicated — and it can't be, since parseState (BreakdownExplorer.tsx:100-134) only builds filters/group-bys from the dimensions array the page offers, which excludes "account" for the account explorer. BreakdownExplorer.test.ts pins this with a direct attempt at ?account=other-account&groupBy=account.
  • The account-scope filter is enforced server-side via SQL (src/lib/clients/analytics/index.ts:518, FILTER_SQL.account), not just hidden in the UI, so there's no way to bypass it by crafting the query string.
  • src/middleware.ts:94 widens the analytics-tab regex from two segments to one-or-two; NON_ACCOUNT_SEGMENTS is still only checked against the first segment, same as before this PR, so no new gap there. The new one-segment case is covered in middleware.test.ts.
  • Auth: authorizedAccount() in src/app/(app)/[account_id]/-/analytics/page.tsx:26 gates both generateMetadata and the page body with the same canManageAccount check and 404, matching the product-analytics precedent.
  • Tabs.tsx/index.ts rename preserves both call sites (ProductTabs in the product layout/analytics page, new AccountTabs in the account page/analytics page) — no dangling imports.
  • showSql defaults to false and is only passed true from the admin page, so the SQL debug payload doesn't leak into the account-scoped view's serialized props.

Nothing here blocks merge. Verification was limited to static review — this environment didn't permit running npx jest/npm run type-check myself, so I relied on the PR description's reported results (485 passed, no new type errors) for dynamic confirmation.

The account view titles itself with the account name, so the shared
explorer no longer owns a heading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@alukach
alukach marked this pull request as ready for review August 4, 2026 18:30
BreakdownExplorer forwarded breakdown.queries unconditionally, so the
account view handed every owner and maintainer the Analytics Engine
dataset name and blob1–blob9 layout. Gate it on an explicit showSql the
caller opts into (admin page only) and withhold the prop entirely when
closed — hiding the trigger would still ship the SQL to the client.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant