From 06eebe36ee167d6640cbcc7f5f28005cdaccdaa4 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Wed, 28 Feb 2024 19:17:40 +0100 Subject: [PATCH] fix: update action indexes for faster query processing (#2154) --- .../in-app/sync/[userId]/route.ts | 9 +- .../migration.sql | 8 ++ packages/database/schema.prisma | 3 +- packages/lib/action/service.ts | 107 ++---------------- packages/lib/person/cache.ts | 19 ++++ packages/lib/person/service.ts | 28 ++++- 6 files changed, 68 insertions(+), 106 deletions(-) create mode 100644 packages/database/migrations/20240228180201_update_action_indexes/migration.sql diff --git a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/[userId]/route.ts b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/[userId]/route.ts index 3c0acba4d9..f45d1c8fc6 100644 --- a/apps/web/app/api/v1/client/[environmentId]/in-app/sync/[userId]/route.ts +++ b/apps/web/app/api/v1/client/[environmentId]/in-app/sync/[userId]/route.ts @@ -3,7 +3,6 @@ import { responses } from "@/app/lib/api/response"; import { transformErrorToDetails } from "@/app/lib/api/validator"; import { NextRequest, userAgent } from "next/server"; -import { getLatestActionByPersonId } from "@formbricks/lib/action/service"; import { getActionClasses } from "@formbricks/lib/actionClass/service"; import { IS_FORMBRICKS_CLOUD, @@ -11,7 +10,7 @@ import { PRICING_USERTARGETING_FREE_MTU, } from "@formbricks/lib/constants"; import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service"; -import { createPerson, getPersonByUserId } from "@formbricks/lib/person/service"; +import { createPerson, getIsPersonMonthlyActive, getPersonByUserId } from "@formbricks/lib/person/service"; import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { getSyncSurveys } from "@formbricks/lib/survey/service"; import { @@ -95,10 +94,12 @@ export async function GET( let person = await getPersonByUserId(environmentId, userId); if (!isMauLimitReached) { + // MAU limit not reached: create person if not exists if (!person) { person = await createPerson(environmentId, userId); } } else { + // MAU limit reached: check if person has been active this month; only continue if person has been active await sendFreeLimitReachedEventToPosthogBiWeekly(environmentId, "userTargeting"); const errorMessage = `Monthly Active Users limit in the current plan is reached in ${environmentId}`; if (!person) { @@ -110,8 +111,8 @@ export async function GET( ); } else { // check if person has been active this month - const latestAction = await getLatestActionByPersonId(person.id); - if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) { + const isPersonMonthlyActive = await getIsPersonMonthlyActive(person.id); + if (!isPersonMonthlyActive) { return responses.tooManyRequestsResponse( errorMessage, true, diff --git a/packages/database/migrations/20240228180201_update_action_indexes/migration.sql b/packages/database/migrations/20240228180201_update_action_indexes/migration.sql new file mode 100644 index 0000000000..a5cbc35da0 --- /dev/null +++ b/packages/database/migrations/20240228180201_update_action_indexes/migration.sql @@ -0,0 +1,8 @@ +-- DropIndex +DROP INDEX "Action_actionClassId_idx"; + +-- CreateIndex +CREATE INDEX "Action_personId_actionClassId_created_at_idx" ON "Action"("personId", "actionClassId", "created_at"); + +-- CreateIndex +CREATE INDEX "Action_actionClassId_created_at_idx" ON "Action"("actionClassId", "created_at"); diff --git a/packages/database/schema.prisma b/packages/database/schema.prisma index 276c69764f..b3b7a0ec29 100644 --- a/packages/database/schema.prisma +++ b/packages/database/schema.prisma @@ -350,8 +350,9 @@ model Action { /// [ActionProperties] properties Json @default("{}") + @@index([personId, actionClassId, createdAt]) + @@index([actionClassId, createdAt]) @@index([personId]) - @@index([actionClassId]) } enum EnvironmentType { diff --git a/packages/lib/action/service.ts b/packages/lib/action/service.ts index 87a662b1c8..8d449ae65b 100644 --- a/packages/lib/action/service.ts +++ b/packages/lib/action/service.ts @@ -14,112 +14,14 @@ import { DatabaseError } from "@formbricks/types/errors"; import { actionClassCache } from "../actionClass/cache"; import { createActionClass, getActionClassByEnvironmentIdAndName } from "../actionClass/service"; import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants"; -import { createPerson, getPersonByUserId } from "../person/service"; +import { activePersonCache } from "../person/cache"; +import { createPerson, getIsPersonMonthlyActive, getPersonByUserId } from "../person/service"; import { surveyCache } from "../survey/cache"; import { formatDateFields } from "../utils/datetime"; import { validateInputs } from "../utils/validate"; import { actionCache } from "./cache"; import { getStartDateOfLastMonth, getStartDateOfLastQuarter, getStartDateOfLastWeek } from "./utils"; -export const getLatestActionByEnvironmentId = async (environmentId: string): Promise => { - const action = await unstable_cache( - async () => { - validateInputs([environmentId, ZId]); - - try { - const actionPrisma = await prisma.action.findFirst({ - where: { - actionClass: { - environmentId: environmentId, - }, - }, - orderBy: { - createdAt: "desc", - }, - include: { - actionClass: true, - }, - }); - if (!actionPrisma) { - return null; - } - const action: TAction = { - id: actionPrisma.id, - createdAt: actionPrisma.createdAt, - personId: actionPrisma.personId, - properties: actionPrisma.properties, - actionClass: actionPrisma.actionClass, - }; - return action; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } - - throw error; - } - }, - [`getLastestActionByEnvironmentId-${environmentId}`], - { - tags: [actionCache.tag.byEnvironmentId(environmentId)], - revalidate: SERVICES_REVALIDATION_INTERVAL, - } - )(); - - // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them - // https://github.com/vercel/next.js/issues/51613 - return action ? formatDateFields(action, ZAction) : null; -}; - -export const getLatestActionByPersonId = async (personId: string): Promise => { - const action = await unstable_cache( - async () => { - validateInputs([personId, ZId]); - - try { - const actionPrisma = await prisma.action.findFirst({ - where: { - personId, - }, - orderBy: { - createdAt: "desc", - }, - include: { - actionClass: true, - }, - }); - - if (!actionPrisma) { - return null; - } - const action: TAction = { - id: actionPrisma.id, - createdAt: actionPrisma.createdAt, - personId: actionPrisma.personId, - properties: actionPrisma.properties, - actionClass: actionPrisma.actionClass, - }; - return action; - } catch (error) { - if (error instanceof Prisma.PrismaClientKnownRequestError) { - throw new DatabaseError("Database operation failed"); - } - - throw error; - } - }, - [`getLastestActionByPersonId-${personId}`], - { - tags: [actionCache.tag.byPersonId(personId)], - revalidate: SERVICES_REVALIDATION_INTERVAL, - } - )(); - - // since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them - // https://github.com/vercel/next.js/issues/51613 - return action ? formatDateFields(action, ZAction) : null; -}; - export const getActionsByPersonId = async (personId: string, page?: number): Promise => { const actions = await unstable_cache( async () => { @@ -257,6 +159,11 @@ export const createAction = async (data: TActionInput): Promise => { }, }); + const isPersonMonthlyActive = await getIsPersonMonthlyActive(person.id); + if (!isPersonMonthlyActive) { + activePersonCache.revalidate({ id: person.id }); + } + actionCache.revalidate({ environmentId, personId: person.id, diff --git a/packages/lib/person/cache.ts b/packages/lib/person/cache.ts index 4e9b6932f8..bc1cbcb0ab 100644 --- a/packages/lib/person/cache.ts +++ b/packages/lib/person/cache.ts @@ -32,3 +32,22 @@ export const personCache = { } }, }; + +interface ActivePersonRevalidateProps { + id?: string; + environmentId?: string; + userId?: string; +} + +export const activePersonCache = { + tag: { + byId(personId: string): string { + return `people-${personId}-active`; + }, + }, + revalidate({ id }: ActivePersonRevalidateProps): void { + if (id) { + revalidateTag(this.tag.byEnvironmentId(id)); + } + }, +}; diff --git a/packages/lib/person/service.ts b/packages/lib/person/service.ts index 0cac26d69f..b97b79ed57 100644 --- a/packages/lib/person/service.ts +++ b/packages/lib/person/service.ts @@ -13,7 +13,7 @@ import { createAttributeClass, getAttributeClassByName } from "../attributeClass import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants"; import { formatDateFields } from "../utils/datetime"; import { validateInputs } from "../utils/validate"; -import { personCache } from "./cache"; +import { activePersonCache, personCache } from "./cache"; export const selectPerson = { id: true, @@ -420,3 +420,29 @@ export const updatePersonAttribute = async ( return attributes; }; + +export const getIsPersonMonthlyActive = async (personId: string): Promise => + unstable_cache( + async () => { + const latestAction = await prisma.action.findFirst({ + where: { + personId, + }, + orderBy: { + createdAt: "desc", + }, + select: { + createdAt: true, + }, + }); + if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) { + return false; + } + return true; + }, + [`isPersonActive-${personId}`], + { + tags: [activePersonCache.tag.byId(personId)], + revalidate: 60 * 60 * 24, // 24 hours + } + )();