Skip to content

Commit 2e6bbd3

Browse files
authoredMar 4, 2025
Fix exceptions due to failures in FeatureFlagClient initialization (#164)
* Fix exceptions during feature client initialization * removing leftover async * removing stale lint-ignore comment
1 parent 82d6eab commit 2e6bbd3

File tree

3 files changed

+63
-67
lines changed

3 files changed

+63
-67
lines changed
 

‎src/util/featureFlags/client.test.ts

+18-18
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,23 @@
22
enum MockFeatures {
33
TestFeature = 'some-very-real-feature',
44
}
5+
6+
enum MockExperiments {
7+
TestExperiment = 'some-very-real-experiment',
8+
}
9+
10+
const MockExperimentGates = {
11+
[MockExperiments.TestExperiment]: {
12+
parameter: 'isEnabled',
13+
defaultValue: false,
14+
},
15+
};
16+
517
jest.mock('./features', () => {
618
return {
719
Features: MockFeatures,
20+
Experiments: MockExperiments,
21+
ExperimentGates: MockExperimentGates,
822
};
923
});
1024

@@ -14,11 +28,12 @@ jest.mock('@atlaskit/feature-gate-js-client', () => {
1428
default: {
1529
initialize: jest.fn(() => Promise.resolve()),
1630
checkGate: jest.fn(() => Promise.resolve(false)),
31+
getExperimentValue: jest.fn(() => Promise.resolve(false)),
1732
},
1833
};
1934
});
20-
import FeatureGates from '@atlaskit/feature-gate-js-client';
2135

36+
import FeatureGates from '@atlaskit/feature-gate-js-client';
2237
import { FeatureFlagClient, FeatureFlagClientOptions } from './client';
2338
import { EventBuilderInterface } from './analytics';
2439

@@ -61,6 +76,8 @@ describe('FeatureFlagClient', () => {
6176
it('should initialize the feature flag client', async () => {
6277
await FeatureFlagClient.initialize(options);
6378
expect(FeatureGates.initialize).toHaveBeenCalled();
79+
expect(FeatureGates.checkGate).toHaveBeenCalled();
80+
expect(FeatureGates.getExperimentValue).toHaveBeenCalled();
6481
});
6582

6683
it('should catch an error when the feature flag client fails to initialize', async () => {
@@ -69,21 +86,4 @@ describe('FeatureFlagClient', () => {
6986
expect(FeatureGates.initialize).toHaveBeenCalled();
7087
});
7188
});
72-
73-
describe('checkGate', () => {
74-
it('should check the feature flag gate', async () => {
75-
FeatureFlagClient.checkGate(MockFeatures.TestFeature);
76-
expect(FeatureGates.checkGate).toHaveBeenCalled();
77-
});
78-
});
79-
80-
describe('evaluateFeatures', () => {
81-
it('should return all evaluated feature flags', async () => {
82-
const map = await FeatureFlagClient.evaluateFeatures();
83-
expect(map).toEqual({
84-
[MockFeatures.TestFeature]: false,
85-
});
86-
expect(FeatureGates.checkGate).toHaveBeenCalled();
87-
});
88-
});
8989
});

‎src/util/featureFlags/client.ts

+41-43
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,27 @@ export class FeatureFlagClient {
2424
}
2525

