Skip to content

Commit 3d88c7a

Browse files
Merge branch 'main' of https://github.com/Azure/AppConfiguration-JavaScriptProvider into zhiyuanliang/startup-timeout
2 parents d61aba9 + 0155bf6 commit 3d88c7a

14 files changed

+234
-131
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: 56 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,20 @@ import {
1818
ENABLED_KEY_NAME,
1919
METADATA_KEY_NAME,
2020
ETAG_KEY_NAME,
21-
FEATURE_FLAG_ID_KEY_NAME,
2221
FEATURE_FLAG_REFERENCE_KEY_NAME,
2322
ALLOCATION_KEY_NAME,
2423
SEED_KEY_NAME,
2524
VARIANTS_KEY_NAME,
2625
CONDITIONS_KEY_NAME,
2726
CLIENT_FILTERS_KEY_NAME
2827
} from "./featureManagement/constants.js";
29-
import { FM_PACKAGE_NAME } from "./requestTracing/constants.js";
28+
import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } from "./requestTracing/constants.js";
29+
import { parseContentType, isJsonContentType, isFeatureFlagContentType, isSecretReferenceContentType } from "./common/contentType.js";
3030
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
3131
import { RefreshTimer } from "./refresh/RefreshTimer.js";
3232
import { RequestTracingOptions, getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
3333
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
34+
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
3435
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
3536
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
3637
import { getFixedBackoffDuration, calculateBackoffDuration } from "./failover.js";
@@ -64,6 +65,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
6465
#isFailoverRequest: boolean = false;
6566
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
6667
#fmVersion: string | undefined;
68+
#aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
6769

6870
// Refresh
6971
#refreshInProgress: boolean = false;
@@ -103,6 +105,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
103105
// enable request tracing if not opt-out
104106
this.#requestTracingEnabled = requestTracingEnabled();
105107
if (this.#requestTracingEnabled) {
108+
this.#aiConfigurationTracing = new AIConfigurationTracingOptions();
106109
this.#featureFlagTracing = new FeatureFlagTracingOptions();
107110
}
108111

@@ -184,7 +187,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
184187
replicaCount: this.#clientManager.getReplicaCount(),
185188
isFailoverRequest: this.#isFailoverRequest,
186189
featureFlagTracing: this.#featureFlagTracing,
187-
fmVersion: this.#fmVersion
190+
fmVersion: this.#fmVersion,
191+
aiConfigurationTracing: this.#aiConfigurationTracing
188192
};
189193
}
190194

@@ -485,9 +489,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
485489
await this.#updateWatchedKeyValuesEtag(loadedSettings);
486490
}
487491

492+
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
493+
// Reset old AI configuration tracing in order to track the information present in the current response from server.
494+
this.#aiConfigurationTracing.reset();
495+
}
496+
488497
// adapt configuration settings to key-values
489498
for (const setting of loadedSettings) {
490-
const [key, value] = await this.#processKeyValues(setting);
499+
const [key, value] = await this.#processKeyValue(setting);
491500
keyValues.push([key, value]);
492501
}
493502

@@ -536,6 +545,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
536545
const loadFeatureFlag = true;
537546
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
538547

548+
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
549+
// Reset old feature flag tracing in order to track the information present in the current response from server.
550+
this.#featureFlagTracing.reset();
551+
}
552+
539553
// parse feature flags
540554
const featureFlags = await Promise.all(
541555
featureFlagSettings.map(setting => this.#parseFeatureFlag(setting))
@@ -703,12 +717,35 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
703717
throw new Error("All fallback clients failed to get configuration settings.");
704718
}
705719

706-
async #processKeyValues(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
720+
async #processKeyValue(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
721+
this.#setAIConfigurationTracing(setting);
722+
707723
const [key, value] = await this.#processAdapters(setting);
708724
const trimmedKey = this.#keyWithPrefixesTrimmed(key);
709725
return [trimmedKey, value];
710726
}
711727

