chore: optimisations in survey summary (#4592)

Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Dhruwang Jariwala
2025-01-15 09:51:58 +05:30
committed by GitHub
parent 01edc0e6e0
commit 17ac777e9b
@@ -2,22 +2,27 @@ import "server-only";
import { getInsightsBySurveyIdQuestionId } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/(analysis)/summary/lib/insights";
import { Prisma } from "@prisma/client";
import { cache as reactCache } from "react";
import { prisma } from "@formbricks/database";
import { cache } from "@formbricks/lib/cache";
import { RESPONSES_PER_PAGE } from "@formbricks/lib/constants";
import { displayCache } from "@formbricks/lib/display/cache";
import { getDisplayCountBySurveyId } from "@formbricks/lib/display/service";
import { getLocalizedValue } from "@formbricks/lib/i18n/utils";
import { responseCache } from "@formbricks/lib/response/cache";
import { getResponseCountBySurveyId, getResponses } from "@formbricks/lib/response/service";
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
import { buildWhereClause } from "@formbricks/lib/response/utils";
import { surveyCache } from "@formbricks/lib/survey/cache";
import { getSurvey } from "@formbricks/lib/survey/service";
import { evaluateLogic, performActions } from "@formbricks/lib/surveyLogic/utils";
import { validateInputs } from "@formbricks/lib/utils/validate";
import { ZId } from "@formbricks/types/common";
import { ZId, ZOptionalNumber } from "@formbricks/types/common";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
import {
TResponse,
TResponseContact,
TResponseContactAttributes,
TResponseData,
TResponseFilterCriteria,
TResponseTtc,
TResponseVariables,
ZResponseFilterCriteria,
} from "@formbricks/types/responses";
@@ -42,8 +47,19 @@ import {
} from "@formbricks/types/surveys/types";
import { convertFloatTo2Decimal } from "./utils";
interface TSurveySummaryResponse {
id: string;
data: TResponseData;
updatedAt: Date;
contact: TResponseContact | null;
contactAttributes: TResponseContactAttributes;
language: string | null;
ttc: TResponseTtc;
finished: boolean;
}
export const getSurveySummaryMeta = (
responses: TResponse[],
responses: TSurveySummaryResponse[],
displayCount: number
): TSurveySummary["meta"] => {
const completedResponses = responses.filter((response) => response.finished).length;
@@ -132,7 +148,7 @@ const evaluateLogicAndGetNextQuestionId = (
export const getSurveySummaryDropOff = (
survey: TSurvey,
responses: TResponse[],
responses: TSurveySummaryResponse[],
displayCount: number
): TSurveySummary["dropOff"] => {
const initialTtc = survey.questions.reduce((acc: Record<string, number>, question) => {
@@ -258,16 +274,16 @@ const getLanguageCode = (surveyLanguages: TSurveyLanguage[], languageCode: strin
return language?.default ? "default" : language?.language.code || "default";
};
const checkForI18n = (response: TResponse, id: string, survey: TSurvey, languageCode: string) => {
const checkForI18n = (responseData: TResponseData, id: string, survey: TSurvey, languageCode: string) => {
const question = survey.questions.find((question) => question.id === id);
if (question?.type === "multipleChoiceMulti" || question?.type === "ranking") {
// Initialize an array to hold the choice values
let choiceValues = [] as string[];
(typeof response.data[id] === "string"
? ([response.data[id]] as string[])
: (response.data[id] as string[])
(typeof responseData[id] === "string"
? ([responseData[id]] as string[])
: (responseData[id] as string[])
)?.forEach((data) => {
choiceValues.push(
getLocalizedValue(
@@ -283,15 +299,15 @@ const checkForI18n = (response: TResponse, id: string, survey: TSurvey, language
// Return the localized value of the choice fo multiSelect single question
const choice = (question as TSurveyMultipleChoiceQuestion)?.choices.find(
(choice) => choice.label[languageCode] === response.data[id]
(choice) => choice.label[languageCode] === responseData[id]
);
return getLocalizedValue(choice?.label, "default") || response.data[id];
return getLocalizedValue(choice?.label, "default") || responseData[id];
};
export const getQuestionSummary = async (
survey: TSurvey,
responses: TResponse[],
responses: TSurveySummaryResponse[],
dropOff: TSurveySummary["dropOff"]
): Promise<TSurveySummary["summary"]> => {
const VALUES_LIMIT = 50;
@@ -353,7 +369,7 @@ export const getQuestionSummary = async (
const answer =
responseLanguageCode === "default"
? response.data[question.id]
: checkForI18n(response, question.id, survey, responseLanguageCode);
: checkForI18n(response.data, question.id, survey, responseLanguageCode);
let hasValidAnswer = false;
@@ -824,7 +840,7 @@ export const getQuestionSummary = async (
const answer =
responseLanguageCode === "default"
? response.data[question.id]
: checkForI18n(response, question.id, survey, responseLanguageCode);
: checkForI18n(response.data, question.id, survey, responseLanguageCode);
if (Array.isArray(answer)) {
totalResponseCount++;
@@ -899,19 +915,23 @@ export const getSurveySummary = reactCache(
throw new ResourceNotFoundError("Survey", surveyId);
}
const batchSize = 10000;
const totalResponseCount = await getResponseCountBySurveyId(surveyId);
const filteredResponseCount = await getResponseCountBySurveyId(surveyId, filterCriteria);
const batchSize = 5000;
const responseCount = await getResponseCountBySurveyId(surveyId, filterCriteria);
const hasFilter = totalResponseCount !== filteredResponseCount;
const hasFilter = Object.keys(filterCriteria ?? {}).length > 0;
const pages = Math.ceil(filteredResponseCount / batchSize);
const pages = Math.ceil(responseCount / batchSize);
let responses: TResponse[] = [];
for (let i = 0; i < pages; i++) {
const batchResponses = await getResponses(surveyId, batchSize, i * batchSize, filterCriteria);
responses = responses.concat(batchResponses);
}
// Create an array of batch fetch promises
const batchPromises = Array.from({ length: pages }, (_, i) =>
getResponsesForSummary(surveyId, batchSize, i * batchSize, filterCriteria)
);
// Fetch all batches in parallel
const batchResults = await Promise.all(batchPromises);
// Combine all batch results
const responses = batchResults.flat();
const responseIds = hasFilter ? responses.map((response) => response.id) : [];
@@ -921,8 +941,10 @@ export const getSurveySummary = reactCache(
});
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
const meta = getSurveySummaryMeta(responses, displayCount);
const questionWiseSummary = await getQuestionSummary(survey, responses, dropOff);
const [meta, questionWiseSummary] = await Promise.all([
getSurveySummaryMeta(responses, displayCount),
getQuestionSummary(survey, responses, dropOff),
]);
return { meta, dropOff, summary: questionWiseSummary };
} catch (error) {
@@ -943,3 +965,86 @@ export const getSurveySummary = reactCache(
}
)()
);
export const getResponsesForSummary = reactCache(
async (
surveyId: string,
limit: number,
offset: number,
filterCriteria?: TResponseFilterCriteria
): Promise<TSurveySummaryResponse[]> =>
cache(
async () => {
validateInputs(
[surveyId, ZId],
[limit, ZOptionalNumber],
[offset, ZOptionalNumber],
[filterCriteria, ZResponseFilterCriteria.optional()]
);
const queryLimit = limit ?? RESPONSES_PER_PAGE;
const survey = await getSurvey(surveyId);
if (!survey) return [];
try {
const responses = await prisma.response.findMany({
where: {
surveyId,
...buildWhereClause(survey, filterCriteria),
},
select: {
id: true,
data: true,
updatedAt: true,
contact: {
select: {
id: true,
attributes: {
select: { attributeKey: true, value: true },
},
},
},
contactAttributes: true,
language: true,
ttc: true,
finished: true,
},
orderBy: [
{
createdAt: "desc",
},
],
take: queryLimit,
skip: offset,
});
const transformedResponses: TSurveySummaryResponse[] = await Promise.all(
responses.map((responsePrisma) => {
return {
...responsePrisma,
contact: responsePrisma.contact
? {
id: responsePrisma.contact.id as string,
userId: responsePrisma.contact.attributes.find(
(attribute) => attribute.attributeKey.key === "userId"
)?.value as string,
}
: null,
};
})
);
return transformedResponses;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
}
throw error;
}
},
[`getResponsesForSummary-${surveyId}-${limit}-${offset}-${JSON.stringify(filterCriteria)}`],
{
tags: [responseCache.tag.bySurveyId(surveyId)],
}
)()
);