Skip to content

Commit

Permalink
Define error codes for evaluation errors (EvaluationErrorCode) and ex…
Browse files Browse the repository at this point in the history
…pose them to the user (EvaluationDetails.ErrorCode)
  • Loading branch information
adams85 committed Mar 25, 2024
1 parent ed0a54d commit d2f0b0c
Show file tree
Hide file tree
Showing 13 changed files with 85 additions and 33 deletions.
12 changes: 8 additions & 4 deletions src/ConfigCatClient/ConfigCatClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ public T GetValue<T>(string key, T defaultValue, User? user = null)
catch (Exception ex)
{
this.logger.SettingEvaluationError(nameof(GetValue), key, nameof(defaultValue), defaultValue, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user, ex.Message, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user,
ex.Message, ex, RolloutEvaluatorExtensions.GetErrorCode(ex));
value = defaultValue;
}

Expand Down Expand Up @@ -322,7 +323,8 @@ public async Task<T> GetValueAsync<T>(string key, T defaultValue, User? user = n
catch (Exception ex)
{
this.logger.SettingEvaluationError(nameof(GetValueAsync), key, nameof(defaultValue), defaultValue, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user, ex.Message, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user,
ex.Message, ex, RolloutEvaluatorExtensions.GetErrorCode(ex));
value = defaultValue;
}

Expand Down Expand Up @@ -356,7 +358,8 @@ public EvaluationDetails<T> GetValueDetails<T>(string key, T defaultValue, User?
catch (Exception ex)
{
this.logger.SettingEvaluationError(nameof(GetValueDetails), key, nameof(defaultValue), defaultValue, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user, ex.Message, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user,
ex.Message, ex, RolloutEvaluatorExtensions.GetErrorCode(ex));
}

this.hooks.RaiseFlagEvaluated(evaluationDetails);
Expand Down Expand Up @@ -393,7 +396,8 @@ public async Task<EvaluationDetails<T>> GetValueDetailsAsync<T>(string key, T de
catch (Exception ex)
{
this.logger.SettingEvaluationError(nameof(GetValueDetailsAsync), key, nameof(defaultValue), defaultValue, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user, ex.Message, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: settings.RemoteConfig?.TimeStamp, user,
ex.Message, ex, RolloutEvaluatorExtensions.GetErrorCode(ex));
}

this.hooks.RaiseFlagEvaluated(evaluationDetails);
Expand Down
7 changes: 5 additions & 2 deletions src/ConfigCatClient/Evaluation/EvaluationDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ internal static EvaluationDetails<TValue> FromEvaluateResult<TValue>(string key,
}

internal static EvaluationDetails<TValue> FromDefaultValue<TValue>(string key, TValue defaultValue, DateTime? fetchTime, User? user,
string? errorMessage = null, Exception? errorException = null)
string errorMessage, Exception? errorException = null, EvaluationErrorCode errorCode = EvaluationErrorCode.UnexpectedError)
{
var instance = new EvaluationDetails<TValue>(key, defaultValue)
{
User = user,
IsDefaultValue = true,
ErrorMessage = errorMessage,
ErrorException = errorException
ErrorException = errorException,
ErrorCode = errorCode,
};

if (fetchTime is not null)
Expand Down Expand Up @@ -94,6 +95,8 @@ private protected EvaluationDetails(string key)
/// </summary>
public Exception? ErrorException { get; set; }

public EvaluationErrorCode ErrorCode { get; set; }

Check warning on line 98 in src/ConfigCatClient/Evaluation/EvaluationDetails.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationDetails.ErrorCode'

Check warning on line 98 in src/ConfigCatClient/Evaluation/EvaluationDetails.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationDetails.ErrorCode'

/// <summary>
/// The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace ConfigCat.Client;

public enum EvaluationErrorCode

Check warning on line 3 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode'

Check warning on line 3 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode'
{
UnexpectedError = -1,

Check warning on line 5 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.UnexpectedError'

Check warning on line 5 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.UnexpectedError'
None = 0,

Check warning on line 6 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.None'

Check warning on line 6 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.None'
InvalidConfigModel = 1,

Check warning on line 7 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.InvalidConfigModel'

Check warning on line 7 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.InvalidConfigModel'
SettingValueTypeMismatch = 2,

Check warning on line 8 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.SettingValueTypeMismatch'

Check warning on line 8 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.SettingValueTypeMismatch'
ConfigJsonNotAvailable = 1000,

Check warning on line 9 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.ConfigJsonNotAvailable'

Check warning on line 9 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.ConfigJsonNotAvailable'
SettingKeyMissing = 1001,

Check warning on line 10 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (macos-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.SettingKeyMissing'

Check warning on line 10 in src/ConfigCatClient/Evaluation/EvaluationErrorCode.cs

View workflow job for this annotation

GitHub Actions / Build & test (ubuntu-latest)

Missing XML comment for publicly visible type or member 'EvaluationErrorCode.SettingKeyMissing'
}
13 changes: 13 additions & 0 deletions src/ConfigCatClient/Evaluation/EvaluationErrorException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

namespace ConfigCat.Client.Evaluation;

internal sealed class EvaluationErrorException : InvalidOperationException
{
public EvaluationErrorException(EvaluationErrorCode errorCode, string message) : base(message)
{
ErrorCode = errorCode;
}

public EvaluationErrorCode ErrorCode { get; }
}
30 changes: 15 additions & 15 deletions src/ConfigCatClient/Evaluation/RolloutEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public EvaluateResult Evaluate<T>(T defaultValue, ref EvaluateContext context, [
// The latter case is handled by SettingValue.GetValue<T> below.
if (context.Setting.SettingType != Setting.UnknownType && context.Setting.SettingType != expectedSettingType)
{
throw new InvalidOperationException(
throw new EvaluationErrorException(EvaluationErrorCode.SettingValueTypeMismatch,
"The type of a setting must match the type of the specified default value. "
+ $"Setting's type was {context.Setting.SettingType} but the default value's type was {typeof(T)}. "
+ $"Please use a default value which corresponds to the setting type {context.Setting.SettingType}. "
Expand Down Expand Up @@ -153,7 +153,7 @@ private bool TryEvaluateTargetingRules(TargetingRule[] targetingRules, ref Evalu
var percentageOptions = targetingRule.PercentageOptions;
if (percentageOptions is not { Length: > 0 })
{
throw new InvalidOperationException("Targeting rule THEN part is missing or invalid.");
throw new InvalidConfigModelException("Targeting rule THEN part is missing or invalid.");
}

logBuilder?.IncreaseIndent();
Expand Down Expand Up @@ -254,7 +254,7 @@ private bool TryEvaluatePercentageOptions(PercentageOption[] percentageOptions,
return true;
}

throw new InvalidOperationException("Sum of percentage option percentages is less than 100.");
throw new InvalidConfigModelException("Sum of percentage option percentages is less than 100.");
}

private bool EvaluateConditions<TCondition>(TCondition[] conditions, TargetingRule? targetingRule, string contextSalt, ref EvaluateContext context, out string? error)
Expand Down Expand Up @@ -359,7 +359,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt,
return false;
}

var userAttributeName = condition.ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing.");
var userAttributeName = condition.ComparisonAttribute ?? throw new InvalidConfigModelException("Comparison attribute name is missing.");
var userAttributeValue = context.User.GetAttribute(userAttributeName);

if (userAttributeValue is null || userAttributeValue is string { Length: 0 })
Expand Down Expand Up @@ -459,7 +459,7 @@ private bool EvaluateUserCondition(UserCondition condition, string contextSalt,
EnsureConfigJsonSalt(context.Setting.ConfigJsonSalt), contextSalt, negate: comparator == UserComparator.SensitiveArrayNotContainsAnyOf);

default:
throw new InvalidOperationException("Comparison operator is invalid.");
throw new InvalidConfigModelException("Comparison operator is invalid.");
}
}

Expand Down Expand Up @@ -717,27 +717,27 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi
var prerequisiteFlagKey = condition.PrerequisiteFlagKey;
if (prerequisiteFlagKey is null)
{
throw new InvalidOperationException("Prerequisite flag key is missing.");
throw new InvalidConfigModelException("Prerequisite flag key is missing.");
}
else if (!context.Settings.TryGetValue(prerequisiteFlagKey, out prerequisiteFlag))
{
throw new InvalidOperationException("Prerequisite flag is missing.");
throw new InvalidConfigModelException("Prerequisite flag is missing.");
}

var comparisonValue = EnsureComparisonValue(condition.ComparisonValue.GetValue(throwIfInvalid: false));

var expectedSettingType = comparisonValue.GetType().ToSettingType();
if (prerequisiteFlag.SettingType != Setting.UnknownType && prerequisiteFlag.SettingType != expectedSettingType)
{
throw new InvalidOperationException($"Type mismatch between comparison value '{comparisonValue}' and prerequisite flag '{prerequisiteFlagKey}'.");
throw new InvalidConfigModelException($"Type mismatch between comparison value '{comparisonValue}' and prerequisite flag '{prerequisiteFlagKey}'.");
}

context.VisitedFlags.Add(context.Key);
if (context.VisitedFlags.Contains(prerequisiteFlagKey!))
{
context.VisitedFlags.Add(prerequisiteFlagKey!);
var dependencyCycle = new StringListFormatter(context.VisitedFlags).ToString("a", CultureInfo.InvariantCulture);
throw new InvalidOperationException($"Circular dependency detected between the following depending flags: {dependencyCycle}.");
throw new InvalidConfigModelException($"Circular dependency detected between the following depending flags: {dependencyCycle}.");
}

var prerequisiteFlagContext = new EvaluateContext(prerequisiteFlagKey!, prerequisiteFlag!, context);
Expand All @@ -758,7 +758,7 @@ private bool EvaluatePrerequisiteFlagCondition(PrerequisiteFlagCondition conditi
{
PrerequisiteFlagComparator.Equals => prerequisiteFlagValue.Equals(comparisonValue),
PrerequisiteFlagComparator.NotEquals => !prerequisiteFlagValue.Equals(comparisonValue),
_ => throw new InvalidOperationException("Comparison operator is invalid.")
_ => throw new InvalidConfigModelException("Comparison operator is invalid.")
};

logBuilder?
Expand Down Expand Up @@ -791,11 +791,11 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo
return false;
}

var segment = condition.Segment ?? throw new InvalidOperationException("Segment reference is invalid.");
var segment = condition.Segment ?? throw new InvalidConfigModelException("Segment reference is invalid.");

if (segment.Name is not { Length: > 0 })
{
throw new InvalidOperationException("Segment name is missing.");
throw new InvalidConfigModelException("Segment name is missing.");
}

logBuilder?
Expand All @@ -810,7 +810,7 @@ private bool EvaluateSegmentCondition(SegmentCondition condition, ref EvaluateCo
{
SegmentComparator.IsIn => segmentResult,
SegmentComparator.IsNotIn => !segmentResult,
_ => throw new InvalidOperationException("Comparison operator is invalid.")
_ => throw new InvalidConfigModelException("Comparison operator is invalid.")
};

if (logBuilder is not null)
Expand Down Expand Up @@ -865,13 +865,13 @@ private static byte[] HashComparisonValue(ReadOnlySpan<byte> valueUtf8, string c

private static string EnsureConfigJsonSalt([NotNull] string? value)
{
return value ?? throw new InvalidOperationException("Config JSON salt is missing.");
return value ?? throw new InvalidConfigModelException("Config JSON salt is missing.");
}

[return: NotNull]
private static T EnsureComparisonValue<T>([NotNull] T? value)
{
return value ?? throw new InvalidOperationException("Comparison value is missing or invalid.");
return value ?? throw new InvalidConfigModelException("Comparison value is missing or invalid.");
}

private static string UserAttributeValueToString(object attributeValue)
Expand Down
19 changes: 16 additions & 3 deletions src/ConfigCatClient/Evaluation/RolloutEvaluatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ public static EvaluationDetails<T> Evaluate<T>(this IRolloutEvaluator evaluator,
if (settings is null)
{
logMessage = logger.ConfigJsonIsNotPresent(key, nameof(defaultValue), defaultValue);
return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user, logMessage.InvariantFormattedMessage);
return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user,
logMessage.InvariantFormattedMessage, errorCode: EvaluationErrorCode.ConfigJsonNotAvailable);
}

if (!settings.TryGetValue(key, out var setting))
{
var availableKeys = new StringListFormatter(settings.Keys).ToString();
logMessage = logger.SettingEvaluationFailedDueToMissingKey(key, nameof(defaultValue), defaultValue, availableKeys);
return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user, logMessage.InvariantFormattedMessage);
return EvaluationDetails.FromDefaultValue(key, defaultValue, fetchTime: remoteConfig?.TimeStamp, user,
logMessage.InvariantFormattedMessage, errorCode: EvaluationErrorCode.SettingKeyMissing);
}

var evaluateContext = new EvaluateContext(key, setting, user, settings);
Expand Down Expand Up @@ -65,7 +67,8 @@ public static EvaluationDetails[] EvaluateAll(this IRolloutEvaluator evaluator,
{
exceptionList ??= new List<Exception>();
exceptionList.Add(ex);
evaluationDetails = EvaluationDetails.FromDefaultValue<object?>(kvp.Key, defaultValue: null, fetchTime: remoteConfig?.TimeStamp, user, ex.Message, ex);
evaluationDetails = EvaluationDetails.FromDefaultValue<object?>(kvp.Key, defaultValue: null, fetchTime: remoteConfig?.TimeStamp, user,
ex.Message, ex, GetErrorCode(ex));
}

evaluationDetailsArray[index++] = evaluationDetails;
Expand All @@ -85,4 +88,14 @@ internal static bool CheckSettingsAvailable([NotNullWhen(true)] Dictionary<strin

return true;
}

internal static EvaluationErrorCode GetErrorCode(Exception exception)
{
return exception switch
{
EvaluationErrorException evaluationErrorException => evaluationErrorException.ErrorCode,
InvalidConfigModelException => EvaluationErrorCode.InvalidConfigModel,
_ => EvaluationErrorCode.UnexpectedError,
};
}
}
2 changes: 1 addition & 1 deletion src/ConfigCatClient/Models/ConditionContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ public PrerequisiteFlagCondition? PrerequisiteFlagCondition
public readonly Condition? GetCondition(bool throwIfInvalid = true)
{
return this.condition as Condition
?? (!throwIfInvalid ? null : throw new InvalidOperationException("Condition is missing or invalid."));
?? (!throwIfInvalid ? null : throw new InvalidConfigModelException("Condition is missing or invalid."));
}
}
8 changes: 8 additions & 0 deletions src/ConfigCatClient/Models/InvalidConfigModelException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System;

namespace ConfigCat.Client;

internal sealed class InvalidConfigModelException : InvalidOperationException
{
public InvalidConfigModelException(string message) : base(message) { }
}
2 changes: 1 addition & 1 deletion src/ConfigCatClient/Models/PrerequisiteFlagCondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ internal sealed class PrerequisiteFlagCondition : Condition, IPrerequisiteFlagCo
#endif
public string? PrerequisiteFlagKey { get; set; }

string IPrerequisiteFlagCondition.PrerequisiteFlagKey => PrerequisiteFlagKey ?? throw new InvalidOperationException("Prerequisite flag key is missing.");
string IPrerequisiteFlagCondition.PrerequisiteFlagKey => PrerequisiteFlagKey ?? throw new InvalidConfigModelException("Prerequisite flag key is missing.");

private PrerequisiteFlagComparator comparator = UnknownComparator;

Expand Down
2 changes: 1 addition & 1 deletion src/ConfigCatClient/Models/Segment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ internal sealed class Segment : ISegment
#endif
public string? Name { get; set; }

string ISegment.Name => Name ?? throw new InvalidOperationException("Segment name is missing.");
string ISegment.Name => Name ?? throw new InvalidConfigModelException("Segment name is missing.");

private UserCondition[]? conditions;

Expand Down
2 changes: 1 addition & 1 deletion src/ConfigCatClient/Models/SegmentCondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal sealed class SegmentCondition : Condition, ISegmentCondition
[JsonIgnore]
public Segment? Segment { get; private set; }

ISegment ISegmentCondition.Segment => Segment ?? throw new InvalidOperationException("Segment reference is invalid.");
ISegment ISegmentCondition.Segment => Segment ?? throw new InvalidConfigModelException("Segment reference is invalid.");

private SegmentComparator comparator = UnknownComparator;

Expand Down
6 changes: 3 additions & 3 deletions src/ConfigCatClient/Models/SettingValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ public object? UnsupportedValue
if (HasUnsupportedValue)
{
var unsupportedValue = UnsupportedValue;
throw new InvalidOperationException(unsupportedValue is not null
throw new InvalidConfigModelException(unsupportedValue is not null
? $"Setting value '{unsupportedValue}' is of an unsupported type ({unsupportedValue.GetType()})."
: "Setting value is null.");
}
// Value is missing or multiple values specified in the config JSON?
else
{
throw new InvalidOperationException("Setting value is missing or invalid.");
throw new InvalidConfigModelException("Setting value is missing or invalid.");
}
}

Expand All @@ -103,7 +103,7 @@ public object? UnsupportedValue

if (value is null || value.GetType().ToSettingType() != settingType)
{
return !throwIfInvalid ? null : throw new InvalidOperationException($"Setting value is not of the expected type {settingType}.");
return !throwIfInvalid ? null : throw new InvalidConfigModelException($"Setting value is not of the expected type {settingType}.");
}

return value;
Expand Down
4 changes: 2 additions & 2 deletions src/ConfigCatClient/Models/UserCondition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal sealed class UserCondition : Condition, IUserCondition
#endif
public string? ComparisonAttribute { get; set; }

string IUserCondition.ComparisonAttribute => ComparisonAttribute ?? throw new InvalidOperationException("Comparison attribute name is missing.");
string IUserCondition.ComparisonAttribute => ComparisonAttribute ?? throw new InvalidConfigModelException("Comparison attribute name is missing.");

private UserComparator comparator = UnknownComparator;

Expand Down Expand Up @@ -105,7 +105,7 @@ public string[]? StringListValue
{
return ModelHelper.IsValidOneOf(this.comparisonValue)
? this.comparisonValue
: (!throwIfInvalid ? null : throw new InvalidOperationException("Comparison value is missing or invalid."));
: (!throwIfInvalid ? null : throw new InvalidConfigModelException("Comparison value is missing or invalid."));
}

public override string ToString()
Expand Down

0 comments on commit d2f0b0c

Please sign in to comment.