Skip to content

Merge main to preview #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 51 commits into from
Feb 12, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e29c5e0
update ci
linglingye001 Oct 9, 2024
cca37e7
Merge pull request #105 from Azure/linglingye/updateCi
linglingye001 Oct 10, 2024
af0ccb3
Feature Flag Telemetry Support (#101)
zhiyuanliang-ms Oct 15, 2024
f7ea66c
Update to ETag (#110)
zhiyuanliang-ms Oct 17, 2024
8de2818
add AllocationId to telemetry metadata
zhiyuanliang-ms Oct 17, 2024
4df864b
fix lint
zhiyuanliang-ms Oct 17, 2024
638338a
update
zhiyuanliang-ms Oct 17, 2024
6df1970
add testcases
zhiyuanliang-ms Oct 18, 2024
233553f
sort json key
zhiyuanliang-ms Oct 24, 2024
6e7724c
fix lint
zhiyuanliang-ms Oct 24, 2024
0fe93b4
update
zhiyuanliang-ms Oct 28, 2024
6dad98d
update testcase
zhiyuanliang-ms Oct 29, 2024
6dae81a
New API to load from CDN endpoint (#106)
zhiyuanliang-ms Oct 31, 2024
7503688
export loadFromCdn (#118)
zhiyuanliang-ms Oct 31, 2024
197f4b8
resolve merge conflict
zhiyuanliang-ms Oct 31, 2024
b3b6b07
update
zhiyuanliang-ms Nov 1, 2024
16bf0c0
fix lint
zhiyuanliang-ms Nov 1, 2024
97d6d88
merge main to preview
zhiyuanliang-ms Nov 5, 2024
8a7cf70
Merge pull request #125 from Azure/zhiyuanliang/merge-main-to-preview
zhiyuanliang-ms Nov 5, 2024
68fba74
remove requestracingoptions (#127)
zhiyuanliang-ms Nov 5, 2024
5ac5294
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Nov 5, 2024
173e890
Merge pull request #129 from Azure/zhiyuanliang/merge-main-to-preview
zhiyuanliang-ms Nov 5, 2024
d573c31
Revert "New API to load from CDN endpoint (#106)"
zhiyuanliang-ms Nov 5, 2024
ae78584
Revert "export loadFromCdn (#118)"
zhiyuanliang-ms Nov 5, 2024
98f1a0a
Merge pull request #130 from Azure/zhiyuanliang/revert
zhiyuanliang-ms Nov 5, 2024
13e6ccc
merge preview
zhiyuanliang-ms Nov 5, 2024
214a1d9
revert change
zhiyuanliang-ms Nov 5, 2024
d110418
Merge pull request #111 from Azure/zhiyuanliang/allocation-id
zhiyuanliang-ms Nov 5, 2024
a251f82
version bump 2.0.0-preview.1 (#132)
zhiyuanliang-ms Nov 8, 2024
7db665f
Failover support (#98)
linglingye001 Nov 18, 2024
477f18d
add dns module to rollup whitelist (#134)
zhiyuanliang-ms Nov 18, 2024
2315e4c
Load balance support (#135)
linglingye001 Nov 27, 2024
11e505c
resolve merge conflict
zhiyuanliang-ms Dec 2, 2024
f30d237
Merge pull request #139 from Azure/zhiyuanliang/merge-main-to-preview
zhiyuanliang-ms Dec 2, 2024
9300106
add replica count tracing
zhiyuanliang-ms Dec 12, 2024
9a38443
fix bug
zhiyuanliang-ms Dec 13, 2024
ddf3d60
audit vulnerablitiy
zhiyuanliang-ms Dec 13, 2024
eced463
centralize timeout
zhiyuanliang-ms Dec 13, 2024
4d77f89
fix lint
zhiyuanliang-ms Dec 13, 2024
04f6d23
add testcase
zhiyuanliang-ms Dec 13, 2024
e036457
Merge pull request #141 from Azure/zhiyuanliang/replica-count-tracing
zhiyuanliang-ms Dec 13, 2024
b178046
resolve merge conflict
zhiyuanliang-ms Dec 18, 2024
9968fe8
Merge pull request #142 from Azure/merge-main-to-preview
zhiyuanliang-ms Dec 18, 2024
71aebab
Refresh key value collection based on page etag (#133)
zhiyuanliang-ms Dec 19, 2024
8600654
fix timeout (#144)
zhiyuanliang-ms Dec 19, 2024
f5d48b2
version bump 2.0.0-preview.2 (#146)
zhiyuanliang-ms Jan 7, 2025
a6cbfdf
Merge branch 'main' of https://github.com/Azure/AppConfiguration-Java…
zhiyuanliang-ms Jan 8, 2025
ed1d35e
Merge pull request #152 from Azure/merge-main-to-preview
zhiyuanliang-ms Jan 8, 2025
5956b72
load all feature flag with no label by default (#158)
zhiyuanliang-ms Feb 10, 2025
ca7c5bb
Add feature management package version tracing (#153)
zhiyuanliang-ms Feb 10, 2025
07026ac
unify code style (#159)
zhiyuanliang-ms Feb 12, 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
139 changes: 138 additions & 1 deletion src/AzureAppConfigurationImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,28 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js";
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js";
import { Disposable } from "./common/disposable.js";
import { FEATURE_FLAGS_KEY_NAME, FEATURE_MANAGEMENT_KEY_NAME, TELEMETRY_KEY_NAME, ENABLED_KEY_NAME, METADATA_KEY_NAME, ETAG_KEY_NAME, FEATURE_FLAG_ID_KEY_NAME, FEATURE_FLAG_REFERENCE_KEY_NAME } from "./featureManagement/constants.js";
import { base64Helper, jsonSorter } from "./common/utils.js";
import {
FEATURE_FLAGS_KEY_NAME,
FEATURE_MANAGEMENT_KEY_NAME,
NAME_KEY_NAME,
TELEMETRY_KEY_NAME,
ENABLED_KEY_NAME,
METADATA_KEY_NAME,
ETAG_KEY_NAME,
FEATURE_FLAG_ID_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
} from "./featureManagement/constants.js";
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
import { RefreshTimer } from "./refresh/RefreshTimer.js";
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
Expand Down Expand Up @@ -546,10 +567,15 @@ 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_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting),
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
...(allocationId !== "" && { [ALLOCATION_ID_KEY_NAME]: allocationId }),
...(metadata || {})
};
}
Expand Down Expand Up @@ -595,6 +621,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
if (crypto.subtle) {
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = new Uint8Array(hashBuffer);
// btoa/atob is also available in Node.js 18+
const base64String = btoa(String.fromCharCode(...hashArray));
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
return base64urlString;
Expand All @@ -613,6 +640,116 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
}
return featureFlagReference;
}

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 getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {
Expand Down
24 changes: 24 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// 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;
}
11 changes: 11 additions & 0 deletions src/featureManagement/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,20 @@

export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management";
export const FEATURE_FLAGS_KEY_NAME = "feature_flags";
export const NAME_KEY_NAME = "name";
export const TELEMETRY_KEY_NAME = "telemetry";
export const ENABLED_KEY_NAME = "enabled";
export const METADATA_KEY_NAME = "metadata";
export const ETAG_KEY_NAME = "ETag";
export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId";
export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference";
export const ALLOCATION_KEY_NAME = "allocation";
export const DEFAULT_WHEN_ENABLED_KEY_NAME = "default_when_enabled";
export const PERCENTILE_KEY_NAME = "percentile";
export const FROM_KEY_NAME = "from";
export const TO_KEY_NAME = "to";
export const SEED_KEY_NAME = "seed";
export const VARIANT_KEY_NAME = "variant";
export const VARIANTS_KEY_NAME = "variants";
export const CONFIGURATION_VALUE_KEY_NAME = "configuration_value";
export const ALLOCATION_ID_KEY_NAME = "AllocationId";
Loading
Loading