fix: Call-to-Action statistics do not work as expected (#2457)

Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
This commit is contained in:
Piyush Gupta
2024-04-25 17:10:42 +05:30
committed by GitHub
parent ad9ddb61cf
commit db5efd3b8c
9 changed files with 93 additions and 44 deletions

View File

@@ -1,3 +1,5 @@
import { InboxIcon } from "lucide-react";
import { TSurveyQuestionSummaryCta } from "@formbricks/types/surveys";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
@@ -10,12 +12,33 @@ interface CTASummaryProps {
export const CTASummary = ({ questionSummary }: CTASummaryProps) => {
return (
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} />
<div className="rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<QuestionSummaryHeader
questionSummary={questionSummary}
showResponses={false}
insights={
<>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.impressionCount} Impressions`}
</div>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.clickCount} Clicks`}
</div>
{!questionSummary.question.required && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.skipCount} Skips`}
</div>
)}
</>
}
/>
<div className="space-y-5 rounded-b-lg bg-white px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">Click-through rate (CTR)</p>
<p className="font-semibold text-slate-700">CTR</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.ctr.percentage, 1)}%
@@ -23,7 +46,7 @@ export const CTASummary = ({ questionSummary }: CTASummaryProps) => {
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.ctr.count} {questionSummary.ctr.count === 1 ? "click" : "clicks"}
{questionSummary.ctr.count} {questionSummary.ctr.count === 1 ? "Click" : "Clicks"}
</p>
</div>
<ProgressBar barColor="bg-brand" progress={questionSummary.ctr.percentage / 100} />

View File

@@ -6,9 +6,11 @@ import { TSurveyQuestionSummary } from "@formbricks/types/surveys";
interface HeadProps {
questionSummary: TSurveyQuestionSummary;
showResponses?: boolean;
insights?: JSX.Element;
}
export const QuestionSummaryHeader = ({ questionSummary }: HeadProps) => {
export const QuestionSummaryHeader = ({ questionSummary, insights, showResponses = true }: HeadProps) => {
const questionType = questionTypes.find((type) => type.id === questionSummary.question.type);
return (
@@ -23,10 +25,13 @@ export const QuestionSummaryHeader = ({ questionSummary }: HeadProps) => {
{questionType && <questionType.icon className="mr-2 h-4 w-4 " />}
{questionType ? questionType.label : "Unknown Question Type"} Question
</div>
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.responseCount} Responses`}
</div>
{showResponses && (
<div className=" flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{`${questionSummary.responseCount} Responses`}
</div>
)}
{insights}
{!questionSummary.question.required && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">Optional</div>
)}

View File

