Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ public function getFeatureFlag(
$personProperties,
$groupProperties
);
} catch (RequiresServerEvaluationException $e) {
$result = null;
} catch (InconclusiveMatchException $e) {
$result = null;
} catch (Exception $e) {
Expand Down Expand Up @@ -387,6 +389,8 @@ public function getAllFlags(
$personProperties,
$groupProperties
);
} catch (RequiresServerEvaluationException $e) {
$fallbackToFlags = true;
} catch (InconclusiveMatchException $e) {
$fallbackToFlags = true;
} catch (Exception $e) {
Expand Down
14 changes: 13 additions & 1 deletion lib/FeatureFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,10 @@ 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");
throw new RequiresServerEvaluationException(
"cohort {$cohortId} not found in local cohorts - " .
"likely a static cohort that requires server evaluation"
);
}

$propertyGroup = $cohortProperties[$cohortId];
Expand Down Expand Up @@ -141,6 +144,9 @@ public static function matchPropertyGroup($propertyGroup, $propertyValues, $coho
return true;
}
}
} catch (RequiresServerEvaluationException $err) {
// Immediately propagate - this condition requires server-side data
throw $err;
} catch (InconclusiveMatchException $err) {
$errorMatchingLocally = true;
}
Expand Down Expand Up @@ -183,6 +189,9 @@ public static function matchPropertyGroup($propertyGroup, $propertyValues, $coho
return true;
}
}
} catch (RequiresServerEvaluationException $err) {
// Immediately propagate - this condition requires server-side data
throw $err;
} catch (InconclusiveMatchException $err) {
// If this is a flag dependency error, preserve the original message
if ($propType === 'flag') {
Expand Down Expand Up @@ -402,6 +411,9 @@ function ($conditionA, $conditionB) {
return FeatureFlag::getMatchingVariant($flag, $distinctId) ?? true;
}
}
} catch (RequiresServerEvaluationException $e) {
// Immediately propagate - this condition requires server-side data
throw $e;
} catch (InconclusiveMatchException $e) {
// If this is a flag dependency error, preserve the original message
if (
Expand Down
14 changes: 14 additions & 0 deletions lib/RequiresServerEvaluationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace PostHog;

use Exception;

class RequiresServerEvaluationException extends Exception
{
public function errorMessage()
{
$errorMsg = 'Error on line ' . $this->getLine() . ' in ' . $this->getFile() . ': <b> Requires Server Evaluation:' . $this->getMessage() . '</b>'; //phpcs:ignore
return $errorMsg;
}
}
91 changes: 91 additions & 0 deletions test/FeatureFlagLocalEvaluationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3840,4 +3840,95 @@ public function testFeatureFlagsWithFlagDependencies(): void
}
$this->assertTrue($threwException, "Expected InconclusiveMatchException was not thrown");
}

public function testFallsBackToAPIWhenFlagHasStaticCohort()
{
$this->http_client = new MockedHttpClient(
host: "app.posthog.com",
flagEndpointResponse: MockedResponses::LOCAL_EVALUATION_WITH_STATIC_COHORT,
flagsEndpointResponse: MockedResponses::FLAGS_WITH_STATIC_COHORT_RESPONSE
);

$this->client = new Client(
self::FAKE_API_KEY,
[
"debug" => true,
],
$this->http_client,
"test"
);

$result = $this->client->getFeatureFlag(
'multi-condition-flag',
'test-user',
[],
['$geoip_country_code' => 'DE']
);

// Should return 'set-1' from API, not 'set-8' from local evaluation
$this->assertEquals('set-1', $result);

$this->checkEmptyErrorLogs();
}

public function testFallsBackToAPIInGetAllFlagsWhenFlagHasStaticCohort()
{
$this->http_client = new MockedHttpClient(
host: "app.posthog.com",
flagEndpointResponse: MockedResponses::LOCAL_EVALUATION_WITH_STATIC_COHORT,
flagsEndpointResponse: MockedResponses::FLAGS_WITH_STATIC_COHORT_RESPONSE
);

$this->client = new Client(
self::FAKE_API_KEY,
[
"debug" => true,
],
$this->http_client,
"test"
);

$result = $this->client->getAllFlags(
'test-user',
[],
['$geoip_country_code' => 'DE']
);

// Should return flags from API
$this->assertEquals([
'multi-condition-flag' => 'set-1'
], $result);

$this->checkEmptyErrorLogs();
}

public function testFallsBackToAPIInGetFeatureFlagPayloadWhenFlagHasStaticCohort()
{
$this->http_client = new MockedHttpClient(
host: "app.posthog.com",
flagEndpointResponse: MockedResponses::LOCAL_EVALUATION_WITH_STATIC_COHORT_FOR_PAYLOAD,
flagsEndpointResponse: MockedResponses::FLAGS_WITH_STATIC_COHORT_PAYLOAD_RESPONSE
);

$this->client = new Client(
self::FAKE_API_KEY,
[
"debug" => true,
],
$this->http_client,
"test"
);

$result = $this->client->getFeatureFlagPayload(
'flag-with-payload',
'test-user'
);

// Should return payload from API, not local evaluation
$this->assertEquals([
'message' => 'from-api'
], $result);

$this->checkEmptyErrorLogs();
}
}
96 changes: 96 additions & 0 deletions test/assests/MockedResponses.php
Original file line number Diff line number Diff line change
Expand Up @@ -1429,4 +1429,100 @@ class MockedResponses
],
]
];

public const LOCAL_EVALUATION_WITH_STATIC_COHORT = [
'flags' => [
[
'id' => 1,
'key' => 'multi-condition-flag',
'filters' => [
'groups' => [
[
'properties' => [
[
'key' => 'id',
'value' => 999,
'type' => 'cohort'
]
],
'rollout_percentage' => 100,
'variant' => 'set-1'
],
[
'properties' => [
[
'key' => '$geoip_country_code',
'operator' => 'exact',
'value' => ['DE'],
'type' => 'person'
]
],
'rollout_percentage' => 100,
'variant' => 'set-8'
]
],
'multivariate' => [
'variants' => [
['key' => 'set-1', 'rollout_percentage' => 50],
['key' => 'set-8', 'rollout_percentage' => 50]
]
],
'payloads' => [
'set-1' => '{"message": "local-payload-1"}',
'set-8' => '{"message": "local-payload-8"}'
]
],
'active' => true,
'is_simple_flag' => false
]
],
'cohorts' => []
];

public const FLAGS_WITH_STATIC_COHORT_RESPONSE = [
'featureFlags' => [
'multi-condition-flag' => 'set-1'
],
'featureFlagPayloads' => [
'multi-condition-flag' => '{"message": "from-api"}'
]
];

public const LOCAL_EVALUATION_WITH_STATIC_COHORT_FOR_PAYLOAD = [
'flags' => [
[
'id' => 2,
'key' => 'flag-with-payload',
'filters' => [
'groups' => [
[
'properties' => [
[
'key' => 'id',
'value' => 999,
'type' => 'cohort'
]
],
'rollout_percentage' => 100
]
],
'payloads' => [
'true' => '{"message": "local-payload"}'
]
],
'active' => true,
'is_simple_flag' => false
]
],
'cohorts' => []
];

public const FLAGS_WITH_STATIC_COHORT_PAYLOAD_RESPONSE = [
'featureFlags' => [
'flag-with-payload' => true
],
'featureFlagPayloads' => [
'flag-with-payload' => '{"message": "from-api"}'
]
];
}