Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 15 additions & 3 deletions src/util/featureFlags/featureFlagClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,15 @@ jest.mock('src/util/staticConfig', () => ({
timeout: 1000,
},
isFX3ConfigValid: jest.fn(() => true),
FeatureFlagOverrides: {
gates: {},
experiments: {},
},
}));

import { Identifiers } from '@atlaskit/feature-gate-js-client';
import { it } from '@jest/globals';
import { isFX3ConfigValid } from 'src/util/staticConfig';
import { FeatureFlagOverrides, isFX3ConfigValid } from 'src/util/staticConfig';
import { forceCastTo } from 'testsutil';

import { ClientInitializedErrorType } from '../../analytics';
Expand Down Expand Up @@ -273,7 +277,8 @@ describe('FeatureFlagClient', () => {

describe('overrides', () => {
it('if overrides are set, checkGate returns the overridden value', async () => {
process.env.ATLASCODE_FF_OVERRIDES = `another-very-real-feature=false`;
// Set up override directly in the mocked object
FeatureFlagOverrides.gates['another-very-real-feature' as Features] = false;

FeatureFlagClient['singleton'] = undefined;
featureFlagClient = FeatureFlagClient.getInstance();
Expand All @@ -287,10 +292,14 @@ describe('FeatureFlagClient', () => {
expect(featureFlagClient.checkGate(forceCastTo<Features>('some-very-real-feature'))).toBeTruthy();
expect(featureFlagClient.checkGate(forceCastTo<Features>('another-very-real-feature'))).toBeFalsy();
expect(featureFlagClient.checkGate(forceCastTo<Features>('some-fake-feature'))).toBeFalsy();

// Clean up
delete FeatureFlagOverrides.gates['another-very-real-feature' as Features];
});

it('if overrides are set, getExperimentValue returns the overridden value', async () => {
process.env.ATLASCODE_EXP_OVERRIDES_STRING = `another-exp-name=another value`;
// Set up override directly in the mocked object
FeatureFlagOverrides.experiments['another-exp-name' as Experiments] = 'another value';

FeatureFlagClient['singleton'] = undefined;
featureFlagClient = FeatureFlagClient.getInstance();
Expand All @@ -316,6 +325,9 @@ describe('FeatureFlagClient', () => {
expect(
featureFlagClient.checkExperimentValue(forceCastTo<Experiments>('one-more-exp-name')),
).toBeUndefined();

// Clean up
delete FeatureFlagOverrides.experiments['another-exp-name' as Experiments];
});
});
});
87 changes: 13 additions & 74 deletions src/util/featureFlags/featureFlagClient.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ClientOptions, FeatureGateEnvironment, Identifiers } from '@atlaskit/feature-gate-js-client';
import { FetcherOptions } from '@atlaskit/feature-gate-js-client/dist/types/client/fetcher';
import { NewFeatureGateOptions } from '@atlaskit/feature-gate-js-client/dist/types/client/types';
import { FX3Config, isFX3ConfigValid } from 'src/util/staticConfig';
import { FeatureFlagOverrides, FX3Config, isFX3ConfigValid } from 'src/util/staticConfig';
import { env } from 'vscode';

import { ClientInitializedErrorType } from '../../analytics';
import { Logger } from '../../logger';
import { ExperimentGates, ExperimentGateValues, Experiments, FeatureGateValues, Features } from '../features';
import { ExperimentGates, Experiments, Features } from '../features';
import { FeatureGateClient } from './utils';

type NewFetcherOptions = FetcherOptions &
Expand Down Expand Up @@ -37,8 +37,6 @@ export class FeatureFlagClient {
return this.singleton;
}

private readonly featureGateOverrides: FeatureGateValues;
private readonly experimentValueOverride: ExperimentGateValues;
private readonly isExperimentationDisabled: boolean;

/* We keep two clients:
Expand All @@ -63,10 +61,6 @@ export class FeatureFlagClient {

private constructor() {
this.isExperimentationDisabled = !!process.env.ATLASCODE_NO_EXP || !env.isTelemetryEnabled;

this.featureGateOverrides = {} as FeatureGateValues;
this.experimentValueOverride = {} as ExperimentGateValues;
this.initializeOverrides();
}

/**
Expand Down Expand Up @@ -181,74 +175,17 @@ export class FeatureFlagClient {
this.clientWithTenant = await this.initializeWithRetry(this.clientOptions, identifiers);
}

private initializeOverrides(): void {
if (process.env.ATLASCODE_FF_OVERRIDES) {
const ffSplit = (process.env.ATLASCODE_FF_OVERRIDES || '')
.split(',')
.map(this.parseBoolOverride<Features>)
.filter((x) => !!x);

for (const { key, value } of ffSplit) {
this.featureGateOverrides[key] = value;
}
}

if (process.env.ATLASCODE_EXP_OVERRIDES_BOOL) {
const boolExpSplit = (process.env.ATLASCODE_EXP_OVERRIDES_BOOL || '')
.split(',')
.map(this.parseBoolOverride<Experiments>)
.filter((x) => !!x);

for (const { key, value } of boolExpSplit) {
this.experimentValueOverride[key] = value;
}
}

if (process.env.ATLASCODE_EXP_OVERRIDES_STRING) {
const strExpSplit = (process.env.ATLASCODE_EXP_OVERRIDES_STRING || '')
.split(',')
.map(this.parseStringOverride)
.filter((x) => !!x);

for (const { key, value } of strExpSplit) {
this.experimentValueOverride[key] = value;
}
}
}

private parseBoolOverride<T>(setting: string): { key: T; value: boolean } | undefined {
const [key, valueRaw] = setting
.trim()
.split('=', 2)
.map((x) => x.trim());

if (key) {
const value = valueRaw.toLowerCase() === 'true';
return { key: key as T, value };
} else {
return undefined;
}
}

private parseStringOverride(setting: string): { key: Experiments; value: string } | undefined {
const [key, value] = setting
.trim()
.split('=', 2)
.map((x) => x.trim());
if (key) {
return { key: key as Experiments, value };
} else {
return undefined;
}
}

private isInitialized(): boolean {
return !!this.client?.initializeCompleted();
}

public checkGate(gate: Features): boolean {
if (this.featureGateOverrides.hasOwnProperty(gate)) {
return this.featureGateOverrides[gate];
if (gate in FeatureFlagOverrides.gates) {
const overrideValue = FeatureFlagOverrides.gates[gate];
Logger.debug(`FeatureGates ${gate} -> ${overrideValue} (overridden)`);
if (overrideValue !== undefined) {
return overrideValue;
}
}

let gateValue = false;
Expand All @@ -263,12 +200,14 @@ export class FeatureFlagClient {

public checkExperimentValue(experiment: Experiments): any {
// unknown experiment name
if (!ExperimentGates.hasOwnProperty(experiment)) {
if (!(experiment in ExperimentGates)) {
return undefined;
}

if (this.experimentValueOverride.hasOwnProperty(experiment)) {
return this.experimentValueOverride[experiment];
if (experiment in FeatureFlagOverrides.experiments) {
const overrideValue = FeatureFlagOverrides.experiments[experiment];
Logger.debug(`Experiment ${experiment} -> ${overrideValue} (overridden)`);
return overrideValue;
}

const experimentGate = ExperimentGates[experiment];
Expand Down
129 changes: 129 additions & 0 deletions src/util/featureFlags/overrideParser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { parseBoolOverride, parseExperimentOverrides, parseGateOverrides, parseStringOverride } from './overrideParser';

describe('overrideParser', () => {
const originalEnv = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
});

afterAll(() => {
process.env = originalEnv;
});

describe('parseBoolOverride', () => {
it('should parse valid boolean overrides', () => {
expect(parseBoolOverride('feature1=true')).toEqual({ key: 'feature1', value: true });
expect(parseBoolOverride('feature2=false')).toEqual({ key: 'feature2', value: false });
expect(parseBoolOverride('feature3=TRUE')).toEqual({ key: 'feature3', value: true });
expect(parseBoolOverride('feature4=FALSE')).toEqual({ key: 'feature4', value: false });
});

it('should handle whitespace', () => {
expect(parseBoolOverride(' feature1 = true ')).toEqual({ key: 'feature1', value: true });
});

it('should return undefined for invalid input', () => {
expect(parseBoolOverride('')).toBeUndefined();
expect(parseBoolOverride('=')).toBeUndefined();
expect(parseBoolOverride(' = ')).toBeUndefined();
expect(parseBoolOverride('feature1')).toBeUndefined();
});

it('should handle missing value as false', () => {
expect(parseBoolOverride('feature1=')).toEqual({ key: 'feature1', value: false });
});
});

describe('parseStringOverride', () => {
it('should parse valid string overrides', () => {
expect(parseStringOverride('exp1=value1')).toEqual({ key: 'exp1', value: 'value1' });
expect(parseStringOverride('exp2=some value')).toEqual({ key: 'exp2', value: 'some value' });
});

it('should handle whitespace', () => {
expect(parseStringOverride(' exp1 = value1 ')).toEqual({ key: 'exp1', value: 'value1' });
});

it('should return undefined for invalid input', () => {
expect(parseStringOverride('')).toBeUndefined();
expect(parseStringOverride('=')).toBeUndefined();
expect(parseStringOverride('exp1=')).toBeUndefined();
expect(parseStringOverride('exp1')).toBeUndefined();
});
});

describe('parseGateOverrides', () => {
it('should parse feature gate overrides from environment', () => {
const overrides = parseGateOverrides('feature1=true,feature2=false');
expect(overrides).toEqual({
feature1: true,
feature2: false,
});
});

it('should handle empty environment variable', () => {
const overrides = parseGateOverrides('');
expect(overrides).toEqual({});
});

it('should handle undefined environment variable', () => {
const overrides = parseGateOverrides(undefined);
expect(overrides).toEqual({});
});

it('should skip invalid entries', () => {
const overrides = parseGateOverrides('feature1=true,,feature2=false,invalid,=,feature3=true');
expect(overrides).toEqual({
feature1: true,
feature2: false,
feature3: true,
});
});
});

describe('parseExperimentOverrides', () => {
it('should parse boolean experiment overrides from environment', () => {
const overrides = parseExperimentOverrides('exp1=true,exp2=false');
expect(overrides).toEqual({
exp1: true,
exp2: false,
});
});

it('should parse string experiment overrides from environment', () => {
const overrides = parseExperimentOverrides(undefined, 'exp1=value1,exp2=value2');
expect(overrides).toEqual({
exp1: 'value1',
exp2: 'value2',
});
});

it('should combine both boolean and string overrides', () => {
const overrides = parseExperimentOverrides('exp1=true', 'exp2=value2');
expect(overrides).toEqual({
exp1: true,
exp2: 'value2',
});
});

it('should handle empty environment variables', () => {
const overrides = parseExperimentOverrides('', '');
expect(overrides).toEqual({});
});

it('should handle undefined environment variables', () => {
const overrides = parseExperimentOverrides(undefined, undefined);
expect(overrides).toEqual({});
});

it('should skip invalid entries', () => {
const overrides = parseExperimentOverrides('exp1=true,,invalid', 'exp2=value2,=,exp3=');
expect(overrides).toEqual({
exp1: true,
exp2: 'value2',
});
});
});
});
Loading
Loading