Skip to content

Commit 8de2818

Browse files
add AllocationId to telemetry metadata
1 parent f7ea66c commit 8de2818

File tree

2 files changed

+156
-1
lines changed

2 files changed

+156
-1
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,27 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter";
99
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter";
1010
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions";
1111
import { Disposable } from "./common/disposable";
12-
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";
12+
import {
13+
FEATURE_FLAGS_KEY_NAME,
14+
FEATURE_MANAGEMENT_KEY_NAME,
15+
NAME_KEY_NAME,
16+
TELEMETRY_KEY_NAME,
17+
ENABLED_KEY_NAME,
18+
METADATA_KEY_NAME,
19+
ETAG_KEY_NAME,
20+
FEATURE_FLAG_ID_KEY_NAME,
21+
FEATURE_FLAG_REFERENCE_KEY_NAME,
22+
ALLOCATION_KEY_NAME,
23+
DEFAULT_WHEN_ENABLED_KEY_NAME,
24+
DEFAULT_WHEN_DISABLED_KEY_NAME,
25+
PERCENTILE_KEY_NAME,
26+
FROM_KEY_NAME,
27+
TO_KEY_NAME,
28+
SEED_KEY_NAME,
29+
VARIANT_KEY_NAME,
30+
VARIANTS_KEY_NAME,
31+
CONFIGURATION_VALUE_KEY_NAME
32+
} from "./featureManagement/constants";
1333
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter";
1434
import { RefreshTimer } from "./refresh/RefreshTimer";
1535
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils";
@@ -550,6 +570,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
550570
[ETAG_KEY_NAME]: setting.etag,
551571
[FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting),
552572
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
573+
[ALLOCATION_KEY_NAME]: await this.#generateAllocationId(featureFlag),
553574
...(metadata || {})
554575
};
555576
}
@@ -595,6 +616,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
595616
if (crypto.subtle) {
596617
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
597618
const hashArray = new Uint8Array(hashBuffer);
619+
// btoa/atob is also available in Node.js 18+
598620
const base64String = btoa(String.fromCharCode(...hashArray));
599621
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
600622
return base64urlString;
@@ -613,6 +635,127 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
613635
}
614636
return featureFlagReference;
615637
}
638+
639+
async #generateAllocationId(featureFlag: any): Promise<string> {
640+
let rawAllocationId = "";
641+
// Only default variant when enabled and variants allocated by percentile involve in the experimentation
642+
// The allocation id is genearted from default variant when enabled and percentile allocation
643+
const variantsForExperiementation: string[] = [];
644+
645+
if (featureFlag[ALLOCATION_KEY_NAME]) {
646+
rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`;
647+
648+
if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) {
649+
variantsForExperiementation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]);
650+
rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`;
651+
}
652+
653+
rawAllocationId += `\npercentiles=`;
654+
655+
const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME];
656+
if (percentileList) {
657+
const sortedPercentileList = percentileList
658+
.filter(p =>
659+
(p[FROM_KEY_NAME] !== undefined) &&
660+
(p[TO_KEY_NAME] !== undefined) &&
661+
(p[VARIANT_KEY_NAME] !== undefined) &&
662+
(p[FROM_KEY_NAME] !== p[TO_KEY_NAME]))
663+
.sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]);
664+
665+
const percentileAllocation: string[] = [];
666+
for (const percentile of sortedPercentileList) {
667+
variantsForExperiementation.push(percentile[VARIANT_KEY_NAME]);
668+
percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`);
669+
}
670+
rawAllocationId += percentileAllocation.join(";");
671+
}
672+
}
673+
674+
if (variantsForExperiementation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) {
675+
// All fields required for generating allocation id are missing, short-circuit and return empty string
676+
return "";
677+
}
678+
679+
rawAllocationId += "\nvariants=";
680+
681+
if (variantsForExperiementation.length !== 0) {
682+
const variantsList = featureFlag[VARIANTS_KEY_NAME];
683+
if (variantsList) {
684+
const sortedVariantsList = variantsList
685+
.filter(v =>
686+
(v[NAME_KEY_NAME] !== undefined) &&
687+
variantsForExperiementation.includes(v[NAME_KEY_NAME]))
688+
.sort((a, b) => (a.name > b.name ? 1 : -1));
689+
690+
const variantConfiguration: string[] = [];
691+
for (const variant of sortedVariantsList) {
692+
const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], null, 0) ?? "";
693+
variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`)
694+
}
695+
rawAllocationId += variantConfiguration.join(";");
696+
}
697+
}
698+
699+
let crypto;
700+
701+
// Check for browser environment
702+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
703+
crypto = window.crypto;
704+
}
705+
// Check for Node.js environment
706+
else if (typeof global !== "undefined" && global.crypto) {
707+
crypto = global.crypto;
708+
}
709+
// Fallback to native Node.js crypto module
710+
else {
711+
try {
712+
if (typeof module !== "undefined" && module.exports) {
713+
crypto = require("crypto");
714+
}
715+
else {
716+
crypto = await import("crypto");
717+
}
718+
} catch (error) {
719+
console.error("Failed to load the crypto module:", error.message);
720+
throw error;
721+
}
722+
}
723+
724+
// Convert to UTF-8 encoded bytes
725+
const data = new TextEncoder().encode(rawAllocationId);
726+
727+
// In the browser, use crypto.subtle.digest
728+
if (crypto.subtle) {
729+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
730+
const hashArray = new Uint8Array(hashBuffer);
731+
732+
// Only use the first 15 bytes
733+
const first15Bytes = hashArray.slice(0, 15);
734+
735+
// btoa/atob is also available in Node.js 18+
736+
const base64String = btoa(String.fromCharCode(...first15Bytes));
737+
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
738+
return base64urlString;
739+
}
740+
// In Node.js, use the crypto module's hash function
741+
else {
742+
const hash = crypto.createHash("sha256").update(data).digest();
743+
744+
// Only use the first 15 bytes
745+
const first15Bytes = hash.slice(0, 15);
746+
747+
return first15Bytes.toString("base64url");
748+
}
749+
}
750+
}
751+
752+
function base64Helper(str: string): string {
753+
const bytes = new TextEncoder().encode(str); // UTF-8 encoding
754+
let chars = "";
755+
for (let i = 0; i < bytes.length; i++) {
756+
chars += String.fromCharCode(bytes[i]);
757+
}
758+
return btoa(chars);
616759
}
617760

