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

[WIP] feat(web): add conversions to A/B tests #99

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@tiptap/pm": "2.0.4",
"@tiptap/react": "2.0.4",
"@tiptap/starter-kit": "2.0.4",
"@tremor/react": "^3.11.0",
"@trpc/client": "^10.19.1",
"@trpc/next": "^10.19.1",
"@trpc/react-query": "^10.19.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- AlterTable
ALTER TABLE `Event` ADD COLUMN `testGoalId` VARCHAR(191) NULL,
ADD COLUMN `testVersion` INTEGER NOT NULL DEFAULT 1;

-- AlterTable
ALTER TABLE `Test` ADD COLUMN `version` INTEGER NOT NULL DEFAULT 1;

-- CreateTable
CREATE TABLE `TestGoal` (
`id` VARCHAR(191) NOT NULL,
`name` VARCHAR(191) NOT NULL,
`testId` VARCHAR(191) NOT NULL,

INDEX `TestGoal_testId_idx`(`testId`),
UNIQUE INDEX `TestGoal_testId_name_key`(`testId`, `name`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateIndex
CREATE INDEX `Event_testGoalId_idx` ON `Event`(`testGoalId`);
34 changes: 28 additions & 6 deletions apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,31 @@ model Test {
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
projectId String

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
options Option[]
events Event[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
options Option[]
conversions TestConversion[]
goals TestGoal[]

version Int @default(1)

@@unique([projectId, name])
@@index([projectId])
}

model TestGoal {
id String @id @default(cuid())
name String

testId String
test Test @relation(fields: [testId], references: [id], onDelete: Cascade)
TestConversion TestConversion[]

@@unique([testId, name])
@@index([testId])
}

model Option {
id String @id @default(cuid())
identifier String
Expand All @@ -168,7 +183,7 @@ model Option {
@@unique([testId, identifier])
}

model Event {
model TestConversion {
id String @id @default(cuid())

test Test @relation(fields: [testId], references: [id], onDelete: Cascade)
Expand All @@ -178,9 +193,16 @@ model Event {
selectedVariant String
createdAt DateTime @default(now())

testVersion Int @default(1)

testGoalId String?
testGoal TestGoal? @relation(fields: [testGoalId], references: [id], onDelete: Cascade)

@@index([testId])
@@index([type])
@@index([selectedVariant])
@@index([testGoalId])
@@map("Event")
}

model FeatureFlag {
Expand Down
38 changes: 29 additions & 9 deletions apps/web/prisma/seedEvents.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { PrismaClient, Prisma } from "@prisma/client";
import { AbbyEventType } from "@tryabby/core";
import crypto from "crypto";

const prisma = new PrismaClient();

function trueRandomNumber(min: number, max: number) {
const cryptoArray = new Uint32Array(1);
crypto.getRandomValues(cryptoArray);
const randomValue = cryptoArray![0]! / (0xffffffff + 1);
return Math.floor(min + randomValue * (max - min + 1));
}

function randomDate(start: Date, end: Date) {
const startTime = start.getTime();
const endTime = end.getTime();
const randomTime = trueRandomNumber(startTime, endTime);
return new Date(randomTime);
}

async function main() {
const user = await prisma.user.findFirst({
where: {},
Expand Down Expand Up @@ -42,6 +57,7 @@ async function main() {
});

await prisma.option.createMany({
skipDuplicates: true,
data: [
{
identifier: "oldFooter",
Expand Down Expand Up @@ -71,47 +87,51 @@ async function main() {
],
});

await prisma.event.createMany({
await prisma.testConversion.createMany({
data: [
...Array.from<Prisma.EventCreateManyInput>({
...Array.from<Prisma.TestConversionCreateManyInput>({
length: Math.floor(Math.random() * 200),
}).map(
() =>
({
selectedVariant: "oldFooter",
testId: footerTest.id,
type: AbbyEventType.PING,
} as Prisma.EventCreateManyInput)
createdAt: randomDate(new Date(2021, 0, 1), new Date()),
} as Prisma.TestConversionCreateManyInput)
),
...Array.from<Prisma.EventCreateManyInput>({
...Array.from<Prisma.TestConversionCreateManyInput>({
length: Math.floor(Math.random() * 200),
}).map(
() =>
({
selectedVariant: "newFooter",
testId: footerTest.id,
type: AbbyEventType.PING,
} as Prisma.EventCreateManyInput)
createdAt: randomDate(new Date(2021, 0, 1), new Date()),
} as Prisma.TestConversionCreateManyInput)
),
...Array.from<Prisma.EventCreateManyInput>({
...Array.from<Prisma.TestConversionCreateManyInput>({
length: Math.floor(Math.random() * 200),
}).map(
() =>
({
selectedVariant: "oldFooter",
testId: footerTest.id,
type: AbbyEventType.ACT,
} as Prisma.EventCreateManyInput)
createdAt: randomDate(new Date(2021, 0, 1), new Date()),
} as Prisma.TestConversionCreateManyInput)
),
...Array.from<Prisma.EventCreateManyInput>({
...Array.from<Prisma.TestConversionCreateManyInput>({
length: Math.floor(Math.random() * 200),
}).map(
() =>
({
selectedVariant: "newFooter",
testId: footerTest.id,
type: AbbyEventType.ACT,
} as Prisma.EventCreateManyInput)
createdAt: randomDate(new Date(2021, 0, 1), new Date()),
} as Prisma.TestConversionCreateManyInput)
),
],
});
Expand Down
8 changes: 4 additions & 4 deletions apps/web/src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ export function Footer() {
</div>
<div className="flex flex-col gap-y-4 md:gap-0">
<h1 className="mb-2 text-2xl font-bold md:mb-3">Integrations</h1>
<Link href={`${DOCS_URL}integrations/react`}>React</Link>
<Link href={`/nextjs`}>Next.js</Link>
<Link href={`${DOCS_URL}integrations/svelte`}>Svelte</Link>
<Link href={`${DOCS_URL}integrations/angular`}>Angular</Link>
<Link href={`/integrations/react`}>React</Link>
<Link href={`/integrations/nextjs`}>Next.js</Link>
<Link href={`/integrations/svelte`}>Svelte</Link>
<Link href={`/integrations/angular`}>Angular</Link>
</div>
<div className="flex flex-col gap-y-4 md:gap-0">
<h1 className="mb-2 text-2xl font-bold md:mb-3">Links</h1>
Expand Down
21 changes: 20 additions & 1 deletion apps/web/src/components/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,55 @@ import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { SiAngular, SiNextdotjs, SiReact, SiSvelte } from "react-icons/si";

const INTEGRATIONS = [
export const INTEGRATIONS = [
{
name: "Next.js",
logo: <SiNextdotjs />,
docsUrlSlug: "nextjs",
logoFill: "#fff",
description: "Feature Flags, Remote Config, and A/B Testing for Next.js",
npmPackage: "next",
additionalFeatures: [
"Server Side Rendering",
"Incremental Static Regeneration",
"Easy to use Hooks",
],
},
{
name: "React",
logo: <SiReact />,
docsUrlSlug: "react",
logoFill: "#61DAFB",
description: "Feature Flags, Remote Config, and A/B Testing for React",
npmPackage: "react",
additionalFeatures: ["Easy to use Hooks"],
},
{
name: "Svelte",
logo: <SiSvelte />,
docsUrlSlug: "svelte",
logoFill: "#FF3E00",
description:
"Feature Flags, Remote Config, and A/B Testing for Svelte & Sveltekit",
npmPackage: "svelte",
additionalFeatures: ["Sveltekit Support"],
},
{
name: "Angular",
logo: <SiAngular />,
docsUrlSlug: "angular",
logoFill: "#DD0031",
description: "Feature Flags, Remote Config, and A/B Testing for Angular",
npmPackage: "angular",
},
] satisfies Array<{
name: string;
logo: React.ReactNode;
logoFill: string;
docsUrlSlug: string;
description: string;
npmPackage: string;
additionalFeatures?: Array<string>;
}>;

export const Integrations = () => {
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/components/Navbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const NAV_ITEMS: Array<NavItem> = [
subTitle: "Painless Debugging",
href: "/devtools",
},
{
title: "Integrations",
subTitle: "SDKs for your favorite frameworks",
href: "/integrations",
},
{
title: "Documentation",
subTitle: "Developers API Reference",
Expand Down
58 changes: 32 additions & 26 deletions apps/web/src/components/Test/Metrics.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Prisma, Event } from "@prisma/client";
import { TestConversion } from "@prisma/client";
import { DonutChart, Text } from "@tremor/react";
import {
Chart as ChartJS,
BarElement,
Expand Down Expand Up @@ -45,46 +46,51 @@ const Metrics = ({
pingEvents,
options,
}: {
pingEvents: Event[];
pingEvents: TestConversion[];
options: ClientOption[];
}) => {
const labels = options.map((option) => option.identifier);
const actualData = useMemo(() => {
return options.map((option) => {
return {
pings: pingEvents.filter(
name: option.identifier,
count: pingEvents.filter(
(event) => event.selectedVariant === option.identifier
).length,
weight: option.chance,
};
});
}, [options, pingEvents]);

const absPings = actualData.reduce((accumulator, value) => {
return accumulator + value.pings;
return accumulator + value.count;
}, 0);

const expectedData = useMemo(() => {
return options.map((option) => ({
name: option.identifier,
count: absPings * option.chance,
}));
}, [absPings, options]);

return (
<div className="relative mb-6 h-full w-full">
<Bar
className="self-end"
options={OPTIONS}
data={{
labels,
datasets: [
{
label: "Actual",
data: actualData.map((d) => d.pings),
backgroundColor: "#A9E4EF",
},
{
label: "Expected",
data: actualData.map((data) => absPings * data.weight),
backgroundColor: "#f472b6",
},
],
}}
/>
<div className="relative grid h-full w-full gap-y-4 md:grid-cols-2">
<div className="flex flex-col items-center space-y-3">
<Text>Conversions</Text>
<DonutChart
data={actualData}
category="count"
index="name"
colors={["pink", "indigo"]}
/>
</div>
<div className="flex flex-col items-center space-y-3">
<Text>Expected</Text>
<DonutChart
data={expectedData}
category="count"
index="name"
colors={["pink", "indigo"]}
/>
</div>
</div>
);
};
Expand Down
Loading