From 229ff4c61c82ac524415c7accd3d6b6f6ed1acc7 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sun, 31 Aug 2025 12:31:39 -0700 Subject: [PATCH 1/3] More query optimizations --- apps/backend/src/app/api/latest/auth/sessions/crud.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/backend/src/app/api/latest/auth/sessions/crud.tsx b/apps/backend/src/app/api/latest/auth/sessions/crud.tsx index e20f8df93..b6f79c5b6 100644 --- a/apps/backend/src/app/api/latest/auth/sessions/crud.tsx +++ b/apps/backend/src/app/api/latest/auth/sessions/crud.tsx @@ -62,6 +62,9 @@ export const sessionsCrudHandlers = createLazyProxy(() => createCrudHandlers(ses JOIN latest_events le ON e.data->>'sessionId' = le."sessionId" AND e."eventStartedAt" = le."lastActiveAt" LEFT JOIN ${sqlQuoteIdent(schema)}."EventIpInfo" geo ON geo.id = e."endUserIpInfoGuessId" WHERE e."systemEventTypeIds" @> '{"$session-activity"}' + AND e.data->>'userId' = ${query.user_id} + AND e.data->>'projectId' = ${auth.tenancy.project.id} + AND COALESCE(e.data->>'branchId', 'main') = ${auth.tenancy.branchId} `; const sessionsWithLastActiveAt = refreshTokenObjs.map(s => { From 41b7e791fd68986ee3680312fdb833c01a3a8ef2 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sun, 31 Aug 2025 12:59:14 -0700 Subject: [PATCH 2/3] Sort projects --- .../(outside-dashboard)/projects/page-client.tsx | 10 +++++++++- turbo.json | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx index ad08cf076..2ab066e7f 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx @@ -49,7 +49,15 @@ export default function PageClient(props: { inviteUser: (origin: string, teamId: }; const grouped = groupBy(newProjects, (project) => project.ownerTeamId); - return Array.from(grouped.entries()).map(([teamId, projects]) => { + return [...grouped.entries()].sort((a, b) => { + if (a[0] === null) return -1; + if (b[0] === null) return 1; + if (sort === "recency") { + return a[1][0].createdAt > b[1][0].createdAt ? -1 : 1; + } else { + return stringCompare(a[1][0].displayName, b[1][0].displayName); + } + }).map(([teamId, projects]) => { return { teamId, projects: projects.sort(projectSort), diff --git a/turbo.json b/turbo.json index 054388045..96fac7d8a 100644 --- a/turbo.json +++ b/turbo.json @@ -2,6 +2,7 @@ "$schema": "https://turbo.build/schema.json", "globalEnv": [ "STACK_*", + "CRON_SECRET", "NEXT_PUBLIC_*", "NEXT_PUBLIC_SENTRY_*", "SENTRY_*", From 1e06ff4c4c80838d12ca6f3a76cb62eb2181de19 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Sun, 31 Aug 2025 15:19:22 -0700 Subject: [PATCH 3/3] More fixes --- .../app/api/latest/internal/metrics/route.tsx | 32 +++++++++++-------- .../[projectId]/(overview)/metrics-page.tsx | 4 ++- .../[projectId]/(overview)/page-client.tsx | 2 +- .../src/interface/admin-interface.ts | 9 ++++-- .../apps/implementations/admin-app-impl.ts | 8 ++--- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/apps/backend/src/app/api/latest/internal/metrics/route.tsx b/apps/backend/src/app/api/latest/internal/metrics/route.tsx index 232b4e13f..44685bf15 100644 --- a/apps/backend/src/app/api/latest/internal/metrics/route.tsx +++ b/apps/backend/src/app/api/latest/internal/metrics/route.tsx @@ -15,7 +15,7 @@ const DataPointsSchema = yupArray(yupObject({ }).defined()).defined(); -async function loadUsersByCountry(tenancy: Tenancy): Promise> { +async function loadUsersByCountry(tenancy: Tenancy, includeAnonymous: boolean = false): Promise> { const a = await globalPrismaClient.$queryRaw<{countryCode: string|null, userCount: bigint}[]>` WITH LatestEventWithCountryCode AS ( SELECT DISTINCT ON ("userId") @@ -27,7 +27,7 @@ async function loadUsersByCountry(tenancy: Tenancy): Promise>'projectId' = ${tenancy.project.id} - AND "data"->>'isAnonymous' != 'true' + AND (${includeAnonymous} OR "data"->>'isAnonymous' != 'true') AND COALESCE("data"->>'branchId', 'main') = ${tenancy.branchId} AND "countryCode" IS NOT NULL ORDER BY "userId", "eventStartedAt" DESC @@ -45,7 +45,7 @@ async function loadUsersByCountry(tenancy: Tenancy): Promise { +async function loadTotalUsers(tenancy: Tenancy, now: Date, includeAnonymous: boolean = false): Promise { const schema = getPrismaSchemaForTenancy(tenancy); const prisma = await getPrismaClientForTenancy(tenancy); return (await prisma.$queryRaw<{date: Date, dailyUsers: bigint, cumUsers: bigint}[]>` @@ -65,7 +65,7 @@ async function loadTotalUsers(tenancy: Tenancy, now: Date): Promise LEFT JOIN ${sqlQuoteIdent(schema)}."ProjectUser" pu ON DATE(pu."createdAt") = ds.registration_day AND pu."tenancyId" = ${tenancy.id}::UUID - AND pu."isAnonymous" = false + AND (${includeAnonymous} OR pu."isAnonymous" = false) GROUP BY ds.registration_day ORDER BY ds.registration_day `).map((x) => ({ @@ -74,7 +74,7 @@ async function loadTotalUsers(tenancy: Tenancy, now: Date): Promise })); } -async function loadDailyActiveUsers(tenancy: Tenancy, now: Date) { +async function loadDailyActiveUsers(tenancy: Tenancy, now: Date, includeAnonymous: boolean = false) { const res = await globalPrismaClient.$queryRaw<{day: Date, dau: bigint}[]>` WITH date_series AS ( SELECT GENERATE_SERIES( @@ -87,7 +87,7 @@ async function loadDailyActiveUsers(tenancy: Tenancy, now: Date) { daily_users AS ( SELECT DATE_TRUNC('day', "eventStartedAt") AS "day", - COUNT(DISTINCT CASE WHEN "data"->>'isAnonymous' = 'false' THEN "data"->'userId' ELSE NULL END) AS "dau" + COUNT(DISTINCT CASE WHEN (${includeAnonymous} OR "data"->>'isAnonymous' = 'false') THEN "data"->'userId' ELSE NULL END) AS "dau" FROM "Event" WHERE "eventStartedAt" >= ${now}::date - INTERVAL '30 days' AND '$user-activity' = ANY("systemEventTypeIds"::text[]) @@ -134,7 +134,7 @@ async function loadLoginMethods(tenancy: Tenancy): Promise<{method: string, coun `; } -async function loadRecentlyActiveUsers(tenancy: Tenancy): Promise { +async function loadRecentlyActiveUsers(tenancy: Tenancy, includeAnonymous: boolean = false): Promise { // use the Events table to get the most recent activity const events = await globalPrismaClient.$queryRaw<{ data: any, eventStartedAt: Date }[]>` WITH RankedEvents AS ( @@ -146,7 +146,7 @@ async function loadRecentlyActiveUsers(tenancy: Tenancy): Promise>'projectId' = ${tenancy.project.id} - AND "data"->>'isAnonymous' != 'true' + AND (${includeAnonymous} OR "data"->>'isAnonymous' != 'true') AND COALESCE("data"->>'branchId', 'main') = ${tenancy.branchId} AND '$user-activity' = ANY("systemEventTypeIds"::text[]) ) @@ -188,6 +188,9 @@ export const GET = createSmartRouteHandler({ type: adminAuthTypeSchema.defined(), tenancy: adaptSchema.defined(), }), + query: yupObject({ + include_anonymous: yupString().oneOf(["true", "false"]).optional(), + }), }), response: yupObject({ statusCode: yupNumber().oneOf([200]).defined(), @@ -205,6 +208,7 @@ export const GET = createSmartRouteHandler({ }), handler: async (req) => { const now = new Date(); + const includeAnonymous = req.query?.include_anonymous === "true"; const prisma = await getPrismaClientForTenancy(req.auth.tenancy); @@ -218,24 +222,24 @@ export const GET = createSmartRouteHandler({ loginMethods ] = await Promise.all([ prisma.projectUser.count({ - where: { tenancyId: req.auth.tenancy.id, isAnonymous: false }, + where: { tenancyId: req.auth.tenancy.id, ...(includeAnonymous ? {} : { isAnonymous: false }) }, }), - loadTotalUsers(req.auth.tenancy, now), - loadDailyActiveUsers(req.auth.tenancy, now), - loadUsersByCountry(req.auth.tenancy), + loadTotalUsers(req.auth.tenancy, now, includeAnonymous), + loadDailyActiveUsers(req.auth.tenancy, now, includeAnonymous), + loadUsersByCountry(req.auth.tenancy, includeAnonymous), usersCrudHandlers.adminList({ tenancy: req.auth.tenancy, query: { order_by: 'signed_up_at', desc: "true", limit: 5, - include_anonymous: "false", + include_anonymous: includeAnonymous ? "true" : "false", }, allowedErrorTypes: [ KnownErrors.UserNotFound, ], }).then(res => res.items), - loadRecentlyActiveUsers(req.auth.tenancy), + loadRecentlyActiveUsers(req.auth.tenancy, includeAnonymous), loadLoginMethods(req.auth.tenancy), ] as const); diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx index d64f46409..621f7d94b 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/metrics-page.tsx @@ -5,6 +5,7 @@ import { ErrorBoundary } from '@sentry/nextjs'; import { UserAvatar } from '@stackframe/stack'; import { fromNow } from '@stackframe/stack-shared/dist/utils/dates'; import { Card, CardContent, CardHeader, CardTitle, Table, TableBody, TableCell, TableRow, Typography } from '@stackframe/stack-ui'; +import { useState } from 'react'; import { PageLayout } from "../page-layout"; import { useAdminApp } from '../use-admin-app'; import { GlobeSection } from './globe'; @@ -38,8 +39,9 @@ const dauConfig = { export default function MetricsPage(props: { toSetup: () => void }) { const adminApp = useAdminApp(); const router = useRouter(); + const [includeAnonymous, setIncludeAnonymous] = useState(false); - const data = (adminApp as any)[stackAppInternalsSymbol].useMetrics(); + const data = (adminApp as any)[stackAppInternalsSymbol].useMetrics(includeAnonymous); return ( diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/page-client.tsx index 3279c5329..811ededb7 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/page-client.tsx @@ -8,7 +8,7 @@ import SetupPage from "./setup-page"; export default function PageClient() { const adminApp = useAdminApp(); - const data = (adminApp as any)[stackAppInternalsSymbol].useMetrics(); + const data = (adminApp as any)[stackAppInternalsSymbol].useMetrics(false); const [page, setPage] = useState<'setup' | 'metrics'>(data.total_users === 0 ? 'setup' : 'metrics'); switch (page) { diff --git a/packages/stack-shared/src/interface/admin-interface.ts b/packages/stack-shared/src/interface/admin-interface.ts index ccdedb8e0..afb24223a 100644 --- a/packages/stack-shared/src/interface/admin-interface.ts +++ b/packages/stack-shared/src/interface/admin-interface.ts @@ -297,9 +297,14 @@ export class StackAdminInterface extends StackServerInterface { ); } - async getMetrics(): Promise { + async getMetrics(includeAnonymous: boolean = false): Promise { + const params = new URLSearchParams(); + if (includeAnonymous) { + params.append('include_anonymous', 'true'); + } + const queryString = params.toString(); const response = await this.sendAdminRequest( - "/internal/metrics", + `/internal/metrics${queryString ? `?${queryString}` : ''}`, { method: "GET", }, diff --git a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts index 812f90d53..8c89eca8a 100644 --- a/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts +++ b/packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts @@ -51,8 +51,8 @@ export class _StackAdminAppImplIncomplete { return await this._interface.getSvixToken(); }); - private readonly _metricsCache = createCache(async () => { - return await this._interface.getMetrics(); + private readonly _metricsCache = createCache(async ([includeAnonymous]: [boolean]) => { + return await this._interface.getMetrics(includeAnonymous); }); private readonly _emailPreviewCache = createCache(async ([themeId, themeTsxSource, templateId, templateTsxSource]: [string | null | false | undefined, string | undefined, string | undefined, string | undefined]) => { return await this._interface.renderEmailPreview({ themeId, themeTsxSource, templateId, templateTsxSource }); @@ -415,8 +415,8 @@ export class _StackAdminAppImplIncomplete { - return useAsyncCache(this._metricsCache, [], "useMetrics()"); + useMetrics: (includeAnonymous: boolean = false): any => { + return useAsyncCache(this._metricsCache, [includeAnonymous] as const, "useMetrics()"); } // END_PLATFORM };