Skip to content

Commit 160f30a

Browse files
Merge branch 'main' into zhiyuanliang/select-snapshot
2 parents 071215e + 0155bf6 commit 160f30a

14 files changed

+237
-135
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
@@ -29,15 +29,15 @@ import {
2929
ENABLED_KEY_NAME,
3030
METADATA_KEY_NAME,
3131
ETAG_KEY_NAME,
32-
FEATURE_FLAG_ID_KEY_NAME,
3332
FEATURE_FLAG_REFERENCE_KEY_NAME,
3433
ALLOCATION_KEY_NAME,
3534
SEED_KEY_NAME,
3635
VARIANTS_KEY_NAME,
3736
CONDITIONS_KEY_NAME,
3837
CLIENT_FILTERS_KEY_NAME
3938
} from "./featureManagement/constants.js";
40-
import { FM_PACKAGE_NAME } from "./requestTracing/constants.js";
39+
import { FM_PACKAGE_NAME, AI_MIME_PROFILE, AI_CHAT_COMPLETION_MIME_PROFILE } from "./requestTracing/constants.js";
40+
import { parseContentType, isJsonContentType, isFeatureFlagContentType, isSecretReferenceContentType } from "./common/contentType.js";
4141
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
4242
import { RefreshTimer } from "./refresh/RefreshTimer.js";
4343
import {
@@ -49,6 +49,7 @@ import {
4949
requestTracingEnabled
5050
} from "./requestTracing/utils.js";
5151
import { FeatureFlagTracingOptions } from "./requestTracing/FeatureFlagTracingOptions.js";
52+
import { AIConfigurationTracingOptions } from "./requestTracing/AIConfigurationTracingOptions.js";
5253
import { KeyFilter, LabelFilter, SettingSelector } from "./types.js";
5354
import { ConfigurationClientManager } from "./ConfigurationClientManager.js";
5455

@@ -78,6 +79,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
7879
#isFailoverRequest: boolean = false;
7980
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
8081
#fmVersion: string | undefined;
82+
#aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
8183

8284
// Refresh
8385
#refreshInProgress: boolean = false;
@@ -117,6 +119,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
117119
// enable request tracing if not opt-out
118120
this.#requestTracingEnabled = requestTracingEnabled();
119121
if (this.#requestTracingEnabled) {
122+
this.#aiConfigurationTracing = new AIConfigurationTracingOptions();
120123
this.#featureFlagTracing = new FeatureFlagTracingOptions();
121124
}
122125

@@ -198,7 +201,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
198201
replicaCount: this.#clientManager.getReplicaCount(),
199202
isFailoverRequest: this.#isFailoverRequest,
200203
featureFlagTracing: this.#featureFlagTracing,
201-
fmVersion: this.#fmVersion
204+
fmVersion: this.#fmVersion,
205+
aiConfigurationTracing: this.#aiConfigurationTracing
202206
};
203207
}
204208

@@ -459,9 +463,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
459463
await this.#updateWatchedKeyValuesEtag(loadedSettings);
460464
}
461465

466+
if (this.#requestTracingEnabled && this.#aiConfigurationTracing !== undefined) {
467+
// Reset old AI configuration tracing in order to track the information present in the current response from server.
468+
this.#aiConfigurationTracing.reset();
469+
}
470+
462471
// process key-values, watched settings have higher priority
463472
for (const setting of loadedSettings) {
464-
const [key, value] = await this.#processKeyValues(setting);
473+
const [key, value] = await this.#processKeyValue(setting);
465474
keyValues.push([key, value]);
466475
}
467476

@@ -510,6 +519,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
510519
const loadFeatureFlag = true;
511520
const featureFlagSettings = await this.#loadConfigurationSettings(loadFeatureFlag);
512521

522+
if (this.#requestTracingEnabled && this.#featureFlagTracing !== undefined) {
523+
// Reset old feature flag tracing in order to track the information present in the current response from server.
524+
this.#featureFlagTracing.reset();
525+
}
526+
513527
// parse feature flags
514528
const featureFlags = await Promise.all(
515529
featureFlagSettings.map(setting => this.#parseFeatureFlag(setting))
@@ -702,12 +716,35 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
702716
throw new Error("All clients failed to get configuration settings.");
703717
}
704718

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

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

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

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/common/utils.ts

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

44
export function shuffleList<T>(array: T[]): T[] {
5-
for (let i = array.length - 1; i > 0; i--) {
6-
const j = Math.floor(Math.random() * (i + 1));
7-
[array[i], array[j]] = [array[j], array[i]];
8-
}
9-
return array;
5+
for (let i = array.length - 1; i > 0; i--) {
6+
const j = Math.floor(Math.random() * (i + 1));
7+
[array[i], array[j]] = [array[j], array[i]];
8+
}
9+
return array;
1010
}

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)