mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-01 16:36:18 -06:00
fix: update action indexes for faster query processing (#2154)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
@@ -350,8 +350,9 @@ model Action {
|
||||
/// [ActionProperties]
|
||||
properties Json @default("{}")
|
||||
|
||||
@@index([personId, actionClassId, createdAt])
|
||||
@@index([actionClassId, createdAt])
|
||||
@@index([personId])
|
||||
@@index([actionClassId])
|
||||
}
|
||||
|
||||
enum EnvironmentType {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)();
|
||||
|
||||
Reference in New Issue
Block a user