Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(toolbar): support feature flag payload overrides in the flag toolbar #28058

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a006864
added support for setting payload overrides
dmarticus Jan 29, 2025
9bbe892
use latest version from posthog-js
dmarticus Jan 29, 2025
cf1cd93
merge master
dmarticus Jan 29, 2025
8f81c61
Update UI snapshots for `chromium` (2)
github-actions[bot] Jan 29, 2025
05850ce
greptile feedback
dmarticus Jan 29, 2025
b32cb58
Merge branch 'feat/support-payload-overrides-in-flag-toolbar' of gith…
dmarticus Jan 29, 2025
93c2630
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 29, 2025
5453459
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 29, 2025
7abe2aa
use latest posthog-js
dmarticus Jan 30, 2025
d081f45
Merge branch 'feat/support-payload-overrides-in-flag-toolbar' of gith…
dmarticus Jan 30, 2025
4629b76
heckin conflicts
dmarticus Jan 30, 2025
5cc6e60
fix docs
dmarticus Jan 30, 2025
6b308cc
Merge branch 'master' into feat/support-payload-overrides-in-flag-too…
dmarticus Jan 30, 2025
d72bd7d
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 30, 2025
c2bd8ab
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 30, 2025
c28c89e
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 30, 2025
0b9f2d7
fix tsc
dmarticus Jan 30, 2025
b099ee0
Merge branch 'feat/support-payload-overrides-in-flag-toolbar' of gith…
dmarticus Jan 30, 2025
6f906c7
Merge branch 'master' into feat/support-payload-overrides-in-flag-too…
dmarticus Jan 30, 2025
a14f8a1
Update UI snapshots for `chromium` (2)
github-actions[bot] Jan 30, 2025
843e840
Update UI snapshots for `chromium` (2)
github-actions[bot] Jan 30, 2025
d73e80c
override these
dmarticus Feb 4, 2025
e3a20b5
Update UI snapshots for `chromium` (2)
github-actions[bot] Feb 4, 2025
f35e1c9
Update UI snapshots for `chromium` (1)
github-actions[bot] Feb 4, 2025
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.
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.
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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions frontend/src/scenes/experiments/ExperimentCodeSnippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export function JSSnippet({ flagKey, variant }: SnippetProps): JSX.Element {
<b>Test that it works</b>
</div>
<CodeSnippet language={Language.JavaScript} wrap>
{`posthog.featureFlags.override({'${flagKey}': '${variant}'})`}
{`posthog.featureFlags.overrideFeatureFlags({ flags: {'${flagKey}': '${variant}'})`}
</CodeSnippet>
</div>
)
Expand Down Expand Up @@ -120,7 +120,7 @@ function App() {
}

// You can also test your code by overriding the feature flag:
posthog.featureFlags.override({'${flagKey}': '${variant}'})`}
posthog.featureFlags.overrideFeatureFlags({ flags: {'${flagKey}': '${variant}'})`}
</CodeSnippet>
</>
)
Expand Down
164 changes: 108 additions & 56 deletions frontend/src/toolbar/flags/FlagsToolbarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,40 @@ import clsx from 'clsx'
import { useActions, useValues } from 'kea'
import { AnimatedCollapsible } from 'lib/components/AnimatedCollapsible'
import { IconOpenInNew } from 'lib/lemon-ui/icons'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonInput } from 'lib/lemon-ui/LemonInput'
import { LemonRadio } from 'lib/lemon-ui/LemonRadio'
import { LemonSwitch } from 'lib/lemon-ui/LemonSwitch'
import { LemonTextArea } from 'lib/lemon-ui/LemonTextArea'
import { Link } from 'lib/lemon-ui/Link'
import { Spinner } from 'lib/lemon-ui/Spinner'
import { useEffect } from 'react'
import { debounce } from 'lib/utils'
import { useEffect, useMemo } from 'react'
import { urls } from 'scenes/urls'

