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