From ee56aed2af2789930e4f41f2a11942de82393105 Mon Sep 17 00:00:00 2001 From: Alex V Date: Tue, 28 Jan 2025 11:40:25 +0100 Subject: [PATCH 1/7] feat: add a survey to opt out SR --- .../InternalMultipleChoiceSurvey.tsx | 60 ++++++++++++ .../InternalMultipleChoiceSurveyLogic.ts | 92 +++++++++++++++++++ frontend/src/lib/constants.tsx | 1 + .../environment/SessionRecordingSettings.tsx | 25 +++-- 4 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx create mode 100644 frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx new file mode 100644 index 0000000000000..c857460956389 --- /dev/null +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx @@ -0,0 +1,60 @@ +/** + * @fileoverview A component that displays an interactive survey within a session recording. It handles survey display, user responses, and submission + */ +import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' +import { useActions, useValues } from 'kea' +import { InternalMultipleChoiceSurveyLogic } from 'lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic' + +import { SurveyQuestion, SurveyQuestionType } from '~/types' + +interface InternalSurveyProps { + surveyId: string +} + +export function InternalMultipleChoiceSurvey({ surveyId }: InternalSurveyProps): JSX.Element { + const logic = InternalMultipleChoiceSurveyLogic({ surveyId }) + const { survey, surveyResponse, showThankYouMessage, thankYouMessage } = useValues(logic) + const { handleChoiceChange, handleSurveyResponse } = useActions(logic) + + if (!survey) { + return <> + } + + return ( +
+
+ {survey.questions.map((question: SurveyQuestion) => ( +
+ {showThankYouMessage && thankYouMessage} + {!showThankYouMessage && ( + <> + {question.question} + {question.type === SurveyQuestionType.MultipleChoice && ( +
    + {question.choices.map((choice) => ( +
  • + handleChoiceChange(choice, checked)} + label={choice} + /> +
  • + ))} +
+ )} + + {question.buttonText ?? 'Submit'} + + + )} +
+ ))} +
+
+ ) +} diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts new file mode 100644 index 0000000000000..98449557729fb --- /dev/null +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts @@ -0,0 +1,92 @@ +/** + * @fileoverview A logic that handles the internal multiple choice survey + */ +import { actions, afterMount, kea, key, listeners, path, props, reducers } from 'kea' +import posthog, { Survey as PostHogSurvey } from 'posthog-js' + +import type { InternalMultipleChoiceSurveyLogicType } from './InternalMultipleChoiceSurveyLogicType' + +export interface InternalSurveyLogicProps { + surveyId: string +} + +export const InternalMultipleChoiceSurveyLogic = kea([ + path(['lib', 'components', 'InternalSurvey', 'InternalMultipleChoiceSurveyLogic']), + props({} as InternalSurveyLogicProps), + key((props) => props.surveyId), + actions({ + setSurveyId: (surveyId: string) => ({ surveyId }), + getSurveys: () => ({}), + setSurvey: (survey: PostHogSurvey) => ({ survey }), + handleSurveys: (surveys: PostHogSurvey[]) => ({ surveys }), + handleSurveyResponse: () => ({}), + handleChoiceChange: (choice: string, isAdded: boolean) => ({ choice, isAdded }), + setShowThankYouMessage: (showThankYouMessage: boolean) => ({ showThankYouMessage }), + setThankYouMessage: (thankYouMessage: string) => ({ thankYouMessage }), + }), + reducers({ + surveyId: [ + null as string | null, + { + setSurveyId: (_, { surveyId }) => surveyId, + }, + ], + survey: [ + null as PostHogSurvey | null, + { + setSurvey: (_, { survey }) => survey, + }, + ], + thankYouMessage: [ + 'Thank you for your feedback!', + { + setThankYouMessage: (_, { thankYouMessage }) => thankYouMessage, + }, + ], + showThankYouMessage: [ + false as boolean, + { + setShowThankYouMessage: (_, { showThankYouMessage }) => showThankYouMessage, + }, + ], + surveyResponse: [ + [] as string[], + { + handleChoiceChange: (state, { choice, isAdded }) => + isAdded ? [...state, choice] : state.filter((c) => c !== choice), + }, + ], + }), + listeners(({ actions, values }) => ({ + /** When surveyId is set, get the list of surveys for the user */ + setSurveyId: () => { + posthog.getSurveys(actions.handleSurveys) + }, + /** Callback for the surveys response. Filter it to the surveyId and set the survey */ + handleSurveys: ({ surveys }) => { + const survey = surveys.find((s: PostHogSurvey) => s.id === values.surveyId) + if (survey) { + posthog.capture('survey shown', { + $survey_id: values.surveyId, + }) + actions.setSurvey(survey) + if (survey.appearance?.thankYouMessageHeader) { + actions.setThankYouMessage(survey.appearance?.thankYouMessageHeader) + } + } + }, + /** When the survey response is sent, capture the response and show the thank you message */ + handleSurveyResponse: () => { + posthog.capture('survey sent', { + $survey_id: values.surveyId, + $survey_response: values.surveyResponse, + }) + actions.setShowThankYouMessage(true) + setTimeout(() => actions.setSurvey(null), 5000) + }, + })), + afterMount(({ actions, props }) => { + /** When the logic is mounted, set the surveyId from the props */ + actions.setSurveyId(props.surveyId) + }), +]) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index d0521f844cc9b..b82b2e4606b15 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -318,6 +318,7 @@ export const SESSION_REPLAY_MINIMUM_DURATION_OPTIONS: LemonSelectOptions(false) + + /** + * Handle the opt in change + * @param checked + */ + const handleOptInChange = (checked: boolean): void => { + updateCurrentTeam({ + session_recording_opt_in: checked, + }) + + //If the user opts out, we show the survey + setShowSurvey(!checked) + } return (
@@ -521,16 +537,13 @@ export function ReplayGeneral(): JSX.Element { { - updateCurrentTeam({ - // when switching replay on or off, - // we set defaults for some of the other settings - session_recording_opt_in: checked, - }) + handleOptInChange(checked) }} label="Record user sessions" bordered checked={!!currentTeam?.session_recording_opt_in} /> + {showSurvey && }
From 8d6c02db9fccc61dffa131d110273a669604b42e Mon Sep 17 00:00:00 2001 From: Alex V Date: Tue, 28 Jan 2025 11:53:37 +0100 Subject: [PATCH 2/7] Update the survey id for production --- frontend/src/lib/constants.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index d0362d39d3718..d12af6b67d45b 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -316,7 +316,7 @@ export const SESSION_REPLAY_MINIMUM_DURATION_OPTIONS: LemonSelectOptions Date: Tue, 28 Jan 2025 16:30:01 +0100 Subject: [PATCH 3/7] Update --- .../InternalMultipleChoiceSurvey.tsx | 22 +++++++--- .../InternalMultipleChoiceSurveyLogic.ts | 41 +++++++++++++++---- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx index c857460956389..61da02dce76ff 100644 --- a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx @@ -1,7 +1,7 @@ /** * @fileoverview A component that displays an interactive survey within a session recording. It handles survey display, user responses, and submission */ -import { LemonButton, LemonCheckbox } from '@posthog/lemon-ui' +import { LemonButton, LemonCheckbox, LemonTextArea } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { InternalMultipleChoiceSurveyLogic } from 'lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic' @@ -13,8 +13,8 @@ interface InternalSurveyProps { export function InternalMultipleChoiceSurvey({ surveyId }: InternalSurveyProps): JSX.Element { const logic = InternalMultipleChoiceSurveyLogic({ surveyId }) - const { survey, surveyResponse, showThankYouMessage, thankYouMessage } = useValues(logic) - const { handleChoiceChange, handleSurveyResponse } = useActions(logic) + const { survey, surveyResponse, showThankYouMessage, thankYouMessage, openChoice } = useValues(logic) + const { handleChoiceChange, handleSurveyResponse, setOpenChoice } = useActions(logic) if (!survey) { return <> @@ -28,19 +28,31 @@ export function InternalMultipleChoiceSurvey({ surveyId }: InternalSurveyProps): {showThankYouMessage && thankYouMessage} {!showThankYouMessage && ( <> - {question.question} + {question.question} {question.type === SurveyQuestionType.MultipleChoice && ( -
    +
      {question.choices.map((choice) => (
    • handleChoiceChange(choice, checked)} label={choice} + className="font-normal" />
    • ))}
    )} + {question.type === SurveyQuestionType.MultipleChoice && question.hasOpenChoice && ( +
    + Other: + setOpenChoice(value)} + value={openChoice ?? ''} + className="my-2" + /> +
    + )} ({ surveyId }), getSurveys: () => ({}), - setSurvey: (survey: PostHogSurvey) => ({ survey }), - handleSurveys: (surveys: PostHogSurvey[]) => ({ surveys }), + setSurvey: (survey: Survey) => ({ survey }), + handleSurveys: (surveys: Survey[]) => ({ surveys }), handleSurveyResponse: () => ({}), handleChoiceChange: (choice: string, isAdded: boolean) => ({ choice, isAdded }), setShowThankYouMessage: (showThankYouMessage: boolean) => ({ showThankYouMessage }), setThankYouMessage: (thankYouMessage: string) => ({ thankYouMessage }), + setOpenChoice: (openChoice: string) => ({ openChoice }), + setQuestions: (questions: SurveyQuestion[]) => ({ questions }), }), reducers({ surveyId: [ @@ -32,11 +36,17 @@ export const InternalMultipleChoiceSurveyLogic = kea survey, }, ], + questions: [ + [] as SurveyQuestion[], + { + setQuestions: (_, { questions }) => questions, + }, + ], thankYouMessage: [ 'Thank you for your feedback!', { @@ -49,6 +59,12 @@ export const InternalMultipleChoiceSurveyLogic = kea showThankYouMessage, }, ], + openChoice: [ + null as string | null, + { + setOpenChoice: (_, { openChoice }) => openChoice, + }, + ], surveyResponse: [ [] as string[], { @@ -60,16 +76,18 @@ export const InternalMultipleChoiceSurveyLogic = kea ({ /** When surveyId is set, get the list of surveys for the user */ setSurveyId: () => { - posthog.getSurveys(actions.handleSurveys) + posthog.getSurveys((surveys) => actions.handleSurveys(surveys as unknown as Survey[])) }, /** Callback for the surveys response. Filter it to the surveyId and set the survey */ handleSurveys: ({ surveys }) => { - const survey = surveys.find((s: PostHogSurvey) => s.id === values.surveyId) + const survey = surveys.find((s: Survey) => s.id === values.surveyId) if (survey) { posthog.capture('survey shown', { $survey_id: values.surveyId, }) actions.setSurvey(survey) + actions.setQuestions(survey.questions) + if (survey.appearance?.thankYouMessageHeader) { actions.setThankYouMessage(survey.appearance?.thankYouMessageHeader) } @@ -77,12 +95,17 @@ export const InternalMultipleChoiceSurveyLogic = kea { - posthog.capture('survey sent', { + const payload = { $survey_id: values.surveyId, $survey_response: values.surveyResponse, - }) + } + if (values.openChoice) { + payload.$survey_response.push(values.openChoice) + } + posthog.capture('survey sent', payload) + actions.setShowThankYouMessage(true) - setTimeout(() => actions.setSurvey(null), 5000) + setTimeout(() => actions.setSurvey(null as unknown as Survey), 5000) }, })), afterMount(({ actions, props }) => { From f95445a1a4a2b10d7f1299303907a78cf1193756 Mon Sep 17 00:00:00 2001 From: Alex V Date: Tue, 28 Jan 2025 16:52:48 +0100 Subject: [PATCH 4/7] Fixes in logic --- .../InternalMultipleChoiceSurvey.tsx | 4 +-- .../InternalMultipleChoiceSurveyLogic.ts | 32 ++++++------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx index 61da02dce76ff..0bc85cad42332 100644 --- a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx @@ -3,7 +3,7 @@ */ import { LemonButton, LemonCheckbox, LemonTextArea } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { InternalMultipleChoiceSurveyLogic } from 'lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic' +import { internalMultipleChoiceSurveyLogic } from 'lib/components/InternalSurvey/internalMultipleChoiceSurveyLogic' import { SurveyQuestion, SurveyQuestionType } from '~/types' @@ -12,7 +12,7 @@ interface InternalSurveyProps { } export function InternalMultipleChoiceSurvey({ surveyId }: InternalSurveyProps): JSX.Element { - const logic = InternalMultipleChoiceSurveyLogic({ surveyId }) + const logic = internalMultipleChoiceSurveyLogic({ surveyId }) const { survey, surveyResponse, showThankYouMessage, thankYouMessage, openChoice } = useValues(logic) const { handleChoiceChange, handleSurveyResponse, setOpenChoice } = useActions(logic) diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts index 76bb35c6ccd37..72e1605bb8676 100644 --- a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts @@ -1,23 +1,19 @@ -/** - * @fileoverview A logic that handles the internal multiple choice survey - */ import { actions, afterMount, kea, key, listeners, path, props, reducers } from 'kea' import posthog from 'posthog-js' import { Survey, SurveyQuestion } from '~/types' -import type { InternalMultipleChoiceSurveyLogicType } from './InternalMultipleChoiceSurveyLogicType' +import type { internalMultipleChoiceSurveyLogicType } from './internalMultipleChoiceSurveyLogicType' export interface InternalSurveyLogicProps { surveyId: string } -export const InternalMultipleChoiceSurveyLogic = kea([ - path(['lib', 'components', 'InternalSurvey', 'InternalMultipleChoiceSurveyLogic']), +export const internalMultipleChoiceSurveyLogic = kea([ + path(['lib', 'components', 'InternalSurvey', 'internalMultipleChoiceSurveyLogicType']), props({} as InternalSurveyLogicProps), key((props) => props.surveyId), actions({ - setSurveyId: (surveyId: string) => ({ surveyId }), getSurveys: () => ({}), setSurvey: (survey: Survey) => ({ survey }), handleSurveys: (surveys: Survey[]) => ({ surveys }), @@ -29,12 +25,6 @@ export const InternalMultipleChoiceSurveyLogic = kea ({ questions }), }), reducers({ - surveyId: [ - null as string | null, - { - setSurveyId: (_, { surveyId }) => surveyId, - }, - ], survey: [ null as Survey | null, { @@ -73,17 +63,15 @@ export const InternalMultipleChoiceSurveyLogic = kea ({ + listeners(({ actions, values, props }) => ({ /** When surveyId is set, get the list of surveys for the user */ - setSurveyId: () => { - posthog.getSurveys((surveys) => actions.handleSurveys(surveys as unknown as Survey[])) - }, + setSurveyId: () => {}, /** Callback for the surveys response. Filter it to the surveyId and set the survey */ handleSurveys: ({ surveys }) => { - const survey = surveys.find((s: Survey) => s.id === values.surveyId) + const survey = surveys.find((s: Survey) => s.id === props.surveyId) if (survey) { posthog.capture('survey shown', { - $survey_id: values.surveyId, + $survey_id: props.surveyId, }) actions.setSurvey(survey) actions.setQuestions(survey.questions) @@ -96,7 +84,7 @@ export const InternalMultipleChoiceSurveyLogic = kea { const payload = { - $survey_id: values.surveyId, + $survey_id: props.surveyId, $survey_response: values.surveyResponse, } if (values.openChoice) { @@ -108,8 +96,8 @@ export const InternalMultipleChoiceSurveyLogic = kea actions.setSurvey(null as unknown as Survey), 5000) }, })), - afterMount(({ actions, props }) => { + afterMount(({ actions }) => { /** When the logic is mounted, set the surveyId from the props */ - actions.setSurveyId(props.surveyId) + posthog.getSurveys((surveys) => actions.handleSurveys(surveys as unknown as Survey[])) }), ]) From 09acc0969071deacb90e9ed8d1d0c65e54901b97 Mon Sep 17 00:00:00 2001 From: Alex V Date: Tue, 28 Jan 2025 17:45:20 +0100 Subject: [PATCH 5/7] Optimisation and fix open question --- .../InternalMultipleChoiceSurvey.tsx | 49 +++++++++++-------- .../InternalMultipleChoiceSurveyLogic.ts | 12 +---- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx index 0bc85cad42332..9c60c7c5b6a2d 100644 --- a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx @@ -31,32 +31,39 @@ export function InternalMultipleChoiceSurvey({ surveyId }: InternalSurveyProps): {question.question} {question.type === SurveyQuestionType.MultipleChoice && (
      - {question.choices.map((choice) => ( -
    • - handleChoiceChange(choice, checked)} - label={choice} - className="font-normal" - /> -
    • - ))} + {question.choices.map((choice, index) => { + // Add an open choice text area if the last choice is an open choice + if (index === question.choices.length - 1 && question.hasOpenChoice) { + return ( +
      + {choice} + +
      + ) + } + return ( +
    • + handleChoiceChange(choice, checked)} + label={choice} + className="font-normal" + /> +
    • + ) + })}
    )} - {question.type === SurveyQuestionType.MultipleChoice && question.hasOpenChoice && ( -
    - Other: - setOpenChoice(value)} - value={openChoice ?? ''} - className="my-2" - /> -
    - )} diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts index 72e1605bb8676..9c756539ebb78 100644 --- a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts @@ -1,7 +1,7 @@ import { actions, afterMount, kea, key, listeners, path, props, reducers } from 'kea' import posthog from 'posthog-js' -import { Survey, SurveyQuestion } from '~/types' +import { Survey } from '~/types' import type { internalMultipleChoiceSurveyLogicType } from './internalMultipleChoiceSurveyLogicType' @@ -22,7 +22,6 @@ export const internalMultipleChoiceSurveyLogic = kea ({ showThankYouMessage }), setThankYouMessage: (thankYouMessage: string) => ({ thankYouMessage }), setOpenChoice: (openChoice: string) => ({ openChoice }), - setQuestions: (questions: SurveyQuestion[]) => ({ questions }), }), reducers({ survey: [ @@ -31,12 +30,6 @@ export const internalMultipleChoiceSurveyLogic = kea survey, }, ], - questions: [ - [] as SurveyQuestion[], - { - setQuestions: (_, { questions }) => questions, - }, - ], thankYouMessage: [ 'Thank you for your feedback!', { @@ -59,7 +52,7 @@ export const internalMultipleChoiceSurveyLogic = kea - isAdded ? [...state, choice] : state.filter((c) => c !== choice), + isAdded ? [...state, choice] : state.filter((c: string) => c !== choice), }, ], }), @@ -74,7 +67,6 @@ export const internalMultipleChoiceSurveyLogic = kea Date: Tue, 28 Jan 2025 17:53:35 +0100 Subject: [PATCH 6/7] Rename --- ...eChoiceSurveyLogic.ts => internalMultipleChoiceSurveyLogic.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/src/lib/components/InternalSurvey/{InternalMultipleChoiceSurveyLogic.ts => internalMultipleChoiceSurveyLogic.ts} (100%) diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts b/frontend/src/lib/components/InternalSurvey/internalMultipleChoiceSurveyLogic.ts similarity index 100% rename from frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurveyLogic.ts rename to frontend/src/lib/components/InternalSurvey/internalMultipleChoiceSurveyLogic.ts From 45b10157b22b5b1f1fcb72b6cf100efc843d3ca1 Mon Sep 17 00:00:00 2001 From: Alex V Date: Tue, 28 Jan 2025 17:55:23 +0100 Subject: [PATCH 7/7] Fix path --- .../components/InternalSurvey/InternalMultipleChoiceSurvey.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx index 9c60c7c5b6a2d..dee3b2f30cda8 100644 --- a/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx +++ b/frontend/src/lib/components/InternalSurvey/InternalMultipleChoiceSurvey.tsx @@ -3,10 +3,11 @@ */ import { LemonButton, LemonCheckbox, LemonTextArea } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' -import { internalMultipleChoiceSurveyLogic } from 'lib/components/InternalSurvey/internalMultipleChoiceSurveyLogic' import { SurveyQuestion, SurveyQuestionType } from '~/types' +import { internalMultipleChoiceSurveyLogic } from './internalMultipleChoiceSurveyLogic' + interface InternalSurveyProps { surveyId: string }