Skip to content

Commit

Permalink
feat(flags): Add FeatureFlagStatusIndicator to detail view for flags (#…
Browse files Browse the repository at this point in the history
…26412)

Co-authored-by: Dylan Martin <[email protected]>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 11, 2024
1 parent 9e610db commit ee20bdc
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 32 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
ExternalDataSourceSyncSchema,
ExternalDataSourceType,
FeatureFlagAssociatedRoleType,
FeatureFlagStatusResponse,
FeatureFlagType,
Group,
GroupListParams,
Expand Down Expand Up @@ -663,6 +664,13 @@ class ApiRequest {
)
}

public featureFlagStatus(teamId: TeamType['id'], featureFlagId: FeatureFlagType['id']): ApiRequest {
return this.projectsDetail(teamId)
.addPathComponent('feature_flags')
.addPathComponent(String(featureFlagId))
.addPathComponent('status')
}

public featureFlagCreateScheduledChange(teamId: TeamType['id']): ApiRequest {
return this.projectsDetail(teamId).addPathComponent('scheduled_changes')
}
Expand Down Expand Up @@ -1042,6 +1050,12 @@ const api = {
): Promise<{ scheduled_change: ScheduledChangeType }> {
return await new ApiRequest().featureFlagDeleteScheduledChange(teamId, scheduledChangeId).delete()
},
async getStatus(
teamId: TeamType['id'],
featureFlagId: FeatureFlagType['id']
): Promise<FeatureFlagStatusResponse> {
return await new ApiRequest().featureFlagStatus(teamId, featureFlagId).get()
},
},

organizationFeatureFlags: {
Expand Down
67 changes: 36 additions & 31 deletions frontend/src/scenes/feature-flags/FeatureFlag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import FeatureFlagProjects from './FeatureFlagProjects'
import { FeatureFlagReleaseConditions } from './FeatureFlagReleaseConditions'
import FeatureFlagSchedule from './FeatureFlagSchedule'
import { featureFlagsLogic, FeatureFlagsTab } from './featureFlagsLogic'
import { FeatureFlagStatusIndicator } from './FeatureFlagStatusIndicator'
import { RecentFeatureFlagInsights } from './RecentFeatureFlagInsightsCard'

export const scene: SceneExport = {
Expand Down Expand Up @@ -734,6 +735,7 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
aggregationTargetName,
featureFlag,
recordingFilterForFlag,
flagStatus,
} = useValues(featureFlagLogic)
const {
distributeVariantsEqually,
Expand Down Expand Up @@ -788,38 +790,41 @@ function FeatureFlagRollout({ readOnly }: { readOnly?: boolean }): JSX.Element {
Deleted
</LemonTag>
) : (
<LemonSwitch
onChange={(newValue) => {
LemonDialog.open({
title: `${newValue === true ? 'Enable' : 'Disable'} this flag?`,
description: `This flag will be immediately ${
newValue === true ? 'rolled out to' : 'rolled back from'
} the users matching the release conditions.`,
primaryButton: {
children: 'Confirm',
type: 'primary',
onClick: () => {
const updatedFlag = { ...featureFlag, active: newValue }
setFeatureFlag(updatedFlag)
saveFeatureFlag(updatedFlag)
<div className="flex gap-2">
<LemonSwitch
onChange={(newValue) => {
LemonDialog.open({
title: `${newValue === true ? 'Enable' : 'Disable'} this flag?`,
description: `This flag will be immediately ${
newValue === true ? 'rolled out to' : 'rolled back from'
} the users matching the release conditions.`,
primaryButton: {
children: 'Confirm',
type: 'primary',
onClick: () => {
const updatedFlag = { ...featureFlag, active: newValue }
setFeatureFlag(updatedFlag)
saveFeatureFlag(updatedFlag)
},
size: 'small',
},
size: 'small',
},
secondaryButton: {
children: 'Cancel',
type: 'tertiary',
size: 'small',
},
})
}}
label="Enabled"
disabledReason={
!featureFlag.can_edit
? "You only have view access to this feature flag. To make changes, contact the flag's creator."
: null
}
checked={featureFlag.active}
/>
secondaryButton: {
children: 'Cancel',
type: 'tertiary',
size: 'small',
},
})
}}
label="Enabled"
disabledReason={
!featureFlag.can_edit
? "You only have view access to this feature flag. To make changes, contact the flag's creator."
: null
}
checked={featureFlag.active}
/>
<FeatureFlagStatusIndicator flagStatus={flagStatus} />
</div>
)}
</div>
<div className="col-span-6">
Expand Down
40 changes: 40 additions & 0 deletions frontend/src/scenes/feature-flags/FeatureFlagStatusIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { LemonTag } from 'lib/lemon-ui/LemonTag'
import { Tooltip } from 'lib/lemon-ui/Tooltip'

import { FeatureFlagStatus, FeatureFlagStatusResponse } from '~/types'

export function FeatureFlagStatusIndicator({
flagStatus,
}: {
flagStatus: FeatureFlagStatusResponse | null
}): JSX.Element | null {
if (
!flagStatus ||
[FeatureFlagStatus.ACTIVE, FeatureFlagStatus.DELETED, FeatureFlagStatus.UNKNOWN].includes(flagStatus.status)
) {
return null
}

return (
<Tooltip
title={
<>
<div className="text-sm">{flagStatus.reason}</div>
<div className="text-xs">
{flagStatus.status === FeatureFlagStatus.STALE &&
'Make sure to remove any references to this flag in your code before deleting it.'}
{flagStatus.status === FeatureFlagStatus.INACTIVE &&
'It is probably not being used in your code, but be sure to remove any references to this flag before deleting it.'}
</div>
</>
}
placement="right"
>
<span>
<LemonTag type="warning" className="uppercase cursor-default">
{flagStatus.status}
</LemonTag>
</span>
</Tooltip>
)
}
8 changes: 8 additions & 0 deletions frontend/src/scenes/feature-flags/FeatureFlags.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mswDecorator } from '~/mocks/browser'
import featureFlags from './__mocks__/feature_flags.json'

const meta: Meta = {
tags: ['ff'],
title: 'Scenes-App/Feature Flags',
parameters: {
layout: 'fullscreen',
Expand All @@ -33,6 +34,13 @@ const meta: Meta = {
200,
featureFlags.results.find((r) => r.id === Number(req.params['flagId'])),
],
'/api/projects/:team_id/feature_flags/:flagId/status': () => [
200,
{
status: 'active',
reason: 'Feature flag is active',
},
],
},
post: {
'/api/environments/:team_id/query': {},
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/scenes/feature-flags/featureFlagLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
EarlyAccessFeatureType,
FeatureFlagGroupType,
FeatureFlagRollbackConditions,
FeatureFlagStatusResponse,
FeatureFlagType,
FilterLogicalOperator,
FilterType,
Expand Down Expand Up @@ -755,6 +756,18 @@ export const featureFlagLogic = kea<featureFlagLogicType>([
}
},
},
flagStatus: [
null as FeatureFlagStatusResponse | null,
{
loadFeatureFlagStatus: () => {
const { currentTeamId } = values
if (currentTeamId && props.id && props.id !== 'new' && props.id !== 'link') {
return api.featureFlags.getStatus(currentTeamId, props.id)
}
return null
},
},
],
})),
listeners(({ actions, values, props }) => ({
submitNewDashboardSuccessWithResult: async ({ result }) => {
Expand Down Expand Up @@ -1040,8 +1053,10 @@ export const featureFlagLogic = kea<featureFlagLogicType>([
actions.setFeatureFlag(formatPayloadsWithFlag)
actions.loadRelatedInsights()
actions.loadAllInsightsForFlag()
actions.loadFeatureFlagStatus()
} else if (props.id !== 'new') {
actions.loadFeatureFlag()
actions.loadFeatureFlagStatus()
}
}),
])
13 changes: 13 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2995,6 +2995,19 @@ export interface FeatureFlagRollbackConditions {
operator?: string
}

export enum FeatureFlagStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
STALE = 'stale',
DELETED = 'deleted',
UNKNOWN = 'unknown',
}

