@@ -25,6 +25,7 @@ import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./startupOptions.js";
2525import { DEFAULT_REFRESH_INTERVAL_IN_MS , MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js" ;
2626import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "./keyvault/keyVaultOptions.js" ;
2727import { Disposable } from "./common/disposable.js" ;
28+ import { base64Helper , jsonSorter } from "./common/utils.js" ;
2829import {
2930 FEATURE_FLAGS_KEY_NAME ,
3031 FEATURE_MANAGEMENT_KEY_NAME ,
@@ -34,9 +35,16 @@ import {
3435 METADATA_KEY_NAME ,
3536 ETAG_KEY_NAME ,
3637 FEATURE_FLAG_REFERENCE_KEY_NAME ,
38+ ALLOCATION_ID_KEY_NAME ,
3739 ALLOCATION_KEY_NAME ,
40+ DEFAULT_WHEN_ENABLED_KEY_NAME ,
41+ PERCENTILE_KEY_NAME ,
42+ FROM_KEY_NAME ,
43+ TO_KEY_NAME ,
3844 SEED_KEY_NAME ,
45+ VARIANT_KEY_NAME ,
3946 VARIANTS_KEY_NAME ,
47+ CONFIGURATION_VALUE_KEY_NAME ,
4048 CONDITIONS_KEY_NAME ,
4149 CLIENT_FILTERS_KEY_NAME
4250} from "./featureManagement/constants.js" ;
@@ -642,23 +650,22 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
642650
643651 // try refresh if any of watched settings is changed.
644652 let needRefresh = false ;
645- let changedSentinel ;
646- let changedSentinelWatcher ;
653+ let changedSentinel : WatchedSetting | undefined ;
654+ let changedSentinelWatcher : SettingWatcher | undefined ;
647655 if ( this . #watchAll) {
648656 needRefresh = await this . #checkConfigurationSettingsChange( this . #kvSelectors) ;
649657 } else {
650658 for ( const watchedSetting of this . #sentinels. keys ( ) ) {
651659 const configurationSettingId : ConfigurationSettingId = { key : watchedSetting . key , label : watchedSetting . label , etag : this . #sentinels. get ( watchedSetting ) ?. etag } ;
652- const response = await this . #getConfigurationSetting( configurationSettingId , {
653- onlyIfChanged : true
654- } ) ;
655-
656- const watcher = this . #sentinels. get ( watchedSetting ) ;
657- if ( response ?. statusCode === 200 // created or changed
658- || ( response === undefined && watcher ?. etag !== undefined ) // deleted
659- ) {
660+ const response : GetConfigurationSettingResponse | undefined =
661+ await this . #getConfigurationSetting( configurationSettingId , { onlyIfChanged : true } ) ;
662+
663+ const watcher : SettingWatcher = this . #sentinels. get ( watchedSetting ) ! ; // watcher should always exist for sentinels
664+ const isDeleted = response === undefined && watcher . etag !== undefined ; // previously existed, now deleted
665+ const isChanged = response && response . statusCode === 200 && watcher . etag !== response . etag ; // etag changed
666+ if ( isDeleted || isChanged ) {
660667 changedSentinel = watchedSetting ;
661- changedSentinelWatcher = watcher ;
668+ changedSentinelWatcher = { etag : isChanged ? response . etag : undefined } ;
662669 needRefresh = true ;
663670 break ;
664671 }
@@ -670,7 +677,11 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
670677 await adapter . onChangeDetected ( ) ;
671678 }
672679 await this . #loadSelectedKeyValues( ) ;
673- this . #sentinels. set ( changedSentinel , changedSentinelWatcher ) ; // update the changed sentinel's watcher
680+
681+ if ( changedSentinel && changedSentinelWatcher ) {
682+ // update the changed sentinel's watcher after loading new values, this can ensure a failed refresh will retry on next refresh
683+ this . #sentinels. set ( changedSentinel , changedSentinelWatcher ) ;
684+ }
674685 }
675686
676687 this . #kvRefreshTimer. reset ( ) ;
@@ -964,9 +975,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
964975
965976 if ( featureFlag [ TELEMETRY_KEY_NAME ] && featureFlag [ TELEMETRY_KEY_NAME ] [ ENABLED_KEY_NAME ] === true ) {
966977 const metadata = featureFlag [ TELEMETRY_KEY_NAME ] [ METADATA_KEY_NAME ] ;
978+ let allocationId = "" ;
979+ if ( featureFlag [ ALLOCATION_KEY_NAME ] !== undefined ) {
980+ allocationId = await this . #generateAllocationId( featureFlag ) ;
981+ }
967982 featureFlag [ TELEMETRY_KEY_NAME ] [ METADATA_KEY_NAME ] = {
968983 [ ETAG_KEY_NAME ] : setting . etag ,
969984 [ FEATURE_FLAG_REFERENCE_KEY_NAME ] : this . #createFeatureFlagReference( setting ) ,
985+ ...( allocationId !== "" && { [ ALLOCATION_ID_KEY_NAME ] : allocationId } ) ,
970986 ...( metadata || { } )
971987 } ;
972988 }
@@ -1004,6 +1020,116 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
10041020 }
10051021 }
10061022 }
1023+
1024+ async #generateAllocationId( featureFlag : any ) : Promise < string > {
1025+ let rawAllocationId = "" ;
1026+ // Only default variant when enabled and variants allocated by percentile involve in the experimentation
1027+ // The allocation id is genearted from default variant when enabled and percentile allocation
1028+ const variantsForExperimentation : string [ ] = [ ] ;
1029+
1030+ rawAllocationId += `seed=${ featureFlag [ ALLOCATION_KEY_NAME ] [ SEED_KEY_NAME ] ?? "" } \ndefault_when_enabled=` ;
1031+
1032+ if ( featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] ) {
1033+ variantsForExperimentation . push ( featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] ) ;
1034+ rawAllocationId += `${ featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] } ` ;
1035+ }
1036+
1037+ rawAllocationId += "\npercentiles=" ;
1038+
1039+ const percentileList = featureFlag [ ALLOCATION_KEY_NAME ] [ PERCENTILE_KEY_NAME ] ;
1040+ if ( percentileList ) {
1041+ const sortedPercentileList = percentileList
1042+ . filter ( p =>
1043+ ( p [ FROM_KEY_NAME ] !== undefined ) &&
1044+ ( p [ TO_KEY_NAME ] !== undefined ) &&
1045+ ( p [ VARIANT_KEY_NAME ] !== undefined ) &&
1046+ ( p [ FROM_KEY_NAME ] !== p [ TO_KEY_NAME ] ) )
1047+ . sort ( ( a , b ) => a [ FROM_KEY_NAME ] - b [ FROM_KEY_NAME ] ) ;
1048+
1049+ const percentileAllocation : string [ ] = [ ] ;
1050+ for ( const percentile of sortedPercentileList ) {
1051+ variantsForExperimentation . push ( percentile [ VARIANT_KEY_NAME ] ) ;
1052+ percentileAllocation . push ( `${ percentile [ FROM_KEY_NAME ] } ,${ base64Helper ( percentile [ VARIANT_KEY_NAME ] ) } ,${ percentile [ TO_KEY_NAME ] } ` ) ;
1053+ }
1054+ rawAllocationId += percentileAllocation . join ( ";" ) ;
1055+ }
1056+
1057+ if ( variantsForExperimentation . length === 0 && featureFlag [ ALLOCATION_KEY_NAME ] [ SEED_KEY_NAME ] === undefined ) {
1058+ // All fields required for generating allocation id are missing, short-circuit and return empty string
1059+ return "" ;
1060+ }
1061+
1062+ rawAllocationId += "\nvariants=" ;
1063+
1064+ if ( variantsForExperimentation . length !== 0 ) {
1065+ const variantsList = featureFlag [ VARIANTS_KEY_NAME ] ;
1066+ if ( variantsList ) {
1067+ const sortedVariantsList = variantsList
1068+ . filter ( v =>
1069+ ( v [ NAME_KEY_NAME ] !== undefined ) &&
1070+ variantsForExperimentation . includes ( v [ NAME_KEY_NAME ] ) )
1071+ . sort ( ( a , b ) => ( a . name > b . name ? 1 : - 1 ) ) ;
1072+
1073+ const variantConfiguration : string [ ] = [ ] ;
1074+ for ( const variant of sortedVariantsList ) {
1075+ const configurationValue = JSON . stringify ( variant [ CONFIGURATION_VALUE_KEY_NAME ] , jsonSorter ) ?? "" ;
1076+ variantConfiguration . push ( `${ base64Helper ( variant [ NAME_KEY_NAME ] ) } ,${ configurationValue } ` ) ;
1077+ }
1078+ rawAllocationId += variantConfiguration . join ( ";" ) ;
1079+ }
1080+ }
1081+
1082+ let crypto ;
1083+
1084+ // Check for browser environment
1085+ if ( typeof window !== "undefined" && window . crypto && window . crypto . subtle ) {
1086+ crypto = window . crypto ;
1087+ }
1088+ // Check for Node.js environment
1089+ else if ( typeof global !== "undefined" && global . crypto ) {
1090+ crypto = global . crypto ;
1091+ }
1092+ // Fallback to native Node.js crypto module
1093+ else {
1094+ try {
1095+ if ( typeof module !== "undefined" && module . exports ) {
1096+ crypto = require ( "crypto" ) ;
1097+ }
1098+ else {
1099+ crypto = await import ( "crypto" ) ;
1100+ }
1101+ } catch ( error ) {
1102+ console . error ( "Failed to load the crypto module:" , error . message ) ;
1103+ throw error ;
1104+ }
1105+ }
1106+
1107+ // Convert to UTF-8 encoded bytes
1108+ const data = new TextEncoder ( ) . encode ( rawAllocationId ) ;
1109+
1110+ // In the browser, use crypto.subtle.digest
1111+ if ( crypto . subtle ) {
1112+ const hashBuffer = await crypto . subtle . digest ( "SHA-256" , data ) ;
1113+ const hashArray = new Uint8Array ( hashBuffer ) ;
1114+
1115+ // Only use the first 15 bytes
1116+ const first15Bytes = hashArray . slice ( 0 , 15 ) ;
1117+
1118+ // btoa/atob is also available in Node.js 18+
1119+ const base64String = btoa ( String . fromCharCode ( ...first15Bytes ) ) ;
1120+ const base64urlString = base64String . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / , "" ) ;
1121+ return base64urlString ;
1122+ }
1123+ // In Node.js, use the crypto module's hash function
1124+ else {
1125+ const hash = crypto . createHash ( "sha256" ) . update ( data ) . digest ( ) ;
1126+
1127+ // Only use the first 15 bytes
1128+ const first15Bytes = hash . slice ( 0 , 15 ) ;
1129+
1130+ return first15Bytes . toString ( "base64url" ) ;
1131+ }
1132+ }
10071133}
10081134
10091135function getValidSettingSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
0 commit comments