Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@TWilson023
Copy link
Collaborator

@TWilson023 TWilson023 commented Nov 24, 2025

Remaining considerations:

  • Should we add time-series granularity support to the usage pipe? Currently the bars get pretty narrow when looking at a full year
  • Grouping and filtering events by folder ID required an extra LEFT JOIN of links metadata for each event query. Do we need to optimize this further?

Screenshot:
Screenshot 2025-11-26 at 4 29 14β€―PM

Summary by CodeRabbit

  • New Features

    • Grouped usage reporting: per-date, per-group breakdowns and totals; date range accepts start/end or interval; backend now supports grouping and interval endpoints.
    • Date-range picker accepts explicit values and presets; usage hook exposes start, end, interval, and groupBy.
    • Chart plumbing: stacked per-date bars now support a radius option and a hover-date callback for external sync.
  • UI/UX Improvements

    • Charts show stacked per-group bars, top-group tooltips, hover-aware breakdowns, loading/empty states, and refined billing usage typography and spacing.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Contributor

vercel bot commented Nov 24, 2025

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

Project Deployment Preview Updated (UTC)
dub Ready Ready Preview Nov 27, 2025 0:12am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

Adds timezone-aware date-range and interval handling, optional grouping by folder or domain, a new Tinybird v3_usage_latest pipe, API grouping with Prisma metadata enrichment, hook and schema updates exposing start/end/interval/groupBy, date-range picker values support, stacked per-group bars, and tooltip hover-sync.

Changes

Cohort / File(s) Summary
API endpoint & Tinybird pipeline
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts, packages/tinybird/pipes/v3_usage_latest.pipe
Switches to v3_usage_latest Tinybird pipe, formats start/end/interval params, supports optional groupBy; when grouped, queries Prisma for folder/domain metadata and merges per-group usage into per-date responses.
Usage hooks & schemas
apps/web/lib/swr/use-usage.ts, apps/web/lib/swr/use-partners-count.ts, apps/web/lib/zod/schemas/usage.ts
useUsage derives/returns start, end, interval, groupBy, updates SWR types to include per-date groups, adds revalidateOnFocus:false. use-partners-count spreads params when ignoreParams. Zod schema: optional groupBy/interval, start/end optional; response allows folder_id/domain.
Date range picker
apps/web/ui/shared/simple-date-range-picker.tsx
Adds values?: { start?, end?, interval? } prop, prefers provided values over URL params, and supports conditional presets selection.
Usage analytics UI
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx
Adds groupBy ToggleGroup and URL sync, integrates SimpleDateRangePicker, builds per-group chartData and groupsMeta, renders stacked per-group bars, per-group totals and list, and hover-aware tooltip breakdown with loading/empty states.
Plan usage page
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx
Passes ignoreParams: true to usePartnersCount and applies small layout, typography, and status-bar color tweaks.
Chart components & tooltip/types
packages/ui/src/charts/bars.tsx, packages/ui/src/charts/time-series-chart.tsx, packages/ui/src/charts/types.ts, packages/ui/src/charts/use-tooltip.ts
Bars refactored to per-date stacked rendering, removes gradientClassName, adds radius prop. TimeSeriesChart updates min/max aggregation and padding, threads onHoverDateChange. types adds optional onHoverDateChange. use-tooltip throttles tooltip updates and emits hover-date changes.
Minor styling changes
apps/web/ui/analytics/analytics-card.tsx, apps/web/app/.../plan-usage.tsx
Small Tailwind class reorder and layout/typography/margin adjustments; no behavioral changes.

Sequence Diagram

sequenceDiagram
    autonumber
    participant User
    participant UI as UsageChart UI
    participant Hook as useUsage
    participant API as /api/.../usage
    participant Tinybird as v3_usage_latest
    participant Prisma as Prisma

    User->>UI: select date range & groupBy
    UI->>Hook: request (start,end,interval,groupBy)
    Hook->>API: GET /usage?start=&end=&interval=&groupBy=
    API->>Tinybird: query with formatted params
    Tinybird-->>API: time-segmented rows (Β± group key)
    alt groupBy provided
        API->>Prisma: fetch group metadata (folder/domain ids)
        Prisma-->>API: metadata {id,name,...}
        API->>API: merge metadata β†’ [{date,total,groups:[{id,name,usage}]}]
    end
    API-->>Hook: structured per-date data (+start/end/interval/groupBy)
    Hook-->>UI: data
    UI->>UI: compute stacked bars & totals, render chart and tooltips
    UI-->>User: visualized stacked per-group usage
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • packages/tinybird/pipes/v3_usage_latest.pipe: SQL templating, timezone, groupBy branching and aggregations.
    • apps/.../usage/route.ts: Tinybird request formatting, Prisma metadata queries, merging logic by date/group.
    • packages/ui/src/charts/bars.tsx: stacking math, radius handling, rendering changes.
    • Tooltip throttling and onHoverDateChange propagation across use-tooltip, types, and time-series-chart.
    • SWR key construction and params spreading behavior in use-partners-count.ts.

