feat: Randomizer for in-app surveys (#1972)

Co-authored-by: Matti Nannt <mail@matthiasnannt.com>
This commit is contained in:
Dhruwang Jariwala
2024-01-29 18:24:32 +05:30
committed by GitHub
parent 07f6f1d04b
commit 698da4c3a1
9 changed files with 96 additions and 7 deletions

View File

@@ -40,9 +40,11 @@ export default function WhenToSendCard({
const [isAddEventModalOpen, setAddEventModalOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const [actionClasses, setActionClasses] = useState<TActionClass[]>(propActionClasses);
const [randomizerToggle, setRandomizerToggle] = useState(localSurvey.displayPercentage ? true : false);
const { isViewer } = getAccessFlags(membershipRole);
const autoClose = localSurvey.autoClose !== null;
const delay = localSurvey.delay !== 0;
const addTriggerEvent = useCallback(() => {
const updatedSurvey = { ...localSurvey };
@@ -71,7 +73,7 @@ export default function WhenToSendCard({
setLocalSurvey(updatedSurvey);
};
const handleCheckMark = () => {
const handleAutoCloseToggle = () => {
if (autoClose) {
const updatedSurvey = { ...localSurvey, autoClose: null };
setLocalSurvey(updatedSurvey);
@@ -81,6 +83,27 @@ export default function WhenToSendCard({
}
};
const handleDelayToggle = () => {
if (delay) {
const updatedSurvey = { ...localSurvey, delay: 0 };
setLocalSurvey(updatedSurvey);
} else {
const updatedSurvey = { ...localSurvey, delay: 5 };
setLocalSurvey(updatedSurvey);
}
};
const handleDisplayPercentageToggle = () => {
if (localSurvey.displayPercentage) {
const updatedSurvey = { ...localSurvey, displayPercentage: null };
setLocalSurvey(updatedSurvey);
} else {
const updatedSurvey = { ...localSurvey, displayPercentage: 50 };
setLocalSurvey(updatedSurvey);
}
setRandomizerToggle(!randomizerToggle);
};
const handleInputSeconds = (e: any) => {
let value = parseInt(e.target.value);
@@ -96,6 +119,11 @@ export default function WhenToSendCard({
setLocalSurvey(updatedSurvey);
};
const handleRandomizerInput = (e) => {
const updatedSurvey = { ...localSurvey, displayPercentage: parseInt(e.target.value) };
setLocalSurvey(updatedSurvey);
};
useEffect(() => {
if (isAddEventModalOpen) return;
if (activeIndex !== null) {
@@ -155,8 +183,9 @@ export default function WhenToSendCard({
</div>
</div>
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="">
<hr className="py-1 text-slate-600" />
<hr className="py-1 text-slate-600" />
<Collapsible.CollapsibleContent className="p-3">
{!isAddEventModalOpen &&
localSurvey.triggers?.map((triggerEventClass, idx) => (
<div className="mt-2" key={idx}>
@@ -209,7 +238,14 @@ export default function WhenToSendCard({
</Button>
</div>
<div className="ml-2 flex items-center space-x-1 px-4 pb-4">
<div className="ml-2 flex items-center space-x-1 px-4 pb-4"></div>
<AdvancedOptionToggle
htmlId="delay"
isChecked={delay}
onToggle={handleDelayToggle}
title="Add delay before showing survey"
description="Wait a few seconds after the trigger before showing the survey"
childBorder={true}>
<label
htmlFor="triggerDelay"
className="flex w-full cursor-pointer items-center rounded-lg border bg-slate-50 p-4">
@@ -228,12 +264,11 @@ export default function WhenToSendCard({
</p>
</div>
</label>
</div>
</AdvancedOptionToggle>
<AdvancedOptionToggle
htmlId="autoClose"
isChecked={autoClose}
onToggle={handleCheckMark}
onToggle={handleAutoCloseToggle}
title="Auto close on inactivity"
description="Automatically close the survey if the user does not respond after certain number of seconds"
childBorder={true}>
@@ -252,6 +287,30 @@ export default function WhenToSendCard({
</p>
</label>
</AdvancedOptionToggle>
<AdvancedOptionToggle
htmlId="randomizer"
isChecked={randomizerToggle}
onToggle={handleDisplayPercentageToggle}
title="Show survey to % of users"
description="Only display the survey to a subset of the users"
childBorder={true}>
<div className="w-full">
<div className="flex flex-col justify-center rounded-lg border bg-slate-50 p-6">
<h3 className="mb-4 text-sm font-semibold text-slate-700">
Show to {localSurvey.displayPercentage}% of targeted users
</h3>
<input
id="small-range"
type="range"
min="1"
max="100"
value={localSurvey.displayPercentage ?? 50}
onChange={handleRandomizerInput}
className="range-sm mb-6 h-1 w-full cursor-pointer appearance-none rounded-lg bg-slate-200 dark:bg-slate-700"
/>
</div>
</div>
</AdvancedOptionToggle>
</Collapsible.CollapsibleContent>
</Collapsible.Root>
<AddNoCodeActionModal

View File

@@ -2522,6 +2522,7 @@ export const minimalSurvey: TSurvey = {
enabled: false,
},
delay: 0, // No delay
displayPercentage: null,
autoComplete: null,
closeOnDate: null,
surveyClosedMessage: {

View File

@@ -111,3 +111,12 @@ input[type="search"]::-ms-clear {
input[type="search"]::-ms-reveal {
display: none;
}
input[type='range']::-webkit-slider-thumb {
background: #0f172a;
height: 20px;
width: 20px;
border-radius: 50%;
-webkit-appearance: none;
}

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Survey" ADD COLUMN "displayPercentage" INTEGER;

View File

@@ -300,6 +300,7 @@ model Survey {
verifyEmail Json?
pin String?
resultShareKey String? @unique
displayPercentage Int?
@@index([environmentId])
}

View File

@@ -12,6 +12,11 @@ const config = Config.getInstance();
const intentsToNotCreateOnApp = ["Exit Intent (Desktop)", "50% Scroll"];
const shouldDisplayBasedOnPercentage = (displayPercentage: number) => {
const randomNum = Math.floor(Math.random() * 100) + 1;
return randomNum <= displayPercentage;
};
export const trackAction = async (
name: string,
properties: TJsActionInput["properties"] = {}
@@ -64,6 +69,14 @@ export const trackAction = async (
export const triggerSurvey = async (actionName: string, activeSurveys: TSurvey[]): Promise<void> => {
for (const survey of activeSurveys) {
// Check if the survey should be displayed based on displayPercentage
if (survey.displayPercentage) {
const shouldDisplaySurvey = shouldDisplayBasedOnPercentage(survey.displayPercentage);
if (!shouldDisplaySurvey) {
logger.debug("Survey display skipped based on displayPercentage.");
continue;
}
}
for (const trigger of survey.triggers) {
if (trigger === actionName) {
logger.debug(`Formbricks: survey ${survey.id} triggered by action "${actionName}"`);

View File

@@ -41,6 +41,7 @@ export const selectSurvey = {
autoClose: true,
closeOnDate: true,
delay: true,
displayPercentage: true,
autoComplete: true,
verifyEmail: true,
redirectUrl: true,

View File

@@ -135,6 +135,7 @@ export const mockSurveyOutput: SurveyMock = {
productOverwrites: null,
singleUse: null,
styling: null,
displayPercentage: null,
pin: null,
resultShareKey: null,
...baseSurveyProperties,
@@ -157,6 +158,7 @@ export const updateSurveyInput: TSurvey = {
productOverwrites: null,
styling: null,
singleUse: null,
displayPercentage: null,
pin: null,
resultShareKey: null,
...commonMockProperties,

View File

@@ -435,6 +435,7 @@ export const ZSurvey = z.object({
verifyEmail: ZSurveyVerifyEmail.nullable(),
pin: z.string().nullable().optional(),
resultShareKey: z.string().nullable(),
displayPercentage: z.number().min(1).max(100).nullable(),
});
export const ZSurveyInput = z.object({