mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-25 10:20:03 -06:00
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:
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user