Skip to content

Commit

Permalink
feat: Add feature variant selector (#662)
Browse files Browse the repository at this point in the history
  • Loading branch information
LautaroPetaccio authored Jan 13, 2025
1 parent 8510756 commit e81c108
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 1 deletion.
122 changes: 122 additions & 0 deletions src/modules/features/selectors.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getMockApplicationFeaturesRecord } from './actions.spec'
import {
getData,
getError,
getFeatureVariant,
getIsFeatureEnabled,
getLoading,
hasLoadedInitialFlags,
Expand Down Expand Up @@ -191,6 +192,127 @@ describe('when getting if a feature is enabled', () => {
})
})

describe('when getting a feature variant', () => {
let data: ReturnType<typeof getMockApplicationFeaturesRecord>
let state: StateWithFeatures

beforeEach(() => {
data = getMockApplicationFeaturesRecord()
state = {
features: {
data,
error: null,
hasLoadedInitialFlags: false,
loading: []
}
}
})

describe('when the variant is defined in the environment', () => {
beforeEach(() => {
process.env.REACT_APP_FF_VARIANT_DAPPS_TEST_FEATURE = 'test-variant'
})

afterEach(() => {
delete process.env.REACT_APP_FF_VARIANT_DAPPS_TEST_FEATURE
})

it('should return a local variant with the environment value', () => {
expect(getFeatureVariant(state, ApplicationName.DAPPS, 'test-feature')).toEqual({
name: 'Local variant',
enabled: true,
payload: {
type: 'string',
value: 'test-variant'
}
})
})
})

describe('when the variant is not defined in the environment', () => {
describe('and the application features are not loaded', () => {
beforeEach(() => {
state = {
features: {
data: {},
error: null,
hasLoadedInitialFlags: false,
loading: []
}
}
})

it('should return null', () => {
expect(getFeatureVariant(state, ApplicationName.DAPPS, 'test-feature')).toBeNull()
})
})

describe('and the application features are loaded', () => {
describe('and the variant exists', () => {
beforeEach(() => {
state = {
features: {
data: {
[ApplicationName.DAPPS]: {
name: ApplicationName.DAPPS,
flags: {},
variants: {
'dapps-test-feature': {
name: 'Remote variant',
enabled: true,
payload: {
type: 'string',
value: 'remote-variant'
}
}
}
}
},
error: null,
hasLoadedInitialFlags: true,
loading: []
}
}
})

it('should return the variant from the store', () => {
expect(getFeatureVariant(state, ApplicationName.DAPPS, 'test-feature')).toEqual({
name: 'Remote variant',
enabled: true,
payload: {
type: 'string',
value: 'remote-variant'
}
})
})
})

describe('and the variant does not exist', () => {
beforeEach(() => {
state = {
features: {
data: {
[ApplicationName.DAPPS]: {
name: ApplicationName.DAPPS,
flags: {},
variants: {}
}
},
error: null,
hasLoadedInitialFlags: true,
loading: []
}
}
})

it('should return null', () => {
expect(getFeatureVariant(state, ApplicationName.DAPPS, 'test-feature')).toBeNull()
})
})
})
})
})

describe('when getting if the feature flags were loaded at least once', () => {
let state: StateWithFeatures

Expand Down
42 changes: 41 additions & 1 deletion src/modules/features/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { FeaturesState } from './reducer'
import {
ApplicationName,
ApplicationFeatures,
StateWithFeatures
StateWithFeatures,
Variant
} from './types'

export const getState = (state: StateWithFeatures): FeaturesState => {
Expand Down Expand Up @@ -73,6 +74,34 @@ export const isLoadingFeatureFlags = (state: StateWithFeatures) => {
return isLoadingType(getLoading(state), FETCH_APPLICATION_FEATURES_REQUEST)
}

export const getFeatureVariant = (state: StateWithFeatures, app: ApplicationName, feature: string): Variant | null => {
const variant = getVariantFromEnv(app, feature)

// Return the flag variant if it has been defined in the env.
// If flag variants are only defined in the env, there is no need to implement the features reducer.
if (variant !== null) {
// Build the variant object
return {
name: 'Local variant',
enabled: true,
payload: {
type: 'string',
value: variant
}
}
}

const appFeatures = getData(state)[app]

// The app might not be defined in the store because the flag variants might not have been fetched yet.
// We suggest using isLoadingFeatureFlags and hasLoadedInitialFlags to handle this first.
if (!appFeatures) {
return null
}

return appFeatures.variants[`${app}-${feature}`] || null
}

export const hasLoadedInitialFlags = (state: StateWithFeatures) => {
return getState(state).hasLoadedInitialFlags
}
Expand All @@ -87,3 +116,14 @@ const getFromEnv = (

return !value || value === '' ? null : value === '1' ? true : false
}

const getVariantFromEnv = (
application: ApplicationName,
flag: string
): string | null => {
const envify = (word: string) => word.toUpperCase().replace(/-/g, '_')
const key = `REACT_APP_FF_VARIANT_${envify(application)}_${envify(flag)}`
const value = process.env[key]

return !value || value === '' ? null : value
}

0 comments on commit e81c108

Please sign in to comment.