diff --git a/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx new file mode 100644 index 0000000000000..2213d9dd605a4 --- /dev/null +++ b/frontend/src/scenes/notebooks/Nodes/NotebookNodeSurvey.tsx @@ -0,0 +1,142 @@ +import { createPostHogWidgetNode } from 'scenes/notebooks/Nodes/NodeWrapper' +import { FeatureFlagBasicType, NotebookNodeType, Survey, SurveyQuestionType } from '~/types' +import { BindLogic, useActions, useValues } from 'kea' +import { IconFlag, IconSurveys } from 'lib/lemon-ui/icons' +import { LemonButton, LemonDivider } from '@posthog/lemon-ui' +import { urls } from 'scenes/urls' +import { LemonSkeleton } from 'lib/lemon-ui/LemonSkeleton' +import { notebookNodeLogic } from './notebookNodeLogic' +import { NotebookNodeViewProps } from '../Notebook/utils' +import { buildFlagContent } from './NotebookNodeFlag' +import { defaultSurveyAppearance, surveyLogic } from 'scenes/surveys/surveyLogic' +import { StatusTag } from 'scenes/surveys/Surveys' +import { SurveyResult } from 'scenes/surveys/SurveyView' +import { SurveyAppearance } from 'scenes/surveys/SurveyAppearance' +import { SurveyReleaseSummary } from 'scenes/surveys/Survey' +import api from 'lib/api' + +const Component = (props: NotebookNodeViewProps): JSX.Element => { + const { id } = props.node.attrs + const { survey, surveyLoading, hasTargetingFlag } = useValues(surveyLogic({ id })) + const { expanded, nextNode } = useValues(notebookNodeLogic) + const { insertAfter } = useActions(notebookNodeLogic) + + return ( +
+ +
+ + {surveyLoading ? ( + + ) : ( + <> + {survey.name} + {/* survey has to exist in notebooks */} + + + )} +
+ + {expanded ? ( + <> + {survey.description && ( + <> + + {survey.description} + + )} + {!survey.start_date ? ( + <> + +
+ + +
+ {}} + /> +
+
+ + ) : ( + <> + {/* show results when the survey is running */} + +
+ +
+ + )} + + ) : null} + + +
+ {survey.linked_flag && ( + } + onClick={(e) => { + e.stopPropagation() + + if (nextNode?.type.name !== NotebookNodeType.FeatureFlag) { + insertAfter(buildFlagContent((survey.linked_flag as FeatureFlagBasicType).id)) + } + }} + disabledReason={ + nextNode?.type.name === NotebookNodeType.FeatureFlag && + 'Feature flag already exists below' + } + > + View Linked Flag + + )} +
+
+
+ ) +} + +type NotebookNodeSurveyAttributes = { + id: string +} + +export const NotebookNodeSurvey = createPostHogWidgetNode({ + nodeType: NotebookNodeType.Survey, + title: async (attributes) => { + const mountedLogic = surveyLogic.findMounted({ id: attributes.id }) + let title = mountedLogic?.values.survey.name || null + if (title === null) { + const retrievedSurvey: Survey = await api.surveys.get(attributes.id) + if (retrievedSurvey) { + title = retrievedSurvey.name + } + } + return title ? `Survey: ${title}` : 'Survey' + }, + Component, + heightEstimate: '3rem', + href: (attrs) => urls.survey(attrs.id), + resizeable: false, + attributes: { + id: {}, + }, + pasteOptions: { + find: urls.survey('') + '(.+)', + getAttributes: async (match) => { + return { id: match[1] } + }, + }, +}) diff --git a/frontend/src/scenes/notebooks/Notebook/Editor.tsx b/frontend/src/scenes/notebooks/Notebook/Editor.tsx index 3a270947728fb..1304350e3c07d 100644 --- a/frontend/src/scenes/notebooks/Notebook/Editor.tsx +++ b/frontend/src/scenes/notebooks/Notebook/Editor.tsx @@ -29,6 +29,7 @@ import { JSONContent, NotebookEditor, EditorFocusPosition, EditorRange, Node } f import { SlashCommandsExtension } from './SlashCommands' import { BacklinkCommandsExtension } from './BacklinkCommands' import { NotebookNodeEarlyAccessFeature } from '../Nodes/NotebookNodeEarlyAccessFeature' +import { NotebookNodeSurvey } from '../Nodes/NotebookNodeSurvey' const CustomDocument = ExtensionDocument.extend({ content: 'heading block*', @@ -92,6 +93,7 @@ export function Editor({ NotebookNodeFlag, NotebookNodeExperiment, NotebookNodeEarlyAccessFeature, + NotebookNodeSurvey, NotebookNodeImage, SlashCommandsExtension, BacklinkCommandsExtension, diff --git a/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx b/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx index 00ffb408ebe30..ac8f58010de68 100644 --- a/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx +++ b/frontend/src/scenes/notebooks/NotebooksTable/ContainsTypeFilter.tsx @@ -7,6 +7,7 @@ export const fromNodeTypeToLabel: Omit, Noteboo [NotebookNodeType.FeatureFlagCodeExample]: 'Feature flag Code Examples', [NotebookNodeType.Experiment]: 'Experiments', [NotebookNodeType.EarlyAccessFeature]: 'Early Access Features', + [NotebookNodeType.Survey]: 'Surveys', [NotebookNodeType.Image]: 'Images', [NotebookNodeType.Insight]: 'Insights', [NotebookNodeType.Person]: 'Persons', diff --git a/frontend/src/scenes/surveys/Survey.tsx b/frontend/src/scenes/surveys/Survey.tsx index 6f72397bd4c54..d59ed4b674e69 100644 --- a/frontend/src/scenes/surveys/Survey.tsx +++ b/frontend/src/scenes/surveys/Survey.tsx @@ -60,7 +60,7 @@ export function SurveyComponent({ id }: { id?: string } = {}): JSX.Element { export function SurveyForm({ id }: { id: string }): JSX.Element { const { survey, surveyLoading, isEditingSurvey, hasTargetingFlag } = useValues(surveyLogic) - const { loadSurvey, editingSurvey, setHasTargetingFlag } = useActions(surveyLogic) + const { loadSurvey, editingSurvey, setSurveyValue } = useActions(surveyLogic) const { featureFlags } = useValues(enabledFeaturesLogic) return ( @@ -374,7 +374,9 @@ export function SurveyForm({ id }: { id: string }): JSX.Element { setHasTargetingFlag(true)} + onClick={() => { + setSurveyValue('targeting_flag_filters', { groups: [] }) + }} > Add user targeting @@ -389,7 +391,10 @@ export function SurveyForm({ id }: { id: string }): JSX.Element { type="secondary" status="danger" className="w-max" - onClick={() => setHasTargetingFlag(false)} + onClick={() => { + setSurveyValue('targeting_flag_filters', undefined) + setSurveyValue('targeting_flag', null) + }} > Remove all user properties diff --git a/frontend/src/scenes/surveys/SurveyView.tsx b/frontend/src/scenes/surveys/SurveyView.tsx index dfe7de4895a4b..4e1e594da5c2b 100644 --- a/frontend/src/scenes/surveys/SurveyView.tsx +++ b/frontend/src/scenes/surveys/SurveyView.tsx @@ -19,18 +19,9 @@ import { SurveyQuestionType, SurveyType } from '~/types' import { SurveyAPIEditor } from './SurveyAPIEditor' import { LemonBanner } from 'lib/lemon-ui/LemonBanner' import { IconOpenInNew } from 'lib/lemon-ui/icons' -import { NodeKind } from '~/queries/schema' export function SurveyView({ id }: { id: string }): JSX.Element { - const { - survey, - dataTableQuery, - surveyLoading, - surveyPlugin, - surveyMetricsQueries, - surveyDataVizQuery, - showSurveyAppWarning, - } = useValues(surveyLogic) + const { survey, surveyLoading, surveyPlugin, showSurveyAppWarning } = useValues(surveyLogic) // TODO: survey results logic // const { surveyImpressionsCount, surveyStartedCount, surveyCompletedCount } = useValues(surveyResultsLogic) const { editingSurvey, updateSurvey, launchSurvey, stopSurvey, archiveSurvey, resumeSurvey } = @@ -134,48 +125,7 @@ export function SurveyView({ id }: { id: string }): JSX.Element { ? { content: (
- {surveyMetricsQueries && ( -
-
- -
-
- -
-
- )} - {survey.questions[0].type === SurveyQuestionType.Rating && ( -
- -
- )} - {(survey.questions[0].type === SurveyQuestionType.SingleChoice || - survey.questions[0].type === SurveyQuestionType.MultipleChoice) && ( -
- {survey.questions[0].type === SurveyQuestionType.SingleChoice ? ( - - ) : ( - - )} -
- )} - {surveyLoading ? : } +
), key: 'results', @@ -305,6 +255,44 @@ export function SurveyView({ id }: { id: string }): JSX.Element { ) } +export function SurveyResult({ disableEventsTable }: { disableEventsTable?: boolean }): JSX.Element { + const { + survey, + dataTableQuery, + surveyLoading, + surveyMetricsQueries, + surveyRatingQuery, + surveyMultipleChoiceQuery, + } = useValues(surveyLogic) + + return ( + <> + {surveyMetricsQueries && ( +
+
+ +
+
+ +
+
+ )} + {survey.questions[0].type === SurveyQuestionType.Rating && ( +
+ +
+ )} + {(survey.questions[0].type === SurveyQuestionType.SingleChoice || + survey.questions[0].type === SurveyQuestionType.MultipleChoice) && ( +
+ +
+ )} + {!disableEventsTable && (surveyLoading ? : )} + + ) +} + const OPT_IN_SNIPPET = `posthog.init('YOUR_PROJECT_API_KEY', { api_host: 'YOUR API HOST', opt_in_site_apps: true // <--- Add this line diff --git a/frontend/src/scenes/surveys/Surveys.tsx b/frontend/src/scenes/surveys/Surveys.tsx index 3d5fc423f40c7..3f18b2df4e154 100644 --- a/frontend/src/scenes/surveys/Surveys.tsx +++ b/frontend/src/scenes/surveys/Surveys.tsx @@ -112,17 +112,7 @@ export function Surveys(): JSX.Element { title: 'Status', width: 100, render: function Render(_, survey: Survey) { - const statusColors = { - running: 'success', - draft: 'default', - complete: 'completion', - } as Record - const status = getSurveyStatus(survey) - return ( - - {status.toUpperCase()} - - ) + return }, }, { @@ -243,3 +233,17 @@ export function Surveys(): JSX.Element { ) } + +export function StatusTag({ survey }: { survey: Survey }): JSX.Element { + const statusColors = { + running: 'success', + draft: 'default', + complete: 'completion', + } as Record + const status = getSurveyStatus(survey) + return ( + + {status.toUpperCase()} + + ) +} diff --git a/frontend/src/scenes/surveys/surveyLogic.tsx b/frontend/src/scenes/surveys/surveyLogic.tsx index 4e935af2022b6..fc0de1bbb63c9 100644 --- a/frontend/src/scenes/surveys/surveyLogic.tsx +++ b/frontend/src/scenes/surveys/surveyLogic.tsx @@ -27,7 +27,6 @@ import { featureFlagLogic } from 'scenes/feature-flags/featureFlagLogic' export interface NewSurvey extends Pick< Survey, - | 'id' | 'name' | 'description' | 'type' @@ -40,6 +39,7 @@ export interface NewSurvey | 'archived' | 'appearance' > { + id: 'new' linked_flag_id: number | undefined targeting_flag_filters: Pick | undefined } @@ -76,73 +76,6 @@ export const surveyEventName = 'survey sent' const SURVEY_RESPONSE_PROPERTY = '$survey_response' -export const getSurveyDataQuery = (survey: Survey): DataTableNode => { - const surveyDataQuery: DataTableNode = { - kind: NodeKind.DataTableNode, - source: { - kind: NodeKind.EventsQuery, - select: ['*', `properties.${SURVEY_RESPONSE_PROPERTY}`, 'timestamp', 'person'], - orderBy: ['timestamp DESC'], - where: [`event == 'survey sent' or event == '${survey.name} survey sent'`], - after: survey.created_at, - properties: [ - { - type: PropertyFilterType.Event, - key: '$survey_id', - operator: PropertyOperator.Exact, - value: survey.id, - }, - ], - }, - propertiesViaUrl: true, - showExport: true, - showReload: true, - showEventFilter: true, - showPropertyFilter: true, - } - return surveyDataQuery -} - -export const getSurveyMetricsQueries = (surveyId: string): SurveyMetricsQueries => { - const surveysShownHogqlQuery = `select count(distinct person.id) as 'survey shown' from events where event == 'survey shown' and properties.$survey_id == '${surveyId}'` - const surveysDismissedHogqlQuery = `select count(distinct person.id) as 'survey dismissed' from events where event == 'survey dismissed' and properties.$survey_id == '${surveyId}'` - return { - surveysShown: { - kind: NodeKind.DataTableNode, - source: { kind: NodeKind.HogQLQuery, query: surveysShownHogqlQuery }, - }, - surveysDismissed: { - kind: NodeKind.DataTableNode, - source: { kind: NodeKind.HogQLQuery, query: surveysDismissedHogqlQuery }, - }, - } -} - -export const getSurveyDataVizQuery = (survey: Survey): InsightVizNode => { - return { - kind: NodeKind.InsightVizNode, - source: { - kind: NodeKind.TrendsQuery, - dateRange: { - date_from: dayjs(survey.created_at).format('YYYY-MM-DD'), - date_to: dayjs().format('YYYY-MM-DD'), - }, - properties: [ - { - type: PropertyFilterType.Event, - key: '$survey_id', - operator: PropertyOperator.Exact, - value: survey.id, - }, - ], - series: [{ event: surveyEventName, kind: NodeKind.EventsNode }], - trendsFilter: { display: ChartDisplayType.ActionsBarValue }, - breakdown: { breakdown: '$survey_response', breakdown_type: 'event' }, - }, - showTable: true, - } -} - export interface SurveyLogicProps { id: string | 'new' } @@ -153,9 +86,9 @@ export interface SurveyMetricsQueries { } export const surveyLogic = kea([ - path(['scenes', 'surveys', 'surveyLogic']), props({} as SurveyLogicProps), key(({ id }) => id), + path((key) => ['scenes', 'surveys', 'surveyLogic', key]), connect(() => ({ actions: [ surveysLogic, @@ -179,10 +112,6 @@ export const surveyLogic = kea([ stopSurvey: true, archiveSurvey: true, resumeSurvey: true, - setDataTableQuery: (query: DataTableNode) => ({ query }), - setSurveyMetricsQueries: (surveyMetricsQueries: SurveyMetricsQueries) => ({ surveyMetricsQueries }), - setSurveyDataVizQuery: (surveyDataVizQuery: InsightVizNode) => ({ surveyDataVizQuery }), - setHasTargetingFlag: (hasTargetingFlag: boolean) => ({ hasTargetingFlag }), }), loaders(({ props, actions }) => ({ survey: { @@ -213,16 +142,6 @@ export const surveyLogic = kea([ }, })), listeners(({ actions }) => ({ - loadSurveySuccess: ({ survey }) => { - if (survey.start_date && survey.id !== 'new') { - actions.setDataTableQuery(getSurveyDataQuery(survey as Survey)) - actions.setSurveyMetricsQueries(getSurveyMetricsQueries(survey.id)) - actions.setSurveyDataVizQuery(getSurveyDataVizQuery(survey as Survey)) - } - if (survey.targeting_flag) { - actions.setHasTargetingFlag(true) - } - }, createSurveySuccess: ({ survey }) => { lemonToast.success(<>Survey {survey.name} created) actions.loadSurveys() @@ -237,8 +156,6 @@ export const surveyLogic = kea([ }, launchSurveySuccess: ({ survey }) => { lemonToast.success(<>Survey {survey.name} launched) - actions.setSurveyMetricsQueries(getSurveyMetricsQueries(survey.id)) - actions.setDataTableQuery(getSurveyDataQuery(survey)) actions.loadSurveys() actions.reportSurveyLaunched(survey) }, @@ -261,30 +178,6 @@ export const surveyLogic = kea([ editingSurvey: (_, { editing }) => editing, }, ], - dataTableQuery: [ - null as DataTableNode | null, - { - setDataTableQuery: (_, { query }) => query, - }, - ], - surveyMetricsQueries: [ - null as SurveyMetricsQueries | null, - { - setSurveyMetricsQueries: (_, { surveyMetricsQueries }) => surveyMetricsQueries, - }, - ], - surveyDataVizQuery: [ - null as InsightVizNode | null, - { - setSurveyDataVizQuery: (_, { surveyDataVizQuery }) => surveyDataVizQuery, - }, - ], - hasTargetingFlag: [ - false, - { - setHasTargetingFlag: (_, { hasTargetingFlag }) => hasTargetingFlag, - }, - ], }), selectors({ isSurveyRunning: [ @@ -320,6 +213,117 @@ export const surveyLogic = kea([ ) }, ], + dataTableQuery: [ + (s) => [s.survey], + (survey): DataTableNode | null => { + if (survey.id === 'new') { + return null + } + const createdAt = (survey as Survey).created_at + + return { + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.EventsQuery, + select: ['*', `properties.${SURVEY_RESPONSE_PROPERTY}`, 'timestamp', 'person'], + orderBy: ['timestamp DESC'], + where: [`event == 'survey sent' or event == '${survey.name} survey sent'`], + after: createdAt, + properties: [ + { + type: PropertyFilterType.Event, + key: '$survey_id', + operator: PropertyOperator.Exact, + value: survey.id, + }, + ], + }, + propertiesViaUrl: true, + showExport: true, + showReload: true, + showEventFilter: true, + showPropertyFilter: true, + showTimings: false, + } + }, + ], + surveyMetricsQueries: [ + (s) => [s.survey], + (survey): SurveyMetricsQueries | null => { + const surveyId = survey.id + if (surveyId === 'new') { + return null + } + + const surveysShownHogqlQuery = `select count(distinct person.id) as 'survey shown' from events where event == 'survey shown' and properties.$survey_id == '${surveyId}'` + const surveysDismissedHogqlQuery = `select count(distinct person.id) as 'survey dismissed' from events where event == 'survey dismissed' and properties.$survey_id == '${surveyId}'` + return { + surveysShown: { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.HogQLQuery, query: surveysShownHogqlQuery }, + }, + surveysDismissed: { + kind: NodeKind.DataTableNode, + source: { kind: NodeKind.HogQLQuery, query: surveysDismissedHogqlQuery }, + }, + } + }, + ], + surveyRatingQuery: [ + (s) => [s.survey], + (survey): InsightVizNode | null => { + if (survey.id === 'new') { + return null + } + const createdAt = (survey as Survey).created_at + + return { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + dateRange: { + date_from: dayjs(createdAt).format('YYYY-MM-DD'), + date_to: dayjs().format('YYYY-MM-DD'), + }, + properties: [ + { + type: PropertyFilterType.Event, + key: '$survey_id', + operator: PropertyOperator.Exact, + value: survey.id, + }, + ], + series: [{ event: surveyEventName, kind: NodeKind.EventsNode }], + trendsFilter: { display: ChartDisplayType.ActionsBarValue }, + breakdown: { breakdown: '$survey_response', breakdown_type: 'event' }, + }, + showTable: true, + } + }, + ], + surveyMultipleChoiceQuery: [ + (s) => [s.survey], + (survey): DataTableNode | null => { + const singleChoiceQuery = `select count(), properties.$survey_response as choice from events where event == 'survey sent' and properties.$survey_id == '${survey.id}' group by choice order by count() desc` + const multipleChoiceQuery = `select count(), arrayJoin(JSONExtractArrayRaw(properties, '$survey_response')) as choice from events where event == 'survey sent' and properties.$survey_id == '${survey.id}' group by choice order by count() desc` + return { + kind: NodeKind.DataTableNode, + source: { + kind: NodeKind.HogQLQuery, + query: + survey.questions[0].type === SurveyQuestionType.SingleChoice + ? singleChoiceQuery + : multipleChoiceQuery, + }, + } + }, + ], + hasTargetingFlag: [ + (s) => [s.survey], + (survey): boolean => { + return !!survey.targeting_flag || !!(survey.id === 'new' && survey.targeting_flag_filters) + }, + ], }), forms(({ actions, props, values }) => ({ survey: { diff --git a/frontend/src/types.ts b/frontend/src/types.ts index cf091c4c88296..1529ff3146f9d 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -3035,6 +3035,7 @@ export enum NotebookNodeType { FeatureFlagCodeExample = 'ph-feature-flag-code-example', Experiment = 'ph-experiment', EarlyAccessFeature = 'ph-early-access-feature', + Survey = 'ph-survey', Person = 'ph-person', Backlink = 'ph-backlink', ReplayTimestamp = 'ph-replay-timestamp',