Dramatically improve load times when creating a new team(#614)

* fix: attempts to reduce time taken to create team

* fix: fixes long time taken in team creation

* fix: refactors prisma logic

* feat: added logic for adding demo data while signing up

* fix: adds comment

* fix: adds another logic for adding demo data

* fix: adds service for adding demo data

* fix: fixes

* fix: adds demo product creation logic in next auth options

* fix: fixes next auth options

* fix: fixes team creation logic

* refactor: clean up

* fix: moves the logic for adding demo product while creating team in bg

* fix: moves individual queries in a transaction

* refactor: service

* fix: moves api route to app-dir

* fix: fixes api calls

* fix: fixes cache

* fix: removes unused code
This commit is contained in:
Anshuman Pandey
2023-08-05 17:29:06 +05:30
committed by GitHub
parent c707896eb6
commit ad42f4cc55
12 changed files with 1418 additions and 1290 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -29,7 +29,9 @@ export default function Onboarding({ session }: OnboardingProps) {
error: isErrorEnvironment,
isLoading: isLoadingEnvironment,
} = useSWR(`/api/v1/environments/find-first`, fetcher);
const { profile } = useProfile();
const { triggerProfileMutate } = useProfileMutation();
const [formbricksResponseId, setFormbricksResponseId] = useState<ResponseId | undefined>();
const [currentStep, setCurrentStep] = useState(1);
@@ -56,28 +58,32 @@ export default function Onboarding({ session }: OnboardingProps) {
setCurrentStep(currentStep + 1);
};
const doLater = () => {
const doLater = async () => {
setCurrentStep(4);
};
const next = () => {
if (currentStep < MAX_STEPS) {
setCurrentStep((value) => value + 1);
return;
}
};
const done = async () => {
setIsLoading(true);
try {
const updatedProfile = { ...profile, onboardingCompleted: true };
await triggerProfileMutate(updatedProfile);
if (environment) {
router.push(`/environments/${environment.id}/surveys`);
return;
}
} catch (e) {
toast.error("An error occured saving your settings.");
setIsLoading(false);
console.error(e);
}
};

View File

@@ -57,6 +57,11 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
toast.error("An error occured saving your settings");
console.error(e);
}
done();
};
const handleLaterClick = async () => {
done();
};
@@ -138,7 +143,7 @@ const Product: React.FC<Product> = ({ done, isLoading, environmentId }) => {
</div>
</div>
<div className="flex items-center justify-end">
<Button size="lg" className="mr-2" variant="minimal" id="product-skip" onClick={done}>
<Button size="lg" className="mr-2" variant="minimal" id="product-skip" onClick={handleLaterClick}>
I&apos;ll do it later
</Button>
<Button

View File

@@ -2,6 +2,7 @@ import { env } from "@/env.mjs";
import { verifyPassword } from "@/lib/auth";
import { verifyToken } from "@/lib/jwt";
import { prisma } from "@formbricks/database";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
import type { IdentityProvider } from "@prisma/client";
import type { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
@@ -254,7 +255,7 @@ export const authOptions: NextAuthOptions = {
return "/auth/login?error=A%20user%20with%20this%20email%20exists%20already.";
}
await prisma.user.create({
const createdUser = await prisma.user.create({
data: {
name: user.name,
email: user.email,
@@ -362,8 +363,21 @@ export const authOptions: NextAuthOptions = {
],
},
},
include: {
memberships: true,
},
});
const teamId = createdUser.memberships?.[0]?.teamId;
if (teamId) {
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
method: "POST",
headers: {
"x-api-key": INTERNAL_SECRET,
},
});
}
return true;
}

View File

@@ -0,0 +1,25 @@
import { INTERNAL_SECRET } from "@formbricks/lib/constants";
import { createDemoProduct } from "@formbricks/lib/services/team";
import { NextResponse } from "next/server";
import { headers } from "next/headers";
import { responses } from "@/lib/api/response";
export async function POST(_: Request, { params }: { params: { teamId: string } }) {
// Check Authentication
if (headers().get("x-api-key") !== INTERNAL_SECRET) {
return responses.notAuthenticatedResponse();
}
const teamId = params.teamId;
if (teamId === undefined) {
return responses.badRequestResponse("Missing teamId");
}
try {
const demoProduct = await createDemoProduct(teamId);
return NextResponse.json(demoProduct);
} catch (err) {
throw new Error(err);
}
}

View File

@@ -4,6 +4,8 @@ import { populateEnvironment } from "@/lib/populate";
import { prisma } from "@formbricks/database";
import { NextResponse } from "next/server";
import { env } from "@/env.mjs";
import { Prisma } from "@prisma/client";
import { INTERNAL_SECRET, WEBAPP_URL } from "@formbricks/lib/constants";
export async function POST(request: Request) {
let { inviteToken, ...user } = await request.json();
@@ -15,7 +17,7 @@ export async function POST(request: Request) {
let inviteId;
try {
let data;
let data: Prisma.UserCreateArgs;
let invite;
if (inviteToken) {
@@ -89,7 +91,26 @@ export async function POST(request: Request) {
};
}
const userData = await prisma.user.create(data);
type UserWithMemberships = Prisma.UserGetPayload<{ include: { memberships: true } }>;
const userData = (await prisma.user.create({
...data,
include: {
memberships: true,
},
// TODO: This is a hack to get the correct types (casting), we should find a better way to do this
})) as UserWithMemberships;
const teamId = userData.memberships[0].teamId;
if (teamId) {
fetch(`${WEBAPP_URL}/api/v1/teams/${teamId}/add_demo_product`, {
method: "POST",
headers: {
"x-api-key": INTERNAL_SECRET,
},
});
}
if (inviteId) {
sendInviteAcceptedEmail(invite.creator.name, user.name, invite.creator.email);

View File

@@ -27,7 +27,9 @@ export const SignupForm = () => {
if (!isValid) {
return;
}
setSigningUp(true);
try {
await createUser(
e.target.elements.name.value,

View File

@@ -25,6 +25,7 @@ export default function CreateTeamModal({ open, setOpen }: CreateTeamModalProps)
const submitTeam = async (data) => {
setLoading(true);
const newTeam = await createTeam(data.name, (profile as any).id);
const newMemberships = await mutateMemberships();
changeEnvironmentByTeam(newTeam.id, newMemberships, router);
toast.success("Team created successfully!");

View File

@@ -19,7 +19,8 @@
"markdown-it": "^13.0.1",
"posthog-node": "^3.1.1",
"server-only": "^0.0.1",
"tailwind-merge": "^1.14.0"
"tailwind-merge": "^1.14.0",
"@paralleldrive/cuid2": "^2.2.1"
},
"devDependencies": {
"@formbricks/tsconfig": "*",

View File

@@ -1,8 +1,27 @@
import { cache } from "react";
import { prisma } from "@formbricks/database";
import { Prisma } from "@prisma/client";
import { DatabaseError } from "@formbricks/errors";
import { TTeam } from "@formbricks/types/v1/teams";
import { Prisma } from "@prisma/client";
import { cache } from "react";
import { createId } from "@paralleldrive/cuid2";
import {
ChurnResponses,
ChurnSurvey,
DEMO_COMPANIES,
DEMO_NAMES,
EASResponses,
EASSurvey,
InterviewPromptResponses,
InterviewPromptSurvey,
OnboardingResponses,
OnboardingSurvey,
PMFResponses,
PMFSurvey,
generateAttributeValue,
generateResponsesAndDisplays,
populateEnvironment,
updateEnvironmentArgs,
} from "../utils/createDemoProductHelpers";
export const select = {
id: true,
@@ -38,3 +57,183 @@ export const getTeamByEnvironmentId = cache(async (environmentId: string): Promi
throw error;
}
});
export const createDemoProduct = cache(async (teamId: string) => {
const productWithEnvironment = Prisma.validator<Prisma.ProductArgs>()({
include: {
environments: true,
},
});
type ProductWithEnvironment = Prisma.ProductGetPayload<typeof productWithEnvironment>;
const demoProduct: ProductWithEnvironment = await prisma.product.create({
data: {
name: "Demo Product",
team: {
connect: {
id: teamId,
},
},
environments: {
create: [
{
type: "production",
...populateEnvironment,
},
{
type: "development",
...populateEnvironment,
},
],
},
},
include: {
environments: true,
},
});
const prodEnvironment = demoProduct.environments.find((environment) => environment.type === "production");
// add attributes to each environment of the product
// dont add dev environment
const updatedEnvironment = await prisma.environment.update({
where: { id: prodEnvironment?.id },
data: {
...updateEnvironmentArgs,
},
include: {
attributeClasses: true, // include attributeClasses
eventClasses: true, // include eventClasses
},
});
const eventClasses = updatedEnvironment.eventClasses;
// check if updatedEnvironment exists and it has attributeClasses
if (!updatedEnvironment || !updatedEnvironment.attributeClasses) {
throw new Error("Attribute classes could not be created");
}
const attributeClasses = updatedEnvironment.attributeClasses;
// create an array for all the events that will be created
const eventPromises: {
eventClassId: string;
sessionId: string;
}[] = [];
// create an array for all the attributes that will be created
const generatedAttributes: {
attributeClassId: string;
value: string;
personId: string;
}[] = [];
// create an array containing all the person ids to be created
const personIds = Array.from({ length: 20 }).map((_) => createId());
// create an array containing all the session ids to be created
const sessionIds = Array.from({ length: 20 }).map((_) => createId());
// loop over the person ids and create attributes for each person
personIds.forEach((personId, i: number) => {
generatedAttributes.push(
...attributeClasses.map((attributeClass) => {
let value = generateAttributeValue(
attributeClass.name,
DEMO_NAMES[i],
DEMO_COMPANIES[i],
`${DEMO_COMPANIES[i].toLowerCase().split(" ").join("")}.com`,
i
);
return {
attributeClassId: attributeClass.id,
value: value,
personId,
};
})
);
});
sessionIds.forEach((sessionId) => {
for (let eventClass of eventClasses) {
// create a random number of events for each event class
const eventCount = Math.floor(Math.random() * 5) + 1;
for (let j = 0; j < eventCount; j++) {
eventPromises.push({
eventClassId: eventClass.id,
sessionId,
});
}
}
});
// create the people, sessions, attributes, and events in a transaction
// the order of the queries is important because of foreign key constraints
try {
await prisma.$transaction([
prisma.person.createMany({
data: personIds.map((personId) => ({
id: personId,
environmentId: demoProduct.environments[0].id,
})),
}),
prisma.session.createMany({
data: sessionIds.map((sessionId, idx) => ({
id: sessionId,
personId: personIds[idx],
})),
}),
prisma.attribute.createMany({
data: generatedAttributes,
}),
prisma.event.createMany({
data: eventPromises.map((eventPromise) => ({
eventClassId: eventPromise.eventClassId,
sessionId: eventPromise.sessionId,
})),
}),
]);
} catch (err: any) {
throw new Error(err);
}
// Create a function that creates a survey
const createSurvey = async (surveyData: any, responses: any, displays: any) => {
return await prisma.survey.create({
data: {
...surveyData,
environment: { connect: { id: demoProduct.environments[0].id } },
questions: surveyData.questions as any,
responses: { create: responses },
displays: { create: displays },
},
});
};
const people = personIds.map((personId) => ({ id: personId }));
const PMFResults = generateResponsesAndDisplays(people, PMFResponses);
const OnboardingResults = generateResponsesAndDisplays(people, OnboardingResponses);
const ChurnResults = generateResponsesAndDisplays(people, ChurnResponses);
const EASResults = generateResponsesAndDisplays(people, EASResponses);
const InterviewPromptResults = generateResponsesAndDisplays(people, InterviewPromptResponses);
// Create the surveys
await createSurvey(PMFSurvey, PMFResults.responses, PMFResults.displays);
await createSurvey(OnboardingSurvey, OnboardingResults.responses, OnboardingResults.displays);
await createSurvey(ChurnSurvey, ChurnResults.responses, ChurnResults.displays);
await createSurvey(EASSurvey, EASResults.responses, EASResults.displays);
await createSurvey(
InterviewPromptSurvey,
InterviewPromptResults.responses,
InterviewPromptResults.displays
);
return demoProduct;
});

File diff suppressed because it is too large Load Diff

45
pnpm-lock.yaml generated
View File

@@ -19,7 +19,7 @@ importers:
version: 3.12.7
turbo:
specifier: latest
version: 1.10.7
version: 1.10.3
apps/demo:
dependencies:
@@ -440,6 +440,9 @@ importers:
'@formbricks/types':
specifier: '*'
version: link:../types
'@paralleldrive/cuid2':
specifier: ^2.2.1
version: 2.2.1
date-fns:
specifier: ^2.30.0
version: 2.30.0
@@ -19763,65 +19766,65 @@ packages:
dependencies:
safe-buffer: 5.2.1
/turbo-darwin-64@1.10.7:
resolution: {integrity: sha512-N2MNuhwrl6g7vGuz4y3fFG2aR1oCs0UZ5HKl8KSTn/VC2y2YIuLGedQ3OVbo0TfEvygAlF3QGAAKKtOCmGPNKA==}
/turbo-darwin-64@1.10.3:
resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-darwin-arm64@1.10.7:
resolution: {integrity: sha512-WbJkvjU+6qkngp7K4EsswOriO3xrNQag7YEGRtfLoDdMTk4O4QTeU6sfg2dKfDsBpTidTvEDwgIYJhYVGzrz9Q==}
/turbo-darwin-arm64@1.10.3:
resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/turbo-linux-64@1.10.7:
resolution: {integrity: sha512-x1CF2CDP1pDz/J8/B2T0hnmmOQI2+y11JGIzNP0KtwxDM7rmeg3DDTtDM/9PwGqfPotN9iVGgMiMvBuMFbsLhg==}
/turbo-linux-64@1.10.3:
resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-linux-arm64@1.10.7:
resolution: {integrity: sha512-JtnBmaBSYbs7peJPkXzXxsRGSGBmBEIb6/kC8RRmyvPAMyqF8wIex0pttsI+9plghREiGPtRWv/lfQEPRlXnNQ==}
/turbo-linux-arm64@1.10.3:
resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/turbo-windows-64@1.10.7:
resolution: {integrity: sha512-7A/4CByoHdolWS8dg3DPm99owfu1aY/W0V0+KxFd0o2JQMTQtoBgIMSvZesXaWM57z3OLsietFivDLQPuzE75w==}
/turbo-windows-64@1.10.3:
resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==}
cpu: [x64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo-windows-arm64@1.10.7:
resolution: {integrity: sha512-D36K/3b6+hqm9IBAymnuVgyePktwQ+F0lSXr2B9JfAdFPBktSqGmp50JNC7pahxhnuCLj0Vdpe9RqfnJw5zATA==}
/turbo-windows-arm64@1.10.3:
resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/turbo@1.10.7:
resolution: {integrity: sha512-xm0MPM28TWx1e6TNC3wokfE5eaDqlfi0G24kmeHupDUZt5Wd0OzHFENEHMPqEaNKJ0I+AMObL6nbSZonZBV2HA==}
/turbo@1.10.3:
resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==}
hasBin: true
requiresBuild: true
optionalDependencies:
turbo-darwin-64: 1.10.7
turbo-darwin-arm64: 1.10.7
turbo-linux-64: 1.10.7
turbo-linux-arm64: 1.10.7
turbo-windows-64: 1.10.7
turbo-windows-arm64: 1.10.7
turbo-darwin-64: 1.10.3
turbo-darwin-arm64: 1.10.3
turbo-linux-64: 1.10.3
turbo-linux-arm64: 1.10.3
turbo-windows-64: 1.10.3
turbo-windows-arm64: 1.10.3
dev: true
/tween-functions@1.2.0: