diff --git a/apps/web/src/app/(dashboard)/admin/stats/mau.tsx b/apps/web/src/app/(dashboard)/admin/stats/mau.tsx new file mode 100644 index 000000000..512463c31 --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/stats/mau.tsx @@ -0,0 +1,64 @@ +'use client'; + +import { DateTime } from 'luxon'; +import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; + +import type { GetMonthlyActiveUsersResult } from '@documenso/lib/server-only/admin/get-users-stats'; + +export type MonthlyActiveUsersChartProps = { + className?: string; + title: string; + cummulative?: boolean; + data: GetMonthlyActiveUsersResult; +}; + +export const MonthlyActiveUsersChart = ({ + className, + data, + title, + cummulative = false, +}: MonthlyActiveUsersChartProps) => { + const formattedData = [...data].reverse().map(({ month, count, cume_count }) => { + return { + month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('MMM yyyy'), + count: Number(count), + cume_count: Number(cume_count), + }; + }); + + return ( +
+
+
+

{title}

+
+ + + + + + + [ + Number(value).toLocaleString('en-US'), + cummulative ? 'Cumulative MAU' : 'Monthly Active Users', + ]} + cursor={{ fill: 'hsl(var(--primary) / 10%)' }} + /> + + + + +
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/admin/stats/monthly-active-users-chart.tsx b/apps/web/src/app/(dashboard)/admin/stats/monthly-active-users-chart.tsx deleted file mode 100644 index 1ffd6d216..000000000 --- a/apps/web/src/app/(dashboard)/admin/stats/monthly-active-users-chart.tsx +++ /dev/null @@ -1,84 +0,0 @@ -'use client'; - -import { DateTime } from 'luxon'; -import type { TooltipProps } from 'recharts'; -import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; -import type { NameType, ValueType } from 'recharts/types/component/DefaultTooltipContent'; - -import type { GetMonthlyActiveUsersResult } from '@documenso/lib/server-only/admin/get-users-stats'; -import { cn } from '@documenso/ui/lib/utils'; - -export type MonthlyActiveUsersChartProps = { - className?: string; - title: string; - data: GetMonthlyActiveUsersResult; - tooltip?: string; -}; - -const CustomTooltip = ({ - active, - payload, - label, - tooltip, -}: TooltipProps & { tooltip?: string }) => { - if (active && payload && payload.length) { - return ( -
-

{label}

-

- {`${tooltip} : `} - {payload[0].value} -

-
- ); - } - - return null; -}; - -export const MonthlyActiveUsersChart = ({ - className, - title, - data, - tooltip, -}: MonthlyActiveUsersChartProps) => { - const formattedData = (data: GetMonthlyActiveUsersResult) => { - return [...data].reverse().map(({ month, count }) => ({ - month: DateTime.fromFormat(month, 'yyyy-MM').toFormat('LLL'), - count: Number(count), - })); - }; - - return ( -
-
-

{title}

-
- -
- - - - - - } - labelStyle={{ - color: 'hsl(var(--primary-foreground))', - }} - cursor={{ fill: 'hsl(var(--primary) / 10%)' }} - /> - - - - -
-
- ); -}; diff --git a/apps/web/src/app/(dashboard)/admin/stats/page.tsx b/apps/web/src/app/(dashboard)/admin/stats/page.tsx index 9384da065..5e22fa950 100644 --- a/apps/web/src/app/(dashboard)/admin/stats/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/stats/page.tsx @@ -29,7 +29,7 @@ import { getSignerConversionMonthly } from '@documenso/lib/server-only/user/get- import { CardMetric } from '~/components/(dashboard)/metric-card/metric-card'; -import { MonthlyActiveUsersChart } from './monthly-active-users-chart'; +import { MonthlyActiveUsersChart } from './mau'; import { SignerConversionChart } from './signer-conversion-chart'; import { UserWithDocumentChart } from './user-with-document'; @@ -147,8 +147,14 @@ export default async function AdminStatsPage() { + + + { export type GetMonthlyActiveUsersResult = Array<{ month: string; count: number; + cume_count: number; }>; type GetMonthlyActiveUsersQueryResult = Array<{ month: Date; count: bigint; + cume_count: bigint; }>; export const getMonthlyActiveUsers = async () => { const result = await prisma.$queryRaw` SELECT DATE_TRUNC('month', "lastSignedIn") AS "month", - COUNT(DISTINCT "id") as "count" + COUNT(DISTINCT "id") as "count", + SUM(COUNT(DISTINCT "id")) OVER (ORDER BY DATE_TRUNC('month', "lastSignedIn")) as "cume_count" FROM "User" WHERE "lastSignedIn" >= NOW() - INTERVAL '1 year' - GROUP BY "month" + GROUP BY DATE_TRUNC('month', "lastSignedIn") ORDER BY "month" DESC LIMIT 12 `; @@ -116,5 +119,6 @@ export const getMonthlyActiveUsers = async () => { return result.map((row) => ({ month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'), count: Number(row.count), + cume_count: Number(row.cume_count), })); }; diff --git a/packages/prisma/mau-seed.ts b/packages/prisma/mau-seed.ts new file mode 100644 index 000000000..007550a5d --- /dev/null +++ b/packages/prisma/mau-seed.ts @@ -0,0 +1,62 @@ +import { DateTime } from 'luxon'; + +import { hashSync } from '@documenso/lib/server-only/auth/hash'; + +import { prisma } from '.'; +import { Role } from './client'; + +const USERS_PER_MONTH = 20; +const MONTHS_OF_HISTORY = 12; + +export const seedMAUData = async () => { + const now = DateTime.now(); + + for (let monthsAgo = MONTHS_OF_HISTORY - 1; monthsAgo >= 0; monthsAgo--) { + const monthStart = now.minus({ months: monthsAgo }).startOf('month'); + const monthEnd = monthStart.endOf('month'); + + console.log(`Seeding users for ${monthStart.toFormat('yyyy-MM')}`); + + const users = await Promise.all( + Array.from({ length: USERS_PER_MONTH }).map(async (_, index) => { + const createdAt = DateTime.fromMillis( + monthStart.toMillis() + Math.random() * (monthEnd.toMillis() - monthStart.toMillis()), + ).toJSDate(); + + const lastSignedIn = + Math.random() > 0.3 + ? DateTime.fromMillis( + createdAt.getTime() + Math.random() * (now.toMillis() - createdAt.getTime()), + ).toJSDate() + : createdAt; + + return prisma.user.create({ + data: { + name: `MAU Test User ${monthsAgo}-${index}`, + email: `mau-test-${monthsAgo}-${index}@documenso.com`, + password: hashSync('password'), + emailVerified: createdAt, + createdAt, + lastSignedIn, + roles: [Role.USER], + }, + }); + }), + ); + + console.log(`Created ${users.length} users for ${monthStart.toFormat('yyyy-MM')}`); + } +}; + +// Run the seed if this file is executed directly +if (require.main === module) { + seedMAUData() + .then(() => { + console.log('MAU seed completed successfully'); + process.exit(0); + }) + .catch((error) => { + console.error('Error seeding MAU data:', error); + process.exit(1); + }); +} diff --git a/packages/prisma/package.json b/packages/prisma/package.json index 5cc8497a6..964fbbd01 100644 --- a/packages/prisma/package.json +++ b/packages/prisma/package.json @@ -15,7 +15,8 @@ "prisma:migrate-deploy": "prisma migrate deploy", "prisma:migrate-reset": "prisma migrate reset", "prisma:seed": "prisma db seed", - "prisma:studio": "prisma studio" + "prisma:studio": "prisma studio", + "prisma:seed-mau": "tsx ./mau-seed.ts" }, "prisma": { "seed": "tsx ./seed-database.ts" @@ -36,4 +37,4 @@ "typescript": "5.6.2", "zod-prisma-types": "3.1.9" } -} \ No newline at end of file +}