diff --git a/frontend/__snapshots__/scenes-app-surveys--survey-view.png b/frontend/__snapshots__/scenes-app-surveys--survey-view.png index ed047d62e2ddd..569cb11dbac2b 100644 Binary files a/frontend/__snapshots__/scenes-app-surveys--survey-view.png and b/frontend/__snapshots__/scenes-app-surveys--survey-view.png differ diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index f53bca6b2e0ab..8442979b221c9 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -78,7 +78,7 @@ export interface SurveyMultipleChoiceResults { export interface SurveyOpenTextResults { [key: number]: { - events: { properties: Record<string, any>; distinct_id: string }[] + events: { distinct_id: string; properties: Record<string, any>; personProperties: Record<string, any> }[] } } @@ -86,6 +86,8 @@ export interface QuestionResultsReady { [key: string]: boolean } +const getResponseField = (i: number): string => (i === 0 ? '$survey_response' : `$survey_response_${i}`) + export const surveyLogic = kea<surveyLogicType>([ props({} as SurveyLogicProps), key(({ id }) => id), @@ -228,13 +230,12 @@ export const surveyLogic = kea<surveyLogicType>([ ? dayjs(survey.end_date).add(1, 'day').format('YYYY-MM-DD') : dayjs().add(1, 'day').format('YYYY-MM-DD') - const surveyResponseField = - questionIndex === 0 ? '$survey_response' : `$survey_response_${questionIndex}` - const query: HogQLQuery = { kind: NodeKind.HogQLQuery, query: ` - SELECT JSONExtractString(properties, '${surveyResponseField}') AS survey_response, COUNT(survey_response) + SELECT + JSONExtractString(properties, '${getResponseField(questionIndex)}') AS survey_response, + COUNT(survey_response) FROM events WHERE event = 'survey sent' AND properties.$survey_id = '${props.id}' @@ -251,7 +252,9 @@ export const surveyLogic = kea<surveyLogicType>([ const data = new Array(dataSize).fill(0) results?.forEach(([value, count]) => { total += count - data[value - 1] = count + + const index = question.scale === 10 ? value : value - 1 + data[index] = count }) return { ...values.surveyRatingResults, [questionIndex]: { total, data } } @@ -269,13 +272,12 @@ export const surveyLogic = kea<surveyLogicType>([ ? dayjs(survey.end_date).add(1, 'day').format('YYYY-MM-DD') : dayjs().add(1, 'day').format('YYYY-MM-DD') - const surveyResponseField = - questionIndex === 0 ? '$survey_response' : `$survey_response_${questionIndex}` - const query: HogQLQuery = { kind: NodeKind.HogQLQuery, query: ` - SELECT JSONExtractString(properties, '${surveyResponseField}') AS survey_response, COUNT(survey_response) + SELECT + JSONExtractString(properties, '${getResponseField(questionIndex)}') AS survey_response, + COUNT(survey_response) FROM events WHERE event = 'survey sent' AND properties.$survey_id = '${props.id}' @@ -312,13 +314,12 @@ export const surveyLogic = kea<surveyLogicType>([ ? dayjs(survey.end_date).add(1, 'day').format('YYYY-MM-DD') : dayjs().add(1, 'day').format('YYYY-MM-DD') - const surveyResponseField = - questionIndex === 0 ? '$survey_response' : `$survey_response_${questionIndex}` - const query: HogQLQuery = { kind: NodeKind.HogQLQuery, query: ` - SELECT count(), arrayJoin(JSONExtractArrayRaw(properties, '${surveyResponseField}')) AS choice + SELECT + count(), + arrayJoin(JSONExtractArrayRaw(properties, '${getResponseField(questionIndex)}')) AS choice FROM events WHERE event == 'survey sent' AND properties.$survey_id == '${survey.id}' @@ -346,7 +347,7 @@ export const surveyLogic = kea<surveyLogicType>([ const data = results?.map((r) => r[0]) const labels = results?.map((r) => r[1]) - return { ...values.surveyRatingResults, [questionIndex]: { labels, data } } + return { ...values.surveyMultipleChoiceResults, [questionIndex]: { labels, data } } }, }, surveyOpenTextResults: { @@ -370,7 +371,7 @@ export const surveyLogic = kea<surveyLogicType>([ const query: HogQLQuery = { kind: NodeKind.HogQLQuery, query: ` - SELECT properties, distinct_id + SELECT distinct_id, properties, person.properties FROM events WHERE event == 'survey sent' AND properties.$survey_id == '${survey.id}' @@ -383,9 +384,15 @@ export const surveyLogic = kea<surveyLogicType>([ const responseJSON = await api.query(query) const { results } = responseJSON - const events = results?.map((r) => ({ properties: JSON.parse(r[0]), distinct_id: r[1] })) + const events = + results?.map((r) => { + const distinct_id = r[0] + const properties = JSON.parse(r[1]) + const personProperties = JSON.parse(r[2]) + return { distinct_id, properties, personProperties } + }) || [] - return { ...values.surveyRatingResults, [questionIndex]: { events } } + return { ...values.surveyOpenTextResults, [questionIndex]: { events } } }, }, })), @@ -592,7 +599,7 @@ export const surveyLogic = kea<surveyLogicType>([ ], dataTableQuery: [ (s) => [s.survey, s.surveyResponseProperty], - (survey, surveyResponseProperty): DataTableNode | null => { + (survey): DataTableNode | null => { if (survey.id === 'new') { return null } @@ -601,7 +608,23 @@ export const surveyLogic = kea<surveyLogicType>([ kind: NodeKind.DataTableNode, source: { kind: NodeKind.EventsQuery, - select: ['*', `properties.${surveyResponseProperty}`, 'timestamp', 'person'], + select: [ + '*', + ...survey.questions.map((q, i) => { + if (q.type === SurveyQuestionType.MultipleChoice) { + // Join array items into a string + return `coalesce(arrayStringConcat(JSONExtractArrayRaw(properties, '${getResponseField( + i + )}'), ', ')) -- ${q.question}` + } + + return `coalesce(JSONExtractString(properties, '${getResponseField(i)}')) -- ${ + q.question + }` + }), + 'timestamp', + 'person', + ], orderBy: ['timestamp DESC'], where: [`event == 'survey sent'`], after: createdAt, diff --git a/frontend/src/scenes/surveys/surveyViewViz.tsx b/frontend/src/scenes/surveys/surveyViewViz.tsx index ad4a95e86ac89..8f5a3955f7bb6 100644 --- a/frontend/src/scenes/surveys/surveyViewViz.tsx +++ b/frontend/src/scenes/surveys/surveyViewViz.tsx @@ -115,15 +115,18 @@ export function UsersStackedBar({ surveyUserStats }: { surveyUserStats: SurveyUs { count: seen, label: 'Viewed', style: { backgroundColor: '#1D4AFF' } }, { count: dismissed, label: 'Dismissed', style: { backgroundColor: '#E3A506' } }, { count: sent, label: 'Submitted', style: { backgroundColor: '#529B08' } }, - ].map(({ count, label, style }) => ( - <div key={`survey-summary-legend-${label}`} className="flex items-center mr-6"> - <div className="w-3 h-3 rounded-full mr-2" style={style} /> - <span className="font-semibold text-muted-alt">{`${label} (${( - (count / total) * - 100 - ).toFixed(1)}%)`}</span> - </div> - ))} + ].map( + ({ count, label, style }) => + count > 0 && ( + <div key={`survey-summary-legend-${label}`} className="flex items-center mr-6"> + <div className="w-3 h-3 rounded-full mr-2" style={style} /> + <span className="font-semibold text-muted-alt">{`${label} (${( + (count / total) * + 100 + ).toFixed(1)}%)`}</span> + </div> + ) + )} </div> </div> </div> @@ -462,16 +465,28 @@ export function OpenTextViz({ {question.question} • <span className="">Latest responses</span> </div> <div className="mt-4 mb-8 masonry-container"> - {surveyOpenTextResults[questionIndex].events.map((event, i) => ( - <div key={`open-text-${questionIndex}-${i}`} className="masonry-item border rounded"> - <div className="masonry-item-text text-center italic font-semibold px-5 py-4"> - {event.properties[surveyResponseField]} - </div> - <div className="masonry-item-link items-center px-5 py-4 border-t rounded-b truncate w-full"> - <PersonDisplay person={event} withIcon={true} noEllipsis={false} isCentered /> + {surveyOpenTextResults[questionIndex].events.map((event, i) => { + const personProp = { + distinct_id: event.distinct_id, + properties: event.personProperties, + } + + return ( + <div key={`open-text-${questionIndex}-${i}`} className="masonry-item border rounded"> + <div className="masonry-item-text text-center italic font-semibold px-5 py-4"> + {event.properties[surveyResponseField]} + </div> + <div className="masonry-item-link items-center px-5 py-4 border-t rounded-b truncate w-full"> + <PersonDisplay + person={personProp} + withIcon={true} + noEllipsis={false} + isCentered + /> + </div> </div> - </div> - ))} + ) + })} </div> </> )}