import { ToolbarMenu } from '~/toolbar/bar/ToolbarMenu'
import { flagsToolbarLogic } from '~/toolbar/flags/flagsToolbarLogic'
import { toolbarConfigLogic } from '~/toolbar/toolbarConfigLogic'

export const FlagsToolbarMenu = (): JSX.Element => {
const { searchTerm, filteredFlags, userFlagsLoading } = useValues(flagsToolbarLogic)
const { searchTerm, filteredFlags, userFlagsLoading, draftPayloads, payloadErrors } = useValues(flagsToolbarLogic)
const {
setSearchTerm,
setOverriddenUserFlag,
deleteOverriddenUserFlag,
getUserFlags,
checkLocalOverrides,
setFeatureFlagValueFromPostHogClient,
setDraftPayload,
savePayloadOverride,
} = useActions(flagsToolbarLogic)
const { apiURL, posthog: posthogClient } = useValues(toolbarConfigLogic)

const debouncedSetDraftPayload = useMemo(
() => debounce((key: string, value: string) => setDraftPayload(key, value), 300),
[setDraftPayload]
)

useEffect(() => {
posthogClient?.onFeatureFlags(setFeatureFlagValueFromPostHogClient)
getUserFlags()
Expand All @@ -48,62 +58,102 @@ export const FlagsToolbarMenu = (): JSX.Element => {
<ToolbarMenu.Body>
<div className="mt-1">
{filteredFlags.length > 0 ? (
filteredFlags.map(({ feature_flag, value, hasOverride, hasVariants, currentValue }) => (
<div className={clsx('-mx-1 py-1 px-2', hasOverride && 'bg-mark')} key={feature_flag.key}>
<div className="flex flex-row items-center">
<div className="flex-1 truncate">
<Link
className="font-medium"
to={`${apiURL}${
feature_flag.id
? urls.featureFlag(feature_flag.id)
: urls.featureFlags()
}`}
subtle
target="_blank"
>
{feature_flag.key}
<IconOpenInNew />
</Link>
filteredFlags.map(
({ feature_flag, value, hasOverride, hasVariants, currentValue, payloadOverride }) => (
<div
className={clsx('-mx-1 py-1 px-2', hasOverride && 'bg-mark')}
key={feature_flag.key}
>
<div className="flex flex-row items-center">
<div className="flex-1 truncate">
<Link
className="font-medium"
to={`${apiURL}${
feature_flag.id
? urls.featureFlag(feature_flag.id)
: urls.featureFlags()
}`}
subtle
target="_blank"
>
{feature_flag.key}
<IconOpenInNew />
</Link>
</div>

<LemonSwitch
checked={!!currentValue}
onChange={(checked) => {
const newValue =
hasVariants && checked
? (feature_flag.filters?.multivariate?.variants[0]
?.key as string)
: checked
if (newValue === value && hasOverride) {
deleteOverriddenUserFlag(feature_flag.key)
} else {
setOverriddenUserFlag(feature_flag.key, newValue)
}
}}
/>
</div>

<LemonSwitch
checked={!!currentValue}
onChange={(checked) => {
const newValue =
hasVariants && checked
? (feature_flag.filters?.multivariate?.variants[0]?.key as string)
: checked
if (newValue === value && hasOverride) {
deleteOverriddenUserFlag(feature_flag.key)
} else {
setOverriddenUserFlag(feature_flag.key, newValue)
}
}}
/>
</div>
<AnimatedCollapsible collapsed={!currentValue}>
<>
{hasVariants ? (
<LemonRadio
className={clsx('pt-1 pl-4 w-full', hasOverride && 'bg-mark')}
value={typeof currentValue === 'string' ? currentValue : undefined}
options={
feature_flag.filters?.multivariate?.variants.map((variant) => ({
label: `${variant.key} - ${variant.name} (${variant.rollout_percentage}%)`,
value: variant.key,
})) || []
}
onChange={(newValue) => {
if (newValue === value && hasOverride) {
deleteOverriddenUserFlag(feature_flag.key)
} else {
setOverriddenUserFlag(feature_flag.key, newValue)
}
}}
/>
) : null}

<AnimatedCollapsible collapsed={!hasVariants || !currentValue}>
<LemonRadio
className={clsx('pt-1 pl-4 w-full', hasOverride && 'bg-mark')}
value={typeof currentValue === 'string' ? currentValue : undefined}
options={
feature_flag.filters?.multivariate?.variants.map((variant) => ({
label: `${variant.key} - ${variant.name} (${variant.rollout_percentage}%)`,
value: variant.key,
})) || []
}
onChange={(newValue) => {
if (newValue === value && hasOverride) {
deleteOverriddenUserFlag(feature_flag.key)
} else {
setOverriddenUserFlag(feature_flag.key, newValue)
}
}}
/>
</AnimatedCollapsible>
</div>
))
<div className={clsx('py-1', hasVariants && 'pl-4')}>
<label className="text-xs font-semibold">Payload</label>
<div className="flex gap-2 items-center mt-1">
<LemonTextArea
className={clsx(
'font-mono text-xs flex-1 !rounded',
payloadErrors[feature_flag.key] && 'border-danger'
)}
value={
draftPayloads[feature_flag.key] ??
(payloadOverride
? JSON.stringify(payloadOverride, null, 2)
: '')
}
onChange={(val) =>
debouncedSetDraftPayload(feature_flag.key, val)
}
placeholder='{"key": "value"}'
minRows={2}
/>
<LemonButton
size="small"
type="primary"
onClick={() => savePayloadOverride(feature_flag.key)}
>
Save
</LemonButton>
</div>
</div>
</>
</AnimatedCollapsible>
</div>
)
)
) : (
<div className="flex flex-row items-center p-1">
{userFlagsLoading ? (
Expand All @@ -119,7 +169,9 @@ export const FlagsToolbarMenu = (): JSX.Element => {
</ToolbarMenu.Body>

<ToolbarMenu.Footer>
<span className="text-xs">Note: overriding feature flags will only affect this browser.</span>
<span className="text-xs">
Note: overriding feature flags and payloads will only affect this browser.
</span>
</ToolbarMenu.Footer>
</ToolbarMenu>
)
Expand Down
88 changes: 81 additions & 7 deletions frontend/src/toolbar/flags/flagsToolbarLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { CombinedFeatureFlagAndValueType } from '~/types'

import type { flagsToolbarLogicType } from './flagsToolbarLogicType'

export type PayloadOverrides = Record<string, any>

export const flagsToolbarLogic = kea<flagsToolbarLogicType>([
path(['toolbar', 'flags', 'flagsToolbarLogic']),
connect(() => ({
Expand All @@ -22,11 +24,23 @@ export const flagsToolbarLogic = kea<flagsToolbarLogicType>([
flags,
variants,
}),
setOverriddenUserFlag: (flagKey: string, overrideValue: string | boolean) => ({ flagKey, overrideValue }),
setOverriddenUserFlag: (
flagKey: string,
overrideValue: string | boolean,
payloadOverride?: PayloadOverrides
) => ({
flagKey,
overrideValue,
payloadOverride,
}),
setPayloadOverride: (flagKey: string, payload: any) => ({ flagKey, payload }),
deleteOverriddenUserFlag: (flagKey: string) => ({ flagKey }),
setSearchTerm: (searchTerm: string) => ({ searchTerm }),
checkLocalOverrides: true,
storeLocalOverrides: (localOverrides: Record<string, string | boolean>) => ({ localOverrides }),
setDraftPayload: (flagKey: string, draftPayload: string) => ({ flagKey, draftPayload }),
savePayloadOverride: (flagKey: string) => ({ flagKey }),
setPayloadError: (flagKey: string, error: string | null) => ({ flagKey, error }),
}),
loaders(({ values }) => ({
userFlags: [
Expand Down Expand Up @@ -70,11 +84,52 @@ export const flagsToolbarLogic = kea<flagsToolbarLogicType>([
},
},
],
payloadOverrides: [
{} as PayloadOverrides,
{
setPayloadOverride: (state, { flagKey, payload }) => ({
...state,
[flagKey]: payload,
}),
deleteOverriddenUserFlag: (state, { flagKey }) => {
const newState = { ...state }
delete newState[flagKey]
return newState
},
},
],
draftPayloads: [
{} as Record<string, string>,
{
setDraftPayload: (state, { flagKey, draftPayload }) => ({
...state,
[flagKey]: draftPayload,
}),
deleteOverriddenUserFlag: (state, { flagKey }) => {
const newState = { ...state }
delete newState[flagKey]
return newState
},
},
],
payloadErrors: [
{} as Record<string, string | null>,
{
setPayloadError: (state, { flagKey, error }) => ({
...state,
[flagKey]: error,
}),
setDraftPayload: (state, { flagKey }) => ({
...state,
[flagKey]: null,
}),
},
],
}),
selectors({
userFlagsWithOverrideInfo: [
(s) => [s.userFlags, s.localOverrides, s.posthogClientFlagValues],
(userFlags, localOverrides, posthogClientFlagValues) => {
(s) => [s.userFlags, s.localOverrides, s.posthogClientFlagValues, s.payloadOverrides],
(userFlags, localOverrides, posthogClientFlagValues, payloadOverrides) => {
return userFlags.map((flag) => {
const hasVariants = (flag.feature_flag.filters?.multivariate?.variants?.length || 0) > 0

Expand All @@ -88,6 +143,7 @@ export const flagsToolbarLogic = kea<flagsToolbarLogicType>([
hasVariants,
currentValue,
hasOverride: flag.feature_flag.key in localOverrides,
payloadOverride: payloadOverrides[flag.feature_flag.key],
}
})
},
Expand Down Expand Up @@ -115,12 +171,19 @@ export const flagsToolbarLogic = kea<flagsToolbarLogicType>([
actions.storeLocalOverrides(locallyOverrideFeatureFlags)
}
},
setOverriddenUserFlag: ({ flagKey, overrideValue }) => {
setOverriddenUserFlag: ({ flagKey, overrideValue, payloadOverride }) => {
const clientPostHog = values.posthog
if (clientPostHog) {
clientPostHog.featureFlags.override({ ...values.localOverrides, [flagKey]: overrideValue })
const payloads = payloadOverride ? { [flagKey]: payloadOverride } : undefined
clientPostHog.featureFlags.overrideFeatureFlags({
flags: { ...values.localOverrides, [flagKey]: overrideValue },
payloads: payloads,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: redundant key/val pair

})
toolbarPosthogJS.capture('toolbar feature flag overridden')
actions.checkLocalOverrides()
if (payloadOverride) {
actions.setPayloadOverride(flagKey, payloadOverride)
}
clientPostHog.featureFlags.reloadFeatureFlags()
}
},
Expand All @@ -130,15 +193,26 @@ export const flagsToolbarLogic = kea<flagsToolbarLogicType>([
const updatedFlags = { ...values.localOverrides }
delete updatedFlags[flagKey]
if (Object.keys(updatedFlags).length > 0) {
clientPostHog.featureFlags.override({ ...updatedFlags })
clientPostHog.featureFlags.overrideFeatureFlags({ flags: updatedFlags })
} else {
clientPostHog.featureFlags.override(false)
clientPostHog.featureFlags.overrideFeatureFlags(false)
}
toolbarPosthogJS.capture('toolbar feature flag override removed')
actions.checkLocalOverrides()
clientPostHog.featureFlags.reloadFeatureFlags()
}
},
savePayloadOverride: ({ flagKey }) => {
try {
const draftPayload = values.draftPayloads[flagKey]
const payload = draftPayload ? JSON.parse(draftPayload) : null
actions.setPayloadError(flagKey, null)
actions.setOverriddenUserFlag(flagKey, true, payload)
} catch (e) {
actions.setPayloadError(flagKey, 'Invalid JSON')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

console.error('Invalid JSON:', e)
}
},
})),
permanentlyMount(),
])
Expand Down
Loading