Track outcomes of querying FFs with getValue

This commit is contained in:
Michael B. Gale
2026-01-28 12:03:10 +00:00
parent b126facd4e
commit 9d2031faa4
8 changed files with 117 additions and 1 deletions
+11
View File
@@ -90761,6 +90761,12 @@ var Features = class {
);
}
gitHubFeatureFlags;
// Tracks features that have been queried at some point and the outcome.
queriedFeatures = {};
/** Gets a record of features that were queried and the corresponding outcomes. */
getQueriedFeatures() {
return this.queriedFeatures;
}
async getDefaultCliVersion(variant) {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
@@ -90777,6 +90783,11 @@ var Features = class {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature, codeql) {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}
async getValueInternal(feature, codeql) {
const config = featureConfig[feature];
if (!codeql && config.minimumVersion) {
throw new Error(
+11
View File
@@ -87094,6 +87094,12 @@ var Features = class {
);
}
gitHubFeatureFlags;
// Tracks features that have been queried at some point and the outcome.
queriedFeatures = {};
/** Gets a record of features that were queried and the corresponding outcomes. */
getQueriedFeatures() {
return this.queriedFeatures;
}
async getDefaultCliVersion(variant) {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
@@ -87110,6 +87116,11 @@ var Features = class {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature, codeql) {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}
async getValueInternal(feature, codeql) {
const config = featureConfig[feature];
if (!codeql && config.minimumVersion) {
throw new Error(
+11
View File
@@ -131403,6 +131403,12 @@ var Features = class {
);
}
gitHubFeatureFlags;
// Tracks features that have been queried at some point and the outcome.
queriedFeatures = {};
/** Gets a record of features that were queried and the corresponding outcomes. */
getQueriedFeatures() {
return this.queriedFeatures;
}
async getDefaultCliVersion(variant) {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
@@ -131419,6 +131425,11 @@ var Features = class {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature, codeql) {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}
async getValueInternal(feature, codeql) {
const config = featureConfig[feature];
if (!codeql && config.minimumVersion) {
throw new Error(
+11
View File
@@ -88252,6 +88252,12 @@ var Features = class {
);
}
gitHubFeatureFlags;
// Tracks features that have been queried at some point and the outcome.
queriedFeatures = {};
/** Gets a record of features that were queried and the corresponding outcomes. */
getQueriedFeatures() {
return this.queriedFeatures;
}
async getDefaultCliVersion(variant) {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
@@ -88268,6 +88274,11 @@ var Features = class {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature, codeql) {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}
async getValueInternal(feature, codeql) {
const config = featureConfig[feature];
if (!codeql && config.minimumVersion) {
throw new Error(
+11
View File
@@ -86995,6 +86995,12 @@ var Features = class {
);
}
gitHubFeatureFlags;
// Tracks features that have been queried at some point and the outcome.
queriedFeatures = {};
/** Gets a record of features that were queried and the corresponding outcomes. */
getQueriedFeatures() {
return this.queriedFeatures;
}
async getDefaultCliVersion(variant) {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
@@ -87011,6 +87017,11 @@ var Features = class {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature, codeql) {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}
async getValueInternal(feature, codeql) {
const config = featureConfig[feature];
if (!codeql && config.minimumVersion) {
throw new Error(
+11
View File
@@ -89949,6 +89949,12 @@ var Features = class {
);
}
gitHubFeatureFlags;
// Tracks features that have been queried at some point and the outcome.
queriedFeatures = {};
/** Gets a record of features that were queried and the corresponding outcomes. */
getQueriedFeatures() {
return this.queriedFeatures;
}
async getDefaultCliVersion(variant) {
return await this.gitHubFeatureFlags.getDefaultCliVersion(variant);
}
@@ -89965,6 +89971,11 @@ var Features = class {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature, codeql) {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}
async getValueInternal(feature, codeql) {
const config = featureConfig[feature];
if (!codeql && config.minimumVersion) {
throw new Error(
+26 -1
View File
@@ -92,6 +92,31 @@ test(`Feature flags are requested in GHEC-DR`, async (t) => {
});
});
test("Queried feature flags are recorded", async (t) => {
await withTmpDir(async (tmpDir) => {
const loggedMessages = [];
const features = setUpFeatureFlagTests(
tmpDir,
getRecordingLogger(loggedMessages),
{ type: GitHubVariant.DOTCOM },
);
mockFeatureFlagApiEndpoint(200, initializeFeatures(true));
// No features should have been queried initially.
t.is(Object.keys(features.getQueriedFeatures()).length, 0);
// Query all features.
const allFeatures = Object.values(Feature);
for (const feature of allFeatures) {
await getFeatureIncludingCodeQlIfRequired(features, feature);
}
// All features should have a been queried.
t.is(Object.keys(features.getQueriedFeatures()).length, allFeatures.length);
});
});
test("API response missing and features use default value", async (t) => {
await withTmpDir(async (tmpDir) => {
const loggedMessages: LoggedMessage[] = [];
@@ -562,7 +587,7 @@ function setUpFeatureFlagTests(
tmpDir: string,
logger = getRunnerLogger(true),
gitHubVersion = { type: GitHubVariant.DOTCOM } as util.GitHubVersion,
): FeatureEnablement {
): Features {
setupActionsVars(tmpDir, tmpDir);
return new Features(gitHubVersion, testRepositoryNwo, tmpDir, logger);
+25
View File
@@ -345,6 +345,14 @@ export interface FeatureEnablement {
*/
type GitHubFeatureFlagsApiResponse = Partial<Record<Feature, boolean>>;
// Even though we are currently only tracking the value of queried features, we use an object
// here rather than just a boolean to keep open the possibility of also tracking the reason
// for why a particular value was resolved (i.e. because of an environment variable or API result)
// in the future.
export interface QueriedFeatureStatus {
value: boolean;
}
export const FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json";
/**
@@ -355,6 +363,9 @@ export const FEATURE_FLAGS_FILE_NAME = "cached-feature-flags.json";
export class Features implements FeatureEnablement {
private gitHubFeatureFlags: GitHubFeatureFlags;
// Tracks features that have been queried at some point and the outcome.
private queriedFeatures: Partial<Record<Feature, QueriedFeatureStatus>> = {};
constructor(
gitHubVersion: util.GitHubVersion,
repositoryNwo: RepositoryNwo,
@@ -369,6 +380,11 @@ export class Features implements FeatureEnablement {
);
}
/** Gets a record of features that were queried and the corresponding outcomes. */
public getQueriedFeatures() {
return this.queriedFeatures;
}
async getDefaultCliVersion(
variant: util.GitHubVariant,
): Promise<CodeQLDefaultVersionInfo> {
@@ -388,6 +404,15 @@ export class Features implements FeatureEnablement {
* @throws if a `minimumVersion` is specified for the feature, and `codeql` is not provided.
*/
async getValue(feature: Feature, codeql?: CodeQL): Promise<boolean> {
const value = await this.getValueInternal(feature, codeql);
this.queriedFeatures[feature] = { value };
return value;
}
private async getValueInternal(
feature: Feature,
codeql?: CodeQL,
): Promise<boolean> {
// Narrow the type to FeatureConfig to avoid type errors. To avoid unsafe use of `as`, we
// check that the required properties exist using `satisfies`.
const config = featureConfig[