export interface FeatureFlagStatusResponse {
status: FeatureFlagStatus
reason: string
}

export interface CombinedFeatureFlagAndValueType {
feature_flag: FeatureFlagType
value: boolean | string
Expand Down
24 changes: 24 additions & 0 deletions posthog/api/test/test_feature_flag.py
Original file line number Diff line number Diff line change
Expand Up @@ -6287,6 +6287,30 @@ def test_flag_status_reasons(self):
FeatureFlagStatus.ACTIVE,
)

# Request status for multivariate flag with a variant set to 100% but no release condition set to 100%
multivariate_flag_rolled_out_release_condition_half_variant = FeatureFlag.objects.create(
name="Multivariate flag with release condition set to 100%, but variants still 50%",
key="multivariate-rolled-out-release-half-variant-flag",
team=self.team,
active=True,
filters={
"multivariate": {
"variants": [
{"key": "var1key", "name": "test", "rollout_percentage": 50},
{"key": "var2key", "name": "control", "rollout_percentage": 50},
],
},
"groups": [
{"variant": None, "properties": [], "rollout_percentage": 100},
],
},
)
self.create_feature_flag_called_event(multivariate_flag_rolled_out_release_condition_half_variant.key)
self.assert_expected_response(
multivariate_flag_rolled_out_release_condition_half_variant.id,
FeatureFlagStatus.ACTIVE,
)

# Request status for multivariate flag with variants set to 100% and a filtered release condition
multivariate_flag_rolled_out_variant_rolled_out_filtered_release = FeatureFlag.objects.create(
name="Multivariate flag with variant and release condition set to 100%",
Expand Down
2 changes: 1 addition & 1 deletion posthog/models/feature_flag/flag_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def is_flag_fully_rolled_out(self, flag: FeatureFlag) -> tuple[bool, FeatureFlag
)
if multivariate and is_multivariate_flag_fully_rolled_out:
return True, f'This flag will always use the variant "{fully_rolled_out_variant_name}"'
elif self.is_boolean_flag_fully_rolled_out(flag):
elif not multivariate and self.is_boolean_flag_fully_rolled_out(flag):
return True, 'This boolean flag will always evaluate to "true"'

return False, ""
Expand Down

0 comments on commit ee20bdc

Please sign in to comment.