Possibly related PRs

  • Folder updatesΒ #2942 β€” Related to folder model/Prisma select changes used by the API’s folder_id metadata queries and merging.

Poem

🐰 I hopped through ranges, dates in a line,

I grouped every folder, each domain made to shine.
I counted tiny clicks and stacked them neat and bright,
Carrots turned to bars, and charts hummed with delight. ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
βœ… Passed checks (2 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title 'Stacked usage charts' directly and clearly describes the main change: introducing stacked visualization for usage metrics across multiple components and data layers.
✨ Finishing touches
  • πŸ“ Generate docstrings
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch stacked-usage

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.

❀️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (7)
apps/web/lib/swr/use-partners-count.ts (1)

21-26: Potential serialization of undefined values as strings.

Spreading params directly into URLSearchParams will serialize any undefined values as the string "undefined". Consider filtering out undefined/null entries before constructing the query string:

   const queryString = ignoreParams
     ? // @ts-ignore
       `?${new URLSearchParams({
-        ...params,
+        ...Object.fromEntries(
+          Object.entries(params).filter(([, v]) => v != null)
+        ),
         workspaceId,
       }).toString()}`
packages/tinybird/pipes/v3_usage_latest.pipe (1)

104-108: Clarify link counting logic with uniq(*).

uniq(*) counts unique rows based on all selected columns. Since you're selecting from dub_links_metadata_latest FINAL, this effectively counts rows. If the intent is to count distinct links, consider using uniq(link_id) or count(*) for clarity:

     SELECT
         toDateTime64(toStartOfDay(timestamp, {{ String(timezone, 'UTC') }}), 3) AS interval,
         {% if defined(groupBy) %} {% if groupBy == 'folder_id' %} folder_id {% else %} domain {% end %}, {% end %}
-        uniq(*) as links
+        count(*) as links
     FROM dub_links_metadata_latest FINAL
apps/web/ui/shared/simple-date-range-picker.tsx (1)

70-73: Redundant type assertion.

The cast (presets as string[]) is unnecessary since presets is already typed as (typeof DATE_RANGE_INTERVAL_PRESETS)[number][], which should be compatible with string[].

-        ? INTERVAL_DISPLAYS.filter(({ value }) =>
-            (presets as string[]).includes(value),
-          )
+        ? INTERVAL_DISPLAYS.filter(({ value }) => presets.includes(value))
packages/ui/src/charts/bars.tsx (1)

74-74: Avoid as any[] type assertion.

The reduce accumulator is typed as any[], losing type safety. Consider defining an explicit type:

+            type StackedBar = {
+              id: string;
+              value: number;
+              colorClassName?: string;
+              styles?: { barClassName?: string; barFill?: string };
+              y: number;
+              height: number;
+            };
+
-            const bars = sortedSeries.reduce((acc, s) => {
+            const bars = sortedSeries.reduce<StackedBar[]>((acc, s) => {
               // ...
-            }, [] as any[]);
+            }, []);
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (1)

53-99: Domain grouping metadata is effectively unused and can be simplified

When groupBy === "domain":

  • groupIds are derived from d[groupBy] where groupBy is "domain", so these are the Tinybird domain field values (slugs per usageResponse), not DB ids.
  • The Prisma query uses where: { projectId: workspace.id, id: { in: groupIds } } and later groupMeta.find((g) => g.id === groupId), but groupId is a slug, so this find will almost always fail.
  • As a result, name falls back to groupId, and the Prisma lookup does no real work for domains, while still adding a DB round-trip.

Given that the slug is already a good display label, you could either:

  • Skip the Prisma query entirely for groupBy === "domain" and just use name: groupId, or
  • If you actually want to look up by slug, change the query & lookup to use slug instead of id.

You might also want to filter out empty groupIds instead of including "" in the IN list and groups array, unless you explicitly want an β€œUnsorted” bucket.

Also applies to: 105-105

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (2)

143-172: Sort tooltip β€œtop groups” by the hovered day’s values, not the first row’s usage

In the tooltip you compute:

const topGroups = usage?.[0]?.groups
  ?.filter((group) => d.values[group.id] > 0)
  .sort((a, b) => b.usage - a.usage)
  .slice(0, 8);

group.usage here is taken from the first data row (usage[0]), so the sort order reflects that first day rather than the hovered day d. Since you already have d.values[group.id], you can sort by that instead to make the ordering match what’s actually shown:

-  .sort((a, b) => b.usage - a.usage)
+  .sort((a, b) => d.values[b.id] - d.values[a.id])

This keeps the displayed values the same but makes the ranking per-day intuitive.

Also applies to: 222-247


155-172: Initialize groupsMeta so loading/error UI can actually render

groupsMeta is always an object:

const groupsMeta = useMemo(
  () =>
    Object.fromEntries(
      usage?.[0]?.groups?.map(/* ... */) ?? [],
    ),
  [usage],
);

Even when usage is undefined, this returns {}, which is truthy. That means the groupsMeta ? (…) : (…) branch later will always take the β€œtruthy” side and the skeleton / β€œFailed to load usage data” UI is effectively unreachable.

If you want to show the loading skeleton until grouped data exists, consider something like:

-const groupsMeta = useMemo(
-  () =>
-    Object.fromEntries(
-      usage?.[0]?.groups?.map(/* ... */) ?? [],
-    ),
-  [usage],
-);
+const groupsMeta = useMemo(() => {
+  if (!usage?.[0]?.groups) return null;
+  return Object.fromEntries(
+    usage[0].groups.map((g, idx) => [/* ... */]),
+  );
+}, [usage]);

and keep the existing groupsMeta ? … : … conditional. That way, skeletons render while loading, and the error message shows if usage never arrives.

Also applies to: 315-391

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 3673b73 and 46d7549.

πŸ“’ Files selected for processing (10)
  • apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (2 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx (5 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (6 hunks)
  • apps/web/lib/swr/use-partners-count.ts (1 hunks)
  • apps/web/lib/swr/use-usage.ts (4 hunks)
  • apps/web/lib/zod/schemas/usage.ts (1 hunks)
  • apps/web/ui/shared/simple-date-range-picker.tsx (2 hunks)
  • packages/tinybird/pipes/v3_usage_latest.pipe (1 hunks)
  • packages/ui/src/charts/bars.tsx (2 hunks)
  • packages/ui/src/charts/time-series-chart.tsx (3 hunks)
🧰 Additional context used
🧠 Learnings (4)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx
πŸ“š Learning: 2025-08-26T15:05:55.081Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/swr/use-bounty.ts:11-16
Timestamp: 2025-08-26T15:05:55.081Z
Learning: In the Dub codebase, workspace authentication and route structures prevent endless loading states when workspaceId or similar route parameters are missing, so gating SWR loading states on parameter availability is often unnecessary.

Applied to files:

  • apps/web/lib/swr/use-usage.ts
πŸ“š Learning: 2025-10-31T19:45:25.702Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 3044
File: apps/web/lib/analytics/utils/get-interval-data.ts:31-0
Timestamp: 2025-10-31T19:45:25.702Z
Learning: In apps/web/lib/analytics/utils/get-interval-data.ts, the `mtd` and `qtd` interval functions are designed to work in both server and client contexts. When no timezone is provided, they intentionally fall back to `new Date()` (local timezone) because front-end code calls these methods without passing a timezone and should use the browser's local timezone for the user's context.

Applied to files:

  • apps/web/lib/zod/schemas/usage.ts
🧬 Code graph analysis (2)
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (3)
apps/web/lib/tinybird/client.ts (1)
  • tb (3-6)
apps/web/lib/analytics/utils/get-start-end-dates.ts (1)
  • getStartEndDates (4-46)
packages/prisma/index.ts (1)
  • prisma (3-9)
apps/web/lib/zod/schemas/usage.ts (1)
apps/web/lib/analytics/constants.ts (1)
  • DATE_RANGE_INTERVAL_PRESETS (1-11)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (8)
apps/web/ui/shared/simple-date-range-picker.tsx (1)

30-30: Potential controlled/uncontrolled behavior mismatch.

When values is provided, the picker displays controlled values but onChange still updates URL params via queryParams(). This might cause parent state and URL params to diverge. If values is intended to enable fully controlled mode, consider accepting an onChange callback prop and conditionally skipping queryParams updates.

packages/ui/src/charts/bars.tsx (1)

54-74: Verify stacking calculation produces expected visual result.

The stacking logic sorts series by value descending (largest first), then accumulates heights. This means:

  • The largest-value series renders at the bottom
  • Smaller values stack on top

Confirm this is the intended behavior. Typically, stacked charts maintain consistent series order (not sorted by value per-date) so users can visually track each series across dates. Sorting by value per-date may make it harder to follow a specific series.

packages/ui/src/charts/time-series-chart.tsx (2)

80-96: LGTM!

The minY/maxY calculation correctly sums values for stacked bar charts, ensuring the y-axis accommodates the full stacked height. The dependency array update to include type is correct.


233-237: LGTM!

The tooltip offset calculation using bandwidth * (1 + padding) correctly positions the tooltip relative to the bar width, accounting for inter-bar spacing.

apps/web/lib/zod/schemas/usage.ts (1)

1-11: Usage schemas align with new grouping + interval behavior

The added groupBy, interval, optional start/end, and optional folder_id/domain fields match how the API route and useUsage hook now construct and consume grouped usage data. Shapes look consistent and backward-compatible (no behavior change when groupBy is omitted).

Also applies to: 18-19

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx (2)

64-68: Confirm ignoreParams: true behavior matches the intended caching semantics

Passing ignoreParams: true to usePartnersCount changes how the SWR key is constructed (and thus how results are cached/shared). That’s fine if partners count should be independent of URL params on this page; just double-check this aligns with the updated use-partners-count implementation and doesn’t accidentally coalesce distinct queries.


334-335: UsageTabCard styling tweaks look good and keep logic unchanged

The adjustments to spacing, typography, progress bar base color, and the remaining-usage label are purely presentational and don’t affect the usage/limit computations or upgrade behavior. No issues from a logic perspective.

Also applies to: 339-339, 357-357, 372-373, 385-385

apps/web/lib/swr/use-usage.ts (1)

39-52: Hook now correctly drives interval + grouping-aware usage queries

The memoized { start, end, interval } logic (interval taking precedence over explicit dates) and the groupBy derivation from URL params line up with the updated API route and zod schema:

  • API is always called with either an interval or a concrete [start, end] pair, never both.
  • groupBy is normalized to "folderId" | "domain" on the client and mapped to "folder_id" | "domain" for the backend.
  • SWR typing to { date; value; groups[] }[] matches the grouped response shape the route now returns when groupBy is always set.

Timezone is still passed using the browser’s local zone, which plays nicely with the existing getStartEndDates behavior that respects a provided timezone. Based on learnings, this is consistent with how other analytics intervals are handled.

Also applies to: 54-58, 63-88

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (1)

80-92: Consider optimizing the nested loop structure for better performance.

The current implementation uses nested loops with filtering, resulting in O(dates Γ— groups Γ— data.length) complexity. For large datasets, this could become a performance bottleneck.

Consider pre-grouping the data for O(data.length) complexity:

// Build a lookup map first
const dataByDateAndGroup = new Map<string, Map<string, number>>();
for (const item of response.data) {
  const groupId = item[groupBy] ?? "";
  if (!dataByDateAndGroup.has(item.date)) {
    dataByDateAndGroup.set(item.date, new Map());
  }
  const groupMap = dataByDateAndGroup.get(item.date)!;
  groupMap.set(groupId, (groupMap.get(groupId) ?? 0) + item.value);
}

// Then build the response
data = dates.map((date) => {
  const groupMap = dataByDateAndGroup.get(date) ?? new Map();
  const groups = groupIds.map((groupId) => ({
    id: groupId,
    name: groupMeta.find((g) => g.id === groupId)?.[
      groupBy === "folder_id" ? "name" : "slug"
    ] ?? groupId,
    usage: groupMap.get(groupId) ?? 0,
  }));

  return {
    date,
    value: sum(groups.map((g) => g.usage)),
    groups,
  };
});
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 46d7549 and a4c5684.

πŸ“’ Files selected for processing (1)
  • apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (7)
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (7)

1-8: LGTM! Import issue from previous review has been addressed.

The subYears import now correctly uses the named export from the main date-fns package, which resolves the previous critical issue.


32-38: LGTM! Date handling logic is sound.

The 1-year historical data limit (dataAvailableFrom) is reasonable for billing usage data, and the use of getStartEndDates provides consistent date range handling.


40-49: LGTM! Pipe request parameters are correctly formatted.

The date formatting (replacing "T" with space and removing "Z") appears intentional for Tinybird's expected format. The conditional parameter inclusion is appropriate.


105-105: LGTM! Sum helper is correctly implemented.

The reduce-based sum implementation handles empty arrays correctly and the TypeScript typing ensures type safety.


10-20: The usageQuerySchema already includes all required fields; no action needed.

Verification confirms that usageQuerySchema in apps/web/lib/zod/schemas/usage.ts (lines 4-13) includes all the fields being destructured in the route handler:

  • groupBy: z.enum(["folder_id", "domain"]).optional() (line 8)
  • interval: z.enum(DATE_RANGE_INTERVAL_PRESETS).optional() (line 9)
  • start: z.string().optional() (line 10)
  • end: z.string().optional() (line 11)

All fields are properly validated with appropriate types and optionality markers. The schema is correctly configured.


64-78: Schema validation for groupBy is correctly implemented and matches the code logic.

The usageQuerySchema defined in apps/web/lib/zod/schemas/usage.ts (line 8) validates groupBy to an enum of exactly ["folder_id", "domain"], which directly corresponds to the conditional logic in the code. The schema is properly applied via usageQuerySchema.parse(searchParams) before the code uses the groupBy value, ensuring only valid values reach the conditional branch.


22-30: Perfect! I've completed the verification. Let me provide the final analysis:

The v3_usage_latest pipe exists and is properly configured.

The pipe definition at ./packages/tinybird/pipes/v3_usage_latest.pipe confirms:

  1. βœ… Pipe exists: Located in the repository at the expected path
  2. βœ… Required parameters supported:
    • start, end, timezone, workspaceId (all explicitly defined in the SQL templates)
    • resource (used in final SELECT to switch between events/links)
  3. βœ… Optional parameters supported:
    • groupBy (conditionally includes folder_id or domain)
    • folderId and domain (conditionally filters events and links)
  4. βœ… Response schema matches:
    • Returns date (formatted DateTime)
    • Returns value (uniq count of events or links)
    • Optionally returns folder_id or domain when groupBy is specified
  5. βœ… Parameter extension is appropriate: Adding workspaceId to usageQuerySchema via .extend() is necessary since the pipe requires it as a mandatory parameter

The implementation correctly passes all required and optional parameters, and the pipe's conditional logic aligns with how the route processes groupBy filtering.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
packages/ui/src/charts/bars.tsx (3)

36-49: Per‑date value sorting changes vertical stack order; confirm this matches UX/legend expectations

Right now each date’s stack is ordered by valueAccessor(d) descending:

  • Line [54] filters to valueAccessor(d) > 0, then
  • Line [56] sorts by b.valueAccessor(d) - a.valueAccessor(d).

Because you then accumulate from bottom to top, the largest series for that date ends up at the bottom and the smallest at the top (which gets the rounded radius). This means the vertical position of a given series’ color can change from bucket to bucket depending on its relative value, even though the legend and series array likely have a fixed order.

If the design intent is for each series to occupy a stable vertical band across time (easier to read / match to legend), consider dropping the per‑date value sort and instead preserve the original series order (or a fixed explicit order), e.g.:

-            const sortedSeries = activeSeries
-              .filter((s) => s.valueAccessor(d) > 0)
-              .sort((a, b) => b.valueAccessor(d) - a.valueAccessor(d));
+            const sortedSeries = activeSeries.filter(
+              (s) => s.valueAccessor(d) > 0,
+            );

If the β€œlargest at bottom” behavior is intentional for this chart, it’d be good to document that somewhere near this block or in the chart component’s props to avoid surprises for future callers.

Also applies to: 50-57, 76-97


50-53: Tighten up accumulator typing and avoid repeated reductions in stack computation

The stacking math looks correct, but there are a couple of maintainability/perf nits here:

  • Line [58] initializes bars as [] as any[], losing all type safety for the computed segments.
  • Line [59] recomputes stackHeight on every iteration via acc.reduce((sum, b) => sum + b.height, 0), even though stackHeight is just the running sum of heights you’re already computing.
  • seriesStyles?.find(({ id }) => id === s.id) is run for every segment in every bucket; if seriesStyles grows, an indexed lookup would be clearer and cheaper.

A small refactor could make this more readable and typed without changing behavior. For example:

-            const bars = sortedSeries.reduce((acc, s) => {
-              const stackHeight = acc.reduce((sum, b) => sum + b.height, 0);
-              const value = s.valueAccessor(d) ?? 0;
-              const y = yScale(value);
-
-              return [
-                ...acc,
-                {
-                  id: s.id,
-                  value,
-                  colorClassName: s.colorClassName,
-                  styles: seriesStyles?.find(({ id }) => id === s.id),
-                  y: stackHeight, // y from x axis to bottom of bar
-                  height: height - y, // height from bottom to top of bar
-                },
-              ];
-            }, [] as any[]);
+            type BarSegment = {
+              id: string;
+              value: number;
+              colorClassName?: string;
+              styles?: (typeof seriesStyles)[number];
+              y: number;      // offset from x axis to bottom of bar (px)
+              height: number; // segment height (px)
+            };
+
+            let stackHeight = 0;
+            const bars: BarSegment[] = sortedSeries.map((s) => {
+              const value = s.valueAccessor(d) ?? 0;
+              const yVal = yScale(value);
+              const heightPx = height - yVal;
+
+              const bar: BarSegment = {
+                id: s.id,
+                value,
+                colorClassName: s.colorClassName,
+                styles: seriesStyles?.find(({ id }) => id === s.id),
+                y: stackHeight,
+                height: heightPx,
+              };
+
+              stackHeight += heightPx;
+              return bar;
+            });

(You could also precompute a Map for seriesStyles outside the data.map loop if this path ever becomes hot.)

Not mandatory for correctness, but it will make the stacking logic easier to reason about and less error‑prone if it evolves.

Also applies to: 58-75, 78-93


50-53: Consider failing fast instead of silently falling back when xScale(d.date) is undefined

Line [52] uses:

const x = xScale(d.date) ?? 0;

If, for whatever reason, d.date isn’t in the x‑scale’s domain, this will quietly render the bar at x = 0 instead of skipping the datum or surfacing the issue. That can make data/domain mismatches hard to spot.

If you expect data and xScale to always be in sync, consider an explicit guard:

const xVal = xScale(d.date);
if (xVal == null) return null; // or skip this <g> entirely
const x = xVal;

That way, a broken invariant doesn’t produce misleading bars at the origin.

πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between a4c5684 and 80f777b.

πŸ“’ Files selected for processing (1)
  • packages/ui/src/charts/bars.tsx (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 3044
File: apps/web/lib/analytics/utils/get-interval-data.ts:31-0
Timestamp: 2025-10-31T19:45:25.702Z
Learning: In apps/web/lib/analytics/utils/get-interval-data.ts, the `mtd` and `qtd` interval functions are designed to work in both server and client contexts. When no timezone is provided, they intentionally fall back to `new Date()` (local timezone) because front-end code calls these methods without passing a timezone and should use the browser's local timezone for the user's context.
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ui/src/charts/use-tooltip.ts (2)

77-110: Add onHoverDateChange to the dependency array.

The onHoverDateChange callback is invoked at lines 95 and 101 but is missing from the dependency array. This will cause stale closures if the callback reference changes.

Apply this diff:

   }, [
     tooltipSyncContext.tooltipDate,
     visxTooltip.tooltipData,
     snapToX,
     snapToY,
     xScale,
     yScale,
+    onHoverDateChange,
   ]);

112-164: Add onHoverDateChange to the dependency array.

The onHoverDateChange callback is invoked at line 152 but is missing from the dependency array. This will cause stale closures if the callback reference changes.

Apply this diff:

     [
       seriesId,
       data,
       xScale,
       yScale,
       series,
       defaultTooltipData,
       visxTooltip.showTooltip,
       tooltipSyncContext.setTooltipDate,
+      onHoverDateChange,
     ],
   );
🧹 Nitpick comments (2)
packages/ui/src/charts/use-tooltip.ts (1)

128-128: Remove debug console.log statement.

This appears to be leftover debug code that should be removed before merging.

Apply this diff:

       if (x0 === undefined) {
-        console.log("x0 is undefined", { defaultTooltipData });
         if (defaultTooltipData) visxTooltip.showTooltip(defaultTooltipData);
         else visxTooltip.hideTooltip();
         return;
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (1)

133-133: Remove unused activeResource from dependency array.

activeResource is included in the dependency array but is not referenced within the memo body. This causes unnecessary recalculations.

-    [activeResource, folders, domains],
+    [folders, domains],
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 80f777b and 9e1fd00.

πŸ“’ Files selected for processing (4)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (6 hunks)
  • packages/ui/src/charts/time-series-chart.tsx (6 hunks)
  • packages/ui/src/charts/types.ts (3 hunks)
  • packages/ui/src/charts/use-tooltip.ts (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ui/src/charts/time-series-chart.tsx
🧰 Additional context used
🧠 Learnings (2)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (4)
packages/ui/src/charts/types.ts (1)

37-41: LGTM! Type definitions are well-structured.

The pattern of using Omit to exclude onHoverDateChange from Required<> and then re-adding it as optional in ChartContext is correct. This ensures the callback remains optional while other chart props become required in the context. The documentation clearly explains the callback's purpose.

Also applies to: 66-68, 81-84

packages/ui/src/charts/use-tooltip.ts (1)

72-74: Consider notifying onHoverDateChange when showing the default tooltip.

When the default tooltip is displayed on mount, onHoverDateChange is not invoked. If the intent is to sync external UI with the initially displayed datum, you should call onHoverDateChange(defaultTooltipDatum.date) here.

If this is intentional (default tooltip doesn't represent active hover), please clarify. Otherwise, consider this adjustment:

   useEffect(() => {
-    if (defaultTooltipData) visxTooltip.showTooltip(defaultTooltipData);
+    if (defaultTooltipData) {
+      visxTooltip.showTooltip(defaultTooltipData);
+      onHoverDateChange?.(defaultTooltipData.tooltipData.date);
+    } else {
+      onHoverDateChange?.(null);
+    }
   }, [defaultTooltipData]);
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (2)

371-448: LGTM on the group list implementation.

Good use of NumberFlowGroup for coordinated animations, skeleton loading states for better UX, and proper handling of division by zero in percentage calculations (|| 1). The interactive hover state updates with animated number transitions provide a polished experience.


272-274: Good loading state handling with opacity transition.

Using isValidating to show reduced opacity during revalidation while keeping content visible provides good UX feedback without jarring layout shifts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/ui/src/charts/use-tooltip.ts (4)

125-158: Critical: Missing dependencies in tooltip sync effect.

The effect uses data, seriesId, series, scheduleTooltipUpdate, and onHoverDateChange but doesn't list them in the dependency array. This violates React's exhaustive-deps rule and can cause stale closures, leading to incorrect tooltip data or missed updates.

Apply this diff:

   }, [
     tooltipSyncContext.tooltipDate,
     visxTooltip.tooltipData,
+    data,
+    seriesId,
+    series,
     snapToX,
     snapToY,
     xScale,
     yScale,
+    scheduleTooltipUpdate,
+    onHoverDateChange,
   ]);

160-212: Critical: Missing dependency in handleTooltip callback.

The handleTooltip callback invokes onHoverDateChange on line 200 but doesn't list it in the dependency array. This can result in stale closures where an outdated callback is invoked.

Apply this diff:

   }, [
     seriesId,
     data,
     xScale,
     yScale,
     series,
     defaultTooltipData,
     scheduleTooltipUpdate,
     tooltipSyncContext.setTooltipDate,
+    onHoverDateChange,
   ]);

