Skip to content

Commit 51d4ad7

Browse files
authored
Merge pull request #600 from Azure/rossgrambo/allocation_id
Add allocation id when telemetry is enabled for feature flag
2 parents d551536 + 603cd0f commit 51d4ad7

File tree

5 files changed

+355
-0
lines changed

5 files changed

+355
-0
lines changed

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/Extensions/StringExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT license.
33
//
4+
using System;
5+
46
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions
57
{
68
internal static class LabelFilters
@@ -16,5 +18,12 @@ public static string NormalizeNull(this string s)
1618
{
1719
return s == LabelFilters.Null ? null : s;
1820
}
21+
22+
public static string ToBase64String(this string s)
23+
{
24+
byte[] bytes = System.Text.Encoding.UTF8.GetBytes(s);
25+
26+
return Convert.ToBase64String(bytes);
27+
}
1928
}
2029
}

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ internal class FeatureManagementConstants
4444
public const string ETag = "ETag";
4545
public const string FeatureFlagId = "FeatureFlagId";
4646
public const string FeatureFlagReference = "FeatureFlagReference";
47+
public const string AllocationId = "AllocationId";
4748

4849
// Dotnet schema keys
4950
public const string DotnetSchemaSectionName = "FeatureManagement";

src/Microsoft.Extensions.Configuration.AzureAppConfiguration/FeatureManagement/FeatureManagementKeyValueAdapter.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.Configuration.AzureAppConfiguration.Extensions;
66
using System;
77
using System.Collections.Generic;
8+
using System.Diagnostics;
89
using System.Linq;
910
using System.Security.Cryptography;
1011
using System.Text;
@@ -319,12 +320,98 @@ private List<KeyValuePair<string, string>> ProcessMicrosoftSchemaFeatureFlag(Fea
319320
keyValues.Add(new KeyValuePair<string, string>($"{telemetryPath}:{FeatureManagementConstants.Metadata}:{FeatureManagementConstants.ETag}", setting.ETag.ToString()));
320321

321322
keyValues.Add(new KeyValuePair<string, string>($"{telemetryPath}:{FeatureManagementConstants.Enabled}", telemetry.Enabled.ToString()));
323+
324+
if (featureFlag.Allocation != null)
325+
{
326+
string allocationId = CalculateAllocationId(featureFlag);
327+
328+
if (allocationId != null)
329+
{
330+
keyValues.Add(new KeyValuePair<string, string>($"{telemetryPath}:{FeatureManagementConstants.Metadata}:{FeatureManagementConstants.AllocationId}", allocationId));
331+
}
332+
}
322333
}
323334
}
324335

325336
return keyValues;
326337
}
327338