@@ -25,8 +25,8 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
</Tooltip>
</TooltipProvider>
</div>
<div className="px-4 text-center md:px-6">Views</div>
<div className="pr-6 text-center md:pl-6">Drop Offs</div>
<div className="px-4 text-center md:px-6">Impressions</div>
<div className="pr-6 text-center md:pl-6">Drop-Offs</div>
</div>
{dropOff.map((quesDropOff) => (
<div
@@ -36,9 +36,9 @@ export const SummaryDropOffs = ({ dropOff }: SummaryDropOffsProps) => {
<div className="whitespace-pre-wrap text-center font-semibold">
{quesDropOff.ttc > 0 ? (quesDropOff.ttc / 1000).toFixed(2) + "s" : "N/A"}
</div>
<div className="whitespace-pre-wrap text-center font-semibold">{quesDropOff.views}</div>
<div className="whitespace-pre-wrap text-center font-semibold">{quesDropOff.impressions}</div>
<div className=" pl-6 text-center md:px-6">
<span className="font-semibold">{quesDropOff.dropOffCount}</span>
<span className="mr-1.5 font-semibold">{quesDropOff.dropOffCount}</span>
<span>({Math.round(quesDropOff.dropOffPercentage)}%)</span>
</div>
</div>

View File

@@ -1,8 +1,7 @@
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { timeSinceConditionally } from "@formbricks/lib/time";
import { TSurveySummary } from "@formbricks/types/surveys";
import { TSurvey } from "@formbricks/types/surveys";
import { TSurvey, TSurveySummary } from "@formbricks/types/surveys";
import { Button } from "@formbricks/ui/Button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
@@ -71,7 +70,7 @@ export const SummaryMetadata = ({
<div className="flex flex-col-reverse gap-y-2 lg:grid lg:grid-cols-3 lg:gap-x-2">
<div className="grid grid-cols-2 gap-4 md:grid-cols-5 md:gap-x-2 lg:col-span-2">
<StatCard
label="Displays"
label="Impressions"
percentage={null}
value={displayCount === 0 ? <span>-</span> : displayCount}
tooltipText="Number of times the survey has been viewed."
@@ -89,7 +88,7 @@ export const SummaryMetadata = ({
tooltipText="Number of times the survey has been completed."
/>
<StatCard
label="Drop Offs"
label="Drop-Offs"
percentage={`${Math.round(dropOffPercentage)}%`}
value={dropOffCount === 0 ? <span>-</span> : dropOffCount}
tooltipText="Number of times the survey has been started but not completed."
@@ -110,7 +109,7 @@ export const SummaryMetadata = ({
className="w-max self-start"
EndIcon={showDropOffs ? ChevronDownIcon : ChevronUpIcon}
onClick={() => setShowDropOffs(!showDropOffs)}>
Analyze Drop Offs
Analyze Drop-Offs
</Button>
</div>
</div>

View File

@@ -117,13 +117,13 @@ test.describe("JS Package Test", async () => {
// Survey should have 2 Displays
await page.waitForTimeout(1000);
await expect(page.getByText("Displays2")).toBeVisible();
await expect(page.getByText("Impressions2")).toBeVisible();
// Survey should have 1 Response
await page.waitForTimeout(1000);
await expect(page.getByRole("button", { name: "Responses50%" })).toBeVisible();
await expect(page.getByText("1 Responses", { exact: true }).first()).toBeVisible();
await expect(page.getByText("Click-through rate (CTR)100%")).toBeVisible();
await expect(page.getByText("CTR50%")).toBeVisible();
await expect(page.getByText("Somewhat disappointed100%")).toBeVisible();
await expect(page.getByText("Founder100%")).toBeVisible();
await expect(page.getByText("People who believe that PMF").first()).toBeVisible();

View File

@@ -230,7 +230,17 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
validateInputs([responseInput, ZResponseInput]);
captureTelemetry("response created");
const { environmentId, language, userId, surveyId, finished, data, meta, singleUseId } = responseInput;
const {
environmentId,
language,
userId,
surveyId,
finished,
data,
meta,
singleUseId,
ttc: initialTtc,
} = responseInput;
try {
let person: TPerson | null = null;
@@ -242,6 +252,8 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
}
}
const ttc = initialTtc ? (finished ? calculateTtcTotal(initialTtc) : initialTtc) : {};
const responsePrisma = await prisma.response.create({
data: {
survey: {
@@ -262,6 +274,7 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
}),
...(meta && ({ meta } as Prisma.JsonObject)),
singleUseId,
ttc: ttc,
},
select: responseSelection,
});
@@ -615,11 +628,12 @@ export const getSurveySummary = (
createdAt: filterCriteria?.createdAt,
});
const meta = getSurveySummaryMeta(responses, displayCount);
const dropOff = getSurveySummaryDropOff(survey, responses, displayCount);
const meta = getSurveySummaryMeta(responses, displayCount);
const questionWiseSummary = getQuestionWiseSummary(
checkForRecallInHeadline(survey, "default"),
responses
responses,
dropOff
);
return { meta, dropOff, summary: questionWiseSummary };

View File

@@ -7,9 +7,8 @@ import {
TResponseFilterCriteria,
TResponseUpdateInput,
TSurveyPersonAttributes,
TSurveySummary,
} from "@formbricks/types/responses";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurveyQuestionType, TSurveySummary } from "@formbricks/types/surveys";
import { TTag } from "@formbricks/types/tags";
import { transformPrismaPerson } from "../../../person/service";
@@ -480,7 +479,7 @@ export const mockSurveySummaryOutput: TSurveySummary = {
headline: "Question Text",
questionId: "ars2tjk8hsi8oqk1uac00mo8",
ttc: 0,
views: 0,
impressions: 0,
},
],
meta: {

View File

@@ -526,7 +526,7 @@ export const getSurveySummaryDropOff = (
let responseCounts = { ...initialTtc };
let dropOffArr = new Array(survey.questions.length).fill(0) as number[];
let viewsArr = new Array(survey.questions.length).fill(0) as number[];
let impressionsArr = new Array(survey.questions.length).fill(0) as number[];
let dropOffPercentageArr = new Array(survey.questions.length).fill(0) as number[];
responses.forEach((response) => {
@@ -546,7 +546,7 @@ export const getSurveySummaryDropOff = (
if (!currQues.required) {
if (!response.data[currQues.id]) {
viewsArr[currQuesIdx]++;
impressionsArr[currQuesIdx]++;
if (currQuesIdx === survey.questions.length - 1 && !response.finished) {
dropOffArr[currQuesIdx]++;
@@ -577,11 +577,11 @@ export const getSurveySummaryDropOff = (
(currQues.required && !response.data[currQues.id])
) {
dropOffArr[currQuesIdx]++;
viewsArr[currQuesIdx]++;
impressionsArr[currQuesIdx]++;
break;
}
viewsArr[currQuesIdx]++;
impressionsArr[currQuesIdx]++;
let nextQuesIdx = currQuesIdx + 1;
const questionHasCustomLogic = currQues.logic;
@@ -598,7 +598,7 @@ export const getSurveySummaryDropOff = (
if (!response.data[survey.questions[nextQuesIdx]?.id] && !response.finished) {
dropOffArr[nextQuesIdx]++;
viewsArr[nextQuesIdx]++;
impressionsArr[nextQuesIdx]++;
break;
}
@@ -613,20 +613,22 @@ export const getSurveySummaryDropOff = (
});
if (!survey.welcomeCard.enabled) {
dropOffArr[0] = displayCount - viewsArr[0];
if (viewsArr[0] > displayCount) dropOffPercentageArr[0] = 0;
dropOffArr[0] = displayCount - impressionsArr[0];
if (impressionsArr[0] > displayCount) dropOffPercentageArr[0] = 0;
dropOffPercentageArr[0] =
viewsArr[0] - displayCount >= 0 ? 0 : ((displayCount - viewsArr[0]) / displayCount) * 100 || 0;
impressionsArr[0] - displayCount >= 0
? 0
: ((displayCount - impressionsArr[0]) / displayCount) * 100 || 0;
viewsArr[0] = displayCount;
impressionsArr[0] = displayCount;
} else {
dropOffPercentageArr[0] = (dropOffArr[0] / viewsArr[0]) * 100;
dropOffPercentageArr[0] = (dropOffArr[0] / impressionsArr[0]) * 100;
}
for (let i = 1; i < survey.questions.length; i++) {
if (viewsArr[i] !== 0) {
dropOffPercentageArr[i] = (dropOffArr[i] / viewsArr[i]) * 100;
if (impressionsArr[i] !== 0) {
dropOffPercentageArr[i] = (dropOffArr[i] / impressionsArr[i]) * 100;
}
}
@@ -635,7 +637,7 @@ export const getSurveySummaryDropOff = (
questionId: question.id,
headline: getLocalizedValue(question.headline, "default"),
ttc: convertFloatTo2Decimal(totalTtc[question.id]) || 0,
views: viewsArr[index] || 0,
impressions: impressionsArr[index] || 0,
dropOffCount: dropOffArr[index] || 0,
dropOffPercentage: convertFloatTo2Decimal(dropOffPercentageArr[index]) || 0,
};
@@ -683,12 +685,13 @@ const checkForI18n = (response: TResponse, id: string, survey: TSurvey, language
export const getQuestionWiseSummary = (
survey: TSurvey,
responses: TResponse[]
responses: TResponse[],
dropOff: TSurveySummary["dropOff"]
): TSurveySummary["summary"] => {
const VALUES_LIMIT = 50;
let summary: TSurveySummary["summary"] = [];
survey.questions.forEach((question) => {
survey.questions.forEach((question, idx) => {
switch (question.type) {
case TSurveyQuestionType.OpenText: {
let values: TSurveyQuestionSummaryOpenText["samples"] = [];
@@ -957,15 +960,18 @@ export const getQuestionWiseSummary = (
});
const totalResponses = data.clicked + data.dismissed;
const impressions = dropOff[idx].impressions;
summary.push({
type: question.type,
question,
impressionCount: impressions,
clickCount: data.clicked,
skipCount: data.dismissed,
responseCount: totalResponses,
ctr: {
count: data.clicked,
percentage:
totalResponses > 0 ? convertFloatTo2Decimal((data.clicked / totalResponses) * 100) : 0,
percentage: impressions > 0 ? convertFloatTo2Decimal((data.clicked / impressions) * 100) : 0,
},
});
break;

View File

@@ -679,6 +679,9 @@ export type TSurveyQuestionSummaryNps = z.infer<typeof ZSurveyQuestionSummaryNps
export const ZSurveyQuestionSummaryCta = z.object({
type: z.literal("cta"),
question: ZSurveyCTAQuestion,
impressionCount: z.number(),
clickCount: z.number(),
skipCount: z.number(),
responseCount: z.number(),
ctr: z.object({
count: z.number(),
@@ -832,7 +835,7 @@ export const ZSurveySummary = z.object({
questionId: z.string().cuid2(),
headline: z.string(),
ttc: z.number(),
views: z.number(),
impressions: z.number(),
dropOffCount: z.number(),
dropOffPercentage: z.number(),
})