Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
92d6531
Merge pull request #162 from Azure/merge-main-to-preview
zhiyuanliang-ms Feb 12, 2025
b1e57c8
Merge pull request #167 from Azure/main
zhiyuanliang-ms Feb 18, 2025
79039b6
Merge pull request #172 from Azure/main
zhiyuanliang-ms Feb 20, 2025
b5f7202
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Apr 11, 2025
3a28792
revert revert allocation id change
zhiyuanliang-ms Apr 11, 2025
1e236bb
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Apr 11, 2025
11f5e23
Merge main to preview (#180)
zhiyuanliang-ms Apr 11, 2025
b13e6d1
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Apr 14, 2025
f23045d
Merge pull request #184 from Azure/merge-main-to-preview
zhiyuanliang-ms Apr 14, 2025
1eb7aac
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Apr 29, 2025
726c112
Merge pull request #190 from Azure/merge-main-to-preview
zhiyuanliang-ms Apr 29, 2025
0a6fe1f
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms May 13, 2025
663deb6
Merge pull request #196 from Azure/merge-main-to-preview
zhiyuanliang-ms May 13, 2025
737c4c6
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Jun 3, 2025
0fb509a
Merge pull request #200 from Azure/merge-main-to-preview
zhiyuanliang-ms Jun 3, 2025
f413119
Merge pull request #202 from Azure/main
zhiyuanliang-ms Jun 5, 2025
6d1b6a1
Merge pull request #204 from Azure/main
zhiyuanliang-ms Jul 30, 2025
72fcff6
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Aug 7, 2025
8643c38
Merge pull request #209 from Azure/merge-main-to-preview
zhiyuanliang-ms Aug 7, 2025
bd0dc85
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Aug 18, 2025
49d376b
Merge pull request #211 from Azure/merge-main-to-preview
zhiyuanliang-ms Aug 19, 2025
80a751f
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Aug 20, 2025
995469c
Merge pull request #212 from Azure/zhiyuanliang/centralize-error-mess…
zhiyuanliang-ms Aug 20, 2025
56665dd
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Sep 7, 2025
f880e53
update lint rule
zhiyuanliang-ms Sep 7, 2025
4682649
Merge pull request #222 from Azure/merge-main-to-preview
zhiyuanliang-ms Sep 7, 2025
d3f95db
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Sep 8, 2025
8d29cd8
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Sep 8, 2025
0fd4a54
Merge pull request #220 from Azure/zhiyuanliang/browser-vitest (#224)
zhiyuanliang-ms Sep 8, 2025
f60fd2a
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Sep 10, 2025
8f75608
Merge main to preview (#227)
zhiyuanliang-ms Sep 10, 2025
4121c87
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Sep 10, 2025
7ead0bc
Merge pull request #229 from Azure/merge-main-to-preview
zhiyuanliang-ms Sep 10, 2025
2b929c2
wip
zhiyuanliang-ms Sep 24, 2025
2903f7e
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Sep 26, 2025
a31cc03
Merge pull request #231 from Azure/merge-main-to-preview
zhiyuanliang-ms Sep 26, 2025
d8e8c6c
support snapshot reference
zhiyuanliang-ms Oct 2, 2025
7348a0c
Merge branch 'zhiyuanliang/snapshot-reference' of https://github.com/…
zhiyuanliang-ms Oct 2, 2025
43d1730
add test
zhiyuanliang-ms Oct 2, 2025
701e3c7
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Oct 2, 2025
78c50b1
fix filename (#233) (#234)
zhiyuanliang-ms Oct 2, 2025
59e825a
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Oct 2, 2025
6a9aa20
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Nov 7, 2025
a50b506
Merge pull request #238 from Azure/merge-main-to-preview
zhiyuanliang-ms Nov 7, 2025
30e879c
Merge pull request #240 from Azure/main
zhiyuanliang-ms Nov 7, 2025
d9b3347
Merge branch 'preview' of https://github.com/Azure/AppConfiguration-J…
zhiyuanliang-ms Nov 7, 2025
1785813
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Nov 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export default defineConfig([globalIgnores([
}],

"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-require-imports": "off",
"eol-last": ["error", "always"],
"no-trailing-spaces": "error",
"space-before-blocks": ["error", "always"],
Expand Down
189 changes: 180 additions & 9 deletions src/appConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./startupOptions.js";
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js";
import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "./keyvault/keyVaultOptions.js";
import { Disposable } from "./common/disposable.js";
import { base64Helper, jsonSorter } from "./common/utils.js";
import {
FEATURE_FLAGS_KEY_NAME,
FEATURE_MANAGEMENT_KEY_NAME,
Expand All @@ -34,9 +35,16 @@ import {
METADATA_KEY_NAME,
ETAG_KEY_NAME,
FEATURE_FLAG_REFERENCE_KEY_NAME,
ALLOCATION_ID_KEY_NAME,
ALLOCATION_KEY_NAME,
DEFAULT_WHEN_ENABLED_KEY_NAME,
PERCENTILE_KEY_NAME,
FROM_KEY_NAME,
TO_KEY_NAME,
SEED_KEY_NAME,
VARIANT_KEY_NAME,
VARIANTS_KEY_NAME,
CONFIGURATION_VALUE_KEY_NAME,
CONDITIONS_KEY_NAME,
CLIENT_FILTERS_KEY_NAME
} from "./featureManagement/constants.js";
Expand Down Expand Up @@ -82,6 +90,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
#featureFlagTracing: FeatureFlagTracingOptions | undefined;
#fmVersion: string | undefined;
#aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
#useSnapshotReference: boolean = false;

// Refresh
#refreshInProgress: boolean = false;
Expand Down Expand Up @@ -213,7 +222,8 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
isFailoverRequest: this.#isFailoverRequest,
featureFlagTracing: this.#featureFlagTracing,
fmVersion: this.#fmVersion,
aiConfigurationTracing: this.#aiConfigurationTracing
aiConfigurationTracing: this.#aiConfigurationTracing,
useSnapshotReference: this.#useSnapshotReference
};
}

Expand Down Expand Up @@ -504,17 +514,26 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
selector.pageWatchers = pageWatchers;
settings = items;
} else { // snapshot selector
const snapshot = await this.#getSnapshot(selector.snapshotName);
if (snapshot === undefined) {
throw new InvalidOperationError(`Could not find snapshot with name ${selector.snapshotName}.`);
}
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
throw new InvalidOperationError(`Composition type for the selected snapshot with name ${selector.snapshotName} must be 'key'.`);
}
settings = await this.#listConfigurationSettingsForSnapshot(selector.snapshotName);
settings = await this.#loadConfigurationSettingsFromSnapshot(selector.snapshotName);
}

for (const setting of settings) {
if (isSnapshotReference(setting) && !loadFeatureFlag) {
this.#useSnapshotReference = true;

// TODO: When SDK supports snapshot reference, use the helper method from SDK.
const snapshotName = parseSnapshotReference(setting).value.snapshotName;
const settingsFromSnapshot = await this.#loadConfigurationSettingsFromSnapshot(snapshotName);

for (const snapshotSetting of settingsFromSnapshot) {
if (!isFeatureFlag(snapshotSetting)) {
// Feature flags inside snapshot are ignored. This is consistent the behavior that key value selectors ignore feature flags.
loadedSettings.set(snapshotSetting.key, snapshotSetting);
}
}
continue;
}

if (loadFeatureFlag === isFeatureFlag(setting)) {
loadedSettings.set(setting.key, setting);
}
Expand Down Expand Up @@ -575,6 +594,18 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}
}

async #loadConfigurationSettingsFromSnapshot(snapshotName: string): Promise<ConfigurationSetting[]> {
const snapshot = await this.#getSnapshot(snapshotName);
if (snapshot === undefined) {
throw new InvalidOperationError(`Could not find snapshot with name ${snapshotName}.`);
}
if (snapshot.compositionType != KnownSnapshotComposition.Key) {
throw new InvalidOperationError(`Composition type for the selected snapshot with name ${snapshotName} must be 'key'.`);
}
const settings: ConfigurationSetting[] = await this.#listConfigurationSettingsForSnapshot(snapshotName);
return settings;
}

