Skip to content

Commit ab3bf05

Browse files
Merge branch 'preview' into linglingye/merge-main-to-preview
2 parents ceef756 + 83180a3 commit ab3bf05

20 files changed

Lines changed: 826 additions & 26 deletions

eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default defineConfig([globalIgnores([
5353
}],
5454

5555
"@typescript-eslint/no-explicit-any": "off",
56+
"@typescript-eslint/no-require-imports": "off",
5657
"eol-last": ["error", "always"],
5758
"no-trailing-spaces": "error",
5859
"space-before-blocks": ["error", "always"],

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@azure/app-configuration-provider",
3-
"version": "2.5.1",
3+
"version": "2.5.0-preview",
44
"description": "The JavaScript configuration provider for Azure App Configuration",
55
"files": [
66
"dist/",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { PipelinePolicy } from "@azure/core-rest-pipeline";
5+
6+
/**
7+
* The pipeline policy that remove the authorization header from the request to allow anonymous access to the Azure Front Door.
8+
* @remarks
9+
* The policy position should be perRetry, since it should be executed after the "Sign" phase: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-client/src/serviceClient.ts
10+
*/
11+
export class AnonymousRequestPipelinePolicy implements PipelinePolicy {
12+
name: string = "AppConfigurationAnonymousRequestPolicy";
13+
14+
async sendRequest(request, next) {
15+
if (request.headers.has("authorization")) {
16+
request.headers.delete("authorization");
17+
}
18+
return next(request);
19+
}
20+
}
21+
22+
/**
23+
* The pipeline policy that remove the "sync-token" header from the request.
24+
* The policy position should be perRetry. It should be executed after the SyncTokenPolicy in @azure/app-configuration, which is executed after retry phase: https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/appconfiguration/app-configuration/src/appConfigurationClient.ts#L198
25+
*/
26+
export class RemoveSyncTokenPipelinePolicy implements PipelinePolicy {
27+
name: string = "AppConfigurationRemoveSyncTokenPolicy";
28+
29+
async sendRequest(request, next) {
30+
if (request.headers.has("sync-token")) {
31+
request.headers.delete("sync-token");
32+
}
33+
return next(request);
34+
}
35+
}

src/afd/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
export const X_MS_DATE_HEADER = "x-ms-date";

src/appConfigurationImpl.ts

Lines changed: 183 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
GetSnapshotOptions,
1818
ListConfigurationSettingsForSnapshotOptions,
1919
GetSnapshotResponse,
20-
KnownSnapshotComposition
20+
KnownSnapshotComposition,
21+
ListConfigurationSettingPage
2122
} from "@azure/app-configuration";
22-
import { isRestError } from "@azure/core-rest-pipeline";
23+
import { isRestError, RestError } from "@azure/core-rest-pipeline";
2324
import { AzureAppConfiguration, ConfigurationObjectConstructionOptions } from "./appConfiguration.js";
2425
import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js";
2526
import { IKeyValueAdapter } from "./keyValueAdapter.js";
@@ -28,6 +29,7 @@ import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./startupOptions.js";
2829
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js";
2930
import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "./keyvault/keyVaultOptions.js";
3031
import { Disposable } from "./common/disposable.js";
32+
import { base64Helper, jsonSorter } from "./common/utils.js";
3133
import {
3234
FEATURE_FLAGS_KEY_NAME,
3335
FEATURE_MANAGEMENT_KEY_NAME,
@@ -37,9 +39,16 @@ import {
3739
METADATA_KEY_NAME,
3840
ETAG_KEY_NAME,
3941
FEATURE_FLAG_REFERENCE_KEY_NAME,
42+
ALLOCATION_ID_KEY_NAME,
4043
ALLOCATION_KEY_NAME,
44+
DEFAULT_WHEN_ENABLED_KEY_NAME,
45+
PERCENTILE_KEY_NAME,
46+
FROM_KEY_NAME,
47+
TO_KEY_NAME,
4148
SEED_KEY_NAME,
49+
VARIANT_KEY_NAME,
4250
VARIANTS_KEY_NAME,
51+
CONFIGURATION_VALUE_KEY_NAME,
4352
CONDITIONS_KEY_NAME,
4453
CLIENT_FILTERS_KEY_NAME
4554
} from "./featureManagement/constants.js";
@@ -49,6 +58,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/keyVaultKeyValueAdapter
4958
import { RefreshTimer } from "./refresh/refreshTimer.js";
5059
import {
5160
RequestTracingOptions,
61+
checkConfigurationSettingsWithTrace,
5262
getConfigurationSettingWithTrace,
5363
listConfigurationSettingsWithTrace,
5464
getSnapshotWithTrace,
@@ -63,6 +73,7 @@ import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common
6373
import { getStatusCode } from "./common/utils.js";
6474
import { InvalidOperationError, ArgumentError, isFailoverableError, isInputError, SnapshotReferenceError } from "./common/errors.js";
6575
import { ErrorMessages } from "./common/errorMessages.js";
76+
import { X_MS_DATE_HEADER } from "./afd/constants.js";
6677

6778
const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000; // 5 seconds
6879

@@ -124,12 +135,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
124135
// Load balancing
125136
#lastSuccessfulEndpoint: string = "";
126137

138+
// Azure Front Door
139+
#isAfdUsed: boolean = false;
140+
127141
constructor(
128142
clientManager: ConfigurationClientManager,
129-
options?: AzureAppConfigurationOptions,
143+
options: AzureAppConfigurationOptions | undefined,
144+
isAfdUsed: boolean
130145
) {
131146
this.#options = options;
132147
this.#clientManager = clientManager;
148+
this.#isAfdUsed = isAfdUsed;
133149

134150
// enable request tracing if not opt-out
135151
this.#requestTracingEnabled = requestTracingEnabled();
@@ -217,6 +233,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
217233
featureFlagTracing: this.#featureFlagTracing,
218234
fmVersion: this.#fmVersion,
219235
aiConfigurationTracing: this.#aiConfigurationTracing,
236+
isAfdUsed: this.#isAfdUsed,
220237
useSnapshotReference: this.#useSnapshotReference
221238
};
222239
}
@@ -487,7 +504,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
487504
* If false, loads key-value using the key-value selectors. Defaults to false.
488505
*/
489506
async #loadConfigurationSettings(loadFeatureFlag: boolean = false): Promise<ConfigurationSetting[]> {
490-
const selectors = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;
507+
const selectors: PagedSettingsWatcher[] = loadFeatureFlag ? this.#ffSelectors : this.#kvSelectors;
491508

492509
// Use a Map to deduplicate configuration settings by key. When multiple selectors return settings with the same key,
493510
// the configuration setting loaded by the later selector in the iteration order will override the one from the earlier selector.
@@ -504,6 +521,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
504521
tagsFilter: selector.tagFilters
505522
};
506523
const { items, pageWatchers } = await this.#listConfigurationSettings(listOptions);
524+
507525
selector.pageWatchers = pageWatchers;
508526
settings = items;
509527
} else { // snapshot selector
@@ -691,7 +709,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
691709
return Promise.resolve(false);
692710
}
693711

694-
const needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);
712+
let needRefresh = false;
713+
needRefresh = await this.#checkConfigurationSettingsChange(this.#ffSelectors);
714+
695715
if (needRefresh) {
696716
await this.#loadFeatureFlags();
697717
}
@@ -741,21 +761,38 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
741761
const listOptions: ListConfigurationSettingsOptions = {
742762
keyFilter: selector.keyFilter,
743763
labelFilter: selector.labelFilter,
744-
tagsFilter: selector.tagFilters,
745-
pageEtags: pageWatchers.map(w => w.etag ?? "")
764+
tagsFilter: selector.tagFilters
746765
};
747766

