From 07e3875b5a0cb44285fa5d1cf5ff061b58cddee9 Mon Sep 17 00:00:00 2001 From: Neil Kakkar Date: Wed, 13 Mar 2024 13:28:34 +0000 Subject: [PATCH] feat(flags): Locally evaluate all cohorts (#63) --- lib/Client.php | 12 +- lib/FeatureFlag.php | 111 +++++++++++++- test/FeatureFlagTest.php | 241 ++++++++++++++++++++++++++++--- test/PostHogTest.php | 52 +++---- test/assests/MockedResponses.php | 218 ++++++++++++++++++++-------- 5 files changed, 523 insertions(+), 111 deletions(-) diff --git a/lib/Client.php b/lib/Client.php index 2e32fa8..096f14d 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -57,6 +57,12 @@ class Client */ public $groupTypeMapping; + /** + * @var array + */ + public $cohorts; + + /** * @var SizeLimitedHash */ @@ -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 @@ -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); } } @@ -413,6 +420,7 @@ public function loadFlags() $this->featureFlags = $payload['flags'] ?? []; $this->groupTypeMapping = $payload['group_type_mapping'] ?? []; + $this->cohorts = $payload['cohorts'] ?? []; } @@ -420,7 +428,7 @@ 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. diff --git a/lib/FeatureFlag.php b/lib/FeatureFlag.php index f499870..2b12f63 100644 --- a/lib/FeatureFlag.php +++ b/lib/FeatureFlag.php @@ -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 = "/^-?(?[0-9]+)(?[a-z])$/"; @@ -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; @@ -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) { @@ -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; } } diff --git a/test/FeatureFlagTest.php b/test/FeatureFlagTest.php index 5e5fbe2..0e26270 100644 --- a/test/FeatureFlagTest.php +++ b/test/FeatureFlagTest.php @@ -523,7 +523,7 @@ public function testMatchPropertyDateOperators(): void self::assertFalse(FeatureFlag::matchProperty($prop_a, [ "key" => "2022-05-30", ])); - + // is date after // is date after // const property_b = { key: 'key', value: '2022-05-01', operator: 'is_date_after' } @@ -604,7 +604,7 @@ public function testMatchPropertyDateOperators(): void public function testMatchPropertyRelativeDateOperators(): void { ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { - + $prop_a = [ "key" => "key", "value" => "-6h", @@ -794,7 +794,7 @@ public function testMatchPropertyRelativeDateOperators(): void self::assertFalse(FeatureFlag::matchProperty($prop_l, [ "key" => "2022-04-16 00:00:00", ])); - + $prop_m = [ "key" => "key", "value" => "1m", @@ -848,7 +848,7 @@ public function testMatchPropertyWithNones(): void self::assertTrue(FeatureFlag::matchProperty($prop_a, [ "key" => "nul", ])); - + $prop_b = [ "key" => "key", "value" => "null", @@ -949,7 +949,7 @@ public function testRelativeDateParsingOverflow() public function testRelativeDateParsingHours() { - + ClockMock::executeAtFrozenDateTime(new \DateTime('2020-01-01T12:01:20Z'), function () { self::assertEquals(FeatureFlag::relativeDateParseForFeatureFlagMatching('1h'), new \DateTime('2020-01-01T11:01:20Z')); self::assertEquals(FeatureFlag::relativeDateParseForFeatureFlagMatching('2h'), new \DateTime('2020-01-01T10:01:20Z')); @@ -1014,7 +1014,7 @@ public function testRelativeDateParsingMonths() self::assertEquals(FeatureFlag::relativeDateParseForFeatureFlagMatching('1y'), new \DateTime('2019-04-03T00:00:00Z')); self::assertEquals(FeatureFlag::relativeDateParseForFeatureFlagMatching('12m'), FeatureFlag::relativeDateParseForFeatureFlagMatching('1y')); - + }); } @@ -1191,7 +1191,7 @@ public function testGetAllFlagsWithFallback() public function testGetAllFlagsWithFallbackEmptyLocalFlags() { - $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse:[]); + $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse: []); $this->client = new Client( self::FAKE_API_KEY, [ @@ -1210,7 +1210,7 @@ public function testGetAllFlagsWithFallbackEmptyLocalFlags() public function testGetAllFlagsWithNoFallback() { - $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse:MockedResponses::MULTIPLE_FLAGS_LOCAL_EVALUATE_REQUEST); + $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse: MockedResponses::MULTIPLE_FLAGS_LOCAL_EVALUATE_REQUEST); $this->client = new Client( self::FAKE_API_KEY, [ @@ -1265,18 +1265,42 @@ public function testLoadFeatureFlagsWrongKey() public function testSimpleFlag() { - $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse: MockedResponses::LOCAL_EVALUATION_SIMPLE_REQUEST); - $this->client = new Client( - self::FAKE_API_KEY, - [ - "debug" => true, - ], - $this->http_client, - "test" - ); - PostHog::init(null, null, $this->client); + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { - $this->assertTrue(PostHog::getFeatureFlag('simple-flag', 'some-distinct-id')); + $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse: MockedResponses::LOCAL_EVALUATION_SIMPLE_REQUEST); + $this->client = new Client( + self::FAKE_API_KEY, + [ + "debug" => true, + ], + $this->http_client, + "test" + ); + PostHog::init(null, null, $this->client); + + $this->assertTrue(PostHog::getFeatureFlag('simple-flag', 'some-distinct-id')); + + PostHog::flush(); + + $this->assertEquals( + $this->http_client->calls, + array ( + 0 => array ( + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", + "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), + ), + // no decide because local eval, but capture flag event + 1 => array ( + "path" => "/batch/", + 'payload' => '{"batch":[{"properties":{"$feature\/simple-flag":true,"$active_feature_flags":["simple-flag"],"$feature_flag":"simple-flag","$feature_flag_response":true,"$lib":"posthog-php","$lib_version":"3.0.3","$lib_consumer":"LibCurl","$groups":[]},"distinct_id":"some-distinct-id","event":"$feature_flag_called","$groups":[],"library":"posthog-php","library_version":"3.0.3","library_consumer":"LibCurl","groups":[],"timestamp":"2022-05-01T00:00:00+00:00","type":"capture"}],"api_key":"random_key"}', + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array(), + ), + ) + ); + }); } public function testFeatureFlagsDontFallbackToDecideWhenOnlyLocalEvaluationIsTrue() @@ -1356,6 +1380,185 @@ public function testComputingInactiveFlagLocally() "enabled-flag" => true, "disabled-flag" => false ]); + + $this->assertEquals( + $this->http_client->calls, + array( + 0 => array( + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", + "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), + ), + // no decide or capture calls + ) + ); + } + + public function testFeatureFlagsLocalEvaluationForCohorts() + { + $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse: MockedResponses::LOCAL_EVALUATION_WITH_COHORTS_REQUEST); + $this->client = new Client( + self::FAKE_API_KEY, + [ + "debug" => true, + ], + $this->http_client, + "test" + ); + PostHog::init(null, null, $this->client); + + $feature_flag_match = PostHog::getFeatureFlag( + "beta-feature", + "some-distinct-id", + [], + ["region" => "UK"] + ); + + $this->assertEquals($feature_flag_match, false); + $this->assertEquals( + $this->http_client->calls, + array( + 0 => array( + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", + "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), + ), + ) + ); + + PostHog::flush(); + // reset calls + $this->http_client->calls = array(); + + $feature_flag_match = PostHog::getFeatureFlag( + "beta-feature", + "some-distinct-id", + [], + ["region" => "USA", "nation" => "UK"] + ); + + $this->assertEquals($feature_flag_match, true); + $this->assertEquals( + $this->http_client->calls, + // no decide calls + array() + ); + + PostHog::flush(); + + // reset calls + $this->http_client->calls = array(); + + $feature_flag_match = PostHog::getFeatureFlag( + "beta-feature", + "some-distinct-id", + [], + ["region" => "USA", "other" => "thing"] + ); + + $this->assertEquals($feature_flag_match, true); + $this->assertEquals( + $this->http_client->calls, + // no decide calls + array() + ); + } + + public function testFeatureFlagsLocalEvaluationForNegatedCohorts() + { + $this->http_client = new MockedHttpClient(host: "app.posthog.com", flagEndpointResponse: MockedResponses::LOCAL_EVALUATION_FOR_NEGATED_COHORTS_REQUEST); + $this->client = new Client( + self::FAKE_API_KEY, + [ + "debug" => true, + ], + $this->http_client, + "test" + ); + PostHog::init(null, null, $this->client); + + $feature_flag_match = PostHog::getFeatureFlag( + "beta-feature", + "some-distinct-id", + [], + ["region" => "UK"] + ); + + $this->assertEquals($feature_flag_match, false); + $this->assertEquals( + $this->http_client->calls, + array( + 0 => array( + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", + "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), + ), + ) + ); + + PostHog::flush(); + // reset calls + $this->http_client->calls = array(); + + $feature_flag_match = PostHog::getFeatureFlag( + "beta-feature", + "some-distinct-id", + [], + ["region" => "USA", "nation" => "UK"] + ); + + // even though 'other' property is not present, the cohort should still match since it's an OR condition + $this->assertEquals($feature_flag_match, true); + $this->assertEquals( + $this->http_client->calls, + // no decide calls + array() + ); + + PostHog::flush(); + // reset calls + $this->http_client->calls = array(); + + $feature_flag_match = PostHog::getFeatureFlag( + "beta-feature", + "some-distinct-id", + [], + ["region" => "USA", "other" => "thing"] + ); + # since 'other' is negated, we return False. Since 'nation' is not present, we can't tell whether the flag should be true or false, so go to decide + $this->assertEquals($feature_flag_match, 'decide-fallback-value'); + $this->assertEquals( + $this->http_client->calls, + array( + 0 => array( + "path" => "/decide/?v=2", + 'payload' => '{"api_key":"random_key","distinct_id":"some-distinct-id","person_properties":{"distinct_id":"some-distinct-id","region":"USA","other":"thing"}}', + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), + ), + ) + ); + + PostHog::flush(); + // reset calls + $this->http_client->calls = array(); + + $feature_flag_match = PostHog::getFeatureFlag( + "beta-feature", + "some-distinct-id", + [], + ["region" => "USA", "other" => "thing2"] + ); + + $this->assertEquals($feature_flag_match, true); + $this->assertEquals( + $this->http_client->calls, + // no decide calls + array() + ); } public function testComputingFlagWithoutRolloutLocally() diff --git a/test/PostHogTest.php b/test/PostHogTest.php index d523735..d0a5d9d 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -98,7 +98,7 @@ public function testCaptureWithSendFeatureFlagsOption(): void $this->assertTrue( PostHog::capture( - array( + array ( "distinctId" => "john", "event" => "Module PHP Event", "send_feature_flags" => true @@ -109,20 +109,20 @@ public function testCaptureWithSendFeatureFlagsOption(): void $this->assertEquals( $this->http_client->calls, - array( - 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + array ( + 0 => array ( + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), ), - 1 => array( + 1 => array ( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"john"}', self::FAKE_API_KEY), "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), "requestOptions" => array("timeout" => 1234, "shouldRetry" => false), ), - 2 => array( + 2 => array ( "path" => "/batch/", "payload" => '{"batch":[{"event":"Module PHP Event","send_feature_flags":true,"properties":{"$feature\/simpleFlag":true,"$feature\/having_fun":false,"$feature\/enabled-flag":true,"$feature\/disabled-flag":false,"$feature\/multivariate-simple-test":"variant-simple-value","$feature\/simple-test":true,"$feature\/multivariate-test":"variant-value","$feature\/group-flag":"decide-fallback-value","$feature\/complex-flag":"decide-fallback-value","$feature\/beta-feature":"decide-fallback-value","$feature\/beta-feature2":"alakazam","$feature\/feature-1":"decide-fallback-value","$feature\/feature-2":"decide-fallback-value","$feature\/variant-1":"variant-1","$feature\/variant-3":"variant-3","$active_feature_flags":["simpleFlag","enabled-flag","multivariate-simple-test","simple-test","multivariate-test","group-flag","complex-flag","beta-feature","beta-feature2","feature-1","feature-2","variant-1","variant-3"],"$lib":"posthog-php","$lib_version":"3.0.3","$lib_consumer":"LibCurl"},"library":"posthog-php","library_version":"3.0.3","library_consumer":"LibCurl","distinct_id":"john","groups":[],"timestamp":"2022-05-01T00:00:00+00:00","type":"capture"}],"api_key":"random_key"}', "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), @@ -159,7 +159,7 @@ public function testCaptureWithLocalSendFlags(): void ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { $this->assertTrue( PostHog::capture( - array( + array ( "distinctId" => "john", "event" => "Module PHP Event", ) @@ -167,17 +167,17 @@ public function testCaptureWithLocalSendFlags(): void ); PostHog::flush(); - + $this->assertEquals( $this->http_client->calls, - array( - 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + array ( + 0 => array ( + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), ), - 1 => array( + 1 => array ( "path" => "/batch/", "payload" => '{"batch":[{"event":"Module PHP Event","properties":{"$feature\/true-flag":true,"$active_feature_flags":["true-flag"],"$lib":"posthog-php","$lib_version":"3.0.3","$lib_consumer":"LibCurl"},"library":"posthog-php","library_version":"3.0.3","library_consumer":"LibCurl","distinct_id":"john","groups":[],"timestamp":"2022-05-01T00:00:00+00:00","type":"capture"}],"api_key":"random_key"}', "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), @@ -204,10 +204,10 @@ public function testCaptureWithLocalSendFlagsNoOverrides(): void ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { $this->assertTrue( PostHog::capture( - array( + array ( "distinctId" => "john", "event" => "Module PHP Event", - "properties" => array( + "properties" => array ( "\$feature/true-flag" => "random-override" ) ) @@ -215,18 +215,18 @@ public function testCaptureWithLocalSendFlagsNoOverrides(): void ); PostHog::flush(); - + $this->assertEquals( $this->http_client->calls, - array( - 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + array ( + 0 => array ( + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), ), - 1 => array( + 1 => array ( "path" => "/batch/", "payload" => '{"batch":[{"event":"Module PHP Event","properties":{"$feature\/true-flag":"random-override","$active_feature_flags":["true-flag"],"$lib":"posthog-php","$lib_version":"3.0.3","$lib_consumer":"LibCurl"},"library":"posthog-php","library_version":"3.0.3","library_consumer":"LibCurl","distinct_id":"john","groups":[],"timestamp":"2022-05-01T00:00:00+00:00","type":"capture"}],"api_key":"random_key"}', "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), @@ -259,7 +259,7 @@ public function testIsFeatureEnabled() $this->http_client->calls, array( 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), @@ -282,7 +282,7 @@ public function testIsFeatureEnabledGroups() $this->http_client->calls, array( 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), @@ -307,7 +307,7 @@ public function testGetFeatureFlag() $this->http_client->calls, array( 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), @@ -340,7 +340,7 @@ public function testGetFeatureFlagGroups() $this->http_client->calls, array( 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), @@ -424,7 +424,7 @@ public function testTimestamps(): void array( "distinctId" => "user-id", "event" => "integer-timestamp", - "timestamp" => (int)mktime(0, 0, 0, date('n'), 1, date('Y')), + "timestamp" => (int) mktime(0, 0, 0, date('n'), 1, date('Y')), ) ) ); @@ -434,7 +434,7 @@ public function testTimestamps(): void array( "distinctId" => "user-id", "event" => "string-integer-timestamp", - "timestamp" => (string)mktime(0, 0, 0, date('n'), 1, date('Y')), + "timestamp" => (string) mktime(0, 0, 0, date('n'), 1, date('Y')), ) ) ); @@ -510,7 +510,7 @@ public function testDefaultPropertiesGetAddedProperly(): void $this->http_client->calls, array( 0 => array( - "path" => "/api/feature_flag/local_evaluation?token=random_key", + "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), "requestOptions" => array(), diff --git a/test/assests/MockedResponses.php b/test/assests/MockedResponses.php index 11905c1..669a73d 100644 --- a/test/assests/MockedResponses.php +++ b/test/assests/MockedResponses.php @@ -55,11 +55,11 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -87,11 +87,11 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -105,11 +105,11 @@ class MockedResponses "groups" => [ [ "properties" => [ - ], + ], "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -131,10 +131,10 @@ class MockedResponses "groups" => [ [ "properties" => [ - ], + ], ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -156,11 +156,11 @@ class MockedResponses "groups" => [ [ "properties" => [ - ], + ], "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -174,11 +174,11 @@ class MockedResponses "groups" => [ [ "properties" => [ - ], + ], "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => false, "is_simple_flag" => true, @@ -187,6 +187,103 @@ class MockedResponses ], ]; + public const LOCAL_EVALUATION_WITH_COHORTS_REQUEST = [ + 'flags' => [ + [ + 'id' => 2, + 'name' => 'Beta Feature', + 'key' => 'beta-feature', + 'is_simple_flag' => false, + 'active' => true, + 'filters' => [ + 'groups' => [ + [ + 'properties' => [ + [ + 'key' => 'region', + 'operator' => 'exact', + 'value' => ['USA'], + 'type' => 'person', + ], + [ + 'key' => 'id', + 'value' => 98, + 'operator' => null, + 'type' => 'cohort', + ], + ], + 'rollout_percentage' => 100, + ], + ], + ], + ] + ], + 'cohorts' => [ + '98' => [ + 'type' => 'OR', + 'values' => [ + ['key' => 'id', 'value' => 1, 'type' => 'cohort'], + ['key' => 'nation', 'operator' => 'exact', 'value' => ['UK'], 'type' => 'person'], + ], + ], + '1' => [ + 'type' => 'AND', + 'values' => [ + ['key' => 'other', 'operator' => 'exact', 'value' => ['thing'], 'type' => 'person'], + ], + ], + ] + + ]; + + public const LOCAL_EVALUATION_FOR_NEGATED_COHORTS_REQUEST = [ + 'flags' => [ + [ + 'id' => 2, + 'name' => 'Beta Feature', + 'key' => 'beta-feature', + 'is_simple_flag' => false, + 'active' => true, + 'filters' => [ + 'groups' => [ + [ + 'properties' => [ + [ + 'key' => 'region', + 'operator' => 'exact', + 'value' => ['USA'], + 'type' => 'person', + ], + [ + 'key' => 'id', + 'value' => 98, + 'operator' => null, + 'type' => 'cohort', + ], + ], + 'rollout_percentage' => 100, + ], + ], + ], + ] + ], + 'cohorts' => [ + '98' => [ + 'type' => 'OR', + 'values' => [ + ['key' => 'id', 'value' => 1, 'type' => 'cohort'], + ['key' => 'nation', 'operator' => 'exact', 'value' => ['UK'], 'type' => 'person'], + ], + ], + '1' => [ + 'type' => 'AND', + 'values' => [ + ['key' => 'other', 'operator' => 'exact', 'value' => ['thing'], 'type' => 'person', 'negation' => true], + ], + ], + ], + ]; + public const LOCAL_EVALUATION_GROUP_PROPERTIES_REQUEST = [ 'count' => 1, 'next' => null, @@ -208,11 +305,11 @@ class MockedResponses "operator" => "exact", "type" => "group" ] - ], + ], "rollout_percentage" => 35 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -250,32 +347,33 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100 + ], + [ + "properties" => [ + [ + "key" => "email", + "value" => ["a@b.com"], + "operator" => "exact", + "type" => "person" + ] ], - [ - "properties" => [ - [ - "key" => "email", - "value" => ["a@b.com"], - "operator" => "exact", - "type" => "person" - ] - ], - "rollout_percentage" => 35 - ],[ - "properties" => [ - [ - "key" => "doesnt_matter", - "value" => ["1", "2"], - "operator" => "exact", - "type" => "person" - ] - ], - "rollout_percentage" => 0 + "rollout_percentage" => 35 + ], + [ + "properties" => [ + [ + "key" => "doesnt_matter", + "value" => ["1", "2"], + "operator" => "exact", + "type" => "person" ] + ], + "rollout_percentage" => 0 + ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -303,7 +401,7 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100, "variant" => "second-variant" ], @@ -359,7 +457,7 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100, "variant" => "second-variant" ], @@ -371,7 +469,7 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100, "variant" => "first-variant" ], @@ -427,7 +525,7 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100, "variant" => "second???" ], @@ -486,7 +584,7 @@ class MockedResponses "operator" => "exact", "type" => "person" ] - ], + ], "rollout_percentage" => 100, "variant" => "second-variant" ], @@ -540,7 +638,7 @@ class MockedResponses "rollout_percentage" => 100 ] ] - ], + ], "ensure_experience_continuity" => true, "deleted" => false, "active" => true, @@ -569,17 +667,17 @@ class MockedResponses "operator" => null, "type" => "cohort" ] - ], - "rollout_percentage" => 100 - ], - ], ], + "rollout_percentage" => 100 + ], + ], + ], "deleted" => false, "active" => true, "is_simple_flag" => false, "rollout_percentage" => null - ], + ], [ "id" => 2, "name" => "feature 2", @@ -594,11 +692,11 @@ class MockedResponses "operator" => null, "type" => "person" ] - ], - "rollout_percentage" => 100 - ], - ], ], + "rollout_percentage" => 100 + ], + ], + ], "deleted" => false, "active" => true, "is_simple_flag" => false, @@ -624,7 +722,7 @@ class MockedResponses "rollout_percentage" => 0 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -648,7 +746,7 @@ class MockedResponses "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -673,7 +771,7 @@ class MockedResponses "rollout_percentage" => 45 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -727,7 +825,7 @@ class MockedResponses ] ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => true, @@ -752,7 +850,7 @@ class MockedResponses "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => false, @@ -769,7 +867,7 @@ class MockedResponses "rollout_percentage" => 0 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => false, @@ -794,7 +892,7 @@ class MockedResponses "rollout_percentage" => 100 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => false, @@ -811,7 +909,7 @@ class MockedResponses "rollout_percentage" => 0 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => false, @@ -835,7 +933,7 @@ class MockedResponses "rollout_percentage" => 0 ] ] - ], + ], "deleted" => false, "active" => true, "is_simple_flag" => false,