/**
* Clears all existing key-values in the local configuration except feature flags.
*/
Expand Down Expand Up @@ -944,9 +975,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {

if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) {
const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME];
let allocationId = "";
if (featureFlag[ALLOCATION_KEY_NAME] !== undefined) {
allocationId = await this.#generateAllocationId(featureFlag);
}
featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = {
[ETAG_KEY_NAME]: setting.etag,
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
...(allocationId !== "" && { [ALLOCATION_ID_KEY_NAME]: allocationId }),
...(metadata || {})
};
}
Expand Down Expand Up @@ -984,6 +1020,116 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}
}
}

async #generateAllocationId(featureFlag: any): Promise<string> {
let rawAllocationId = "";
// Only default variant when enabled and variants allocated by percentile involve in the experimentation
// The allocation id is genearted from default variant when enabled and percentile allocation
const variantsForExperimentation: string[] = [];

rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`;

if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) {
variantsForExperimentation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]);
rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`;
}

rawAllocationId += "\npercentiles=";

const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME];
if (percentileList) {
const sortedPercentileList = percentileList
.filter(p =>
(p[FROM_KEY_NAME] !== undefined) &&
(p[TO_KEY_NAME] !== undefined) &&
(p[VARIANT_KEY_NAME] !== undefined) &&
(p[FROM_KEY_NAME] !== p[TO_KEY_NAME]))
.sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]);

const percentileAllocation: string[] = [];
for (const percentile of sortedPercentileList) {
variantsForExperimentation.push(percentile[VARIANT_KEY_NAME]);
percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`);
}
rawAllocationId += percentileAllocation.join(";");
}

if (variantsForExperimentation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) {
// All fields required for generating allocation id are missing, short-circuit and return empty string
return "";
}

rawAllocationId += "\nvariants=";

if (variantsForExperimentation.length !== 0) {
const variantsList = featureFlag[VARIANTS_KEY_NAME];
if (variantsList) {
const sortedVariantsList = variantsList
.filter(v =>
(v[NAME_KEY_NAME] !== undefined) &&
variantsForExperimentation.includes(v[NAME_KEY_NAME]))
.sort((a, b) => (a.name > b.name ? 1 : -1));

const variantConfiguration: string[] = [];
for (const variant of sortedVariantsList) {
const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], jsonSorter) ?? "";
variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`);
}
rawAllocationId += variantConfiguration.join(";");
}
}

