fix: update action indexes for faster query processing (#2154)

This commit is contained in:
Matti Nannt
2024-02-28 19:17:40 +01:00
committed by GitHub
parent 5fc18fc445
commit 06eebe36ee
6 changed files with 68 additions and 106 deletions

View File

@@ -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,

View File

@@ -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");

View File

@@ -350,8 +350,9 @@ model Action {
/// [ActionProperties]
properties Json @default("{}")
@@index([personId, actionClassId, createdAt])
@@index([actionClassId, createdAt])
@@index([personId])
@@index([actionClassId])
}
enum EnvironmentType {

View File

@@ -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<TAction | null> => {
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<TAction | null> => {
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<TAction[]> => {
const actions = await unstable_cache(
async () => {
@@ -257,6 +159,11 @@ export const createAction = async (data: TActionInput): Promise<TAction> => {
},
});
const isPersonMonthlyActive = await getIsPersonMonthlyActive(person.id);
if (!isPersonMonthlyActive) {
activePersonCache.revalidate({ id: person.id });
}
actionCache.revalidate({
environmentId,
personId: person.id,

View File

@@ -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));
}
},
};

View File

@@ -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<boolean> =>
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
}
)();