Skip to content

Commit 6f6b43d

Browse files
committed
feat(surveys): ff cross-sell with mini survey builder
1 parent 6dacd51 commit 6f6b43d

File tree

6 files changed

+571
-230
lines changed

6 files changed

+571
-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
AGENT_MODES: 'phai-agent-modes', // owner: @skoob13 #team-posthog-ai
344344
APP_SHORTCUTS: 'app-shortcuts', // owner: @adamleithp #team-platform-ux
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: 35 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -48,22 +48,19 @@ 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 { ProductIntentContext, addProductIntent } from 'lib/utils/product-intents'
51+
import { ProductIntentContext } from 'lib/utils/product-intents'
5252
import { FeatureFlagPermissions } from 'scenes/FeatureFlagPermissions'
5353
import { Dashboard } from 'scenes/dashboard/Dashboard'
5454
import { EmptyDashboardComponent } from 'scenes/dashboard/EmptyDashboardComponent'
5555
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
5656
import { UTM_TAGS } from 'scenes/feature-flags/FeatureFlagSnippets'
5757
import { JSONEditorInput } from 'scenes/feature-flags/JSONEditorInput'
58-
import { useMaxTool } from 'scenes/max/useMaxTool'
5958
import { SceneExport } from 'scenes/sceneTypes'
60-
import { SURVEY_CREATED_SOURCE } from 'scenes/surveys/constants'
61-
import { captureMaxAISurveyCreationException } from 'scenes/surveys/utils'
59+
import { QuickSurveyModal } from 'scenes/surveys/QuickSurveyModal'
6260
import { teamLogic } from 'scenes/teamLogic'
6361
import { urls } from 'scenes/urls'
6462
import { userLogic } from 'scenes/userLogic'
6563

66-
import { iconForType } from '~/layout/panel-layout/ProjectTree/defaultTree'
6764
import {
6865
ScenePanel,
6966
ScenePanelActionsSection,
@@ -101,6 +98,7 @@ import { FeatureFlagAutoRollback } from './FeatureFlagAutoRollout'
10198
import { FeatureFlagCodeExample } from './FeatureFlagCodeExample'
10299
import { FeatureFlagConditionWarning } from './FeatureFlagConditionWarning'
103100
import { FeatureFlagEvaluationTags } from './FeatureFlagEvaluationTags'
101+
import { FeedbackTab } from './FeatureFlagFeedbackTab'
104102
import FeatureFlagProjects from './FeatureFlagProjects'
105103
import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions'
106104
import FeatureFlagSchedule from './FeatureFlagSchedule'
@@ -112,85 +110,6 @@ import { FeatureFlagsTab, featureFlagsLogic } from './featureFlagsLogic'
112110

113111
const RESOURCE_TYPE = 'feature_flag'
114112

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-
194113
export const scene: SceneExport<FeatureFlagLogicProps> = {
195114
component: FeatureFlag,
196115
logic: featureFlagLogic,
@@ -209,8 +128,6 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
209128
isEditingFlag,
210129
activeTab,
211130
accessDeniedToFeatureFlag,
212-
multivariateEnabled,
213-
variants,
214131
experiment,
215132
} = useValues(featureFlagLogic)
216133
const { featureFlags } = useValues(enabledFeaturesLogic)
@@ -229,14 +146,13 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
229146
const { earlyAccessFeaturesList } = useValues(featureFlagLogic)
230147

231148
const { tags } = useValues(tagsModel)
232-
const { hasAvailableFeature, user } = useValues(userLogic)
149+
const { hasAvailableFeature } = useValues(userLogic)
233150
const { currentTeamId } = useValues(teamLogic)
234151

235152
// whether the key for an existing flag is being changed
236153
const [hasKeyChanged, setHasKeyChanged] = useState(false)
237154

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

241157
const [advancedSettingsExpanded, setAdvancedSettingsExpanded] = useState(false)
242158

@@ -340,7 +256,7 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
340256
label: (
341257
<div className="flex flex-row">
342258
<div>Analysis</div>
343-
<LemonTag className="ml-1 float-right uppercase" type="warning">
259+
<LemonTag className="ml-2 float-right uppercase" type="warning">
344260
{' '}
345261
Beta
346262
</LemonTag>
@@ -371,6 +287,22 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
371287
})
372288
}
373289

290+
if (featureFlags[FEATURE_FLAGS.SURVEYS_FF_CROSS_SELL]) {
291+
tabs.push({
292+
label: (
293+
<div className="flex flex-row">
294+
<div>User feedback</div>
295+
<LemonTag className="ml-2 float-right uppercase" type="primary">
296+
{' '}
297+
New
298+
</LemonTag>
299+
</div>
300+
),
301+
key: FeatureFlagsTab.FEEDBACK,
302+
content: <FeedbackTab featureFlag={featureFlag} />,
303+
})
304+
}
305+
374306
return (
375307
<>
376308
<div className="feature-flag">
@@ -820,16 +752,14 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
820752
Create cohort
821753
</ButtonPrimitive>
822754
)}
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-
)}
755+
<ButtonPrimitive
756+
menuItem
757+
data-attr={`${RESOURCE_TYPE}-create-survey`}
758+
onClick={() => setIsQuickSurveyModalOpen(true)}
759+
>
760+
<IconPlusSmall />
761+
Create survey
762+
</ButtonPrimitive>
833763
</ScenePanelActionsSection>
834764
<ScenePanelDivider />
835765
<ScenePanelActionsSection>
@@ -926,6 +856,11 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
926856
</>
927857
)}
928858
</div>
859+
<QuickSurveyModal
860+
flag={featureFlag}
861+
isOpen={isQuickSurveyModalOpen}
862+
onClose={() => setIsQuickSurveyModalOpen(false)}
863+
/>
929864
</>
930865
)
931866
}
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)