@@ -17,9 +17,10 @@ import {
1717 GetSnapshotOptions ,
1818 ListConfigurationSettingsForSnapshotOptions ,
1919 GetSnapshotResponse ,
20- KnownSnapshotComposition
20+ KnownSnapshotComposition ,
21+ ListConfigurationSettingPage
2122} from "@azure/app-configuration" ;
22- import { isRestError } from "@azure/core-rest-pipeline" ;
23+ import { isRestError , RestError } from "@azure/core-rest-pipeline" ;
2324import { AzureAppConfiguration , ConfigurationObjectConstructionOptions } from "./appConfiguration.js" ;
2425import { AzureAppConfigurationOptions } from "./appConfigurationOptions.js" ;
2526import { IKeyValueAdapter } from "./keyValueAdapter.js" ;
@@ -28,6 +29,7 @@ import { DEFAULT_STARTUP_TIMEOUT_IN_MS } from "./startupOptions.js";
2829import { DEFAULT_REFRESH_INTERVAL_IN_MS , MIN_REFRESH_INTERVAL_IN_MS } from "./refresh/refreshOptions.js" ;
2930import { MIN_SECRET_REFRESH_INTERVAL_IN_MS } from "./keyvault/keyVaultOptions.js" ;
3031import { Disposable } from "./common/disposable.js" ;
32+ import { base64Helper , jsonSorter } from "./common/utils.js" ;
3133import {
3234 FEATURE_FLAGS_KEY_NAME ,
3335 FEATURE_MANAGEMENT_KEY_NAME ,
@@ -37,9 +39,16 @@ import {
3739 METADATA_KEY_NAME ,
3840 ETAG_KEY_NAME ,
3941 FEATURE_FLAG_REFERENCE_KEY_NAME ,
42+ ALLOCATION_ID_KEY_NAME ,
4043 ALLOCATION_KEY_NAME ,
44+ DEFAULT_WHEN_ENABLED_KEY_NAME ,
45+ PERCENTILE_KEY_NAME ,
46+ FROM_KEY_NAME ,
47+ TO_KEY_NAME ,
4148 SEED_KEY_NAME ,
49+ VARIANT_KEY_NAME ,
4250 VARIANTS_KEY_NAME ,
51+ CONFIGURATION_VALUE_KEY_NAME ,
4352 CONDITIONS_KEY_NAME ,
4453 CLIENT_FILTERS_KEY_NAME
4554} from "./featureManagement/constants.js" ;
@@ -49,6 +58,7 @@ import { AzureKeyVaultKeyValueAdapter } from "./keyvault/keyVaultKeyValueAdapter
4958import { RefreshTimer } from "./refresh/refreshTimer.js" ;
5059import {
5160 RequestTracingOptions ,
61+ checkConfigurationSettingsWithTrace ,
5262 getConfigurationSettingWithTrace ,
5363 listConfigurationSettingsWithTrace ,
5464 getSnapshotWithTrace ,
@@ -63,6 +73,7 @@ import { getFixedBackoffDuration, getExponentialBackoffDuration } from "./common
6373import { getStatusCode } from "./common/utils.js" ;
6474import { InvalidOperationError , ArgumentError , isFailoverableError , isInputError , SnapshotReferenceError } from "./common/errors.js" ;
6575import { ErrorMessages } from "./common/errorMessages.js" ;
76+ import { X_MS_DATE_HEADER } from "./afd/constants.js" ;
6677
6778const MIN_DELAY_FOR_UNHANDLED_FAILURE = 5_000 ; // 5 seconds
6879
@@ -124,12 +135,17 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
124135 // Load balancing
125136 #lastSuccessfulEndpoint: string = "" ;
126137
138+ // Azure Front Door
139+ #isAfdUsed: boolean = false ;
140+
127141 constructor (
128142 clientManager : ConfigurationClientManager ,
129- options ?: AzureAppConfigurationOptions ,
143+ options : AzureAppConfigurationOptions | undefined ,
144+ isAfdUsed : boolean
130145 ) {
131146 this . #options = options ;
132147 this . #clientManager = clientManager ;
148+ this . #isAfdUsed = isAfdUsed ;
133149
134150 // enable request tracing if not opt-out
135151 this . #requestTracingEnabled = requestTracingEnabled ( ) ;
@@ -217,6 +233,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
217233 featureFlagTracing : this . #featureFlagTracing,
218234 fmVersion : this . #fmVersion,
219235 aiConfigurationTracing : this . #aiConfigurationTracing,
236+ isAfdUsed : this . #isAfdUsed,
220237 useSnapshotReference : this . #useSnapshotReference
221238 } ;
222239 }
@@ -487,7 +504,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
487504 * If false, loads key-value using the key-value selectors. Defaults to false.
488505 */
489506 async #loadConfigurationSettings( loadFeatureFlag : boolean = false ) : Promise < ConfigurationSetting [ ] > {
490- const selectors = loadFeatureFlag ? this . #ffSelectors : this . #kvSelectors;
507+ const selectors : PagedSettingsWatcher [ ] = loadFeatureFlag ? this . #ffSelectors : this . #kvSelectors;
491508
492509 // Use a Map to deduplicate configuration settings by key. When multiple selectors return settings with the same key,
493510 // the configuration setting loaded by the later selector in the iteration order will override the one from the earlier selector.
@@ -504,6 +521,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
504521 tagsFilter : selector . tagFilters
505522 } ;
506523 const { items, pageWatchers } = await this . #listConfigurationSettings( listOptions ) ;
524+
507525 selector . pageWatchers = pageWatchers ;
508526 settings = items ;
509527 } else { // snapshot selector
@@ -691,7 +709,9 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
691709 return Promise . resolve ( false ) ;
692710 }
693711
694- const needRefresh = await this . #checkConfigurationSettingsChange( this . #ffSelectors) ;
712+ let needRefresh = false ;
713+ needRefresh = await this . #checkConfigurationSettingsChange( this . #ffSelectors) ;
714+
695715 if ( needRefresh ) {
696716 await this . #loadFeatureFlags( ) ;
697717 }
@@ -741,21 +761,38 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
741761 const listOptions : ListConfigurationSettingsOptions = {
742762 keyFilter : selector . keyFilter ,
743763 labelFilter : selector . labelFilter ,
744- tagsFilter : selector . tagFilters ,
745- pageEtags : pageWatchers . map ( w => w . etag ?? "" )
764+ tagsFilter : selector . tagFilters
746765 } ;
747766
748- const pageIterator = listConfigurationSettingsWithTrace (
767+ if ( ! this . #isAfdUsed) {
768+ // if AFD is not used, add page etags to the listOptions to send conditional request
769+ listOptions . pageEtags = pageWatchers . map ( w => w . etag ?? "" ) ;
770+ }
771+
772+ const pageIterator = checkConfigurationSettingsWithTrace (
749773 this . #requestTraceOptions,
750774 client ,
751775 listOptions
752776 ) . byPage ( ) ;
753777
778+ let i = 0 ;
754779 for await ( const page of pageIterator ) {
755- // when conditional request is sent, the response will be 304 if not changed
756- if ( getStatusCode ( page . _response . status ) === 200 ) { // created or changed
780+ const serverResponseTime : Date = this . #getMsDateHeader ( page ) ;
781+ if ( i >= pageWatchers . length ) {
757782 return true ;
758783 }
784+
785+ const lastServerResponseTime = pageWatchers [ i ] . lastServerResponseTime ;
786+ let isResponseFresh = false ;
787+ if ( lastServerResponseTime !== undefined ) {
788+ isResponseFresh = serverResponseTime > lastServerResponseTime ;
789+ }
790+ if ( isResponseFresh &&
791+ page . _response . status === 200 && // conditional request returns 304 if not changed
792+ page . etag !== pageWatchers [ i ] . etag ) {
793+ return true ;
794+ }
795+ i ++ ;
759796 }
760797 }
761798 return false ;
@@ -766,7 +803,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
766803 }
767804
768805 /**
769- * Gets a configuration setting by key and label.If the setting is not found, return undefine instead of throwing an error.
806+ * Gets a configuration setting by key and label. If the setting is not found, return undefined instead of throwing an error.
770807 */
771808 async #getConfigurationSetting( configurationSettingId : ConfigurationSettingId , getOptions ?: GetConfigurationSettingOptions ) : Promise < GetConfigurationSettingResponse | undefined > {
772809 const funcToExecute = async ( client : AppConfigurationClient ) => {
@@ -802,7 +839,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
802839
803840 const items : ConfigurationSetting [ ] = [ ] ;
804841 for await ( const page of pageIterator ) {
805- pageWatchers . push ( { etag : page . etag } ) ;
842+ pageWatchers . push ( { etag : page . etag , lastServerResponseTime : this . #getMsDateHeader ( page ) } ) ;
806843 items . push ( ...page . items ) ;
807844 }
808845 return { items, pageWatchers } ;
@@ -954,9 +991,14 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
954991
955992 if ( featureFlag [ TELEMETRY_KEY_NAME ] && featureFlag [ TELEMETRY_KEY_NAME ] [ ENABLED_KEY_NAME ] === true ) {
956993 const metadata = featureFlag [ TELEMETRY_KEY_NAME ] [ METADATA_KEY_NAME ] ;
994+ let allocationId = "" ;
995+ if ( featureFlag [ ALLOCATION_KEY_NAME ] !== undefined ) {
996+ allocationId = await this . #generateAllocationId( featureFlag ) ;
997+ }
957998 featureFlag [ TELEMETRY_KEY_NAME ] [ METADATA_KEY_NAME ] = {
958999 [ ETAG_KEY_NAME ] : setting . etag ,
9591000 [ FEATURE_FLAG_REFERENCE_KEY_NAME ] : this . #createFeatureFlagReference( setting ) ,
1001+ ...( allocationId !== "" && { [ ALLOCATION_ID_KEY_NAME ] : allocationId } ) ,
9601002 ...( metadata || { } )
9611003 } ;
9621004 }
@@ -994,6 +1036,135 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
9941036 }
9951037 }
9961038 }
1039+
1040+ async #generateAllocationId( featureFlag : any ) : Promise < string > {
1041+ let rawAllocationId = "" ;
1042+ // Only default variant when enabled and variants allocated by percentile involve in the experimentation
1043+ // The allocation id is genearted from default variant when enabled and percentile allocation
1044+ const variantsForExperimentation : string [ ] = [ ] ;
1045+
1046+ rawAllocationId += `seed=${ featureFlag [ ALLOCATION_KEY_NAME ] [ SEED_KEY_NAME ] ?? "" } \ndefault_when_enabled=` ;
1047+
1048+ if ( featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] ) {
1049+ variantsForExperimentation . push ( featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] ) ;
1050+ rawAllocationId += `${ featureFlag [ ALLOCATION_KEY_NAME ] [ DEFAULT_WHEN_ENABLED_KEY_NAME ] } ` ;
1051+ }
1052+
1053+ rawAllocationId += "\npercentiles=" ;
1054+
1055+ const percentileList = featureFlag [ ALLOCATION_KEY_NAME ] [ PERCENTILE_KEY_NAME ] ;
1056+ if ( percentileList ) {
1057+ const sortedPercentileList = percentileList
1058+ . filter ( p =>
1059+ ( p [ FROM_KEY_NAME ] !== undefined ) &&
1060+ ( p [ TO_KEY_NAME ] !== undefined ) &&
1061+ ( p [ VARIANT_KEY_NAME ] !== undefined ) &&
1062+ ( p [ FROM_KEY_NAME ] !== p [ TO_KEY_NAME ] ) )
1063+ . sort ( ( a , b ) => a [ FROM_KEY_NAME ] - b [ FROM_KEY_NAME ] ) ;
1064+
1065+ const percentileAllocation : string [ ] = [ ] ;
1066+ for ( const percentile of sortedPercentileList ) {
1067+ variantsForExperimentation . push ( percentile [ VARIANT_KEY_NAME ] ) ;
1068+ percentileAllocation . push ( `${ percentile [ FROM_KEY_NAME ] } ,${ base64Helper ( percentile [ VARIANT_KEY_NAME ] ) } ,${ percentile [ TO_KEY_NAME ] } ` ) ;
1069+ }
1070+ rawAllocationId += percentileAllocation . join ( ";" ) ;
1071+ }
1072+
1073+ if ( variantsForExperimentation . length === 0 && featureFlag [ ALLOCATION_KEY_NAME ] [ SEED_KEY_NAME ] === undefined ) {
1074+ // All fields required for generating allocation id are missing, short-circuit and return empty string
1075+ return "" ;
1076+ }
1077+
1078+ rawAllocationId += "\nvariants=" ;
1079+
1080+ if ( variantsForExperimentation . length !== 0 ) {
1081+ const variantsList = featureFlag [ VARIANTS_KEY_NAME ] ;
1082+ if ( variantsList ) {
1083+ const sortedVariantsList = variantsList
1084+ . filter ( v =>
1085+ ( v [ NAME_KEY_NAME ] !== undefined ) &&
1086+ variantsForExperimentation . includes ( v [ NAME_KEY_NAME ] ) )
1087+ . sort ( ( a , b ) => ( a . name > b . name ? 1 : - 1 ) ) ;
1088+
1089+ const variantConfiguration : string [ ] = [ ] ;
1090+ for ( const variant of sortedVariantsList ) {
1091+ const configurationValue = JSON . stringify ( variant [ CONFIGURATION_VALUE_KEY_NAME ] , jsonSorter ) ?? "" ;
1092+ variantConfiguration . push ( `${ base64Helper ( variant [ NAME_KEY_NAME ] ) } ,${ configurationValue } ` ) ;
1093+ }
1094+ rawAllocationId += variantConfiguration . join ( ";" ) ;
1095+ }
1096+ }
1097+
1098+ let crypto ;
1099+
1100+ // Check for browser environment
1101+ if ( typeof window !== "undefined" && window . crypto && window . crypto . subtle ) {
1102+ crypto = window . crypto ;
1103+ }
1104+ // Check for Node.js environment
1105+ else if ( typeof global !== "undefined" && global . crypto ) {
1106+ crypto = global . crypto ;
1107+ }
1108+ // Fallback to native Node.js crypto module
1109+ else {
1110+ try {
1111+ if ( typeof module !== "undefined" && module . exports ) {
1112+ crypto = require ( "crypto" ) ;
1113+ }
1114+ else {
1115+ crypto = await import ( "crypto" ) ;
1116+ }
1117+ } catch ( error ) {
1118+ console . error ( "Failed to load the crypto module:" , error . message ) ;
1119+ throw error ;
1120+ }
1121+ }
1122+
1123+ // Convert to UTF-8 encoded bytes
1124+ const data = new TextEncoder ( ) . encode ( rawAllocationId ) ;
1125+
1126+ // In the browser, use crypto.subtle.digest
1127+ if ( crypto . subtle ) {
1128+ const hashBuffer = await crypto . subtle . digest ( "SHA-256" , data ) ;
1129+ const hashArray = new Uint8Array ( hashBuffer ) ;
1130+
1131+ // Only use the first 15 bytes
1132+ const first15Bytes = hashArray . slice ( 0 , 15 ) ;
1133+
1134+ // btoa/atob is also available in Node.js 18+
1135+ const base64String = btoa ( String . fromCharCode ( ...first15Bytes ) ) ;
1136+ const base64urlString = base64String . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / , "" ) ;
1137+ return base64urlString ;
1138+ }
1139+ // In Node.js, use the crypto module's hash function
1140+ else {
1141+ const hash = crypto . createHash ( "sha256" ) . update ( data ) . digest ( ) ;
1142+
1143+ // Only use the first 15 bytes
1144+ const first15Bytes = hash . slice ( 0 , 15 ) ;
1145+
1146+ return first15Bytes . toString ( "base64url" ) ;
1147+ }
1148+ }
1149+
1150+ /**
1151+ * Extracts the response timestamp (x-ms-date) from the response headers. If not found, returns the current time.
1152+ */
1153+ #getMsDateHeader( response : GetConfigurationSettingResponse | ListConfigurationSettingPage | RestError ) : Date {
1154+ let header : string | undefined ;
1155+ if ( isRestError ( response ) ) {
1156+ header = response . response ?. headers ?. get ( X_MS_DATE_HEADER ) ;
1157+ } else {
1158+ header = response . _response ?. headers ?. get ( X_MS_DATE_HEADER ) ;
1159+ }
1160+ if ( header !== undefined ) {
1161+ const date = new Date ( header ) ;
1162+ if ( ! isNaN ( date . getTime ( ) ) ) {
1163+ return date ;
1164+ }
1165+ }
1166+ return new Date ( ) ;
1167+ }
9971168}
9981169
9991170function getValidSettingSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
0 commit comments