Skip to content

Commit 4bd4ed2

Browse files
Cursor PR review changes #1
1 parent f34c4ba commit 4bd4ed2

File tree

7 files changed

+4429
-87
lines changed

7 files changed

+4429
-87
lines changed

apps/backend/prisma/seed.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2114,12 +2114,6 @@ async function seedDummyEmails(options: EmailSeedOptions) {
21142114
const hasClicked = hasOpened && emailBulkRand() < 0.3;
21152115
const hasError = !hasBounce && !hasOpened && emailBulkRand() < 0.05;
21162116

2117-
const existing = await globalPrismaClient.emailOutbox.findUnique({
2118-
where: { tenancyId_id: { tenancyId, id: bulkId } },
2119-
select: { id: true },
2120-
});
2121-
if (existing) continue;
2122-
21232117
const canHaveDelivery = hasOpened || hasClicked || hasBounce;
21242118

21252119
await globalPrismaClient.emailOutbox.upsert({

apps/backend/src/app/api/latest/internal/metrics/route.tsx

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { getClickhouseAdminClient } from "@/lib/clickhouse";
22
import { Tenancy } from "@/lib/tenancies";
3-
import { getPrismaClientForTenancy, PrismaClientTransaction, getPrismaSchemaForTenancy, sqlQuoteIdent } from "@/prisma-client";
3+
import { getPrismaClientForTenancy, getPrismaSchemaForTenancy, sqlQuoteIdent } from "@/prisma-client";
44
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
55
import { KnownErrors } from "@stackframe/stack-shared";
66
import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users";
77
import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
8+
import { captureError, StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
89
import { adaptSchema, adminAuthTypeSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
910
import yup from 'yup';
1011
import { userFullInclude, userPrismaToCrud, usersCrudHandlers } from "../../users/crud";
@@ -23,7 +24,7 @@ function formatClickhouseDateTimeParam(date: Date): string {
2324
return date.toISOString().slice(0, 19);
2425
}
2526

26-
async function loadUsersByCountry(tenancy: Tenancy, prisma: PrismaClientTransaction, includeAnonymous: boolean = false): Promise<Record<string, number>> {
27+
async function loadUsersByCountry(tenancy: Tenancy, includeAnonymous: boolean = false): Promise<Record<string, number>> {
2728
const clickhouseClient = getClickhouseAdminClient();
2829
const res = await clickhouseClient.query({
2930
query: `
@@ -240,6 +241,7 @@ async function loadDailyActiveUsersSplit(tenancy: Tenancy, now: Date, includeAno
240241
AND event_at >= {since:DateTime}
241242
AND event_at < {untilExclusive:DateTime}
242243
AND ({includeAnonymous:UInt8} = 1 OR JSONExtract(toJSONString(data), 'is_anonymous', 'UInt8') = 0)
244+
GROUP BY day, user_id
243245
`,
244246
query_params: {
245247
projectId: tenancy.project.id,
@@ -309,6 +311,7 @@ async function loadDailyActiveTeamsSplit(tenancy: Tenancy, now: Date): Promise<A
309311
AND team_id IS NOT NULL
310312
AND event_at >= {since:DateTime}
311313
AND event_at < {untilExclusive:DateTime}
314+
GROUP BY day, team_id
312315
`,
313316
query_params: {
314317
projectId: tenancy.project.id,
@@ -430,7 +433,15 @@ async function loadMonthlyActiveUsers(tenancy: Tenancy, includeAnonymous: boolea
430433
});
431434
const rows: { mau: number }[] = await result.json();
432435
return Number(rows[0]?.mau ?? 0);
433-
} catch {
436+
} catch (error) {
437+
captureError("internal-metrics-load-monthly-active-users-failed", new StackAssertionError(
438+
"Failed to load monthly active users for internal metrics.",
439+
{
440+
cause: error,
441+
projectId: tenancy.project.id,
442+
branchId: tenancy.branchId,
443+
},
444+
));
434445
return 0;
435446
}
436447
}
@@ -587,11 +598,9 @@ async function loadEmailOverview(tenancy: Tenancy) {
587598

588599
// Daily email sends for last 30 days
589600
const emailByDay = new Map<string, number>();
590-
for (const email of recentEmails) {
591-
if (email.createdAt >= thirtyDaysAgo) {
592-
const key = email.createdAt.toISOString().split('T')[0];
593-
emailByDay.set(key, (emailByDay.get(key) ?? 0) + 1);
594-
}
601+
for (const email of emailsByDayAndStatus) {
602+
const key = email.createdAt.toISOString().split('T')[0];
603+
emailByDay.set(key, (emailByDay.get(key) ?? 0) + 1);
595604
}
596605
const dailyEmails: DataPoints = [];
597606
for (let i = 0; i <= 30; i++) {
@@ -663,7 +672,7 @@ async function loadAnalyticsOverview(tenancy: Tenancy, now: Date, includeAnonymo
663672
const clickhouseClient = getClickhouseAdminClient();
664673

665674
try {
666-
const [pageViewResult, clickResult, dailyVisitorResult, totalVisitorResult, referrerResult, topRegionResult, onlineResult, replayResult] = await Promise.all([
675+
const [pageViewResult, dailyVisitorResult, totalVisitorResult, clickResult, referrerResult, topRegionResult, onlineResult, replayResult] = await Promise.all([
667676
clickhouseClient.query({
668677
query: `
669678
SELECT
@@ -926,7 +935,15 @@ async function loadAnalyticsOverview(tenancy: Tenancy, now: Date, includeAnonymo
926935
count: Number(topRegionRows[0].cnt),
927936
} : null,
928937
};
929-
} catch {
938+
} catch (error) {
939+
captureError("internal-metrics-analytics-overview-fallback", new StackAssertionError(
940+
"Falling back to empty analytics overview due to query failure.",
941+
{
942+
cause: error,
943+
projectId: tenancy.project.id,
944+
branchId: tenancy.branchId,
945+
},
946+
));
930947
// Analytics may not be enabled for all projects
931948
return {
932949
daily_page_views: [] as DataPoints,
@@ -1171,7 +1188,7 @@ export const GET = createSmartRouteHandler({
11711188
}),
11721189
loadTotalUsers(req.auth.tenancy, now, includeAnonymous),
11731190
loadDailyActiveUsers(req.auth.tenancy, now, includeAnonymous),
1174-
loadUsersByCountry(req.auth.tenancy, prisma, includeAnonymous),
1191+
loadUsersByCountry(req.auth.tenancy, includeAnonymous),
11751192
usersCrudHandlers.adminList({
11761193
tenancy: req.auth.tenancy,
11771194
query: {

apps/dashboard/DESIGN-GUIDE.md

Lines changed: 0 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -851,49 +851,4 @@ Whenever a new reusable visual pattern is introduced in dashboard features:
851851
- then document the component contract and preferred usage here
852852
- avoid introducing permanent page-local UI primitives that duplicate design-components behavior
853853

854-
---
855-
856-
## 13) Frontend Design Reference
857-
858-
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
859-
860-
This guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
861-
862-
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
863-
864-
### Design Thinking
865-
866-
Before coding, understand the context and commit to a bold aesthetic direction:
867-
868-
- **Purpose**: What problem does this interface solve? Who uses it?
869-
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. Use these for inspiration but design one that is true to the aesthetic direction.
870-
- **Constraints**: Technical requirements (framework, performance, accessibility).
871-
- **Differentiation**: What makes this unforgettable? What's the one thing someone will remember?
872-
873-
**Critical**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
874-
875-
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
876-
877-
- Production-grade and functional
878-
- Visually striking and memorable
879-
- Cohesive with a clear aesthetic point-of-view
880-
- Meticulously refined in every detail
881-
882-
### Frontend Aesthetics Guidelines
883-
884-
Focus on:
885-
886-
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
887-
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
888-
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (`animation-delay`) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
889-
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
890-
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
891-
892-
Never use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
893-
894-
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. Never converge on common choices (Space Grotesk, for example) across generations.
895-
896-
**Important**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
897-
898-
Remember: You are capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
899854

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/line-chart.tsx

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ function parseChartDate(dateValue: string): Date {
103103
return new Date(year, month - 1, day);
104104
}
105105

106-
return new Date(dateValue);
106+
const parsed = new Date(dateValue);
107+
if (Number.isNaN(parsed.getTime())) {
108+
throw new Error(`Unsupported chart date format: ${dateValue}`);
109+
}
110+
return parsed;
107111
}
108112

109113
function formatDateRangeLabel(range: CustomDateRange | null): string {
@@ -117,11 +121,11 @@ function formatDateRangeLabel(range: CustomDateRange | null): string {
117121
return `${fromLabel} - ${toLabel}`;
118122
}
119123

120-
export function filterDatapointsByTimeRange(
121-
datapoints: DataPoint[],
124+
function filterPointsByTimeRange<T extends { date: string }>(
125+
datapoints: T[],
122126
timeRange: TimeRange,
123127
customDateRange: CustomDateRange | null = null,
124-
): DataPoint[] {
128+
): T[] {
125129
if (timeRange === '7d') {
126130
return datapoints.slice(-7);
127131
}
@@ -141,24 +145,20 @@ export function filterDatapointsByTimeRange(
141145
return datapoints;
142146
}
143147

148+
export function filterDatapointsByTimeRange(
149+
datapoints: DataPoint[],
150+
timeRange: TimeRange,
151+
customDateRange: CustomDateRange | null = null,
152+
): DataPoint[] {
153+
return filterPointsByTimeRange(datapoints, timeRange, customDateRange);
154+
}
155+
144156
export function filterStackedDatapointsByTimeRange<T extends { date: string }>(
145157
datapoints: T[],
146158
timeRange: TimeRange,
147159
customDateRange: CustomDateRange | null = null,
148160
): T[] {
149-
if (timeRange === '7d') return datapoints.slice(-7);
150-
if (timeRange === '30d') return datapoints.slice(-30);
151-
if (timeRange === 'custom') {
152-
if (customDateRange == null) {
153-
return datapoints;
154-
}
155-
156-
const fromKey = getDateKey(customDateRange.from);
157-
const toKey = getDateKey(customDateRange.to);
158-
159-
return datapoints.filter((point) => point.date >= fromKey && point.date <= toKey);
160-
}
161-
return datapoints;
161+
return filterPointsByTimeRange(datapoints, timeRange, customDateRange);
162162
}
163163

164164
function getHoveredDataIndex(activeTooltipIndex: unknown, dataLength: number): number | null {

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,20 @@ function calculatePeriodDelta(currentValue: number, previousValue: number): numb
8787
return Number((((currentValue - previousValue) / previousValue) * 100).toFixed(1));
8888
}
8989

90+
function useMetricsOrThrow(adminApp: object, includeAnonymous: boolean) {
91+
const internals = Reflect.get(adminApp, stackAppInternalsSymbol);
92+
if (typeof internals !== "object" || internals == null || !("useMetrics" in internals)) {
93+
throw new Error("Admin app internals are unavailable: missing useMetrics");
94+
}
95+
96+
const useMetrics = internals.useMetrics;
97+
if (typeof useMetrics !== "function") {
98+
throw new Error("Admin app internals are unavailable: useMetrics is not callable");
99+
}
100+
101+
return useMetrics(includeAnonymous);
102+
}
103+
90104
// ── Compact dual-value stat card ─────────────────────────────────────────────
91105

92106
function DualStatCard({
@@ -845,7 +859,7 @@ function MetricsContent({
845859
const config = project.useConfig();
846860
const projectId = useProjectId();
847861
const router = useRouter();
848-
const data = (adminApp as any)[stackAppInternalsSymbol].useMetrics(includeAnonymous);
862+
const data = useMetricsOrThrow(adminApp, includeAnonymous);
849863
const installedApps = useMemo(
850864
() => typedEntries(config.apps.installed)
851865
.filter(([_, appConfig]) => appConfig?.enabled === true)
@@ -992,7 +1006,6 @@ function MetricsContent({
9921006
// ── Hero outer stats: MAUs, Total Emails sent, Session time ───────────────
9931007
const heroOuterStats = useMemo<AnalyticsStatPill[]>(() => {
9941008
const analyticsObj = data.analytics_overview ?? {};
995-
const deltasObj = (analyticsObj.deltas ?? {}) as Record<string, number>;
9961009
const mau = (auth.mau ?? 0) as number;
9971010
const totalEmailsSent = (email.emails_sent ?? 0) as number;
9981011
return [

0 commit comments

Comments
 (0)