Skip to content

Commit 989ba6f

Browse files
committed
feat(surveys): ff cross-sell with mini survey builder
1 parent 0fee956 commit 989ba6f

File tree

6 files changed

+530
-230
lines changed

6 files changed

+530
-230
lines changed

frontend/src/lib/constants.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,7 @@ export const FEATURE_FLAGS = {
343343
EXPERIMENTS_NEW_CALCULATOR: 'experiments-new-calculator', // owner: @jurajmajerik #team-experiments
344344
AGENT_MODES: 'phai-agent-modes', // owner: @skoob13 #team-posthog-ai
345345
SURVEYS_FUNNELS_CROSS_SELL: 'survey-funnels-cross-sell', // owner: @adboio #team-surveys
346+
SURVEYS_FF_CROSS_SELL: 'surveys-ff-cross-sell', // owner: @adboio #team-surveys
346347
} as const
347348
export type FeatureFlagLookupKey = keyof typeof FEATURE_FLAGS
348349
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]

frontend/src/scenes/feature-flags/FeatureFlag.tsx

Lines changed: 34 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,18 @@ import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagL
4848
import { ButtonPrimitive } from 'lib/ui/Button/ButtonPrimitives'
4949
import { Label } from 'lib/ui/Label/Label'
5050
import { capitalizeFirstLetter } from 'lib/utils'
51-
import { addProductIntent } from 'lib/utils/product-intents'
5251
import { FeatureFlagPermissions } from 'scenes/FeatureFlagPermissions'
5352
import { Dashboard } from 'scenes/dashboard/Dashboard'
5453
import { EmptyDashboardComponent } from 'scenes/dashboard/EmptyDashboardComponent'
5554
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
5655
import { UTM_TAGS } from 'scenes/feature-flags/FeatureFlagSnippets'
5756
import { JSONEditorInput } from 'scenes/feature-flags/JSONEditorInput'
58-
import { useMaxTool } from 'scenes/max/useMaxTool'
5957
import { SceneExport } from 'scenes/sceneTypes'
60-
import { SURVEY_CREATED_SOURCE } from 'scenes/surveys/constants'
61-
import { captureMaxAISurveyCreationException } from 'scenes/surveys/utils'
58+
import { QuickSurveyModal } from 'scenes/surveys/QuickSurveyModal'
6259
import { teamLogic } from 'scenes/teamLogic'
6360
import { urls } from 'scenes/urls'
6461
import { userLogic } from 'scenes/userLogic'
6562