748-
const pageIterator = listConfigurationSettingsWithTrace(
767+
if (!this.#isAfdUsed) {
768+
// if AFD is not used, add page etags to the listOptions to send conditional request
769+
listOptions.pageEtags = pageWatchers.map(w => w.etag ?? "") ;
770+
}
771+
772+
const pageIterator = checkConfigurationSettingsWithTrace(
749773
this.#requestTraceOptions,
750774
client,
751775
listOptions
752776
).byPage();
753777

778+
let i = 0;
754779
for await (const page of pageIterator) {
755-
// when conditional request is sent, the response will be 304 if not changed
756-
if (getStatusCode(page._response.status) === 200) { // created or changed
780+
const serverResponseTime: Date = this.#getMsDateHeader(page);
781+
if (i >= pageWatchers.length) {
757782
return true;
758783
}
784+
785+
const lastServerResponseTime = pageWatchers[i].lastServerResponseTime;
786+
let isResponseFresh = false;
787+
if (lastServerResponseTime !== undefined) {
788+
isResponseFresh = serverResponseTime > lastServerResponseTime;
789+
}
790+
if (isResponseFresh &&
791+
page._response.status === 200 && // conditional request returns 304 if not changed
792+
page.etag !== pageWatchers[i].etag) {
793+
return true;
794+
}
795+
i++;
759796
}
760797
}
761798
return false;
@@ -766,7 +803,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
766803
}
767804

