From 54c1036134d894e73a246f7b02f4e90714412d36 Mon Sep 17 00:00:00 2001 From: Anubhav Tandon Date: Thu, 28 May 2026 23:08:53 +0530 Subject: [PATCH 1/2] added detection for multivariate differences in diff environments --- .../web/components/CompareEnvironments.js | 15 +++++- frontend/web/components/CompareFeatures.js | 4 +- .../compare-multivariate-utils.test.ts | 52 +++++++++++++++++++ .../components/compare-multivariate-utils.ts | 40 ++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 frontend/web/components/__tests__/compare-multivariate-utils.test.ts create mode 100644 frontend/web/components/compare-multivariate-utils.ts diff --git a/frontend/web/components/CompareEnvironments.js b/frontend/web/components/CompareEnvironments.js index e3d1b17e99b3..4a118de3a0e3 100644 --- a/frontend/web/components/CompareEnvironments.js +++ b/frontend/web/components/CompareEnvironments.js @@ -20,6 +20,7 @@ import { withRouter } from 'react-router-dom' import { getDarkMode } from 'project/darkMode' import { getStore } from 'common/store' import { removeProjectFlag } from 'common/services/useProjectFlag' +import { hasMultivariateChange } from 'components/compare-multivariate-utils' const featureNameWidth = 300 @@ -111,9 +112,14 @@ class CompareEnvironments extends Component { } change.enabledChanged = change.rightEnabled !== change.leftEnabled change.valueChanged = change.rightValue !== change.leftValue + change.multivariateChanged = hasMultivariateChange( + leftSide, + rightSide, + ) if ( change.enabledChanged || change.valueChanged || + change.multivariateChanged || projectFlagLeft.num_identity_overrides || projectFlagLeft.num_segment_overrides || projectFlagRight.num_identity_overrides || @@ -412,7 +418,12 @@ class CompareEnvironments extends Component { className='no-pad mt-2' items={this.filter(this.state.changes)} renderRow={(p, i) => - renderRow(p, i, !p.enabledChanged, !p.valueChanged) + renderRow( + p, + i, + !p.enabledChanged, + !(p.valueChanged || p.multivariateChanged), + ) } /> @@ -467,7 +478,7 @@ class CompareEnvironments extends Component { p, i, !p.enabledChanged, - !p.valueChanged, + !(p.valueChanged || p.multivariateChanged), ) } /> diff --git a/frontend/web/components/CompareFeatures.js b/frontend/web/components/CompareFeatures.js index fa6be6cc3b21..9a8c337da30f 100644 --- a/frontend/web/components/CompareFeatures.js +++ b/frontend/web/components/CompareFeatures.js @@ -9,6 +9,7 @@ import Permission from 'common/providers/Permission' import { withRouter } from 'react-router-dom' import { getStore } from 'common/store' import { removeProjectFlag } from 'common/services/useProjectFlag' +import { hasMultivariateChange } from 'components/compare-multivariate-utils' const featureNameWidth = 300 @@ -118,7 +119,8 @@ class CompareFeatures extends Component { const flagB = compare[this.state.flagId] const fadeEnabled = flagA.enabled === flagB.enabled const fadeValue = - flagB.feature_state_value === flagA.feature_state_value + flagB.feature_state_value === flagA.feature_state_value && + !hasMultivariateChange(flagA, flagB) const changeRequestsEnabled = Utils.changeRequestsEnabled( data.minimum_change_request_approvals, ) diff --git a/frontend/web/components/__tests__/compare-multivariate-utils.test.ts b/frontend/web/components/__tests__/compare-multivariate-utils.test.ts new file mode 100644 index 000000000000..962e5ce902ad --- /dev/null +++ b/frontend/web/components/__tests__/compare-multivariate-utils.test.ts @@ -0,0 +1,52 @@ +import { FeatureState } from 'common/types/responses' +import { hasMultivariateChange } from 'components/compare-multivariate-utils' + +const featureState = ( + multivariate_feature_state_values: FeatureState['multivariate_feature_state_values'], +): FeatureState => + ({ + multivariate_feature_state_values, + }) as FeatureState + +describe('hasMultivariateChange', () => { + it('returns false when both sides have no multivariate values', () => { + expect(hasMultivariateChange(featureState([]), featureState([]))).toBe(false) + expect(hasMultivariateChange(undefined, undefined)).toBe(false) + }) + + it('returns true when percentage allocations differ', () => { + const left = featureState([ + { id: 1, multivariate_feature_option: 10, percentage_allocation: 0 }, + ]) + const right = featureState([ + { id: 2, multivariate_feature_option: 10, percentage_allocation: 100 }, + ]) + + expect(hasMultivariateChange(left, right)).toBe(true) + }) + + it('returns true when the number of multivariate options differs', () => { + const left = featureState([ + { id: 1, multivariate_feature_option: 10, percentage_allocation: 50 }, + ]) + const right = featureState([ + { id: 2, multivariate_feature_option: 10, percentage_allocation: 50 }, + { id: 3, multivariate_feature_option: 11, percentage_allocation: 50 }, + ]) + + expect(hasMultivariateChange(left, right)).toBe(true) + }) + + it('returns false when values match regardless of array order', () => { + const left = featureState([ + { id: 1, multivariate_feature_option: 10, percentage_allocation: 0 }, + { id: 2, multivariate_feature_option: 11, percentage_allocation: 100 }, + ]) + const right = featureState([ + { id: 3, multivariate_feature_option: 11, percentage_allocation: 100 }, + { id: 4, multivariate_feature_option: 10, percentage_allocation: 0 }, + ]) + + expect(hasMultivariateChange(left, right)).toBe(false) + }) +}) diff --git a/frontend/web/components/compare-multivariate-utils.ts b/frontend/web/components/compare-multivariate-utils.ts new file mode 100644 index 000000000000..5ede08d8abff --- /dev/null +++ b/frontend/web/components/compare-multivariate-utils.ts @@ -0,0 +1,40 @@ +import { FeatureState } from 'common/types/responses' + +const normaliseMultivariate = (featureState?: FeatureState) => { + const values = featureState?.multivariate_feature_state_values || [] + return values + .map((v) => ({ + multivariate_feature_option: v.multivariate_feature_option, + percentage_allocation: Number(v.percentage_allocation) || 0, + })) + .sort((a, b) => { + if (a.multivariate_feature_option !== b.multivariate_feature_option) { + return a.multivariate_feature_option - b.multivariate_feature_option + } + return a.percentage_allocation - b.percentage_allocation + }) +} + +export const hasMultivariateChange = ( + leftFeatureState?: FeatureState, + rightFeatureState?: FeatureState, +) => { + const left = normaliseMultivariate(leftFeatureState) + const right = normaliseMultivariate(rightFeatureState) + + if (left.length !== right.length) { + return true + } + + for (let i = 0; i < left.length; i++) { + if ( + left[i].multivariate_feature_option !== + right[i].multivariate_feature_option || + left[i].percentage_allocation !== right[i].percentage_allocation + ) { + return true + } + } + + return false +} From fac5b947292e18e361509b48a4b18f743f0f6c05 Mon Sep 17 00:00:00 2001 From: Anubhav Tandon Date: Thu, 28 May 2026 23:34:06 +0530 Subject: [PATCH 2/2] prevented NaN results --- frontend/web/components/compare-multivariate-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/web/components/compare-multivariate-utils.ts b/frontend/web/components/compare-multivariate-utils.ts index 5ede08d8abff..38a1b918011e 100644 --- a/frontend/web/components/compare-multivariate-utils.ts +++ b/frontend/web/components/compare-multivariate-utils.ts @@ -4,8 +4,8 @@ const normaliseMultivariate = (featureState?: FeatureState) => { const values = featureState?.multivariate_feature_state_values || [] return values .map((v) => ({ - multivariate_feature_option: v.multivariate_feature_option, - percentage_allocation: Number(v.percentage_allocation) || 0, + multivariate_feature_option: Number(v?.multivariate_feature_option) || 0, + percentage_allocation: Number(v?.percentage_allocation) || 0, })) .sort((a, b) => { if (a.multivariate_feature_option !== b.multivariate_feature_option) {