66-
import { iconForType } from '~/layout/panel-layout/ProjectTree/defaultTree'
6763
import {
6864
ScenePanel,
6965
ScenePanelActionsSection,
@@ -101,6 +97,7 @@ import { FeatureFlagAutoRollback } from './FeatureFlagAutoRollout'
10197
import { FeatureFlagCodeExample } from './FeatureFlagCodeExample'
10298
import { FeatureFlagConditionWarning } from './FeatureFlagConditionWarning'
10399
import { FeatureFlagEvaluationTags } from './FeatureFlagEvaluationTags'
100+
import { FeedbackTab } from './FeatureFlagFeedbackTab'
104101
import FeatureFlagProjects from './FeatureFlagProjects'
105102
import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions'
106103
import FeatureFlagSchedule from './FeatureFlagSchedule'
@@ -112,85 +109,6 @@ import { FeatureFlagsTab, featureFlagsLogic } from './featureFlagsLogic'
112109

113110
const RESOURCE_TYPE = 'feature_flag'
114111

115-
// Utility function to create MaxTool configuration for feature flag survey creation
116-
export function createMaxToolSurveyConfig(
117-
featureFlag: FeatureFlagType,
118-
user: any,
119-
multivariateEnabled: boolean,
120-
variants: any[]
121-
): {
122-
identifier: 'create_survey'
123-
active: boolean
124-
initialMaxPrompt: string
125-
suggestions: string[]
126-
context: Record<string, any>
127-
contextDescription: {
128-
text: string
129-
icon: JSX.Element
130-
}
131-
callback: (toolOutput: { survey_id?: string; survey_name?: string; error?: string }) => void
132-
} {
133-
return {
134-
identifier: 'create_survey' as const,
135-
active: Boolean(user?.uuid),
136-
initialMaxPrompt: `Create a survey to collect feedback about the "${featureFlag.key}" feature flag${featureFlag.name ? ` (${featureFlag.name})` : ''}${multivariateEnabled && variants?.length > 0 ? ` which has variants: ${variants.map((v) => `"${v.key}"`).join(', ')}` : ''}`,
137-
suggestions:
138-
multivariateEnabled && variants?.length > 0
139-
? [
140-
`Create a feedback survey comparing variants of the "${featureFlag.key}" feature flag`,
141-
`Create a survey for users who saw the "${variants[0]?.key}" variant of the "${featureFlag.key}" feature flag`,
142-
`Create an A/B test survey asking users to compare the "${featureFlag.key}" feature flag variants`,
143-
`Create a survey to understand which variant of the "${featureFlag.key}" feature flag performs better`,
144-
`Create a survey targeting all variants of the "${featureFlag.key}" feature flag to gather overall feedback`,
145-
]
146-
: [
147-
`Create a feedback survey for users who see the "${featureFlag.key}" feature flag`,
148-
`Create an NPS survey for users exposed to the "${featureFlag.key}" feature flag`,
149-
`Create a satisfaction survey asking about the "${featureFlag.key}" feature flag experience`,
150-
`Create a survey to understand user reactions to the "${featureFlag.key}" feature flag`,
151-
],
152-
context: {
153-
feature_flag_key: featureFlag.key,
154-
feature_flag_id: featureFlag.id,
155-
feature_flag_name: featureFlag.name,
156-
target_feature_flag: featureFlag.key,
157-
survey_purpose: 'collect_feedback_for_feature_flag',
158-
is_multivariate: multivariateEnabled,
159-
variants:
160-
multivariateEnabled && variants?.length > 0
161-
? variants.map((v) => ({
162-
key: v.key,
163-
name: v.name || '',
164-
rollout_percentage: v.rollout_percentage,
165-
}))
166-
: [],
167-
variant_count: variants?.length || 0,
168-
},
169-
contextDescription: {
170-
text: featureFlag.name,
171-
icon: iconForType('feature_flag'),
172-
},
173-
callback: (toolOutput: { survey_id?: string; survey_name?: string; error?: string }) => {
174-
addProductIntent({
175-
product_type: ProductKey.SURVEYS,
176-
intent_context: ProductIntentContext.SURVEY_CREATED,
177-
metadata: {
178-
survey_id: toolOutput.survey_id,
179-
source: SURVEY_CREATED_SOURCE.FEATURE_FLAGS,
180-
created_successfully: !toolOutput?.error,
181-
},
182-
})
183-
184-
if (toolOutput?.error || !toolOutput?.survey_id) {
185-
return captureMaxAISurveyCreationException(toolOutput.error, SURVEY_CREATED_SOURCE.FEATURE_FLAGS)
186-
}
187-
188-
// Redirect to the new survey
189-
router.actions.push(urls.survey(toolOutput.survey_id))
190-
},
191-
}
192-
}
193-
194112
export const scene: SceneExport<FeatureFlagLogicProps> = {
195113
component: FeatureFlag,
196114
logic: featureFlagLogic,
@@ -209,8 +127,6 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
209127
isEditingFlag,
210128
activeTab,
211129
accessDeniedToFeatureFlag,
212-
multivariateEnabled,
213-
variants,
214130
experiment,
215131
} = useValues(featureFlagLogic)
216132
const { featureFlags } = useValues(enabledFeaturesLogic)
@@ -229,14 +145,13 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
229145
const { earlyAccessFeaturesList } = useValues(featureFlagLogic)
230146

231147
const { tags } = useValues(tagsModel)
232-
const { hasAvailableFeature, user } = useValues(userLogic)
148+
const { hasAvailableFeature } = useValues(userLogic)
233149
const { currentTeamId } = useValues(teamLogic)
234150

235151
// whether the key for an existing flag is being changed
236152
const [hasKeyChanged, setHasKeyChanged] = useState(false)
237153

238-
// Initialize MaxTool hook for survey creation
239-
const { openMax } = useMaxTool(createMaxToolSurveyConfig(featureFlag, user, multivariateEnabled, variants))
154+
const [isQuickSurveyModalOpen, setIsQuickSurveyModalOpen] = useState(false)
240155

241156
const [advancedSettingsExpanded, setAdvancedSettingsExpanded] = useState(false)
242157

@@ -340,7 +255,7 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
340255
label: (
341256
<div className="flex flex-row">
342257
<div>Analysis</div>
343-
<LemonTag className="ml-1 float-right uppercase" type="warning">
258+
<LemonTag className="ml-2 float-right uppercase" type="warning">
344259
{' '}
345260
Beta
346261
</LemonTag>
@@ -371,6 +286,22 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
371286
})
372287
}
373288

