From 4b832200cb20ef034b407eb08b9e3e8d6a644dfb Mon Sep 17 00:00:00 2001 From: Tim Raderschad Date: Thu, 22 Aug 2024 07:14:24 +0200 Subject: [PATCH] feat(web): improve ab testing overview page --- apps/web/src/components/Test/Metrics.tsx | 23 ++------ apps/web/src/components/Test/Section.tsx | 26 ++++------ apps/web/src/components/Test/Serves.tsx | 24 ++------- apps/web/src/components/charts/Donut.tsx | 9 ++-- .../src/pages/projects/[projectId]/index.tsx | 12 ++++- apps/web/src/server/trpc/router/project.ts | 52 +++++++++++++------ 6 files changed, 69 insertions(+), 77 deletions(-) diff --git a/apps/web/src/components/Test/Metrics.tsx b/apps/web/src/components/Test/Metrics.tsx index 107b1a65..02b60c69 100644 --- a/apps/web/src/components/Test/Metrics.tsx +++ b/apps/web/src/components/Test/Metrics.tsx @@ -1,37 +1,22 @@ -import type { Event } from "@prisma/client"; import { DonutChart } from "components/charts/Donut"; -import { useMemo } from "react"; +import type { ProjectClientEvents } from "pages/projects/[projectId]"; import type { ClientOption } from "server/trpc/router/project"; const Metrics = ({ actEvents, options, }: { - actEvents: Event[]; + actEvents: ProjectClientEvents; options: ClientOption[]; }) => { const labels = options.map((option) => option.identifier); - const actualData = useMemo(() => { - return options.map((option) => { - return { - variant: option.identifier, - events: actEvents.filter( - (event) => event.selectedVariant === option.identifier - ).length, - }; - }); - }, [options, actEvents]); - - const absPings = actualData.reduce((accumulator, value) => { - return accumulator + value.events; - }, 0); return (
acc + e._count._all, 0)} variants={labels} - events={actualData} + events={actEvents} totalText="Interactions" />
diff --git a/apps/web/src/components/Test/Section.tsx b/apps/web/src/components/Test/Section.tsx index 30a0cc9e..6dd7e08c 100644 --- a/apps/web/src/components/Test/Section.tsx +++ b/apps/web/src/components/Test/Section.tsx @@ -1,6 +1,5 @@ -import type { Event, Test } from "@prisma/client"; +import type { Test } from "@prisma/client"; import * as Popover from "@radix-ui/react-popover"; -import { AbbyEventType } from "@tryabby/core"; import { Modal } from "components/Modal"; import { TitleEdit } from "components/TitleEdit"; import { Button } from "components/ui/button"; @@ -17,6 +16,7 @@ import { trpc } from "utils/trpc"; import { Metrics } from "./Metrics"; import { Serves } from "./Serves"; import Weights from "./Weights"; +import type { ProjectClientEvents } from "pages/projects/[projectId]"; function getBestVariant({ absPings, @@ -128,11 +128,13 @@ export const Card = ({ const Section = ({ name, options = [], - events = [], + actEvents, + pingEvents, id, }: Test & { options: ClientOption[]; - events: Event[]; + pingEvents: ProjectClientEvents; + actEvents: ProjectClientEvents; }) => { const router = useRouter(); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); @@ -140,7 +142,7 @@ const Section = ({ const showAdvancedTestStats = useFeatureFlag("AdvancedTestStats"); const bestVariant = getBestVariant({ - absPings: events.filter((event) => event.type === AbbyEventType.ACT).length, + absPings: actEvents.length + pingEvents.length, options, }).identifier; @@ -201,12 +203,7 @@ const Section = ({

} > - event.type === AbbyEventType.PING - )} - /> + } > - event.type === AbbyEventType.ACT - )} - /> +
diff --git a/apps/web/src/components/Test/Serves.tsx b/apps/web/src/components/Test/Serves.tsx index bbb16c7b..10e258b3 100644 --- a/apps/web/src/components/Test/Serves.tsx +++ b/apps/web/src/components/Test/Serves.tsx @@ -1,38 +1,22 @@ -import type { Event } from "@prisma/client"; import { DonutChart } from "components/charts/Donut"; -import { useMemo } from "react"; +import type { ProjectClientEvents } from "pages/projects/[projectId]"; import type { ClientOption } from "server/trpc/router/project"; const Serves = ({ pingEvents, options, }: { - pingEvents: Event[]; + pingEvents: ProjectClientEvents; options: ClientOption[]; }) => { const labels = options.map((option) => option.identifier); - const actualData = useMemo(() => { - return options.map((option) => { - return { - variant: option.identifier, - events: pingEvents.filter( - (event) => event.selectedVariant === option.identifier - ).length, - }; - }); - }, [options, pingEvents]); - - const absPings = actualData.reduce((accumulator, value) => { - return accumulator + value.events; - }, 0); - return (
acc + e._count._all, 0)} variants={labels} - events={actualData} + events={pingEvents} totalText="Visits" />
diff --git a/apps/web/src/components/charts/Donut.tsx b/apps/web/src/components/charts/Donut.tsx index 40565738..a619d945 100644 --- a/apps/web/src/components/charts/Donut.tsx +++ b/apps/web/src/components/charts/Donut.tsx @@ -11,6 +11,7 @@ import { ChartTooltipContent, } from "components/ui/chart"; import { useMemo } from "react"; +import type { ProjectClientEvents } from "pages/projects/[projectId]"; export function DonutChart({ totalVisits, @@ -21,7 +22,7 @@ export function DonutChart({ totalVisits: number; totalText: string; variants: string[]; - events: Array<{ variant: string; events: number }>; + events: ProjectClientEvents; }) { const chartConfig = useMemo( () => @@ -42,9 +43,9 @@ export function DonutChart({ const chartData = useMemo(() => { return events.map((event) => ({ - variant: event.variant, - events: event.events, - fill: `var(--color-${event.variant})`, + variant: event.selectedVariant, + events: event._count._all, + fill: `var(--color-${event.selectedVariant})`, })); }, [events]); diff --git a/apps/web/src/pages/projects/[projectId]/index.tsx b/apps/web/src/pages/projects/[projectId]/index.tsx index 7cd01010..3454968f 100644 --- a/apps/web/src/pages/projects/[projectId]/index.tsx +++ b/apps/web/src/pages/projects/[projectId]/index.tsx @@ -1,3 +1,4 @@ +import type { inferRouterOutputs } from "@trpc/server"; import { AddABTestModal } from "components/AddABTestModal"; import { DashboardHeader } from "components/DashboardHeader"; import { Layout } from "components/Layout"; @@ -9,8 +10,12 @@ import type { GetStaticPaths, GetStaticProps } from "next"; import type { NextPageWithLayout } from "pages/_app"; import { useState } from "react"; import { AiOutlinePlus } from "react-icons/ai"; +import type { AppRouter } from "server/trpc/router/_app"; import { trpc } from "utils/trpc"; +export type ProjectClientEvents = + inferRouterOutputs["project"]["getProjectData"]["project"]["tests"][number]["pingEvents"]; + const Projects: NextPageWithLayout = () => { const [isCreateTestModalOpen, setIsCreateTestModalOpen] = useState(false); @@ -59,7 +64,12 @@ const Projects: NextPageWithLayout = () => {
{data?.project?.tests.map((test) => ( -
+
))}
diff --git a/apps/web/src/server/trpc/router/project.ts b/apps/web/src/server/trpc/router/project.ts index f6523833..18434056 100644 --- a/apps/web/src/server/trpc/router/project.ts +++ b/apps/web/src/server/trpc/router/project.ts @@ -10,10 +10,10 @@ import { z } from "zod"; export type ClientOption = Omit & { chance: number; }; - -import ms from "ms"; import { updateProjectsOnSession } from "utils/updateSession"; import { protectedProcedure, router } from "../trpc"; +import dayjs from "dayjs"; +import { AbbyEventType } from "@tryabby/core"; export const projectRouter = router({ getProjectData: protectedProcedure @@ -32,13 +32,6 @@ export const projectRouter = router({ tests: { include: { options: true, - events: { - where: { - createdAt: { - gte: new Date(Date.now() - ms("30d")), - }, - }, - }, }, }, environments: true, @@ -53,17 +46,44 @@ export const projectRouter = router({ const { events: eventsThisPeriod } = await EventService.getEventsForCurrentPeriod(project.id); + const events = await ctx.prisma.event.groupBy({ + by: ["selectedVariant", "type", "testId"], + _count: { _all: true }, + where: { + testId: { in: project.tests.map((test) => test.id) }, + createdAt: { + gte: dayjs().subtract(30, "days").toDate(), + }, + }, + }); + return { project: { ...project, eventsThisPeriod, - tests: project.tests.map((test) => ({ - ...test, - options: test.options.map((option) => ({ - ...option, - chance: option.chance.toNumber(), - })), - })), + tests: project.tests.map((test) => { + const actEvents: typeof events = []; + const pingEvents: typeof events = []; + for (const event of events) { + if (event.testId === test.id) { + if (event.type === AbbyEventType.ACT) { + actEvents.push(event); + } else { + pingEvents.push(event); + } + } + } + return { + ...test, + + actEvents, + pingEvents, + options: test.options.map((option) => ({ + ...option, + chance: option.chance.toNumber(), + })), + }; + }), }, }; }),