Skip to content

Commit d110418

Browse files
Merge pull request #111 from Azure/zhiyuanliang/allocation-id
Add AllocationId to telemetry metadata
2 parents 98f1a0a + 214a1d9 commit d110418

File tree

4 files changed

+374
-2
lines changed

4 files changed

+374
-2
lines changed

src/AzureAppConfigurationImpl.ts

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,28 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
99
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js";
1010
import { DEFAULT_REFRESH_INTERVAL_IN_MS, MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js";
1111
import { Disposable } from "./common/disposable.js";
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.js";
12+
import { base64Helper, jsonSorter } from "./common/utils.js";
13+
import {
14+
FEATURE_FLAGS_KEY_NAME,
15+
FEATURE_MANAGEMENT_KEY_NAME,
16+
NAME_KEY_NAME,
17+
TELEMETRY_KEY_NAME,
18+
ENABLED_KEY_NAME,
19+
METADATA_KEY_NAME,
20+
ETAG_KEY_NAME,
21+
FEATURE_FLAG_ID_KEY_NAME,
22+
FEATURE_FLAG_REFERENCE_KEY_NAME,
23+
ALLOCATION_ID_KEY_NAME,
24+
ALLOCATION_KEY_NAME,
25+
DEFAULT_WHEN_ENABLED_KEY_NAME,
26+
PERCENTILE_KEY_NAME,
27+
FROM_KEY_NAME,
28+
TO_KEY_NAME,
29+
SEED_KEY_NAME,
30+
VARIANT_KEY_NAME,
31+
VARIANTS_KEY_NAME,
32+
CONFIGURATION_VALUE_KEY_NAME
33+
} from "./featureManagement/constants.js";
1334
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js";
1435
import { RefreshTimer } from "./refresh/RefreshTimer.js";
1536
import { getConfigurationSettingWithTrace, listConfigurationSettingsWithTrace, requestTracingEnabled } from "./requestTracing/utils.js";
@@ -546,10 +567,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
546567

547568
if (featureFlag[TELEMETRY_KEY_NAME] && featureFlag[TELEMETRY_KEY_NAME][ENABLED_KEY_NAME] === true) {
548569
const metadata = featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME];
570+
let allocationId = "";
571+
if (featureFlag[ALLOCATION_KEY_NAME] !== undefined) {
572+
allocationId = await this.#generateAllocationId(featureFlag);
573+
}
549574
featureFlag[TELEMETRY_KEY_NAME][METADATA_KEY_NAME] = {
550575
[ETAG_KEY_NAME]: setting.etag,
551576
[FEATURE_FLAG_ID_KEY_NAME]: await this.#calculateFeatureFlagId(setting),
552577
[FEATURE_FLAG_REFERENCE_KEY_NAME]: this.#createFeatureFlagReference(setting),
578+
...(allocationId !== "" && { [ALLOCATION_ID_KEY_NAME]: allocationId }),
553579
...(metadata || {})
554580
};
555581
}
@@ -595,6 +621,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
595621
if (crypto.subtle) {
596622
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
597623
const hashArray = new Uint8Array(hashBuffer);
624+
// btoa/atob is also available in Node.js 18+
598625
const base64String = btoa(String.fromCharCode(...hashArray));
599626
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
600627
return base64urlString;
@@ -613,6 +640,116 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
613640
}
614641
return featureFlagReference;
615642
}
643+
644+
async #generateAllocationId(featureFlag: any): Promise<string> {
645+
let rawAllocationId = "";
646+
// Only default variant when enabled and variants allocated by percentile involve in the experimentation
647+
// The allocation id is genearted from default variant when enabled and percentile allocation
648+
const variantsForExperimentation: string[] = [];
649+
650+
rawAllocationId += `seed=${featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] ?? ""}\ndefault_when_enabled=`;
651+
652+
if (featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]) {
653+
variantsForExperimentation.push(featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]);
654+
rawAllocationId += `${featureFlag[ALLOCATION_KEY_NAME][DEFAULT_WHEN_ENABLED_KEY_NAME]}`;
655+
}
656+
657+
rawAllocationId += "\npercentiles=";
658+
659+
const percentileList = featureFlag[ALLOCATION_KEY_NAME][PERCENTILE_KEY_NAME];
660+
if (percentileList) {
661+
const sortedPercentileList = percentileList
662+
.filter(p =>
663+
(p[FROM_KEY_NAME] !== undefined) &&
664+
(p[TO_KEY_NAME] !== undefined) &&
665+
(p[VARIANT_KEY_NAME] !== undefined) &&
666+
(p[FROM_KEY_NAME] !== p[TO_KEY_NAME]))
667+
.sort((a, b) => a[FROM_KEY_NAME] - b[FROM_KEY_NAME]);
668+
669+
const percentileAllocation: string[] = [];
670+
for (const percentile of sortedPercentileList) {
671+
variantsForExperimentation.push(percentile[VARIANT_KEY_NAME]);
672+
percentileAllocation.push(`${percentile[FROM_KEY_NAME]},${base64Helper(percentile[VARIANT_KEY_NAME])},${percentile[TO_KEY_NAME]}`);
673+
}
674+
rawAllocationId += percentileAllocation.join(";");
675+
}
676+
677+
if (variantsForExperimentation.length === 0 && featureFlag[ALLOCATION_KEY_NAME][SEED_KEY_NAME] === undefined) {
678+
// All fields required for generating allocation id are missing, short-circuit and return empty string
679+
return "";
680+
}
681+
682+
rawAllocationId += "\nvariants=";
683+
684+
if (variantsForExperimentation.length !== 0) {
685+
const variantsList = featureFlag[VARIANTS_KEY_NAME];
686+
if (variantsList) {
687+
const sortedVariantsList = variantsList
688+
.filter(v =>
689+
(v[NAME_KEY_NAME] !== undefined) &&
690+
variantsForExperimentation.includes(v[NAME_KEY_NAME]))
691+
.sort((a, b) => (a.name > b.name ? 1 : -1));
692+
693+
const variantConfiguration: string[] = [];
694+
for (const variant of sortedVariantsList) {
695+
const configurationValue = JSON.stringify(variant[CONFIGURATION_VALUE_KEY_NAME], jsonSorter) ?? "";
696+
variantConfiguration.push(`${base64Helper(variant[NAME_KEY_NAME])},${configurationValue}`);
697+
}
698+
rawAllocationId += variantConfiguration.join(";");
699+
}
700+
}
701+
702+
let crypto;
703+
704+
// Check for browser environment
705+
if (typeof window !== "undefined" && window.crypto && window.crypto.subtle) {
706+
crypto = window.crypto;
707+
}
708+
// Check for Node.js environment
709+
else if (typeof global !== "undefined" && global.crypto) {
710+
crypto = global.crypto;
711+
}
712+
// Fallback to native Node.js crypto module
713+
else {
714+
try {
715+
if (typeof module !== "undefined" && module.exports) {
716+
crypto = require("crypto");
717+
}
718+
else {
719+
crypto = await import("crypto");
720+
}
721+
} catch (error) {
722+
console.error("Failed to load the crypto module:", error.message);
723+
throw error;
724+
}
725+
}
726+
727+
// Convert to UTF-8 encoded bytes
728+
const data = new TextEncoder().encode(rawAllocationId);
729+
730+
// In the browser, use crypto.subtle.digest
731+
if (crypto.subtle) {
732+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
733+
const hashArray = new Uint8Array(hashBuffer);
734+
735+
// Only use the first 15 bytes
736+
const first15Bytes = hashArray.slice(0, 15);
737+
738+
// btoa/atob is also available in Node.js 18+
739+
const base64String = btoa(String.fromCharCode(...first15Bytes));
740+
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
741+
return base64urlString;
742+
}
743+
// In Node.js, use the crypto module's hash function
744+
else {
745+
const hash = crypto.createHash("sha256").update(data).digest();
746+
747+
// Only use the first 15 bytes
748+
const first15Bytes = hash.slice(0, 15);
749+
750+
return first15Bytes.toString("base64url");
751+
}
752+
}
616753
}
617754

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

src/common/utils.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
export function base64Helper(str: string): string {
5+
const bytes = new TextEncoder().encode(str); // UTF-8 encoding
6+
let chars = "";
7+
for (let i = 0; i < bytes.length; i++) {
8+
chars += String.fromCharCode(bytes[i]);
9+
}
10+
return btoa(chars);
11+
}
12+
13+
export function jsonSorter(key, value) {
14+
if (value === null) {
15+
return null;
16+
}
17+
if (Array.isArray(value)) {
18+
return value;
19+
}
20+
if (typeof value === "object") {
21+
return Object.fromEntries(Object.entries(value).sort());
22+
}
23+
return value;
24+
}

src/featureManagement/constants.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@
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 PERCENTILE_KEY_NAME = "percentile";
16+
export const FROM_KEY_NAME = "from";
17+
export const TO_KEY_NAME = "to";
18+
export const SEED_KEY_NAME = "seed";
19+
export const VARIANT_KEY_NAME = "variant";
20+
export const VARIANTS_KEY_NAME = "variants";
21+
export const CONFIGURATION_VALUE_KEY_NAME = "configuration_value";
22+
export const ALLOCATION_ID_KEY_NAME = "AllocationId";

0 commit comments

Comments
 (0)