@@ -9,6 +9,7 @@ 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 { base64Helper , jsonSorter } from "./common/utils.js" ;
1213import {
1314 FEATURE_FLAGS_KEY_NAME ,
1415 FEATURE_MANAGEMENT_KEY_NAME ,
@@ -19,9 +20,16 @@ import {
1920 ETAG_KEY_NAME ,
2021 FEATURE_FLAG_ID_KEY_NAME ,
2122 FEATURE_FLAG_REFERENCE_KEY_NAME ,
23+ ALLOCATION_ID_KEY_NAME ,
2224 ALLOCATION_KEY_NAME ,
25+ DEFAULT_WHEN_ENABLED_KEY_NAME ,
26+ PERCENTILE_KEY_NAME ,
27+ FROM_KEY_NAME ,
28+ TO_KEY_NAME ,
2329 SEED_KEY_NAME ,
30+ VARIANT_KEY_NAME ,
2431 VARIANTS_KEY_NAME ,
32+ CONFIGURATION_VALUE_KEY_NAME ,
2533 CONDITIONS_KEY_NAME ,
2634 CLIENT_FILTERS_KEY_NAME
2735} from "./featureManagement/constants.js" ;
@@ -669,10 +677,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
669677
670678 if ( featureFlag [ TELEMETRY_KEY_NAME ] && featureFlag [ TELEMETRY_KEY_NAME ] [ ENABLED_KEY_NAME ] === true ) {
671679 const metadata = featureFlag [ TELEMETRY_KEY_NAME ] [ METADATA_KEY_NAME ] ;
680+ let allocationId = "" ;
681+ if ( featureFlag [ ALLOCATION_KEY_NAME ] !== undefined ) {
682+ allocationId = await this . #generateAllocationId( featureFlag ) ;
683+ }
672684 featureFlag [ TELEMETRY_KEY_NAME ] [ METADATA_KEY_NAME ] = {
673685 [ ETAG_KEY_NAME ] : setting . etag ,
674686 [ FEATURE_FLAG_ID_KEY_NAME ] : await this . #calculateFeatureFlagId( setting ) ,
675687 [ FEATURE_FLAG_REFERENCE_KEY_NAME ] : this . #createFeatureFlagReference( setting ) ,
688+ ...( allocationId !== "" && { [ ALLOCATION_ID_KEY_NAME ] : allocationId } ) ,
676689 ...( metadata || { } )
677690 } ;
678691 }
@@ -756,6 +769,116 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
756769 }
757770 return featureFlagReference ;
758771 }
772+
773+ async #generateAllocationId( featureFlag : any ) : Promise < string > {
774+ let rawAllocationId = "" ;
775+ // Only default variant when enabled and variants allocated by percentile involve in the experimentation
776+ // The allocation id is genearted from default variant when enabled and percentile allocation
777+ const variantsForExperimentation : string [ ] = [ ] ;
778+
779+ rawAllocationId += `seed=${ featureFlag [ ALLOCATION_KEY_NAME ] [ SEED_KEY_NAME ] ?? "" } \ndefault_when_enabled=` ;
780+
781+ if ( featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] ) {
782+ variantsForExperimentation . push ( featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] ) ;
783+ rawAllocationId += `${ featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] } ` ;
784+ }
785+
786+ rawAllocationId += "\npercentiles=" ;
787+
788+ const percentileList = featureFlag [ ALLOCATION_KEY_NAME ] [ PERCENTILE_KEY_NAME ] ;
789+ if ( percentileList ) {
790+ const sortedPercentileList = percentileList
791+ . filter ( p =>
792+ ( p [ FROM_KEY_NAME ] !== undefined ) &&
793+ ( p [ TO_KEY_NAME ] !== undefined ) &&
794+ ( p [ VARIANT_KEY_NAME ] !== undefined ) &&
795+ ( p [ FROM_KEY_NAME ] !== p [ TO_KEY_NAME ] ) )
796+ . sort ( ( a , b ) => a [ FROM_KEY_NAME ] - b [ FROM_KEY_NAME ] ) ;
797+
798+ const percentileAllocation : string [ ] = [ ] ;
799+ for ( const percentile of sortedPercentileList ) {
800+ variantsForExperimentation . push ( percentile [ VARIANT_KEY_NAME ] ) ;
801+ percentileAllocation . push ( `${ percentile [ FROM_KEY_NAME ] } ,${ base64Helper ( percentile [ VARIANT_KEY_NAME ] ) } ,${ percentile [ TO_KEY_NAME ] } ` ) ;
802+ }
803+ rawAllocationId += percentileAllocation . join ( ";" ) ;
804+ }
805+
806+ if ( variantsForExperimentation . length === 0 && featureFlag [ ALLOCATION_KEY_NAME ] [ SEED_KEY_NAME ] === undefined ) {
807+ // All fields required for generating allocation id are missing, short-circuit and return empty string
808+ return "" ;
809+ }
810+
811+ rawAllocationId += "\nvariants=" ;
812+
813+ if ( variantsForExperimentation . length !== 0 ) {
814+ const variantsList = featureFlag [ VARIANTS_KEY_NAME ] ;
815+ if ( variantsList ) {
816+ const sortedVariantsList = variantsList
817+ . filter ( v =>
818+ ( v [ NAME_KEY_NAME ] !== undefined ) &&
819+ variantsForExperimentation . includes ( v [ NAME_KEY_NAME ] ) )
820+ . sort ( ( a , b ) => ( a . name > b . name ? 1 : - 1 ) ) ;
821+
822+ const variantConfiguration : string [ ] = [ ] ;
823+ for ( const variant of sortedVariantsList ) {
824+ const configurationValue = JSON . stringify ( variant [ CONFIGURATION_VALUE_KEY_NAME ] , jsonSorter ) ?? "" ;
825+ variantConfiguration . push ( `${ base64Helper ( variant [ NAME_KEY_NAME ] ) } ,${ configurationValue } ` ) ;
826+ }
827+ rawAllocationId += variantConfiguration . join ( ";" ) ;
828+ }
829+ }
830+
831+ let crypto ;
832+
833+ // Check for browser environment
834+ if ( typeof window !== "undefined" && window . crypto && window . crypto . subtle ) {
835+ crypto = window . crypto ;
836+ }
837+ // Check for Node.js environment
838+ else if ( typeof global !== "undefined" && global . crypto ) {
839+ crypto = global . crypto ;
840+ }
841+ // Fallback to native Node.js crypto module
842+ else {
843+ try {
844+ if ( typeof module !== "undefined" && module . exports ) {
845+ crypto = require ( "crypto" ) ;
846+ }
847+ else {
848+ crypto = await import ( "crypto" ) ;
849+ }
850+ } catch ( error ) {
851+ console . error ( "Failed to load the crypto module:" , error . message ) ;
852+ throw error ;
853+ }
854+ }
855+
856+ // Convert to UTF-8 encoded bytes
857+ const data = new TextEncoder ( ) . encode ( rawAllocationId ) ;
858+
859+ // In the browser, use crypto.subtle.digest
860+ if ( crypto . subtle ) {
861+ const hashBuffer = await crypto . subtle . digest ( "SHA-256" , data ) ;
862+ const hashArray = new Uint8Array ( hashBuffer ) ;
863+
864+ // Only use the first 15 bytes
865+ const first15Bytes = hashArray . slice ( 0 , 15 ) ;
866+
867+ // btoa/atob is also available in Node.js 18+
868+ const base64String = btoa ( String . fromCharCode ( ...first15Bytes ) ) ;
869+ const base64urlString = base64String . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / , "" ) ;
870+ return base64urlString ;
871+ }
872+ // In Node.js, use the crypto module's hash function
873+ else {
874+ const hash = crypto . createHash ( "sha256" ) . update ( data ) . digest ( ) ;
875+
876+ // Only use the first 15 bytes
877+ const first15Bytes = hash . slice ( 0 , 15 ) ;
878+
879+ return first15Bytes . toString ( "base64url" ) ;
880+ }
881+ }
759882}
760883
761884function getValidSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
0 commit comments