Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,12 @@ class OpenFeatureProviderSmokeTest extends AbstractServerSmokeTest {
private static Map<String, Boolean> buildLoggedAllocations(final Map<String, Object> config) {
final logged = [:]
(config.flags as Map<String, Object>).each { flag, definition ->
(definition.allocations ?: []).each { allocation ->
logged["${flag}\u0000${allocation.key}"] = allocation.doLog == true
if (definition.allocations instanceof List) {
definition.allocations.each { allocation ->
if (allocation instanceof Map) {
logged["${flag}\u0000${allocation.key}"] = allocation.doLog == true
}
}
}
}
return logged
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import datadog.trace.api.featureflag.ufc.v1.ConditionConfiguration;
import datadog.trace.api.featureflag.ufc.v1.ConditionOperator;
import datadog.trace.api.featureflag.ufc.v1.Flag;
import datadog.trace.api.featureflag.ufc.v1.ParsedSemver;
import datadog.trace.api.featureflag.ufc.v1.Rule;
import datadog.trace.api.featureflag.ufc.v1.ServerConfiguration;
import datadog.trace.api.featureflag.ufc.v1.Shard;
Expand Down Expand Up @@ -171,6 +172,20 @@ public <T> ProviderEvaluation<T> evaluate(

final Flag flag = config.flags.get(key);
if (flag == null) {
if (config.invalidFlags != null && config.invalidFlags.containsKey(key)) {
if ("invalid_semver_comparand".equals(config.invalidFlags.get(key))) {
return error(
defaultValue,
ErrorCode.PARSE_ERROR,
"invalid configuration for flag " + key,
observeFullEvaluationData);
}
return ProviderEvaluation.<T>builder()
.value(defaultValue)
.reason(Reason.DEFAULT.name())
.flagMetadata(consentMetadata(observeFullEvaluationData))
.build();
}
return error(defaultValue, ErrorCode.FLAG_NOT_FOUND, null, observeFullEvaluationData);
}

Expand Down Expand Up @@ -361,6 +376,18 @@ private static boolean evaluateCondition(
return compareNumber(attributeValue, condition.value, (a, b) -> a <= b);
case LT:
return compareNumber(attributeValue, condition.value, (a, b) -> a < b);
case SEMVER_EQ:
return evaluateSemverCondition(attributeValue, condition.semverComparand, (o) -> o == 0);
case SEMVER_NEQ:
return evaluateSemverCondition(attributeValue, condition.semverComparand, (o) -> o != 0);
case SEMVER_LT:
return evaluateSemverCondition(attributeValue, condition.semverComparand, (o) -> o < 0);
case SEMVER_LTE:
return evaluateSemverCondition(attributeValue, condition.semverComparand, (o) -> o <= 0);
case SEMVER_GT:
return evaluateSemverCondition(attributeValue, condition.semverComparand, (o) -> o > 0);
case SEMVER_GTE:
return evaluateSemverCondition(attributeValue, condition.semverComparand, (o) -> o >= 0);
default:
return false;
}
Expand All @@ -369,10 +396,20 @@ private static boolean evaluateCondition(
private static boolean matchesRegex(final Object attributeValue, final Object conditionValue) {
// PatternSyntaxException is intentionally not caught here so it propagates to evaluate(),
// which maps it to ErrorCode.PARSE_ERROR.
final Pattern pattern = Pattern.compile(String.valueOf(conditionValue));
final Pattern pattern = Pattern.compile(normalizeRegex(String.valueOf(conditionValue)));
return pattern.matcher(String.valueOf(attributeValue)).find();
}

private static String normalizeRegex(final String regex) {
return regex
.replace("[:alnum:]", "\\p{Alnum}")
.replace("[:alpha:]", "\\p{Alpha}")
.replace("[:digit:]", "\\p{Digit}")
.replace("[:lower:]", "\\p{Lower}")
.replace("[:upper:]", "\\p{Upper}")
.replace("[:space:]", "\\p{Space}");
}

private static boolean isOneOf(final Object attributeValue, final Object conditionValue) {
if (!(conditionValue instanceof Iterable)) {
return false;
Expand Down Expand Up @@ -404,6 +441,25 @@ private static boolean compareNumber(
return comparator.compare(a, b);
}

/**
* Evaluates a semantic version comparison operator. The attribute value must be a string that is
* a valid semantic version, and the comparand must have been pre-parsed during configuration
* validation. If either is missing or invalid, the condition does not match.
*/
private static boolean evaluateSemverCondition(
final Object attributeValue,
final ParsedSemver comparand,
final SemverComparator comparator) {
if (!(attributeValue instanceof String) || comparand == null) {
return false;
}
final ParsedSemver parsedAttribute = ParsedSemver.parse((String) attributeValue);
if (parsedAttribute == null) {
return false;
}
return comparator.compare(ParsedSemver.compare(parsedAttribute, comparand));
}

private static boolean matchesShard(final Shard shard, final String targetingKey) {
final int assignedShard = getShard(shard.salt, targetingKey, shard.totalShards);
for (final ShardRange range : shard.ranges) {
Expand Down Expand Up @@ -513,7 +569,9 @@ private static <T> ProviderEvaluation<T> resolveVariant(
.reason(
!isEmpty(allocation.rules)
? Reason.TARGETING_MATCH.name()
: !isEmpty(split.shards) ? Reason.SPLIT.name() : Reason.STATIC.name())
: allocation.startAt != null || allocation.endAt != null
? Reason.DEFAULT.name()
: !isEmpty(split.shards) ? Reason.SPLIT.name() : Reason.STATIC.name())
.variant(variant.key)
.flagMetadata(metadataBuilder.build())
.build();
Expand Down Expand Up @@ -911,6 +969,11 @@ private interface NumberComparator {
boolean compare(double a, double b);
}

@FunctionalInterface
private interface SemverComparator {
boolean compare(int ordering);
}

private static class FlattenEntry {
private final String key;
private final Value value;
Expand Down
Loading