Skip to content

Commit b50daed

Browse files
committed
enhanced ff: remove prefix in feature flag reference and update client fitler parameter/variant value handling
1 parent 4ce3f15 commit b50daed

6 files changed

Lines changed: 83 additions & 7 deletions

File tree

src/appConfigurationImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1124,7 +1124,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
11241124
async #parseEnhancedFeatureFlag(featureFlag: FeatureFlag): Promise<any> {
11251125
const parsedFeatureFlag = convert(featureFlag);
11261126

1127-
let featureFlagReference = `${this.#clientManager.endpoint.origin}/ff/${featureFlagPrefix}${featureFlag.name}`;
1127+
let featureFlagReference = `${this.#clientManager.endpoint.origin}/ff/${featureFlag.name}`;
11281128
if (featureFlag.label && featureFlag.label.trim().length !== 0) {
11291129
featureFlagReference += `?label=${featureFlag.label}`;
11301130
}

src/featureManagement/featureFlagConverter.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
FeatureFlagTelemetry,
1010
FeatureFlagVariant
1111
} from "./featureFlags.js";
12+
import { isJsonContentType, parseContentType } from "../common/contentType.js";
1213

1314
/**
1415
* Converts @see FeatureFlag into the
@@ -25,7 +26,7 @@ export function convert(featureFlag: AzAppConfigFeatureFlag): FeatureFlag {
2526
const clientFilter: FeatureFilter = { name: filter.name };
2627
if (filter.parameters != null) {
2728
clientFilter.parameters = Object.fromEntries(
28-
Object.entries(filter.parameters).map(([name, value]): [string, unknown] => [name, JSON.parse(value) as unknown]));
29+
Object.entries(filter.parameters).map(([name, value]) => [name, parseParameterValue(value, featureFlag.name)]));
2930
}
3031
return clientFilter;
3132
})
@@ -45,7 +46,9 @@ export function convert(featureFlag: AzAppConfigFeatureFlag): FeatureFlag {
4546
result.variants = featureFlag.variants.map(variant => {
4647
const resultVariant: FeatureFlagVariant = { name: variant.name };
4748
if (variant.value !== undefined) {
48-
resultVariant.configuration_value = variant.value;
49+
resultVariant.configuration_value = isJsonContentType(parseContentType(variant.contentType))
50+
? parseJsonValue(variant.value, featureFlag.name)
51+
: variant.value;
4952
}
5053
if (variant.statusOverride != null) {
5154
resultVariant.status_override = variant.statusOverride;
@@ -90,3 +93,21 @@ export function convert(featureFlag: AzAppConfigFeatureFlag): FeatureFlag {
9093

9194
return result;
9295
}
96+
97+
function parseParameterValue(value: string, featureFlagName: string): unknown {
98+
const trimmedValue = value.trim();
99+
if (trimmedValue.length > 0 && (trimmedValue[0] === "{" || trimmedValue[0] === "[")) {
100+
return parseJsonValue(value, featureFlagName);
101+
}
102+
103+
return value;
104+
}
105+
106+
function parseJsonValue(value: string, featureFlagName: string): unknown {
107+
try {
108+
return JSON.parse(value) as unknown;
109+
} catch (error) {
110+
const errorMessage = error instanceof Error ? error.message : String(error);
111+
throw new SyntaxError(`Enhanced feature flag '${featureFlagName}': ${errorMessage}`, { cause: error });
112+
}
113+
}

src/featureManagement/featureFlags.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface FeatureFlagConditions {
2525

2626
export interface FeatureFlagVariant {
2727
name: AzAppConfigFeatureFlagVariantDefinition["name"];
28-
configuration_value?: AzAppConfigFeatureFlagVariantDefinition["value"];
28+
configuration_value?: unknown;
2929
status_override?: AzAppConfigFeatureFlagVariantDefinition["statusOverride"];
3030
}
3131

src/requestTracing/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export function requestTracingEnabled(): boolean {
143143
function usesAnyTracingFeature(requestTracingOptions: RequestTracingOptions): boolean {
144144
return (requestTracingOptions.appConfigOptions?.loadBalancingEnabled ?? false) ||
145145
(requestTracingOptions.aiConfigurationTracing?.usesAnyTracingFeature() ?? false) ||
146+
requestTracingOptions.useEnhancedFeatureFlag ||
146147
requestTracingOptions.isAfdUsed;
147148
}
148149

test/featureFlag.test.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import * as chai from "chai";
66
import chaiAsPromised from "chai-as-promised";
77
import { featureFlagContentType } from "@azure/app-configuration";
88
import { load } from "../src/index.js";
9-
import { mockAppConfigurationClientGetSnapshot, mockAppConfigurationClientListConfigurationSettingsForSnapshot, createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedEnhancedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, mockFeatureFlagClientListFeatureFlags, restoreMocks, sleepInMs } from "./utils/testHelper.js";
9+
import { convert } from "../src/featureManagement/featureFlagConverter.js";
10+
import { mockAppConfigurationClientGetSnapshot, mockAppConfigurationClientListConfigurationSettingsForSnapshot, createMockedConnectionString, createMockedEndpoint, createMockedFeatureFlag, createMockedEnhancedFeatureFlag, createMockedKeyValue, mockAppConfigurationClientListConfigurationSettings, mockFeatureFlagClientListFeatureFlags, restoreMocks, sleepInMs, expectEnhancedFeatureFlagJsonError } from "./utils/testHelper.js";
1011
chai.use(chaiAsPromised);
1112
const expect = chai.expect;
1213

@@ -567,6 +568,7 @@ describe("enhanced feature flags", function () {
567568
name: "Microsoft.Targeting",
568569
parameters: {
569570
Audience: JSON.stringify(audience),
571+
JsonArray: " [\"one\",\"two\"] ",
570572
PlainText: "not-json",
571573
Percentage: "50"
572574
}
@@ -583,8 +585,43 @@ describe("enhanced feature flags", function () {
583585
.find(ff => ff.id === "Targeted");
584586
const parameters = featureFlag.conditions.client_filters[0].parameters;
585587
expect(parameters.Audience).deep.equals(audience);
588+
expect(parameters.JsonArray).deep.equals(["one", "two"]);
586589
expect(parameters.PlainText).equals("not-json");
587-
expect(parameters.Percentage).equals(50);
590+
expect(parameters.Percentage).equals("50");
591+
});
592+
593+
it("should throw for invalid JSON in enhanced feature flag filter parameters", () => {
594+
const enhancedFeatureFlag = createMockedEnhancedFeatureFlag("InvalidParameter", {
595+
conditions: {
596+
filters: [{
597+
name: "CustomFilter",
598+
parameters: { Value: "{not-json}" }
599+
}]
600+
}
601+
});
602+
603+
expectEnhancedFeatureFlagJsonError(() => convert(enhancedFeatureFlag), "InvalidParameter");
604+
});
605+
606+
it("should parse enhanced feature flag variants based on content type", () => {
607+
const enhancedFeatureFlag = createMockedEnhancedFeatureFlag("VariantContentType", {
608+
variants: [
609+
{ name: "Json", value: "{\"color\":\"blue\"}", contentType: "application/json" },
610+
{ name: "Text", value: "{\"color\":\"blue\"}", contentType: "text/plain" }
611+
]
612+
});
613+
614+
const featureFlag = convert(enhancedFeatureFlag);
615+
expect(featureFlag.variants?.[0].configuration_value).deep.equals({ color: "blue" });
616+
expect(featureFlag.variants?.[1].configuration_value).equals("{\"color\":\"blue\"}");
617+
});
618+
619+
it("should throw for invalid JSON in an enhanced feature flag variant", () => {
620+
const enhancedFeatureFlag = createMockedEnhancedFeatureFlag("InvalidVariant", {
621+
variants: [{ name: "Json", value: "{not-json}", contentType: "application/json" }]
622+
});
623+
624+
expectEnhancedFeatureFlagJsonError(() => convert(enhancedFeatureFlag), "InvalidVariant");
588625
});
589626

590627
it("should let an enhanced feature flag supersede a feature flag with the same name", async () => {
@@ -647,7 +684,7 @@ describe("enhanced feature flags", function () {
647684
expect(featureFlag.allocation.percentile[0].variant).equals("On");
648685
// telemetry enabled => metadata populated with the feature flag reference and allocation id
649686
expect(featureFlag.telemetry.metadata).not.undefined;
650-
expect(featureFlag.telemetry.metadata.FeatureFlagReference).contains(".appconfig.featureflag/Variant");
687+
expect(featureFlag.telemetry.metadata.FeatureFlagReference).equals(`${createMockedEndpoint()}/ff/Variant`);
651688
expect(featureFlag.telemetry.metadata.AllocationId).not.undefined;
652689
});
653690

test/utils/testHelper.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT license.
33

44
import * as sinon from "sinon";
5+
import * as chai from "chai";
56
import { AppConfigurationClient as ConfigurationClient, ConfigurationSetting, FeatureFlagClient, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration";
67
import { ClientSecretCredential } from "@azure/identity";
78
import { KeyVaultSecret, SecretClient } from "@azure/keyvault-secrets";
@@ -13,6 +14,21 @@ import { AppConfigurationClient } from "../../src/appConfigurationClient.js";
1314

1415
const sleepInMs = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
1516

17+
function expectEnhancedFeatureFlagJsonError(action: () => unknown, featureFlagName: string): void {
18+
let thrownError: unknown;
19+
try {
20+
action();
21+
} catch (error) {
22+
thrownError = error;
23+
}
24+
25+
chai.expect(thrownError).instanceOf(SyntaxError);
26+
const syntaxError = thrownError as SyntaxError;
27+
chai.expect(syntaxError.message).contains(`Enhanced feature flag '${featureFlagName}':`);
28+
chai.expect(syntaxError.cause).instanceOf(SyntaxError);
29+
chai.expect(syntaxError.message).contains((syntaxError.cause as SyntaxError).message);
30+
}
31+
1632
// Async, browser-safe SHA-256 using native crypto.subtle when available; falls back to tiny FNV-1a for Node without subtle.
1733
async function _sha256(input: string): Promise<string> {
1834
let crypto;
@@ -589,6 +605,7 @@ export {
589605
createMockedFeatureFlag,
590606
createMockedEnhancedFeatureFlag,
591607
createMockedSnapshotReference,
608+
expectEnhancedFeatureFlagJsonError,
592609

593610
sleepInMs,
594611
HttpRequestHeadersPolicy

0 commit comments

Comments
 (0)