Skip to content

Commit d1ad647

Browse files
Merge branch 'zhiyuanliang/startup-timeout' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/secret-refresh
2 parents d81f8a9 + 3d88c7a commit d1ad647

14 files changed

+237
-134
lines changed

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.0.1",
3+
"version": "2.0.2",
44
"description": "The JavaScript configuration provider for Azure App Configuration",
55
"main": "dist/index.js",
66
"module": "./dist-esm/index.js",

src/AzureAppConfigurationImpl.ts

Lines changed: 59 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,20 @@ import {
1919
ENABLED_KEY_NAME,
2020
METADATA_KEY_NAME,
2121
ETAG_KEY_NAME,
22-
FEATURE_FLAG_ID_KEY_NAME,
2322
FEATURE_FLAG_REFERENCE_KEY_NAME,
2423
ALLOCATION_KEY_NAME,
2524
SEED_KEY_NAME,
2625
VARIANTS_KEY_NAME,
2726
CONDITIONS_KEY_NAME,
2827
CLIENT_FILTERS_KEY_NAME
2928
} from "./featureManagement/constants.js";
30-
import { FM_PACKAGE_NAME } from "./requestTracing/constants.js";
29+
import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } from "./requestTracing/constants.js";
30+
import { parseContentType, isJsonContentType, isFeatureFlagContentType, isSecretReferenceContentType } from "./common/contentType.js";
3131
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
3232
import { RefreshTimer } from "./refresh/RefreshTimer.js";
3333
import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
3434
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
35+
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
3536
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
3637
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
3738
import { getFixedBackoffDuration, calculateBackoffDuration } from "./failover.js";
@@ -65,6 +66,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
6566
#isFailoverRequest: boolean = false;
6667
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
6768
#fmVersion: string | undefined;
69+
#aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
6870

6971
// Refresh
7072
#refreshInProgress: boolean = false;
@@ -111,6 +113,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
111113
// enable request tracing if not opt-out
112114
this.#requestTracingEnabled = requestTracingEnabled();
113115
if (this.#requestTracingEnabled) {
116+
this.#aiConfigurationTracing = new AIConfigurationTracingOptions();
114117
this.#featureFlagTracing = new FeatureFlagTracingOptions();
115118
}
116119

@@ -191,7 +194,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
191194
replicaCount: this.#clientManager.getReplicaCount(),
192195
isFailoverRequest: this.#isFailoverRequest,
193196
featureFlagTracing: this.#featureFlagTracing,
194-
fmVersion: this.#fmVersion
197+
fmVersion: this.#fmVersion,
198+
aiConfigurationTracing: this.#aiConfigurationTracing
195199
};
196200
}
197201

@@ -489,19 +493,24 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
489493
* Loads selected key-values and watched settings (sentinels) for refresh from App Configuration to the local configuration.
490494
*/
491495
async #loadSelectedAndWatchedKeyValues() {
496+
this.#secretReferences = []; // clear all cached key vault reference configuration settings
492497
const keyValues: [key: string, value: unknown][] = [];
493498
const loadedSettings = await this.#loadConfigurationSettings();
494499
if (this.#refreshEnabled && !this.#watchAll) {
495500
await this.#updateWatchedKeyValuesEtag(loadedSettings);
496501
}
497502

498-
// clear all cached key vault reference configuration settings
499-
this.#secretReferences = [];
503+
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
504+
// reset old AI configuration tracing in order to track the information present in the current response from server
505+
this.#aiConfigurationTracing.reset();
506+
}
507+
508+
// adapt configuration settings to key-values
500509
for (const setting of loadedSettings) {
501510
if (this.#secretRefreshEnabled && isSecretReference(setting)) {
502511
this.#secretReferences.push(setting);
503512
}
504-
const [key, value] = await this.#processKeyValues(setting);
513+
const [key, value] = await this.#processKeyValue(setting);
505514
keyValues.push([key, value]);
506515
}
507516

@@ -550,6 +559,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
550559
const loadFeatureFlag = true;
551560
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
552561

562+
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
563+
// Reset old feature flag tracing in order to track the information present in the current response from server.
564+
this.#featureFlagTracing.reset();
565+
}
566+
553567
// parse feature flags
554568
const featureFlags = await Promise.all(
555569
featureFlagSettings.map(setting => this.#parseFeatureFlag(setting))
@@ -625,7 +639,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
625639
}
626640

627641
for (const setting of this.#secretReferences) {
628-
const [key, value] = await this.#processKeyValues(setting);
642+
const [key, value] = await this.#processKeyValue(setting);
629643
this.#configMap.set(key, value);
630644
}
631645

@@ -735,12 +749,35 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
735749
throw new Error("All fallback clients failed to get configuration settings.");
736750
}
737751

