diff --git a/frontend/src/lib/constants.tsx b/frontend/src/lib/constants.tsx index b750e3558b2fc..663e0beaf2bf8 100644 --- a/frontend/src/lib/constants.tsx +++ b/frontend/src/lib/constants.tsx @@ -169,6 +169,7 @@ export const FEATURE_FLAGS = { WEBHOOKS_DENYLIST: 'webhooks-denylist', // owner: #team-pipeline SURVEYS_SITE_APP_DEPRECATION: 'surveys-site-app-deprecation', // owner: @neilkakkar SURVEYS_MULTIPLE_QUESTIONS: 'surveys-multiple-questions', // owner: @liyiy + SURVEYS_RESULTS_VISUALIZATIONS: 'surveys-results-visualizations', // owner: @jurajmajerik CONSOLE_RECORDING_SEARCH: 'console-recording-search', // owner: #team-monitoring } as const export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS] diff --git a/frontend/src/scenes/surveys/SurveyView.tsx b/frontend/src/scenes/surveys/SurveyView.tsx index a88f5381ebcb5..b437e61fe4654 100644 --- a/frontend/src/scenes/surveys/SurveyView.tsx +++ b/frontend/src/scenes/surveys/SurveyView.tsx @@ -32,11 +32,10 @@ import { dayjs } from 'lib/dayjs' import { defaultSurveyAppearance, SURVEY_EVENT_NAME } from './constants' import { FEATURE_FLAGS } from 'lib/constants' import { featureFlagLogic } from 'lib/logic/featureFlagLogic' +import { Summary } from './surveyViewViz' export function SurveyView({ id }: { id: string }): JSX.Element { const { survey, surveyLoading, surveyPlugin, showSurveyAppWarning } = useValues(surveyLogic) - // TODO: survey results logic - // const { surveyImpressionsCount, surveyStartedCount, surveyCompletedCount } = useValues(surveyResultsLogic) const { editingSurvey, updateSurvey, launchSurvey, stopSurvey, archiveSurvey, resumeSurvey } = useActions(surveyLogic) const { deleteSurvey } = useActions(surveysLogic) @@ -289,12 +288,17 @@ export function SurveyResult({ disableEventsTable }: { disableEventsTable?: bool surveyRatingQuery, surveyMultipleChoiceQuery, currentQuestionIndexAndType, + surveyUserStats, + surveyUserStatsLoading, } = useValues(surveyLogic) const { setCurrentQuestionIndexAndType } = useActions(surveyLogic) const { featureFlags } = useValues(featureFlagLogic) return ( <> + {featureFlags[FEATURE_FLAGS.SURVEYS_RESULTS_VISUALIZATIONS] && ( + + )} {surveyMetricsQueries && (
diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index 8f019b65c92b2..f8b3f464db674 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -17,7 +17,8 @@ import { SurveyType, } from '~/types' import type { surveyLogicType } from './surveyLogicType' -import { DataTableNode, InsightVizNode, NodeKind } from '~/queries/schema' +import { DataTableNode, InsightVizNode, HogQLQuery, NodeKind } from '~/queries/schema' +import { hogql } from '~/queries/utils' import { surveysLogic } from './surveysLogic' import { dayjs } from 'lib/dayjs' import { pluginsLogic } from 'scenes/plugins/pluginsLogic' @@ -42,6 +43,12 @@ export interface SurveyMetricsQueries { surveysDismissed: DataTableNode } +export interface SurveyUserStats { + seen: number + dismissed: number + sent: number +} + export const surveyLogic = kea([ props({} as SurveyLogicProps), key(({ id }) => id), @@ -89,7 +96,7 @@ export const surveyLogic = kea([ resumeSurvey: true, setCurrentQuestionIndexAndType: (idx: number, type: SurveyQuestionType) => ({ idx, type }), }), - loaders(({ props, actions }) => ({ + loaders(({ props, actions, values }) => ({ survey: { loadSurvey: async () => { if (props.id && props.id !== 'new') { @@ -116,6 +123,49 @@ export const surveyLogic = kea([ return await api.surveys.update(props.id, { end_date: null }) }, }, + surveyUserStats: { + loadSurveyUserStats: async (): Promise => { + const { survey } = values + const startDate = dayjs((survey as Survey).created_at).format('YYYY-MM-DD') + const endDate = survey.end_date + ? dayjs(survey.end_date).add(1, 'day').format('YYYY-MM-DD') + : dayjs().add(1, 'day').format('YYYY-MM-DD') + + const query: HogQLQuery = { + kind: NodeKind.HogQLQuery, + query: hogql` + SELECT + (SELECT COUNT(DISTINCT person_id) + FROM events + WHERE event = 'survey shown' + AND properties.$survey_id = ${props.id} + AND timestamp >= ${startDate} + AND timestamp <= ${endDate}), + (SELECT COUNT(DISTINCT person_id) + FROM events + WHERE event = 'survey dismissed' + AND properties.$survey_id = ${props.id} + AND timestamp >= ${startDate} + AND timestamp <= ${endDate}), + (SELECT COUNT(DISTINCT person_id) + FROM events + WHERE event = 'survey sent' + AND properties.$survey_id = ${props.id} + AND timestamp >= ${startDate} + AND timestamp <= ${endDate}) + `, + } + const responseJSON = await api.query(query) + const { results } = responseJSON + if (results && results[0]) { + const [totalSeen, dismissed, sent] = results[0] + const onlySeen = totalSeen - dismissed - sent + return { seen: onlySeen < 0 ? 0 : onlySeen, dismissed, sent } + } else { + return { seen: 0, dismissed: 0, sent: 0 } + } + }, + }, })), listeners(({ actions }) => ({ createSurveySuccess: ({ survey }) => { @@ -148,6 +198,7 @@ export const surveyLogic = kea([ }, loadSurveySuccess: ({ survey }) => { actions.setCurrentQuestionIndexAndType(0, survey.questions[0].type) + actions.loadSurveyUserStats() }, })), reducers({ @@ -434,7 +485,7 @@ export const surveyLogic = kea([ await actions.loadSurvey() } if (props.id === 'new') { - actions.resetSurvey() + await actions.resetSurvey() } }), ]) diff --git a/frontend/src/scenes/surveys/surveyViewViz.tsx b/frontend/src/scenes/surveys/surveyViewViz.tsx new file mode 100644 index 0000000000000..4969dff248da4 --- /dev/null +++ b/frontend/src/scenes/surveys/surveyViewViz.tsx @@ -0,0 +1,138 @@ +import { LemonTable } from '@posthog/lemon-ui' +import { SurveyUserStats } from './surveyLogic' +import { Tooltip } from 'lib/lemon-ui/Tooltip' + +const formatCount = (count: number, total: number): string => { + if ((count / total) * 100 < 3) { + return '' + } + return `${count}` +} + +export function UsersCount({ surveyUserStats }: { surveyUserStats: SurveyUserStats }): JSX.Element { + const { seen, dismissed, sent } = surveyUserStats + const total = seen + dismissed + sent + const labelTotal = total === 1 ? 'Unique user viewed' : 'Unique users viewed' + const labelSent = sent === 1 ? 'Response submitted' : 'Responses submitted' + + return ( +
+
+
{total}
+
{labelTotal}
+
+ {sent > 0 && ( +
+
{sent}
+
{labelSent}
+
+ )} +
+ ) +} + +export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUserStats }): JSX.Element { + const { seen, dismissed, sent } = surveyUserStats + + const total = seen + dismissed + sent + const seenPercentage = (seen / total) * 100 + const dismissedPercentage = (dismissed / total) * 100 + const sentPercentage = (sent / total) * 100 + + return ( + <> + {total > 0 && ( +
+
+ {[ + { + count: seen, + label: 'Viewed', + classes: `bg-primary rounded-l ${dismissed === 0 && sent === 0 ? 'rounded-r' : ''}`, + style: { width: `${seenPercentage}%` }, + }, + { + count: dismissed, + label: 'Dismissed', + classes: `${seen === 0 ? 'rounded-l' : ''} ${sent === 0 ? 'rounded-r' : ''}`, + style: { + backgroundColor: '#E3A506', + width: `${dismissedPercentage}%`, + left: `${seenPercentage}%`, + }, + }, + { + count: sent, + label: 'Submitted', + classes: `rounded-r ${seen === 0 && dismissed === 0 ? 'rounded-l' : ''}`, + style: { + backgroundColor: '#529B08', + width: `${sentPercentage}%`, + left: `${seenPercentage + dismissedPercentage}%`, + }, + }, + ].map(({ count, label, classes, style }) => ( + +
+ + {formatCount(count, total)} + +
+
+ ))} +
+
+
+ {[ + { count: seen, label: 'Viewed', color: 'bg-primary' }, + { count: dismissed, label: 'Dismissed', color: 'bg-warning' }, + { count: sent, label: 'Submitted', color: 'bg-success' }, + ].map(({ count, label, color }) => ( +
+
+ {`${label} (${( + (count / total) * + 100 + ).toFixed(1)}%)`} +
+ ))} +
+
+
+ )} + + ) +} + +export function Summary({ + surveyUserStats, + surveyUserStatsLoading, +}: { + surveyUserStats: SurveyUserStats + surveyUserStatsLoading: boolean +}): JSX.Element { + if (!surveyUserStats) { + return <> + } + + return ( +
+ {surveyUserStatsLoading ? ( + + ) : ( + <> + + + + )} +
+ ) +}