176-179: Notify hover date change in fallback path.

When x0 is undefined, the code shows the default tooltip or hides it but doesn't invoke onHoverDateChange. For consistency, notify the callback in this fallback path.

Apply this diff:

       if (x0 === undefined) {
         console.log("x0 is undefined", { defaultTooltipData });
-        if (defaultTooltipData) visxTooltip.showTooltip(defaultTooltipData);
-        else visxTooltip.hideTooltip();
+        if (defaultTooltipData) {
+          visxTooltip.showTooltip(defaultTooltipData);
+          onHoverDateChange?.(defaultTooltipData.tooltipData.date);
+        } else {
+          visxTooltip.hideTooltip();
+          onHoverDateChange?.(null);
+        }
         return;
       }

223-231: Inconsistent hover date notification when showing default tooltip.

Line 229 invokes onHoverDateChange(null) even when the default tooltip is shown (line 227). The external UI will think no date is hovered despite a tooltip being visible with a specific date.

Apply this diff:

     hideTooltip: () => {
       tooltipSyncContext.setTooltipDate?.(null);

-      defaultTooltipData
-        ? visxTooltip.showTooltip(defaultTooltipData)
-        : visxTooltip.hideTooltip();
-      onHoverDateChange?.(null);
+      if (defaultTooltipData) {
+        visxTooltip.showTooltip(defaultTooltipData);
+        onHoverDateChange?.(defaultTooltipData.tooltipData.date);
+      } else {
+        visxTooltip.hideTooltip();
+        onHoverDateChange?.(null);
+      }
     },