2626
public static async initialize(options: FeatureFlagClientOptions): Promise<void> {
27-
const targetApp = process.env.ATLASCODE_FX3_TARGET_APP || '';
28-
const environment =
29-
(process.env.ATLASCODE_FX3_ENVIRONMENT as FeatureGateEnvironment) || FeatureGateEnvironment.Production;
27+
const targetApp = process.env.ATLASCODE_FX3_TARGET_APP;
28+
const environment = process.env.ATLASCODE_FX3_ENVIRONMENT as FeatureGateEnvironment;
29+
const apiKey = process.env.ATLASCODE_FX3_API_KEY;
30+
const timeout = process.env.ATLASCODE_FX3_TIMEOUT;
31+
32+
if (!targetApp || !environment || !apiKey || !timeout) {
33+
this._featureGates = this.evaluateFeatures();
34+
this._experimentValues = this.evaluateExperiments();
35+
return;
36+
}
3037

3138
console.log(`FeatureGates: initializing, target: ${targetApp}, environment: ${environment}`);
3239
this.analyticsClient = new AnalyticsClientMapper(options.analyticsClient, options.identifiers);
3340
this.eventBuilder = options.eventBuilder;
3441

35-
return FeatureGates.initialize(
42+
await FeatureGates.initialize(
3643
{
37-
apiKey: process.env.ATLASCODE_FX3_API_KEY || '',
44+
apiKey,
3845
environment,
3946
targetApp,
40-
fetchTimeoutMs: Number.parseInt(process.env.ATLASCODE_FX3_TIMEOUT || '2000'),
47+
fetchTimeoutMs: Number.parseInt(timeout),
4148
analyticsWebClient: Promise.resolve(this.analyticsClient),
4249
},
4350
options.identifiers,
@@ -47,29 +54,27 @@ export class FeatureFlagClient {
4754
this.eventBuilder.featureFlagClientInitializedEvent().then((e) => {
4855
options.analyticsClient.sendTrackEvent(e);
4956
});
50-
})
51-
.then(() => {
57+
5258
// console log all feature gates and values
5359
for (const feat of Object.values(Features)) {
5460
console.log(`FeatureGates: ${feat} -> ${FeatureGates.checkGate(feat)}`);
5561
}
5662
})
57-
.then(async () => {
58-
this._featureGates = await this.evaluateFeatures();
59-
this._experimentValues = await this.evaluateExperiments();
60-
})
6163
.catch((err) => {
6264
console.warn(`FeatureGates: Failed to initialize client. ${err}`);
63-
console.warn('FeatureGates: Disabling feature flags');
6465
this.eventBuilder
6566
.featureFlagClientInitializationFailedEvent()
6667
.then((e) => options.analyticsClient.sendTrackEvent(e));
68+
})
69+
.finally(() => {
70+
this._featureGates = this.evaluateFeatures();
71+
this._experimentValues = this.evaluateExperiments();
6772
});
6873
}
6974

70-
public static checkGate(gate: string): boolean {
75+
private static checkGate(gate: Features): boolean {
7176
let gateValue = false;
72-
if (FeatureGates === null) {
77+
if (!FeatureGates) {
7378
console.warn('FeatureGates: FeatureGates is not initialized. Defaulting to False');
7479
} else {
7580
// FeatureGates.checkGate returns false if any errors
@@ -79,20 +84,18 @@ export class FeatureFlagClient {
7984
return gateValue;
8085
}
8186

82-
public static checkExperimentValue(experiment: string): any {
83-
let gateValue: any;
87+
private static checkExperimentValue(experiment: Experiments): any {
8488
const experimentGate = ExperimentGates[experiment];
8589
if (!experimentGate) {
8690
return undefined;
8791
}
88-
if (FeatureGates === null) {
89-
console.warn(
90-
`FeatureGates: FeatureGates is not initialized. Returning default value: ${experimentGate.defaultValue}`,
91-
);
92-
gateValue = experimentGate.defaultValue;
92+
93+
let gateValue = experimentGate.defaultValue;
94+
if (!FeatureGates) {
95+
console.warn(`FeatureGates: FeatureGates is not initialized. Returning default value: ${gateValue}`);
9396
} else {
9497
gateValue = FeatureGates.getExperimentValue(
95-
experimentGate.gate,
98+
experiment,
9699
experimentGate.parameter,
97100
experimentGate.defaultValue,
98101
);
@@ -101,29 +104,24 @@ export class FeatureFlagClient {
101104
return gateValue;
102105
}
103106

104-
public static async evaluateFeatures() {
105-
const featureFlags = await Promise.all(
106-
Object.values(Features).map(async (feature) => {
107-
return {
108-
// eslint-disable-next-line @typescript-eslint/await-thenable
109-
[feature]: await this.checkGate(feature),
110-
};
111-
}),
112-
);
113-
114-
return featureFlags.reduce((acc, val) => ({ ...acc, ...val }), {});
107+
private static evaluateFeatures(): FeatureGateValues {
108+
const featureFlags = Object.values(Features).map(async (feature) => {
109+
return {
110+
[feature]: this.checkGate(feature),
111+
};
112+
});
113+
114+
return featureFlags.reduce((acc, val) => ({ ...acc, ...val }), {}) as FeatureGateValues;
115115
}
116116

117-
public static async evaluateExperiments() {
118-
const experimentGates = await Promise.all(
119-
Object.values(Experiments).map(async (experiment) => {
120-
return {
121-
[experiment]: await this.checkExperimentValue(experiment),
122-
};
123-
}),
124-
);
117+
private static evaluateExperiments(): ExperimentGateValues {
118+
const experimentGates = Object.values(Experiments).map(async (experiment) => {
119+
return {
120+
[experiment]: this.checkExperimentValue(experiment),
121+
};
122+
});
125123

126-
return experimentGates.reduce((acc, val) => ({ ...acc, ...val }), {});
124+
return experimentGates.reduce((acc, val) => ({ ...acc, ...val }), {}) as ExperimentGateValues;
127125
}
128126

129127
static dispose() {

‎src/util/featureFlags/features.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,18 @@ export enum Experiments {
1010

1111
export const ExperimentGates: ExperimentGate = {
1212
[Experiments.NewAuthUI]: {
13-
gate: 'atlascode_new_auth_ui',
1413
parameter: 'isEnabled',
1514
defaultValue: false,
1615
},
1716
[Experiments.AtlascodeAA]: {
18-
gate: 'atlascode_aa_experiment',
1917
parameter: 'isEnabled',
2018
defaultValue: false,
2119
},
2220
};
2321

24-
type ExperimentPayload = { gate: string; parameter: string; defaultValue: any };
25-
type ExperimentGate = Record<string, ExperimentPayload>;
22+
type ExperimentPayload = { parameter: string; defaultValue: any };
23+
type ExperimentGate = Record<Experiments, ExperimentPayload>;
2624

27-
export type FeatureGateValues = Record<string, boolean>;
25+
export type FeatureGateValues = Record<Features, boolean>;
2826

29-
export type ExperimentGateValues = Record<string, any>;
27+
export type ExperimentGateValues = Record<Experiments, any>;

0 commit comments

Comments
 (0)
Failed to load comments.