Skip to content

Commit bf6eacc

Browse files
committed
Streamlines feature flag handling in various components
(#5092, gitkraken/vscode-gitlens-private#78)
1 parent 8e28fa7 commit bf6eacc

4 files changed

Lines changed: 62 additions & 73 deletions

File tree

src/constants.storage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { SubscriptionState } from './constants.subscription.js';
66
import type { TrackedUsage, TrackedUsageKeys } from './constants.telemetry.js';
77
import type { GroupableTreeViewTypes, TreeViewTypes } from './constants.views.js';
88
import type { Environment } from './container.js';
9+
import type { FeatureFlagMap } from './featureFlags/featureFlagService.js';
910
import type { FeaturePreviews } from './features.js';
1011
import type { OrganizationSettings } from './plus/gk/models/organization.js';
1112
import type { PaidSubscriptionPlanIds, Subscription } from './plus/gk/models/subscription.js';
@@ -95,6 +96,7 @@ interface GlobalStorageCore {
9596
'graph:useNaturalLanguageSearch': boolean;
9697
'views:scm:grouped:welcome:dismissed': boolean;
9798
'integrations:configured': StoredIntegrationConfigurations;
99+
'featureFlags:flags': FeatureFlagMap;
98100
}
99101

100102
type GlobalStorageDynamic = Record<`plus:preview:${FeaturePreviews}:usages`, StoredFeaturePreviewUsagePeriod[]> &

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ async function setFeatureFlagTelemetryGlobalAttributes(container: Container): Pr
312312
const featureFlags = await container.featureFlags;
313313
if (featureFlags == null) return;
314314

315-
const flags = await featureFlags.getAllFlags();
315+
const flags = featureFlags.getAllFlags();
316316
if (Object.keys(flags).length === 0) return;
317317

318318
container.telemetry.setGlobalAttribute(

src/featureFlags/featureFlagService.ts

Lines changed: 58 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { deserializeConfig, IConfigCatCache, IConfigCatClient, SettingTypeOf } from '@configcat/sdk';
1+
import type { deserializeConfig, IConfigCatCache, IConfigCatClient } from '@configcat/sdk';
22
import type * as FeatureFlagProjectConfigModule from '@configcat/sdk/lib/esm/ProjectConfig.js';
33
import { env as vscodeEnv } from 'vscode';
44
import { fetch } from '@env/fetch.js';
@@ -13,11 +13,11 @@ export type FeatureFlagValue = boolean | string | number;
1313
export enum FeatureFlagKey {
1414
WelcomeTitle = 'glensWelcomeTitle',
1515
}
16-
export type FeatureFlagMap = Partial<Record<FeatureFlagKey, FeatureFlagValue>>;
16+
export type FeatureFlagMap = Readonly<Partial<Record<FeatureFlagKey, FeatureFlagValue>>>;
1717
export interface FeatureFlagService {
1818
dispose(): void;
19-
getFlag<T extends FeatureFlagValue>(key: FeatureFlagKey, defaultValue: T): Promise<FeatureFlagValue>;
20-
getAllFlags(): Promise<FeatureFlagMap>;
19+
getFlag<T extends FeatureFlagValue>(key: FeatureFlagKey, defaultValue: T): T;
20+
getAllFlags(): FeatureFlagMap;
2121
}
2222

2323
const _featureFlagKeys: ReadonlySet<string> = new Set<FeatureFlagKey>(Object.values(FeatureFlagKey));
@@ -50,95 +50,64 @@ class PrefetchedConfigCache implements IConfigCatCache {
5050
}
5151

5252
export class ConfigCatFeatureFlagService implements FeatureFlagService {
53-
private readonly _client: Promise<IConfigCatClient | undefined>;
53+
private readonly _flags: FeatureFlagMap;
5454

5555
constructor(private readonly container: Container) {
56-
this._client = this.loadClient();
57-
}
56+
this._flags = Object.freeze(this.container.storage.get('featureFlags:flags') ?? {});
5857

59-
dispose(): void {
60-
void this._client.then(
61-
client => {
62-
client?.dispose();
63-
},
64-
() => {},
65-
);
58+
// Fire background fetch to evaluate flags and store them for the NEXT activation
59+
void this.fetchAndCacheFlags();
6660
}
6761

68-
async getFlag<T extends FeatureFlagValue>(key: FeatureFlagKey, defaultValue: T): Promise<SettingTypeOf<T> | T> {
69-
using scope = maybeStartScopedLogger(`${getLoggableName(this)}.getFlag`);
70-
71-
const client = await this._client;
72-
if (client == null) return defaultValue;
62+
dispose(): void {}
7363

74-
try {
75-
const details = await client.getValueDetailsAsync<T>(key, defaultValue);
76-
return details.value;
77-
} catch (ex) {
78-
Logger.debug(ex, scope, `Failed to evaluate feature flag '${key}'; return default value`);
79-
return defaultValue;
80-
}
64+
getFlag<T extends FeatureFlagValue>(key: FeatureFlagKey, defaultValue: T): T {
65+
return (this._flags[key] ?? defaultValue) as T;
8166
}
8267

83-
async getAllFlags(): Promise<FeatureFlagMap> {
84-
using scope = maybeStartScopedLogger(`${getLoggableName(this)}.getAllFlags`);
85-
86-
const client = await this._client;
87-
if (client == null) return {};
88-
89-
try {
90-
const values = await client.getAllValuesAsync();
91-
const flags: FeatureFlagMap = {};
92-
93-
for (const { settingKey, settingValue } of values) {
94-
if (
95-
isFeatureFlagKey(settingKey) &&
96-
(typeof settingValue === 'boolean' ||
97-
typeof settingValue === 'number' ||
98-
typeof settingValue === 'string')
99-
) {
100-
flags[settingKey] = settingValue;
101-
}
102-
}
103-
104-
return flags;
105-
} catch (ex) {
106-
Logger.debug(ex, scope, 'Failed to evaluate feature flags; returning no flags');
107-
return {};
108-
}
68+
getAllFlags(): FeatureFlagMap {
69+
return this._flags;
10970
}
11071

111-
private async loadClient(): Promise<IConfigCatClient | undefined> {
112-
using scope = maybeStartScopedLogger(`${getLoggableName(this)}.loadClient`);
72+
/**
73+
* Fetches fresh config from the API, evaluates all flags via ConfigCat SDK,
74+
* and stores the resolved flag map in globalState for the next activation.
75+
* Fire-and-forget — errors are logged but never propagated.
76+
*/
77+
private async fetchAndCacheFlags(): Promise<void> {
78+
using scope = maybeStartScopedLogger(`${getLoggableName(this)}.fetchAndCacheFlags`);
11379

11480
try {
11581
const response = await fetch(this.container.urls.getGkApiUrl('feature-flags', 'config'), {
11682
headers: { Accept: 'application/json' },
11783
});
11884

11985
if (!response.ok) {
120-
Logger.debug(
121-
scope,
122-
`Failed to fetch feature flags config (${response.status} ${response.statusText}); using defaults`,
123-
);
124-
return undefined;
86+
Logger.debug(scope, `Failed to fetch feature flags config (${response.status} ${response.statusText})`);
87+
return;
12588
}
12689

12790
const configJson = await response.text();
12891
if (!configJson) {
129-
Logger.debug(scope, 'Feature flags config response was empty; using defaults');
130-
return undefined;
92+
Logger.debug(scope, 'Feature flags config response was empty');
93+
return;
13194
}
13295

133-
return await this.createClient(configJson);
96+
const flags = await this.evaluateFlags(configJson);
97+
if (flags != null) {
98+
await this.container.storage.store('featureFlags:flags', flags);
99+
}
134100
} catch (ex) {
135-
Logger.debug(ex, scope, 'Failed to fetch feature flags config; using defaults');
136-
return undefined;
101+
Logger.debug(ex, scope, 'Failed to fetch and cache feature flags');
137102
}
138103
}
139104

140-
private async createClient(configJson: string): Promise<IConfigCatClient | undefined> {
141-
using scope = maybeStartScopedLogger(`${getLoggableName(this)}.createClient`);
105+
/**
106+
* Creates a temporary ConfigCat client to evaluate all flags from the given config JSON,
107+
* then disposes it immediately.
108+
*/
109+
private async evaluateFlags(configJson: string): Promise<FeatureFlagMap | undefined> {
110+
using scope = maybeStartScopedLogger(`${getLoggableName(this)}.evaluateFlags`);
142111

143112
const [sdkResult, projectConfigResult] = await Promise.allSettled([
144113
import(/* webpackChunkName: "feature-flags" */ '@configcat/sdk'),
@@ -149,26 +118,44 @@ export class ConfigCatFeatureFlagService implements FeatureFlagService {
149118
const projectConfigModule = getSettledValue(projectConfigResult);
150119

151120
if (sdk == null || projectConfigModule == null) {
152-
Logger.debug(scope, 'Failed to load ConfigCat SDK modules; using defaults');
121+
Logger.debug(scope, 'Failed to load ConfigCat SDK modules');
153122
return undefined;
154123
}
155124

125+
let client: IConfigCatClient | undefined;
156126
try {
157127
const cache = new PrefetchedConfigCache(
158128
this.serializeProjectConfig(configJson, sdk.deserializeConfig, projectConfigModule),
159129
);
160-
const client = sdk.getClient(localSdkKey, sdk.PollingMode.ManualPoll, {
130+
client = sdk.getClient(localSdkKey, sdk.PollingMode.ManualPoll, {
161131
cache: cache,
162-
defaultUser: { identifier: vscodeEnv.machineId, country: 'ES' },
132+
defaultUser: { identifier: vscodeEnv.machineId },
163133
offline: true,
164134
});
165135

166136
await client.waitForReady();
167137
await client.forceRefreshAsync();
168-
return client;
138+
139+
const values = await client.getAllValuesAsync();
140+
const flags: Partial<Record<FeatureFlagKey, FeatureFlagValue>> = {};
141+
142+
for (const { settingKey, settingValue } of values) {
143+
if (
144+
isFeatureFlagKey(settingKey) &&
145+
(typeof settingValue === 'boolean' ||
146+
typeof settingValue === 'number' ||
147+
typeof settingValue === 'string')
148+
) {
149+
flags[settingKey] = settingValue;
150+
}
151+
}
152+
153+
return flags;
169154
} catch (ex) {
170-
Logger.debug(ex, scope, 'Failed to initialize ConfigCat feature flag client; using defaults');
155+
Logger.debug(ex, scope, 'Failed to evaluate feature flags');
171156
return undefined;
157+
} finally {
158+
client?.dispose();
172159
}
173160
}
174161

src/webviews/welcome/welcomeWebview.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ export class WelcomeWebviewProvider implements WebviewProvider<State, State, Wel
110110

111111
private async getWelcomeTitleVariant(): Promise<string | undefined> {
112112
const featureFlags = await this.container.featureFlags;
113-
const showVariant = (await featureFlags?.getFlag(FeatureFlagKey.WelcomeTitle, false)) ?? false;
113+
const showVariant = featureFlags?.getFlag(FeatureFlagKey.WelcomeTitle, false) ?? false;
114114
return showVariant ? 'Welcome' : undefined;
115115
}
116116

0 commit comments

Comments
 (0)