Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dashboard Metrics #361

Merged
merged 108 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from 90 commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
cea70e4
start building the dashboard
TheCactusBlue Dec 8, 2024
30518b6
add test api route
TheCactusBlue Dec 9, 2024
1440c19
Neon project provision (#358)
N2D4 Dec 8, 2024
d9ac2d2
chore: update package versions
N2D4 Dec 8, 2024
6c3d032
Use different passwords for development environment dependencies (#359)
N2D4 Dec 9, 2024
014597f
updated self host vars, reduced the number of required env vars
fomalhautb Dec 9, 2024
c7e4768
rename /dashboard to /metrics
TheCactusBlue Dec 9, 2024
3493b05
load datapoints from an endpoint
TheCactusBlue Dec 9, 2024
ec8c353
add actual data fetching
TheCactusBlue Dec 9, 2024
2ec6ac4
calculate DAUs
TheCactusBlue Dec 10, 2024
0858014
remove placeholder
TheCactusBlue Dec 10, 2024
9eba27d
Merge branch 'dev' into metric-dashboard
N2D4 Dec 10, 2024
d585e12
All auth type schemas are now required
N2D4 Dec 10, 2024
42b672a
Disallow interfaces
N2D4 Dec 10, 2024
e32a22e
accessibility props for sidebar sheet (#362)
TheCactusBlue Dec 10, 2024
05cf6bc
remove route descriptions
TheCactusBlue Dec 10, 2024
8a36b88
restrict access to admins only
TheCactusBlue Dec 10, 2024
2fdeb02
Update apps/dashboard/src/app/(main)/(protected)/projects/[projectId]…
TheCactusBlue Dec 10, 2024
cda38fe
remove getMetrics() from adminInterface
TheCactusBlue Dec 10, 2024
00452e5
Interface to Type
TheCactusBlue Dec 10, 2024
823634a
remove npm from package
TheCactusBlue Dec 10, 2024
7262204
remove mistakenly installed packages
TheCactusBlue Dec 10, 2024
c58001b
add TODO
TheCactusBlue Dec 10, 2024
e02d390
add sendadminrequest
TheCactusBlue Dec 10, 2024
dad701e
move the cards and the charts
TheCactusBlue Dec 10, 2024
2464be5
Merge branch 'dev' into metric-dashboard
N2D4 Dec 10, 2024
a6547a1
work on the globe
TheCactusBlue Dec 10, 2024
85548a2
Upgrade backend to Next.js 15 (#360)
N2D4 Dec 10, 2024
5738d9b
chore: update known errors to correct URL (#356)
mrl5 Dec 10, 2024
606c09d
Fix build
N2D4 Dec 10, 2024
f30de21
Added Prisma Accelerate (#363)
fomalhautb Dec 10, 2024
e8768b7
Merge branch 'dev' into metric-dashboard
N2D4 Dec 10, 2024
396d148
bump pnpm-lock
TheCactusBlue Dec 10, 2024
019c05a
move recharts to peer dependency
TheCactusBlue Dec 10, 2024
78f8eaa
responsive design for the globe
TheCactusBlue Dec 11, 2024
00b6310
print pnpm lock in docker
N2D4 Dec 11, 2024
2999efa
move back to regular deps
TheCactusBlue Dec 11, 2024
7343dbd
mark recharts as optional
TheCactusBlue Dec 11, 2024
1098a5b
actually add peerdep
TheCactusBlue Dec 11, 2024
77be2ad
disable side effects
TheCactusBlue Dec 11, 2024
196dbd8
Update tsconfig
N2D4 Dec 11, 2024
0aea48d
dynamic import the rechart
TheCactusBlue Dec 11, 2024
2fa1377
move recharts back
TheCactusBlue Dec 11, 2024
33b5acb
Fix lint
N2D4 Dec 11, 2024
164a900
fix
N2D4 Dec 11, 2024
92c1f99
Fix trusted domains table
N2D4 Dec 10, 2024
585553f
Fix dotenv file
N2D4 Dec 11, 2024
61b0d4f
Fix domains page
N2D4 Dec 11, 2024
3002921
chore: update package versions
N2D4 Dec 11, 2024
2b4da7e
Don't recommend Prettier
N2D4 Dec 11, 2024
59ed31b
Use Ubicloud for GitHub Actions (#365)
N2D4 Dec 11, 2024
6578f95
Merge remote-tracking branch 'origin/dev' into metric-dashboard
TheCactusBlue Dec 11, 2024
adc70f8
hover the dots
TheCactusBlue Dec 11, 2024
ca41242
factor out the display into another component
TheCactusBlue Dec 11, 2024
75ab6c2
make it look better
TheCactusBlue Dec 11, 2024
644f8dd
correct the total number
TheCactusBlue Dec 11, 2024
1de0307
add activity info
TheCactusBlue Dec 12, 2024
3acf8ae
use relative times
TheCactusBlue Dec 12, 2024
9093079
actually use suspense
TheCactusBlue Dec 12, 2024
e21b971
Merge branch 'dev' into metric-dashboard
N2D4 Dec 12, 2024
19c4b5f
show login methods
TheCactusBlue Dec 12, 2024
374797d
rollback error sink
TheCactusBlue Dec 12, 2024
455f189
remove placeholders
TheCactusBlue Dec 13, 2024
51c5480
Apply suggestions from code review
TheCactusBlue Dec 13, 2024
be3de75
fix more errorz
TheCactusBlue Dec 13, 2024
ee08381
remove duplicate geojson
TheCactusBlue Dec 13, 2024
e1f765c
population normalize the globe
TheCactusBlue Dec 13, 2024
a821052
fix the off-by-one error
TheCactusBlue Dec 13, 2024
57e1fe0
narrow down the types
TheCactusBlue Dec 13, 2024
0743f4c
add a snapshot test
TheCactusBlue Dec 13, 2024
532e71c
generate test properly
TheCactusBlue Dec 13, 2024
a2afa26
Add ipData field to backendContext
N2D4 Dec 14, 2024
d65dcc1
solve the issue with nan values in globe
TheCactusBlue Dec 14, 2024
8c2d54f
correct the event records
TheCactusBlue Dec 14, 2024
f8f8d7f
Update apps/backend/src/app/api/v1/internal/metrics/route.tsx
TheCactusBlue Dec 14, 2024
7beadfe
forgot to import yup
TheCactusBlue Dec 14, 2024
de0c79a
Increase the number of loops
TheCactusBlue Dec 14, 2024
f96264f
another test case
TheCactusBlue Dec 14, 2024
d96b194
move inline snapshots out
TheCactusBlue Dec 14, 2024
992071d
Update apps/backend/src/app/api/v1/internal/metrics/route.tsx
TheCactusBlue Dec 14, 2024
5bc8374
Retry Prisma transactions automatically
N2D4 Dec 12, 2024
195448b
Don't retry transactions during development
N2D4 Dec 12, 2024
23d496b
Fail instead of retrying Prisma tx without transaction
N2D4 Dec 12, 2024
9096aae
Rename retryTransaction
N2D4 Dec 12, 2024
599ec56
chore: update package versions
N2D4 Dec 13, 2024
a31a848
added more error capturing info
fomalhautb Dec 14, 2024
a439598
fixed accelerate and docker
fomalhautb Dec 14, 2024
9c40b9e
Improved error capturing
N2D4 Dec 14, 2024
1a558c7
Merge branch 'dev' into metric-dashboard
N2D4 Dec 14, 2024
2b1c817
use raw sql for fetching recently updated users
TheCactusBlue Dec 15, 2024
75344e1
map the data
TheCactusBlue Dec 15, 2024
03b69e0
Merge branch 'dev' into metric-dashboard
N2D4 Dec 17, 2024
65b88a8
Merge branch 'dev' into metric-dashboard
N2D4 Dec 20, 2024
80591b2
j
N2D4 Dec 20, 2024
7874506
seed script stuff
N2D4 Dec 20, 2024
1cb0c70
Fix pie chart
N2D4 Dec 20, 2024
3aa483c
Improve globe
N2D4 Dec 20, 2024
c99efd4
LIVE indicator
N2D4 Dec 20, 2024
90599ba
Merge branch 'dev' into metric-dashboard
N2D4 Dec 20, 2024
f201130
Improve globe perf
N2D4 Dec 20, 2024
6f9083f
Merge branch 'dev' into metric-dashboard
N2D4 Dec 20, 2024
335715d
Signups -> Sign Ups
N2D4 Dec 20, 2024
e5bb50a
More
N2D4 Dec 20, 2024
04ef4e7
Fix
N2D4 Dec 20, 2024
b757a32
More fixes
N2D4 Dec 21, 2024
1f9a223
Merge branch 'dev' into metric-dashboard
N2D4 Dec 21, 2024
a6ee07d
Fix tests
N2D4 Dec 22, 2024
d944062
Better graphs
N2D4 Dec 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"hookform",
"hostable",
"INBUCKET",
"ipcountry",
"Jwks",
"JWTs",
"katex",
Expand Down
226 changes: 226 additions & 0 deletions apps/backend/src/app/api/v1/internal/metrics/route.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import { prismaClient } from "@/prisma-client";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { ContactChannel, ProjectUser } from "@prisma/client";
import { adaptSchema, adminAuthTypeSchema, yupArray, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import yup from 'yup';

type DataPoints = yup.InferType<typeof DataPointsSchema>;

const DataPointsSchema = yupArray(yupObject({
date: yupString().defined(),
activity: yupNumber().defined(),
}).defined()).defined();


async function loadUsersByCountry(projectId: string): Promise<Record<string, number>> {
const a = await prismaClient.$queryRaw<{countryCode: string|null, userCount: bigint}[]>`
WITH LatestEvent AS (
SELECT "data"->'userId' AS "userId",
"countryCode", MAX("eventStartedAt") AS latest_timestamp
FROM "Event"
LEFT JOIN "EventIpInfo" eip
ON "Event"."endUserIpInfoGuessId" = eip.id
WHERE '$user-activity' = ANY("systemEventTypeIds"::text[])
AND "data"->>'projectId' = ${projectId}
GROUP BY "userId", "countryCode"
)
SELECT "countryCode", COUNT(DISTINCT "userId") AS "userCount"
FROM LatestEvent
GROUP BY "countryCode"
ORDER BY "userCount" DESC;
`;

const rec = Object.fromEntries(
a.map(({ userCount, countryCode }) => [countryCode, Number(userCount)])
.filter(([countryCode, userCount]) => countryCode)
);
return rec;
}

async function loadTotalUsers(projectId: string, now: Date): Promise<DataPoints> {
return (await prismaClient.$queryRaw<{date: Date, dailyUsers: bigint, cumUsers: bigint}[]>`
WITH date_series AS (
SELECT GENERATE_SERIES(
${now}::date - INTERVAL '1 month',
${now}::date,
'1 day'
)
AS registration_day
)
SELECT
ds.registration_day AS "date",
COALESCE(COUNT(pu."projectUserId"), 0) AS "dailyUsers",
SUM(COALESCE(COUNT(pu."projectUserId"), 0)) OVER (ORDER BY ds.registration_day) AS "cumUsers"
FROM date_series ds
LEFT JOIN "ProjectUser" pu
ON DATE(pu."createdAt") = ds.registration_day AND pu."projectId" = ${projectId}
GROUP BY ds.registration_day
ORDER BY ds.registration_day
`).map((x) => ({
date: x.date.toLocaleDateString('en-US', { month: 'short', day: '2-digit' }),
activity: Number(x.dailyUsers),
}));
}

async function loadDailyActiveUsers(projectId: string, now: Date) {
const res = await prismaClient.$queryRaw<{day: Date, dau: bigint}[]>`
WITH date_series AS (
SELECT GENERATE_SERIES(
${now}::date - INTERVAL '1 month',
${now}::date,
'1 day'
)
AS "day"
),
daily_users AS (
SELECT
DATE_TRUNC('day', "eventStartedAt") AS "day",
COUNT(DISTINCT "data"->'userId') AS "dau"
FROM "Event"
WHERE "eventStartedAt" >= ${now} - INTERVAL '1 month'
AND "eventStartedAt" < ${now}
AND '$user-activity' = ANY("systemEventTypeIds"::text[])
AND "data"->>'projectId' = ${projectId}
GROUP BY DATE_TRUNC('day', "eventStartedAt")
)
SELECT ds."day", COALESCE(du.dau, 0) AS dau
FROM date_series ds
LEFT JOIN daily_users du
ON ds."day" = du."day"
ORDER BY ds."day"
`;

return res.map(x => ({
date: x.day.toLocaleDateString('en-US', { month: 'short', day: '2-digit' }),
activity: Number(x.dau),
}));
}

async function loadLoginMethods(projectId: string): Promise<{method: string, count: number }[]> {
return await prismaClient.$queryRaw<{ method: string, count: number }[]>`
WITH tab AS (
SELECT COALESCE(soapc."type"::text, poapc."type"::text, 'other') AS "method", method.id AS id FROM "AuthMethod" method
LEFT JOIN "OAuthAuthMethod" oaam ON method.id = oaam."authMethodId"
LEFT JOIN "OAuthProviderConfig" oapc
ON oaam."projectConfigId" = oapc."projectConfigId" AND oaam."oauthProviderConfigId" = oapc.id
LEFT JOIN "StandardOAuthProviderConfig" soapc
ON oapc."projectConfigId" = soapc."projectConfigId" AND oapc.id = soapc.id
LEFT JOIN "ProxiedOAuthProviderConfig" poapc
ON oapc."projectConfigId" = poapc."projectConfigId" AND oapc.id = poapc.id
WHERE method."projectId" = ${projectId})
SELECT LOWER("method") AS method, COUNT(id)::int AS "count" FROM tab
GROUP BY "method"
`;
}

function simplifyUsers(users: (ProjectUser & { contactChannels: ContactChannel[] })[]): any {
Copy link

Choose a reason for hiding this comment

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

Avoid using any type. Define a specific return type for simplifyUsers.

Copy link
Contributor

Choose a reason for hiding this comment

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

++

return users.map((user) => ({
id: user.projectUserId,
display_name: user.displayName,
email: user.contactChannels.find(x => x.isPrimary)?.value ?? '-',
created_at_millis: user.createdAt.getTime(),
updated_at_millis: user.updatedAt.getTime(),
Copy link
Contributor

Choose a reason for hiding this comment

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

can we get rid of the updated_at_millis now?

Suggested change
updated_at_millis: user.updatedAt.getTime(),

}));
}

async function loadRecentlyActiveUsers(projectId: string):
Promise<(ProjectUser & { contactChannels: ContactChannel[] })[]> {

// use the Events table to get the most recent activity
const events = await prismaClient.$queryRaw<{ data: any }[]>`
Copy link

Choose a reason for hiding this comment

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

Using prismaClient.$queryRaw with template literals can lead to SQL injection vulnerabilities. Consider using parameterized queries instead.

WITH RankedEvents AS (
SELECT
"data",
ROW_NUMBER() OVER (
PARTITION BY "data"->>'userId'
ORDER BY "eventStartedAt" DESC
) as rn
FROM "Event"
WHERE "data"->>'projectId' = ${projectId}
AND '$user-activity' = ANY("systemEventTypeIds"::text[])
)
SELECT "data"
FROM RankedEvents
WHERE rn = 1
LIMIT 10
`;
return await prismaClient.projectUser.findMany({
Copy link
Contributor

Choose a reason for hiding this comment

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

you have to return last_updated_at somewhere

take: 10,
N2D4 marked this conversation as resolved.
Show resolved Hide resolved
where: { projectId, projectUserId: { in: events.map(x => (x.data as any).userId) } },
N2D4 marked this conversation as resolved.
Show resolved Hide resolved
include: { contactChannels: true },
orderBy: [{
updatedAt: 'desc'
}]
N2D4 marked this conversation as resolved.
Show resolved Hide resolved
});
}

export const GET = createSmartRouteHandler({
metadata: {
hidden: true,
},
request: yupObject({
auth: yupObject({
type: adminAuthTypeSchema.defined(),
project: adaptSchema.defined(),
}),
}),
response: yupObject({
statusCode: yupNumber().oneOf([200]).defined(),
bodyType: yupString().oneOf(["json"]).defined(),
body: yupObject({
total_users: yupNumber().integer().defined(),
daily_users: DataPointsSchema,
daily_active_users: DataPointsSchema,
// TODO: Narrow down the types further
users_by_country: yupMixed().defined(),
TheCactusBlue marked this conversation as resolved.
Show resolved Hide resolved
recently_registered: yupMixed().defined(),
recently_active: yupMixed().defined(),
login_methods: yupMixed().defined(),
}).defined(),
}),
handler: async (req) => {
N2D4 marked this conversation as resolved.
Show resolved Hide resolved
const now = new Date();

const [
totalUsers,
dailyUsers,
dailyActiveUsers,
usersByCountry,
recentlyRegistered,
recentlyActive,
loginMethods
] = await Promise.all([
prismaClient.projectUser.count({
where: { projectId: req.auth.project.id, },
}),
loadTotalUsers(req.auth.project.id, now),
loadDailyActiveUsers(req.auth.project.id, now),
loadUsersByCountry(req.auth.project.id),
prismaClient.projectUser.findMany({
take: 10,
where: { projectId: req.auth.project.id, },
include: { contactChannels: true },
orderBy: [{
createdAt: 'desc'
}]
}),
loadRecentlyActiveUsers(req.auth.project.id),
loadLoginMethods(req.auth.project.id),
] as const);

return {
statusCode: 200,
bodyType: "json",
body: {
total_users: totalUsers,
daily_users: dailyUsers,
daily_active_users: dailyActiveUsers,
users_by_country: usersByCountry,
recently_registered: simplifyUsers(recentlyRegistered),
recently_active: simplifyUsers(recentlyActive),
login_methods: loginMethods,
}
};
},
});

3 changes: 3 additions & 0 deletions apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@next/mdx": "^14",
"@node-oauth/oauth2-server": "^5.1.0",
"@radix-ui/react-icons": "^1.3.1",
"@react-hook/resize-observer": "^2.0.2",
"@sentry/nextjs": "^8.40.0",
"@stackframe/stack": "workspace:*",
"@stackframe/stack-emails": "workspace:*",
Expand All @@ -47,8 +48,10 @@
"posthog-js": "^1.149.1",
"react": "^18.2",
"react-dom": "^18",
"react-globe.gl": "^2.28.2",
"react-hook-form": "^7.53.1",
"react-icons": "^5.0.1",
"recharts": "^2.14.1",
"rehype-katex": "^7",
"remark-gfm": "^4",
"remark-heading-id": "^1.0.1",
Expand Down
Binary file added apps/dashboard/public/static/globe_background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading