Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement UFC (FF-2235) #11

Merged
merged 29 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ help: Makefile
@sed -n 's/^##//p' $<


.PHONY: test-data


testDataDir := eppo-sdk-test/files
tempDir := ${testDataDir}/temp
gitDataDir := ${tempDir}/sdk-test-data
branchName := main
githubRepoLink := https://github.com/Eppo-exp/sdk-test-data.git

.PHONY: test-data
test-data:
rm -rf $(testDataDir)
mkdir -p $(testDataDir)
gsutil cp gs://sdk-test-data/rac-experiments-*.json $(testDataDir)
gsutil cp -r gs://sdk-test-data/assignment-v2 $(testDataDir)
mkdir -p $(tempDir)
git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir}
cp -r ${gitDataDir}/ufc ${testDataDir}/
rm -rf ${tempDir}


.PHONY: build
build:
Expand Down
100 changes: 45 additions & 55 deletions dot-net-sdk/EppoClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using eppo_sdk.constants;
using System.Linq.Expressions;
using eppo_sdk.constants;
using eppo_sdk.dto;
using eppo_sdk.exception;
using eppo_sdk.helpers;
Expand Down Expand Up @@ -30,35 +31,48 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC
_fetchExperimentsTask = fetchExperimentsTask;
}

public JObject? GetJsonAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null)
private HasEppoValue _typeCheckedAssignment(string flagKey, string subjectKey, Subject subjectAttributes, EppoValueType expectedValueType, object defaultValue)
{
return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.JsonValue();
var result = GetAssignment(flagKey, subjectKey, subjectAttributes);
var eppoDefaultValue = new HasEppoValue(defaultValue);
if (HasEppoValue.IsNullValue(result)) return eppoDefaultValue;
var assignment = result!;
if (assignment.Type != expectedValueType)
{
Logger.Warn($"[Eppo SDK] Expected type {expectedValueType} does not match parsed type {assignment.Type}");
return eppoDefaultValue;
}
return assignment;
}
public JObject GetJsonAssignment(string flagKey, string subjectKey, Subject subjectAttributes, JObject defaultValue)
{
return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.JSON, defaultValue).JsonValue();
}

public bool? GetBoolAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null)
public bool GetBooleanAssignment(string flagKey, string subjectKey, Subject subjectAttributes, bool defaultValue)
{
return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.BoolValue();
return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.BOOLEAN, defaultValue).BoolValue();
}

public double? GetNumericAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null)
public double GetNumericAssignment(string flagKey, string subjectKey, Subject subjectAttributes, double defaultValue)
{
return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.DoubleValue();
return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.NUMERIC, defaultValue).DoubleValue();
}


public long? GetIntegerAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null)
public long GetIntegerAssignment(string flagKey, string subjectKey, Subject subjectAttributes, long defaultValue)
{
return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.IntegerValue();
return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.INTEGER, defaultValue).IntegerValue();
}


public string? GetStringAssignment(string subjectKey, string flagKey, SubjectAttributes? subjectAttributes = null)
public string GetStringAssignment(string flagKey, string subjectKey, Subject subjectAttributes, string defaultValue)
{
return GetAssignment(subjectKey, flagKey, subjectAttributes ?? new SubjectAttributes())?.StringValue();
return _typeCheckedAssignment(flagKey, subjectKey, subjectAttributes, EppoValueType.STRING, defaultValue).StringValue();
}


private HasEppoValue? GetAssignment(string subjectKey, string flagKey, SubjectAttributes subjectAttributes)
private HasEppoValue? GetAssignment(string flagKey, string subjectKey, Subject subjectAttributes)
{
InputValidator.ValidateNotBlank(subjectKey, "Invalid argument: subjectKey cannot be blank");
InputValidator.ValidateNotBlank(flagKey, "Invalid argument: flagKey cannot be blank");
Expand All @@ -70,75 +84,51 @@ private EppoClient(ConfigurationStore configurationStore, EppoClientConfig eppoC
return null;
}

var subjectVariationOverride = this.GetSubjectVariationOverride(subjectKey, configuration);
if (!subjectVariationOverride.IsNull())
{
return subjectVariationOverride;
}

if (!configuration.enabled)
{
Logger.Info(
$"[Eppo SDK] No assigned variation because the experiment or feature flag {flagKey} is disabled");
return null;
}

var rule = RuleValidator.FindMatchingRule(subjectAttributes, configuration.rules);
if (rule == null)
var result = RuleValidator.EvaluateFlag(configuration, subjectKey, subjectAttributes);
if (result == null)
{
Logger.Info("[Eppo SDK] No assigned variation. The subject attributes did not match any targeting rules");
return null;
}

var allocation = configuration.GetAllocation(rule.allocationKey);
if (!IsInExperimentSample(subjectKey, flagKey, configuration.subjectShards, allocation!.percentExposure))
var assignment = result.Variation;

if (HasEppoValue.IsNullValue(assignment))
{
Logger.Info("[Eppo SDK] No assigned variation. The subject is not part of the sample population");
Logger.Warn("[Eppo SDK] Assigned varition is null");
return null;
}

var assignedVariation =
GetAssignedVariation(subjectKey, flagKey, configuration.subjectShards, allocation.variations);
if (assignedVariation != null && !assignedVariation.IsNull())
AssignmentLogData assignmentEvent = new AssignmentLogData(
flagKey,
result.AllocationKey,
result.Variation.Key,
subjectKey,
subjectAttributes,
AppDetails.GetInstance().AsDict(),
result.ExtraLogging
);

if (result.DoLog)
{
try
{
_eppoClientConfig.AssignmentLogger
.LogAssignment(new AssignmentLogData(
flagKey,
rule.allocationKey,
assignedVariation.StringValue() ?? "null",
subjectKey,
subjectAttributes
));
.LogAssignment(assignmentEvent);
}
catch (Exception)
{
// Ignore Exception
}
}

return assignedVariation;
}

private bool IsInExperimentSample(string subjectKey, string flagKey, int subjectShards,
float percentageExposure)
{
var shard = Shard.GetShard($"exposure-{subjectKey}-{flagKey}", subjectShards);
return shard <= percentageExposure * subjectShards;
}

private Variation GetAssignedVariation(string subjectKey, string flagKey, int subjectShards,
List<Variation> variations)
{
var shard = Shard.GetShard($"assignment-{subjectKey}-{flagKey}", subjectShards);
return variations.Find(config => Shard.IsInRange(shard, config.shardRange))!;
}

public HasEppoValue GetSubjectVariationOverride(string subjectKey, ExperimentConfiguration experimentConfiguration)
{
var hexedSubjectKey = Shard.GetHex(subjectKey);
return new HasEppoValue(experimentConfiguration.typedOverrides.GetValueOrDefault(hexedSubjectKey, null));
return assignment;
}

public static EppoClient Init(EppoClientConfig eppoClientConfig)
Expand Down
4 changes: 2 additions & 2 deletions dot-net-sdk/constants/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ public class Constants

public const int MAX_CACHE_ENTRIES = 1000;

public const string RAC_ENDPOINT = "/randomized_assignment/v3/config";
}
public const string UFC_ENDPOINT = "/flag-config/v1/config";
}
6 changes: 2 additions & 4 deletions dot-net-sdk/dto/Allocation.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
namespace eppo_sdk.dto;

public class Allocation
public record Allocation(string key, List<Rule> rules, List<Split> splits, bool doLog, DateTime? startAt, DateTime? endAt)
{
public float percentExposure { get; set; }
public List<Variation> variations { get; set; }
}
}
52 changes: 31 additions & 21 deletions dot-net-sdk/dto/AssignmentLogData.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,38 @@
using System.Reflection.Metadata.Ecma335;

namespace eppo_sdk.dto;

