Skip to content

Commit 4858504

Browse files
Add feature filter usage tracing (#108)
* wip * WIP * add integration test * fix lint * update port * update port * update * update * avoid create new featureFlagTracing object when request tracing is disabled * update * update * add variant and telemetry tracing
1 parent ad0290b commit 4858504

File tree

7 files changed

+351
-27
lines changed

7 files changed

+351
-27
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
99
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js";
1010
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js";
1111
import { Disposable } from "./common/disposable.js";
12-
import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME } from "./featureManagement/constants.js";
12+
import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME, TELEMETRY_KEY_NAME, VARIANTS_KEY_NAME, ALLOCATION_KEY_NAME, SEED_KEY_NAME, NAME_KEY_NAME, ENABLED_KEY_NAME } from "./featureManagement/constants.js";
1313
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
1414
import { RefreshTimer } from "./refresh/RefreshTimer.js";
1515
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
16+
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
1617
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
1718

1819
type PagedSettingSelector = SettingSelector & {
@@ -38,6 +39,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
3839
#client: AppConfigurationClient;
3940
#options: AzureAppConfigurationOptions | undefined;
4041
#isInitialLoadCompleted: boolean = false;
42+
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
4143

4244
// Refresh
4345
#refreshInProgress: boolean = false;
@@ -66,6 +68,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
6668

6769
// Enable request tracing if not opt-out
6870
this.#requestTracingEnabled = requestTracingEnabled();
71+
if (this.#requestTracingEnabled) {
72+
this.#featureFlagTracing = new FeatureFlagTracingOptions();
73+
}
6974

7075
if (options?.trimKeyPrefixes) {
7176
this.#sortedTrimKeyPrefixes = [...options.trimKeyPrefixes].sort((a, b) => b.localeCompare(a));
@@ -175,7 +180,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
175180
return {
176181
requestTracingEnabled: this.#requestTracingEnabled,
177182
initialLoadCompleted: this.#isInitialLoadCompleted,
178-
appConfigOptions: this.#options
183+
appConfigOptions: this.#options,
184+
featureFlagTracingOptions: this.#featureFlagTracing
179185
};
180186
}
181187

@@ -257,8 +263,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
257263
}
258264

259265
async #loadFeatureFlags() {
260-
// Temporary map to store feature flags, key is the key of the setting, value is the raw value of the setting
261-
const featureFlagsMap = new Map<string, any>();
266+
const featureFlagSettings: ConfigurationSetting[] = [];
262267
for (const selector of this.#featureFlagSelectors) {
263268
const listOptions: ListConfigurationSettingsOptions = {
264269
keyFilter: `${featureFlagPrefix}${selector.keyFilter}`,
@@ -275,15 +280,21 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
275280
pageEtags.push(page.etag ?? "");
276281
for (const setting of page.items) {
277282
if (isFeatureFlag(setting)) {
278-
featureFlagsMap.set(setting.key, setting.value);
283+
featureFlagSettings.push(setting);
279284
}
280285
}
281286
}
282287
selector.pageEtags = pageEtags;
283288
}
284289

290+
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
291+
this.#featureFlagTracing.resetFeatureFlagTracing();
292+
}
293+
285294
// parse feature flags
286-
const featureFlags = Array.from(featureFlagsMap.values()).map(rawFlag => JSON.parse(rawFlag));
295+
const featureFlags = await Promise.all(
296+
featureFlagSettings.map(setting => this.#parseFeatureFlag(setting))
297+
);
287298

288299
// feature_management is a reserved key, and feature_flags is an array of feature flags
289300
this.#configMap.set(FEATURE_MANAGEMENT_KEY_NAME, { [FEATURE_FLAGS_KEY_NAME]: featureFlags });
@@ -546,6 +557,33 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
546557
}
547558
return response;
548559
}
560+
561+
async #parseFeatureFlag(setting: ConfigurationSetting<string>): Promise<any> {
562+
const rawFlag = setting.value;
563+
if (rawFlag === undefined) {
564+
throw new Error("The value of configuration setting cannot be undefined.");
565+
}
566+
const featureFlag = JSON.parse(rawFlag);
567+
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
568+
if (featureFlag[CONDITIONS_KEY_NAME] &&
569+
featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] &&
570+
Array.isArray(featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME])) {
571+
for (const filter of featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME]) {
572+
this.#featureFlagTracing.updateFeatureFilterTracing(filter[NAME_KEY_NAME]);
573+
}
574+
}
575+
if (featureFlag[VARIANTS_KEY_NAME] && Array.isArray(featureFlag[VARIANTS_KEY_NAME])) {
576+
this.#featureFlagTracing.notifyMaxVariants(featureFlag[VARIANTS_KEY_NAME].length);
577+
}
578+
if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME]) {
579+
this.#featureFlagTracing.usesTelemetry = true;
580+
}
581+
if (featureFlag[ALLOCATION_KEY_NAME] && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME]) {
582+
this.#featureFlagTracing.usesSeed = true;
583+
}
584+
}
585+
return featureFlag;
586+
}
549587
}
550588

