feat: Enable Prefilling of several values (#2482)

Co-authored-by: Johannes <72809645+jobenjada@users.noreply.github.com>
Co-authored-by: Johannes <johannes@formbricks.com>
Co-authored-by: pandeymangg <anshuman.pandey9999@gmail.com>
This commit is contained in:
Piyush Gupta
2024-04-29 14:29:28 +05:30
committed by GitHub
parent 7751a3fcfd
commit e95a8c760c
7 changed files with 128 additions and 104 deletions
@@ -12,15 +12,16 @@ export const metadata = {
# Data Prefilling in Link Surveys
Data prefilling via the URL allows you to increase conversion rate by prefilling data you already have in a different system.
Data prefilling via the URL allows you to increase completion rate by prefilling data you already have in a different system. Formbricks allows you to prefill multiple questions in a survey.
## Purpose
URL prefilling of data comes in handy when you:
Data prefilling via URL comes in handy when you:
- Have data for some of the respondents, but not all
- Have data you want the respondent to confirm or update
- Have data in a different system (e.g. your database) and want to add it to the user profile in Formbricks
- Want to embed the first question in an email and increase conversion by prefilling the choice
- Want to embed a survey in an email and increase completion by prefilling the choice selected in the email
## Quick Example
@@ -28,26 +29,24 @@ URL prefilling of data comes in handy when you:
<CodeGroup title="Example URL">
```sh
https://app.formbricks.com/s/clin3dxja02k8l80hpwmx4bjy?question_id=5
https://app.formbricks.com/s/clin3dxja02k8l80hpwmx4bjy?question_id_1=answer_1&question_id_2=answer_2
```
</CodeGroup>
</Col>
## How it works
To prefill the first question of a survey, append `?question_id=answer` at the end of the survey URL. The answer has to match the expected type of the question. For example, if the first question is a rating question, the answer has to be a number. If the first question is a single select question, the answer has to be a string.
To prefill the questions of a survey, add query parameters to the survey URL. The query parameter should be in the format `questionId=answer`. The answer has to match the expected type of the question to pass through the [validation](/docs/link-surveys/data-prefilling#validation). For example, if the first question is a rating question, the answer has to be a number. If the first question is a single select question, the answer has to be a string identical to one of the answer options.
Please make sure the answer is [URL encoded](https://www.urlencoder.org/).
<Note>Please make sure the answer is [URL encoded](https://www.urlencoder.org/).</Note>
<Note>
## Prefill only the first question
## Prefilling multiple values
Currently, you can only prefill the first question of a link survey.
</Note>
Formbricks let's you prefill as many values as you want. You can combine multiple values in the URL using `&` so for example `name=Bernadette&age=18`. The order of the query parameters does not matter so you can always move around questions or add new ones without having to worry about the order of the query parameters.
## Where do I find my question Id?
You find the `questionId` in the Advanced Settings at the bottom of each question card in the Survey Editor. As you see, you can update the `questionId` to any string you like. However, once you published your survey, this `questionId` cannot be updated anymore:
You find the `questionId` in the **Advanced Settings** toggle at the bottom of each question card in the Survey Editor. As you see, you can update the `questionId` to any string you like. However, once you published your survey, this `questionId` cannot be updated anymore:
<MdxImage
src={QuestionId}
@@ -62,8 +61,10 @@ Here are a few examples to get you started:
### Rating Question
Translates to 5 stars / points / emojis:
<Col>
<CodeGroup title="Translates to 5 stars / points / emojis">
<CodeGroup title="Rating Question">
```sh
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?rating_question_id=5
@@ -71,9 +72,13 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?rating_question_id=5
</CodeGroup>
</Col>
### NPS Question
Translates to an NPS rating of 10:
<Col>
<CodeGroup title="Translates to an NPS rating of 10">
<CodeGroup title="NPS Questions">
```sh
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?nps_question_id=10
@@ -81,9 +86,13 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?nps_question_id=10
</CodeGroup>
</Col>
### Single Select Question (Radio)
Chooses the option 'Very disappointed' in the single select question. The string has to be identical to the option in your question:
<Col>
<CodeGroup title="Chooses the option 'Very disappointed' in the single select question. The string has to be identical to the option in your question">
<CodeGroup title="Single-select Question">
```sh
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?single_select_question_id=Very%20disappointed
@@ -91,9 +100,13 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?single_select_question_id
</CodeGroup>
</Col>
### Multi Select Question (Checkbox)
Selects three options 'Sun, Palms and Beach' in the multi select question. The strings have to be identical to the options in your question:
<Col>
<CodeGroup title="Selects three options 'Sun, Palms and Beach' in the multi select question. The strings have to be identical to the options in your question">
<CodeGroup title="Multi-select Question">
```sh
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?multi_select_question_id=Sun%2CPalms%2CBeach
@@ -101,9 +114,13 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?multi_select_question_id=
</CodeGroup>
</Col>
### Open Text Question
Adds 'I love Formbricks' as the answer to the open text question:
<Col>
<CodeGroup title="Adds 'I love Formbricks' as the answer to the open text question">
<CodeGroup title="Open Text Question">
```sh
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?openText_question_id=I%20love%20Formbricks
@@ -111,9 +128,13 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?openText_question_id=I%20
</CodeGroup>
</Col>
### CTA Question
Adds 'clicked' as the answer to the CTA question. Alternatively, you can set it to 'dismissed' to skip the question:
<Col>
<CodeGroup title="Adds 'clicked' as the answer to the CTA question. Alternatively, you can set it to 'dismissed' to skip the question.">
<CodeGroup title="CTA Question">
```txt
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?cta_question_id=clicked
@@ -121,15 +142,50 @@ https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?cta_question_id=clicked
</CodeGroup>
</Col>
### Consent Question
Adds 'accepted' as the answer to the Consent question. Alternatively, you can set it to 'dismissed' to skip the question.
<Col>
<CodeGroup title="Consent Question">
```txt
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?consent_question_id=accepted
```
</CodeGroup>
</Col>
### Picture Selection Question
Adds index of the selected image(s) as the answer to the Picture Selection question. The index starts from 1
<Col>
<CodeGroup title="Picture Selection Question.">
```txt
https://app.formbricks.com/s/clin3yxja52k8l80hpwmx4bjy?pictureSelection_question_id=1%2C2%2C3
```
</CodeGroup>
</Col>
<Note>
All other question types, you currently cannot prefill via the URL.
</Note>
## Validation
Make sure that the answer in the URL matches the expected type for the first question.
Make sure that the answer in the URL matches the expected type for the questions.
The URL validation works as follows:
- For Rating or NPS questions, the response is parsed as a number and verified if it's accepted by the schema.
- For CTA type questions, the valid values are "clicked" (main CTA) and "dismissed" (skip CTA).
- For Consent type questions, the valid values are "accepted" (consent given) and "dismissed" (consent not given).
- For Picture Selection type questions, the response is parsed as an array of numbers and verified if it's accepted by the schema.
- All other question types are strings.
### Youre good to go! 🎉
<Note>
If an answer is invalid, the prefilling will be ignored and the question is presented as if not prefilled.
</Note>
@@ -2,7 +2,7 @@
import SurveyLinkUsed from "@/app/s/[surveyId]/components/SurveyLinkUsed";
import VerifyEmail from "@/app/s/[surveyId]/components/VerifyEmail";
import { getPrefillResponseData } from "@/app/s/[surveyId]/lib/prefilling";
import { getPrefillValue } from "@/app/s/[surveyId]/lib/prefilling";
import { useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
@@ -10,7 +10,7 @@ import { FormbricksAPI } from "@formbricks/api";
import { ResponseQueue } from "@formbricks/lib/responseQueue";
import { SurveyState } from "@formbricks/lib/surveyState";
import { TProduct } from "@formbricks/types/product";
import { TResponse, TResponseData, TResponseUpdate } from "@formbricks/types/responses";
import { TResponse, TResponseUpdate } from "@formbricks/types/responses";
import { TUploadFileConfig } from "@formbricks/types/storage";
import { TSurvey } from "@formbricks/types/surveys";
import { ClientLogo } from "@formbricks/ui/ClientLogo";
@@ -27,7 +27,6 @@ interface LinkSurveyProps {
product: TProduct;
userId?: string;
emailVerificationStatus?: string;
prefillAnswer?: string;
singleUseId?: string;
singleUseResponse?: TResponse;
webAppUrl: string;
@@ -41,7 +40,6 @@ export default function LinkSurvey({
product,
userId,
emailVerificationStatus,
prefillAnswer,
singleUseId,
singleUseResponse,
webAppUrl,
@@ -80,9 +78,7 @@ export default function LinkSurvey({
return new SurveyState(survey.id, singleUseId, responseId, userId);
}, [survey.id, singleUseId, responseId, userId]);
const prefillResponseData: TResponseData | undefined = prefillAnswer
? getPrefillResponseData(survey.questions[0], survey, prefillAnswer, languageCode)
: undefined;
const prefillValue = getPrefillValue(survey, searchParams, languageCode);
const responseQueue = useMemo(
() =>
@@ -263,7 +259,7 @@ export default function LinkSurvey({
return uploadedUrl;
}}
autoFocus={autoFocus}
prefillResponseData={prefillResponseData}
prefillResponseData={prefillValue}
responseCount={responseCount}
getSetQuestionId={(f: (value: string) => void) => {
setQuestionId = f;
@@ -19,7 +19,6 @@ interface LinkSurveyPinScreenProps {
product: TProduct;
userId?: string;
emailVerificationStatus?: string;
prefillAnswer?: string;
singleUseId?: string;
singleUseResponse?: TResponse;
webAppUrl: string;
@@ -37,7 +36,6 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
webAppUrl,
emailVerificationStatus,
userId,
prefillAnswer,
singleUseId,
singleUseResponse,
IMPRINT_URL,
@@ -120,7 +118,6 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
product={product}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={prefillAnswer}
singleUseId={singleUseId}
singleUseResponse={singleUseResponse}
webAppUrl={webAppUrl}
+39 -37
View File
@@ -2,40 +2,33 @@ import { TResponseData } from "@formbricks/types/responses";
import { TSurveyQuestionType } from "@formbricks/types/surveys";
import { TSurvey, TSurveyQuestion } from "@formbricks/types/surveys";
export function getPrefillResponseData(
currentQuestion: TSurveyQuestion,
export const getPrefillValue = (
survey: TSurvey,
firstQuestionPrefill: string,
searchParams: URLSearchParams,
languageId: string
): TResponseData | undefined {
try {
if (firstQuestionPrefill) {
if (!currentQuestion) return;
const firstQuestionId = survey?.questions[0].id;
if (currentQuestion.id !== firstQuestionId) return;
const question = survey?.questions.find((q: any) => q.id === firstQuestionId);
if (!question) throw new Error("Question not found");
): TResponseData | undefined => {
const prefillAnswer: TResponseData = {};
let questionIdxMap: { [key: string]: number } = {};
const answer = transformAnswer(question, firstQuestionPrefill || "", languageId);
const answerObj = { [firstQuestionId]: answer };
survey.questions.forEach((q, idx) => {
questionIdxMap[q.id] = idx;
});
if (
question.type === TSurveyQuestionType.CTA &&
question.buttonExternal &&
question.buttonUrl &&
answer === "clicked"
) {
window?.open(question.buttonUrl, "blank");
}
searchParams.forEach((value, key) => {
const questionId = key;
const questionIdx = questionIdxMap[questionId];
const question = survey.questions[questionIdx];
const answer = value;
return answerObj;
if (question && checkValidity(question, answer, languageId)) {
prefillAnswer[questionId] = transformAnswer(question, answer, languageId);
}
} catch (error) {
console.error(error);
}
}
});
export const checkValidity = (question: TSurveyQuestion, answer: any, language: string): boolean => {
return Object.keys(prefillAnswer).length > 0 ? prefillAnswer : undefined;
};
export const checkValidity = (question: TSurveyQuestion, answer: string, language: string): boolean => {
if (question.required && (!answer || answer === "")) return false;
try {
switch (question.type) {
@@ -56,11 +49,13 @@ export const checkValidity = (question: TSurveyQuestion, answer: any, language:
return true;
}
case TSurveyQuestionType.MultipleChoiceMulti: {
answer = answer.split(",");
const answerChoices = answer.split(",");
const hasOther = question.choices[question.choices.length - 1].id === "other";
if (!hasOther) {
if (
!answer.every((ans: string) => question.choices.find((choice) => choice.label[language] === ans))
!answerChoices.every((ans: string) =>
question.choices.find((choice) => choice.label[language] === ans)
)
)
return false;
return true;
@@ -92,10 +87,8 @@ export const checkValidity = (question: TSurveyQuestion, answer: any, language:
return true;
}
case TSurveyQuestionType.PictureSelection: {
answer = answer.split(",");
if (!answer.every((ans: string) => question.choices.find((choice) => choice.id === ans)))
return false;
return true;
const answerChoices = answer.split(",");
return answerChoices.every((ans: string) => !isNaN(Number(ans)));
}
default:
return false;
@@ -125,7 +118,16 @@ export const transformAnswer = (
}
case TSurveyQuestionType.PictureSelection: {
return answer.split(",");
const answerChoicesIdx = answer.split(",");
const answerArr: string[] = [];
answerChoicesIdx.forEach((ansIdx) => {
const choice = question.choices[Number(ansIdx) - 1];
if (choice) answerArr.push(choice.id);
});
if (question.allowMulti) return answerArr;
return answerArr.slice(0, 1);
}
case TSurveyQuestionType.MultipleChoiceMulti: {
@@ -134,9 +136,9 @@ export const transformAnswer = (
if (!hasOthers) return ansArr;
// answer can be "a,b,c,d" and options can be a,c,others so we are filtering out the options that are not in the options list and sending these non-existing values as a single string(representing others) like "a", "c", "b,d"
const options = question.choices.map((o) => o.label);
const others = ansArr.filter((a: string) => !options.includes(a[language]));
if (others.length > 0) ansArr = ansArr.filter((a: string) => options.includes(a[language]));
const options = question.choices.map((o) => o.label[language]);
const others = ansArr.filter((a: string) => !options.includes(a));
if (others.length > 0) ansArr = ansArr.filter((a: string) => options.includes(a));
if (others.length > 0) ansArr.push(others.join(","));
return ansArr;
}
-9
View File
@@ -4,7 +4,6 @@ import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
import { MediaBackground } from "@/app/s/[surveyId]/components/MediaBackground";
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
import { getMetadataForLinkSurvey } from "@/app/s/[surveyId]/metadata";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
@@ -153,12 +152,6 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
const isSurveyPinProtected = Boolean(!!survey && survey.pin);
const responseCount = await getResponseCountBySurveyId(survey.id);
// question pre filling: Check if the first question is prefilled and if it is valid
const prefillAnswer = searchParams[survey.questions[0].id];
const isPrefilledAnswerValid = prefillAnswer
? checkValidity(survey!.questions[0], prefillAnswer, languageCode)
: false;
if (isSurveyPinProtected) {
return (
<PinScreen
@@ -166,7 +159,6 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
product={product}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
webAppUrl={WEBAPP_URL}
@@ -187,7 +179,6 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
product={product}
userId={userId}
emailVerificationStatus={emailVerificationStatus}
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
singleUseResponse={singleUseResponse ? singleUseResponse : undefined}
webAppUrl={WEBAPP_URL}
@@ -52,7 +52,7 @@ export const LanguageSelect = ({ language, onLanguageChange, disabled }: Languag
disabled={disabled}
variant="minimal"
onClick={toggleDropdown}
className="flex h-full w-full justify-between border px-3 py-2">
className="flex h-full w-full justify-between border border-slate-200 px-3 py-2">
<span className="mr-2">{selectedOption?.english ?? "Select"}</span>
<ChevronDown className="h-4 w-4" />
</Button>
@@ -75,9 +75,9 @@ export const Survey = ({
// call onDisplay when component is mounted
onDisplay();
if (prefillResponseData) {
onSubmit(prefillResponseData, {}, true);
onChange(prefillResponseData);
}
if (startAtQuestionId && !prefillResponseData) {
if (startAtQuestionId) {
setQuestionId(startAtQuestionId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -110,18 +110,12 @@ export const Survey = ({
let currIdxTemp = currentQuestionIndex;
let currQuesTemp = currentQuestion;
const getNextQuestionId = (data: TResponseData, isFormPrefilling: Boolean = false): string => {
const getNextQuestionId = (data: TResponseData): string => {
const questions = survey.questions;
const responseValue = data[questionId];
if (questionId === "start") {
if (!isFormPrefilling) {
return questions[0]?.id || "end";
} else {
currIdxTemp = 0;
currQuesTemp = questions[0];
}
}
if (questionId === "start") return questions[0]?.id || "end";
if (currIdxTemp === -1) throw new Error("Question not found");
if (currQuesTemp?.logic && currQuesTemp?.logic.length > 0 && currentQuestion) {
for (let logic of currQuesTemp.logic) {
@@ -175,11 +169,7 @@ export const Survey = ({
}
}
}
// Code to handle case where prefilling and startAt, both are included
if (startAtQuestionId && isFormPrefilling && currIdxTemp === 0) {
// if isFormPrefilling enabled, then instead of going to the next question in sequence, we go to startAtQuestionId
return startAtQuestionId;
}
return questions[currIdxTemp + 1]?.id || "end";
};
@@ -188,13 +178,10 @@ export const Survey = ({
setResponseData(updatedResponseData);
};
const onSubmit = (responseData: TResponseData, ttc: TResponseTtc, isFormPrefilling: Boolean = false) => {
const onSubmit = (responseData: TResponseData, ttc: TResponseTtc) => {
const questionId = Object.keys(responseData)[0];
if (isFormPrefilling) {
onChange(responseData);
}
setLoadingElement(true);
const nextQuestionId = getNextQuestionId(responseData, isFormPrefilling);
const nextQuestionId = getNextQuestionId(responseData);
const finished = nextQuestionId === "end";
onResponse({ data: responseData, ttc, finished });
if (finished) {
@@ -253,7 +240,6 @@ export const Survey = ({
if (history?.length > 0) {
const newHistory = [...history];
prevQuestionId = newHistory.pop();
if (prefillResponseData && prevQuestionId === survey.questions[0].id) return;
setHistory(newHistory);
} else {
// otherwise go back to previous question in array
@@ -313,11 +299,7 @@ export const Survey = ({
ttc={ttc}
setTtc={setTtc}
onFileUpload={onFileUpload}
isFirstQuestion={
history && prefillResponseData
? history[history.length - 1] === survey.questions[0].id
: currentQuestion.id === survey?.questions[0]?.id
}
isFirstQuestion={currentQuestion.id === survey?.questions[0]?.id}
isLastQuestion={currentQuestion.id === survey.questions[survey.questions.length - 1].id}
languageCode={languageCode}
isInIframe={isInIframe}