public class AssignmentLogData
public record AssignmentLogData
{
public string experiment;
public string featureFlag;
public string allocation;
public string variation;
public DateTime timestamp;
public string subject;
public SubjectAttributes subjectAttributes;
public string Experiment;
public string FeatureFlag;
public string Allocation;
public string Variation;
public DateTime Timestamp;
public string Subject;
public Subject SubjectAttributes;

public IReadOnlyDictionary<string, string>? ExtraLogging;
public IReadOnlyDictionary<string, string> MetaData;

public AssignmentLogData(
string featureFlag,
string allocation,
string variation,
string subject,
SubjectAttributes subjectAttributes)
public AssignmentLogData(string featureFlag,
string allocation,
string variation,
string subject,
Subject subjectAttributes,
IReadOnlyDictionary<string, string> metaData,
IReadOnlyDictionary<string, string> extraLoggging

)
{
this.experiment = featureFlag + "-" + allocation;
this.featureFlag = featureFlag;
this.allocation = allocation;
this.variation = variation;
this.timestamp = new DateTime();
this.subject = subject;
this.subjectAttributes = subjectAttributes;
this.Experiment = featureFlag + "-" + allocation;
this.FeatureFlag = featureFlag;
this.Allocation = allocation;
this.Variation = variation;
this.Timestamp = new DateTime();
this.Subject = subject;
this.SubjectAttributes = subjectAttributes;
MetaData = metaData;
ExtraLogging = extraLoggging;
}
}
14 changes: 10 additions & 4 deletions dot-net-sdk/dto/Condition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ namespace eppo_sdk.dto;

public class Condition : HasEppoValue
{
public string attribute { get; set; }
public string Attribute { get; set; }

[JsonProperty(PropertyName = "operator", NamingStrategyType = typeof(DefaultNamingStrategy))]
public OperatorType operatorType { get; set; }
public OperatorType Operator { get; set; }

public Condition(string attribute, OperatorType op, object? value)
{
Attribute = attribute;
Operator = op;
Value = value;
}

public override string ToString()
{
return $"operator: {operatorType} | Attribute: {attribute} | value: {Value}";
return $"operator: {Operator} | Attribute: {Attribute} | value: {Value}";
}
}
2 changes: 1 addition & 1 deletion dot-net-sdk/dto/EppoValueType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace eppo_sdk.dto;

public enum EppoValueType
{
NUMBER,
NUMERIC,
INTEGER,
STRING,
BOOLEAN,
Expand Down
17 changes: 0 additions & 17 deletions dot-net-sdk/dto/ExperimentConfiguration.cs

This file was deleted.

2 changes: 1 addition & 1 deletion dot-net-sdk/dto/ExperimentConfigurationResponse.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
namespace eppo_sdk.dto;

public class ExperimentConfigurationResponse {
public Dictionary<string, ExperimentConfiguration> flags { get; set; }
public Dictionary<string, Flag> flags { get; set; }

Check warning on line 4 in dot-net-sdk/dto/ExperimentConfigurationResponse.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable property 'flags' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
}
11 changes: 11 additions & 0 deletions dot-net-sdk/dto/Flag.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace eppo_sdk.dto;

public record Flag(
string key,
bool enabled,
List<Allocation> Allocations,
EppoValueType variationType,
Dictionary<string, Variation> variations,
int totalShards)
{
}
19 changes: 19 additions & 0 deletions dot-net-sdk/dto/FlagEvaluation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace eppo_sdk.dto;

public record FlagEvaluation
{
public Variation Variation;
public bool DoLog;
public string AllocationKey;
public IReadOnlyDictionary<string, string> ExtraLogging;

public FlagEvaluation(Variation variation, bool doLog, string allocationKey, IReadOnlyDictionary<string, object>? extraLogging)
{
Variation = variation;
DoLog = doLog;
AllocationKey = allocationKey;
ExtraLogging = extraLogging == null ?
new Dictionary<string, string>() :
(IReadOnlyDictionary<string, string>)extraLogging.ToDictionary(pair => pair.Key, pair => Convert.ToString(pair.Value));
}
}
Loading
Loading