Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ export const FEATURE_FLAGS = {
EXPERIMENTS_NEW_CALCULATOR: 'experiments-new-calculator', // owner: @jurajmajerik #team-experiments
AGENT_MODES: 'phai-agent-modes', // owner: @skoob13 #team-posthog-ai
SURVEYS_FUNNELS_CROSS_SELL: 'survey-funnels-cross-sell', // owner: @adboio #team-surveys
SURVEYS_FF_CROSS_SELL: 'surveys-ff-cross-sell', // owner: @adboio #team-surveys
} as const
export type FeatureFlagLookupKey = keyof typeof FEATURE_FLAGS
export type FeatureFlagKey = (typeof FEATURE_FLAGS)[keyof typeof FEATURE_FLAGS]
Expand Down
134 changes: 34 additions & 100 deletions frontend/src/scenes/feature-flags/FeatureFlag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,18 @@ import { featureFlagLogic as enabledFeaturesLogic } from 'lib/logic/featureFlagL
import { ButtonPrimitive } from 'lib/ui/Button/ButtonPrimitives'
import { Label } from 'lib/ui/Label/Label'
import { capitalizeFirstLetter } from 'lib/utils'
import { addProductIntent } from 'lib/utils/product-intents'
import { FeatureFlagPermissions } from 'scenes/FeatureFlagPermissions'
import { Dashboard } from 'scenes/dashboard/Dashboard'
import { EmptyDashboardComponent } from 'scenes/dashboard/EmptyDashboardComponent'
import { dashboardLogic } from 'scenes/dashboard/dashboardLogic'
import { UTM_TAGS } from 'scenes/feature-flags/FeatureFlagSnippets'
import { JSONEditorInput } from 'scenes/feature-flags/JSONEditorInput'
import { useMaxTool } from 'scenes/max/useMaxTool'
import { SceneExport } from 'scenes/sceneTypes'
import { SURVEY_CREATED_SOURCE } from 'scenes/surveys/constants'
import { captureMaxAISurveyCreationException } from 'scenes/surveys/utils'
import { QuickSurveyModal } from 'scenes/surveys/QuickSurveyModal'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'

import { iconForType } from '~/layout/panel-layout/ProjectTree/defaultTree'
import {
ScenePanel,
ScenePanelActionsSection,
Expand Down Expand Up @@ -101,6 +97,7 @@ import { FeatureFlagAutoRollback } from './FeatureFlagAutoRollout'
import { FeatureFlagCodeExample } from './FeatureFlagCodeExample'
import { FeatureFlagConditionWarning } from './FeatureFlagConditionWarning'
import { FeatureFlagEvaluationTags } from './FeatureFlagEvaluationTags'
import { FeedbackTab } from './FeatureFlagFeedbackTab'
import FeatureFlagProjects from './FeatureFlagProjects'
import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions'
import FeatureFlagSchedule from './FeatureFlagSchedule'
Expand All @@ -112,85 +109,6 @@ import { FeatureFlagsTab, featureFlagsLogic } from './featureFlagsLogic'

const RESOURCE_TYPE = 'feature_flag'

// Utility function to create MaxTool configuration for feature flag survey creation
export function createMaxToolSurveyConfig(
featureFlag: FeatureFlagType,
user: any,
multivariateEnabled: boolean,
variants: any[]
): {
identifier: 'create_survey'
active: boolean
initialMaxPrompt: string
suggestions: string[]
context: Record<string, any>
contextDescription: {
text: string
icon: JSX.Element
}
callback: (toolOutput: { survey_id?: string; survey_name?: string; error?: string }) => void
} {
return {
identifier: 'create_survey' as const,
active: Boolean(user?.uuid),
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(', ')}` : ''}`,
suggestions:
multivariateEnabled && variants?.length > 0
? [
`Create a feedback survey comparing variants of the "${featureFlag.key}" feature flag`,
`Create a survey for users who saw the "${variants[0]?.key}" variant of the "${featureFlag.key}" feature flag`,
`Create an A/B test survey asking users to compare the "${featureFlag.key}" feature flag variants`,
`Create a survey to understand which variant of the "${featureFlag.key}" feature flag performs better`,
`Create a survey targeting all variants of the "${featureFlag.key}" feature flag to gather overall feedback`,
]
: [
`Create a feedback survey for users who see the "${featureFlag.key}" feature flag`,
`Create an NPS survey for users exposed to the "${featureFlag.key}" feature flag`,
`Create a satisfaction survey asking about the "${featureFlag.key}" feature flag experience`,
`Create a survey to understand user reactions to the "${featureFlag.key}" feature flag`,
],
context: {
feature_flag_key: featureFlag.key,
feature_flag_id: featureFlag.id,
feature_flag_name: featureFlag.name,
target_feature_flag: featureFlag.key,
survey_purpose: 'collect_feedback_for_feature_flag',
is_multivariate: multivariateEnabled,
variants:
multivariateEnabled && variants?.length > 0
? variants.map((v) => ({
key: v.key,
name: v.name || '',
rollout_percentage: v.rollout_percentage,
}))
: [],
variant_count: variants?.length || 0,
},
contextDescription: {
text: featureFlag.name,
icon: iconForType('feature_flag'),
},
callback: (toolOutput: { survey_id?: string; survey_name?: string; error?: string }) => {
addProductIntent({
product_type: ProductKey.SURVEYS,
intent_context: ProductIntentContext.SURVEY_CREATED,
metadata: {
survey_id: toolOutput.survey_id,
source: SURVEY_CREATED_SOURCE.FEATURE_FLAGS,
created_successfully: !toolOutput?.error,
},
})

if (toolOutput?.error || !toolOutput?.survey_id) {
return captureMaxAISurveyCreationException(toolOutput.error, SURVEY_CREATED_SOURCE.FEATURE_FLAGS)
}

// Redirect to the new survey
router.actions.push(urls.survey(toolOutput.survey_id))
},
}
}

