Skip to content

Commit 0f2f28f

Browse files
authoredMar 24, 2025··
Add feature gates and experiment exposure telemetry events (#221)
1 parent 1428e50 commit 0f2f28f

File tree

4 files changed

+95
-11
lines changed

4 files changed

+95
-11
lines changed
 

‎src/analytics.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,27 @@ export async function featureFlagClientInitializedEvent(success: true): Promise<
9191
export async function featureFlagClientInitializedEvent(
9292
success: false,
9393
errorType: ClientInitializedErrorType,
94+
reason: string,
9495
): Promise<TrackEvent>;
9596
export async function featureFlagClientInitializedEvent(
9697
success: boolean,
9798
errorType?: ClientInitializedErrorType,
99+
reason?: string,
98100
): Promise<TrackEvent> {
99101
return trackEvent('initialized', 'featureFlagClient', {
100-
attributes: { success, errorType: errorType ?? 0 },
102+
attributes: { success, errorType: errorType ?? 0, reason },
103+
});
104+
}
105+
106+
// debugging event, meant to measure the exposure rate of a feature flag or an experiment
107+
export async function featureGateExposureBoolEvent(
108+
ffName: string,
109+
success: boolean,
110+
value: boolean,
111+
errorType: number,
112+
): Promise<TrackEvent> {
113+
return trackEvent('gateExposureBool', 'featureFlagClient', {
114+
attributes: { ffName, success, value, errorType },
101115
});
102116
}
103117

‎src/container.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -207,25 +207,23 @@ export class Container {
207207
} catch (err) {
208208
const error = err as FeatureFlagClientInitError;
209209
Logger.debug(`FeatureFlagClient: Failed to initialize the client: ${error.reason}`);
210-
featureFlagClientInitializedEvent(false, error.errorType).then((e) => {
210+
featureFlagClientInitializedEvent(false, error.errorType, error.reason).then((e) => {
211211
this.analyticsClient.sendTrackEvent(e);
212212
});
213213
}
214214

215+
FeatureFlagClient.checkExperimentBooleanValueWithInstrumentation(Experiments.AtlascodeAA);
216+
FeatureFlagClient.checkGateValueWithInstrumentation(Features.NoOpFeature);
217+
215218
this.initializeUriHandler(context, this._analyticsApi, this._bitbucketHelper);
216219
this.initializeNewSidebarView(context, config);
217-
this.initializeAAExperiment();
218220
}
219221

220222
private static getAnalyticsEnable(): boolean {
221223
const telemetryConfig = workspace.getConfiguration('telemetry');
222224
return telemetryConfig.get<boolean>('enableTelemetry', true);
223225
}
224226

225-
private static initializeAAExperiment() {
226-
FeatureFlagClient.checkExperimentValue(Experiments.AtlascodeAA);
227-
}
228-
229227
private static initializeUriHandler(
230228
context: ExtensionContext,
231229
analyticsApi: VSCAnalyticsApi,

‎src/util/featureFlags/client.ts

+75-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import FeatureGates, { FeatureGateEnvironment, Identifiers } from '@atlaskit/fea
22
import { AnalyticsClient } from '../../analytics-node-client/src/client.min';
33
import { AnalyticsClientMapper } from './analytics';
44
import { ExperimentGates, ExperimentGateValues, Experiments, FeatureGateValues, Features } from './features';
5-
import { ClientInitializedErrorType } from '../../analytics';
5+
import { ClientInitializedErrorType, featureGateExposureBoolEvent } from '../../analytics';
66
import { Logger } from '../../logger';
77

88
export type FeatureFlagClientOptions = {
@@ -18,7 +18,8 @@ export class FeatureFlagClientInitError {
1818
}
1919

2020
export abstract class FeatureFlagClient {
21-
private static analyticsClient: AnalyticsClientMapper;
21+
private static analyticsClient: AnalyticsClient;
22+
private static analyticsClientMapper: AnalyticsClientMapper;
2223

2324
private static featureGateOverrides: FeatureGateValues;
2425
private static experimentValueOverride: ExperimentGateValues;
@@ -44,7 +45,9 @@ export abstract class FeatureFlagClient {
4445
}
4546

4647
Logger.debug(`FeatureGates: initializing, target: ${targetApp}, environment: ${environment}`);
47-
this.analyticsClient = new AnalyticsClientMapper(options.analyticsClient, options.identifiers);
48+
49+
this.analyticsClient = options.analyticsClient;
50+
this.analyticsClientMapper = new AnalyticsClientMapper(options.analyticsClient, options.identifiers);
4851

4952
try {
5053
await FeatureGates.initialize(
@@ -53,7 +56,7 @@ export abstract class FeatureFlagClient {
5356
environment,
5457
targetApp,
5558
fetchTimeoutMs: Number.parseInt(timeout),
56-
analyticsWebClient: Promise.resolve(this.analyticsClient),
59+
analyticsWebClient: Promise.resolve(this.analyticsClientMapper),
5760
},
5861
options.identifiers,
5962
);
@@ -159,6 +162,74 @@ export abstract class FeatureFlagClient {
159162
return gateValue;
160163
}
161164

165+
static checkGateValueWithInstrumentation(gate: Features): any {
166+
if (this.featureGateOverrides.hasOwnProperty(gate)) {
167+
const value = this.featureGateOverrides[gate];
168+
featureGateExposureBoolEvent(gate, false, value, 3).then((e) => {
169+
this.analyticsClient.sendTrackEvent(e);
170+
});
171+
return value;
172+
}
173+
174+
let gateValue = false;
175+
if (FeatureGates.initializeCompleted()) {
176+
// FeatureGates.checkGate returns false if any errors
177+
gateValue = FeatureGates.checkGate(gate);
178+
featureGateExposureBoolEvent(gate, true, gateValue, 0).then((e) => {
179+
this.analyticsClient.sendTrackEvent(e);
180+
});
181+
} else {
182+
featureGateExposureBoolEvent(gate, false, gateValue, 1).then((e) => {
183+
this.analyticsClient.sendTrackEvent(e);
184+
});
185+
}
186+
187+
Logger.debug(`FeatureGates ${gate} -> ${gateValue}`);
188+
return gateValue;
189+
}
190+
191+
static checkExperimentBooleanValueWithInstrumentation(experiment: Experiments): any {
192+
// unknown experiment name
193+
if (!ExperimentGates.hasOwnProperty(experiment)) {
194+
featureGateExposureBoolEvent(experiment, false, false, 2).then((e) => {
195+
this.analyticsClient.sendTrackEvent(e);
196+
});
197+
return undefined;
198+
}
199+
200+
if (this.experimentValueOverride.hasOwnProperty(experiment)) {
201+
const value = this.experimentValueOverride[experiment];
202+
featureGateExposureBoolEvent(experiment, false, value, 3).then((e) => {
203+
this.analyticsClient.sendTrackEvent(e);
204+
});
205+
return value;
206+
}
207+
208+
const experimentGate = ExperimentGates[experiment];
209+
let gateValue = experimentGate.defaultValue;
210+
if (FeatureGates.initializeCompleted()) {
211+
gateValue = FeatureGates.getExperimentValue(experiment, experimentGate.parameter, 'N/A');
212+
213+
if (gateValue === 'N/A') {
214+
gateValue = experimentGate.defaultValue;
215+
featureGateExposureBoolEvent(experiment, false, gateValue, 4).then((e) => {
216+
this.analyticsClient.sendTrackEvent(e);
217+
});
218+
} else {
219+
featureGateExposureBoolEvent(experiment, true, gateValue, 0).then((e) => {
220+
this.analyticsClient.sendTrackEvent(e);
221+
});
222+
}
223+
} else {
224+
featureGateExposureBoolEvent(experiment, false, gateValue, 1).then((e) => {
225+
this.analyticsClient.sendTrackEvent(e);
226+
});
227+
}
228+
229+
Logger.debug(`Experiment ${experiment} -> ${gateValue}`);
230+
return gateValue;
231+
}
232+
162233
static dispose() {
163234
FeatureGates.shutdownStatsig();
164235
}

‎src/util/featureFlags/features.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const enum Features {
22
EnableNewUriHandler = 'atlascode-enable-new-uri-handler',
33
NewSidebarTreeView = 'atlascode-new-sidebar-treeview',
4+
NoOpFeature = 'atlascode-noop',
45
}
56

67
export const enum Experiments {

0 commit comments

Comments
 (0)
Please sign in to comment.