Skip to content

Commit b276673

Browse files
add variant and telemetry tracing
1 parent fcfff03 commit b276673

File tree

6 files changed

+181
-10
lines changed

6 files changed

+181
-10
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ 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, CONDITIONS_KEY_NAME, CLIENT_FILTERS_KEY_NAME, NAME_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";
@@ -565,11 +565,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
565565
}
566566
const featureFlag = JSON.parse(rawFlag);
567567
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
568-
if (featureFlag[CONDITIONS_KEY_NAME] && featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME]) {
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])) {
569571
for (const filter of featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME]) {
570572
this.#featureFlagTracing.updateFeatureFilterTracing(filter[NAME_KEY_NAME]);
571573
}
572574
}
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+
}
573584
}
574585
return featureFlag;
575586
}

src/featureManagement/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management";
55
export const FEATURE_FLAGS_KEY_NAME = "feature_flags";
66
export const CONDITIONS_KEY_NAME = "conditions";
77
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";
812
export const NAME_KEY_NAME = "name";
13+
export const ENABLED_KEY_NAME = "enabled";
914

1015
export const TIME_WINDOW_FILTER_NAMES = ["TimeWindow", "Microsoft.TimeWindow", "TimeWindowFilter", "Microsoft.TimeWindowFilter"];
1116
export const TARGETING_FILTER_NAMES = ["Targeting", "Microsoft.Targeting", "TargetingFilter", "Microsoft.TargetingFilter"];

src/requestTracing/FeatureFlagTracingOptions.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33

4-
import { TIME_WINDOW_FILTER_NAMES, TARGETING_FILTER_NAMES } from "../featureManagement/constants";
5-
import { CUSTOM_FILTER_KEY, TIME_WINDOW_FILTER_KEY, TARGETING_FILTER_KEY, DELIMITER } from "./constants";
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";
66