738-
async #processKeyValues(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
752+
async #processKeyValue(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
753+
this.#setAIConfigurationTracing(setting);
754+
739755
const [key, value] = await this.#processAdapters(setting);
740756
const trimmedKey = this.#keyWithPrefixesTrimmed(key);
741757
return [trimmedKey, value];
742758
}
743759

760+
#setAIConfigurationTracing(setting: ConfigurationSetting<string>): void {
761+
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
762+
const contentType = parseContentType(setting.contentType);
763+
// content type: "application/json; profile=\"https://azconfig.io/mime-profiles/ai\"""
764+
if (isJsonContentType(contentType) &&
765+
!isFeatureFlagContentType(contentType) &&
766+
!isSecretReferenceContentType(contentType)) {
767+
const profile = contentType?.parameters["profile"];
768+
if (profile === undefined) {
769+
return;
770+
}
771+
if (profile.includes(AI_MIME_PROFILE)) {
772+
this.#aiConfigurationTracing.usesAIConfiguration = true;
773+
}
774+
if (profile.includes(AI_CHAT_COMPLETION_MIME_PROFILE)) {
775+
this.#aiConfigurationTracing.usesAIChatCompletionConfiguration = true;
776+
}
777+
}
778+
}
779+
}
780+
744781
async #processAdapters(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
745782
for (const adapter of this.#adapters) {
746783
if (adapter.canProcess(setting)) {
@@ -772,12 +809,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
772809
const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME];
773810
featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = {
774811
[ETAG_KEY_NAME]: setting.etag,
775-
[FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting),
776812
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
777813
...(metadata || {})
778814
};
779815
}
780816

817+
this.#setFeatureFlagTracing(featureFlag);
818+
819+
return featureFlag;
820+
}
821+
822+
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
823+
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
824+
if (setting.label && setting.label.trim().length !== 0) {
825+
featureFlagReference += `?label=${setting.label}`;
826+
}
827+
return featureFlagReference;
828+
}
829+
830+
#setFeatureFlagTracing(featureFlag: any): void {
781831
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
782832
if (featureFlag[CONDITIONS_KEY_NAME] &&
783833
featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] &&
@@ -796,66 +846,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
796846
this.#featureFlagTracing.usesSeed = true;
797847
}
798848
}
799-
800-
return featureFlag;
801-
}
802-
803-
async #calculateFeatureFlagId(setting: ConfigurationSetting<string>): Promise<string> {
804-
let crypto;
805-
806-
// Check for browser environment
807-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
808-
crypto = window.crypto;
809-
}
810-
// Check for Node.js environment
811-
else if (typeof global !== "undefined" && global.crypto) {
812-
crypto = global.crypto;
813-
}
814-
// Fallback to native Node.js crypto module
815-
else {
816-
try {
817-
if (typeof module !== "undefined" && module.exports) {
818-
crypto = require("crypto");
819-
}
820-
else {
821-
crypto = await import("crypto");
822-
}
823-
} catch (error) {
824-
console.error("Failed to load the crypto module:", error.message);
825-
throw error;
826-
}
827-
}
828-
829-
let baseString = `${setting.key}\n`;
830-
if (setting.label && setting.label.trim().length !== 0) {
831-
baseString += `${setting.label}`;
832-
}
833-
834-
// Convert to UTF-8 encoded bytes
835-
const data = new TextEncoder().encode(baseString);
836-
837-
// In the browser, use crypto.subtle.digest
838-
if (crypto.subtle) {
839-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
840-
const hashArray = new Uint8Array(hashBuffer);
841-
// btoa/atob is also available in Node.js 18+
842-
const base64String = btoa(String.fromCharCode(...hashArray));
843-
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
844-
return base64urlString;
845-
}
846-
// In Node.js, use the crypto module's hash function
847-
else {
848-
const hash = crypto.createHash("sha256").update(data).digest();
849-
return hash.toString("base64url");
850-
}
851-
}
852-
853-
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
854-
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
855-
if (setting.label && setting.label.trim().length !== 0) {
856-
featureFlagReference += `?label=${setting.label}`;
857-
}
858-
return featureFlagReference;
859849
}
860850
}
861851

src/ConfigurationClientManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@ const DNS_RESOLVER_TRIES = 2;
2828
const MAX_ALTNATIVE_SRV_COUNT = 10;
2929