339+
private string CalculateAllocationId(FeatureFlag flag)
340+
{
341+
Debug.Assert(flag.Allocation != null);
342+
343+
StringBuilder inputBuilder = new StringBuilder();
344+
345+
// Seed
346+
inputBuilder.Append($"seed={flag.Allocation.Seed ?? string.Empty}");
347+
348+
var allocatedVariants = new HashSet<string>();
349+
350+
// DefaultWhenEnabled
351+
if (flag.Allocation.DefaultWhenEnabled != null)
352+
{
353+
allocatedVariants.Add(flag.Allocation.DefaultWhenEnabled);
354+
}
355+
356+
inputBuilder.Append($"\ndefault_when_enabled={flag.Allocation.DefaultWhenEnabled ?? string.Empty}");
357+
358+
// Percentiles
359+
inputBuilder.Append("\npercentiles=");
360+
361+
if (flag.Allocation.Percentile != null && flag.Allocation.Percentile.Any())
362+
{
363+
IEnumerable<FeaturePercentileAllocation> sortedPercentiles = flag.Allocation.Percentile
364+
.Where(p => p.From != p.To)
365+
.OrderBy(p => p.From)
366+
.ToList();
367+
368+
allocatedVariants.UnionWith(sortedPercentiles.Select(p => p.Variant));
369+
370+
inputBuilder.Append(string.Join(";", sortedPercentiles.Select(p => $"{p.From},{p.Variant.ToBase64String()},{p.To}")));
371+
}
372+
373+
// If there's no custom seed and no variants allocated, stop now and return null
374+
if (flag.Allocation.Seed == null &&
375+
!allocatedVariants.Any())
376+
{
377+
return null;
378+
}
379+
380+
// Variants
381+
inputBuilder.Append("\nvariants=");
382+
383+
if (allocatedVariants.Any() && flag.Variants != null && flag.Variants.Any())
384+
{
385+
IEnumerable<FeatureVariant> sortedVariants = flag.Variants
386+
.Where(variant => allocatedVariants.Contains(variant.Name))
387+
.OrderBy(variant => variant.Name)
388+
.ToList();
389+
390+
inputBuilder.Append(string.Join(";", sortedVariants.Select(v =>
391+
{
392+
var variantValue = string.Empty;
393+
394+
if (v.ConfigurationValue.ValueKind != JsonValueKind.Null && v.ConfigurationValue.ValueKind != JsonValueKind.Undefined)
395+
{
396+
variantValue = v.ConfigurationValue.SerializeWithSortedKeys();
397+
}
398+
399+
return $"{v.Name.ToBase64String()},{(variantValue)}";
400+
})));
401+
}
402+
403+
// Example input string
404+
// input == "seed=123abc\ndefault_when_enabled=Control\npercentiles=0,Blshdk,20;20,Test,100\nvariants=TdLa,standard;Qfcd,special"
405+
string input = inputBuilder.ToString();
406+
407+
using (SHA256 sha256 = SHA256.Create())
408+
{
409+
byte[] truncatedHash = new byte[15];
410+
Array.Copy(sha256.ComputeHash(Encoding.UTF8.GetBytes(input)), truncatedHash, 15);
411+
return truncatedHash.ToBase64Url();
412+
}
413+
}
414+
328415
private FormatException CreateFeatureFlagFormatException(string jsonPropertyName, string settingKey, string foundJsonValueKind, string expectedJsonValueKind)
329416
{
330417
return new FormatException(string.Format(
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Text.Json;
6+
7+
namespace Microsoft.Extensions.Configuration.AzureAppConfiguration.FeatureManagement
8+
{
9+
internal static class JsonElementExtensions
10+
{
11+
public static string SerializeWithSortedKeys(this JsonElement rootElement)
12+
{
13+
using var stream = new MemoryStream();
14+
15+
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }))
16+
{
17+
WriteElementWithSortedKeys(rootElement, writer);
18+
}
19+
20+
return Encoding.UTF8.GetString(stream.ToArray());
21+
}
22+
23+
private static void WriteElementWithSortedKeys(JsonElement element, Utf8JsonWriter writer)
24+
{
25+
switch (element.ValueKind)
26+
{
27+
case JsonValueKind.Object:
28+
writer.WriteStartObject();
29+
30+
foreach (JsonProperty property in element.EnumerateObject().OrderBy(p => p.Name))
31+
{
32+
writer.WritePropertyName(property.Name);
33+
WriteElementWithSortedKeys(property.Value, writer);
34+
}
35+
36+
writer.WriteEndObject();
37+
break;
38+
39+
case JsonValueKind.Array:
40+
writer.WriteStartArray();
41+
42+
foreach (JsonElement item in element.EnumerateArray())
43+
{
44+
WriteElementWithSortedKeys(item, writer);
45+
}
46+
47+
writer.WriteEndArray();
48+
break;
49+
50+
case JsonValueKind.String:
51+
writer.WriteStringValue(element.GetString());
52+
break;
53+
54+
case JsonValueKind.Number:
55+
if (element.TryGetInt32(out int intValue))
56+
{
57+
writer.WriteNumberValue(intValue);
58+
}
59+
else if (element.TryGetInt64(out long longValue))
60+
{
61+
writer.WriteNumberValue(longValue);
62+
}
63+
else if (element.TryGetDecimal(out decimal decimalValue))
64+
{
65+
writer.WriteNumberValue(element.GetDecimal());
66+
}
67+
else if (element.TryGetDouble(out double doubleValue))
68+
{
69+
writer.WriteNumberValue(element.GetDouble());
70+
}
71+
72+
break;
73+
74+
case JsonValueKind.True:
75+
writer.WriteBooleanValue(true);
76+
break;
77+
78+
case JsonValueKind.False:
79+
writer.WriteBooleanValue(false);
80+
break;
81+
82+
case JsonValueKind.Null:
83+
writer.WriteNullValue();
84+
break;
85+
86+
default:
87+
throw new InvalidOperationException($"Unsupported JsonValueKind: {element.ValueKind}");
88+
}
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)