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 && (
+
+ )}
+
+ )
+}
+
+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 ? (
+
+ ) : (
+ <>
+
+
+ >
+ )}
+
+ )
+}