export const scene: SceneExport<FeatureFlagLogicProps> = {
component: FeatureFlag,
logic: featureFlagLogic,
Expand All @@ -209,8 +127,6 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
isEditingFlag,
activeTab,
accessDeniedToFeatureFlag,
multivariateEnabled,
variants,
experiment,
} = useValues(featureFlagLogic)
const { featureFlags } = useValues(enabledFeaturesLogic)
Expand All @@ -229,14 +145,13 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
const { earlyAccessFeaturesList } = useValues(featureFlagLogic)

const { tags } = useValues(tagsModel)
const { hasAvailableFeature, user } = useValues(userLogic)
const { hasAvailableFeature } = useValues(userLogic)
const { currentTeamId } = useValues(teamLogic)

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

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

const [advancedSettingsExpanded, setAdvancedSettingsExpanded] = useState(false)

Expand Down Expand Up @@ -340,7 +255,7 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
label: (
<div className="flex flex-row">
<div>Analysis</div>
<LemonTag className="ml-1 float-right uppercase" type="warning">
<LemonTag className="ml-2 float-right uppercase" type="warning">
{' '}
Beta
</LemonTag>
Expand Down Expand Up @@ -371,6 +286,22 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
})
}

if (featureFlags[FEATURE_FLAGS.SURVEYS_FF_CROSS_SELL]) {
tabs.push({
label: (
<div className="flex flex-row">
<div>User feedback</div>
<LemonTag className="ml-2 float-right uppercase" type="primary">
{' '}
New
</LemonTag>
</div>
),
key: FeatureFlagsTab.FEEDBACK,
content: <FeedbackTab featureFlag={featureFlag} />,
})
}

return (
<>
<div className="feature-flag">
Expand Down Expand Up @@ -820,16 +751,14 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
Create cohort
</ButtonPrimitive>
)}
{openMax && (
<ButtonPrimitive
menuItem
data-attr={`${RESOURCE_TYPE}-create-survey`}
onClick={() => openMax()}
>
<IconPlusSmall />
Create survey
</ButtonPrimitive>
)}
<ButtonPrimitive
menuItem
data-attr={`${RESOURCE_TYPE}-create-survey`}
onClick={() => setIsQuickSurveyModalOpen(true)}
>
<IconPlusSmall />
Create survey
</ButtonPrimitive>
</ScenePanelActionsSection>
<ScenePanelDivider />
<ScenePanelActionsSection>
Expand Down Expand Up @@ -926,6 +855,11 @@ export function FeatureFlag({ id }: FeatureFlagLogicProps): JSX.Element {
</>
)}
</div>
<QuickSurveyModal
flag={featureFlag}
isOpen={isQuickSurveyModalOpen}
onClose={() => setIsQuickSurveyModalOpen(false)}
/>
</>
)
}
Expand Down
117 changes: 117 additions & 0 deletions frontend/src/scenes/feature-flags/FeatureFlagFeedbackTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { BindLogic, useValues } from 'kea'