let crypto;

// Check for browser environment
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
crypto = window.crypto;
}
// Check for Node.js environment
else if (typeof global !== "undefined" && global.crypto) {
crypto = global.crypto;
}
// Fallback to native Node.js crypto module
else {
try {
if (typeof module !== "undefined" && module.exports) {
crypto = require("crypto");
}
else {
crypto = await import("crypto");
}
} catch (error) {
console.error("Failed to load the crypto module:", error.message);
throw error;
}
}

// Convert to UTF-8 encoded bytes
const data = new TextEncoder().encode(rawAllocationId);

// In the browser, use crypto.subtle.digest
if (crypto.subtle) {
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = new Uint8Array(hashBuffer);

// Only use the first 15 bytes
const first15Bytes = hashArray.slice(0, 15);

// btoa/atob is also available in Node.js 18+
const base64String = btoa(String.fromCharCode(...first15Bytes));
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return base64urlString;
}
// In Node.js, use the crypto module's hash function
else {
const hash = crypto.createHash("sha256").update(data).digest();

// Only use the first 15 bytes
const first15Bytes = hash.slice(0, 15);

return first15Bytes.toString("base64url");
}
}
}

function getValidSettingSelectors(selectors: SettingSelector[]): SettingSelector[] {
Expand Down Expand Up @@ -1071,3 +1217,28 @@ function validateTagFilters(tagFilters: string[]): void {
}
}
}

// TODO: Temporary workaround until SDK supports snapshot reference
const snapshotReferenceContentType = "application/json; profile=\"https://azconfig.io/mime-profiles/snapshot-ref\"; charset=utf-8";

interface JsonSnapshotReferenceValue {
snapshot_name: string;
}

function isSnapshotReference(setting: ConfigurationSetting):
setting is ConfigurationSetting & Required<Pick<ConfigurationSetting, "value">> {
return (setting && setting.contentType === snapshotReferenceContentType && typeof setting.value === "string");
}

function parseSnapshotReference(setting: ConfigurationSetting) {
if (!isSnapshotReference(setting)) {
throw new Error(`Invalid snapshot reference: ${setting}`);
}
const jsonSnapshotReferenceValue = JSON.parse(setting.value) as JsonSnapshotReferenceValue;

const snapshotReference = {
...setting,
value: { snapshotName: jsonSnapshotReferenceValue.snapshot_name },
};
return snapshotReference;
}
22 changes: 22 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export function base64Helper(str: string): string {
const bytes = new TextEncoder().encode(str); // UTF-8 encoding
let chars = "";
for (let i = 0; i < bytes.length; i++) {
chars += String.fromCharCode(bytes[i]);
}
return btoa(chars);
}

export function jsonSorter(key, value) {
if (value === null) {
return null;
}
if (Array.isArray(value)) {
return value;
}
if (typeof value === "object") {
return Object.fromEntries(Object.entries(value).sort());
}
return value;
}

export function shuffleList<T>(array: T[]): T[] {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
Expand Down
1 change: 1 addition & 0 deletions src/requestTracing/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const REPLICA_COUNT_KEY = "ReplicaCount";
export const KEY_VAULT_CONFIGURED_TAG = "UsesKeyVault";
export const KEY_VAULT_REFRESH_CONFIGURED_TAG = "RefreshesKeyVault";
export const FAILOVER_REQUEST_TAG = "Failover";
export const SNAPSHOT_REFERENCE_TAG = "SnapshotRef";

// Compact feature tags
export const FEATURES_KEY = "Features";
Expand Down
7 changes: 6 additions & 1 deletion src/requestTracing/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ import {
FM_VERSION_KEY,
DELIMITER,
AI_CONFIGURATION_TAG,
AI_CHAT_COMPLETION_CONFIGURATION_TAG
AI_CHAT_COMPLETION_CONFIGURATION_TAG,
SNAPSHOT_REFERENCE_TAG
} from "./constants.js";

export interface RequestTracingOptions {
Expand All @@ -53,6 +54,7 @@ export interface RequestTracingOptions {
featureFlagTracing: FeatureFlagTracingOptions | undefined;
fmVersion: string | undefined;
aiConfigurationTracing: AIConfigurationTracingOptions | undefined;
useSnapshotReference: boolean;
}

// Utils
Expand Down Expand Up @@ -195,6 +197,9 @@ function createFeaturesString(requestTracingOptions: RequestTracingOptions): str
if (requestTracingOptions.aiConfigurationTracing?.usesAIChatCompletionConfiguration) {
tags.push(AI_CHAT_COMPLETION_CONFIGURATION_TAG);
}
if (requestTracingOptions.useSnapshotReference) {
tags.push(SNAPSHOT_REFERENCE_TAG);
}
return tags.join(DELIMITER);
}

Expand Down
Loading