Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(surveys): survey results summary #17815

Merged
merged 10 commits into from
Oct 9, 2023
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/scenes/surveys/SurveyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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] && (
<Summary surveyUserStatsLoading={surveyUserStatsLoading} surveyUserStats={surveyUserStats} />
)}
{surveyMetricsQueries && (
<div className="flex flex-row gap-4 mb-4">
<div className="flex-1">
Expand Down
57 changes: 54 additions & 3 deletions frontend/src/scenes/surveys/surveyLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -42,6 +43,12 @@ export interface SurveyMetricsQueries {
surveysDismissed: DataTableNode
}

export interface SurveyUserStats {
seen: number
dismissed: number
sent: number
}

export const surveyLogic = kea<surveyLogicType>([
props({} as SurveyLogicProps),
key(({ id }) => id),
Expand Down Expand Up @@ -89,7 +96,7 @@ export const surveyLogic = kea<surveyLogicType>([
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') {
Expand All @@ -116,6 +123,49 @@ export const surveyLogic = kea<surveyLogicType>([
return await api.surveys.update(props.id, { end_date: null })
},
},
surveyUserStats: {
loadSurveyUserStats: async (): Promise<SurveyUserStats> => {
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 }) => {
Expand Down Expand Up @@ -148,6 +198,7 @@ export const surveyLogic = kea<surveyLogicType>([
},
loadSurveySuccess: ({ survey }) => {
actions.setCurrentQuestionIndexAndType(0, survey.questions[0].type)
actions.loadSurveyUserStats()
},
})),
reducers({
Expand Down Expand Up @@ -434,7 +485,7 @@ export const surveyLogic = kea<surveyLogicType>([
await actions.loadSurvey()
}
if (props.id === 'new') {
actions.resetSurvey()
await actions.resetSurvey()
}
}),
])
138 changes: 138 additions & 0 deletions frontend/src/scenes/surveys/surveyViewViz.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="inline-flex mb-4">
<div>
<div className="text-4xl font-bold">{total}</div>
<div className="font-semibold text-muted-alt">{labelTotal}</div>
</div>
{sent > 0 && (
<div className="ml-10">
<div className="text-4xl font-bold">{sent}</div>
<div className="font-semibold text-muted-alt">{labelSent}</div>
</div>
)}
</div>
)
}

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 && (
<div className="mb-6">
<div className="w-full mx-auto h-10 mb-4">
{[
{
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 }) => (
<Tooltip
key={`survey-summary-chart-${label}`}
title={`${label} surveys: ${count}`}
delayMs={0}
placement="top"
>
<div
className={`h-10 text-white text-center absolute cursor-pointer ${classes}`}
style={style}
>
<span className="inline-flex font-semibold max-w-full px-1 truncate leading-10">
{formatCount(count, total)}
</span>
</div>
</Tooltip>
))}
</div>
<div className="w-full flex justify-center">
<div className="flex items-center">
{[
{ 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 }) => (
<div key={`survey-summary-legend-${label}`} className="flex items-center mr-6">
<div className={`w-3 h-3 rounded-full mr-2 ${color}`} />
<span className="font-semibold text-muted-alt">{`${label} (${(
(count / total) *
100
).toFixed(1)}%)`}</span>
</div>
))}
</div>
</div>
</div>
)}
</>
)
}

export function Summary({
surveyUserStats,
surveyUserStatsLoading,
}: {
surveyUserStats: SurveyUserStats
surveyUserStatsLoading: boolean
}): JSX.Element {
if (!surveyUserStats) {
return <></>
}

return (
<div className="mb-4">
{surveyUserStatsLoading ? (
<LemonTable dataSource={[]} columns={[]} loading={true} />
) : (
<>
<UsersCount surveyUserStats={surveyUserStats} />
<UsersStackedBar surveyUserStats={surveyUserStats} />
</>
)}
</div>
)
}
Loading