Compare commits

...

10 Commits

Author SHA1 Message Date
Ephraim Atta-Duncan
8d85d01c93 chore: remove unused type 2025-01-30 05:20:38 +00:00
Ephraim Atta-Duncan
cdb980bd4c chore: avoid using lastSignedIn 2025-01-29 07:25:07 +00:00
Ephraim Duncan
413c3cbc6e Merge branch 'main' into feat/mau 2025-01-29 00:30:43 +00:00
Ephraim Atta-Duncan
ff4ba71ef3 chore: add tooltip 2025-01-29 00:25:07 +00:00
Ephraim Atta-Duncan
9bc825b308 chore: cleanup 2025-01-28 16:19:14 +00:00
Ephraim Atta-Duncan
d7f7cbf27f fix: exclude disabled users 2025-01-28 15:57:15 +00:00
Ephraim Atta-Duncan
97f7d77a34 chore: cleanup 2025-01-28 15:40:40 +00:00
Ephraim Atta-Duncan
6993c52b2a feat: mau 2025-01-28 15:29:36 +00:00
Ephraim Atta-Duncan
68c8eba2c3 chore: wip 2025-01-28 05:08:28 +00:00
Ephraim Atta-Duncan
c40e802396 feat: add mau card 2025-01-25 12:23:26 +00:00
7 changed files with 152 additions and 8 deletions

View File

@@ -0,0 +1,73 @@
'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';
export type MonthlyActiveUsersChartProps = {
className?: string;
title: string;
cummulative?: boolean;
data: GetMonthlyActiveUsersResult;
};
const CustomTooltip = ({ active, payload, label }: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p>{label}</p>
<p className="text-documenso">
{payload[0].name === 'cume_count' ? 'Cumulative MAU' : 'Monthly Active Users'}:{' '}
<span className="text-black">{Number(payload[0].value).toLocaleString('en-US')}</span>
</p>
</div>
);
}
return null;
};
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 (
<div className={className}>
<div className="border-border flex flex-1 flex-col justify-center rounded-2xl border p-6 pl-2">
<div className="mb-6 flex px-4">
<h3 className="text-lg font-semibold">{title}</h3>
</div>
<ResponsiveContainer width="100%" height={400}>
<BarChart data={formattedData}>
<XAxis dataKey="month" />
<YAxis />
<Tooltip content={<CustomTooltip />} cursor={{ fill: 'hsl(var(--primary) / 10%)' }} />
<Bar
dataKey={cummulative ? 'cume_count' : 'count'}
fill="hsl(var(--primary))"
radius={[4, 4, 0, 0]}
maxBarSize={60}
label={cummulative ? 'Cumulative MAU' : 'Monthly Active Users'}
/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
);
};

View File

