feat: implement workspace analytics and usage tracking#249
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| 🔵 In progress View logs |
marble-api | caebea0 | Nov 18 2025, 10:30 PM |
WalkthroughRefactors analytics from Redis to DB-backed UsageEvent logging with optional Polar ingestion, centralizes webhook delivery via a dispatcher, introduces a unified /api/metrics/usage endpoint and dashboard cards, adds usage schema/migrations, and applies UI/layout and spinner refactors across the CMS. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant Middleware as Analytics Middleware
participant ExecCtx as Execution Context
participant DB as Database
participant Polar as Polar Service
Client->>Middleware: HTTP request (with workspaceId)
Middleware->>Middleware: extract endpoint, workspaceId, method, status
Middleware->>Middleware: await next()
Middleware-->>Client: respond
Middleware->>ExecCtx: waitUntil(async task)
activate ExecCtx
ExecCtx->>DB: insert usage_event(type: api_request, workspaceId, endpoint)
ExecCtx->>DB: load organization, owner
alt POLAR configured
ExecCtx->>Polar: ingest api_request (externalCustomerId, endpoint, method, status)
Polar-->>ExecCtx: ack
end
ExecCtx->>ExecCtx: try/catch errors -> console.error
deactivate ExecCtx
sequenceDiagram
participant API
participant Dispatcher as dispatchWebhooks
participant DB as Database
participant ClientWebhook as WebhookClient
participant Usage as trackWebhookUsage
participant Polar as Polar Service
API->>Dispatcher: dispatchWebhooks(workspaceId, event, payload)
Dispatcher->>DB: getWebhooks(workspaceId, event)
DB-->>Dispatcher: [webhook1, webhook2, ...]
loop per webhook
Dispatcher->>ClientWebhook: new WebhookClient(config)
ClientWebhook->>ClientWebhook: POST payload to endpoint
ClientWebhook-->>Dispatcher: success/failure
Dispatcher->>Usage: trackWebhookUsage(workspaceId, endpoint)
Usage->>DB: insert usage_event(type: webhook_delivery, workspaceId, endpoint)
alt POLAR configured
Usage->>Polar: ingest webhook_delivery (externalCustomerId, endpoint)
end
end
Dispatcher-->>API: return (fire-and-forget / Promise.allSettled)
sequenceDiagram
participant UI as Dashboard Page
participant Query as React Query
participant API as /api/metrics/usage
participant DB as Database
UI->>Query: useQuery(USAGE_DASHBOARD(workspaceId))
Query->>API: GET /api/metrics/usage
API->>DB: aggregate API, webhook, media metrics (30-day windows)
DB-->>API: aggregated UsageDashboardData
API-->>Query: UsageDashboardData
Query->>UI: render ApiUsageCard, WebhookUsageCard, MediaUsageCard
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas requiring extra attention:
Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 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 |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx (1)
178-178: Hardcoded API usage contradicts new usage tracking.This PR introduces
apps/cms/src/app/api/metrics/usage/route.tswith real API request metrics, but the billing page still displays a hardcoded0. Consider integrating the new usage endpoint to show actual API request counts.Would you like me to generate the integration code to fetch and display real usage data?
apps/cms/src/components/ui/async-button.tsx (1)
7-31: Ref handling on AsyncButton is broken; useforwardRefinsteadBecause
AsyncButtonis a plain function component, React will not passrefthroughprops, sorefhere will always beundefinedand consumers can’t actually get a ref to the underlying button. You should convert this to aforwardRefand dropreffrom the props surface.-import type { ComponentProps, ReactNode, RefObject } from "react"; +import type { ComponentProps, ReactNode } from "react"; +import { forwardRef } from "react"; @@ -interface AsyncButtonProps extends ComponentProps<typeof Button> { +interface AsyncButtonProps extends ComponentProps<typeof Button> { @@ - /** - * Optional ref for the underlying button element - */ - ref?: RefObject<HTMLButtonElement | null>; } @@ -const AsyncButton = ({ - children, - isLoading = false, - loadingText, - keepTextWhileLoading = false, - disabled, - variant = "default", - className, - ref, - ...props -}: AsyncButtonProps) => { - const renderLoadingContent = () => { +const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>( + ( + { + children, + isLoading = false, + loadingText, + keepTextWhileLoading = false, + disabled, + variant = "default", + className, + ...props + }, + ref + ) => { + const renderLoadingContent = () => { @@ - // Default: spinner only - return <ButtonLoadingSpinner variant={variant} />; - }; - - return ( - <Button - className={className} - disabled={disabled || isLoading} - ref={ref} - variant={variant} - {...props} - > - {isLoading ? renderLoadingContent() : children} - </Button> - ); -}; + // Default: spinner only + return <ButtonLoadingSpinner variant={variant} />; + }; + + return ( + <Button + className={className} + disabled={disabled || isLoading} + ref={ref} + variant={variant} + {...props} + > + {isLoading ? renderLoadingContent() : children} + </Button> + ); + } +);Also applies to: 33-78
🧹 Nitpick comments (16)
apps/api/.dev.vars.example (1)
4-5: Consider adding comments for new environment variables.While the additions are correct, adding brief comments would help developers understand the purpose and expected values (e.g., "production" or "sandbox" for ENVIRONMENT, and that POLAR_ACCESS_TOKEN is optional).
Apply this diff to add helpful comments:
REDIS_URL= REDIS_TOKEN= +# Optional: Access token for Polar integration (analytics/usage tracking) POLAR_ACCESS_TOKEN= +# Optional: Environment for Polar (e.g., "production" or leave empty for sandbox) ENVIRONMENT=apps/cms/src/components/media/media-card.tsx (1)
86-94: Conflicting Image sizing approach.The Image component has
absolute inset-0 size-fullclasses (line 89) combined with explicitheight={160} width={250}props. This creates a conflict:
- The classes suggest the image should fill its container
- The explicit dimensions override the responsive behavior
- The parent has
aspect-videowhich won't work as intended with fixed dimensionsApply this diff to use the
fillprop instead for proper responsive behavior:<Image alt={media.name} className="absolute inset-0 size-full object-cover" - height={160} + fill src={media.url} unoptimized - width={250} />apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx (1)
47-47: Remove debug statement.This
console.logappears to be a debugging leftover.- console.log("invitations", activeWorkspace.invitations);apps/cms/src/components/nav/nav-main.tsx (1)
24-24: Remove unused LayoutIcon import.LayoutIcon is imported but no longer used after switching to HouseIcon for the Home navigation entry.
Apply this diff to remove the unused import:
import { FadersIcon, HouseIcon, ImagesIcon, - LayoutIcon, NoteIcon, PackageIcon, TagIcon, UsersThreeIcon, } from "@phosphor-icons/react";apps/cms/src/components/home/api-usage-card.tsx (1)
30-36: Minor cleanup: redundant mapping and identity formatter
chartDatais just copyinglabel,value, anddatefromrawChartData, andformatXAxisLabelis an identity function. You could drop themapand inlinetickFormatter={value => value}(or omit it if default is fine) to simplify a bit, unless you expect to transform these later.Also applies to: 44-44
packages/db/prisma/migrations/20251114225009_add_usage_event_table/migration.sql (1)
16-20: Consider dropping the second overlapping index
usage_event_workspaceId_type_createdAt_idxcan already support queries filtered by justworkspaceId(and ordered bycreatedAt), sousage_event_workspaceId_createdAt_idxmay be redundant and add write/storage overhead unless you have a specific query pattern that benefits from it. Worth double‑checking your query plans before keeping both.apps/cms/src/lib/webhooks/utils.ts (1)
1-1: CentralizedgetWebhookshelper looks solid; types could be tightened later.Query filters (
enabled,workspaceId,events: { has: event }) and the workspaceId guard align well with dispatcher usage. If you want a bit more safety in the future, consider replacingDatabaseFields = Record<string, any>with a narrower type (or a generic) sowhere/selectare checked at compile time, but this is non-blocking.Also applies to: 3-3, 75-96
apps/cms/src/lib/webhooks/usage.ts (1)
1-71: trackWebhookUsage flow looks correct; you can skip the org lookup when Polar is disabled.DB write + Polar ingestion and error handling all look good. One small optimization:
db.organization.findFirstruns even whencreatePolarClient()returnsnull, so you could move the customerId resolution block insideif (polarClient)to avoid an extra query when Polar isn’t configured.- let customerId = workspaceId; - - try { - const organization = await db.organization.findFirst({ - where: { id: workspaceId }, - select: { members: { where: { role: "owner" }, select: { userId: true } } }, - }); - if (organization?.members[0]?.userId) { - customerId = organization.members[0].userId; - } - } catch (error) { - console.error("[WebhookUsage] Failed to get customer ID:", error); - } - - if (polarClient) { + if (polarClient) { + let customerId = workspaceId; + try { + const organization = await db.organization.findFirst({ + where: { id: workspaceId }, + select: { + members: { where: { role: "owner" }, select: { userId: true } }, + }, + }); + if (organization?.members[0]?.userId) { + customerId = organization.members[0].userId; + } + } catch (error) { + console.error("[WebhookUsage] Failed to get customer ID:", error); + } try { await polarClient.events.ingest({ </blockquote></details> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary><blockquote> `1-66`: **Spinner components look good; consider extracting the SVG to avoid duplication.** Both `LoadingSpinner` and `ButtonLoadingSpinner` are correct and nicely typed. If this grows, you could pull the SVG into a shared `SpinnerIcon` and have each wrapper just supply the appropriate `className` to reduce duplication, but it’s fine as-is. </blockquote></details> <details> <summary>apps/cms/src/components/home/webhook-usage-card.tsx (1)</summary><blockquote> `1-139`: **WebhookUsageCard implementation is sound; only micro-nits.** Loading/empty/data states, workspace totals, and date range logic all look good. The `chartData` transform is effectively an identity and `?? []` after `.map` is redundant since `rawChartData` already defaults to `[]`, so you could simplify that later if you touch this file, but there’s no functional issue. </blockquote></details> <details> <summary>apps/api/src/middleware/analytics.ts (2)</summary><blockquote> `12-31`: **Double‑check analytics gating, endpoint derivation, and runtime assumptions** A few things worth confirming here: - Line 20: `status >= 400` means failed requests are not logged at all; if you want to see error traffic in usage analytics, you may want to record these too. - Lines 24‑26: `pathParts.slice(2)` will keep workspaceId in the `endpoint` for paths like `/api/workspaces/:workspaceId/...`; confirm that including the workspaceId in the stored endpoint is what you want. - Line 98: `c.executionCtx?.waitUntil(task())` will no‑op on runtimes that don’t provide `executionCtx` (e.g. non‑Workers), so analytics won’t run there; consider a fallback `await task()` in those environments if you need coverage. - Lines 30‑31: If `createClient(DATABASE_URL)` is an expensive construction, you may want to reuse a cached client instead of creating it on every request inside the task. Also applies to: 20-27, 98-99 --- `62-92`: **Tighten Polar error handling and logging noise** - Lines 80‑86: The `errorDetails` object is built but never used, so the Polar error is effectively swallowed without any signal; either log/selectively log it or remove the dead code. - Lines 88‑91: Logging “Skipping Polar: POLAR_ACCESS_TOKEN not configured” on every request can get noisy in environments without Polar; consider guarding this behind a debug flag or logging it once at startup instead. </blockquote></details> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (2)</summary><blockquote> `6-24`: **Keep event typings in sync with validation and client event types** You’re effectively duplicating the event → payload map here; to avoid drift with `WebhookValidationEvent` / the `WebhookEvent` used in `WebhookClient`, consider deriving `WebhookEventMap` from a single shared type (or zod schema) instead of hand-maintaining this object shape. --- `33-90`: **Clarify async semantics of `dispatchWebhooks` and background work** `dispatchWebhooks` is `async` and awaits `getWebhooks`, but the actual deliveries and `trackWebhookUsage` calls run in a fire‑and‑forget `Promise.allSettled` that is not awaited, so `await dispatchWebhooks(...)` does *not* wait for sends or usage tracking to finish. If this is intentional, consider either returning `Promise.allSettled(deliveries)` or making the function sync and documenting it as best‑effort background dispatch to avoid surprising callers; also worth noting that `trackWebhookUsage` runs once per payload per webhook, which could be heavy at high volume. </blockquote></details> <details> <summary>apps/cms/src/styles/globals.css (1)</summary><blockquote> `65-90`: **Avoid drift between tokens, Tailwind defaults, and media queries** Defining `--breakpoint-*` here duplicates Tailwind v4’s defaults and hard‑codes the same rem values again in the `@media (width >= …)` rules; if these ever change, your tokens and media queries can get out of sync. Consider either relying on Tailwind’s built‑in `--breakpoint-*` vars, or basing the media queries and workspace container max‑widths directly on your custom vars (e.g. `var(--breakpoint-sm)`, `var(--workspace-container-sm)`) so there’s a single source of truth. Also applies to: 295-342 </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx (1)</summary><blockquote> `14-29`: **Confirm workspace scoping and disabled query behavior** Using `enabled: Boolean(workspaceId)` with a fallback `["usage-dashboard", "disabled"]` key is solid, but note that when `workspaceId` is ever `null`, `isPending` will stay true and you render a loader indefinitely. If a missing workspace is possible, you might want an explicit guard (e.g. render nothing or an error) instead. Also, ensure `/api/metrics/usage` is truly workspace‑scoped server‑side (via auth/context) since you’re not passing `workspaceId` in the request. </blockquote></details> </blockquote></details> <details> <summary>📜 Review details</summary> **Configuration used**: CodeRabbit UI **Review profile**: CHILL **Plan**: Pro <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 4e96e56cf8f896b7deb318a4b8cc14442d111e5d and 96a2d984fbf7620e5484b6a996315d943f6256ae. </details> <details> <summary>⛔ Files ignored due to path filters (1)</summary> * `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml` </details> <details> <summary>📒 Files selected for processing (71)</summary> * `apps/api/.dev.vars.example` (1 hunks) * `apps/api/package.json` (1 hunks) * `apps/api/src/lib/polar.ts` (1 hunks) * `apps/api/src/middleware/analytics.ts` (1 hunks) * `apps/api/src/middleware/ratelimit.ts` (1 hunks) * `apps/api/src/types/env.ts` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/keys/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/layout.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx` (3 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/editor-preferences/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/schemas/page.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx` (1 hunks) * `apps/cms/src/app/(main)/[workspace]/(dashboard)/webhooks/page-client.tsx` (2 hunks) * `apps/cms/src/app/api/categories/[id]/route.ts` (4 hunks) * `apps/cms/src/app/api/categories/route.ts` (2 hunks) * `apps/cms/src/app/api/media/route.ts` (6 hunks) * `apps/cms/src/app/api/metrics/api/route.tsx` (0 hunks) * `apps/cms/src/app/api/metrics/publishing/route.tsx` (0 hunks) * `apps/cms/src/app/api/metrics/usage/route.ts` (1 hunks) * `apps/cms/src/app/api/metrics/workspace/route.tsx` (0 hunks) * `apps/cms/src/app/api/posts/[id]/route.ts` (4 hunks) * `apps/cms/src/app/api/posts/import/route.ts` (2 hunks) * `apps/cms/src/app/api/posts/route.ts` (2 hunks) * `apps/cms/src/app/api/tags/[id]/route.ts` (4 hunks) * `apps/cms/src/app/api/tags/route.ts` (2 hunks) * `apps/cms/src/app/api/upload/complete/route.ts` (2 hunks) * `apps/cms/src/components/auth/login-form.tsx` (2 hunks) * `apps/cms/src/components/auth/register-form.tsx` (2 hunks) * `apps/cms/src/components/billing/upgrade-modal.tsx` (0 hunks) * `apps/cms/src/components/editor/editor-page.tsx` (1 hunks) * `apps/cms/src/components/editor/image-upload-modal.tsx` (1 hunks) * `apps/cms/src/components/editor/tabs/analysis-tab.tsx` (1 hunks) * `apps/cms/src/components/editor/tabs/metadata-tab.tsx` (1 hunks) * `apps/cms/src/components/home/AnalyticsChart.tsx` (0 hunks) * `apps/cms/src/components/home/ApiUsageStats.tsx` (0 hunks) * `apps/cms/src/components/home/PublishingActivityGraph.tsx` (0 hunks) * `apps/cms/src/components/home/QuickStats.tsx` (0 hunks) * `apps/cms/src/components/home/api-usage-card.tsx` (1 hunks) * `apps/cms/src/components/home/media-usage-card.tsx` (1 hunks) * `apps/cms/src/components/home/webhook-usage-card.tsx` (1 hunks) * `apps/cms/src/components/layout/wrapper.tsx` (1 hunks) * `apps/cms/src/components/media/media-card.tsx` (1 hunks) * `apps/cms/src/components/media/media-controls.tsx` (1 hunks) * `apps/cms/src/components/nav/app-sidebar.tsx` (1 hunks) * `apps/cms/src/components/nav/nav-main.tsx` (3 hunks) * `apps/cms/src/components/nav/page-header.tsx` (1 hunks) * `apps/cms/src/components/nav/workspace-switcher.tsx` (1 hunks) * `apps/cms/src/components/posts/data-grid.tsx` (2 hunks) * `apps/cms/src/components/ui/async-button.tsx` (4 hunks) * `apps/cms/src/components/ui/loading-spinner.tsx` (1 hunks) * `apps/cms/src/components/webhooks/create-webhook.tsx` (1 hunks) * `apps/cms/src/hooks/use-analytics.ts` (0 hunks) * `apps/cms/src/lib/polar/client.ts` (1 hunks) * `apps/cms/src/lib/queries/keys.ts` (1 hunks) * `apps/cms/src/lib/validations/webhook.ts` (2 hunks) * `apps/cms/src/lib/webhooks/dispatcher.ts` (1 hunks) * `apps/cms/src/lib/webhooks/usage.ts` (1 hunks) * `apps/cms/src/lib/webhooks/utils.ts` (2 hunks) * `apps/cms/src/lib/webhooks/webhook-client.ts` (3 hunks) * `apps/cms/src/styles/globals.css` (2 hunks) * `apps/cms/src/types/usage-dashboard.ts` (1 hunks) * `packages/db/prisma/migrations/20251114225009_add_usage_event_table/migration.sql` (1 hunks) * `packages/db/prisma/migrations/20251116173412_new_media_enum_and_alt_text_column/migration.sql` (1 hunks) * `packages/db/prisma/schema.prisma` (5 hunks) </details> <details> <summary>💤 Files with no reviewable changes (9)</summary> * apps/cms/src/components/billing/upgrade-modal.tsx * apps/cms/src/app/api/metrics/workspace/route.tsx * apps/cms/src/components/home/QuickStats.tsx * apps/cms/src/app/api/metrics/publishing/route.tsx * apps/cms/src/components/home/PublishingActivityGraph.tsx * apps/cms/src/components/home/ApiUsageStats.tsx * apps/cms/src/components/home/AnalyticsChart.tsx * apps/cms/src/app/api/metrics/api/route.tsx * apps/cms/src/hooks/use-analytics.ts </details> <details> <summary>🧰 Additional context used</summary> <details> <summary>🧠 Learnings (6)</summary> <details> <summary>📓 Common learnings</summary>Learnt from: taqh
Repo: usemarble/marble PR: 137
File: packages/db/prisma/migrations/20250826194942_make_category_required/migration.sql:11-11
Timestamp: 2025-08-26T20:20:46.367Z
Learning: User "taqh" prefers concise code review comments rather than verbose explanations. Keep reviews short and to the point.</details> <details> <summary>📚 Learning: 2025-10-26T20:32:09.573Z</summary>Learnt from: dpaulos6
Repo: usemarble/marble PR: 233
File: apps/cms/src/app/(main)/[workspace]/set-workspace-cookie.tsx:1-16
Timestamp: 2025-10-26T20:32:09.573Z
Learning: In Next.js, Server Actions defined in modules with a module-level "use server" directive can be called from client components, including within useEffect hooks. This is a valid and documented pattern. The server function will be executed on the server even when invoked from client-side code.**Applied to files:** - `apps/cms/src/app/api/categories/route.ts` - `apps/cms/src/app/api/tags/route.ts` </details> <details> <summary>📚 Learning: 2025-09-28T22:18:20.726Z</summary>Learnt from: mezotv
Repo: usemarble/marble PR: 198
File: apps/cms/src/app/(main)/[workspace]/(workspace)/keys/page-client.tsx:23-32
Timestamp: 2025-09-28T22:18:20.726Z
Learning: For Radix UI tooltips with disabled buttons: using asChild with disabled buttons breaks tooltip functionality because disabled elements don't receive the pointer events needed to trigger tooltips. Solutions include: 1) wrapper span with asChild and pointer-events: none on the disabled button, or 2) using aria-disabled instead of disabled to preserve interaction while preventing clicks.**Applied to files:** - `apps/cms/src/components/nav/workspace-switcher.tsx` </details> <details> <summary>📚 Learning: 2025-08-20T06:18:53.734Z</summary>Learnt from: brandonmcconnell
Repo: usemarble/marble PR: 113
File: apps/cms/src/components/shared/container.tsx:0-0
Timestamp: 2025-08-20T06:18:53.734Z
Learning: Tailwind CSS v4 provides default CSS variables for breakpoints including --breakpoint-2xl (1536px), --breakpoint-xl, --breakpoint-lg, --breakpoint-md, and --breakpoint-sm. These are available without manual definition and can be used with arbitrary value syntax like max-w-(--breakpoint-2xl).**Applied to files:** - `apps/cms/src/styles/globals.css` </details> <details> <summary>📚 Learning: 2025-08-20T06:18:53.734Z</summary>Learnt from: brandonmcconnell
Repo: usemarble/marble PR: 113
File: apps/cms/src/components/shared/container.tsx:0-0
Timestamp: 2025-08-20T06:18:53.734Z
Learning: Tailwind CSS v4 provides default CSS variables for breakpoints including --breakpoint-2xl (96rem/1536px), --breakpoint-xl, --breakpoint-lg, --breakpoint-md, and --breakpoint-sm. These are available without manual definition and can be used with arbitrary value syntax like max-w-(--breakpoint-2xl).**Applied to files:** - `apps/cms/src/styles/globals.css` </details> <details> <summary>📚 Learning: 2025-08-20T06:22:02.309Z</summary>Learnt from: brandonmcconnell
Repo: usemarble/marble PR: 113
File: apps/web/src/pages/privacy/index.astro:0-0
Timestamp: 2025-08-20T06:22:02.309Z
Learning: --breakpoint-sm is a default CSS variable provided by Tailwind CSS v4, available without manual definition and can be used with arbitrary value syntax like max-w-(--breakpoint-sm).**Applied to files:** - `apps/cms/src/styles/globals.css` </details> </details><details> <summary>🧬 Code graph analysis (36)</summary> <details> <summary>apps/cms/src/app/api/categories/route.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> </blockquote></details> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary><blockquote> <details> <summary>packages/ui/src/components/button.tsx (1)</summary> * `buttonVariants` (58-58) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/components/posts/data-grid.tsx (1)</summary><blockquote> <details> <summary>packages/ui/src/components/card.tsx (1)</summary> * `CardContent` (91-91) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/posts/import/route.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> </blockquote></details> <details> <summary>apps/cms/src/components/auth/register-form.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary> * `ButtonLoadingSpinner` (31-66) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/categories/[id]/route.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/posts/route.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> </blockquote></details> <details> <summary>apps/cms/src/lib/webhooks/usage.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/polar/client.ts (1)</summary> * `createPolarClient` (5-18) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/posts/[id]/route.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/upload/complete/route.ts (2)</summary><blockquote> <details> <summary>apps/cms/src/lib/polar/client.ts (1)</summary> * `createPolarClient` (5-18) </details> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> </blockquote></details> <details> <summary>apps/cms/src/components/webhooks/create-webhook.tsx (1)</summary><blockquote> <details> <summary>packages/ui/src/components/label.tsx (1)</summary> * `Label` (24-24) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/components/auth/login-form.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary> * `ButtonLoadingSpinner` (31-66) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/tags/route.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/utils.ts (1)</summary> * `getWebhooks` (78-96) </details> </blockquote></details> <details> <summary>apps/cms/src/components/home/api-usage-card.tsx (3)</summary><blockquote> <details> <summary>apps/cms/src/types/usage-dashboard.ts (1)</summary> * `UsageDashboardData` (1-40) </details> <details> <summary>packages/ui/src/components/chart.tsx (4)</summary> * `ChartConfig` (11-19) * `ChartContainer` (347-347) * `ChartTooltip` (348-348) * `ChartTooltipContent` (349-349) </details> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary> * `LoadingSpinner` (8-29) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/metrics/usage/route.ts (2)</summary><blockquote> <details> <summary>apps/cms/src/app/api/media/route.ts (1)</summary> * `GET` (12-111) </details> <details> <summary>apps/cms/src/lib/auth/session.ts (1)</summary> * `getServerSession` (4-14) </details> </blockquote></details> <details> <summary>apps/cms/src/components/nav/nav-main.tsx (1)</summary><blockquote> <details> <summary>packages/ui/src/components/sidebar.tsx (1)</summary> * `SidebarGroupLabel` (715-715) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/editor-preferences/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/tags/[id]/route.ts (3)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> <details> <summary>apps/cms/src/app/api/categories/[id]/route.ts (1)</summary> * `DELETE` (72-137) </details> <details> <summary>apps/cms/src/lib/auth/session.ts (1)</summary> * `getServerSession` (4-14) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/schemas/page.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/api/src/middleware/analytics.ts (3)</summary><blockquote> <details> <summary>packages/db/src/index.ts (2)</summary> * `db` (29-29) * `createClient` (29-29) </details> <details> <summary>apps/cms/src/lib/polar/client.ts (1)</summary> * `createPolarClient` (5-18) </details> <details> <summary>apps/api/src/lib/polar.ts (1)</summary> * `createPolarClient` (3-8) </details> </blockquote></details> <details> <summary>apps/cms/src/components/home/webhook-usage-card.tsx (3)</summary><blockquote> <details> <summary>apps/cms/src/types/usage-dashboard.ts (1)</summary> * `UsageDashboardData` (1-40) </details> <details> <summary>packages/ui/src/components/chart.tsx (1)</summary> * `ChartConfig` (11-19) </details> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary> * `LoadingSpinner` (8-29) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/keys/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/webhooks/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/api/media/route.ts (1)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (1)</summary> * `dispatchWebhooks` (33-90) </details> </blockquote></details> <details> <summary>apps/cms/src/lib/webhooks/dispatcher.ts (3)</summary><blockquote> <details> <summary>apps/cms/src/lib/webhooks/utils.ts (1)</summary> * `getWebhooks` (78-96) </details> <details> <summary>apps/cms/src/lib/webhooks/webhook-client.ts (1)</summary> * `WebhookClient` (79-188) </details> <details> <summary>apps/cms/src/lib/webhooks/usage.ts (1)</summary> * `trackWebhookUsage` (9-71) </details> </blockquote></details> <details> <summary>apps/cms/src/components/home/media-usage-card.tsx (4)</summary><blockquote> <details> <summary>apps/cms/src/types/usage-dashboard.ts (1)</summary> * `UsageDashboardData` (1-40) </details> <details> <summary>apps/cms/src/utils/string.ts (1)</summary> * `formatBytes` (13-34) </details> <details> <summary>apps/cms/src/components/ui/hidden-scrollbar.tsx (1)</summary> * `HiddenScrollbar` (8-31) </details> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary> * `LoadingSpinner` (8-29) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx (7)</summary><blockquote> <details> <summary>apps/cms/src/hooks/use-workspace-id.ts (1)</summary> * `useWorkspaceId` (7-10) </details> <details> <summary>apps/cms/src/lib/queries/keys.ts (1)</summary> * `QUERY_KEYS` (1-40) </details> <details> <summary>apps/cms/src/types/usage-dashboard.ts (1)</summary> * `UsageDashboardData` (1-40) </details> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> <details> <summary>apps/cms/src/components/home/api-usage-card.tsx (1)</summary> * `ApiUsageCard` (27-140) </details> <details> <summary>apps/cms/src/components/home/webhook-usage-card.tsx (1)</summary> * `WebhookUsageCard` (26-139) </details> <details> <summary>apps/cms/src/components/home/media-usage-card.tsx (1)</summary> * `MediaUsageCard` (47-128) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary> * `WorkspacePageWrapper` (6-28) </details> </blockquote></details> <details> <summary>apps/cms/src/components/ui/async-button.tsx (1)</summary><blockquote> <details> <summary>apps/cms/src/components/ui/loading-spinner.tsx (1)</summary> * `ButtonLoadingSpinner` (31-66) </details> </blockquote></details> </details> </details> <details> <summary>🔇 Additional comments (56)</summary><blockquote> <details> <summary>apps/cms/src/components/nav/workspace-switcher.tsx (1)</summary><blockquote> `73-73`: **LGTM! Clean styling refactor.** Switching to semantic design tokens (`sidebar-accent`) improves consistency. </blockquote></details> <details> <summary>apps/cms/src/components/posts/data-grid.tsx (2)</summary><blockquote> `53-53`: **LGTM: Visual refinement to shadow styling.** The change from `shadow-sm` to `shadow-xs` is a subtle visual improvement for the card link containers. --- `66-66`: **LGTM: Fixes malformed flex classes.** Corrects invalid CSS class syntax. The current `mt-auto flex` properly positions the card footer content. </blockquote></details> <details> <summary>apps/cms/src/components/editor/image-upload-modal.tsx (1)</summary><blockquote> `210-210`: **LGTM!** The `fill` prop is appropriate here with the parent container having `position: relative` (line 199). This allows the image to adapt to its container size in the gallery grid. </blockquote></details> <details> <summary>apps/api/src/lib/polar.ts (1)</summary><blockquote> `1-8`: **LGTM!** Clean factory function for creating Polar clients with appropriate server selection based on environment. </blockquote></details> <details> <summary>apps/cms/src/components/webhooks/create-webhook.tsx (2)</summary><blockquote> `256-268`: **Layout refinements look good.** The spacing adjustments and border removal create a cleaner visual hierarchy for the Events section. --- `260-260`: **Documentation URL verified as accessible.** The webhook documentation URL at `https://docs.marblecms.com/guides/features/webhooks` returns an HTTP 200 status code, confirming it is valid and publicly accessible. </blockquote></details> <details> <summary>apps/cms/src/lib/queries/keys.ts (1)</summary><blockquote> `30-30`: **Migration from removed query keys is complete and verified.** Comprehensive verification confirms: - No remaining references to `PUBLISHING_METRICS`, `API_ANALYTICS`, or `WORKSPACE_METRICS` exist in any file across the codebase - New `USAGE_DASHBOARD` key is properly defined and already in active use - All usages have been successfully migrated to the unified dashboard approach </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx (2)</summary><blockquote> `156-165`: **LGTM! Clean refactor.** Consolidating the upload message logic into `getUploadMessage` eliminates duplication and improves maintainability. --- `211-214`: **LGTM!** Consistent with the project-wide compact layout adoption. </blockquote></details> <details> <summary>apps/api/src/middleware/ratelimit.ts (1)</summary><blockquote> `14-39`: **LGTM! Cleaner structure and sensible rate limiting.** The conditional rate limiting (200/10s for workspace requests, 10/10s for anonymous) appropriately differentiates authenticated from unauthenticated traffic. </blockquote></details> <details> <summary>apps/cms/src/app/api/tags/[id]/route.ts (2)</summary><blockquote> `53-67`: **LGTM!** Webhook dispatch integration follows the fire-and-forget pattern correctly with appropriate error logging. --- `77-112`: **LGTM!** Added workspaceId validation prevents unauthorized access, and webhook dispatch mirrors the PATCH handler pattern. </blockquote></details> <details> <summary>apps/cms/src/app/api/metrics/usage/route.ts (4)</summary><blockquote> `9-14`: **LGTM!** Standard auth check pattern. --- `22-82`: **LGTM! Solid metrics aggregation.** The parallel query pattern, chart bucketing logic, and change percentage calculation (with proper zero-handling) are well-implemented. --- `84-190`: **LGTM! Comprehensive webhook and media metrics.** The parallel fetching strategy and chart construction for webhooks mirror the API metrics pattern effectively. --- `192-225`: **LGTM!** Response structure is well-organized and aligns with the usage dashboard data type. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/members/page-client.tsx (1)</summary><blockquote> `50-50`: **LGTM!** Consistent with project-wide compact layout changes. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/billing/page-client.tsx (1)</summary><blockquote> `133-133`: **LGTM!** Consistent with project-wide compact layout changes. </blockquote></details> <details> <summary>packages/db/prisma/migrations/20251116173412_new_media_enum_and_alt_text_column/migration.sql (1)</summary><blockquote> `1-5`: **LGTM!** The `media_updated` webhook event and `alt` text column are straightforward additions that enhance media tracking and accessibility. </blockquote></details> <details> <summary>apps/cms/src/components/nav/app-sidebar.tsx (1)</summary><blockquote> `16-16`: **LGTM!** Removing the border aligns with the streamlined navigation styling. </blockquote></details> <details> <summary>apps/cms/src/components/editor/editor-page.tsx (1)</summary><blockquote> `34-34`: **LGTM - Import path refactor.** The HiddenScrollbar import correctly reflects the component relocation to the ui directory. </blockquote></details> <details> <summary>apps/cms/src/components/auth/register-form.tsx (1)</summary><blockquote> `13-13`: **LGTM - Loading indicator update.** The ButtonLoadingSpinner correctly replaces the old spinner for GitHub button loading state, aligning with button-specific loading visuals. Also applies to: 147-147 </blockquote></details> <details> <summary>apps/cms/src/app/api/posts/import/route.ts (1)</summary><blockquote> `8-8`: **LGTM - Webhook dispatch centralization.** The dispatchWebhooks call properly centralizes webhook delivery with appropriate error handling and contextual logging. Also applies to: 142-157 </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/keys/page-client.tsx (1)</summary><blockquote> `14-17`: **LGTM - Layout consistency update.** The size="compact" prop correctly applies compact layout styling to the wrapper. </blockquote></details> <details> <summary>apps/cms/src/components/editor/tabs/metadata-tab.tsx (1)</summary><blockquote> `6-6`: **LGTM - Import path refactor.** The HiddenScrollbar import correctly reflects the component relocation to the ui directory. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/schemas/page.tsx (1)</summary><blockquote> `9-12`: **LGTM - Layout consistency update.** The size="compact" prop correctly applies compact layout styling to the wrapper. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/editor-preferences/page-client.tsx (1)</summary><blockquote> `8-8`: **LGTM - Layout consistency update.** The size="compact" prop correctly applies compact layout styling to the wrapper. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx (1)</summary><blockquote> `40-43`: **LGTM - Layout consistency update.** The size="compact" prop and spacing classes correctly apply compact layout styling to the wrapper. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/layout.tsx (1)</summary><blockquote> `30-30`: **LGTM! Responsive padding enhancement.** The progressive padding adjustments for larger viewports improve the layout's responsiveness and visual hierarchy. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx (1)</summary><blockquote> `43-46`: **LGTM! Consistent compact sizing.** The addition of `size="compact"` aligns with the layout standardization across dashboard pages. </blockquote></details> <details> <summary>apps/cms/src/components/editor/tabs/analysis-tab.tsx (1)</summary><blockquote> `16-16`: **LGTM! Import path centralization.** Moving the import to the UI directory standardizes component organization. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/general/page-client.tsx (1)</summary><blockquote> `13-13`: **LGTM! Consistent compact sizing.** The compact size prop maintains layout consistency with other settings pages. </blockquote></details> <details> <summary>apps/cms/src/components/nav/nav-main.tsx (1)</summary><blockquote> `93-106`: **LGTM! Improved navigation semantics and accessibility.** The changes enhance both accessibility (screen-reader only label) and user experience (Home icon/label is clearer than Overview). </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx (1)</summary><blockquote> `56-66`: **LGTM! Consistent compact sizing across both states.** Both rendering branches (categories exist and empty state) correctly apply the compact size prop for layout consistency. </blockquote></details> <details> <summary>apps/cms/src/app/api/tags/route.ts (2)</summary><blockquote> `5-6`: **LGTM! Updated webhook imports.** The import changes align with the refactored webhook utilities that now use workspace-scoped operations. --- `73-73`: **LGTM! Webhook refactor to workspace-scoped context.** The change from session-based to `workspaceId`-based webhook retrieval correctly aligns with the new `getWebhooks` signature and the PR's centralized webhook dispatching approach. </blockquote></details> <details> <summary>apps/cms/src/lib/validations/webhook.ts (1)</summary><blockquote> `14-14`: **LGTM!** The new `media_uploaded` event is properly integrated into both the Zod enum and the events metadata array, following the established pattern for other webhook events. Also applies to: 132-136 </blockquote></details> <details> <summary>apps/cms/src/components/nav/page-header.tsx (1)</summary><blockquote> `40-40`: **LGTM!** Simplifying the fallback to a static "Home" heading aligns with the PR's goal of standardizing the dashboard UI. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx (1)</summary><blockquote> `52-54`: **LGTM!** Adding `size="compact"` to both wrapper instances standardizes the layout across dashboard pages. Also applies to: 59-61 </blockquote></details> <details> <summary>apps/cms/src/app/api/posts/route.ts (1)</summary><blockquote> `179-196`: **LGTM!** The migration to centralized webhook dispatching is well-implemented. The fire-and-forget pattern with error logging is appropriate, ensuring webhook failures don't block the post creation response. </blockquote></details> <details> <summary>apps/cms/src/app/api/categories/route.ts (1)</summary><blockquote> `77-91`: **LGTM!** Webhook dispatching follows the same centralized pattern as other routes. The error logging includes helpful context for debugging. </blockquote></details> <details> <summary>apps/cms/src/components/auth/login-form.tsx (1)</summary><blockquote> `13-16`: **LGTM!** The loading spinner refactor aligns with the new centralized loading-spinner module while maintaining the existing authentication flow. Also applies to: 22-22, 133-133 </blockquote></details> <details> <summary>apps/cms/src/app/api/upload/complete/route.ts (1)</summary><blockquote> `133-149`: **LGTM!** Webhook dispatching follows the established centralized pattern with appropriate fire-and-forget error handling. </blockquote></details> <details> <summary>apps/cms/src/components/layout/wrapper.tsx (1)</summary><blockquote> `9-9`: **CSS classes verified and properly defined.** The `workspace-container` (line 295) and `workspace-container-compact` (line 320) classes are both present in `apps/cms/src/styles/globals.css` with responsive breakpoints (sm, md, lg, xl, 2xl) and corresponding CSS variables. The implementation is complete and the new `size` prop has the necessary backing styles. </blockquote></details> <details> <summary>apps/cms/src/lib/polar/client.ts (1)</summary><blockquote> `3-17`: **Polar client singleton looks good** The lazy, cached client with a `null` return when `POLAR_ACCESS_TOKEN` is missing is reasonable. Just ensure all call sites treat a `null` client as “Polar not configured” and branch accordingly so you don’t silently skip expected side effects. </blockquote></details> <details> <summary>apps/cms/src/app/(main)/[workspace]/(dashboard)/webhooks/page-client.tsx (1)</summary><blockquote> `107-110`: **Consistent use of compact workspace wrapper** Switching both the empty and populated states to `WorkspacePageWrapper` with `size="compact"` (and explicit padding on the list view) keeps layout consistent with the new compact workspace pattern. Looks good. Also applies to: 133-136 </blockquote></details> <details> <summary>apps/cms/src/app/api/categories/[id]/route.ts (1)</summary><blockquote> `42-51`: **Webhook dispatch is fire‑and‑forget; confirm this is intentional** Both update and delete now call `dispatchWebhooks(...)` without `await`, relying on the dispatcher + `.catch` handlers to run delivery in the background and log failures. That avoids blocking the HTTP response on webhook delivery, but it also means callers aren’t guaranteed that webhooks have been enqueued before the 200/204 is returned. If that trade‑off is intended, this wiring looks solid. Also applies to: 53-67, 117-128 </blockquote></details> <details> <summary>apps/cms/src/app/api/posts/[id]/route.ts (2)</summary><blockquote> `196-203`: **Publish/update webhook flow looks coherent** Using `post_published` only on a draft→published transition and always emitting `post_updated` with the same payload is a clean separation of “first publish” vs “any change”. Fire‑and‑forget dispatch with error logging matches the intent in the comment. Also applies to: 205-228 --- `245-247`: **Verify Prisma delete filter and workspace scoping** The DELETE handler now uses: ```ts const workspaceId = sessionData?.session.activeOrganizationId; // ... const deletedPost = await db.post.delete({ where: { id, workspaceId }, });In Prisma,
deleteexpects awherematching a unique constraint. Please confirm that yourPostmodel defines a unique or ID constraint that uses bothidandworkspaceId(e.g.@@id([id, workspaceId])or@@unique([id, workspaceId])). Ifidalone is the primary key, thiswherewill not type‑check and you likely want eitherwhere: { id }(relying on earlier workspace checks) ordeleteMany({ where: { id, workspaceId } })instead. The newpost_deleteddispatch wiring itself looks fine.Also applies to: 255-255, 262-273
apps/cms/src/lib/webhooks/webhook-client.ts (1)
59-65: Ensure allmedia.uploadedemitters providesizeandtypeYou’ve tightened the
media.uploadedevent schema to requiresizeand atypeenum. This is good for downstream consumers, but any existing code that callsWebhookClient.send({ event: "media.uploaded", ... })without these fields will now fail Zod validation at the receiver. Worth checking all emitters to confirm they’re sending both fields with values in["image", "video", "audio", "document"].apps/cms/src/app/api/media/route.ts (1)
9-224: Media DELETE scoping + batched webhook dispatch look correct.Filtering
db.media.findManybyworkspaceId, buildingmediaDeletedFromR2from items actually removed (or missing URLs), and then deleting+tracking only those IDs is a nice tightening of behavior. The newdispatchWebhookscall withvalidationEvent: "media_deleted"and a single batched payload looks consistent and won’t block the response since it’s fire‑and‑forget with.catch. No issues from my side here.apps/cms/src/components/home/media-usage-card.tsx (1)
47-127: MediaUsageCard looks solidLoading, empty, and populated states are handled cleanly, and recent upload rendering (including icons, size, and relative time) is straightforward with no obvious issues.
packages/db/prisma/schema.prisma (1)
34-57: UsageEvent schema and media alt additions look consistentThe new
UsageEventmodel, its relations/indexes, and theusageEventsback‑reference onOrganizationalign with existing workspace‑scoped tables, andMedia.altplusmedia_updatedevent fit cleanly into the current schema with no obvious migration or relational issues.Also applies to: 150-165, 343-407
apps/cms/src/types/usage-dashboard.ts (1)
1-40: UsageDashboardData shape matches dashboard usageThe structure cleanly covers API, webhook, and media metrics and matches how the dashboard components consume it, so type‑safety around usage data looks good.
apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx (1)
31-56: Usage dashboard layout and card wiring look goodThe loading/error branches are straightforward, and the compact
WorkspacePageWrapperwith the three usage cards wired todata?.api,data?.webhooks, anddata?.mediamatches theUsageDashboardDatashape and the cards’ expected props; this composition looks clean.
…ub.com/taqh/marble into workspace-analytics-and-usage-tracking
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/cms/src/app/api/upload/complete/route.ts (1)
17-84: Fix user ID access in webhook payload and simplify background tracking
- Bug: In the media case, you use
sessionData.user.id(Line 75) while the rest of the file usessessionData.session.userId(e.g., Line 34). UnlessgetServerSessionactually adds auserproperty, this will beundefinedand throw at runtime. For consistency and safety, use the samesessionData.session.userIdshape:- payload: { - id: media.id, - name: media.name, - userId: sessionData.user.id, - size: media.size, - type: media.type, - }, + payload: { + id: media.id, + name: media.name, + userId: sessionData.session.userId, + size: media.size, + type: media.type, + },
- Optional:
trackMediaUploadalready wraps both DB and Polar calls intry/catch, so it never rejects. The.catchat the call site (Lines 64–66) is therefore redundant; you can safely fire-and-forget:- trackMediaUpload(workspaceId, fileSize, mediaType).catch((err) => { - console.error("[Media Upload] Failed to track upload:", err); - }); + void trackMediaUpload(workspaceId, fileSize, mediaType);Similarly,
dispatchWebhooksitself defensive‑logs delivery errors; the extra.catchis only needed if you specifically want to log unexpected top-level failures, otherwise it can be dropped for brevity.
🧹 Nitpick comments (6)
apps/cms/src/components/nav/nav-main.tsx (1)
24-24: Remove unused import.
LayoutIconis no longer used after replacing "Overview" with "Home" (line 104).- LayoutIcon,apps/cms/src/components/home/api-usage-card.tsx (3)
3-3: Remove unused import.The
Badgeimport is not used in this component.-import { Badge } from "@marble/ui/components/badge";
30-35: Remove redundant fallback operator.The
?? []on line 35 is redundant sincemap()always returns an array.const chartData = - rawChartData.map((item) => ({ + rawChartData.map((item) => ({ label: item.label, value: item.value, date: item.date, - })) ?? []; + }));
44-44: Remove no-op function.
formatXAxisLabelreturns its input unchanged. Remove it and thetickFormatterprop.- const formatXAxisLabel = (value: string) => value; -<XAxis axisLine={false} dataKey="label" minTickGap={24} - tickFormatter={formatXAxisLabel} tickLine={false} tickMargin={8} />Also applies to: 111-111
apps/cms/src/components/home/media-usage-card.tsx (1)
47-117: Add accessible label for the icon-only open buttonThe icon-only
Buttonthat opens the media URL (Lines 107–117) has no accessible text, so screen readers won’t know what it does. Add anaria-label(or visually hidden text) describing the action, e.g.:- <Button + <Button + aria-label="Open media in new tab" className="rounded-full bg-sidebar hover:bg-primary/10 hover:text-primary dark:bg-accent/50 dark:hover:text-accent-foreground" onClick={(e) => { e.stopPropagation(); openMediaFile(upload.url); }} size="icon" variant="ghost" >apps/cms/src/utils/usage/media.ts (1)
8-101: Solid non-blocking tracking; a couple of optional refinementsThe overall structure (DB + Polar with independent
try/catchblocks and workspace fallback) looks good and resilient.Two small, optional tweaks:
- Simplify owner lookup (Lines 23–26):
- const ownerUserId = - organization?.members && organization.members.length > 0 - ? organization.members[0]?.userId - : undefined; + const ownerUserId = organization?.members?.[0]?.userId;
- Consider running DB and Polar tracking in parallel in
trackMediaUploadif ordering doesn’t matter, to reduce tail latency:-export async function trackMediaUpload( - workspaceId: string, - fileSize: number, - mediaType: string -): Promise<void> { - try { - await trackMediaUploadInDB(workspaceId, fileSize); - } catch (error) { - console.error("[Media Upload] Failed to track in DB:", error); - } - - try { - const customerId = await getCustomerIdForWorkspace(workspaceId); - await trackMediaUploadInPolar(customerId, fileSize, mediaType); - } catch (error) { - console.error( - "[Media Upload] Polar ingestion error (events may still be processed):", - error instanceof Error ? error.message : error - ); - } -} +export async function trackMediaUpload( + workspaceId: string, + fileSize: number, + mediaType: string +): Promise<void> { + await Promise.allSettled([ + (async () => { + try { + await trackMediaUploadInDB(workspaceId, fileSize); + } catch (error) { + console.error("[Media Upload] Failed to track in DB:", error); + } + })(), + (async () => { + try { + const customerId = await getCustomerIdForWorkspace(workspaceId); + await trackMediaUploadInPolar(customerId, fileSize, mediaType); + } catch (error) { + console.error( + "[Media Upload] Polar ingestion error (events may still be processed):", + error instanceof Error ? error.message : error + ); + } + })(), + ]); +}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/cms/src/app/api/upload/complete/route.ts(3 hunks)apps/cms/src/components/home/api-usage-card.tsx(1 hunks)apps/cms/src/components/home/media-usage-card.tsx(1 hunks)apps/cms/src/components/home/webhook-usage-card.tsx(1 hunks)apps/cms/src/components/nav/nav-devs.tsx(1 hunks)apps/cms/src/components/nav/nav-main.tsx(3 hunks)apps/cms/src/utils/usage/media.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/cms/src/components/home/webhook-usage-card.tsx
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: taqh
Repo: usemarble/marble PR: 137
File: packages/db/prisma/migrations/20250826194942_make_category_required/migration.sql:11-11
Timestamp: 2025-08-26T20:20:46.367Z
Learning: User "taqh" prefers concise code review comments rather than verbose explanations. Keep reviews short and to the point.
📚 Learning: 2025-09-28T22:18:20.726Z
Learnt from: mezotv
Repo: usemarble/marble PR: 198
File: apps/cms/src/app/(main)/[workspace]/(workspace)/keys/page-client.tsx:23-32
Timestamp: 2025-09-28T22:18:20.726Z
Learning: For Radix UI tooltips with disabled buttons: using asChild with disabled buttons breaks tooltip functionality because disabled elements don't receive the pointer events needed to trigger tooltips. Solutions include: 1) wrapper span with asChild and pointer-events: none on the disabled button, or 2) using aria-disabled instead of disabled to preserve interaction while preventing clicks.
Applied to files:
apps/cms/src/components/nav/nav-devs.tsx
🧬 Code graph analysis (4)
apps/cms/src/components/home/api-usage-card.tsx (3)
apps/cms/src/types/usage-dashboard.ts (1)
UsageDashboardData(1-40)packages/ui/src/components/chart.tsx (1)
ChartConfig(11-19)apps/cms/src/components/ui/loading-spinner.tsx (1)
LoadingSpinner(8-29)
apps/cms/src/utils/usage/media.ts (1)
packages/db/src/index.ts (1)
db(29-29)
apps/cms/src/app/api/upload/complete/route.ts (2)
apps/cms/src/utils/usage/media.ts (1)
trackMediaUpload(82-102)apps/cms/src/lib/webhooks/dispatcher.ts (1)
dispatchWebhooks(33-90)
apps/cms/src/components/home/media-usage-card.tsx (4)
apps/cms/src/types/usage-dashboard.ts (1)
UsageDashboardData(1-40)apps/cms/src/utils/string.ts (1)
formatBytes(13-34)apps/cms/src/components/ui/hidden-scrollbar.tsx (1)
HiddenScrollbar(8-31)apps/cms/src/components/ui/loading-spinner.tsx (1)
LoadingSpinner(8-29)
🔇 Additional comments (7)
apps/cms/src/components/nav/nav-devs.tsx (1)
46-46: LGTM!Styling simplification is consistent with the broader navigation updates.
apps/cms/src/components/nav/nav-main.tsx (4)
93-93: LGTM!The
sr-onlyclass maintains screen reader accessibility while simplifying the visual UI.
99-106: LGTM!Overview → Home rebranding with HouseIcon and simplified active state styling are intentional improvements.
113-114: LGTM!Active state styling simplification is consistent with the broader navigation updates.
135-136: LGTM!Settings active state styling matches the simplified pattern used throughout the navigation.
apps/cms/src/components/home/api-usage-card.tsx (2)
37-42: LGTM!Chart configuration is well-structured and properly typed.
72-139: LGTM!The component properly handles loading, empty, and data states with clean JSX structure.
Description
Makes the dashboard sexier
Motivation and Context
well i wasn't showing users their usage before so this fixes that
How to Test
Screenshots (if applicable)
Video Demo (if applicable)
Types of Changes
Summary by CodeRabbit
New Features
Improvements
Chores