618761
function getValidSelectors(selectors: SettingSelector[]): SettingSelector[] {

src/featureManagement/constants.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,21 @@
33

44
export const FEATURE_MANAGEMENT_KEY_NAME = "feature_management";
55
export const FEATURE_FLAGS_KEY_NAME = "feature_flags";
6+
export const NAME_KEY_NAME = "name";
67
export const TELEMETRY_KEY_NAME = "telemetry";
78
export const ENABLED_KEY_NAME = "enabled";
89
export const METADATA_KEY_NAME = "metadata";
910
export const ETAG_KEY_NAME = "ETag";
1011
export const FEATURE_FLAG_ID_KEY_NAME = "FeatureFlagId";
1112
export const FEATURE_FLAG_REFERENCE_KEY_NAME = "FeatureFlagReference";
13+
export const ALLOCATION_KEY_NAME = "allocation";
14+
export const DEFAULT_WHEN_ENABLED_KEY_NAME = "default_when_enabled";
15+
export const DEFAULT_WHEN_DISABLED_KEY_NAME = "default_when_disabled";
16+
export const PERCENTILE_KEY_NAME = "percentile";
17+
export const FROM_KEY_NAME = "from";
18+
export const TO_KEY_NAME = "to";
19+
export const SEED_KEY_NAME = "seed";
20+
export const VARIANT_KEY_NAME = "variant";
21+
export const VARIANTS_KEY_NAME = "variants";
22+
export const CONFIGURATION_VALUE_KEY_NAME = "configuration_value";
23+
export const ALLOCATION_ID_KEY_NAME = "AllocationId";

0 commit comments

Comments
 (0)