@@ -18,6 +18,7 @@ import { setupI18nSSR } from '@documenso/lib/client-only/providers/i18n.server';
import { getDocumentStats } from '@documenso/lib/server-only/admin/get-documents-stats';
import { getRecipientsStats } from '@documenso/lib/server-only/admin/get-recipients-stats';
import {
getMonthlyActiveUsers,
getUserWithSignedDocumentMonthlyGrowth,
getUsersCount,
getUsersWithSubscriptionsCount,
@@ -26,6 +27,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 { SignerConversionChart } from './signer-conversion-chart';
import { UserWithDocumentChart } from './user-with-document';
@@ -43,6 +45,7 @@ export default async function AdminStatsPage() {
// userWithAtLeastOneDocumentPerMonth,
// userWithAtLeastOneDocumentSignedPerMonth,
MONTHLY_USERS_SIGNED,
MONTHLY_ACTIVE_USERS,
] = await Promise.all([
getUsersCount(),
getUsersWithSubscriptionsCount(),
@@ -52,6 +55,7 @@ export default async function AdminStatsPage() {
// getUserWithAtLeastOneDocumentPerMonth(),
// getUserWithAtLeastOneDocumentSignedPerMonth(),
getUserWithSignedDocumentMonthlyGrowth(),
getMonthlyActiveUsers(),
]);
return (
@@ -68,7 +72,6 @@ export default async function AdminStatsPage() {
title={_(msg`Active Subscriptions`)}
value={usersWithSubscriptionsCount}
/>
<CardMetric
icon={FileCog}
title={_(msg`App Version`)}
@@ -132,6 +135,14 @@ export default async function AdminStatsPage() {
<Trans>Charts</Trans>
</h3>
<div className="mt-5 grid grid-cols-2 gap-8">
<MonthlyActiveUsersChart title={_(msg`MAU (signed in)`)} data={MONTHLY_ACTIVE_USERS} />
<MonthlyActiveUsersChart
title={_(msg`Cumulative MAU (signed in)`)}
data={MONTHLY_ACTIVE_USERS}
cummulative
/>
<UserWithDocumentChart
data={MONTHLY_USERS_SIGNED}
title={_(msg`MAU (created document)`)}

View File

@@ -1,8 +1,8 @@
'use client';
import { DateTime } from 'luxon';
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
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 { GetUserWithDocumentMonthlyGrowth } from '@documenso/lib/server-only/admin/get-users-stats';
@@ -23,7 +23,7 @@ const CustomTooltip = ({
}: TooltipProps<ValueType, NameType> & { tooltip?: string }) => {
if (active && payload && payload.length) {
return (
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<div className="z-100 w-60 space-y-1 rounded-md border border-solid bg-white p-2 px-3">
<p className="">{label}</p>
<p className="text-documenso">
{`${tooltip} : `}

15
package-lock.json generated
View File

@@ -35722,6 +35722,21 @@
"engines": {
"node": ">=6"
}
},
"packages/trpc/node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.6.tgz",
"integrity": "sha512-hNukAxq7hu4o5/UjPp5jqoBEtrpCbOmnUqZSKNJG8GrUVzfq0ucdhQFVrHcLRMvQcwqqDh1a5AJN9ORnNDpgBQ==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}

View File

@@ -1,7 +1,11 @@
import { DateTime } from 'luxon';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, SubscriptionStatus } from '@documenso/prisma/client';
import { kyselyPrisma, prisma, sql } from '@documenso/prisma';
import {
DocumentStatus,
SubscriptionStatus,
UserSecurityAuditLogType,
} from '@documenso/prisma/client';
export const getUsersCount = async () => {
return await prisma.user.count();
@@ -80,3 +84,38 @@ export const getUserWithSignedDocumentMonthlyGrowth = async () => {
signed_count: Number(row.signed_count),
}));
};
export type GetMonthlyActiveUsersResult = Array<{
month: string;
count: number;
cume_count: number;
}>;
export const getMonthlyActiveUsers = async () => {
const qb = kyselyPrisma.$kysely
.selectFrom('UserSecurityAuditLog')
.select(({ fn }) => [
fn<Date>('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']).as('month'),
fn.count('userId').distinct().as('count'),
fn
.sum(fn.count('userId').distinct())
.over((ob) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ob.orderBy(fn('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']) as any),
)
.as('cume_count'),
])
// @ts-expect-error - Raw SQL enum casting not properly typed by Kysely
.where(sql`type = ${UserSecurityAuditLogType.SIGN_IN}::"UserSecurityAuditLogType"`)
.groupBy(({ fn }) => fn('DATE_TRUNC', [sql.lit('MONTH'), 'UserSecurityAuditLog.createdAt']))
.orderBy('month', 'desc')
.limit(12);
const result = await qb.execute();
return result.map((row) => ({
month: DateTime.fromJSDate(row.month).toFormat('yyyy-MM'),
count: Number(row.count),
cume_count: Number(row.cume_count),
}));
};

View File

@@ -36,4 +36,4 @@
"typescript": "5.6.2",
"zod-prisma-types": "3.1.9"
}
}
}

View File

@@ -13,8 +13,14 @@ import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import type { TDocument } from '@documenso/lib/types/document';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentVisibility, TeamMemberRole } from '@documenso/prisma/client';
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
import {
DocumentStatus,
DocumentVisibility,
type Field,
type Recipient,
SendStatus,
TeamMemberRole,
} from '@documenso/prisma/client';
import {
DocumentGlobalAuthAccessSelect,
DocumentGlobalAuthAccessTooltip,