import { IconArrowRight } from '@posthog/icons'
import { LemonBanner, LemonTable, LemonTableColumn, Link, Spinner } from '@posthog/lemon-ui'

import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink'
import { createdAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
import stringWithWBR from 'lib/utils/stringWithWBR'
import { QuickSurveyForm } from 'scenes/surveys/QuickSurveyModal'
import { SurveyResult } from 'scenes/surveys/SurveyView'
import { SurveyStatusTag } from 'scenes/surveys/components/SurveyStatusTag'
import { surveyLogic } from 'scenes/surveys/surveyLogic'
import { SurveysTabs, surveysLogic } from 'scenes/surveys/surveysLogic'
import { urls } from 'scenes/urls'

import { FeatureFlagType, Survey } from '~/types'

export function FeedbackTab({ featureFlag }: { featureFlag: FeatureFlagType }): JSX.Element {
const {
data: { surveys },
dataLoading,
surveysResponsesCountLoading,
surveysResponsesCount,
} = useValues(surveysLogic)
const surveysForFlag = surveys.filter((survey) => survey.linked_flag_id === featureFlag.id)

if (surveysForFlag.length === 0) {
return (
<div className="flex flex-col items-center pt-5">
<div className="w-full max-w-5xl">
<LemonBanner type="info" className="mb-6">
Gather valuable insights by automatically displaying a survey to users in this feature flag
</LemonBanner>
<div className="border rounded p-6 bg-bg-light">
<QuickSurveyForm flag={featureFlag} />
</div>
</div>
</div>
)
}

if (surveysForFlag.length === 1) {
return (
<BindLogic logic={surveyLogic} props={{ id: surveysForFlag[0].id }}>
<div className="">
<LemonBanner type="info" className="mb-6">
Showing results for survey "{surveysForFlag[0].name}".{' '}
<Link to={urls.survey(surveysForFlag[0].id)}>
Manage in surveys <IconArrowRight />
</Link>
</LemonBanner>
<SurveyResult />
</div>
</BindLogic>
)
}

return (
<div className="space-y-6">
<LemonBanner type="info" className="">
Showing only surveys associated with this feature flag.{' '}
<Link to={urls.surveys(SurveysTabs.Active)}>
See all surveys <IconArrowRight />
</Link>
</LemonBanner>

<LemonTable
dataSource={surveysForFlag}
defaultSorting={{
columnKey: 'created_at',
order: -1,
}}
rowKey="name"
nouns={['survey', 'surveys']}
data-attr="surveys-table"
loading={dataLoading}
columns={[
{
dataIndex: 'name',
title: 'Name',
render: function RenderName(_, survey) {
return <LemonTableLink to={urls.survey(survey.id)} title={stringWithWBR(survey.name, 17)} />
},
},
{
title: 'Responses',
dataIndex: 'id',
render: function RenderResponses(_, survey) {
return (
<>
{surveysResponsesCountLoading ? (
<Spinner />
) : (
<div>{surveysResponsesCount[survey.id] ?? 0}</div>
)}
</>
)
},
sorter: (surveyA, surveyB) => {
const countA = surveysResponsesCount[surveyA.id] ?? 0
const countB = surveysResponsesCount[surveyB.id] ?? 0
return countA - countB
},
},
createdAtColumn<Survey>() as LemonTableColumn<Survey, keyof Survey | undefined>,
{
title: 'Status',
width: 100,
render: function Render(_: any, survey: Survey) {
return <SurveyStatusTag survey={survey} />
},
},
]}
/>
</div>
)
}
Loading
Loading