-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Stacked usage charts #3150
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Stacked usage charts #3150
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effortπ― 4 (Complex) | β±οΈ ~60 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touchesβ Failed checks (1 warning)
β Passed checks (2 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.
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
paramsdirectly intoURLSearchParamswill serialize anyundefinedvalues 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 withuniq(*).
uniq(*)counts unique rows based on all selected columns. Since you're selecting fromdub_links_metadata_latest FINAL, this effectively counts rows. If the intent is to count distinct links, consider usinguniq(link_id)orcount(*)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 FINALapps/web/ui/shared/simple-date-range-picker.tsx (1)
70-73: Redundant type assertion.The cast
(presets as string[])is unnecessary sincepresetsis already typed as(typeof DATE_RANGE_INTERVAL_PRESETS)[number][], which should be compatible withstring[].- ? 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: Avoidas 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 simplifiedWhen
groupBy === "domain":
groupIdsare derived fromd[groupBy]wheregroupByis"domain", so these are the Tinybirddomainfield values (slugs perusageResponse), not DBids.- The Prisma query uses
where: { projectId: workspace.id, id: { in: groupIds } }and latergroupMeta.find((g) => g.id === groupId), butgroupIdis a slug, so this find will almost always fail.- As a result,
namefalls back togroupId, 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 usename: groupId, or- If you actually want to look up by slug, change the query & lookup to use
sluginstead ofid.You might also want to filter out empty
groupIdsinstead of including""in theINlist andgroupsarray, 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 usageIn 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.usagehere is taken from the first data row (usage[0]), so the sort order reflects that first day rather than the hovered dayd. Since you already haved.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: InitializegroupsMetaso loading/error UI can actually render
groupsMetais always an object:const groupsMeta = useMemo( () => Object.fromEntries( usage?.[0]?.groups?.map(/* ... */) ?? [], ), [usage], );Even when
usageisundefined, this returns{}, which is truthy. That means thegroupsMeta ? (β¦) : (β¦)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 ifusagenever arrives.Also applies to: 315-391
π Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
π 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.tsxapps/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
valuesis provided, the picker displays controlled values butonChangestill updates URL params viaqueryParams(). This might cause parent state and URL params to diverge. Ifvaluesis intended to enable fully controlled mode, consider accepting anonChangecallback prop and conditionally skippingqueryParamsupdates.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/maxYcalculation correctly sums values for stacked bar charts, ensuring the y-axis accommodates the full stacked height. The dependency array update to includetypeis 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 behaviorThe added
groupBy,interval, optionalstart/end, and optionalfolder_id/domainfields match how the API route anduseUsagehook now construct and consume grouped usage data. Shapes look consistent and backward-compatible (no behavior change whengroupByis omitted).Also applies to: 18-19
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/plan-usage.tsx (2)
64-68: ConfirmignoreParams: truebehavior matches the intended caching semanticsPassing
ignoreParams: truetousePartnersCountchanges 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 updateduse-partners-countimplementation and doesnβt accidentally coalesce distinct queries.
334-335: UsageTabCard styling tweaks look good and keep logic unchangedThe 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 queriesThe memoized
{ start, end, interval }logic (interval taking precedence over explicit dates) and thegroupByderivation from URL params line up with the updated API route and zod schema:
- API is always called with either an
intervalor a concrete[start, end]pair, never both.groupByis 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 whengroupByis always set.Timezone is still passed using the browserβs local zone, which plays nicely with the existing
getStartEndDatesbehavior 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
There was a problem hiding this 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
π 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
subYearsimport now correctly uses the named export from the maindate-fnspackage, 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 ofgetStartEndDatesprovides 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: TheusageQuerySchemaalready includes all required fields; no action needed.Verification confirms that
usageQuerySchemainapps/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 forgroupByis correctly implemented and matches the code logic.The
usageQuerySchemadefined inapps/web/lib/zod/schemas/usage.ts(line 8) validatesgroupByto an enum of exactly["folder_id", "domain"], which directly corresponds to the conditional logic in the code. The schema is properly applied viausageQuerySchema.parse(searchParams)before the code uses thegroupByvalue, 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_latestpipe exists and is properly configured.The pipe definition at
./packages/tinybird/pipes/v3_usage_latest.pipeconfirms:
- β Pipe exists: Located in the repository at the expected path
- β Required parameters supported:
start,end,timezone,workspaceId(all explicitly defined in the SQL templates)resource(used in final SELECT to switch between events/links)- β Optional parameters supported:
groupBy(conditionally includes folder_id or domain)folderIdanddomain(conditionally filters events and links)- β Response schema matches:
- Returns
date(formatted DateTime)- Returns
value(uniq count of events or links)- Optionally returns
folder_idordomainwhen groupBy is specified- β Parameter extension is appropriate: Adding
workspaceIdtousageQuerySchemavia.extend()is necessary since the pipe requires it as a mandatory parameterThe implementation correctly passes all required and optional parameters, and the pipe's conditional logic aligns with how the route processes groupBy filtering.
There was a problem hiding this 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 expectationsRight 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 andseriesarray 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
seriesorder (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 computationThe stacking math looks correct, but there are a couple of maintainability/perf nits here:
- Line [58] initializes
barsas[] as any[], losing all type safety for the computed segments.- Line [59] recomputes
stackHeighton every iteration viaacc.reduce((sum, b) => sum + b.height, 0), even thoughstackHeightis just the running sum of heights youβre already computing.seriesStyles?.find(({ id }) => id === s.id)is run for every segment in every bucket; ifseriesStylesgrows, 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
MapforseriesStylesoutside thedata.maploop 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 whenxScale(d.date)is undefinedLine [52] uses:
const x = xScale(d.date) ?? 0;If, for whatever reason,
d.dateisnβt in the xβscaleβs domain, this will quietly render the bar atx = 0instead of skipping the datum or surfacing the issue. That can make data/domain mismatches hard to spot.If you expect
dataandxScaleto 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
π 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
There was a problem hiding this 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: AddonHoverDateChangeto the dependency array.The
onHoverDateChangecallback 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: AddonHoverDateChangeto the dependency array.The
onHoverDateChangecallback 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 debugconsole.logstatement.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 unusedactiveResourcefrom dependency array.
activeResourceis 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
π 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
Omitto excludeonHoverDateChangefromRequired<>and then re-adding it as optional inChartContextis 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 notifyingonHoverDateChangewhen showing the default tooltip.When the default tooltip is displayed on mount,
onHoverDateChangeis not invoked. If the intent is to sync external UI with the initially displayed datum, you should callonHoverDateChange(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
NumberFlowGroupfor 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
isValidatingto show reduced opacity during revalidation while keeping content visible provides good UX feedback without jarring layout shifts.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx
Show resolved
Hide resolved
There was a problem hiding this 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, andonHoverDateChangebut 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
handleTooltipcallback invokesonHoverDateChangeon 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
x0is undefined, the code shows the default tooltip or hides it but doesn't invokeonHoverDateChange. 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 inuseMemo.
activeResourceis 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: Missingscroll: falsein groupBy toggle.The other filter handlers use
scroll: falseto 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
π 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
usageIndexByDateMap 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
requestAnimationFramethrottling and proper cleanup inuseEffectprevents 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:
NumberFlowGroupfor smooth animated transitions- Safe percentage calculation with
|| 1fallback- 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.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/billing/usage-chart.tsx
Show resolved
Hide resolved
There was a problem hiding this 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 togroupIdswhend[groupBy]is null/undefined. This empty string is then passed to the Prisma query'sid: { 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:hoveredGroupsMetaassigns colors by day-specific index.This assigns
colorClassNamebased on the hovered day's group ordering, which could differ from the stablegroupsMetaordering. However, sincecolorClassNamefromhoveredGroupsMetais never actually used (onlytotalis accessed at line 473), this is effectively dead code.Consider removing the unused
colorClassNameto 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 inuseMemo.
activeResourceis included in the dependency array but is not used within the memo body. This causes unnecessary recalculations whenactiveResourcechanges.], - [activeResource, folders, domains], + [folders, domains], );
573-582: Modal content may clip without scroll.Using
overflow-hiddenon a container withmax-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
π 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 inuseEffectproperly cancels pending frames on unmount.
468-549: Well-implemented grouped breakdown with deferred updates.The use of
useDeferredValueforhoveredGroupsMetaandhoveredTotalUsagekeeps the chart tooltip responsive while allowing the breakdown list to catch up. The conditional rendering based onloadingstate with skeleton placeholders provides good UX.
Remaining considerations:
LEFT JOINof links metadata for each event query. Do we need to optimize this further?Screenshot:

Summary by CodeRabbit
New Features
UI/UX Improvements
βοΈ Tip: You can customize this high-level summary in your review settings.