551589
function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {

src/featureManagement/constants.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,15 @@
22
// Licensed under the MIT license.
33

44
export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management";
5-
export const FEATURE_FLAGS_KEY_NAME = "feature_flags";
5+
export const FEATURE_FLAGS_KEY_NAME = "feature_flags";
6+
export const CONDITIONS_KEY_NAME = "conditions";
7+
export const CLIENT_FILTERS_KEY_NAME = "client_filters";
8+
export const TELEMETRY_KEY_NAME = "telemetry";
9+
export const VARIANTS_KEY_NAME = "variants";
10+
export const ALLOCATION_KEY_NAME = "allocation";
11+
export const SEED_KEY_NAME = "seed";
12+
export const NAME_KEY_NAME = "name";
13+
export const ENABLED_KEY_NAME = "enabled";
14+
15+
export const TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "Microsoft.TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindowFilter"];
16+
export const TARGETING_FILTER_NAMES = ["Targeting", "Microsoft.Targeting", "TargetingFilter", "Microsoft.TargetingFilter"];
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES } from "../featureManagement/constants.js";
5+
import { CUSTOM_FILTER_KEY, TIME_WINDOW_FILTER_KEY, TARGETING_FILTER_KEY, FF_SEED_USED_TAG, FF_TELEMETRY_USED_TAG, DELIMITER } from "./constants.js";
6+
7+
/**
8+
* Tracing for tracking feature flag usage.
9+
*/
10+
export class FeatureFlagTracingOptions {
11+
/**
12+
* Built-in feature filter usage.
13+
*/
14+
usesCustomFilter: boolean = false;
15+
usesTimeWindowFilter: boolean = false;
16+
usesTargetingFilter: boolean = false;
17+
usesTelemetry: boolean = false;
18+
usesSeed: boolean = false;
19+
maxVariants: number = 0;
20+
21+
resetFeatureFlagTracing(): void {
22+
this.usesCustomFilter = false;
23+
this.usesTimeWindowFilter = false;
24+
this.usesTargetingFilter = false;
25+
this.usesTelemetry = false;
26+
this.usesSeed = false;
27+
this.maxVariants = 0;
28+
}
29+
30+
updateFeatureFilterTracing(filterName: string): void {
31+
if (TIME_WINDOW_FILTER_NAMES.some(name => name === filterName)) {
32+
this.usesTimeWindowFilter = true;
33+
} else if (TARGETING_FILTER_NAMES.some(name => name === filterName)) {
34+
this.usesTargetingFilter = true;
35+
} else {
36+
this.usesCustomFilter = true;
37+
}
38+
}
39+
40+
notifyMaxVariants(currentFFTotalVariants: number): void {
41+
if (currentFFTotalVariants > this.maxVariants) {
42+
this.maxVariants = currentFFTotalVariants;
43+
}
44+
}
45+
46+
usesAnyFeatureFilter(): boolean {
47+
return this.usesCustomFilter || this.usesTimeWindowFilter || this.usesTargetingFilter;
48+
}
49+
50+
usesAnyTracingFeature() {
51+
return this.usesSeed || this.usesTelemetry;
52+
}
53+
54+
createFeatureFiltersString(): string {
55+
if (!this.usesAnyFeatureFilter()) {
56+
return "";
57+
}
58+
59+
let result: string = "";
60+
if (this.usesCustomFilter) {
61+
result += CUSTOM_FILTER_KEY;
62+
}
63+
if (this.usesTimeWindowFilter) {
64+
if (result !== "") {
65+
result += DELIMITER;
66+
}
67+
result += TIME_WINDOW_FILTER_KEY;
68+
}
69+
if (this.usesTargetingFilter) {
70+
if (result !== "") {
71+
result += DELIMITER;
72+
}
73+
result += TARGETING_FILTER_KEY;
74+
}
75+
return result;
76+
}
77+
78+
createFeaturesString(): string {
79+
if (!this.usesAnyTracingFeature()) {
80+
return "";
81+
}
82+
83+
let result: string = "";
84+
if (this.usesSeed) {
85+
result += FF_SEED_USED_TAG;
86+
}
87+
if (this.usesTelemetry) {
88+
if (result !== "") {
89+
result += DELIMITER;
90+
}
91+
result += FF_TELEMETRY_USED_TAG;
92+
}
93+
return result;
94+
}
95+
}

src/requestTracing/constants.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const CONTAINER_APP_ENV_VAR = "CONTAINER_APP_NAME";
3737
export const KUBERNETES_ENV_VAR = "KUBERNETES_PORT";
3838
export const SERVICE_FABRIC_ENV_VAR = "Fabric_NodeName"; // See: https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-environment-variables-reference
3939