77
/**
88
* Tracing for tracking feature flag usage.
@@ -14,11 +14,17 @@ export class FeatureFlagTracingOptions {
1414
usesCustomFilter: boolean = false;
1515
usesTimeWindowFilter: boolean = false;
1616
usesTargetingFilter: boolean = false;
17+
usesTelemetry: boolean = false;
18+
usesSeed: boolean = false;
19+
maxVariants: number = 0;
1720

1821
resetFeatureFlagTracing(): void {
1922
this.usesCustomFilter = false;
2023
this.usesTimeWindowFilter = false;
2124
this.usesTargetingFilter = false;
25+
this.usesTelemetry = false;
26+
this.usesSeed = false;
27+
this.maxVariants = 0;
2228
}
2329

2430
updateFeatureFilterTracing(filterName: string): void {
@@ -31,35 +37,59 @@ export class FeatureFlagTracingOptions {
3137
}
3238
}
3339

40+
notifyMaxVariants(currentFFTotalVariants: number): void {
41+
if (currentFFTotalVariants > this.maxVariants) {
42+
this.maxVariants = currentFFTotalVariants;
43+
}
44+
}
45+
3446
usesAnyFeatureFilter(): boolean {
3547
return this.usesCustomFilter || this.usesTimeWindowFilter || this.usesTargetingFilter;
3648
}
3749

50+
usesAnyTracingFeature() {
51+
return this.usesSeed || this.usesTelemetry;
52+
}
53+
3854
createFeatureFiltersString(): string {
3955
if (!this.usesAnyFeatureFilter()) {
4056
return "";
4157
}
4258

4359
let result: string = "";
44-
4560
if (this.usesCustomFilter) {
4661
result += CUSTOM_FILTER_KEY;
4762
}
48-
4963
if (this.usesTimeWindowFilter) {
5064
if (result !== "") {
5165
result += DELIMITER;
5266
}
5367
result += TIME_WINDOW_FILTER_KEY;
5468
}
55-
5669
if (this.usesTargetingFilter) {
5770
if (result !== "") {
5871
result += DELIMITER;
5972
}
6073
result += TARGETING_FILTER_KEY;
6174
}
75+
return result;
76+
}
6277

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+
}
6393
return result;
6494
}
6595
}

src/requestTracing/constants.ts

Lines changed: 7 additions & 2 deletions
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",
@@ -47,10 +47,15 @@ export enum RequestType {
4747
// Tag names
4848
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
4949

50-
// Feature Flag Usage Tracing
50+
// Feature flag usage tracing
5151
export const FEATURE_FILTER_TYPE_KEY = "Filter";
5252
export const CUSTOM_FILTER_KEY = "CSTM";
5353
export const TIME_WINDOW_FILTER_KEY = "TIME";
5454
export const TARGETING_FILTER_KEY = "TRGT";
5555

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+
5661
export const DELIMITER = "+";

src/requestTracing/utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
ENV_AZURE_APP_CONFIGURATION_TRACING_DISABLED,
1313
ENV_KEY,
1414
FEATURE_FILTER_TYPE_KEY,
15+
FF_MAX_VARIANTS_KEY,
16+
FF_FEATURES_KEY,
1517
HOST_TYPE_KEY,
1618
HostType,
1719
KEY_VAULT_CONFIGURED_TAG,
@@ -85,7 +87,14 @@ export function createCorrelationContextHeader(options: AzureAppConfigurationOpt
8587
keyValues.set(REQUEST_TYPE_KEY, isInitialLoadCompleted ? RequestType.WATCH : RequestType.STARTUP);
8688
keyValues.set(HOST_TYPE_KEY, getHostType());
8789
keyValues.set(ENV_KEY, isDevEnvironment() ? DEV_ENV_VAL : undefined);
88-
keyValues.set(FEATURE_FILTER_TYPE_KEY, featureFlagTracing?.usesAnyFeatureFilter() ? featureFlagTracing.createFeatureFiltersString() : undefined);
90+
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+
}
8998

9099
const tags: string[] = [];
91100
if (options?.keyVaultOptions) {

test/requestTracing.test.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,117 @@ describe("request tracing", function () {
176176
restoreMocks();
177177
});
178178

179+
it("should have max variants in correlation-context header if feature flags use variants", async () => {
180+
let correlationContext: string = "";
181+
const listKvCallback = (listOptions) => {
182+
correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? "";
183+
};
184+
185+
mockAppConfigurationClientListConfigurationSettings([[
186+
createMockedFeatureFlag("Alpha_1", { variants: [ {name: "a"}, {name: "b"}] }),
187+
createMockedFeatureFlag("Alpha_2", { variants: [ {name: "a"}, {name: "b"}, {name: "c"}] }),
188+
createMockedFeatureFlag("Alpha_3", { variants: [] })
189+
]], listKvCallback);
190+
191+
const settings = await load(createMockedConnectionString(fakeEndpoint), {
192+
featureFlagOptions: {
193+
enabled: true,
194+
selectors: [ {keyFilter: "*"} ],
195+
refresh: {
196+
enabled: true,
197+
refreshIntervalInMs: 1000
198+
}
199+
}
200+
});
201+
202+
expect(correlationContext).not.undefined;
203+
expect(correlationContext?.includes("RequestType=Startup")).eq(true);
204+
205+
await sleepInMs(1000 + 1);
206+
try {
207+
await settings.refresh();
208+
} catch (e) { /* empty */ }
209+
expect(headerPolicy.headers).not.undefined;
210+
expect(correlationContext).not.undefined;
211+
expect(correlationContext?.includes("RequestType=Watch")).eq(true);
212+
expect(correlationContext?.includes("MaxVariants=3")).eq(true);
213+
214+
restoreMocks();
215+
});
216+
217+
it("should have telemety tag in correlation-context header if feature flags enable telemetry", async () => {
218+
let correlationContext: string = "";
219+
const listKvCallback = (listOptions) => {
220+
correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? "";
221+
};
222+
223+
mockAppConfigurationClientListConfigurationSettings([[
224+
createMockedFeatureFlag("Alpha_1", { telemetry: {enabled: true} })
225+
]], listKvCallback);
226+
227+
const settings = await load(createMockedConnectionString(fakeEndpoint), {
228+
featureFlagOptions: {
229+
enabled: true,
230+
selectors: [ {keyFilter: "*"} ],
231+
refresh: {
232+
enabled: true,
233+
refreshIntervalInMs: 1000
234+
}
235+
}
236+
});
237+
238+
expect(correlationContext).not.undefined;
239+
expect(correlationContext?.includes("RequestType=Startup")).eq(true);
240+
241+
await sleepInMs(1000 + 1);
242+
try {
243+
await settings.refresh();
244+
} catch (e) { /* empty */ }
245+
expect(headerPolicy.headers).not.undefined;
246+
expect(correlationContext).not.undefined;
247+
expect(correlationContext?.includes("RequestType=Watch")).eq(true);
248+
expect(correlationContext?.includes("FFFeatures=Telemetry")).eq(true);
249+
250+
restoreMocks();
251+
});
252+
253+
it("should have seed tag in correlation-context header if feature flags use allocation seed", async () => {
254+
let correlationContext: string = "";
255+
const listKvCallback = (listOptions) => {
256+
correlationContext = listOptions?.requestOptions?.customHeaders[CORRELATION_CONTEXT_HEADER_NAME] ?? "";
257+
};
258+
259+
mockAppConfigurationClientListConfigurationSettings([[
260+
createMockedFeatureFlag("Alpha_1", { telemetry: {enabled: true} }),
261+
createMockedFeatureFlag("Alpha_2", { allocation: {seed: "123"} })
262+
]], listKvCallback);
263+
264+
const settings = await load(createMockedConnectionString(fakeEndpoint), {
265+
featureFlagOptions: {
266+
enabled: true,
267+
selectors: [ {keyFilter: "*"} ],
268+
refresh: {
269+
enabled: true,
270+
refreshIntervalInMs: 1000
271+
}
272+
}
273+
});
274+
275+
expect(correlationContext).not.undefined;
276+
expect(correlationContext?.includes("RequestType=Startup")).eq(true);
277+
278+
await sleepInMs(1000 + 1);
279+
try {
280+
await settings.refresh();
281+
} catch (e) { /* empty */ }
282+
expect(headerPolicy.headers).not.undefined;
283+
expect(correlationContext).not.undefined;
284+
expect(correlationContext?.includes("RequestType=Watch")).eq(true);
285+
expect(correlationContext?.includes("FFFeatures=Seed+Telemetry")).eq(true);
286+
287+
restoreMocks();
288+
});
289+
179290
describe("request tracing in Web Worker environment", () => {
180291
let originalNavigator;
181292
let originalWorkerNavigator;

0 commit comments

Comments
 (0)