Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(flags): Locally evaluate all cohorts #63

Merged
merged 5 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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
112 changes: 108 additions & 4 deletions lib/FeatureFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,103 @@ 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)
{
// TODO: check empty case
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 +336,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 +372,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 +397,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
Loading