768805
/**
769-
* Gets a configuration setting by key and label.If the setting is not found, return undefine instead of throwing an error.
806+
* Gets a configuration setting by key and label. If the setting is not found, return undefined instead of throwing an error.
770807
*/
771808
async #getConfigurationSetting(configurationSettingId: ConfigurationSettingId, getOptions?: GetConfigurationSettingOptions): Promise<GetConfigurationSettingResponse | undefined> {
772809
const funcToExecute = async (client: AppConfigurationClient) => {
@@ -802,7 +839,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
802839

803840
const items: ConfigurationSetting[] = [];
804841
for await (const page of pageIterator) {
805-
pageWatchers.push({ etag: page.etag });
842+
pageWatchers.push({ etag: page.etag, lastServerResponseTime: this.#getMsDateHeader(page) });
806843
items.push(...page.items);
807844
}
808845
return { items, pageWatchers };
@@ -954,9 +991,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
954991

955992
if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) {
956993
const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME];
994+
let allocationId = "";
995+
if (featureFlag[ALLOCATION_KEY_NAME] !== undefined) {
996+
allocationId = await this.#generateAllocationId(featureFlag);
997+
}
957998
featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = {
958999
[ETAG_KEY_NAME]: setting.etag,
9591000
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
1001+
...(allocationId !== "" && { [ALLOCATION_ID_KEY_NAME]: allocationId }),
9601002
...(metadata || {})
9611003
};
9621004
}
@@ -994,6 +1036,135 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
9941036
}
9951037
}
9961038
}
1039+
1040+
async #generateAllocationId(featureFlag: any): Promise<string> {
1041+
let rawAllocationId = "";
1042+
// Only default variant when enabled and variants allocated by percentile involve in the experimentation
1043+
// The allocation id is genearted from default variant when enabled and percentile allocation
1044+
const variantsForExperimentation: string[] = [];
1045+
1046+
rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`;
1047+
1048+
if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) {
1049+
variantsForExperimentation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]);
1050+
rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`;
1051+
}
1052+
1053+
rawAllocationId += "\npercentiles=";
1054+
1055+
const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME];
1056+
if (percentileList) {
1057+
const sortedPercentileList = percentileList
1058+
.filter(p =>
1059+
(p[FROM_KEY_NAME] !== undefined) &&
1060+
(p[TO_KEY_NAME] !== undefined) &&
1061+
(p[VARIANT_KEY_NAME] !== undefined) &&
1062+
(p[FROM_KEY_NAME] !== p[TO_KEY_NAME]))
1063+
.sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]);
1064+
1065+
const percentileAllocation: string[] = [];
1066+
for (const percentile of sortedPercentileList) {
1067+
variantsForExperimentation.push(percentile[VARIANT_KEY_NAME]);
1068+
percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`);
1069+
}
1070+
rawAllocationId += percentileAllocation.join(";");
1071+
}
1072+
1073+
if (variantsForExperimentation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) {
1074+
// All fields required for generating allocation id are missing, short-circuit and return empty string
1075+
return "";
1076+
}
1077+
1078+
rawAllocationId += "\nvariants=";
1079+
1080+
if (variantsForExperimentation.length !== 0) {
1081+
const variantsList = featureFlag[VARIANTS_KEY_NAME];
1082+
if (variantsList) {
1083+
const sortedVariantsList = variantsList
1084+
.filter(v =>
1085+
(v[NAME_KEY_NAME] !== undefined) &&
1086+
variantsForExperimentation.includes(v[NAME_KEY_NAME]))
1087+
.sort((a, b) => (a.name > b.name ? 1 : -1));
1088+
1089+
const variantConfiguration: string[] = [];
1090+
for (const variant of sortedVariantsList) {
1091+
const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], jsonSorter) ?? "";
1092+
variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`);
1093+
}
1094+
rawAllocationId += variantConfiguration.join(";");
1095+
}
1096+
}
1097+
1098+
let crypto;
1099+
1100+
// Check for browser environment
1101+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
1102+
crypto = window.crypto;
1103+
}
1104+
// Check for Node.js environment
1105+
else if (typeof global !== "undefined" && global.crypto) {
1106+
crypto = global.crypto;
1107+
}
1108+
// Fallback to native Node.js crypto module
1109+
else {
1110+
try {
1111+
if (typeof module !== "undefined" && module.exports) {
1112+
crypto = require("crypto");
1113+
}
1114+
else {
1115+
crypto = await import("crypto");
1116+
}
1117+
} catch (error) {
1118+
console.error("Failed to load the crypto module:", error.message);
1119+
throw error;
1120+
}
1121+
}
1122+
1123+
// Convert to UTF-8 encoded bytes
1124+
const data = new TextEncoder().encode(rawAllocationId);
1125+
1126+
// In the browser, use crypto.subtle.digest
1127+
if (crypto.subtle) {
1128+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1129+
const hashArray = new Uint8Array(hashBuffer);
1130+
1131+
// Only use the first 15 bytes
1132+
const first15Bytes = hashArray.slice(0, 15);
1133+
1134+
// btoa/atob is also available in Node.js 18+
1135+
const base64String = btoa(String.fromCharCode(...first15Bytes));
1136+
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1137+
return base64urlString;
1138+
}
1139+
// In Node.js, use the crypto module's hash function
1140+
else {
1141+
const hash = crypto.createHash("sha256").update(data).digest();
1142+
1143+
// Only use the first 15 bytes
1144+
const first15Bytes = hash.slice(0, 15);
1145+
1146+
return first15Bytes.toString("base64url");
1147+
}
1148+
}
1149+
1150+
/**
1151+
* Extracts the response timestamp (x-ms-date) from the response headers. If not found, returns the current time.
1152+
*/
1153+
#getMsDateHeader(response: GetConfigurationSettingResponse | ListConfigurationSettingPage | RestError): Date {
1154+
let header: string | undefined;
1155+
if (isRestError(response)) {
1156+
header = response.response?.headers?.get(X_MS_DATE_HEADER);
1157+
} else {
1158+
header = response._response?.headers?.get(X_MS_DATE_HEADER);
1159+
}
1160+
if (header !== undefined) {
1161+
const date = new Date(header);
1162+
if (!isNaN(date.getTime())) {
1163+
return date;
1164+
}
1165+
}
1166+
return new Date();
1167+
}
9971168
}
9981169

9991170
function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {

0 commit comments

Comments
 (0)