Skip to content

Commit

Permalink
feat(flags): Locally evaluate all cohorts (#63)
Browse files Browse the repository at this point in the history
  • Loading branch information
neilkakkar authored Mar 13, 2024
1 parent 04f7509 commit 07e3875
Show file tree
Hide file tree
Showing 5 changed files with 523 additions and 111 deletions.
12 changes: 10 additions & 2 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ class Client
*/
public $groupTypeMapping;

/**
* @var array
*/
public $cohorts;


/**
* @var SizeLimitedHash
*/
Expand Down Expand Up @@ -92,6 +98,7 @@ public function __construct(
$this->featureFlagsRequestTimeout = (int) ($options['feature_flag_request_timeout_ms'] ?? 3000);
$this->featureFlags = [];
$this->groupTypeMapping = [];
$this->cohorts = [];
$this->distinctIdsFeatureFlagsReported = new SizeLimitedHash(SIZE_LIMIT);

// Populate featureflags and grouptypemapping if possible
Expand Down Expand Up @@ -375,7 +382,7 @@ private function computeFlagLocally(
$focusedGroupProperties = $groupProperties[$groupName];
return FeatureFlag::matchFeatureFlagProperties($featureFlag, $groups[$groupName], $focusedGroupProperties);
} else {
return FeatureFlag::matchFeatureFlagProperties($featureFlag, $distinctId, $personProperties);
return FeatureFlag::matchFeatureFlagProperties($featureFlag, $distinctId, $personProperties, $this->cohorts);
}
}

Expand Down Expand Up @@ -413,14 +420,15 @@ public function loadFlags()

$this->featureFlags = $payload['flags'] ?? [];
$this->groupTypeMapping = $payload['group_type_mapping'] ?? [];
$this->cohorts = $payload['cohorts'] ?? [];
}


public function localFlags()
{

return $this->httpClient->sendRequest(
'/api/feature_flag/local_evaluation?token=' . $this->apiKey,
'/api/feature_flag/local_evaluation?send_cohorts&token=' . $this->apiKey,
null,
[
// Send user agent in the form of {library_name}/{library_version} as per RFC 7231.
Expand Down
111 changes: 107 additions & 4 deletions lib/FeatureFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,102 @@ public static function matchProperty($property, $propertyValues)
return false;
}

public static function matchCohort($property, $propertyValues, $cohortProperties)
{
$cohortId = strval($property["value"]);
if (!array_key_exists($cohortId, $cohortProperties)) {
throw new InconclusiveMatchException("can't match cohort without a given cohort property value");
}

$propertyGroup = $cohortProperties[$cohortId];
return FeatureFlag::matchPropertyGroup($propertyGroup, $propertyValues, $cohortProperties);

}

public static function matchPropertyGroup($propertyGroup, $propertyValues, $cohortProperties)
{
if (!$propertyGroup) {
return true;
}

$propertyGroupType = $propertyGroup["type"];
$properties = $propertyGroup["values"];

if (!$properties || count($properties) === 0) {
// empty groups are no-ops, always match
return true;
}

$errorMatchingLocally = false;

if (array_key_exists("values", $properties[0])) {
// a nested property group
foreach ($properties as $prop) {
try {
$matches = FeatureFlag::matchPropertyGroup($prop, $propertyValues, $cohortProperties);
if ($propertyGroupType === 'AND') {
if (!$matches) {
return false;
}
} else {
// OR group
if ($matches) {
return true;
}
}
} catch (InconclusiveMatchException $err) {
$errorMatchingLocally = true;
}
}

if ($errorMatchingLocally) {
throw new InconclusiveMatchException("Can't match cohort without a given cohort property value");
}
// if we get here, all matched in AND case, or none matched in OR case
return $propertyGroupType === 'AND';
} else {
foreach ($properties as $prop) {
try {
$matches = false;
if ($prop["type"] === 'cohort') {
$matches = FeatureFlag::matchCohort($prop, $propertyValues, $cohortProperties);
} else {
$matches = FeatureFlag::matchProperty($prop, $propertyValues);
}

$negation = $prop["negation"] ?? false;

if ($propertyGroupType === 'AND') {
// if negated property, do the inverse
if (!$matches && !$negation) {
return false;
}
if ($matches && $negation) {
return false;
}
} else {
// OR group
if ($matches && !$negation) {
return true;
}
if (!$matches && $negation) {
return true;
}
}
} catch (InconclusiveMatchException $err) {
$errorMatchingLocally = true;
}
}

if ($errorMatchingLocally) {
throw new InconclusiveMatchException("can't match cohort without a given cohort property value");
}

// if we get here, all matched in AND case, or none matched in OR case
return $propertyGroupType === 'AND';
}
}

public static function relativeDateParseForFeatureFlagMatching($value)
{
$regex = "/^-?(?<number>[0-9]+)(?<interval>[a-z])$/";
Expand Down Expand Up @@ -239,7 +335,7 @@ private static function compareFlagConditions($conditionA, $conditionB)
}
}

public static function matchFeatureFlagProperties($flag, $distinctId, $properties)
public static function matchFeatureFlagProperties($flag, $distinctId, $properties, $cohorts = [])
{
$flagConditions = ($flag["filters"] ?? [])["groups"] ?? [];
$isInconclusive = false;
Expand Down Expand Up @@ -275,7 +371,7 @@ function ($conditionA, $conditionB) {
foreach ($flagConditionsWithIndexes as $conditionWithIndex) {
$condition = $conditionWithIndex[0];
try {
if (FeatureFlag::isConditionMatch($flag, $distinctId, $condition, $properties)) {
if (FeatureFlag::isConditionMatch($flag, $distinctId, $condition, $properties, $cohorts)) {
$variantOverride = $condition["variant"] ?? null;
$flagVariants = (($flag["filters"] ?? [])["multivariate"] ?? [])["variants"] ?? [];
$variantKeys = array_map(function ($variant) {
Expand All @@ -300,13 +396,20 @@ function ($conditionA, $conditionB) {
return false;
}

private static function isConditionMatch($featureFlag, $distinctId, $condition, $properties)
private static function isConditionMatch($featureFlag, $distinctId, $condition, $properties, $cohorts)
{
$rolloutPercentage = array_key_exists("rollout_percentage", $condition) ? $condition["rollout_percentage"] : null;

if (count($condition['properties'] ?? []) > 0) {
foreach ($condition['properties'] as $property) {
if (!FeatureFlag::matchProperty($property, $properties)) {
$matches = false;
if ($property['type'] == 'cohort') {
$matches = FeatureFlag::matchCohort($property, $properties, $cohorts);
} else {
$matches = FeatureFlag::matchProperty($property, $properties);
}

if (!$matches) {
return false;
}
}
Expand Down
Loading

0 comments on commit 07e3875

Please sign in to comment.