diff --git a/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png b/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png index 9dcb691a87b2b..db26861ff22e7 100644 Binary files a/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png and b/frontend/__snapshots__/replay-player-success--second-recording-in-list--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png index fe07c80104e2c..a31c66c58955c 100644 Binary files a/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png and b/frontend/__snapshots__/scenes-app-pipeline--pipeline-node-new-hog-function--light.png differ diff --git a/frontend/__snapshots__/scenes-other-toolbar--feature-flags--dark.png b/frontend/__snapshots__/scenes-other-toolbar--feature-flags--dark.png index 5243592a7a183..6d01ce74f7165 100644 Binary files a/frontend/__snapshots__/scenes-other-toolbar--feature-flags--dark.png and b/frontend/__snapshots__/scenes-other-toolbar--feature-flags--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-toolbar--feature-flags--light.png b/frontend/__snapshots__/scenes-other-toolbar--feature-flags--light.png index b531547d94a4b..b6154a980b367 100644 Binary files a/frontend/__snapshots__/scenes-other-toolbar--feature-flags--light.png and b/frontend/__snapshots__/scenes-other-toolbar--feature-flags--light.png differ diff --git a/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--dark.png b/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--dark.png index 4139a54d59545..e16a758a0e9cb 100644 Binary files a/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--dark.png and b/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--dark.png differ diff --git a/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--light.png b/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--light.png index 514f86b9e74c0..b487e494bea51 100644 Binary files a/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--light.png and b/frontend/__snapshots__/scenes-other-toolbar--feature-flags-dark--light.png differ diff --git a/frontend/__snapshots__/scenes-other-toolbar--inspect-dark--light.png b/frontend/__snapshots__/scenes-other-toolbar--inspect-dark--light.png index 6818c6b7b16b1..c97a0e9b97a82 100644 Binary files a/frontend/__snapshots__/scenes-other-toolbar--inspect-dark--light.png and b/frontend/__snapshots__/scenes-other-toolbar--inspect-dark--light.png differ diff --git a/frontend/src/scenes/experiments/ExperimentCodeSnippets.tsx b/frontend/src/scenes/experiments/ExperimentCodeSnippets.tsx index 8d9aefd7269a9..00683f8d896a7 100644 --- a/frontend/src/scenes/experiments/ExperimentCodeSnippets.tsx +++ b/frontend/src/scenes/experiments/ExperimentCodeSnippets.tsx @@ -83,7 +83,7 @@ export function JSSnippet({ flagKey, variant }: SnippetProps): JSX.Element { Test that it works - {`posthog.featureFlags.override({'${flagKey}': '${variant}'})`} + {`posthog.featureFlags.overrideFeatureFlags({ flags: {'${flagKey}': '${variant}'})`} ) @@ -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}'})`} ) diff --git a/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx b/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx index 99efa265d6050..4287ee01e60d0 100644 --- a/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx +++ b/frontend/src/toolbar/flags/FlagsToolbarMenu.tsx @@ -2,12 +2,15 @@ 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' @@ -15,7 +18,7 @@ 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, @@ -23,9 +26,16 @@ export const FlagsToolbarMenu = (): JSX.Element => { 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() @@ -48,62 +58,102 @@ export const FlagsToolbarMenu = (): JSX.Element => {
{filteredFlags.length > 0 ? ( - filteredFlags.map(({ feature_flag, value, hasOverride, hasVariants, currentValue }) => ( -
-
-
- - {feature_flag.key} - - + filteredFlags.map( + ({ feature_flag, value, hasOverride, hasVariants, currentValue, payloadOverride }) => ( +
+
+
+ + {feature_flag.key} + + +
+ + { + 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) + } + }} + />
- { - 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) - } - }} - /> -
+ + <> + {hasVariants ? ( + ({ + 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} - - ({ - 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) - } - }} - /> - -
- )) +
+ +
+ + debouncedSetDraftPayload(feature_flag.key, val) + } + placeholder='{"key": "value"}' + minRows={2} + /> + savePayloadOverride(feature_flag.key)} + > + Save + +
+
+ + +
+ ) + ) ) : (
{userFlagsLoading ? ( @@ -119,7 +169,9 @@ export const FlagsToolbarMenu = (): JSX.Element => { - Note: overriding feature flags will only affect this browser. + + Note: overriding feature flags and payloads will only affect this browser. + ) diff --git a/frontend/src/toolbar/flags/flagsToolbarLogic.ts b/frontend/src/toolbar/flags/flagsToolbarLogic.ts index 07e7082646023..1c25de396eafd 100644 --- a/frontend/src/toolbar/flags/flagsToolbarLogic.ts +++ b/frontend/src/toolbar/flags/flagsToolbarLogic.ts @@ -11,6 +11,8 @@ import { CombinedFeatureFlagAndValueType } from '~/types' import type { flagsToolbarLogicType } from './flagsToolbarLogicType' +export type PayloadOverrides = Record + export const flagsToolbarLogic = kea([ path(['toolbar', 'flags', 'flagsToolbarLogic']), connect(() => ({ @@ -22,11 +24,23 @@ export const flagsToolbarLogic = kea([ 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) => ({ localOverrides }), + setDraftPayload: (flagKey: string, draftPayload: string) => ({ flagKey, draftPayload }), + savePayloadOverride: (flagKey: string) => ({ flagKey }), + setPayloadError: (flagKey: string, error: string | null) => ({ flagKey, error }), }), loaders(({ values }) => ({ userFlags: [ @@ -70,11 +84,52 @@ export const flagsToolbarLogic = kea([ }, }, ], + payloadOverrides: [ + {} as PayloadOverrides, + { + setPayloadOverride: (state, { flagKey, payload }) => ({ + ...state, + [flagKey]: payload, + }), + deleteOverriddenUserFlag: (state, { flagKey }) => { + const newState = { ...state } + delete newState[flagKey] + return newState + }, + }, + ], + draftPayloads: [ + {} as Record, + { + setDraftPayload: (state, { flagKey, draftPayload }) => ({ + ...state, + [flagKey]: draftPayload, + }), + deleteOverriddenUserFlag: (state, { flagKey }) => { + const newState = { ...state } + delete newState[flagKey] + return newState + }, + }, + ], + payloadErrors: [ + {} as Record, + { + 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 @@ -88,6 +143,7 @@ export const flagsToolbarLogic = kea([ hasVariants, currentValue, hasOverride: flag.feature_flag.key in localOverrides, + payloadOverride: payloadOverrides[flag.feature_flag.key], } }) }, @@ -115,12 +171,19 @@ export const flagsToolbarLogic = kea([ 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, + }) toolbarPosthogJS.capture('toolbar feature flag overridden') actions.checkLocalOverrides() + if (payloadOverride) { + actions.setPayloadOverride(flagKey, payloadOverride) + } clientPostHog.featureFlags.reloadFeatureFlags() } }, @@ -130,15 +193,26 @@ export const flagsToolbarLogic = kea([ 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') + console.error('Invalid JSON:', e) + } + }, })), permanentlyMount(), ])