diff --git a/frontend/common/utils/__tests__/featureValueValidation.test.ts b/frontend/common/utils/__tests__/featureValueValidation.test.ts
new file mode 100644
index 000000000000..be764531af99
--- /dev/null
+++ b/frontend/common/utils/__tests__/featureValueValidation.test.ts
@@ -0,0 +1,33 @@
+import {
+ getFeatureValueLengthError,
+ hasInvalidFeatureValueLength,
+} from 'common/utils/featureValueValidation'
+
+describe('getFeatureValueLengthError', () => {
+ it('accepts values up to the configured maximum', () => {
+ expect(getFeatureValueLengthError('x'.repeat(20_000))).toBeUndefined()
+ })
+
+ it('reports the actual and maximum character counts when exceeded', () => {
+ expect(getFeatureValueLengthError('x'.repeat(20_001))).toBe(
+ 'Flag value is 20,001 characters. Maximum length is 20,000 characters.',
+ )
+ })
+
+ it('counts Unicode code points as characters', () => {
+ expect(getFeatureValueLengthError('😀'.repeat(20_001))).toContain(
+ '20,001 characters',
+ )
+ })
+
+ it('does not apply the string limit to non-string values', () => {
+ expect(getFeatureValueLengthError(20_001)).toBeUndefined()
+ })
+
+ it('detects any over-limit value in a save', () => {
+ expect(hasInvalidFeatureValueLength(['valid', 'x'.repeat(20_001)])).toBe(
+ true,
+ )
+ expect(hasInvalidFeatureValueLength(['valid', 'also valid'])).toBe(false)
+ })
+})
diff --git a/frontend/common/utils/featureValueValidation.ts b/frontend/common/utils/featureValueValidation.ts
new file mode 100644
index 000000000000..cba21eae81dd
--- /dev/null
+++ b/frontend/common/utils/featureValueValidation.ts
@@ -0,0 +1,31 @@
+export const MAX_FEATURE_VALUE_LENGTH = 20_000
+
+export function getFeatureValueLengthError(
+ value: unknown,
+ label = 'Flag value',
+ maxLength = MAX_FEATURE_VALUE_LENGTH,
+): string | undefined {
+ if (typeof value !== 'string') return undefined
+
+ const characterCount = Array.from(value).length
+ if (characterCount <= maxLength) return undefined
+
+ return `${label} is ${characterCount.toLocaleString(
+ 'en-GB',
+ )} characters. Maximum length is ${maxLength.toLocaleString(
+ 'en-GB',
+ )} characters.`
+}
+
+export function getFeatureValueLengthErrors(
+ values: { label: string; value: unknown }[],
+): string[] {
+ return values.flatMap(({ label, value }) => {
+ const error = getFeatureValueLengthError(value, label)
+ return error ? [error] : []
+ })
+}
+
+export function hasInvalidFeatureValueLength(values: unknown[]): boolean {
+ return values.some((value) => !!getFeatureValueLengthError(value))
+}
diff --git a/frontend/web/components/SegmentOverrides.js b/frontend/web/components/SegmentOverrides.js
index 30c29d73d34a..3351f65a3738 100644
--- a/frontend/web/components/SegmentOverrides.js
+++ b/frontend/web/components/SegmentOverrides.js
@@ -278,6 +278,7 @@ const SegmentOverrideInner = class Override extends React.Component {
}
value={v.value}
disabled={readOnly}
diff --git a/frontend/web/components/ValueEditor/ValueEditor.tsx b/frontend/web/components/ValueEditor/ValueEditor.tsx
index 847760764f96..1da2f1e79f79 100644
--- a/frontend/web/components/ValueEditor/ValueEditor.tsx
+++ b/frontend/web/components/ValueEditor/ValueEditor.tsx
@@ -9,9 +9,14 @@ import React, {
} from 'react'
import cx from 'classnames'
+import { FlagsmithValue } from 'common/types/responses'
+import {
+ getFeatureValueLengthError,
+ MAX_FEATURE_VALUE_LENGTH,
+} from 'common/utils/featureValueValidation'
+import ErrorMessage from 'components/ErrorMessage'
import FieldLabel from 'components/base/forms/FieldLabel'
import Highlight from 'components/Highlight'
-import { FlagsmithValue } from 'common/types/responses'
import CopyValueButton from './components/CopyValueButton'
import LanguageSelector from './components/LanguageSelector'
@@ -36,6 +41,7 @@ export interface ValueEditorProps {
// caller's job (Utils.getTypedValue, Utils.valueToFeatureState).
onChange?: (value: string) => void
value?: FlagsmithValue
+ validateLength?: boolean
}
const ValueEditor: FC = ({
@@ -47,6 +53,7 @@ const ValueEditor: FC = ({
language: languageProp,
onBlur,
onChange,
+ validateLength = false,
value,
}) => {
const [picked, setPicked] = useState()
@@ -56,6 +63,13 @@ const ValueEditor: FC = ({
const language = languageProp ?? picked ?? detected ?? 'txt'
const labelId = useId()
const text = value === undefined || value === null ? '' : `${value}`
+ const lengthError = validateLength
+ ? getFeatureValueLengthError(
+ text,
+ typeof label === 'string' ? label : 'Value',
+ MAX_FEATURE_VALUE_LENGTH,
+ )
+ : undefined
// Detection waits for a value rather than running on mount: values load
// after mount, and a mount-only check left JSON rendering as plaintext.
@@ -112,12 +126,18 @@ const ValueEditor: FC = ({
)}
+ {!!lengthError && (
+
+
+
+ )}
{showControls &&
}
= (props) => {
return { featureError, featureWarning }
}
+ const getValueLengthErrors = () =>
+ getFeatureValueLengthErrors([
+ {
+ label: identity ? 'Identity override' : 'Flag value',
+ value: environmentFlag.feature_state_value,
+ },
+ ...(projectFlag.multivariate_options || []).map(
+ (option: any, index: number) => ({
+ label: `Variant ${option.key || index + 1} value`,
+ value: option.string_value,
+ }),
+ ),
+ ...(segmentOverrides || [])
+ .filter((override: any) => !override.toRemove)
+ .map((override: any, index: number) => ({
+ label: `Segment override ${override.segment_name || index + 1} value`,
+ value: override.value,
+ })),
+ ])
+
+ const showValueLengthErrors = (errors = getValueLengthErrors()) => {
+ errors.forEach((error) => toast(error, 'danger'))
+ return errors.length > 0
+ }
+
const save = (func: (...args: any[]) => void, isSaving: boolean) => {
+ if (showValueLengthErrors()) return
+
const hasMultivariate =
props.environmentFlag?.multivariate_feature_state_values?.length
@@ -476,6 +504,11 @@ const CreateFeatureModal: FC = (props) => {
!!projectFlag.multivariate_options &&
projectFlag.multivariate_options.length &&
controlValue < 0
+ const validateValueLengths =
+ (cb: (schedule?: boolean) => void) => (schedule?: boolean) => {
+ if (showValueLengthErrors()) return
+ cb(schedule)
+ }
const isVersionedChangeRequest = existingChangeRequest && isVersioned
const hideIdentityOverridesTab = Utils.getShouldHideIdentityOverridesTab()
const isDependenciesTabEnabled = !existingChangeRequest
@@ -577,7 +610,16 @@ const CreateFeatureModal: FC = (props) => {
const hasVariationChanges =
variationChanges.values || variationChanges.added
- const onSaveVariationValues = () =>
+ const onSaveVariationValues = () => {
+ const valueLengthErrors = getFeatureValueLengthErrors(
+ (projectFlag.multivariate_options || []).map(
+ (option: any, index: number) => ({
+ label: `Variant ${option.key || index + 1} value`,
+ value: option.string_value,
+ }),
+ ),
+ )
+ if (showValueLengthErrors(valueLengthErrors)) return
openConfirm({
body: 'Variation values belong to the feature, so this applies to every environment straight away and is not part of a change request.',
noText: 'Cancel',
@@ -624,9 +666,11 @@ const CreateFeatureModal: FC = (props) => {
title: 'Save variation values',
yesText: 'Save for all environments',
})
+ }
const saveFeatureValue = saveFeatureWithValidation(
(schedule?: boolean) => {
+ if (showValueLengthErrors()) return
if ((is4Eyes || schedule) && !identity) {
const approvable =
dependenciesChanged ||
@@ -831,7 +875,9 @@ const CreateFeatureModal: FC = (props) => {
hasVariationChanges={hasVariationChanges}
onSaveVariationValues={onSaveVariationValues}
originalMultivariateOptions={originalMultivariateOptions}
- onSaveFeatureValue={saveFeatureValue}
+ onSaveFeatureValue={validateValueLengths(
+ saveFeatureValue,
+ )}
onEnvironmentFlagChange={(changes: any) => {
setEnvironmentFlag((prev: any) => ({
...prev,
diff --git a/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx b/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx
index 172cefada982..39fcdc64405b 100644
--- a/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx
+++ b/frontend/web/components/modals/create-feature/tabs/FeatureValueTab.tsx
@@ -404,6 +404,7 @@ const FeatureValueTab: FC = ({
diff --git a/frontend/web/components/mv/VariationValueInput/VariationValueInput.tsx b/frontend/web/components/mv/VariationValueInput/VariationValueInput.tsx
index 3e633ddb5212..dd74a311eef0 100644
--- a/frontend/web/components/mv/VariationValueInput/VariationValueInput.tsx
+++ b/frontend/web/components/mv/VariationValueInput/VariationValueInput.tsx
@@ -72,6 +72,7 @@ export const VariationValueInput: React.FC = ({
: Constants.projectPermissions(ProjectPermission.CREATE_FEATURE),