Conversation
- Implemented loading of per-day email status counts for the last 30 days in the email overview. - Enhanced the ActivityBarChart component to track hovered data points, adjusting opacity for non-hovered bars. - Updated the StackedTooltip to display a 7-day average for email metrics. - Refactored the metrics page to utilize the new stacked data points and improved layout for better user experience.
- Introduced a new function to load monthly active users (MAU) from ClickHouse, aggregating user activity over the last 30 days. - Updated the auth overview to include the MAU metric. - Enhanced the metrics page to support hover interactions for visitors and revenue data, allowing users to switch between different chart views with fade transitions. - Added new data types for visitors and revenue hover charts to improve data representation in the dashboard.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds DesignAnalyticsCard UI primitives and infinite-scroll utilities; refactors dashboard metrics and email pages to use them; extends backend internal metrics route with auth/payments/email/analytics overview loaders (ClickHouse-backed with dev fallbacks); expands Prisma seed data (users/subscriptions/emails) and stabilizes e2e metrics tests via polling. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Dashboard Client
participant MetricsAPI as Metrics Route
participant Prisma as Prisma DB
participant ClickHouse as ClickHouse
participant Fallback as Dev Fallback Generator
Client->>MetricsAPI: GET /internal/metrics
MetricsAPI->>Prisma: Load auth, payments, email records
Prisma-->>MetricsAPI: DB rows
MetricsAPI->>ClickHouse: Query analytics/DAU/MAU and aggregates
alt ClickHouse responds
ClickHouse-->>MetricsAPI: Analytics aggregates
else ClickHouse missing/unavailable
MetricsAPI->>Fallback: generateDevDauSplit()/generateDevAnalyticsOverview()
Fallback-->>MetricsAPI: Synthetic aggregates
end
MetricsAPI->>MetricsAPI: Compose auth_overview, payments_overview, email_overview, analytics_overview
MetricsAPI-->>Client: JSON response with composed overviews
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ❌ 3❌ Failed checks (2 warnings, 1 inconclusive)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
- Added a `CustomDateRange` type and updated the `TimeRange` to include a 'custom' option for date filtering. - Implemented shared helper functions for parsing and normalizing date inputs to ensure accurate date handling in charts. - Updated various components, including `MetricsPage`, `line-chart`, and `metrics-page`, to support custom date ranges and maintain synchronization across the dashboard. - Improved scrollbar styling in `globals.css` for better user experience.
- Replaced instances of custom GlassCard components with the new DesignAnalyticsCard across various pages, including email drafts, email themes, and metrics pages. - Introduced DesignAnalyticsCard for improved chart rendering and tooltip handling, enhancing the overall dashboard interactivity. - Updated the DESIGN-GUIDE.md to include guidelines for using DesignAnalyticsCard and its associated components. - Added new demo data for analytics points to support the updated card functionality.
- Updated the `loadAnalyticsOverview` function to include parameters for anonymous user tracking. - Added queries to retrieve daily visitor counts and total unique visitors from ClickHouse. - Adjusted the metrics returned to include daily visitors and total revenue calculations based on visitor data. - Modified the `MetricsContent` component to handle revenue display logic based on available data, improving overall dashboard accuracy.
…s and revenue metrics - Added `showVisitors` and `showRevenue` props to the `ComposedAnalyticsChart` for better control over displayed metrics. - Updated logic to conditionally render visitor and revenue lines based on the new props. - Introduced `SetupAppPrompt` component in `metrics-page` to guide users in enabling analytics and payments features. - Adjusted `MetricsContent` to handle data visibility based on the availability of analytics and payments apps.
…itional rendering - Added `fullBleed` and `wrapHeaderInCard` props to the `PageLayout` component for improved layout flexibility. - Updated `ComposedAnalyticsChart` to utilize `useMemo` for tagged data points, enhancing performance and conditional rendering of visitor and revenue metrics based on new props. - Improved tooltip display logic in `ComposedAnalyticsChart` to handle visibility based on user preferences for visitors and revenue data.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Revamps the dashboard project overview (metrics page) into a more analytics-focused layout, backed by an expanded internal metrics API that returns cross-product aggregate groups (auth/payments/email/analytics) and shared UI primitives for chart cards and infinite-scroll lists.
Changes:
- Expand
/api/v1/internal/metricsto returnauth_overview,payments_overview,email_overview, andanalytics_overview, including dev-mode synthetic fallbacks for ClickHouse-dependent series. - Introduce
DesignAnalyticsCard(+ header/legend/infinite list helpers) and migrate multiple dashboard pages and the overview to use it. - Update e2e coverage for the internal metrics endpoint and extend dummy seed data for more realistic overview surfaces.
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
claude/CLAUDE-KNOWLEDGE.md |
Adds internal notes/Q&A for metrics endpoint expansion and overview chart behavior. |
apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts |
Stabilizes anonymous-exclusion testing and adds coverage for new aggregate groups. |
apps/dashboard/src/components/design-components/index.ts |
Re-exports the new analytics card module. |
apps/dashboard/src/components/design-components/analytics-card.tsx |
New shared analytics card shell + legend + infinite list hook/component. |
apps/dashboard/src/app/globals.css |
Adds global scrollbar styling. |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx |
Adds fullBleed and wrapHeaderInCard options for overview-style layout. |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx |
Migrates email page cards to DesignAnalyticsCard. |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx |
Migrates theme page cards to DesignAnalyticsCard and adds a shared section header. |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx |
Migrates to DesignAnalyticsCard and changes drafts page structure. |
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx |
Major redesign of overview layout, hero widget interactions, and new email/referrer cards. |
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/playground/page-client.tsx |
Adds playground controls/demo for DesignAnalyticsCard. |
apps/dashboard/DESIGN-GUIDE.md |
Documents DesignAnalyticsCard and updates guidance against local GlassCard/ChartCard clones. |
apps/backend/src/app/api/latest/internal/metrics/route.tsx |
Adds cross-product aggregates + DAU/team split series + MAU + analytics/email/payments overview loaders. |
apps/backend/prisma/seed.ts |
Adds more realistic dummy users/subscriptions/emails for overview breadth. |
Comments suppressed due to low confidence (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx:182
- The Active Theme header renders "Currently using …" twice (once near the left group and again after the "Active Theme" badge), which looks like an accidental duplication and will clutter the UI. Remove one of the duplicate spans so the header has a single source of truth for that text.
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4">
<SectionHeader icon={Palette} title="Active Theme" />
<span className="text-sm text-muted-foreground">
Currently using <span className="font-medium text-foreground">{selectedThemeData.displayName}</span>
</span>
</div>
<span className="text-xs font-semibold text-foreground uppercase tracking-wider">
Active Theme
</span>
<span className="text-sm text-muted-foreground">
Currently using <span className="font-medium text-foreground">{selectedThemeData.displayName}</span>
</span>
</div>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx
Show resolved
Hide resolved
apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/components/design-components/analytics-card.tsx
Outdated
Show resolved
Hide resolved
Greptile SummaryThis PR revamps the dashboard overview page with richer analytics, new KPI cards, animated hero charts, and a new What changed:
Remaining concerns:
Confidence Score: 4/5Not quite ready to merge — two new unbounded findMany queries can OOM on high-volume projects The UI work is solid and all prior P1 concerns have been resolved, but the new unbounded findMany for emailsByDayAndStatus and sessionReplay are genuine reliability risks at production scale, keeping the score at 4. apps/backend/src/app/api/latest/internal/metrics/route.tsx — specifically loadEmailOverview (emailsByDayAndStatus ~line 678) and loadAnalyticsOverview (sessionReplay.findMany ~line 942) Important Files Changed
Sequence DiagramsequenceDiagram
participant D as Dashboard Client
participant API as /internal/metrics
participant PG as Postgres
participant CH as ClickHouse
D->>API: GET /internal/metrics
API->>PG: count(projectUser), loadTotalUsers
API->>CH: loadDailyActiveUsers, loadUsersByCountry
API->>CH: loadDailyActiveUsersSplit (user IDs → bounded)
API->>PG: findMany(projectUser, {in: activeUserIds})
API->>PG: loadPaymentsOverview (subscriptions, invoices)
API->>PG: emailsByDayAndStatus ⚠️ unbounded 30d
API->>CH: loadAnalyticsOverview (pageViews, clicks)
API->>PG: sessionReplay.findMany ⚠️ unbounded 30d
API-->>D: 200 { auth_overview, payments_overview,\n email_overview, analytics_overview }
Prompt To Fix All With AIThis is a comment left during a code review.
Path: apps/backend/src/app/api/latest/internal/metrics/route.tsx
Line: 678-688
Comment:
**Unbounded `findMany` for 30-day email data can OOM**
`emailsByDayAndStatus` loads every `emailOutbox` row created in the last 30 days with no `take` limit. For a project sending bulk newsletters or notifications, this can easily pull millions of rows into Node.js heap and cause an OOM crash or a very long timeout — exactly the same pattern that was fixed in `loadDailyActiveUsersSplit`.
The per-day, per-status breakdown can be computed with a pure database aggregate instead:
```typescript
// Replace the findMany with a groupBy on (date bucket, simpleStatus)
const schema = await getPrismaSchemaForTenancy(tenancy);
const prisma = await getPrismaClientForTenancy(tenancy);
const rows = await prisma.$queryRaw<{ day: string, status: string, cnt: number }[]>`
SELECT DATE("createdAt") AS day, "simpleStatus" AS status, COUNT(*)::int AS cnt
FROM ${sqlQuoteIdent(schema)}."EmailOutbox"
WHERE "tenancyId" = ${tenancy.id}::UUID
AND "createdAt" >= ${thirtyDaysAgo}
GROUP BY DATE("createdAt"), "simpleStatus"
ORDER BY day
`;
```
This avoids loading individual rows entirely.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/internal/metrics/route.tsx
Line: 942-960
Comment:
**Unbounded `sessionReplay.findMany` — push avg calculation to the DB**
`sessionReplay.findMany` fetches every replay row from the last 30 days (`startedAt: { gte: since }`) with no `take` limit so that the average session duration can be computed in JavaScript. On a project with many active users this loads potentially thousands of replay rows into memory.
The average can be computed directly in Postgres, eliminating the `findMany` entirely:
```typescript
const schema = await getPrismaSchemaForTenancy(tenancy);
const avgResult = await prisma.$replica().$queryRaw<[{ avg_ms: number | null }]>`
SELECT AVG(EXTRACT(EPOCH FROM ("lastEventAt" - "startedAt")) * 1000) AS avg_ms
FROM ${sqlQuoteIdent(schema)}."SessionReplay"
WHERE "tenancyId" = ${tenancy.id}::UUID
AND "startedAt" >= ${since}
`;
const avgSessionSeconds = Number(((avgResult[0]?.avg_ms ?? 0) / 1000).toFixed(1));
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/internal/metrics/route.tsx
Line: 523
Comment:
**`refund_cents` is always `0` — refunds never shown in chart**
Every entry in `daily_revenue` hardcodes `refund_cents: 0`. If the dashboard chart subtracts refunds from gross revenue to show net revenue, the line will always equal gross revenue, silently overstating earnings for any project that has issued refunds.
Either query refunded invoices and aggregate them by day (similar to how `paidInvoicesInRange` is built), or document that this is a known placeholder and note that the chart shows gross-only revenue.
```suggestion
dailyRevenue.push({ date: key, new_cents: revenueByDay.get(key) ?? 0, refund_cents: refundByDay.get(key) ?? 0 });
```
How can I resolve this? If you propose a fix, please make it concise.Greploops — Automatically fix all review issues by running Reviews (2): Last reviewed commit: "Enhance metrics calculations and UI comp..." | Re-trigger Greptile |
| dailySubscriptions.push({ date: key, activity: recentByDay.get(key) ?? 0 }); | ||
| } | ||
|
|
||
| const estimatedMrrCents = activeSubscriptionCount * 10_000; |
There was a problem hiding this comment.
Hardcoded MRR estimate is misleading
estimatedMrrCents = activeSubscriptionCount * 10_000 hardcodes a fixed $100 per active subscription regardless of the actual plan price, quantity, billing interval (monthly vs annual), or currency. This value is exposed as mrr_cents in the API response and rendered on the overview dashboard.
Even for seed data, the value will be wildly wrong for any project whose subscriptions aren't exactly $100/month. It would be better to either sum invoiceRevenue._sum.amountTotal (actual billed amount) as a proxy, or label the field as estimated_mrr_cents with a note in the API docs that it's a rough estimate.
apps/dashboard/src/components/design-components/analytics-card.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx (1)
169-181:⚠️ Potential issue | 🟡 MinorRemove the duplicated header text in the active-theme card.
This still renders the old
Active Theme/Currently using ...spans next to the newSectionHeader, so the card header shows the same information twice.Proposed fix
<div className="flex items-center justify-between gap-4"> <div className="flex items-center gap-4"> <SectionHeader icon={Palette} title="Active Theme" /> <span className="text-sm text-muted-foreground"> Currently using <span className="font-medium text-foreground">{selectedThemeData.displayName}</span> </span> </div> - <span className="text-xs font-semibold text-foreground uppercase tracking-wider"> - Active Theme - </span> - <span className="text-sm text-muted-foreground"> - Currently using <span className="font-medium text-foreground">{selectedThemeData.displayName}</span> - </span> </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx around lines 169 - 181, The card header currently renders the same title and subtitle twice; keep the SectionHeader (SectionHeader with icon Palette) and the first subtitle that references selectedThemeData.displayName, and remove the duplicate spans that render "Active Theme" (the uppercase span) and the second "Currently using …" span. Update the JSX in the active-theme card to only include SectionHeader and the single subtitle using selectedThemeData.displayName so the header text is not duplicated.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (1)
26-36:⚠️ Potential issue | 🔴 CriticalImport
ElementTypehere instead of referencing the undefinedReact.ElementType.The file imports specific exports from react (line 18:
useEffect, useMemo, useState) but does not import React as a namespace, makingReact.ElementTypeinaccessible and causing a type-checking failure.Additionally,
ArrowSquareOutis used at line 112 but is missing from the@phosphor-icons/reactimport on line 10.Proposed fix
-import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, type ElementType } from "react"; +import { CheckCircle, Envelope, HardDrive, Sliders, WarningCircleIcon, XCircle, XIcon, ArrowSquareOut } from "@phosphor-icons/react"; @@ -function SectionHeader({ icon: Icon, title }: { icon: React.ElementType, title: string }) { +function SectionHeader({ icon: Icon, title }: { icon: ElementType, title: string }) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx around lines 26 - 36, The SectionHeader component uses React.ElementType which is undefined because React isn't imported as a namespace; update the imports to import { ElementType, useEffect, useMemo, useState } from 'react' and change the prop type to use ElementType (i.e., function SectionHeader({ icon: Icon, title }: { icon: ElementType, title: string })), and also add the missing ArrowSquareOut symbol to the `@phosphor-icons/react` import so ArrowSquareOut is available where it’s used.
🧹 Nitpick comments (2)
apps/dashboard/src/app/globals.css (1)
134-141: Consider consolidating adjacent universal selector rules.There are two separate
* { }rule blocks (lines 134-136 and 138-141). These can be combined into a single block for cleaner CSS organization.♻️ Suggested consolidation
`@layer` base { * { `@apply` border-border; - } - - * { scrollbar-width: thin; scrollbar-color: hsl(var(--foreground) / 0.22) transparent; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/globals.css` around lines 134 - 141, Two adjacent universal selector blocks should be consolidated: merge the two separate "* { `@apply` border-border; }" and "* { scrollbar-width: thin; scrollbar-color: hsl(var(--foreground) / 0.22) transparent; }" into a single universal selector block so all global rules for "*" are defined in one place (preserve the existing properties and their order) — update the CSS around the selectors that reference "*" accordingly.apps/dashboard/src/components/design-components/analytics-card.tsx (1)
237-240: Keep these refs nullable instead of casting awaynull.The casts hide the real
useRef(...).currenttype and then force you to cast back to nullable again at Lines 253-254. ReturningRefObject<HTMLDivElement | null>keeps the hook honest and avoids bypassing the type system.Proposed fix
export type InfiniteListWindow = { visibleCount: number, - scrollRef: React.RefObject<HTMLDivElement>, - sentinelRef: React.RefObject<HTMLDivElement>, + scrollRef: React.RefObject<HTMLDivElement | null>, + sentinelRef: React.RefObject<HTMLDivElement | null>, hasMore: boolean, }; @@ - const scrollRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>; - const sentinelRef = useRef<HTMLDivElement>(null) as React.RefObject<HTMLDivElement>; + const scrollRef = useRef<HTMLDivElement | null>(null); + const sentinelRef = useRef<HTMLDivElement | null>(null); @@ - const root = scrollRef.current as HTMLDivElement | null; - const target = sentinelRef.current as HTMLDivElement | null; + const root = scrollRef.current; + const target = sentinelRef.current;As per coding guidelines, "Do NOT use
as/any/type casts or anything else to bypass the type system unless you specifically asked the user about it."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/components/design-components/analytics-card.tsx` around lines 237 - 240, The refs scrollRef and sentinelRef are being cast to non-nullable React.RefObject<HTMLDivElement>, hiding their real nullable type; remove the "as React.RefObject<HTMLDivElement>" casts and declare them as nullable refs (e.g., useRef<HTMLDivElement | null>(null)) so their .current remains HTMLDivElement | null, and then adjust any usage (e.g., places that do non-null assertions) to handle the nullable type safely instead of casting back.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/prisma/seed.ts`:
- Around line 2102-2146: The seed is non-idempotent because you call
generateUuid() into bulkId for each upsert (emailOutbox.upsert) so every run
inserts new rows; replace generateUuid() with a deterministic id derived from
tenancyId, dayBack (createdAt date) and emailBulkIndex (e.g. a stable hash/UUID
v5 or deterministic string) so the same bulkId is produced across runs, and keep
using that deterministic bulkId wherever bulkId is referenced
(renderedByWorkerId, id, startedRenderingAt, etc.) to make the bulk email
seeding idempotent.
- Line 590: The daysAgo helper currently computes timestamps relative to
Date.now(), causing produced hours to vary with seed run time; update the
daysAgo function (and any similar helper around line with the duplicate at 1892)
to anchor calculation to midnight of "today" first (create a Date at
start-of-day with hours/minutes/seconds/ms zeroed), subtract d days from that
baseline, then add the h hour offset so daysAgo(d, h) reliably returns that
day's start plus h hours and never lands in the future.
In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx`:
- Around line 855-859: The response is using placeholder zeros for revenue;
instead use the real values from replayResult (e.g.,
replayResult.totalRevenueCents and its daily revenue series) when building the
payload in route.tsx. Replace the hard-coded total_revenue_cents: 0 with
replayResult.totalRevenueCents and construct daily_revenue by mapping
replayResult.dailyRevenue (or the equivalent array on replayResult) into the
expected bucket shape rather than filling zeros; apply the same fix to the
similar block referenced around lines 914-927 so both endpoints return real
invoice-derived revenue data.
- Around line 1113-1124: The return value double-counts anonymous users because
unverified_users is computed as filteredTotal - verifiedUsers while
anonymous_users is added separately; update the unverified_users calculation in
the route (replace the current expression that uses filteredTotal,
verifiedUsers, includeAnonymous) to subtract anonymousUsers when
includeAnonymous is true so unverified_users = filteredTotal - verifiedUsers -
(includeAnonymous ? anonymousUsers : 0); adjust the expression near
filteredTotal/includeAnonymous/verifiedUsers/anonymousUsers in the route.tsx
return block accordingly.
- Around line 473-477: The recentSubscriptions query is capped with take: 30
which causes daily_subscriptions to undercount when more than 30 signups occur
in the window; update the prisma.subscription.findMany used to build
recentSubscriptions to filter by the 30-day window (e.g., where.createdAt >=
windowStart) and remove the hard cap (remove take: 30) or implement proper
pagination to retrieve all subscriptions in that window so the histogram
(daily_subscriptions) uses the full dataset; ensure the same fix is applied to
the other query block referenced (the block that builds the series at the later
location).
In
`@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/playground/page-client.tsx:
- Around line 479-483: The generated legend in the Playground is out of sync
with the preview: ensure the demoLegendItems array (keys "new", "retained",
"reactivated") is fully represented in any code the Playground emits or copies;
update the snippet-generation logic (the function that builds the copied
preview/code snippet, e.g., generateCodeSnippet/handleCopy in page-client.tsx)
to iterate over demoLegendItems and include all items (including "reactivated")
so the copied snippet matches the rendered preview and keep this behavior
consistent wherever the preview is composed.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx:
- Around line 1136-1138: The hero revenueTotal currently falls back to lifetime
payments.revenue_cents when the selected range sums to 0; update revenueTotal
logic so it only uses totalRevenueCentsInRange when > 0 and does not use
payments.revenue_cents as a fallback. Specifically, modify the expression that
sets revenueTotal (referencing revenueTotal, paymentsEnabled,
formatUsdFromCents, totalRevenueCentsInRange, and payments.revenue_cents) to
return formatUsdFromCents(totalRevenueCentsInRange) when
totalRevenueCentsInRange > 0, otherwise return "—" (and keep "—" when
paymentsEnabled is false), removing the fallback to payments.revenue_cents.
- Around line 216-235: The pill currently only triggers chart changes via
onMouseEnter, leaving keyboard and touch users unable to switch modes; update
the HeroInChartPill component to also call the same handler onFocus and onClick,
and clear the state onBlur by introducing and using an onMouseLeave (or
onDeactivate) prop. Concretely: add an onMouseLeave?: () => void prop to the
HeroInChartPill signature, pass that into the button's onMouseLeave and onBlur,
add onFocus={onMouseEnter} and onClick={onMouseEnter}, and consider setting an
accessibility state like aria-pressed={isHovered} so screen reader/keyboard
users get feedback.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx:
- Around line 20-31: The type error is caused by using the undefined React
namespace in the SectionHeader signature (React.ElementType); update the import
and type to use ElementType directly by importing it from "react" (e.g., import
type { ElementType } from "react") and change the prop type to use ElementType
(the SectionHeader function and its icon prop/Icon variable) so the type
resolves correctly.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx:
- Around line 23-34: The SectionHeader prop type uses React.ElementType but
React isn't imported as a namespace; update the component to import ElementType
from "react" and use it in the props type (e.g., change the signature to use
ElementType for the icon prop) and adjust the file's React imports to include
ElementType so TypeScript strict mode can resolve the type for SectionHeader.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 93-118: EmulatorModeCard references the ArrowSquareOut icon but it
isn't imported, causing a compilation error; fix this by adding ArrowSquareOut
to the existing icon import list (the same place HardDrive is imported) so
ArrowSquareOut is available to the EmulatorModeCard component and used in the
Button's JSX.
In `@apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts`:
- Around line 332-343: The test currently only asserts the shape of
analytics_overview.daily_revenue; seed at least one paid invoice in the test
setup and then assert that analytics_overview.daily_revenue contains a non-zero
entry for the expected day and that the summed total matches the seeded invoice
amount (use the same date bucketing used by the API), updating the assertions
around analytics_overview.daily_revenue and analytics_overview.visitors/visitors
totals to verify the populated series rather than just Array.isArray checks.
- Around line 17-18: The polling helper returns early based only on
totals/countries and is_anonymous, which allows it to false-pass when
recently_registered/recently_active ordering changes; modify the poll condition
to also verify the baseline ID lists by comparing baselineRecentlyRegisteredIds
and baselineRecentlyActiveIds against the current
metricsResponse.body.recently_registered and .recently_active (ensure both
length and contents/top-N order match) before allowing the early return so the
baseline ID assertions (also referenced in the block around lines 31-37) cannot
be skipped.
In `@claude/CLAUDE-KNOWLEDGE.md`:
- Around line 127-128: Update the doc entry to point to the correct component
location: replace the erroneous path
`packages/dashboard/src/components/design-components/analytics-card.tsx` with
`apps/dashboard/src/components/design-components/analytics-card.tsx`; mention
that this file exports DesignAnalyticsCard, DesignAnalyticsCardHeader,
DesignChartLegend, useInfiniteListWindow, and DesignInfiniteScrollList and that
the page-local ChartCard wrapper in line-chart.tsx and GlassCard clones in email
pages were replaced with DesignAnalyticsCard.
---
Outside diff comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx:
- Around line 169-181: The card header currently renders the same title and
subtitle twice; keep the SectionHeader (SectionHeader with icon Palette) and the
first subtitle that references selectedThemeData.displayName, and remove the
duplicate spans that render "Active Theme" (the uppercase span) and the second
"Currently using …" span. Update the JSX in the active-theme card to only
include SectionHeader and the single subtitle using
selectedThemeData.displayName so the header text is not duplicated.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 26-36: The SectionHeader component uses React.ElementType which is
undefined because React isn't imported as a namespace; update the imports to
import { ElementType, useEffect, useMemo, useState } from 'react' and change the
prop type to use ElementType (i.e., function SectionHeader({ icon: Icon, title
}: { icon: ElementType, title: string })), and also add the missing
ArrowSquareOut symbol to the `@phosphor-icons/react` import so ArrowSquareOut is
available where it’s used.
---
Nitpick comments:
In `@apps/dashboard/src/app/globals.css`:
- Around line 134-141: Two adjacent universal selector blocks should be
consolidated: merge the two separate "* { `@apply` border-border; }" and "* {
scrollbar-width: thin; scrollbar-color: hsl(var(--foreground) / 0.22)
transparent; }" into a single universal selector block so all global rules for
"*" are defined in one place (preserve the existing properties and their order)
— update the CSS around the selectors that reference "*" accordingly.
In `@apps/dashboard/src/components/design-components/analytics-card.tsx`:
- Around line 237-240: The refs scrollRef and sentinelRef are being cast to
non-nullable React.RefObject<HTMLDivElement>, hiding their real nullable type;
remove the "as React.RefObject<HTMLDivElement>" casts and declare them as
nullable refs (e.g., useRef<HTMLDivElement | null>(null)) so their .current
remains HTMLDivElement | null, and then adjust any usage (e.g., places that do
non-null assertions) to handle the nullable type safely instead of casting back.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d6cd1512-b7f3-4be6-b124-0e3098b510c8
⛔ Files ignored due to path filters (1)
apps/e2e/tests/backend/endpoints/api/v1/__snapshots__/internal-metrics.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (15)
apps/backend/prisma/seed.tsapps/backend/src/app/api/latest/internal/metrics/route.tsxapps/dashboard/DESIGN-GUIDE.mdapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/playground/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsxapps/dashboard/src/app/globals.cssapps/dashboard/src/components/design-components/analytics-card.tsxapps/dashboard/src/components/design-components/index.tsapps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.tsclaude/CLAUDE-KNOWLEDGE.md
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
| // Analytics overview shape (may have empty arrays for sparse projects) | ||
| const analyticsOverview = response.body.analytics_overview; | ||
| expect(Array.isArray(analyticsOverview.daily_page_views)).toBe(true); | ||
| expect(Array.isArray(analyticsOverview.daily_clicks)).toBe(true); | ||
| expect(typeof analyticsOverview.total_replays).toBe('number'); | ||
| expect(typeof analyticsOverview.recent_replays).toBe('number'); | ||
| // Fields used by visitors/revenue hover charts | ||
| expect(Array.isArray(analyticsOverview.daily_visitors)).toBe(true); | ||
| expect(Array.isArray(analyticsOverview.daily_revenue)).toBe(true); | ||
| expect(typeof analyticsOverview.visitors).toBe('number'); | ||
| expect(Array.isArray(analyticsOverview.top_referrers)).toBe(true); | ||
| }); |
There was a problem hiding this comment.
Assert a populated revenue series, not just the field shape.
This only verifies that analytics_overview.daily_revenue exists, so an all-zero or misbucketed series still passes even though the overview charts depend on it. Seed at least one paid invoice and assert the expected day/total.
As per coding guidelines: **/apps/e2e/**/*.{ts,tsx}: ALWAYS add new E2E tests when you change the API or SDK interface. Generally, err on the side of creating too many tests.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts` around
lines 332 - 343, The test currently only asserts the shape of
analytics_overview.daily_revenue; seed at least one paid invoice in the test
setup and then assert that analytics_overview.daily_revenue contains a non-zero
entry for the expected day and that the summed total matches the seeded invoice
amount (use the same date bucketing used by the API), updating the assertions
around analytics_overview.daily_revenue and analytics_overview.visitors/visitors
totals to verify the populated series rather than just Array.isArray checks.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 92-118: EmulatorModeCard is implemented but never used; either
remove the EmulatorModeCard function or render it conditionally when the
emulator flag is set (use the existing isLocalEmulator boolean) alongside
EmailServerCard and EmailLogCard in the page render; to implement conditional
rendering, add {isLocalEmulator && <EmulatorModeCard />} in the parent
component's JSX where EmailServerCard and EmailLogCard are rendered, or delete
the EmulatorModeCard declaration and any related imports if you choose to remove
it.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c510a186-022f-43a1-8722-219996eff01f
📒 Files selected for processing (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
- Updated `daysAgo` function in `seedDummyUsers` and `seedDummyEmails` to set hours correctly and improve date handling. - Refactored metrics loading in `loadDailyActiveUsersSplit` and `loadDailyActiveTeamsSplit` to streamline database queries and enhance performance. - Improved handling of active user and team IDs to ensure accurate data retrieval. - Added tooltip escape styles directly in the `DesignAnalyticsCard` component for better tooltip visibility. - Introduced a draft history section in the email drafts page to display sent drafts, enhancing user experience.
There was a problem hiding this comment.
Actionable comments posted: 8
♻️ Duplicate comments (1)
apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts (1)
290-347:⚠️ Potential issue | 🟡 MinorSeed real data for the new aggregate groups, not just their shapes.
These assertions only prove that the fields exist. Empty or misbucketed
daily_revenue/daily_subscriptions/daily_emailsarrays still pass, which leaves the new overview contract largely untested.As per coding guidelines:
**/apps/e2e/**/*.{ts,tsx}: ALWAYS add new E2E tests when you change the API or SDK interface. Generally, err on the side of creating too many tests.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts` around lines 290 - 347, The test "should return cross-product aggregates in the metrics response" only checks field shapes; seed real data before calling niceBackendFetch so daily_revenue, daily_subscriptions, daily_emails (and related aggregates like active_subscription_count, total_one_time_purchases, total_emails, daily_visitors) contain meaningful entries: after Project.createAndSwitch(...) create users (verified/unverified/anonymous), create at least one subscription and one one-time purchase, send at least one email event, and emit analytics events/pageviews so daily_* arrays are non-empty; then call niceBackendFetch("/api/v1/internal/metrics", { accessType: 'admin' }) and add assertions that the daily_* arrays contain expected lengths/values and that counts (e.g., payments_overview.active_subscription_count, payments_overview.total_one_time_purchases, email_overview.total_emails, analytics_overview.daily_revenue) reflect the seeded data.
🧹 Nitpick comments (4)
apps/backend/src/app/api/latest/internal/metrics/route.tsx (4)
949-974: Same silent-swallow pattern for analytics overview.Same concern as
loadMonthlyActiveUsers—all ClickHouse errors are caught and the function returns an empty fallback. The inline comment "Analytics may not be enabled for all projects" provides rationale, which helps.If this is the accepted pattern for optional ClickHouse-dependent features, consider extracting a shared helper (e.g.,
withClickHouseFallback) to standardize this behavior and make the intent explicit.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx` around lines 949 - 974, The analytics overview catch block silently swallows ClickHouse errors and returns an empty fallback (mirroring loadMonthlyActiveUsers); extract and use a shared helper (e.g., withClickHouseFallback) that centralizes the try/catch, calls captureError/StackAssertionError with context (projectId, branchId, and a descriptive key like "internal-metrics-analytics-overview-fallback"), and returns the standardized empty overview payload; update the current catch to delegate to that helper and reuse it from loadMonthlyActiveUsers to make the fallback behavior explicit and consistent.
1230-1246: Dev-mode fallback logic is sound but could use type consistency.The dev fallback generation for empty ClickHouse data is a good UX improvement. Minor suggestion: the inline type annotation at line 1232
(d: { activity: number })could reference the existingDataPoints[number]or be inferred fromauthOverview.daily_active_users_split.total.- const dauSplitIsEmpty = authOverview.daily_active_users_split.total.every( - (d: { activity: number }) => d.activity === 0 - ); + const dauSplitIsEmpty = authOverview.daily_active_users_split.total.every( + (d) => d.activity === 0 + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx` around lines 1230 - 1246, The inline type annotation (d: { activity: number }) used in the dauSplitIsEmpty check should use the existing type for the array items to ensure consistency—replace it with the appropriate union/indexed type (e.g., DataPoints[number] or the actual type exported for authOverview.daily_active_users_split.total) or remove the explicit annotation to let TypeScript infer the type from authOverview.daily_active_users_split.total; update the predicate in the dauSplitIsEmpty declaration (which references authOverview.daily_active_users_split.total and the variable dauSplitIsEmpty) to use that canonical type so the check stays type-safe and consistent with generateDevDauSplit/finalAuthOverview logic.
1243-1246: Type cast ontop_referrerscould be avoided.The cast
as { referrer: string, visitors: number }[]suggests the return type ofloadAnalyticsOverviewisn't fully typed. Consider adding explicit return type annotations toloadAnalyticsOverviewto avoid the cast here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx` around lines 1243 - 1246, The cast on analyticsOverview.top_referrers indicates loadAnalyticsOverview lacks a precise return type; update the loadAnalyticsOverview function signature to return a properly typed object (e.g., an interface/type that includes top_referrers: {referrer: string; visitors: number}[]) so callers like the code computing referrersEmpty can omit the inline cast, then remove the "as { referrer: string, visitors: number }[]" cast in the referrersEmpty computation and rely on the new return type; ensure any internal parsing/mapping in loadAnalyticsOverview constructs top_referrers with those exact field names and types so TypeScript will infer them for finalAnalyticsOverview/generateDevAnalyticsOverview usage.
458-540: Inconsistent error handling across loaders.
loadPaymentsOverview(andloadEmailOverviewbelow) perform multiple Prisma queries without try-catch, whileloadMonthlyActiveUsersandloadAnalyticsOverviewwrap ClickHouse calls in try-catch withcaptureErrorfallbacks.If a Prisma query fails here, the entire
/internal/metricsrequest will fail. If this inconsistency is intentional (Prisma failures should propagate but ClickHouse failures should degrade gracefully), consider adding a brief comment explaining why.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx` around lines 458 - 540, The loaders loadPaymentsOverview and loadEmailOverview call multiple Prisma queries without error handling, which is inconsistent with other loaders (e.g., loadMonthlyActiveUsers and loadAnalyticsOverview) that catch ClickHouse errors and call captureError; wrap the Prisma calls and subsequent processing in a try-catch inside loadPaymentsOverview (and mirror in loadEmailOverview) around getPrismaClientForTenancy(...) and the Promise.all(...) block, call captureError(error, { source: 'loadPaymentsOverview', tenancyId: tenancy.id }) in the catch, and return a safe fallback object matching the loader's return shape (zeros/empty maps/arrays) so the /internal/metrics endpoint degrades gracefully; alternatively, if propagation is intentional, add a short comment in loadPaymentsOverview and loadEmailOverview explaining why Prisma errors should bubble up while ClickHouse errors are degraded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx:
- Around line 83-95: The function useMetricsOrThrow currently returns an untyped
value so callers mask missing fields with `?? {}`; define a proper TypeScript
interface (e.g., MetricsContract) describing the expected shape (groups, arrays,
counts, etc.), change useMetricsOrThrow to assert and return that
MetricsContract (use Reflect.get(adminApp, stackAppInternalsSymbol) to locate
internals and internals.useMetrics to call), and add a runtime validation step
inside useMetricsOrThrow that checks required top-level keys and throws a clear
Error if keys are missing or the types are wrong; then remove the `?? {}` / `??
[]` fallbacks in the widgets and rely on the typed/validated result so contract
breaks fail loud.
- Around line 652-658: The deliverability counts in MetricsContent can be
undefined because deliverabilityStatus may be an empty object, causing items'
count values and the computed total to become NaN; update the items creation and
total computation to default each count to 0 (e.g., use nullish coalescing or
Number fallback when building items for Delivered/Bounced/In Progress/Error) and
compute total from those normalized numeric counts so that total and subsequent
calls like item.count.toLocaleString() never throw when data is absent.
- Around line 491-497: The file references React.ElementType in the type for
emailStatusConfig but does not import React, causing TS to fail; add an explicit
type import like "import type { ElementType } from 'react'" at the top and
update the type usage to ElementType (e.g., change React.ElementType to
ElementType) so the emailStatusConfig declaration compiles.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx:
- Around line 371-396: The new history section replaced the existing
HistoryDraftCard (which contains the delete dialog) with plain Buttons, removing
the delete flow; restore deletion by replacing the mapped Buttons with the
existing HistoryDraftCard component for each historyDraft (use the same map over
historyDrafts), passing the draft object and wiring the onOpen and onDelete
handlers (e.g., reuse handleOpenHistoryDraft for opening and the same delete
handler used by HistoryDraftCard) so the delete dialog and behavior are
preserved while keeping the surrounding DesignAnalyticsCard and layout.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 106-112: The Button's onClick uses
getPublicEnvVar('NEXT_PUBLIC_STACK_INBUCKET_WEB_URL') without checking for
undefined, which can open "undefined/monitor"; update the code around the Button
(the onClick handler and Button props) to first read const inbucketUrl =
getPublicEnvVar('NEXT_PUBLIC_STACK_INBUCKET_WEB_URL') and then either (a)
disable the CTA when inbucketUrl is falsy by adding disabled and an explanatory
title/tooltip on the Button, or (b) fail loudly in the handler by throwing or
logging a clear error if inbucketUrl is falsy before calling window.open;
reference the Button component and getPublicEnvVar call to locate where to add
this guard.
In `@apps/dashboard/src/app/globals.css`:
- Around line 135-137: The stylelint failure is caused by missing blank line
between the `@apply` declaration and subsequent declarations in the same ruleset;
edit the ruleset containing "@apply border-border;" and insert a single empty
line before "scrollbar-width: thin;" (thus separating the `@apply` from the
following declarations) so the declaration-empty-line-before rule is satisfied.
In `@apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts`:
- Around line 349-380: The test "should return correct auth_overview breakdown
including teams" never creates a team, so add a team creation step after
Project.createAndSwitch (e.g., call Team.create or a project-team helper
equivalent) so the project has at least one team, and then assert that
authOverview.total_teams is >= 1; update the test to call Team.create (or
Team.createAndSet / Team.createAndSwitch depending on available helper) before
the niceBackendFetch and add
expect(authOverview.total_teams).toBeGreaterThanOrEqual(1) alongside the
existing assertions.
In `@claude/CLAUDE-KNOWLEDGE.md`:
- Around line 130-131: Update the documentation to remove the unnecessary cast
recommendation: delete the advice to cast the result of useRef (the snippet
suggesting "as React.RefObject<HTMLDivElement>") and replace it with guidance
that useRef<HTMLDivElement>(null) is already correctly typed for JSX ref props
in React 19 + TypeScript; reference the useRef<HTMLDivElement>(null) usage and
mention that callers should not bypass types with an "as" cast and should
instead perform normal null checks on ref.current in effects to satisfy
`@typescript-eslint` rules.
---
Duplicate comments:
In `@apps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.ts`:
- Around line 290-347: The test "should return cross-product aggregates in the
metrics response" only checks field shapes; seed real data before calling
niceBackendFetch so daily_revenue, daily_subscriptions, daily_emails (and
related aggregates like active_subscription_count, total_one_time_purchases,
total_emails, daily_visitors) contain meaningful entries: after
Project.createAndSwitch(...) create users (verified/unverified/anonymous),
create at least one subscription and one one-time purchase, send at least one
email event, and emit analytics events/pageviews so daily_* arrays are
non-empty; then call niceBackendFetch("/api/v1/internal/metrics", { accessType:
'admin' }) and add assertions that the daily_* arrays contain expected
lengths/values and that counts (e.g.,
payments_overview.active_subscription_count,
payments_overview.total_one_time_purchases, email_overview.total_emails,
analytics_overview.daily_revenue) reflect the seeded data.
---
Nitpick comments:
In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx`:
- Around line 949-974: The analytics overview catch block silently swallows
ClickHouse errors and returns an empty fallback (mirroring
loadMonthlyActiveUsers); extract and use a shared helper (e.g.,
withClickHouseFallback) that centralizes the try/catch, calls
captureError/StackAssertionError with context (projectId, branchId, and a
descriptive key like "internal-metrics-analytics-overview-fallback"), and
returns the standardized empty overview payload; update the current catch to
delegate to that helper and reuse it from loadMonthlyActiveUsers to make the
fallback behavior explicit and consistent.
- Around line 1230-1246: The inline type annotation (d: { activity: number })
used in the dauSplitIsEmpty check should use the existing type for the array
items to ensure consistency—replace it with the appropriate union/indexed type
(e.g., DataPoints[number] or the actual type exported for
authOverview.daily_active_users_split.total) or remove the explicit annotation
to let TypeScript infer the type from
authOverview.daily_active_users_split.total; update the predicate in the
dauSplitIsEmpty declaration (which references
authOverview.daily_active_users_split.total and the variable dauSplitIsEmpty) to
use that canonical type so the check stays type-safe and consistent with
generateDevDauSplit/finalAuthOverview logic.
- Around line 1243-1246: The cast on analyticsOverview.top_referrers indicates
loadAnalyticsOverview lacks a precise return type; update the
loadAnalyticsOverview function signature to return a properly typed object
(e.g., an interface/type that includes top_referrers: {referrer: string;
visitors: number}[]) so callers like the code computing referrersEmpty can omit
the inline cast, then remove the "as { referrer: string, visitors: number }[]"
cast in the referrersEmpty computation and rely on the new return type; ensure
any internal parsing/mapping in loadAnalyticsOverview constructs top_referrers
with those exact field names and types so TypeScript will infer them for
finalAnalyticsOverview/generateDevAnalyticsOverview usage.
- Around line 458-540: The loaders loadPaymentsOverview and loadEmailOverview
call multiple Prisma queries without error handling, which is inconsistent with
other loaders (e.g., loadMonthlyActiveUsers and loadAnalyticsOverview) that
catch ClickHouse errors and call captureError; wrap the Prisma calls and
subsequent processing in a try-catch inside loadPaymentsOverview (and mirror in
loadEmailOverview) around getPrismaClientForTenancy(...) and the
Promise.all(...) block, call captureError(error, { source:
'loadPaymentsOverview', tenancyId: tenancy.id }) in the catch, and return a safe
fallback object matching the loader's return shape (zeros/empty maps/arrays) so
the /internal/metrics endpoint degrades gracefully; alternatively, if
propagation is intentional, add a short comment in loadPaymentsOverview and
loadEmailOverview explaining why Prisma errors should bubble up while ClickHouse
errors are degraded.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 3abd3c12-5b7c-4831-b74c-37094df1458a
📒 Files selected for processing (10)
apps/backend/prisma/seed.tsapps/backend/src/app/api/latest/internal/metrics/route.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-themes/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsxapps/dashboard/src/app/globals.cssapps/dashboard/src/components/design-components/analytics-card.tsxapps/e2e/tests/backend/endpoints/api/v1/internal-metrics.test.tsclaude/CLAUDE-KNOWLEDGE.md
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/dashboard/src/components/design-components/analytics-card.tsx
| function useMetricsOrThrow(adminApp: object, includeAnonymous: boolean) { | ||
| const internals = Reflect.get(adminApp, stackAppInternalsSymbol); | ||
| if (typeof internals !== "object" || internals == null || !("useMetrics" in internals)) { | ||
| throw new Error("Admin app internals are unavailable: missing useMetrics"); | ||
| } | ||
| } satisfies LineChartDisplayConfig; | ||
|
|
||
| const dauConfig = { | ||
| name: 'Daily Active Users', | ||
| chart: { | ||
| activity: { | ||
| label: "Activity", | ||
| theme: { | ||
| light: "hsl(180, 95%, 53%)", | ||
| dark: "hsl(200, 91%, 70%)", | ||
| }, | ||
| }, | ||
| const useMetrics = internals.useMetrics; | ||
| if (typeof useMetrics !== "function") { | ||
| throw new Error("Admin app internals are unavailable: useMetrics is not callable"); | ||
| } | ||
| } satisfies LineChartDisplayConfig; | ||
|
|
||
| function TotalUsersDisplay({ includeAnonymous, minimal = false }: { includeAnonymous: boolean, minimal?: boolean }) { | ||
| const adminApp = useAdminApp(); | ||
| const data = (adminApp as any)[stackAppInternalsSymbol].useMetrics(includeAnonymous); | ||
| return useMetrics(includeAnonymous); | ||
| } |
There was a problem hiding this comment.
Please type the metrics contract instead of defaulting every overview group to {}.
useMetricsOrThrow() currently returns an untyped value, and the new widgets immediately paper over missing groups with ?? {} / ?? []. If /api/v1/internal/metrics drops or renames a field, this page now renders convincing zero-state cards instead of surfacing the contract break.
As per coding guidelines: Try to avoid the any type. Whenever you need to use it, leave a comment explaining why you're using it and how you can be certain that errors would still be flagged. Fail early, fail loud. Fail fast with an error instead of silently continuing.
Also applies to: 904-910
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
around lines 83 - 95, The function useMetricsOrThrow currently returns an
untyped value so callers mask missing fields with `?? {}`; define a proper
TypeScript interface (e.g., MetricsContract) describing the expected shape
(groups, arrays, counts, etc.), change useMetricsOrThrow to assert and return
that MetricsContract (use Reflect.get(adminApp, stackAppInternalsSymbol) to
locate internals and internals.useMetrics to call), and add a runtime validation
step inside useMetricsOrThrow that checks required top-level keys and throws a
clear Error if keys are missing or the types are wrong; then remove the `?? {}`
/ `?? []` fallbacks in the widgets and rely on the typed/validated result so
contract breaks fail loud.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/email-drafts/page-client.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
- Updated date parsing in `loadDailyActiveUsers`, `loadDailyActiveUsersSplit`, `loadDailyActiveTeamsSplit`, and `loadAnalyticsOverview` to simplify date extraction by removing unnecessary timezone handling. - Improved code readability and consistency across metrics loading functions.
- Updated the `bulkId` generation in `seedDummyEmails` for better idempotency. - Introduced `normalizeUuidFromEvent` function to sanitize user and team IDs in metrics loading functions, ensuring only valid UUIDs are processed. - Enhanced the logic in `loadDailyActiveUsersSplit` and `loadDailyActiveTeamsSplit` to utilize sanitized data, improving data integrity and accuracy in metrics retrieval. - Removed unnecessary mouse leave event handlers in `HeroAnalyticsWidget` for cleaner interaction.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/backend/prisma/seed.ts (1)
2033-2075:⚠️ Potential issue | 🟠 MajorUse
updateto refresh delivery state and timestamps on rerun.Both
emailOutbox.upsert()calls useupdate: {}, preventing idempotent convergence. Despite the deterministic IDs and calculations, re-running the seed leaves old delivery-state fields unchanged forever. Move delivery-state and timing fields into theupdateblock so that changes to seed definitions (new delivery states, adjusted timestamps) take effect on rerun.Affected blocks
Lines 2033–2075 (fixed emails: welcomeAmelia, passkeyMilo, etc.)
Lines 2126–2158 (bulk emails with deterministic PRNG state)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/prisma/seed.ts` around lines 2033 - 2075, The upsert call to globalPrismaClient.emailOutbox.upsert currently uses update: {} so delivery-state/timestamp fields (e.g., startedSendingAt, finishedSendingAt, canHaveDeliveryInfo, deliveredAt, sendServerErrorExternalMessage, sendServerErrorExternalDetails, sendServerErrorInternalMessage, sendServerErrorInternalDetails, bouncedAt, markedAsSpamAt, openedAt, clickedAt, isQueued) never change on rerun; copy the same conditional delivery-state and timing assignments you have in create into the update object (preserving use of Prisma.DbNull where appropriate and the isQueued conditional logic) so that rerunning the seed refreshes delivery state and timestamps while leaving stable identity fields (tenancyId, id, createdAt, tsxSource, to, extraRenderVariables, createdWith, renderedByWorkerId, renderedSubject, renderedHtml, renderedText) untouched.
♻️ Duplicate comments (4)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx (3)
649-663:⚠️ Potential issue | 🟠 MajorDefault counts to
0to preventNaNtotal and broken empty-state detection.When
deliverabilityStatusis{}(passed from line 1266 viaemail.deliverability_status ?? {}), all counts becomeundefined. Thereducethen producesNaN, and sinceNaN === 0isfalse, the empty-state branch is skipped, resulting in a broken chart instead of the "No email data" message.🩹 Suggested fix
const items = [ - { label: 'Delivered', count: deliverabilityStatus.delivered, color: '#10b981' }, - { label: 'Bounced', count: deliverabilityStatus.bounced, color: '#ef4444' }, - { label: 'In Progress', count: deliverabilityStatus.in_progress, color: '#06b6d4' }, - { label: 'Error', count: deliverabilityStatus.error, color: '#f59e0b' }, + { label: 'Delivered', count: deliverabilityStatus.delivered ?? 0, color: '#10b981' }, + { label: 'Bounced', count: deliverabilityStatus.bounced ?? 0, color: '#ef4444' }, + { label: 'In Progress', count: deliverabilityStatus.in_progress ?? 0, color: '#06b6d4' }, + { label: 'Error', count: deliverabilityStatus.error ?? 0, color: '#f59e0b' }, ]; const total = items.reduce((s, i) => s + i.count, 0);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx around lines 649 - 663, The items' counts come from deliverabilityStatus and can be undefined causing total to become NaN and skip the empty-state; update the construction of items in metrics-page.tsx so each count uses a default 0 (e.g., delivered, bounced, in_progress, error should be read as deliverabilityStatus.delivered ?? 0 etc.) and/or make the reduce robust by summing (i.count ?? 0) to ensure total is a numeric 0 when no data, preserving the empty-state branch.
901-907:⚠️ Potential issue | 🟠 MajorSilent fallbacks mask API contract breaks.
The metrics response is untyped (
Promise<any>peradmin-interface.ts), and these lines immediately default missing groups to{}or[]. If the API drops or renames a field (e.g.,auth_overview→authentication_overview), this page will render zero-state cards without any error, making debugging difficult.Per coding guidelines: "NEVER silently use fallback values when type errors occur. If a state should never happen due to higher-level logic, either update the types or throw an error."
Consider adding runtime validation for required top-level keys in
useMetricsOrThrowand throwing if they're missing, or at minimum define an expected interface and validate against it.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx around lines 901 - 907, The current silent fallbacks for auth, payments, email, and analytics (variables auth, payments, email, analytics and derived recentEmails/topReferrers) mask API contract breaks; update useMetricsOrThrow to perform runtime validation of the metrics payload shape (ensure top-level keys like auth_overview, payments_overview, email_overview, analytics_overview exist and have expected substructure) and throw a clear error when a required key is missing or has wrong type instead of returning undefined/{} or []; then remove the inline empty-object/empty-array fallbacks in metrics-page.tsx so the page fails fast on contract violations.
488-501:⚠️ Potential issue | 🔴 CriticalImport
ElementTypeas a type from React.The
emailStatusConfigMap usesReact.ElementTypebut this file does not import the React namespace. This will cause a TypeScript compile error: "Cannot find namespace 'React'".🔧 Suggested fix
At the import line (line 16):
-import { Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; +import { Suspense, useEffect, useLayoutEffect, useMemo, useRef, useState, type ElementType } from "react";At the type usage (line 490):
const emailStatusConfig = new Map<string, { label: string, - icon: React.ElementType, + icon: ElementType, bg: string, text: string, dot: string, }>([🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx around lines 488 - 501, The file uses React.ElementType in the emailStatusConfig Map but doesn't import React, causing a TS error; import the type and update the Map to use it by adding "import type { ElementType } from 'react'" and change the Map generic from React.ElementType to ElementType (referencing emailStatusConfig and the Map definition) so the type resolves correctly.apps/backend/src/app/api/latest/internal/metrics/route.tsx (1)
947-951:⚠️ Potential issue | 🟠 Major
daily_revenueis still hard-coded to zeros.The total now comes from real invoice data, but the per-day series is still placeholder data. Any chart or delta built from
daily_revenuewill stay flat even whentotal_revenue_centsis non-zero.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx` around lines 947 - 951, daily_revenue is populated with zeros while total_revenue_cents uses real invoices; replace the placeholder mapping so daily_revenue is computed from invoice data instead of hard-coded zeros. Locate where daily_revenue is built (the daily_revenue: dailyPageViews.map(...) expression) and aggregate invoice line items by date (matching dailyPageViews p.date) to set new_cents and refund_cents per day, using the same date normalization logic as total_revenue_cents calculation; ensure sums default to 0 when no invoices exist for a date and preserve the date field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/prisma/seed.ts`:
- Around line 881-912: The current branch skips updating createdAt and
userEmailToId when an existing projectUser is found; modify the logic around
prisma.projectUser.findFirst so that whether the user is newly created or
existing you (1) ensure prisma.projectUser.updateMany is called to set createdAt
for tenancy/projectUserId, and (2) always call userEmailToId.set(email, <id>)
using created.id for newly created users or existing.projectUserId for found
users; keep the existing usersCrudHandlers.adminCreate flow unchanged for the
create path and reuse the selected projectUserId from the existing result when
present.
In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx`:
- Around line 442-465: The MAU query currently uniques raw user_id via
uniqExact(assumeNotNull(user_id)) which lets malformed/stale IDs inflate MAU;
update the clickhouseClient.query payload in route.tsx to apply the same
normalization/filtering expression used for the DAU split (replace
uniqExact(assumeNotNull(user_id)) with the sanitized expression/function used in
the DAU code path — e.g. the shared sanitize/normalize_user_id ClickHouse
expression or helper constant) and keep the same WHERE filters (including the
includeAnonymous condition and project/branch/time params); ensure the
query_params and result parsing (rows: { mau: number }[]) remain compatible so
the returned Number(rows[0]?.mau ?? 0) reflects the sanitized unique count.
- Around line 546-558: Replace the placeholder estimatedMrrCents (currently set
via activeSubscriptionCount * 10_000) with a computed sum of each active
subscription's monthly recurring value: iterate active subscriptions (use the
same data source that produced subsByStatus/activeSubscriptionCount — e.g., the
subscription objects returned earlier), for each subscription extract its price
amount and interval and normalize to a monthly amount (if interval === 'year'
divide by 12, if 'month' use as-is; handle other intervals similarly), multiply
by quantity and apply any subscription-level discounts or prorations available
on the subscription/invoice lines, then sum and set mrr_cents to that total (in
cents) instead of estimatedMrrCents so mrr reflects actual price points,
quantities and billing intervals.
- Around line 441-476: The current try/catch around clickhouseClient.query
swallows all errors and returns 0, hiding real failures; change it to only
handle expected/benign cases (e.g., handle a specific ClickHouse "no data" or
timeout error by logging via captureError and returning 0) and for any other
unexpected error rethrow after logging (or remove the catch so the error
surfaces); update the catch to inspect the thrown error (e.g., error.code or
instanceof) before returning a synthetic value and keep the existing
StackAssertionError/logging via captureError for handled cases; apply the same
pattern to the other similar loader blocks that use clickhouseClient.query and
return synthetic defaults.
---
Outside diff comments:
In `@apps/backend/prisma/seed.ts`:
- Around line 2033-2075: The upsert call to
globalPrismaClient.emailOutbox.upsert currently uses update: {} so
delivery-state/timestamp fields (e.g., startedSendingAt, finishedSendingAt,
canHaveDeliveryInfo, deliveredAt, sendServerErrorExternalMessage,
sendServerErrorExternalDetails, sendServerErrorInternalMessage,
sendServerErrorInternalDetails, bouncedAt, markedAsSpamAt, openedAt, clickedAt,
isQueued) never change on rerun; copy the same conditional delivery-state and
timing assignments you have in create into the update object (preserving use of
Prisma.DbNull where appropriate and the isQueued conditional logic) so that
rerunning the seed refreshes delivery state and timestamps while leaving stable
identity fields (tenancyId, id, createdAt, tsxSource, to, extraRenderVariables,
createdWith, renderedByWorkerId, renderedSubject, renderedHtml, renderedText)
untouched.
---
Duplicate comments:
In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx`:
- Around line 947-951: daily_revenue is populated with zeros while
total_revenue_cents uses real invoices; replace the placeholder mapping so
daily_revenue is computed from invoice data instead of hard-coded zeros. Locate
where daily_revenue is built (the daily_revenue: dailyPageViews.map(...)
expression) and aggregate invoice line items by date (matching dailyPageViews
p.date) to set new_cents and refund_cents per day, using the same date
normalization logic as total_revenue_cents calculation; ensure sums default to 0
when no invoices exist for a date and preserve the date field.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx:
- Around line 649-663: The items' counts come from deliverabilityStatus and can
be undefined causing total to become NaN and skip the empty-state; update the
construction of items in metrics-page.tsx so each count uses a default 0 (e.g.,
delivered, bounced, in_progress, error should be read as
deliverabilityStatus.delivered ?? 0 etc.) and/or make the reduce robust by
summing (i.count ?? 0) to ensure total is a numeric 0 when no data, preserving
the empty-state branch.
- Around line 901-907: The current silent fallbacks for auth, payments, email,
and analytics (variables auth, payments, email, analytics and derived
recentEmails/topReferrers) mask API contract breaks; update useMetricsOrThrow to
perform runtime validation of the metrics payload shape (ensure top-level keys
like auth_overview, payments_overview, email_overview, analytics_overview exist
and have expected substructure) and throw a clear error when a required key is
missing or has wrong type instead of returning undefined/{} or []; then remove
the inline empty-object/empty-array fallbacks in metrics-page.tsx so the page
fails fast on contract violations.
- Around line 488-501: The file uses React.ElementType in the emailStatusConfig
Map but doesn't import React, causing a TS error; import the type and update the
Map to use it by adding "import type { ElementType } from 'react'" and change
the Map generic from React.ElementType to ElementType (referencing
emailStatusConfig and the Map definition) so the type resolves correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4e7397ee-902e-41e6-801f-e1d7c961116f
📒 Files selected for processing (3)
apps/backend/prisma/seed.tsapps/backend/src/app/api/latest/internal/metrics/route.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx
| try { | ||
| const result = await clickhouseClient.query({ | ||
| query: ` | ||
| SELECT | ||
| uniqExact(assumeNotNull(user_id)) AS mau | ||
| FROM analytics_internal.events | ||
| WHERE event_type = '$token-refresh' | ||
| AND project_id = {projectId:String} | ||
| AND branch_id = {branchId:String} | ||
| AND user_id IS NOT NULL | ||
| AND event_at >= {since:DateTime} | ||
| AND event_at < {untilExclusive:DateTime} | ||
| AND ({includeAnonymous:UInt8} = 1 OR JSONExtract(toJSONString(data), 'is_anonymous', 'UInt8') = 0) | ||
| `, | ||
| query_params: { | ||
| projectId: tenancy.project.id, | ||
| branchId: tenancy.branchId, | ||
| since: formatClickhouseDateTimeParam(since), | ||
| untilExclusive: formatClickhouseDateTimeParam(untilExclusive), | ||
| includeAnonymous: includeAnonymous ? 1 : 0, | ||
| }, | ||
| format: "JSONEachRow", | ||
| }); | ||
| const rows: { mau: number }[] = await result.json(); | ||
| return Number(rows[0]?.mau ?? 0); | ||
| } catch (error) { | ||
| captureError("internal-metrics-load-monthly-active-users-failed", new StackAssertionError( | ||
| "Failed to load monthly active users for internal metrics.", | ||
| { | ||
| cause: error, | ||
| projectId: tenancy.project.id, | ||
| branchId: tenancy.branchId, | ||
| }, | ||
| )); | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
Avoid catch-all fallbacks for these loaders.
Both blocks swallow every failure and return synthetic zero/empty metrics. That hides query bugs and schema regressions behind a successful 200 response, which makes production breakage look like “no traffic”. Please narrow the handled error cases or let unexpected failures surface.
As per coding guidelines, "**/*.{ts,tsx}: NEVER try-catch-all" and "Fail early, fail loud. Fail fast with an error instead of silently continuing.`"
Also applies to: 707-708, 971-995
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/backend/src/app/api/latest/internal/metrics/route.tsx` around lines 441
- 476, The current try/catch around clickhouseClient.query swallows all errors
and returns 0, hiding real failures; change it to only handle expected/benign
cases (e.g., handle a specific ClickHouse "no data" or timeout error by logging
via captureError and returning 0) and for any other unexpected error rethrow
after logging (or remove the catch so the error surfaces); update the catch to
inspect the thrown error (e.g., error.code or instanceof) before returning a
synthetic value and keep the existing StackAssertionError/logging via
captureError for handled cases; apply the same pattern to the other similar
loader blocks that use clickhouseClient.query and return synthetic defaults.
|
@greptile-ai review |
- Introduced a new function to load daily revenue over the last 30 days, aggregating paid invoices. - Updated the payments overview to include the new daily revenue data and adjusted MRR calculations accordingly. - Improved the globe component's zoom logic for better visual consistency across different screen sizes. - Refined the email breakdown card's deliverability status type to be more flexible. - Replaced JSON.stringify comparisons with deep equality checks in internal metrics tests for improved accuracy. - Cleaned up exports in the dashboard UI components by removing unused exports.
|
@greptile-ai review |
- Replaced Prisma's `findMany` method with raw SQL queries for fetching active users and teams, improving performance and flexibility. - Updated the daily revenue calculation to utilize raw SQL for aggregating subscription invoices. - Introduced schema retrieval for tenancy to ensure correct table references in queries. - Enhanced the payments overview to aggregate subscription data using raw SQL, streamlining data processing.

Summary by CodeRabbit
New Features
Style
Tests
Documentation