From 4b6012e9a95f5cc1caaa4384b616b22f933c809a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Hern=C3=A1ndez?= Date: Sun, 2 Jun 2024 14:12:49 +0200 Subject: [PATCH 1/5] implement feature flag dtos --- .../com/posthog/java/flags/FeatureFlag.java | 158 ++++++++++++++++++ .../java/flags/FeatureFlagCondition.java | 74 ++++++++ .../posthog/java/flags/FeatureFlagConfig.java | 126 ++++++++++++++ .../posthog/java/flags/FeatureFlagFilter.java | 87 ++++++++++ .../java/flags/FeatureFlagProperty.java | 105 ++++++++++++ .../java/flags/FeatureFlagPropertyGroup.java | 63 +++++++ .../flags/FeatureFlagPropertyOperator.java | 34 ++++ .../java/flags/FeatureFlagVariant.java | 71 ++++++++ .../java/flags/FeatureFlagVariantMeta.java | 75 +++++++++ .../java/flags/FeatureFlagVariants.java | 52 ++++++ .../com/posthog/java/flags/FeatureFlags.java | 74 ++++++++ 11 files changed, 919 insertions(+) create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlag.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagConfig.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagFilter.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagProperty.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyGroup.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyOperator.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariant.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariantMeta.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariants.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlags.java diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlag.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlag.java new file mode 100644 index 0000000..36a6f70 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlag.java @@ -0,0 +1,158 @@ +package com.posthog.java.flags; + +import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; + +public class FeatureFlag { + private final String key; + private final String name; + private final int id; + private final int teamId; + private final int rolloutPercentage; + private final boolean isSimpleFlag; + private final boolean active; + private final boolean ensureExperienceContinuity; + private final boolean deleted; + private final FeatureFlagFilter featureFlagFilter; + + public static class Builder { + private final String key; + private final int id; + private final int teamId; + + private int rolloutPercentage; + private String name; + private boolean isSimpleFlag; + private boolean active; + private boolean ensureExperienceContinuity; + private boolean deleted; + private FeatureFlagFilter featureFlagFilter; + + public Builder(String key, int id, int teamId) { + this.key = key; + this.id = id; + this.teamId = teamId; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder rolloutPercentage(int rolloutPercentage) { + this.rolloutPercentage = rolloutPercentage; + return this; + } + + public Builder isSimpleFlag(boolean isSimpleFlag) { + this.isSimpleFlag = isSimpleFlag; + return this; + } + + public Builder active(boolean active) { + this.active = active; + return this; + } + + public Builder deleted(boolean deleted) { + this.deleted = deleted; + return this; + } + + public Builder ensureExperienceContinuity(boolean ensureExperienceContinuity) { + this.ensureExperienceContinuity = ensureExperienceContinuity; + return this; + } + + public Builder filter(FeatureFlagFilter featureFlagFilter) { + this.featureFlagFilter = featureFlagFilter; + return this; + } + + public FeatureFlag build() { + return new FeatureFlag(this); + } + } + + private FeatureFlag(Builder builder) { + this.key = builder.key; + this.name = builder.name; + this.id = builder.id; + this.teamId = builder.teamId; + this.rolloutPercentage = builder.rolloutPercentage; + this.isSimpleFlag = builder.isSimpleFlag; + this.active = builder.active; + this.ensureExperienceContinuity = builder.ensureExperienceContinuity; + this.featureFlagFilter = builder.featureFlagFilter; + this.deleted = builder.deleted; + } + + public String getKey() { + return key; + } + + public String getName() { + return name; + } + + public int getId() { + return id; + } + + public int getTeamId() { + return teamId; + } + + public int getRolloutPercentage() { + return rolloutPercentage; + } + + public boolean isSimpleFlag() { + return isSimpleFlag; + } + + public boolean isActive() { + return active; + } + + public boolean isDeleted() { + return deleted; + } + + public boolean isEnsureExperienceContinuity() { + return ensureExperienceContinuity; + } + + public Optional getFilter() { + return Optional.ofNullable(featureFlagFilter); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlag that = (FeatureFlag) o; + return isActive() == that.isActive() && Objects.equals(getKey(), that.getKey()) && Objects.equals(getId(), that.getId()) && Objects.equals(getTeamId(), that.getTeamId()); + } + + @Override + public int hashCode() { + return Objects.hash(getKey(), getId(), getTeamId(), isActive()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlag.class.getSimpleName() + "[", "]") + .add("key='" + key + "'") + .add("name='" + name + "'") + .add("id='" + id + "'") + .add("teamId='" + teamId + "'") + .add("rolloutPercentage=" + rolloutPercentage) + .add("isSimpleFlag=" + isSimpleFlag) + .add("active=" + active) + .add("ensureExperienceContinuity=" + ensureExperienceContinuity) + .add("filters=" + featureFlagFilter) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java new file mode 100644 index 0000000..146b5ec --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java @@ -0,0 +1,74 @@ +package com.posthog.java.flags; + +import java.util.*; + +public class FeatureFlagCondition { + private final List properties; + private final int rolloutPercentage; + private final String variant; + + private FeatureFlagCondition(final Builder builder) { + this.properties = builder.properties; + this.rolloutPercentage = builder.rolloutPercentage; + this.variant = builder.variant; + } + + public static class Builder { + private List properties = new ArrayList<>(); + private int rolloutPercentage = 0; + private String variant = ""; + + public Builder properties(List properties) { + this.properties = properties; + return this; + } + + public Builder rolloutPercentage(int rolloutPercentage) { + this.rolloutPercentage = rolloutPercentage; + return this; + } + + public Builder variant(String variant) { + this.variant = variant; + return this; + } + + public FeatureFlagCondition build() { + return new FeatureFlagCondition(this); + } + } + + public List getProperties() { + return properties; + } + + public int getRolloutPercentage() { + return rolloutPercentage; + } + + public Optional getVariant() { + return Optional.ofNullable(variant); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagCondition that = (FeatureFlagCondition) o; + return getRolloutPercentage() == that.getRolloutPercentage() && Objects.equals(getProperties(), that.getProperties()) && Objects.equals(getVariant(), that.getVariant()); + } + + @Override + public int hashCode() { + return Objects.hash(getProperties(), getRolloutPercentage(), getVariant()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagCondition.class.getSimpleName() + "[", "]") + .add("properties=" + properties) + .add("rolloutPercentage=" + rolloutPercentage) + .add("variant='" + variant + "'") + .toString(); + } +} \ No newline at end of file diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagConfig.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagConfig.java new file mode 100644 index 0000000..05d4826 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagConfig.java @@ -0,0 +1,126 @@ +package com.posthog.java.flags; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.StringJoiner; + +public class FeatureFlagConfig { + + private final String key; + private final String distinctId; + private final Map groups; + private final Map personProperties; + private final Map> groupProperties; + private final boolean onlyEvaluateLocally; + private final boolean sendFeatureFlagEvents; + + private FeatureFlagConfig(Builder builder) { + this.key = builder.key; + this.distinctId = builder.distinctId; + this.groups = builder.groups; + this.personProperties = builder.personProperties; + this.groupProperties = builder.groupProperties; + this.onlyEvaluateLocally = builder.onlyEvaluateLocally; + this.sendFeatureFlagEvents = builder.sendFeatureFlagEvents; + } + + public static class Builder { + private final String key; + private final String distinctId; + + private Map groups = new HashMap<>(); + private Map personProperties = new HashMap<>(); + private Map> groupProperties = new HashMap<>(); + private boolean onlyEvaluateLocally = false; + private boolean sendFeatureFlagEvents = false; + + public Builder(String key, String distinctId) { + this.key = key; + this.distinctId = distinctId; + } + + public Builder groups(Map groups) { + this.groups = groups; + return this; + } + + public Builder personProperties(Map personProperties) { + this.personProperties = personProperties; + return this; + } + + public Builder groupProperties(Map> groupProperties) { + this.groupProperties = groupProperties; + return this; + } + + public Builder onlyEvaluateLocally(boolean onlyEvaluateLocally) { + this.onlyEvaluateLocally = onlyEvaluateLocally; + return this; + } + + public Builder sendFeatureFlagEvents(boolean sendFeatureFlagEvents) { + this.sendFeatureFlagEvents = sendFeatureFlagEvents; + return this; + } + + public FeatureFlagConfig build() { + return new FeatureFlagConfig(this); + } + } + + public String getKey() { + return key; + } + + public String getDistinctId() { + return distinctId; + } + + public Map getGroups() { + return groups; + } + + public Map getPersonProperties() { + return personProperties; + } + + public Map> getGroupProperties() { + return groupProperties; + } + + public boolean isOnlyEvaluateLocally() { + return onlyEvaluateLocally; + } + + public boolean isSendFeatureFlagEvents() { + return sendFeatureFlagEvents; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagConfig that = (FeatureFlagConfig) o; + return isOnlyEvaluateLocally() == that.isOnlyEvaluateLocally() && isSendFeatureFlagEvents() == that.isSendFeatureFlagEvents() && Objects.equals(getKey(), that.getKey()) && Objects.equals(getDistinctId(), that.getDistinctId()) && Objects.equals(getGroups(), that.getGroups()) && Objects.equals(getPersonProperties(), that.getPersonProperties()) && Objects.equals(getGroupProperties(), that.getGroupProperties()); + } + + @Override + public int hashCode() { + return Objects.hash(getKey(), getDistinctId(), getGroups(), getPersonProperties(), getGroupProperties(), isOnlyEvaluateLocally(), isSendFeatureFlagEvents()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagConfig.class.getSimpleName() + "[", "]") + .add("key='" + key + "'") + .add("distinctId='" + distinctId + "'") + .add("groups=" + groups) + .add("personProperties=" + personProperties) + .add("groupProperties=" + groupProperties) + .add("onlyEvaluateLocally=" + onlyEvaluateLocally) + .add("sendFeatureFlagEvents=" + sendFeatureFlagEvents) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagFilter.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagFilter.java new file mode 100644 index 0000000..a58759a --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagFilter.java @@ -0,0 +1,87 @@ +package com.posthog.java.flags; + +import java.util.*; + +public class FeatureFlagFilter { + private final int aggregationGroupTypeIndex; + private final List groups; + private final FeatureFlagVariants multivariate; + private final Map payloads; + + private FeatureFlagFilter(Builder builder) { + this.aggregationGroupTypeIndex = builder.aggregationGroupTypeIndex; + this.groups = builder.groups; + this.multivariate = builder.multivariate; + this.payloads = builder.payloads; + } + + public static class Builder { + private int aggregationGroupTypeIndex = 0; + private List groups = new ArrayList<>(); + private FeatureFlagVariants multivariate = null; + private Map payloads = new HashMap<>(); + + public Builder aggregationGroupTypeIndex(int aggregationGroupTypeIndex) { + this.aggregationGroupTypeIndex = aggregationGroupTypeIndex; + return this; + } + + public Builder groups(List groups) { + this.groups = groups; + return this; + } + + public Builder multivariate(FeatureFlagVariants multivariate) { + this.multivariate = multivariate; + return this; + } + + public Builder payloads(Map payloads) { + this.payloads = payloads; + return this; + } + + public FeatureFlagFilter build() { + return new FeatureFlagFilter(this); + } + } + + public int getAggregationGroupTypeIndex() { + return aggregationGroupTypeIndex; + } + + public List getGroups() { + return groups; + } + + public Optional getMultivariate() { + return Optional.ofNullable(multivariate); + } + + public Map getPayloads() { + return payloads; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagFilter featureFlagFilter = (FeatureFlagFilter) o; + return getAggregationGroupTypeIndex() == featureFlagFilter.getAggregationGroupTypeIndex() && Objects.equals(getGroups(), featureFlagFilter.getGroups()) && Objects.equals(getMultivariate(), featureFlagFilter.getMultivariate()); + } + + @Override + public int hashCode() { + return Objects.hash(getAggregationGroupTypeIndex(), getGroups(), getMultivariate()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagFilter.class.getSimpleName() + "[", "]") + .add("aggregationGroupTypeIndex=" + aggregationGroupTypeIndex) + .add("groups=" + groups) + .add("multivariate=" + multivariate) + .add("payloads=" + payloads) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagProperty.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagProperty.java new file mode 100644 index 0000000..3cc46c5 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagProperty.java @@ -0,0 +1,105 @@ +package com.posthog.java.flags; + +import java.util.*; + +public class FeatureFlagProperty { + + private final String key; + private final String operator; + private final List value; + private final String type; + private final boolean negation; + + private FeatureFlagProperty(final Builder builder) { + this.key = builder.key; + this.operator = builder.operator; + this.value = builder.value; + this.type = builder.type; + this.negation = builder.negation; + } + + public static class Builder { + private final String key; + + private String operator; + private String type; + private List value = new ArrayList<>(); + private boolean negation = false; + + public Builder(String key) { + this.key = key; + } + + public Builder operator(String operator) { + this.operator = operator; + return this; + } + + public Builder value(List value) { + this.value = value; + return this; + } + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder negation(boolean negation) { + this.negation = negation; + return this; + } + + public FeatureFlagProperty build() { + return new FeatureFlagProperty(this); + } + } + + public String getKey() { + return key; + } + + public Optional getOperator() { + return Optional.of(FeatureFlagPropertyOperator.fromString(operator)); + } + + public List getValue() { + return value; + } + + public Optional getType() { + return Optional.ofNullable(type); + } + + public boolean isNegation() { + return negation; + } + + public boolean isCohort() { + return type.equals("cohort"); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagProperty that = (FeatureFlagProperty) o; + return Objects.equals(getKey(), that.getKey()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagProperty.class.getSimpleName() + "[", "]") + .add("key='" + key + "'") + .add("operator='" + operator + "'") + .add("value=" + value) + .add("type='" + type + "'") + .add("negation=" + negation) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyGroup.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyGroup.java new file mode 100644 index 0000000..f637443 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyGroup.java @@ -0,0 +1,63 @@ +package com.posthog.java.flags; + +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +public class FeatureFlagPropertyGroup { + private final String type; + private final List values; + + private FeatureFlagPropertyGroup(final Builder builder) { + this.type = builder.type; + this.values = builder.values; + } + + public static class Builder { + private String type; + private List values; + + public Builder type(String type) { + this.type = type; + return this; + } + + public Builder values(List values) { + this.values = values; + return this; + } + + public FeatureFlagPropertyGroup build() { + return new FeatureFlagPropertyGroup(this); + } + } + + public String getType() { + return type; + } + + public List getValues() { + return values; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagPropertyGroup that = (FeatureFlagPropertyGroup) o; + return Objects.equals(getType(), that.getType()) && Objects.equals(getValues(), that.getValues()); + } + + @Override + public int hashCode() { + return Objects.hash(getType(), getValues()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagPropertyGroup.class.getSimpleName() + "[", "]") + .add("type='" + type + "'") + .add("values=" + values) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyOperator.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyOperator.java new file mode 100644 index 0000000..437a6db --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagPropertyOperator.java @@ -0,0 +1,34 @@ +package com.posthog.java.flags; + +public enum FeatureFlagPropertyOperator { + EXACT("exact"), + IS_NOT("is_not"), + IS_SET("is_set"), + CONTAINS_INSENSITIVE("icontains"), + NOT_CONTAINS_INSENSITIVE("not_icontains"), + REGEX("regex"), + NOT_REGEX("not_regex"), + GREATER_THAN("gt"), + GREATER_THAN_OR_EQUAL("gte"), + LESS_THAN("lt"), + LESS_THAN_OR_EQUAL("lte"); + + private final String operator; + + FeatureFlagPropertyOperator(String operator) { + this.operator = operator; + } + + public String getOperator() { + return operator; + } + + public static FeatureFlagPropertyOperator fromString(String operator) { + for (FeatureFlagPropertyOperator op : FeatureFlagPropertyOperator.values()) { + if (op.getOperator().equalsIgnoreCase(operator)) { + return op; + } + } + throw new IllegalArgumentException("No enum constant with operator: " + operator); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariant.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariant.java new file mode 100644 index 0000000..7c6b767 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariant.java @@ -0,0 +1,71 @@ +package com.posthog.java.flags; + +import java.util.Objects; +import java.util.StringJoiner; + +public class FeatureFlagVariant { + private final String key; + private final String name; + private final int rolloutPercentage; + + private FeatureFlagVariant(Builder builder) { + this.key = builder.key; + this.name = builder.name; + this.rolloutPercentage = builder.rolloutPercentage; + } + + public static class Builder { + private final String key; + private final String name; + + private int rolloutPercentage = 0; + + public Builder(String key, String name) { + this.key = key; + this.name = name; + } + + public Builder rolloutPercentage(int rolloutPercentage) { + this.rolloutPercentage = rolloutPercentage; + return this; + } + + public FeatureFlagVariant build() { + return new FeatureFlagVariant(this); + } + } + + public String getKey() { + return key; + } + + public String getName() { + return name; + } + + public int getRolloutPercentage() { + return rolloutPercentage; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagVariant that = (FeatureFlagVariant) o; + return Objects.equals(getKey(), that.getKey()) && Objects.equals(getName(), that.getName()); + } + + @Override + public int hashCode() { + return Objects.hash(getKey(), getName()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagVariant.class.getSimpleName() + "[", "]") + .add("key='" + key + "'") + .add("name='" + name + "'") + .add("rolloutPercentage=" + rolloutPercentage) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariantMeta.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariantMeta.java new file mode 100644 index 0000000..4f7d5f1 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariantMeta.java @@ -0,0 +1,75 @@ +package com.posthog.java.flags; + +import java.util.Objects; +import java.util.StringJoiner; + +public class FeatureFlagVariantMeta { + public final String key; + public final double valueMin; + public final double valueMax; + + private FeatureFlagVariantMeta(Builder builder) { + this.key = builder.key; + this.valueMin = builder.valueMin; + this.valueMax = builder.valueMax; + } + + public static class Builder { + private final String key; + + private double valueMin = 0; + private double valueMax = 0; + + public Builder(String key) { + this.key = key; + } + + public Builder valueMin(double valueMin) { + this.valueMin = valueMin; + return this; + } + + public Builder valueMax(double valueMax) { + this.valueMax = valueMax; + return this; + } + + public FeatureFlagVariantMeta build() { + return new FeatureFlagVariantMeta(this); + } + } + + public String getKey() { + return key; + } + + public double getValueMin() { + return valueMin; + } + + public double getValueMax() { + return valueMax; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagVariantMeta that = (FeatureFlagVariantMeta) o; + return Objects.equals(getKey(), that.getKey()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getKey()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagVariantMeta.class.getSimpleName() + "[", "]") + .add("key='" + key + "'") + .add("valueMin=" + valueMin) + .add("valueMax=" + valueMax) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariants.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariants.java new file mode 100644 index 0000000..47a632e --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagVariants.java @@ -0,0 +1,52 @@ +package com.posthog.java.flags; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +public class FeatureFlagVariants { + + private final List variants; + + private FeatureFlagVariants(final Builder builder) { + this.variants = builder.variants; + } + + public static class Builder { + private List variants = new ArrayList<>(); + + public Builder variants(List variants) { + this.variants = variants; + return this; + } + + public FeatureFlagVariants build() { + return new FeatureFlagVariants(this); + } + } + + public List getVariants() { + return variants; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlagVariants featureFlagVariants1 = (FeatureFlagVariants) o; + return Objects.equals(getVariants(), featureFlagVariants1.getVariants()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getVariants()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlagVariants.class.getSimpleName() + "[", "]") + .add("variants=" + variants) + .toString(); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlags.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlags.java new file mode 100644 index 0000000..f3b7464 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlags.java @@ -0,0 +1,74 @@ +package com.posthog.java.flags; + +import java.util.*; + +public class FeatureFlags { + private final List flags; + private final Map groupTypeMapping; + private final Map cohorts; + + private FeatureFlags(final Builder builder) { + this.flags = builder.flags; + this.groupTypeMapping = builder.groupTypeMapping; + this.cohorts = builder.cohorts; + } + + public static class Builder { + private List flags = new ArrayList<>(); + private Map groupTypeMapping = new HashMap<>(); + private Map cohorts = new HashMap<>(); + + public Builder flags(List flags) { + this.flags = flags; + return this; + } + + public Builder groupTypeMapping(Map groupTypeMapping) { + this.groupTypeMapping = groupTypeMapping; + return this; + } + + public Builder cohorts(Map cohorts) { + this.cohorts = cohorts; + return this; + } + + public FeatureFlags build() { + return new FeatureFlags(this); + } + } + + public List getFlags() { + return flags; + } + + public Map getGroupTypeMapping() { + return groupTypeMapping; + } + + public Map getCohorts() { + return cohorts; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FeatureFlags that = (FeatureFlags) o; + return Objects.equals(getFlags(), that.getFlags()) && Objects.equals(getGroupTypeMapping(), that.getGroupTypeMapping()) && Objects.equals(getCohorts(), that.getCohorts()); + } + + @Override + public int hashCode() { + return Objects.hash(getFlags(), getGroupTypeMapping(), getCohorts()); + } + + @Override + public String toString() { + return new StringJoiner(", ", FeatureFlags.class.getSimpleName() + "[", "]") + .add("flags=" + flags) + .add("groupTypeMapping=" + groupTypeMapping) + .add("cohorts=" + cohorts) + .toString(); + } +} From 75eb6ebb92c654d6df1fe4e23e2e8d291cb464fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Hern=C3=A1ndez?= Date: Sun, 2 Jun 2024 14:16:08 +0200 Subject: [PATCH 2/5] implement feature flag poller --- .../com/posthog/java/FeatureFlagPoller.java | 502 ++++++++++++++++++ .../main/java/com/posthog/java/Getter.java | 11 + .../java/InconclusiveMatchException.java | 7 + .../posthog/java/flags/FeatureFlagParser.java | 158 ++++++ .../com/posthog/java/flags/hash/Hasher.java | 30 ++ .../posthog/java/FeatureFlagPollerTest.java | 74 +++ .../java/com/posthog/java/TestGetter.java | 90 ++++ .../java/flags/FeatureFlagParserTest.java | 180 +++++++ 8 files changed, 1052 insertions(+) create mode 100644 posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java create mode 100644 posthog/src/main/java/com/posthog/java/Getter.java create mode 100644 posthog/src/main/java/com/posthog/java/InconclusiveMatchException.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/FeatureFlagParser.java create mode 100644 posthog/src/main/java/com/posthog/java/flags/hash/Hasher.java create mode 100644 posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java create mode 100644 posthog/src/test/java/com/posthog/java/TestGetter.java create mode 100644 posthog/src/test/java/com/posthog/java/flags/FeatureFlagParserTest.java diff --git a/posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java b/posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java new file mode 100644 index 0000000..49590a7 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java @@ -0,0 +1,502 @@ +package com.posthog.java; + +import com.posthog.java.flags.*; +import com.posthog.java.flags.hash.Hasher; +import org.json.JSONObject; + +import java.time.Duration; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.util.concurrent.TimeUnit.*; + +public class FeatureFlagPoller { + private final String projectApiKey; + private final String personalApiKey; + private final CountDownLatch initialLoadLatch; + private final Duration featureFlagPollingInterval; + private final ScheduledExecutorService executor; + private final Getter getter; + private volatile List featureFlags = new ArrayList<>(); + private volatile Map cohorts = new HashMap<>(); + private volatile Map groups = new HashMap<>(); + + private FeatureFlagPoller(Builder builder) { + this.executor = builder.executor; + this.projectApiKey = builder.projectApiKey; + this.personalApiKey = builder.personalApiKey; + this.initialLoadLatch = new CountDownLatch(1); + this.featureFlagPollingInterval = builder.featureFlagPollingInterval; + this.getter = builder.getter; + } + + public static class Builder { + private final String projectApiKey; + private final String personalApiKey; + private final Getter getter; + + private Duration featureFlagPollingInterval = Duration.ofSeconds(300); + private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + + public Builder(String projectApiKey, String personalApiKey, Getter getter) { + this.projectApiKey = projectApiKey; + this.personalApiKey = personalApiKey; + this.getter = getter; + } + + public Builder featureFlagPollingInterval(Duration featureFlagPollingInterval) { + this.featureFlagPollingInterval = featureFlagPollingInterval; + return this; + } + + public Builder executor(ScheduledExecutorService executor) { + this.executor = executor; + return this; + } + + public FeatureFlagPoller build() { + return new FeatureFlagPoller(this); + } + } + + /** + * Polls the PostHog API for feature flags at the specified interval. + * The feature flags are stored in memory and can be accessed using the other methods in this class. + * This method will block until the initial load of feature flags is complete. + */ + public void poll() { + this.executor.scheduleAtFixedRate(() -> { + this.fetchFeatureFlags(); + this.initialLoadLatch.countDown(); + }, 0, this.featureFlagPollingInterval.getSeconds(), SECONDS); + } + + private void fetchFeatureFlags() { + final String url = String.format("/v1/api/feature_flag/local_evaluation?token=%s&send_cohorts=true", this.projectApiKey); + + final Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + this.personalApiKey); + headers.put("Content-Type", "application/json"); + headers.put("Accept", "application/json"); + headers.put("User-Agent", "PostHog-Java/1.0.0"); + + final JSONObject jsonResponse = getter.get(url, headers); + if (jsonResponse == null) { + System.err.println("Failed to fetch feature flags: response is null"); + return; + } + + final FeatureFlags featureFlags = FeatureFlagParser.parse(jsonResponse); + this.featureFlags = featureFlags.getFlags(); + this.cohorts = featureFlags.getCohorts(); + this.groups = featureFlags.getGroupTypeMapping(); + } + + /** + * Shuts down the executor service. + */ + public void shutdown() { + executor.shutdownNow(); + } + + /** + * @param config FeatureFlagConfig + * key: String + * distinctId: String + * groupProperties: Map> + * personProperties: Map + * groupProperties and personProperties are optional + * groupProperties is used for cohort matching + * personProperties is used for property matching + * + * @return boolean indicating whether the feature flag is enabled + */ + public boolean isFeatureFlagEnabled(FeatureFlagConfig config) { + final Optional featureFlag = getFeatureFlag(config); + return featureFlag.map(flag -> { + try { + return computeFlagLocally(flag, config, this.cohorts).isPresent(); + } catch (InconclusiveMatchException e) { + System.err.println("Error computing flag locally: " + e.getMessage()); + return false; + } + }).orElse(false); + } + + /** + * @param key String + * key of the feature flag + * @param distinctId String + * distinctId of the user + * @return boolean indicating whether the feature flag is enabled + */ + public boolean isFeatureFlagEnabled(String key, String distinctId) { + final FeatureFlagConfig config = new FeatureFlagConfig.Builder(key, distinctId).build(); + final Optional featureFlag = getFeatureFlag(config); + return featureFlag.map(flag -> { + try { + return computeFlagLocally(flag, config, this.cohorts).isPresent(); + } catch (InconclusiveMatchException e) { + System.err.println("Error computing flag locally: " + e.getMessage()); + return false; + } + }).orElse(false); + } + + /** + * @param config FeatureFlagConfig + * key: String + * distinctId: String + * groupProperties: Map> + * personProperties: Map + * groupProperties and personProperties are optional + * groupProperties is used for cohort matching + * personProperties is used for property matching + * + * @return Optional variant key of the feature flag + */ + public Optional getFeatureFlagVariant(FeatureFlagConfig config) { + final Optional featureFlag = getFeatureFlag(config); + return featureFlag.flatMap(flag -> getMatchingVariant(flag, config.getDistinctId()).map(FeatureFlagVariantMeta::getKey)); + } + + /** + * @param config FeatureFlagConfig + * key: String + * distinctId: String + * groupProperties: Map> + * personProperties: Map + * groupProperties and personProperties are optional + * groupProperties is used for cohort matching + * personProperties is used for property matching + * @return Optional feature flag + */ + public Optional getFeatureFlag(FeatureFlagConfig config) { + final Optional featureFlag = getFeatureFlags().stream() + .filter(flag -> flag.getKey().equals(config.getKey())) + .findFirst(); + + if (!featureFlag.isPresent()) { + return Optional.empty(); + } + + try { + final Optional computedFlag = computeFlagLocally(featureFlag.get(), config, this.cohorts); + if (computedFlag.isPresent()) { + return featureFlag; + } + } catch (InconclusiveMatchException e) { + System.err.println("Error computing flag locally: " + e.getMessage()); + } + + return Optional.empty(); + } + + /** + * If the feature flags have not been loaded, this method will block until they are loaded. + * + * @return List feature flags + */ + public List getFeatureFlags() { + try { + this.initialLoadLatch.await(); + if (this.featureFlags.isEmpty()) { + System.err.println("No feature flags loaded"); + return new ArrayList<>(); + } + } catch (InterruptedException e) { + System.err.println("Error waiting for initial load: " + e.getMessage()); + } + return featureFlags; + } + + private Optional computeFlagLocally( + FeatureFlag flag, + FeatureFlagConfig config, + Map cohorts + ) throws InconclusiveMatchException { + if (flag.isEnsureExperienceContinuity()) { + throw new InconclusiveMatchException("Flag has experience continuity enabled"); + } + + if (!flag.isActive()) { + return Optional.empty(); + } + + final int aggregationIndex = flag.getFilter() + .map(FeatureFlagFilter::getAggregationGroupTypeIndex) + .orElse(0); + + if (aggregationIndex > 0) { + final String groupName = groups.get(String.valueOf(aggregationIndex)); + if (groupName == null) { + throw new InconclusiveMatchException("Flag has unknown group type index"); + } + + final Map> groupProperties = addLocalGroupProperties(config.getGroupProperties(), groups); + return matchFeatureFlagProperties(flag, config.getDistinctId(), groupProperties.get(groupName), cohorts); + } + + final Map personProperties = addLocalPersonProperties(config.getPersonProperties(), config.getDistinctId()); + return matchFeatureFlagProperties(flag, config.getDistinctId(), personProperties, cohorts); + } + + private Map> addLocalGroupProperties(final Map> properties, final Map groups) { + return groups.entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> { + String groupName = entry.getKey(); + Map groupProps = new HashMap<>(); + groupProps.put("$group_key", entry.getValue()); + if (properties.containsKey(groupName)) { + groupProps.putAll(properties.get(groupName)); + } + return groupProps; + } + )); + } + + private Map addLocalPersonProperties(Map properties, String distinctId) { + final Map localProperties = properties.entrySet() + .stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue + )); + localProperties.put("distinct_id", distinctId); + return localProperties; + } + + private Optional matchFeatureFlagProperties( + FeatureFlag featureFlag, + String distinctId, + Map properties, + Map cohorts + ) throws InconclusiveMatchException { + + final List conditions = featureFlag.getFilter() + .map(FeatureFlagFilter::getGroups) + .orElse(new ArrayList<>()); + + final List sortedConditions = conditions.stream() + .sorted(Comparator.comparingInt(a -> a.getVariant().isPresent() ? -1 : 1)) + .collect(Collectors.toList()); + + boolean isInconclusive = false; + for (FeatureFlagCondition condition : sortedConditions) { + boolean isMatch; + + try { + isMatch = isConditionMatch(featureFlag, distinctId, condition, properties, cohorts); + } catch (InconclusiveMatchException e) { + isInconclusive = true; + continue; + } + + if (isMatch) { + if (condition.getVariant().isPresent()) { + return condition.getVariant(); + } + + return getMatchingVariant(featureFlag, distinctId) + .map(FeatureFlagVariantMeta::getKey); + } + } + + if (isInconclusive) { + throw new InconclusiveMatchException("Error matching conditions"); + } + + return Optional.empty(); + } + + private boolean isConditionMatch( + FeatureFlag featureFlag, + String distinctId, + FeatureFlagCondition condition, + Map properties, + Map cohorts + ) throws InconclusiveMatchException { + + for (FeatureFlagProperty property : condition.getProperties()) { + boolean matches; + if (property.isCohort()) { + matches = matchCohort(property, properties, cohorts); + } else { + matches = matchProperty(property, properties); + } + + if (!matches) { + return false; + } + } + + return condition.getRolloutPercentage() == 0 || + isSimpleFlagEnabled(featureFlag.getKey(), distinctId, condition.getRolloutPercentage()); + } + + private boolean matchCohort( + FeatureFlagProperty property, + Map properties, + Map cohorts + ) throws InconclusiveMatchException { + + final FeatureFlagPropertyGroup cohort = cohorts.get(property.getKey()); + + if (cohort == null) { + throw new InconclusiveMatchException("Cohort not found"); + } + + return matchPropertyGroup(cohort, properties); + } + + private boolean matchPropertyGroup( + FeatureFlagPropertyGroup featureFlagPropertyGroup, + Map properties + ) throws InconclusiveMatchException { + + if (featureFlagPropertyGroup.getValues().isEmpty()) { + return true; + } + + boolean errorMatchingLocally = false; + + for (Object value : featureFlagPropertyGroup.getValues()) { + boolean matches; + + if (value instanceof FeatureFlagPropertyGroup) { + try { + matches = matchPropertyGroup((FeatureFlagPropertyGroup) value, properties); + } catch (InconclusiveMatchException e) { + errorMatchingLocally = true; + continue; + } + } else { + final FeatureFlagProperty flagProperty = (FeatureFlagProperty) value; + try { + matches = matchProperty(flagProperty, properties); + } catch (InconclusiveMatchException e) { + errorMatchingLocally = true; + continue; + } + } + + final String propertyGroupType = featureFlagPropertyGroup.getType(); + if (propertyGroupType.equals("AND") && !matches) { + return false; + } else if (propertyGroupType.equals("OR") && matches) { + return true; + } + } + + if (errorMatchingLocally) { + throw new InconclusiveMatchException("Error matching property group"); + } + + return featureFlagPropertyGroup.getType().equals("AND"); + } + + private boolean matchProperty( + FeatureFlagProperty property, + Map properties + ) throws InconclusiveMatchException { + final Optional overrideValue = Optional.ofNullable(properties.get(property.getKey())); + final List propertyValue = property.getValue(); + + return propertyValue.stream() + .anyMatch(eachPropertyValue -> + property.getOperator() + .map(operator -> { + switch (operator) { + case EXACT: + final boolean result = overrideValue + .map(value -> value.equals(eachPropertyValue)) + .orElse(false); + return result; + case IS_NOT: + return overrideValue + .map(value -> !value.equals(eachPropertyValue)) + .orElse(false); + case IS_SET: + return overrideValue.isPresent(); + case CONTAINS_INSENSITIVE: + return overrideValue.map(value -> value.toString().toLowerCase()) + .map(value -> value.contains(eachPropertyValue.toLowerCase())) + .orElse(false); + + case NOT_CONTAINS_INSENSITIVE: + return overrideValue.map(value -> value.toString().toLowerCase()) + .map(value -> !value.contains(eachPropertyValue.toLowerCase())) + .orElse(false); + case REGEX: + return overrideValue.map(value -> overrideValue.toString()) + .map(value -> Pattern.compile(eachPropertyValue).matcher(value).find()) + .orElse(false); + case NOT_REGEX: + return overrideValue.map(value -> overrideValue.toString()) + .map(value -> !Pattern.compile(eachPropertyValue).matcher(value).find()) + .orElse(false); + case GREATER_THAN: + return overrideValue.map(value -> Double.parseDouble(value.toString())) + .orElse(0.0) > Double.parseDouble(eachPropertyValue); + case LESS_THAN: + return overrideValue + .map(value -> Double.parseDouble(value.toString())) + .orElse(0.0) < Double.parseDouble(eachPropertyValue); + case GREATER_THAN_OR_EQUAL: + return overrideValue + .map(value -> Double.parseDouble(value.toString())) + .orElse(0.0) >= Double.parseDouble(eachPropertyValue); + case LESS_THAN_OR_EQUAL: + return overrideValue + .map(value -> Double.parseDouble(value.toString())) + .orElse(0.0) <= Double.parseDouble(eachPropertyValue); + } + return false; + }) + .orElse(false)); + } + + private Optional getMatchingVariant(FeatureFlag featureFlag, String distinctId) { + final List lookupTable = getVariantLookupTable(featureFlag); + + double flagHash = Hasher.hash(featureFlag.getKey(), distinctId, "variant"); + + return lookupTable.stream() + .filter(variantMeta -> flagHash >= variantMeta.getValueMin() && flagHash < variantMeta.getValueMax()) + .findFirst(); + } + + private List getVariantLookupTable(FeatureFlag flag) { + final List variants = flag.getFilter() + .flatMap(FeatureFlagFilter::getMultivariate) + .map(FeatureFlagVariants::getVariants) + .orElse(new ArrayList<>()); + + double valueMin = 0.0; + + List lookupTable = new ArrayList<>(); + + for (FeatureFlagVariant variant : variants) { + final double valueMax = valueMin + (double) variant.getRolloutPercentage() / 100; + final FeatureFlagVariantMeta variantMeta = new FeatureFlagVariantMeta.Builder(variant.getKey()) + .valueMin(valueMin) + .valueMax(valueMax) + .build(); + lookupTable.add(variantMeta); + valueMin = valueMax; + } + + return lookupTable; + } + + private boolean isSimpleFlagEnabled(String key, String distinctId, int rolloutPercentage) { + return Hasher.hash(key, distinctId, "") < (double) rolloutPercentage / 100; + } +} diff --git a/posthog/src/main/java/com/posthog/java/Getter.java b/posthog/src/main/java/com/posthog/java/Getter.java new file mode 100644 index 0000000..1a53200 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/Getter.java @@ -0,0 +1,11 @@ +package com.posthog.java; + +import org.json.JSONObject; + +import java.util.Map; + +interface Getter { + + JSONObject get(String route, Map headers); + +} diff --git a/posthog/src/main/java/com/posthog/java/InconclusiveMatchException.java b/posthog/src/main/java/com/posthog/java/InconclusiveMatchException.java new file mode 100644 index 0000000..abf6da9 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/InconclusiveMatchException.java @@ -0,0 +1,7 @@ +package com.posthog.java; + +public class InconclusiveMatchException extends Exception { + public InconclusiveMatchException(String message) { + super(message); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagParser.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagParser.java new file mode 100644 index 0000000..f662122 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagParser.java @@ -0,0 +1,158 @@ +package com.posthog.java.flags; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +public class FeatureFlagParser { + + public static FeatureFlags parse(JSONObject responseRaw) { + final List flags = StreamSupport.stream(responseRaw.optJSONArray("flags").spliterator(), false) + .map(JSONObject.class::cast) + .map(FeatureFlagParser::parseFeatureFlag) + .collect(Collectors.toList()); + + return new FeatureFlags.Builder() + .flags(flags) + .groupTypeMapping(parseGroupTypeMapping(responseRaw.optJSONObject("group_type_mapping"))) + .cohorts(parseCohorts(responseRaw.optJSONObject("cohorts"))) + .build(); + } + + private static FeatureFlag parseFeatureFlag(JSONObject featureFlagRaw) throws JSONException { + if (featureFlagRaw == null) + return null; + + final String key = featureFlagRaw.getString("key"); + final int id = featureFlagRaw.getInt("id"); + final int teamId = featureFlagRaw.getInt("team_id"); + + return new FeatureFlag.Builder(key, id, teamId) + .isSimpleFlag(featureFlagRaw.optBoolean("is_simple_flag")) + .rolloutPercentage(featureFlagRaw.optInt("rollout_percentage")) + .active(featureFlagRaw.optBoolean("active")) + .filter(parseFilter(featureFlagRaw.optJSONObject("filters"))) + .ensureExperienceContinuity(featureFlagRaw.optBoolean("ensure_experience_continuity")) + .build(); + } + + private static FeatureFlagFilter parseFilter(JSONObject filterRaw) throws JSONException { + if (filterRaw == null) + return null; + + return new FeatureFlagFilter.Builder() + .aggregationGroupTypeIndex(filterRaw.optInt("aggregation_group_type_index")) + .groups(parseFeatureFlagConditions(filterRaw.optJSONArray("groups"))) + .multivariate(parseVariants(filterRaw.optJSONObject("multivariate"))) + .payloads(filterRaw.optJSONObject("payloads").toMap()) + .build(); + } + + private static List parseFeatureFlagConditions(JSONArray conditionsRaw) throws JSONException { + if (conditionsRaw == null) + return new ArrayList<>(); + + return StreamSupport.stream(conditionsRaw.spliterator(), false) + .map(JSONObject.class::cast) + .map(conditionRaw -> { + final List properties = StreamSupport.stream(conditionRaw.getJSONArray("properties").spliterator(), false) + .map(JSONObject.class::cast) + .map(FeatureFlagParser::parseFlagProperty) + .collect(Collectors.toList()); + return parseFeatureFlagCondition(properties, conditionRaw); + }) + .collect(Collectors.toList()); + } + + private static FeatureFlagProperty parseFlagProperty(JSONObject flagPropertyRaw) throws JSONException { + if (flagPropertyRaw == null) + return null; + + final List value = flagPropertyRaw.optJSONArray("value") != null + ? StreamSupport.stream(flagPropertyRaw.getJSONArray("value").spliterator(), false) + .map(Object::toString) + .collect(Collectors.toList()) + : Collections.singletonList(flagPropertyRaw.optString("value")); + + return new FeatureFlagProperty.Builder(flagPropertyRaw.optString("key")) + .negation(flagPropertyRaw.optBoolean("negation", false)) + .value(value) + .operator(flagPropertyRaw.optString("operator")) + .type(flagPropertyRaw.optString("type")) + .build(); + } + + private static FeatureFlagCondition parseFeatureFlagCondition(final List properties, JSONObject conditionRaw) throws JSONException { + if (conditionRaw == null) + return null; + + return new FeatureFlagCondition.Builder() + .properties(properties) + .rolloutPercentage(conditionRaw.optInt("rollout_percentage")) + .variant(conditionRaw.optString("variant")) + .build(); + } + + private static FeatureFlagVariants parseVariants(JSONObject variantsRaw) throws JSONException { + if (variantsRaw == null) + return null; + + return StreamSupport.stream(variantsRaw.getJSONArray("variants").spliterator(), false) + .map(JSONObject.class::cast) + .map(FeatureFlagParser::parseFlagVariant) + .collect(Collectors.collectingAndThen(Collectors.toList(), variants -> new FeatureFlagVariants.Builder().variants(variants).build())); + } + + private static FeatureFlagVariant parseFlagVariant(JSONObject flagVariantRaw) throws JSONException { + if (flagVariantRaw == null) + return null; + + return new FeatureFlagVariant.Builder(flagVariantRaw.getString("key"), flagVariantRaw.getString("name")) + .rolloutPercentage(flagVariantRaw.optInt("rollout_percentage")) + .build(); + } + + private static Map parseCohorts(JSONObject cohortsRaw) throws JSONException { + if (cohortsRaw == null) + return new HashMap<>(); + + return cohortsRaw.keySet() + .stream() + .collect(Collectors.toMap(key -> key, key -> parsePropertyGroup(cohortsRaw.getJSONObject(key)))); + } + + private static FeatureFlagPropertyGroup parsePropertyGroup(JSONObject propertyGroupRaw) throws JSONException { + final List values = new ArrayList<>(); + final JSONArray valuesJson = propertyGroupRaw.getJSONArray("values"); + + for (int i = 0; i < valuesJson.length(); i++) { + Object value = valuesJson.get(i); + if (value instanceof JSONObject) { + final JSONObject possibleChild = (JSONObject) value; + if (possibleChild.has("type")) { + values.add(parsePropertyGroup(possibleChild)); + } + } else { + values.add(value); + } + } + + return new FeatureFlagPropertyGroup.Builder() + .type(propertyGroupRaw.optString("type")) + .values(values) + .build(); + } + + private static Map parseGroupTypeMapping(JSONObject groupTypeMappingRaw) throws JSONException { + if (groupTypeMappingRaw == null) + return new HashMap<>(); + + return groupTypeMappingRaw.keySet() + .stream() + .collect(Collectors.toMap(key -> key, groupTypeMappingRaw::getString)); + } +} diff --git a/posthog/src/main/java/com/posthog/java/flags/hash/Hasher.java b/posthog/src/main/java/com/posthog/java/flags/hash/Hasher.java new file mode 100644 index 0000000..acc4f78 --- /dev/null +++ b/posthog/src/main/java/com/posthog/java/flags/hash/Hasher.java @@ -0,0 +1,30 @@ +package com.posthog.java.flags.hash; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class Hasher { + private static final long LONG_SCALE = 0xfffffffffffffffL; + + public static double hash(String key, String distinctId, String salt) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.update((key + "." + distinctId + salt).getBytes(StandardCharsets.UTF_8)); + byte[] hash = digest.digest(); + String hexString = bytesToHex(hash).substring(0, 15); + long value = Long.parseLong(hexString, 16); + return (double) value / LONG_SCALE; + } catch (NoSuchAlgorithmException | NumberFormatException e) { + throw new RuntimeException("Hashing error: " + e.getMessage(), e); + } + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } +} diff --git a/posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java b/posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java new file mode 100644 index 0000000..86a7e4e --- /dev/null +++ b/posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java @@ -0,0 +1,74 @@ +package com.posthog.java; + +import com.posthog.java.TestGetter; +import com.posthog.java.flags.FeatureFlag; +import com.posthog.java.flags.FeatureFlagConfig; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.*; + +public class FeatureFlagPollerTest { + + private FeatureFlagPoller sut; + + @Before + public void setUp() { + TestGetter testGetter = new TestGetter(); + sut = new FeatureFlagPoller.Builder("", "", testGetter) + .build(); + + sut.poll(); + } + + @Test + public void shouldRetrieveAllFlags() { + final List flags = sut.getFeatureFlags(); + assertEquals(1, flags.size()); + assertEquals("java-feature-flag", flags.get(0).getKey()); + assertEquals(1000, flags.get(0).getId()); + assertEquals(20000, flags.get(0).getTeamId()); + } + + @Test + public void shouldReturnTrueWhenFeatureFlagIsEnabledForUser() { + FeatureFlagConfig config = new FeatureFlagConfig.Builder("java-feature-flag", "id-1") + .build(); + + final boolean enabled = sut.isFeatureFlagEnabled(config); + assertTrue(enabled); + } + + @Test + public void shouldReturnFalseWhenFeatureFlagIsDisabledForUser() { + FeatureFlagConfig config = new FeatureFlagConfig.Builder("java-feature-flag", "some-id") + .build(); + + final boolean enabled = sut.isFeatureFlagEnabled(config); + assertFalse(enabled); + } + + @Test + public void shouldReturnFeatureFlagVariant() { + FeatureFlagConfig config = new FeatureFlagConfig.Builder("java-feature-flag", "id-1") + .build(); + + final Optional variant = sut.getFeatureFlagVariant(config); + assertTrue(variant.isPresent()); + } + + @Test + public void shouldBeAbleToReturnTheFullFeatureFlag() { + FeatureFlagConfig config = new FeatureFlagConfig.Builder("java-feature-flag", "id-1") + .build(); + + final Optional flag = sut.getFeatureFlag(config); + assertTrue(flag.isPresent()); + assertEquals("java-feature-flag", flag.get().getKey()); + assertEquals(1000, flag.get().getId()); + assertEquals(20000, flag.get().getTeamId()); + } +} diff --git a/posthog/src/test/java/com/posthog/java/TestGetter.java b/posthog/src/test/java/com/posthog/java/TestGetter.java new file mode 100644 index 0000000..e0e9117 --- /dev/null +++ b/posthog/src/test/java/com/posthog/java/TestGetter.java @@ -0,0 +1,90 @@ +package com.posthog.java; + +import org.json.JSONObject; + +import java.util.Map; + +public class TestGetter implements Getter { + + @Override + public JSONObject get(String route, Map headers) { + String jsonString = "{\n" + + " \"flags\": [\n" + + " {\n" + + " \"id\": 1000,\n" + + " \"team_id\": 20000,\n" + + " \"name\": \"\",\n" + + " \"key\": \"java-feature-flag\",\n" + + " \"filters\": {\n" + + " \"groups\": [\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"id\",\n" + + " \"type\": \"cohort\",\n" + + " \"value\": 17231\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 39\n" + + " },\n" + + " {\n" + + " \"variant\": null,\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": [\n" + + " \"id-1\"\n" + + " ],\n" + + " \"operator\": \"exact\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": \"a-value\",\n" + + " \"operator\": \"icontains\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 41\n" + + " }\n" + + " ],\n" + + " \"payloads\": {\n" + + " \"variant-1\": \"{\\\"something\\\": 1}\",\n" + + " \"variant-2\": \"1\"\n" + + " },\n" + + " \"multivariate\": {\n" + + " \"variants\": [\n" + + " {\n" + + " \"key\": \"variant-1\",\n" + + " \"name\": \"\",\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"key\": \"variant-2\",\n" + + " \"name\": \"with description\",\n" + + " \"rollout_percentage\": 0\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"deleted\": false,\n" + + " \"active\": true,\n" + + " \"ensure_experience_continuity\": false\n" + + " }\n" + + " ],\n" + + " \"group_type_mapping\": {},\n" + + " \"cohorts\": {}\n" + + "}"; + + + return new JSONObject(jsonString); + } + +} diff --git a/posthog/src/test/java/com/posthog/java/flags/FeatureFlagParserTest.java b/posthog/src/test/java/com/posthog/java/flags/FeatureFlagParserTest.java new file mode 100644 index 0000000..e40c287 --- /dev/null +++ b/posthog/src/test/java/com/posthog/java/flags/FeatureFlagParserTest.java @@ -0,0 +1,180 @@ +package com.posthog.java.flags; + +import org.json.JSONObject; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class FeatureFlagParserTest { + + @Test + public void testParse() { + // Arrange + String jsonString = "{\n" + + " \"flags\": [\n" + + " {\n" + + " \"id\": 1000,\n" + + " \"team_id\": 20000,\n" + + " \"name\": \"\",\n" + + " \"key\": \"java-feature-flag\",\n" + + " \"filters\": {\n" + + " \"groups\": [\n" + + " {\n" + + " \"variant\": \"variant-1\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": \"is_set\",\n" + + " \"operator\": \"is_set\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 53\n" + + " },\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"id\",\n" + + " \"type\": \"cohort\",\n" + + " \"value\": 17231\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 39\n" + + " },\n" + + " {\n" + + " \"variant\": null,\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": [\n" + + " \"\\\"id-1\\\"\"\n" + + " ],\n" + + " \"operator\": \"exact\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 30\n" + + " },\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": \"a-value\",\n" + + " \"operator\": \"icontains\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 41\n" + + " }\n" + + " ],\n" + + " \"payloads\": {\n" + + " \"variant-1\": \"{\\\"something\\\": 1}\",\n" + + " \"variant-2\": \"1\"\n" + + " },\n" + + " \"multivariate\": {\n" + + " \"variants\": [\n" + + " {\n" + + " \"key\": \"variant-1\",\n" + + " \"name\": \"\",\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"key\": \"variant-2\",\n" + + " \"name\": \"with description\",\n" + + " \"rollout_percentage\": 0\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"deleted\": false,\n" + + " \"active\": true,\n" + + " \"ensure_experience_continuity\": false\n" + + " }\n" + + " ],\n" + + " \"group_type_mapping\": {},\n" + + " \"cohorts\": {}\n" + + "}"; + + // Act + FeatureFlags flags = FeatureFlagParser.parse(new JSONObject(jsonString)); + + // Assert + assertNotNull(flags); + assertEquals(1, flags.getFlags().size()); + FeatureFlag flag = flags.getFlags().get(0); + assertEquals(1000, flag.getId()); + assertEquals(20000, flag.getTeamId()); + assertEquals("java-feature-flag", flag.getKey()); + assertTrue(flag.isActive()); + assertFalse(flag.isSimpleFlag()); + assertFalse(flag.isEnsureExperienceContinuity()); + assertFalse(flag.isSimpleFlag()); + assertFalse(flag.isDeleted()); + + assertTrue(flag.getFilter().isPresent()); + final FeatureFlagFilter filter = flag.getFilter().get(); + assertEquals(0, filter.getAggregationGroupTypeIndex()); + assertEquals(4, filter.getGroups().size()); + assertEquals(2, filter.getPayloads().size()); + + final List groups = filter.getGroups(); + assertEquals(4, groups.size()); + assertTrue(groups.get(0).getVariant().isPresent()); + assertEquals("variant-1", groups.get(0).getVariant().get()); + assertEquals(1, groups.get(0).getProperties().size()); + assertEquals(53, groups.get(0).getRolloutPercentage()); + assertFalse(groups.get(0).getProperties().get(0).getValue().isEmpty()); + assertTrue(groups.get(0).getProperties().get(0).getType().isPresent()); + assertEquals("person", groups.get(0).getProperties().get(0).getType().get()); + + assertTrue(groups.get(1).getVariant().isPresent()); + assertEquals("variant-2", groups.get(1).getVariant().get()); + assertEquals(1, groups.get(1).getProperties().size()); + assertEquals(39, groups.get(1).getRolloutPercentage()); + assertFalse(groups.get(1).getProperties().get(0).getValue().isEmpty()); + assertTrue(groups.get(1).getProperties().get(0).getType().isPresent()); + assertEquals("cohort", groups.get(1).getProperties().get(0).getType().get()); + + assertTrue(filter.getMultivariate().isPresent()); + final FeatureFlagVariants multivariate = filter.getMultivariate().get(); + assertEquals(2, multivariate.getVariants().size()); + + final FeatureFlagVariant variant1 = multivariate.getVariants().get(0); + assertEquals("variant-1", variant1.getKey()); + assertEquals("", variant1.getName()); + assertEquals(100, variant1.getRolloutPercentage()); + + final FeatureFlagVariant variant2 = multivariate.getVariants().get(1); + assertEquals("variant-2", variant2.getKey()); + assertEquals("with description", variant2.getName()); + assertEquals(0, variant2.getRolloutPercentage()); + + final FeatureFlagCondition group = filter.getGroups().get(0); + assertTrue(group.getVariant().isPresent()); + assertEquals("variant-1", group.getVariant().get()); + assertEquals(1, group.getProperties().size()); + assertEquals("distinct_id", group.getProperties().get(0).getKey()); + + final FeatureFlagProperty property = group.getProperties().get(0); + assertTrue(property.getOperator().isPresent()); + assertEquals(FeatureFlagPropertyOperator.IS_SET, property.getOperator().get()); + assertFalse(property.getValue().isEmpty()); + assertEquals(Collections.singletonList("is_set"), property.getValue()); + assertTrue(property.getType().isPresent()); + assertEquals("person", property.getType().get()); + assertEquals("distinct_id", property.getKey()); + + assertEquals(2, filter.getPayloads().size()); + assertTrue(filter.getPayloads().containsKey("variant-1")); + assertEquals("{\"something\": 1}", filter.getPayloads().get("variant-1")); + + assertTrue(filter.getPayloads().containsKey("variant-2")); + assertEquals("1", filter.getPayloads().get("variant-2")); + } + +} From 17a6d6a95d30da382c63d353d4df71c044122662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Hern=C3=A1ndez?= Date: Sun, 2 Jun 2024 14:27:21 +0200 Subject: [PATCH 3/5] document the GetterAPI --- posthog/src/main/java/com/posthog/java/Getter.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/posthog/src/main/java/com/posthog/java/Getter.java b/posthog/src/main/java/com/posthog/java/Getter.java index 1a53200..38e7aed 100644 --- a/posthog/src/main/java/com/posthog/java/Getter.java +++ b/posthog/src/main/java/com/posthog/java/Getter.java @@ -4,8 +4,18 @@ import java.util.Map; +/* + * Getter interface for making HTTP GET requests to the PostHog API + */ interface Getter { + /* + * Make a GET request to the PostHog API + * + * @param route The route to make the GET request to + * @param headers The headers to include in the GET request + * @return The JSON response from the GET request + */ JSONObject get(String route, Map headers); } From 9a5f2ab45c49d1228020387cce8c251870697074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Hern=C3=A1ndez?= Date: Sun, 2 Jun 2024 14:42:14 +0200 Subject: [PATCH 4/5] add forceReload method and test --- .../com/posthog/java/FeatureFlagPoller.java | 7 + .../posthog/java/FeatureFlagPollerTest.java | 170 +++++++++++++++++- .../java/com/posthog/java/TestGetter.java | 164 +++++++++-------- 3 files changed, 263 insertions(+), 78 deletions(-) diff --git a/posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java b/posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java index 49590a7..cbac007 100644 --- a/posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java +++ b/posthog/src/main/java/com/posthog/java/FeatureFlagPoller.java @@ -103,6 +103,13 @@ public void shutdown() { executor.shutdownNow(); } + /** + * Forces a reload of the feature flags. + */ + public void forceReload() { + this.fetchFeatureFlags(); + } + /** * @param config FeatureFlagConfig * key: String diff --git a/posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java b/posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java index 86a7e4e..276958e 100644 --- a/posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java +++ b/posthog/src/test/java/com/posthog/java/FeatureFlagPollerTest.java @@ -13,11 +13,12 @@ public class FeatureFlagPollerTest { + private TestGetter testGetter; private FeatureFlagPoller sut; @Before public void setUp() { - TestGetter testGetter = new TestGetter(); + testGetter = new TestGetter(); sut = new FeatureFlagPoller.Builder("", "", testGetter) .build(); @@ -71,4 +72,171 @@ public void shouldBeAbleToReturnTheFullFeatureFlag() { assertEquals(1000, flag.get().getId()); assertEquals(20000, flag.get().getTeamId()); } + + @Test + public void reloadFeatureFlags() { + final List flags = sut.getFeatureFlags(); + assertEquals(1, flags.size()); + assertEquals("java-feature-flag", flags.get(0).getKey()); + assertEquals(1000, flags.get(0).getId()); + assertEquals(20000, flags.get(0).getTeamId()); + + + testGetter.setJsonString( + "{\n" + + " \"flags\": [\n" + + " {\n" + + " \"id\": 1000,\n" + + " \"team_id\": 20000,\n" + + " \"name\": \"\",\n" + + " \"key\": \"java-feature-flag\",\n" + + " \"filters\": {\n" + + " \"groups\": [\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"id\",\n" + + " \"type\": \"cohort\",\n" + + " \"value\": 17231\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 39\n" + + " },\n" + + " {\n" + + " \"variant\": null,\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": [\n" + + " \"id-1\"\n" + + " ],\n" + + " \"operator\": \"exact\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": \"a-value\",\n" + + " \"operator\": \"icontains\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 41\n" + + " }\n" + + " ],\n" + + " \"payloads\": {\n" + + " \"variant-1\": \"{\\\"something\\\": 1}\",\n" + + " \"variant-2\": \"1\"\n" + + " },\n" + + " \"multivariate\": {\n" + + " \"variants\": [\n" + + " {\n" + + " \"key\": \"variant-1\",\n" + + " \"name\": \"\",\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"key\": \"variant-2\",\n" + + " \"name\": \"with description\",\n" + + " \"rollout_percentage\": 0\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"deleted\": false,\n" + + " \"active\": true,\n" + + " \"ensure_experience_continuity\": false\n" + + " },\n" + + " {\n" + + " \"id\": 1001,\n" + + " \"team_id\": 20000,\n" + + " \"name\": \"\",\n" + + " \"key\": \"java-feature-flag-2\",\n" + + " \"filters\": {\n" + + " \"groups\": [\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"id\",\n" + + " \"type\": \"cohort\",\n" + + " \"value\": 17231\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 39\n" + + " },\n" + + " {\n" + + " \"variant\": null,\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": [\n" + + " \"id-1\"\n" + + " ],\n" + + " \"operator\": \"exact\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": \"a-value\",\n" + + " \"operator\": \"icontains\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 41\n" + + " }\n" + + " ],\n" + + " \"payloads\": {\n" + + " \"variant-1\": \"{\\\"something\\\": 1}\",\n" + + " \"variant-2\": \"1\"\n" + + " },\n" + + " \"multivariate\": {\n" + + " \"variants\": [\n" + + " {\n" + + " \"key\": \"variant-1\",\n" + + " \"name\": \"\",\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"key\": \"variant-2\",\n" + + " \"name\": \"with description\",\n" + + " \"rollout_percentage\": 0\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"deleted\": false,\n" + + " \"active\": true,\n" + + " \"ensure_experience_continuity\": false\n" + + " }\n" + + " ],\n" + + " \"group_type_mapping\": {},\n" + + " \"cohorts\": {}\n" + + "}" + ); + + sut.forceReload(); + + final List flags2 = sut.getFeatureFlags(); + assertEquals(2, flags2.size()); + assertEquals("java-feature-flag", flags2.get(0).getKey()); + assertEquals(1000, flags2.get(0).getId()); + assertEquals(20000, flags2.get(0).getTeamId()); + assertEquals("java-feature-flag-2", flags2.get(1).getKey()); + assertEquals(1001, flags2.get(1).getId()); + assertEquals(20000, flags2.get(1).getTeamId()); + } + } diff --git a/posthog/src/test/java/com/posthog/java/TestGetter.java b/posthog/src/test/java/com/posthog/java/TestGetter.java index e0e9117..b51479e 100644 --- a/posthog/src/test/java/com/posthog/java/TestGetter.java +++ b/posthog/src/test/java/com/posthog/java/TestGetter.java @@ -6,85 +6,95 @@ public class TestGetter implements Getter { - @Override - public JSONObject get(String route, Map headers) { - String jsonString = "{\n" - + " \"flags\": [\n" - + " {\n" - + " \"id\": 1000,\n" - + " \"team_id\": 20000,\n" - + " \"name\": \"\",\n" - + " \"key\": \"java-feature-flag\",\n" - + " \"filters\": {\n" - + " \"groups\": [\n" - + " {\n" - + " \"variant\": \"variant-2\",\n" - + " \"properties\": [\n" - + " {\n" - + " \"key\": \"id\",\n" - + " \"type\": \"cohort\",\n" - + " \"value\": 17231\n" - + " }\n" - + " ],\n" - + " \"rollout_percentage\": 39\n" - + " },\n" - + " {\n" - + " \"variant\": null,\n" - + " \"properties\": [\n" - + " {\n" - + " \"key\": \"distinct_id\",\n" - + " \"type\": \"person\",\n" - + " \"value\": [\n" - + " \"id-1\"\n" - + " ],\n" - + " \"operator\": \"exact\"\n" - + " }\n" - + " ],\n" - + " \"rollout_percentage\": 100\n" - + " },\n" - + " {\n" - + " \"variant\": \"variant-2\",\n" - + " \"properties\": [\n" - + " {\n" - + " \"key\": \"distinct_id\",\n" - + " \"type\": \"person\",\n" - + " \"value\": \"a-value\",\n" - + " \"operator\": \"icontains\"\n" - + " }\n" - + " ],\n" - + " \"rollout_percentage\": 41\n" - + " }\n" - + " ],\n" - + " \"payloads\": {\n" - + " \"variant-1\": \"{\\\"something\\\": 1}\",\n" - + " \"variant-2\": \"1\"\n" - + " },\n" - + " \"multivariate\": {\n" - + " \"variants\": [\n" - + " {\n" - + " \"key\": \"variant-1\",\n" - + " \"name\": \"\",\n" - + " \"rollout_percentage\": 100\n" - + " },\n" - + " {\n" - + " \"key\": \"variant-2\",\n" - + " \"name\": \"with description\",\n" - + " \"rollout_percentage\": 0\n" - + " }\n" - + " ]\n" - + " }\n" - + " },\n" - + " \"deleted\": false,\n" - + " \"active\": true,\n" - + " \"ensure_experience_continuity\": false\n" - + " }\n" - + " ],\n" - + " \"group_type_mapping\": {},\n" - + " \"cohorts\": {}\n" - + "}"; + private String jsonString; + public String getJsonString() { + if (jsonString == null) { + return "{\n" + + " \"flags\": [\n" + + " {\n" + + " \"id\": 1000,\n" + + " \"team_id\": 20000,\n" + + " \"name\": \"\",\n" + + " \"key\": \"java-feature-flag\",\n" + + " \"filters\": {\n" + + " \"groups\": [\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"id\",\n" + + " \"type\": \"cohort\",\n" + + " \"value\": 17231\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 39\n" + + " },\n" + + " {\n" + + " \"variant\": null,\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": [\n" + + " \"id-1\"\n" + + " ],\n" + + " \"operator\": \"exact\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"variant\": \"variant-2\",\n" + + " \"properties\": [\n" + + " {\n" + + " \"key\": \"distinct_id\",\n" + + " \"type\": \"person\",\n" + + " \"value\": \"a-value\",\n" + + " \"operator\": \"icontains\"\n" + + " }\n" + + " ],\n" + + " \"rollout_percentage\": 41\n" + + " }\n" + + " ],\n" + + " \"payloads\": {\n" + + " \"variant-1\": \"{\\\"something\\\": 1}\",\n" + + " \"variant-2\": \"1\"\n" + + " },\n" + + " \"multivariate\": {\n" + + " \"variants\": [\n" + + " {\n" + + " \"key\": \"variant-1\",\n" + + " \"name\": \"\",\n" + + " \"rollout_percentage\": 100\n" + + " },\n" + + " {\n" + + " \"key\": \"variant-2\",\n" + + " \"name\": \"with description\",\n" + + " \"rollout_percentage\": 0\n" + + " }\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"deleted\": false,\n" + + " \"active\": true,\n" + + " \"ensure_experience_continuity\": false\n" + + " }\n" + + " ],\n" + + " \"group_type_mapping\": {},\n" + + " \"cohorts\": {}\n" + + "}"; + } + return jsonString; + } - return new JSONObject(jsonString); + public void setJsonString(String jsonString) { + this.jsonString = jsonString; + } + + @Override + public JSONObject get(String route, Map headers) { + return new JSONObject(this.getJsonString()); } } From 1c84fe24794ea320a7589f0512d4082aca916067 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Hern=C3=A1ndez?= Date: Sun, 2 Jun 2024 17:56:55 +0200 Subject: [PATCH 5/5] fix FeatureFlagCondition getValue --- .../java/com/posthog/java/flags/FeatureFlagCondition.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java index 146b5ec..fd0d34e 100644 --- a/posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java +++ b/posthog/src/main/java/com/posthog/java/flags/FeatureFlagCondition.java @@ -47,7 +47,11 @@ public int getRolloutPercentage() { } public Optional getVariant() { - return Optional.ofNullable(variant); + if (variant == null || variant.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(variant); } @Override