feat: org analytics + devpass profile updates - #2833
Conversation
Org-level analytics (apps/ui): - New enterprise-only Organization analytics page at org/analytics with summary stats and cost/usage charts aggregated across every project, read from the hourly rollup tables (not raw logs). - New GET /analytics/activity endpoint (enterprise admin only) with a groupBy of model | project | api key; models collapse to canonical ids server-side. - Group-by toggle (model / project / API key) on the org page, mirroring the existing group-by-API-key control. - Small "Ent" indicator on enterprise-only sidebar items for non- enterprise orgs (Analytics, Guardrails, Compliance, Security Events, Audit Logs). Enterprise contact form: - Add a deployment preference field (self-host / cloud / not sure yet) to the form, API schema, enterprise_contact_submission table, the notification email, and the Discord webhook. DevPass profiles (apps/code): - Move the GitHub README badge off the public profile to the edit page. - Models section now shows canonical models with the provider-family logo (e.g. Alibaba for Qwen) regardless of serving provider, is clickable through to the model page, and drops the Providers section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds organization activity analytics, deployment metadata for enterprise contact submissions, canonical profile model grouping with README badge sharing, sidebar enterprise indicators, and a members loading state. ChangesOrganization activity analytics
Enterprise contact deployment
Profile canonical models and badge sharing
Enterprise navigation badges
Enterprise members loading state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/ui/src/components/enterprise/contact.tsx (1)
82-99: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winUse the generated API client for this submit path.
This PR changes the request shape, but the form still posts with raw
fetch(), so the newdeploymentcontract is not tied to the generated Hono client and can drift silently. Please switch this call touseFetchClient()/useApi()here.As per coding guidelines, "In frontend apps, always use the generated typed API client (
useFetchClient()oruseApi()from@/lib/fetch-client) to call the Hono API, never use rawfetch()for API calls."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/enterprise/contact.tsx` around lines 82 - 99, The enterprise contact form submit path still uses raw fetch instead of the generated typed Hono client. Update the onSubmit flow in ContactForm to use useFetchClient() or useApi() from `@/lib/fetch-client` for the enterprise contact request, and keep the payload aligned with the new deployment field through the typed client so this contract stays source-of-truth and cannot drift.Source: Coding guidelines
🧹 Nitpick comments (1)
apps/ui/src/components/dashboard/dashboard-sidebar.tsx (1)
350-352: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove these explanatory comments.
These blocks just restate what
EnterpriseIndicatorandshowEnterpriseBadge = !isEnterprisealready make obvious, so they add noise without preserving extra context. As per coding guidelines,**/*.{ts,tsx,js,jsx}: No unnecessary code comments.Also applies to: 425-426
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx` around lines 350 - 352, Remove the unnecessary explanatory comments in dashboard-sidebar.tsx around the EnterpriseIndicator usage and the showEnterpriseBadge = !isEnterprise logic; they only restate what the code already makes clear. Keep the implementation as-is, but delete those comment blocks so the Sidebar rendering and enterprise badge conditions remain self-explanatory without extra noise.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/routes/analytics.ts`:
- Around line 539-550: The eachDay() helper is silently truncating date ranges
after 1000 iterations, which can produce partial analytics series. Update the
range handling in analytics.ts to validate the requested span before calling
eachDay(), and reject oversized query-param windows instead of padding a
shortened result. Keep the fix anchored around eachDay() and the route logic
that builds the activity series/summary cards so the SQL totals and returned
buckets always cover the same requested range.
- Around line 803-810: The API-key breakdown query is filtering out deleted keys
because the `leftJoin` in `analytics.ts` is effectively turned into an inner
join by the `inArray(tables.apiKey.keyType, ...)` condition in the `where`
clause. Update the `apiKeyHourlyStats` query so the key-type filter is applied
on the joined `apiKey` side without excluding null joins, allowing
`row.description ?? "Deleted key"` to still label historical rows for removed
keys.
In `@apps/code/src/components/profile/ProfileReadmeBadge.tsx`:
- Around line 6-17: The badge snippet in ProfileReadmeBadge is hard-coding the
public origin, so copied Markdown always points to the production host. Update
ProfileReadmeBadge to receive the public base URL from config or page state
instead of using SITE_URL, and build profileUrl and badgeMarkdown from that
injected origin so preview, staging, and self-hosted deployments generate
correct links.
In `@apps/code/src/components/profile/ProfileView.tsx`:
- Around line 51-60: The fallback icon key in ProfileView’s model aggregation is
using resolveCanonicalModel(row.id).iconKey even when the model is unresolved,
which leaves getProviderIcon() with a raw model string instead of a provider
key. Update the branch that builds the byCanonical entry so unresolved rows use
row.provider as the iconKey fallback, while keeping resolved model data from
resolveCanonicalModel and preserving the existing aggregation logic in
ProfileView.
In `@apps/ui/src/app/dashboard/`[orgId]/org/analytics/org-analytics-client.tsx:
- Around line 132-139: The `isAdmin` check in `org-analytics-client.tsx` treats
`useTeamMembers()` as unauthorized while `teamData` is still undefined, which
causes a brief denial state and keeps analytics queries disabled on first
render. Update the logic around `currentUserRole`/`isAdmin` to distinguish
“membership still loading” from “not admin” and render a neutral/loading state
until team membership is resolved. Apply the same loading-aware handling in the
other affected analytics sections referenced by the existing `isAdmin`/query
gating logic.
In `@apps/ui/src/components/analytics/chart-helpers.ts`:
- Around line 211-243: The top-N selection in aggregateByDimension is still
always sorted by cost, which makes Requests and Tokens views rank the wrong
dimensions. Update the ranking/slicing logic in aggregateByDimension (and the
related helper in the other referenced block) to accept the active metric and
sort by that metric before applying the limit, while still accumulating all
totals correctly.
In `@packages/db/migrations/1782486651_wealthy_tempest.sql`:
- Line 1: The new deployment column on enterprise_contact_submission is
currently unconstrained text, so update the migration to add a database-level
check that limits values to the same enum-like set used elsewhere: self_host,
cloud, and not_sure. Keep the change localized to this migration and make sure
the constraint matches the existing deployment contract enforced by the UI,
route schema, and Drizzle model so invalid values cannot be stored.
---
Outside diff comments:
In `@apps/ui/src/components/enterprise/contact.tsx`:
- Around line 82-99: The enterprise contact form submit path still uses raw
fetch instead of the generated typed Hono client. Update the onSubmit flow in
ContactForm to use useFetchClient() or useApi() from `@/lib/fetch-client` for the
enterprise contact request, and keep the payload aligned with the new deployment
field through the typed client so this contract stays source-of-truth and cannot
drift.
---
Nitpick comments:
In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx`:
- Around line 350-352: Remove the unnecessary explanatory comments in
dashboard-sidebar.tsx around the EnterpriseIndicator usage and the
showEnterpriseBadge = !isEnterprise logic; they only restate what the code
already makes clear. Keep the implementation as-is, but delete those comment
blocks so the Sidebar rendering and enterprise badge conditions remain
self-explanatory without extra noise.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: bbef82b7-928e-48bb-9307-e555031cd9d1
📒 Files selected for processing (19)
apps/api/src/routes/analytics.tsapps/api/src/routes/public-contact.tsapps/api/src/utils/discord.tsapps/code/src/app/profile/ProfilePageClient.tsxapps/code/src/components/profile/ProfileReadmeBadge.tsxapps/code/src/components/profile/ProfileView.tsxapps/code/src/components/profile/ProfileWrapped.tsxapps/code/src/lib/model-family.tsapps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsxapps/ui/src/app/dashboard/[orgId]/org/analytics/page.tsxapps/ui/src/components/analytics/chart-helpers.tsapps/ui/src/components/analytics/dimension-usage-card.tsxapps/ui/src/components/analytics/dimension-usage-over-time-card.tsxapps/ui/src/components/dashboard/dashboard-sidebar.tsxapps/ui/src/components/enterprise/contact.tsxpackages/db/migrations/1782486651_wealthy_tempest.sqlpackages/db/migrations/meta/1782486651_snapshot.jsonpackages/db/migrations/meta/_journal.jsonpackages/db/src/schema.ts
| .leftJoin(tables.apiKey, eq(tables.apiKey.id, apiKeyHourlyStats.apiKeyId)) | ||
| .where( | ||
| and( | ||
| inArray(apiKeyHourlyStats.projectId, projectIds), | ||
| inArray(tables.apiKey.keyType, ["user", "end_user_customer"]), | ||
| gte(apiKeyHourlyStats.hourTimestamp, startDate), | ||
| lte(apiKeyHourlyStats.hourTimestamp, endDate), | ||
| ), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
The API-key breakdown drops deleted keys entirely.
The leftJoin() is nullified by inArray(tables.apiKey.keyType, ...) in the WHERE clause, so historical rows for deleted keys are filtered out before row.description ?? "Deleted key" can run. That makes /activity?groupBy=apiKey under-report usage whenever keys have been removed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/api/src/routes/analytics.ts` around lines 803 - 810, The API-key
breakdown query is filtering out deleted keys because the `leftJoin` in
`analytics.ts` is effectively turned into an inner join by the
`inArray(tables.apiKey.keyType, ...)` condition in the `where` clause. Update
the `apiKeyHourlyStats` query so the key-type filter is applied on the joined
`apiKey` side without excluding null joins, allowing `row.description ??
"Deleted key"` to still label historical rows for removed keys.
✅ Verified locally against seeded dataRan the full stack ( Org-level analytics (
Enterprise gating + sidebar indicators (free org):
A ~13s screen recording of the org-analytics + breakdown-by-project flow was captured (GitHub doesn't accept video uploads via CLI — it can be dragged into this PR from the recording). |
- Project and org nav items can share a trailing segment (e.g. /analytics), so gate isActive on whether the route is org-scoped — otherwise the project "Analytics" item also highlighted on the org analytics page. - Enterprise indicator is now an icon-only subtle blue mark (dropped the "Ent" label, which read as noise). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/ui/src/components/dashboard/dashboard-sidebar.tsx (1)
350-352: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRemove these explanatory comments.
They restate what the nearby JSX/logic already makes clear, and this repo explicitly avoids unnecessary comments. As per coding guidelines, "No unnecessary code comments".
Also applies to: 965-968
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx` around lines 350 - 352, Remove the unnecessary explanatory comments in dashboard-sidebar.tsx around the sidebar entry marker logic; the nearby JSX and conditional rendering already make the intent clear. Delete the comment block near the enterprise-plan indicator and the similar comment in the collapsed-sidebar section, keeping the actual rendering logic in the dashboard sidebar component unchanged.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@apps/ui/src/components/dashboard/dashboard-sidebar.tsx`:
- Around line 350-352: Remove the unnecessary explanatory comments in
dashboard-sidebar.tsx around the sidebar entry marker logic; the nearby JSX and
conditional rendering already make the intent clear. Delete the comment block
near the enterprise-plan indicator and the similar comment in the
collapsed-sidebar section, keeping the actual rendering logic in the dashboard
sidebar component unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: caeef04f-783b-4745-931e-eb07e0f954ec
📒 Files selected for processing (1)
apps/ui/src/components/dashboard/dashboard-sidebar.tsx
- Sidebar enterprise indicator now uses the Building2 icon (matching the navbar Resources → Enterprise item) instead of Sparkles. - New knowledge-base page learn/org-analytics.mdx documenting Organization Analytics + breakdown by model/project/API key, with light/dark screenshots, added to the learn nav. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 68.5%, saving 196.7 KB.
|
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 8.6%, saving 4.1 KB.
1 image did not require optimisation. |
|
Caution Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted. Error details |
- analytics: cap org-activity window at 366 days and reject larger spans (400) instead of eachDay() silently truncating, so SQL totals and returned daily buckets always cover the same range. - ProfileReadmeBadge: take the public origin via a baseUrl prop instead of a hard-coded host, so preview/staging/self-hosted builds emit correct links. - ProfileView: unresolved models fall back to the serving provider's logo instead of passing a raw model string to getProviderIcon. - analytics charts: aggregateByDimension / buildDimensionTimeseries now rank by the active metric, so Requests/Tokens views show the right top-N (not top spenders). - org & member analytics: distinguish "membership loading" from "not admin" so the denial card no longer flashes before team resolves. - enterprise contact form: submit via the generated typed Hono client instead of raw fetch. - sidebar: drop redundant explanatory comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/code/src/components/profile/ProfileReadmeBadge.tsx`:
- Around line 21-22: The README badge generation in ProfileReadmeBadge should
not depend on a client-only baseUrl from window.location.origin, since that is
empty during SSR and causes relative markdown plus hydration flicker. Update the
caller and ProfileReadmeBadge to require a stable absolute public origin before
rendering, and build profileUrl and badgeMarkdown from that server-resolved
baseUrl instead of deriving it at render time.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 7eb20868-3b3b-4a8e-8bd6-61ddb9eda466
⛔ Files ignored due to path filters (2)
apps/docs/public/learn/org-analytics-dark.pngis excluded by!**/*.pngapps/docs/public/learn/org-analytics-light.pngis excluded by!**/*.png
📒 Files selected for processing (13)
apps/api/src/routes/analytics.tsapps/code/src/app/profile/ProfilePageClient.tsxapps/code/src/components/profile/ProfileReadmeBadge.tsxapps/code/src/components/profile/ProfileView.tsxapps/docs/content/learn/meta.jsonapps/docs/content/learn/org-analytics.mdxapps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsxapps/ui/src/app/dashboard/[orgId]/org/members/members-client.tsxapps/ui/src/components/analytics/chart-helpers.tsapps/ui/src/components/analytics/dimension-usage-card.tsxapps/ui/src/components/analytics/dimension-usage-over-time-card.tsxapps/ui/src/components/dashboard/dashboard-sidebar.tsxapps/ui/src/components/enterprise/contact.tsx
✅ Files skipped from review due to trivial changes (1)
- apps/docs/content/learn/meta.json
🚧 Files skipped from review as they are similar to previous changes (7)
- apps/code/src/app/profile/ProfilePageClient.tsx
- apps/ui/src/components/analytics/dimension-usage-card.tsx
- apps/ui/src/components/analytics/dimension-usage-over-time-card.tsx
- apps/code/src/components/profile/ProfileView.tsx
- apps/api/src/routes/analytics.ts
- apps/ui/src/app/dashboard/[orgId]/org/analytics/org-analytics-client.tsx
- apps/ui/src/components/dashboard/dashboard-sidebar.tsx
| const profileUrl = `${baseUrl}/profiles/${username}`; | ||
| const badgeMarkdown = `[](${profileUrl})`; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Require a stable absolute baseUrl before rendering this snippet.
With the current caller, baseUrl comes from window.location.origin, which is "" during SSR. That makes the initial badge Markdown and preview image relative (/devpass-badge.svg, /profiles/...) and then flips them to absolute after hydration. For a README snippet, that first render is wrong and can also cause a hydration mismatch/flicker. Pass a server-resolved public origin into this component instead of deriving it from window at render time.
Also applies to: 41-41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/code/src/components/profile/ProfileReadmeBadge.tsx` around lines 21 -
22, The README badge generation in ProfileReadmeBadge should not depend on a
client-only baseUrl from window.location.origin, since that is empty during SSR
and causes relative markdown plus hydration flicker. Update the caller and
ProfileReadmeBadge to require a stable absolute public origin before rendering,
and build profileUrl and badgeMarkdown from that server-resolved baseUrl instead
of deriving it at render time.
- Order: Provider Keys, Your Discounts, then the enterprise cluster (Custom Models, Analytics, Guardrails, Compliance, Security Events, Master Keys), then Settings. - Flag Custom Models, Master Keys, and the Members sub-item as enterprise-only. - Replace the native title tooltip on the indicator with the Radix Tooltip (delayDuration=0 from the sidebar provider) so the hint is instant instead of the slow browser default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Preferences, Policies, and Billing wrapped their content in an extra max-w-3xl mx-auto container, so they rendered narrower than the rest of the dashboard. Drop the width clamp (keep the vertical spacing) and align the billing payment-status banner so they fill the content area like every other page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the leftover max-w-3xl mx-auto clamp on the Team page so it fills the content area like the other org settings pages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
enterprise_contact_submission.deployment is limited to self_host/cloud/not_sure (NULL allowed) via a Drizzle check(), matching the route/Zod schema and the text() enum. Column add + constraint ship in a single migration (one migration per PR). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d85c71b to
2779b2e
Compare
- changelog: Organization-Wide Analytics (org-level rollup, breakdown by model/project/API key), with OG image. - blog: enterprise LLM analytics announcement (cost by model, per-key, org-wide, and member analytics), enterprise-focused, with OG image. - fix: dedupe changelog id 56 (claude-fable-5-access-suspended -> 62), which caused a duplicate React key on the changelog page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 77.1%, saving 2.0 MB.
2 images did not require optimisation. |
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 10.2%, saving 61.2 KB.
2 images did not require optimisation. |
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 15%, saving 80.4 KB.
2 images did not require optimisation. |
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 8.6%, saving 39.4 KB.
2 images did not require optimisation. |
|
Images automagically compressed by Calibre's image-actions ✨ Compression reduced images by 7.4%, saving 14.6 KB.
3 images did not require optimisation. |
Summary
A batch of dashboard + DevPass improvements.
Org-level analytics (apps/ui) — Enterprise
dashboard/[orgId]/org/analytics(sidebar → Organization → Analytics). Summary stat cards (total spend / requests / tokens) plus cost & usage charts aggregated across every project in the org.projectHourlyStats,projectHourlyModelStats,apiKeyHourlyStats) — never raw logs.GET /analytics/activityendpoint, gated byrequireEnterpriseAdmin, supportinggroupBy=model | project | apiKey. Models collapse to their canonical id server-side so a model routed through several providers is one series.Enterprise-only sidebar indicators
Enterprise contact form
/public/contact/enterpriseAPI, theenterprise_contact_submissiontable (new nullabledeploymentcolumn + migration), the notification email, and the Discord webhook.DevPass profiles (apps/code)
Database
deployment(nullable text) toenterprise_contact_submission. Migration:1782486651_wealthy_tempest.sql.Testing
pnpm build(full monorepo) ✅pnpm format✅🤖 Generated with Claude Code
Summary by CodeRabbit