@@ -9,7 +9,28 @@ import { IKeyValueAdapter } from "./IKeyValueAdapter.js";
9
9
import { JsonKeyValueAdapter } from "./JsonKeyValueAdapter.js" ;
10
10
import { DEFAULT_REFRESH_INTERVAL_IN_MS , MIN_REFRESH_INTERVAL_IN_MS } from "./RefreshOptions.js" ;
11
11
import { 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" ;
13
34
import { AzureKeyVaultKeyValueAdapter } from "./keyvault/AzureKeyVaultKeyValueAdapter.js" ;
14
35
import { RefreshTimer } from "./refresh/RefreshTimer.js" ;
15
36
import { getConfigurationSettingWithTrace , listConfigurationSettingsWithTrace , requestTracingEnabled } from "./requestTracing/utils.js" ;
@@ -546,10 +567,15 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
546
567
547
568
if ( featureFlag [ TELEMETRY_KEY_NAME ] && featureFlag [ TELEMETRY_KEY_NAME ] [ ENABLED_KEY_NAME ] === true ) {
548
569
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
+ }
549
574
featureFlag [ TELEMETRY_KEY_NAME ] [ METADATA_KEY_NAME ] = {
550
575
[ ETAG_KEY_NAME ] : setting . etag ,
551
576
[ FEATURE_FLAG_ID_KEY_NAME ] : await this . #calculateFeatureFlagId( setting ) ,
552
577
[ FEATURE_FLAG_REFERENCE_KEY_NAME ] : this . #createFeatureFlagReference( setting ) ,
578
+ ...( allocationId !== "" && { [ ALLOCATION_ID_KEY_NAME ] : allocationId } ) ,
553
579
...( metadata || { } )
554
580
} ;
555
581
}
@@ -595,6 +621,7 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
595
621
if ( crypto . subtle ) {
596
622
const hashBuffer = await crypto . subtle . digest ( "SHA-256" , data ) ;
597
623
const hashArray = new Uint8Array ( hashBuffer ) ;
624
+ // btoa/atob is also available in Node.js 18+
598
625
const base64String = btoa ( String . fromCharCode ( ...hashArray ) ) ;
599
626
const base64urlString = base64String . replace ( / \+ / g, "-" ) . replace ( / \/ / g, "_" ) . replace ( / = + $ / , "" ) ;
600
627
return base64urlString ;
@@ -613,6 +640,116 @@ export class AzureAppConfigurationImpl implements AzureAppConfiguration {
613
640
}
614
641
return featureFlagReference ;
615
642
}
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
+ }
616
753
}
617
754
618
755
function getValidSelectors ( selectors : SettingSelector [ ] ) : SettingSelector [ ] {
0 commit comments