mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Merge dev into update-oauth-docs
This commit is contained in:
commit
e540c93c58
@ -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 => {
|
||||
|
||||
@ -15,7 +15,7 @@ const DataPointsSchema = yupArray(yupObject({
|
||||
}).defined()).defined();
|
||||
|
||||
|
||||
async function loadUsersByCountry(tenancy: Tenancy): Promise<Record<string, number>> {
|
||||
async function loadUsersByCountry(tenancy: Tenancy, includeAnonymous: boolean = false): Promise<Record<string, number>> {
|
||||
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<Record<string, numb
|
||||
ON "Event"."endUserIpInfoGuessId" = eip.id
|
||||
WHERE '$user-activity' = ANY("systemEventTypeIds"::text[])
|
||||
AND "data"->>'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<Record<string, numb
|
||||
return rec;
|
||||
}
|
||||
|
||||
async function loadTotalUsers(tenancy: Tenancy, now: Date): Promise<DataPoints> {
|
||||
async function loadTotalUsers(tenancy: Tenancy, now: Date, includeAnonymous: boolean = false): Promise<DataPoints> {
|
||||
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<DataPoints>
|
||||
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<DataPoints>
|
||||
}));
|
||||
}
|
||||
|
||||
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<UsersCrud["Admin"]["Read"][]> {
|
||||
async function loadRecentlyActiveUsers(tenancy: Tenancy, includeAnonymous: boolean = false): Promise<UsersCrud["Admin"]["Read"][]> {
|
||||
// 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<UsersCrud["Adm
|
||||
) as rn
|
||||
FROM "Event"
|
||||
WHERE "data"->>'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);
|
||||
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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 (
|
||||
<PageLayout fillWidth>
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -297,9 +297,14 @@ export class StackAdminInterface extends StackServerInterface {
|
||||
);
|
||||
}
|
||||
|
||||
async getMetrics(): Promise<any> {
|
||||
async getMetrics(includeAnonymous: boolean = false): Promise<any> {
|
||||
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",
|
||||
},
|
||||
|
||||
@ -51,8 +51,8 @@ export class _StackAdminAppImplIncomplete<HasTokenStore extends boolean, Project
|
||||
private readonly _svixTokenCache = createCache(async () => {
|
||||
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<HasTokenStore extends boolean, Project
|
||||
return {
|
||||
...super[stackAppInternalsSymbol],
|
||||
// IF_PLATFORM react-like
|
||||
useMetrics: (): any => {
|
||||
return useAsyncCache(this._metricsCache, [], "useMetrics()");
|
||||
useMetrics: (includeAnonymous: boolean = false): any => {
|
||||
return useAsyncCache(this._metricsCache, [includeAnonymous] as const, "useMetrics()");
|
||||
}
|
||||
// END_PLATFORM
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalEnv": [
|
||||
"STACK_*",
|
||||
"CRON_SECRET",
|
||||
"NEXT_PUBLIC_*",
|
||||
"NEXT_PUBLIC_SENTRY_*",
|
||||
"SENTRY_*",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user