40-
// Request Type
40+
// Request type
4141
export const REQUEST_TYPE_KEY = "RequestType";
4242
export enum RequestType {
4343
STARTUP = "Startup",
@@ -46,3 +46,16 @@ export enum RequestType {
4646

4747
// Tag names
4848
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
49+
50+
// Feature flag usage tracing
51+
export const FEATURE_FILTER_TYPE_KEY = "Filter";
52+
export const CUSTOM_FILTER_KEY = "CSTM";
53+
export const TIME_WINDOW_FILTER_KEY = "TIME";
54+
export const TARGETING_FILTER_KEY = "TRGT";
55+
56+
export const FF_TELEMETRY_USED_TAG = "Telemetry";
57+
export const FF_MAX_VARIANTS_KEY = "MaxVariants";
58+
export const FF_SEED_USED_TAG = "Seed";
59+
export const FF_FEATURES_KEY = "FFFeatures";
60+
61+
export const DELIMITER = "+";

src/requestTracing/utils.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33

44
import { AppConfigurationClient, ConfigurationSettingId, GetConfigurationSettingOptions, ListConfigurationSettingsOptions } from "@azure/app-configuration";
55
import { AzureAppConfigurationOptions } from "../AzureAppConfigurationOptions.js";
6+
import { FeatureFlagTracingOptions } from "./FeatureFlagTracingOptions.js";
67
import {
78
AZURE_FUNCTION_ENV_VAR,
89
AZURE_WEB_APP_ENV_VAR,
910
CONTAINER_APP_ENV_VAR,
1011
DEV_ENV_VAL,
1112
ENV_AZURE_APP_CONFIGURATION_TRACING_DISABLED,
1213
ENV_KEY,
14+
FEATURE_FILTER_TYPE_KEY,
15+
FF_MAX_VARIANTS_KEY,
16+
FF_FEATURES_KEY,
1317
HOST_TYPE_KEY,
1418
HostType,
1519
KEY_VAULT_CONFIGURED_TAG,
@@ -28,17 +32,18 @@ export function listConfigurationSettingsWithTrace(
2832
requestTracingEnabled: boolean;
2933
initialLoadCompleted: boolean;
3034
appConfigOptions: AzureAppConfigurationOptions | undefined;
35+
featureFlagTracingOptions: FeatureFlagTracingOptions | undefined;
3136
},
3237
client: AppConfigurationClient,
3338
listOptions: ListConfigurationSettingsOptions
3439
) {
35-
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions;
40+
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, featureFlagTracingOptions } = requestTracingOptions;
3641

3742
const actualListOptions = { ...listOptions };
3843
if (requestTracingEnabled) {
3944
actualListOptions.requestOptions = {
4045
customHeaders: {
41-
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted)
46+
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, featureFlagTracingOptions, initialLoadCompleted)
4247
}
4348
};
4449
}
@@ -51,26 +56,27 @@ export function getConfigurationSettingWithTrace(
5156
requestTracingEnabled: boolean;
5257
initialLoadCompleted: boolean;
5358
appConfigOptions: AzureAppConfigurationOptions | undefined;
59+
featureFlagTracingOptions: FeatureFlagTracingOptions | undefined;
5460
},
5561
client: AppConfigurationClient,
5662
configurationSettingId: ConfigurationSettingId,
5763
getOptions?: GetConfigurationSettingOptions,
5864
) {
59-
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions } = requestTracingOptions;
65+
const { requestTracingEnabled, initialLoadCompleted, appConfigOptions, featureFlagTracingOptions } = requestTracingOptions;
6066
const actualGetOptions = { ...getOptions };
6167

6268
if (requestTracingEnabled) {
6369
actualGetOptions.requestOptions = {
6470
customHeaders: {
65-
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, initialLoadCompleted)
71+
[CORRELATION_CONTEXT_HEADER_NAME]: createCorrelationContextHeader(appConfigOptions, featureFlagTracingOptions, initialLoadCompleted)
6672
}
6773
};
6874
}
6975

7076
return client.getConfigurationSetting(configurationSettingId, actualGetOptions);
7177
}
7278

73-
export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, isInitialLoadCompleted: boolean): string {
79+
export function createCorrelationContextHeader(options: AzureAppConfigurationOptions | undefined, featureFlagTracing: FeatureFlagTracingOptions | undefined, isInitialLoadCompleted: boolean): string {
7480
/*
7581
RequestType: 'Startup' during application starting up, 'Watch' after startup completed.
7682
Host: identify with defined envs
@@ -82,6 +88,14 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt
8288
keyValues.set(HOST_TYPE_KEY, getHostType());
8389
keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined);
8490

91+
if (featureFlagTracing) {
92+
keyValues.set(FEATURE_FILTER_TYPE_KEY, featureFlagTracing.usesAnyFeatureFilter() ? featureFlagTracing.createFeatureFiltersString() : undefined);
93+
keyValues.set(FF_FEATURES_KEY, featureFlagTracing.usesAnyTracingFeature() ? featureFlagTracing.createFeaturesString() : undefined);
94+
if (featureFlagTracing.maxVariants > 0) {
95+
keyValues.set(FF_MAX_VARIANTS_KEY, featureFlagTracing.maxVariants.toString());
96+
}
97+
}
98+
8599
const tags: string[] = [];
86100
if (options?.keyVaultOptions) {
87101
const { credential, secretClients, secretResolver } = options.keyVaultOptions;

0 commit comments

Comments
 (0)