728+
#setAIConfigurationTracing(setting: ConfigurationSetting<string>): void {
729+
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
730+
const contentType = parseContentType(setting.contentType);
731+
// content type: "application/json; profile=\"https://azconfig.io/mime-profiles/ai\"""
732+
if (isJsonContentType(contentType) &&
733+
!isFeatureFlagContentType(contentType) &&
734+
!isSecretReferenceContentType(contentType)) {
735+
const profile = contentType?.parameters["profile"];
736+
if (profile === undefined) {
737+
return;
738+
}
739+
if (profile.includes(AI_MIME_PROFILE)) {
740+
this.#aiConfigurationTracing.usesAIConfiguration = true;
741+
}
742+
if (profile.includes(AI_CHAT_COMPLETION_MIME_PROFILE)) {
743+
this.#aiConfigurationTracing.usesAIChatCompletionConfiguration = true;
744+
}
745+
}
746+
}
747+
}
748+
712749
async #processAdapters(setting: ConfigurationSetting<string>): Promise<[string, unknown]> {
713750
for (const adapter of this.#adapters) {
714751
if (adapter.canProcess(setting)) {
@@ -740,12 +777,25 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
740777
const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME];
741778
featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = {
742779
[ETAG_KEY_NAME]: setting.etag,
743-
[FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting),
744780
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
745781
...(metadata || {})
746782
};
747783
}
748784

785+
this.#setFeatureFlagTracing(featureFlag);
786+
787+
return featureFlag;
788+
}
789+
790+
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
791+
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
792+
if (setting.label && setting.label.trim().length !== 0) {
793+
featureFlagReference += `?label=${setting.label}`;
794+
}
795+
return featureFlagReference;
796+
}
797+
798+
#setFeatureFlagTracing(featureFlag: any): void {
749799
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
750800
if (featureFlag[CONDITIONS_KEY_NAME] &&
751801
featureFlag[CONDITIONS_KEY_NAME][CLIENT_FILTERS_KEY_NAME] &&
@@ -764,66 +814,6 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
764814
this.#featureFlagTracing.usesSeed = true;
765815
}
766816
}
767-
768-
return featureFlag;
769-
}
770-
771-
async #calculateFeatureFlagId(setting: ConfigurationSetting<string>): Promise<string> {
772-
let crypto;
773-
774-
// Check for browser environment
775-
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
776-
crypto = window.crypto;
777-
}
778-
// Check for Node.js environment
779-
else if (typeof global !== "undefined" && global.crypto) {
780-
crypto = global.crypto;
781-
}
782-
// Fallback to native Node.js crypto module
783-
else {
784-
try {
785-
if (typeof module !== "undefined" && module.exports) {
786-
crypto = require("crypto");
787-
}
788-
else {
789-
crypto = await import("crypto");
790-
}
791-
} catch (error) {
792-
console.error("Failed to load the crypto module:", error.message);
793-
throw error;
794-
}
795-
}
796-
797-
let baseString = `${setting.key}\n`;
798-
if (setting.label && setting.label.trim().length !== 0) {
799-
baseString += `${setting.label}`;
800-
}
801-
802-
// Convert to UTF-8 encoded bytes
803-
const data = new TextEncoder().encode(baseString);
804-
805-
// In the browser, use crypto.subtle.digest
806-
if (crypto.subtle) {
807-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
808-
const hashArray = new Uint8Array(hashBuffer);
809-
// btoa/atob is also available in Node.js 18+
810-
const base64String = btoa(String.fromCharCode(...hashArray));
811-
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
812-
return base64urlString;
813-
}
814-
// In Node.js, use the crypto module's hash function
815-
else {
816-
const hash = crypto.createHash("sha256").update(data).digest();
817-
return hash.toString("base64url");
818-
}
819-
}
820-
821-
#createFeatureFlagReference(setting: ConfigurationSetting<string>): string {
822-
let featureFlagReference = `${this.#clientManager.endpoint.origin}/kv/${setting.key}`;
823-
if (setting.label && setting.label.trim().length !== 0) {
824-
featureFlagReference += `?label=${setting.label}`;
825-
}
826-
return featureFlagReference;
827817
}
828818
}
829819

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]> {
@@ -34,24 +36,3 @@ export class JsonKeyValueAdapter implements IKeyValueAdapter {
3436
return [setting.key, parsedValue];
3537
}
3638
}
37-
38-
// Determine whether a content type string is a valid JSON content type.
39-
// https://docs.microsoft.com/en-us/azure/azure-app-configuration/howto-leverage-json-content-type
40-
function isJsonContentType(contentTypeValue: string): boolean {
41-
if (!contentTypeValue) {
42-
return false;
43-
}
44-
45-
const contentTypeNormalized: string = contentTypeValue.trim().toLowerCase();
46-
const mimeType: string = contentTypeNormalized.split(";", 1)[0].trim();
47-
const typeParts: string[] = mimeType.split("/");
48-
if (typeParts.length !== 2) {
49-
return false;
50-
}
51-
52-
if (typeParts[0] !== "application") {
53-
return false;
54-
}
55-
56-
return typeParts[1].split("+").includes("json");
57-
}

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)