3030
export class ConfigurationClientManager {
31+
readonly endpoint: URL; // primary endpoint, which is the one specified in the connection string or passed in as a parameter
3132
#isFailoverable: boolean;
3233
#dns: any;
3334
#secret : string;
3435
#id : string;
3536
#credential: TokenCredential;
3637
#clientOptions: AppConfigurationClientOptions | undefined;
3738
#appConfigOptions: AzureAppConfigurationOptions | undefined;
38-
#validDomain: string;
39+
#validDomain: string; // valid domain for the primary endpoint, which is used to discover replicas
3940
#staticClients: ConfigurationClientWrapper[]; // there should always be only one static client
4041
#dynamicClients: ConfigurationClientWrapper[];
4142
#replicaCount: number = 0;

src/JsonKeyValueAdapter.ts

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

44
import { ConfigurationSetting, featureFlagContentType, secretReferenceContentType } from "@azure/app-configuration";
5+
import { parseContentType, isJsonContentType } from "./common/contentType.js";
56
import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
67

78
export class JsonKeyValueAdapter implements IKeyValueAdapter {
@@ -17,7 +18,8 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
1718
if (JsonKeyValueAdapter.#ExcludedJsonContentTypes.includes(setting.contentType)) {
1819
return false;
1920
}
20-
return isJsonContentType(setting.contentType);
21+
const contentType = parseContentType(setting.contentType);
22+
return isJsonContentType(contentType);
2123
}
2224

2325
async processKeyValue(setting: ConfigurationSetting): Promise<[string, unknown]> {
@@ -38,24 +40,3 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
3840
return;
3941
}
4042
}
41-
42-
// Determine whether a content type string is a valid JSON content type.
43-
// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type
44-
function isJsonContentType(contentTypeValue: string): boolean {
45-
if (!contentTypeValue) {
46-
return false;
47-
}
48-
49-
const contentTypeNormalized: string = contentTypeValue.trim().toLowerCase();
50-
const mimeType: string = contentTypeNormalized.split(";", 1)[0].trim();
51-
const typeParts: string[] = mimeType.split("/");
52-
if (typeParts.length !== 2) {
53-
return false;
54-
}
55-
56-
if (typeParts[0] !== "application") {
57-
return false;
58-
}
59-
60-
return typeParts[1].split("+").includes("json");
61-
}

src/common/contentType.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { secretReferenceContentType, featureFlagContentType } from "@azure/app-configuration";
5+
6+
export type ContentType = {
7+
mediaType: string;
8+
parameters: Record<string, string>;
9+
}
10+
11+
export function parseContentType(contentTypeValue: string | undefined): ContentType | undefined {
12+
if (!contentTypeValue) {
13+
return undefined;
14+
}
15+
const [mediaType, ...args] = contentTypeValue.split(";").map((s) => s.trim().toLowerCase());
16+
const parameters: Record<string, string> = {};
17+
18+
for (const param of args) {
19+
const [key, value] = param.split("=").map((s) => s.trim().toLowerCase());
20+
if (key && value) {
21+
parameters[key] = value;
22+
}
23+
}
24+
25+
return { mediaType, parameters };
26+
}
27+
28+
// Determine whether a content type string is a valid JSON content type.
29+
// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type
30+
export function isJsonContentType(contentType: ContentType | undefined): boolean {
31+
const mediaType = contentType?.mediaType;
32+
if (!mediaType) {
33+
return false;
34+
}
35+
36+
const typeParts: string[] = mediaType.split("/");
37+
if (typeParts.length !== 2) {
38+
return false;
39+
}
40+
41+
if (typeParts[0] !== "application") {
42+
return false;
43+
}
44+
45+
return typeParts[1].split("+").includes("json");
46+
}
47+
48+
export function isFeatureFlagContentType(contentType: ContentType | undefined): boolean {
49+
const mediaType = contentType?.mediaType;
50+
if (!mediaType) {
51+
return false;
52+
}
53+
return mediaType === featureFlagContentType;
54+
}
55+
56+
export function isSecretReferenceContentType(contentType: ContentType | undefined): boolean {
57+
const mediaType = contentType?.mediaType;
58+
if (!mediaType) {
59+
return false;
60+
}
61+
return mediaType === secretReferenceContentType;
62+
}

src/featureManagement/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export const TELEMETRY_KEY_NAME = "telemetry";
88
export const ENABLED_KEY_NAME = "enabled";
99
export const METADATA_KEY_NAME = "metadata";
1010
export const ETAG_KEY_NAME = "ETag";
11-
export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId";
1211
export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference";
1312
export const ALLOCATION_KEY_NAME = "allocation";
1413
export const DEFAULT_WHEN_ENABLED_KEY_NAME = "default_when_enabled";
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
export class AIConfigurationTracingOptions {
5+
usesAIConfiguration: boolean = false;
6+
usesAIChatCompletionConfiguration: boolean = false;
7+
8+
reset(): void {
9+
this.usesAIConfiguration = false;
10+
this.usesAIChatCompletionConfiguration = false;
11+
}
12+
13+
usesAnyTracingFeature() {
14+
return this.usesAIConfiguration || this.usesAIChatCompletionConfiguration;
15+
}
16+
}

0 commit comments

Comments
 (0)