🧹 Nitpick comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (2)

141-143: Unnecessary dependency in useMemo.

activeResource is listed in the dependency array but isn't used in the filter computation. This can cause unnecessary recalculations.

     ],
-    [activeResource, folders, domains],
+    [folders, domains],
   );

285-295: Missing scroll: false in groupBy toggle.

The other filter handlers use scroll: false to prevent unexpected page scrolling. This should be consistent.

           selectAction={(id) => queryParams({ set: { groupBy: id } })}
+          selectAction={(id) => queryParams({ set: { groupBy: id }, scroll: false })}
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 9e1fd00 and 2690621.

πŸ“’ Files selected for processing (2)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (6 hunks)
  • packages/ui/src/charts/use-tooltip.ts (8 hunks)
🧰 Additional context used
🧠 Learnings (2)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx
🧬 Code graph analysis (1)
packages/ui/src/charts/use-tooltip.ts (1)
packages/ui/src/charts/types.ts (1)
  • TimeSeriesDatum (9-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
πŸ”‡ Additional comments (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (5)

30-40: LGTM!

The color palette provides good visual distinction between groups with 10 colors cycling.


189-197: Nice optimization with O(1) date lookup.

The usageIndexByDate Map enables constant-time lookups during hover interactions, which is much better than linear search through the usage array.


201-219: Good use of RAF throttling for hover updates.

The requestAnimationFrame throttling and proper cleanup in useEffect prevents excessive re-renders during rapid mouse movements.


336-340: Tooltip sorting fix verified.

The sorting now correctly uses day-specific values (d.values[b.id] ?? 0) - (d.values[a.id] ?? 0) as addressed in the previous review.


410-488: Well-implemented group breakdown list.

Good patterns here:

  • NumberFlowGroup for smooth animated transitions
  • Safe percentage calculation with || 1 fallback
  • Proper loading skeleton state
  • Disabled state for items without valid IDs
packages/ui/src/charts/use-tooltip.ts (1)

72-112: LGTM! Clean frame-throttling implementation.

The requestAnimationFrame-based batching correctly prevents excessive React state updates during mousemove events while maintaining UI responsiveness.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (1)

53-62: Null/undefined groupBy values still handled with empty string fallback.

The code still uses ?? "" which adds empty strings to groupIds when d[groupBy] is null/undefined. This empty string is then passed to the Prisma query's id: { in: groupIds } clause.

Filter out null/undefined values explicitly:

-  const groupIds = [...new Set(response.data.map((d) => d[groupBy] ?? ""))];
+  const groupIds = [
+    ...new Set(
+      response.data
+        .map((d) => d[groupBy])
+        .filter((id): id is string => Boolean(id)),
+    ),
+  ];
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (1)

223-241: Color inconsistency: hoveredGroupsMeta assigns colors by day-specific index.

This assigns colorClassName based on the hovered day's group ordering, which could differ from the stable groupsMeta ordering. However, since colorClassName from hoveredGroupsMeta is never actually used (only total is accessed at line 473), this is effectively dead code.

Consider removing the unused colorClassName to avoid confusion:

   return Object.fromEntries(
-    dayUsage.groups.map((g, idx) => [
+    dayUsage.groups.map((g) => [
       g.id,
       {
         name: g.name,
-        colorClassName: BAR_COLORS[idx % BAR_COLORS.length],
         total: g.usage,
       },
     ]),
   );
🧹 Nitpick comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (2)

105-145: Unused dependency in useMemo.

activeResource is included in the dependency array but is not used within the memo body. This causes unnecessary recalculations when activeResource changes.

   ],
-  [activeResource, folders, domains],
+  [folders, domains],
 );

573-582: Modal content may clip without scroll.

Using overflow-hidden on a container with max-h-[70vh] could clip content if the BarList exceeds the viewport height without providing scroll access.

-          <div className="max-h-[70vh] overflow-hidden">
+          <div className="max-h-[70vh] overflow-y-auto">
πŸ“œ Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 2690621 and 4050bd0.

πŸ“’ Files selected for processing (3)
  • apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (2 hunks)
  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (6 hunks)
  • apps/web/ui/analytics/analytics-card.tsx (1 hunks)
βœ… Files skipped from review due to trivial changes (1)
  • apps/web/ui/analytics/analytics-card.tsx
🧰 Additional context used
🧠 Learnings (4)
πŸ““ Common learnings
Learnt from: TWilson023
Repo: dubinc/dub PR: 2872
File: apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/profile-details-form.tsx:180-189
Timestamp: 2025-09-24T15:50:16.414Z
Learning: TWilson023 prefers to keep security vulnerability fixes separate from refactoring PRs when the vulnerable code is existing and was only moved/relocated rather than newly introduced.
πŸ“š Learning: 2025-08-16T11:14:00.667Z
Learnt from: devkiran
Repo: dubinc/dub PR: 2754
File: apps/web/lib/partnerstack/schemas.ts:47-52
Timestamp: 2025-08-16T11:14:00.667Z
Learning: The PartnerStack API always includes the `group` field in partner responses, so the schema should use `.nullable()` rather than `.nullish()` since the field is never omitted/undefined.

Applied to files:

  • apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts
πŸ“š Learning: 2025-08-26T15:38:48.173Z
Learnt from: TWilson023
Repo: dubinc/dub PR: 2736
File: apps/web/lib/api/bounties/get-bounty-or-throw.ts:53-63
Timestamp: 2025-08-26T15:38:48.173Z
Learning: In bounty-related code, getBountyOrThrow returns group objects with { id } field (transformed from BountyGroup.groupId), while other routes working directly with BountyGroup Prisma records use the actual groupId field. This is intentional - getBountyOrThrow abstracts the join table details.

Applied to files:

  • apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts
πŸ“š Learning: 2025-10-15T01:05:43.266Z
Learnt from: steven-tey
Repo: dubinc/dub PR: 2958
File: apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx:432-457
Timestamp: 2025-10-15T01:05:43.266Z
Learning: In apps/web/app/app.dub.co/(dashboard)/[slug]/settings/members/page-client.tsx, defer refactoring the custom MenuItem component (lines 432-457) to use the shared dub/ui MenuItem component to a future PR, as requested by steven-tey.

Applied to files:

  • apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx
🧬 Code graph analysis (1)
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (2)
apps/web/lib/analytics/utils/get-start-end-dates.ts (1)
  • getStartEndDates (4-46)
packages/prisma/index.ts (1)
  • prisma (3-9)
πŸ”‡ Additional comments (3)
apps/web/app/api/workspaces/[idOrSlug]/billing/usage/route.ts (1)

80-99: Data transformation logic is well-structured.

The date-wise aggregation with per-group usage mapping is correctly implemented. The approach of extracting unique dates, fetching metadata, then rebuilding the structure provides clean, normalized output for the frontend.

apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx (2)

201-221: Good pattern: RAF-throttled hover updates.

The requestAnimationFrame-based throttling for hover state updates is a solid performance optimization. The cleanup in useEffect properly cancels pending frames on unmount.


468-549: Well-implemented grouped breakdown with deferred updates.

The use of useDeferredValue for hoveredGroupsMeta and hoveredTotalUsage keeps the chart tooltip responsive while allowing the breakdown list to catch up. The conditional rendering based on loading state with skeleton placeholders provides good UX.

@steven-tey steven-tey merged commit fad10fd into main Nov 27, 2025
7 of 8 checks passed
@steven-tey steven-tey deleted the stacked-usage branch November 27, 2025 00:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants