mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
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:
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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'll do it later
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
25
apps/web/app/api/v1/teams/[teamId]/add_demo_product/route.ts
Normal file
25
apps/web/app/api/v1/teams/[teamId]/add_demo_product/route.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -27,7 +27,9 @@ export const SignupForm = () => {
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSigningUp(true);
|
||||
|
||||
try {
|
||||
await createUser(
|
||||
e.target.elements.name.value,
|
||||
|
||||
@@ -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!");
|
||||
|
||||
@@ -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": "*",
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
1101
packages/lib/utils/createDemoProductHelpers.ts
Normal file
1101
packages/lib/utils/createDemoProductHelpers.ts
Normal file
File diff suppressed because it is too large
Load Diff
45
pnpm-lock.yaml
generated
45
pnpm-lock.yaml
generated
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user