@@ -9,7 +9,28 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
99import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js" ;
1010import { DEFAULT_REFRESH_INTERVAL_IN_MS , MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js" ;
1111import { 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" ;
1334import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js" ;
1435import { RefreshTimer } from "./refresh/RefreshTimer.js" ;
1536import { 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
618755function getValidSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
0 commit comments