289+
if (featureFlags[FEATURE_FLAGS.SURVEYS_FF_CROSS_SELL]) {
290+
tabs.push({
291+
label: (
292+
<div className="flex flex-row">
293+
<div>User feedback</div>
294+
<LemonTag className="ml-2 float-right uppercase" type="primary">
295+
{' '}
296+
New
297+
</LemonTag>
298+
</div>
299+
),
300+
key: FeatureFlagsTab.FEEDBACK,
301+
content: <FeedbackTab featureFlag={featureFlag} />,
302+
})
303+
}
304+
374305
return (
375306
<>
376307
<div className="feature-flag">
@@ -820,16 +751,14 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
820751
Create cohort
821752
</ButtonPrimitive>
822753
)}
823-
{openMax && (
824-
<ButtonPrimitive
825-
menuItem
826-
data-attr={`${RESOURCE_TYPE}-create-survey`}
827-
onClick={() => openMax()}
828-
>
829-
<IconPlusSmall />
830-
Create survey
831-
</ButtonPrimitive>
832-
)}
754+
<ButtonPrimitive
755+
menuItem
756+
data-attr={`${RESOURCE_TYPE}-create-survey`}
757+
onClick={() => setIsQuickSurveyModalOpen(true)}
758+
>
759+
<IconPlusSmall />
760+
Create survey
761+
</ButtonPrimitive>
833762
</ScenePanelActionsSection>
834763
<ScenePanelDivider />
835764
<ScenePanelActionsSection>
@@ -926,6 +855,11 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
926855
</>
927856
)}
928857
</div>
858+
<QuickSurveyModal
859+
flag={featureFlag}
860+
isOpen={isQuickSurveyModalOpen}
861+
onClose={() => setIsQuickSurveyModalOpen(false)}
862+
/>
929863
</>
930864
)
931865
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { BindLogic, useValues } from 'kea'
2+
3+
import { IconArrowRight } from '@posthog/icons'
4+
import { LemonBanner, LemonTable, LemonTableColumn, Link, Spinner } from '@posthog/lemon-ui'
5+
6+
import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink'
7+
import { createdAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
8+
import stringWithWBR from 'lib/utils/stringWithWBR'
9+
import { QuickSurveyForm } from 'scenes/surveys/QuickSurveyModal'
10+
import { SurveyResult } from 'scenes/surveys/SurveyView'
11+
import { SurveyStatusTag } from 'scenes/surveys/components/SurveyStatusTag'
12+
import { surveyLogic } from 'scenes/surveys/surveyLogic'
13+
import { SurveysTabs, surveysLogic } from 'scenes/surveys/surveysLogic'
14+
import { urls } from 'scenes/urls'
15+
16+
import { FeatureFlagType, Survey } from '~/types'
17+
18+
export function FeedbackTab({ featureFlag }: { featureFlag: FeatureFlagType }): JSX.Element {
19+
const {
20+
data: { surveys },
21+
dataLoading,
22+
surveysResponsesCountLoading,
23+
surveysResponsesCount,
24+
} = useValues(surveysLogic)
25+
const surveysForFlag = surveys.filter((survey) => survey.linked_flag_id === featureFlag.id)
26+
27+
if (surveysForFlag.length === 0) {
28+
return (
29+
<div className="flex flex-col items-center pt-5">
30+
<div className="w-full max-w-5xl">
31+
<LemonBanner type="info" className="mb-6">
32+
Gather valuable insights by automatically displaying a survey to users in this feature flag
33+
</LemonBanner>
34+
<div className="border rounded p-6 bg-bg-light">
35+
<QuickSurveyForm flag={featureFlag} />
36+
</div>
37+
</div>
38+
{/* <pre>{JSON.stringify(featureFlag, null, 2)}</pre> */}
39+
{/* <pre>{JSON.stringify(surveys, null, 2)}</pre> */}
40+
</div>
41+
)
42+
}
43+
44+
if (surveysForFlag.length === 1) {
45+
return (
46+
<BindLogic logic={surveyLogic} props={{ id: surveysForFlag[0].id }}>
47+
<div className="">
48+
<LemonBanner type="info" className="mb-6">
49+
Showing results for survey "{surveysForFlag[0].name}".{' '}
50+
<Link to={urls.survey(surveysForFlag[0].id)}>
51+
Manage in surveys <IconArrowRight />
52+
</Link>
53+
</LemonBanner>
54+
<SurveyResult />
55+
</div>
56+
</BindLogic>
57+
)
58+
}
59+
60+
return (
61+
<div className="space-y-6">
62+
<LemonBanner type="info" className="">
63+
Showing only surveys associated with this feature flag.{' '}
64+
<Link to={urls.surveys(SurveysTabs.Active)}>
65+
See all surveys <IconArrowRight />
66+
</Link>
67+
</LemonBanner>
68+
69+
<LemonTable
70+
dataSource={surveysForFlag}
71+
defaultSorting={{
72+
columnKey: 'created_at',
73+
order: -1,
74+
}}
75+
rowKey="name"
76+
nouns={['survey', 'surveys']}
77+
data-attr="surveys-table"
78+
loading={dataLoading}
79+
columns={[
80+
{
81+
dataIndex: 'name',
82+
title: 'Name',
83+
render: function RenderName(_, survey) {
84+
return <LemonTableLink to={urls.survey(survey.id)} title={stringWithWBR(survey.name, 17)} />
85+
},
86+
},
87+
{
88+
title: 'Responses',
89+
dataIndex: 'id',
90+
render: function RenderResponses(_, survey) {
91+
return (
92+
<>
93+
{surveysResponsesCountLoading ? (
94+
<Spinner />
95+
) : (
96+
<div>{surveysResponsesCount[survey.id] ?? 0}</div>
97+
)}
98+
</>
99+
)
100+
},
101+
sorter: (surveyA, surveyB) => {
102+
const countA = surveysResponsesCount[surveyA.id] ?? 0
103+
const countB = surveysResponsesCount[surveyB.id] ?? 0
104+
return countA - countB
105+
},
106+
},
107+
createdAtColumn<Survey>() as LemonTableColumn<Survey, keyof Survey | undefined>,
108+
{
109+
title: 'Status',
110+
width: 100,
111+
render: function Render(_: any, survey: Survey) {
112+
return <SurveyStatusTag survey={survey} />
113+
},
114+
},
115+
]}
116+
/>
117+
</div>
118+
)
119+
}

0 commit comments

Comments
 (0)