diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a09d76c8..3309e3b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ All notable changes to the LaunchDarkly PHP SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [3.4.0] - 2018-09-04 +### Added: +- The new `LDClient` method `variationDetail` allows you to evaluate a feature flag (using the same parameters as you would for `variation`) and receive more information about how the value was calculated. This information is returned in an object that contains both the result value and a "reason" object which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error. + +### Fixed: +- When evaluating a prerequisite feature flag, the analytics event for the evaluation did not include the result value if the prerequisite flag was off. + ## [3.3.0] - 2018-08-27 ### Added: - The new `LDClient` method `allFlagsState()` should be used instead of `allFlags()` if you are passing flag data to the front end for use with the JavaScript SDK. It preserves some flag metadata that the front end requires in order to send analytics events correctly. Versions 2.5.0 and above of the JavaScript SDK are able to use this metadata, but the output of `allFlagsState()` will still work with older versions. diff --git a/VERSION b/VERSION index 15a279981..18091983f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.0 +3.4.0 diff --git a/src/LaunchDarkly/EvalResult.php b/src/LaunchDarkly/EvalResult.php index f692abac1..6b4b4ab21 100644 --- a/src/LaunchDarkly/EvalResult.php +++ b/src/LaunchDarkly/EvalResult.php @@ -4,37 +4,28 @@ class EvalResult { - private $_variation = null; - private $_value = null; + /** @var EvaluationDetail */ + private $_detail = null; /** @var array */ private $_prerequisiteEvents = []; /** * EvalResult constructor. - * @param null $value + * @param EvaluationDetail $detail * @param array $prerequisiteEvents */ - public function __construct($variation, $value, array $prerequisiteEvents) + public function __construct($detail, array $prerequisiteEvents) { - $this->_variation = $variation; - $this->_value = $value; + $this->_detail = $detail; $this->_prerequisiteEvents = $prerequisiteEvents; } /** - * @return int | null + * @return EvaluationDetail */ - public function getVariation() + public function getDetail() { - return $this->_variation; - } - - /** - * @return null - */ - public function getValue() - { - return $this->_value; + return $this->_detail; } /** diff --git a/src/LaunchDarkly/EvaluationDetail.php b/src/LaunchDarkly/EvaluationDetail.php new file mode 100644 index 000000000..b2ffb970b --- /dev/null +++ b/src/LaunchDarkly/EvaluationDetail.php @@ -0,0 +1,68 @@ +_value = $value; + $this->_variationIndex = $variationIndex; + $this->_reason = $reason; + } + + /** + * Returns the value of the flag variation for the user. + * + * @return mixed + */ + public function getValue() + { + return $this->_value; + } + + /** + * Returns the index of the flag variation for the user, e.g. 0 for the first variation - + * or null if it was the default value. + * + * @return int | null + */ + public function getVariationIndex() + { + return $this->_variationIndex; + } + + /** + * Returns information about how the flag value was calculated. + * + * @return EvaluationReason + */ + public function getReason() + { + return $this->_reason; + } + + /** + * Returns true if the flag evaluated to the default value, rather than one of its variations. + * + * @return bool + */ + public function isDefaultValue() + { + return ($this->_variationIndex === null); + } +} diff --git a/src/LaunchDarkly/EvaluationReason.php b/src/LaunchDarkly/EvaluationReason.php new file mode 100644 index 000000000..92e2b3403 --- /dev/null +++ b/src/LaunchDarkly/EvaluationReason.php @@ -0,0 +1,222 @@ +_kind = $kind; + $this->_errorKind = $errorKind; + $this->_ruleIndex = $ruleIndex; + $this->_ruleId = $ruleId; + $this->_prerequisiteKey = $prerequisiteKey; + } + + /** + * Returns a constant indicating the general category of the reason, such as OFF. + * @return string + */ + public function getKind() + { + return $this->_kind; + } + + /** + * Returns a constant indicating the nature of the error, if getKind() is OFF. Otherwise + * returns null. + * @return string|null + */ + public function getErrorKind() + { + return $this->_errorKind; + } + + /** + * Returns the positional index of the rule that was matched (0 for the first), if getKind() + * is RULE_MATCH. Otherwise returns null. + * @return int|null + */ + public function getRuleIndex() + { + return $this->_ruleIndex; + } + + /** + * Returns the unique identifier of the rule that was matched, if getKind() is RULE_MATCH. + * Otherwise returns null. + * @return string|null + */ + public function getRuleId() + { + return $this->_ruleId; + } + + /** + * Returns the key of the prerequisite feature flag that failed, if getKind() is + * PREREQUISITE_FAILED. Otherwise returns null. + * @return string|null + */ + public function getPrerequisiteKey() + { + return $this->_prerequisiteKey; + } + + /** + * Returns a simple string representation of this object. + */ + public function __toString() + { + switch ($this->_kind) { + case self::RULE_MATCH: + return $this->_kind . '(' . $this->_ruleIndex . ',' . $this->_ruleId . ')'; + case self::PREREQUISITE_FAILED: + return $this->_kind . '(' . $this->_prerequisiteKey . ')'; + case self::ERROR: + return $this->_kind . '(' . $this->_errorKind . ')'; + default: + return $this->_kind; + } + } + + /** + * Returns a JSON representation of this object. This method is used automatically + * if you call json_encode(). + */ + public function jsonSerialize() + { + $ret = array('kind' => $this->_kind); + if ($this->_errorKind !== null) { + $ret['errorKind'] = $this->_errorKind; + } + if ($this->_ruleIndex !== null) { + $ret['ruleIndex'] = $this->_ruleIndex; + } + if ($this->_ruleId !== null) { + $ret['ruleId'] = $this->_ruleId; + } + if ($this->_prerequisiteKey !== null) { + $ret['prerequisiteKey'] = $this->_prerequisiteKey; + } + return $ret; + } +} diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 893d855f0..46123f8a4 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -102,121 +102,136 @@ public function isOn() } /** - * @param $user LDUser - * @param $featureRequester FeatureRequester - * @return EvalResult|null + * @param LDUser $user + * @param FeatureRequester $featureRequester + * @param bool $includeReasonsInEvents + * @return EvalResult */ - public function evaluate($user, $featureRequester) + public function evaluate($user, $featureRequester, $includeReasonsInEvents = false) { $prereqEvents = array(); - if (is_null($user) || is_null($user->getKey())) { - return new EvalResult(null, $prereqEvents); + $detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $includeReasonsInEvents); + return new EvalResult($detail, $prereqEvents); + } + + /** + * @param LDUser $user + * @param FeatureRequester $featureRequester + * @param array $events + * @param bool $includeReasonsInEvents + * @return EvaluationDetail + */ + private function evaluateInternal($user, $featureRequester, &$events, $includeReasonsInEvents) + { + if (!$this->isOn()) { + return $this->getOffValue(EvaluationReason::off()); + } + + $prereqFailureReason = $this->checkPrerequisites($user, $featureRequester, $events, $includeReasonsInEvents); + if ($prereqFailureReason !== null) { + return $this->getOffValue($prereqFailureReason); } - if ($this->isOn()) { - $result = $this->_evaluate($user, $featureRequester, $prereqEvents); - if ($result !== null) { - return $result; + + // Check to see if targets match + if ($this->_targets != null) { + foreach ($this->_targets as $target) { + foreach ($target->getValues() as $value) { + if ($value === $user->getKey()) { + return $this->getVariation($target->getVariation(), EvaluationReason::targetMatch()); + } + } + } + } + // Now walk through the rules and see if any match + if ($this->_rules != null) { + foreach ($this->_rules as $i => $rule) { + if ($rule->matchesUser($user, $featureRequester)) { + return $this->getValueForVariationOrRollout($rule, $user, + EvaluationReason::ruleMatch($i, $rule->getId())); + } } } - $offVariationValue = $this->getOffVariationValue(); - return new EvalResult($this->_offVariation, $offVariationValue, $prereqEvents); + return $this->getValueForVariationOrRollout($this->_fallthrough, $user, EvaluationReason::fallthrough()); } /** - * @param $user LDUser - * @param $featureRequester FeatureRequester - * @param $events - * @return EvalResult|null + * @param LDUser $user + * @param FeatureRequester $featureRequester + * @param array $events + * @param bool $includeReasonsInEvents + * @return EvaluationReason|null */ - private function _evaluate($user, $featureRequester, &$events) + private function checkPrerequisites($user, $featureRequester, &$events, $includeReasonsInEvents) { - $prereqOk = true; if ($this->_prerequisites != null) { foreach ($this->_prerequisites as $prereq) { + $prereqOk = true; try { $prereqEvalResult = null; $prereqFeatureFlag = $featureRequester->getFeature($prereq->getKey()); if ($prereqFeatureFlag == null) { - return null; - } elseif ($prereqFeatureFlag->isOn()) { - $prereqEvalResult = $prereqFeatureFlag->_evaluate($user, $featureRequester, $events); + $prereqOk = false; + } else { + $prereqEvalResult = $prereqFeatureFlag->evaluateInternal($user, $featureRequester, $events, $includeReasonsInEvents); $variation = $prereq->getVariation(); - if ($prereqEvalResult === null || $variation === null || $prereqEvalResult->getVariation() !== $variation) { + if (!$prereqFeatureFlag->isOn() || $prereqEvalResult->getVariationIndex() !== $variation) { $prereqOk = false; } - } else { - $prereqOk = false; + array_push($events, Util::newFeatureRequestEvent($prereq->getKey(), $user, + $prereqEvalResult->getVariationIndex(), $prereqEvalResult->getValue(), + null, $prereqFeatureFlag->getVersion(), $this->_key, + ($includeReasonsInEvents && $prereqEvalResult) ? $prereqEvalResult->getReason() : null + )); } - array_push($events, Util::newFeatureRequestEvent($prereqFeatureFlag->getKey(), $user, - $prereqEvalResult === null ? null : $prereqEvalResult->getVariation(), - $prereqEvalResult === null ? null : $prereqEvalResult->getValue(), - null, $prereqFeatureFlag->getVersion(), $this->_key)); } catch (EvaluationException $e) { $prereqOk = false; } + if (!$prereqOk) { + return EvaluationReason::prerequisiteFailed($prereq->getKey()); + } } } - if ($prereqOk) { - $variation = $this->evaluateIndex($user, $featureRequester); - $value = $this->getVariation($variation); - return new EvalResult($variation, $value, $events); - } return null; } /** - * @param $user LDUser - * @return int|null + * @param int $index + * @param EvaluationReason $reason + * @return EvaluationDetail */ - private function evaluateIndex($user, $featureRequester) + private function getVariation($index, $reason) { - // Check to see if targets match - if ($this->_targets != null) { - foreach ($this->_targets as $target) { - foreach ($target->getValues() as $value) { - if ($value === $user->getKey()) { - return $target->getVariation(); - } - } - } + if ($index < 0 || $index >= count($this->_variations)) { + return new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); } - // Now walk through the rules and see if any match - if ($this->_rules != null) { - foreach ($this->_rules as $rule) { - if ($rule->matchesUser($user, $featureRequester)) { - return $rule->variationIndexForUser($user, $this->_key, $this->_salt); - } - } - } - // Walk through the fallthrough and see if it matches - return $this->_fallthrough->variationIndexForUser($user, $this->_key, $this->_salt); + return new EvaluationDetail($this->_variations[$index], $index, $reason); } - private function getVariation($index) + /** + * @param EvaluationReason reason + * @return EvaluationDetail + */ + private function getOffValue($reason) { - // If the supplied index is null, then rules didn't match, and we want to return - // the off variation - if (!isset($index)) { - return null; - } - // If the index doesn't refer to a valid variation, that's an unexpected exception and we will - // return the default variation - if ($index >= count($this->_variations)) { - throw new EvaluationException("Invalid Index"); - } else { - return $this->_variations[$index]; + if ($this->_offVariation === null) { + return new EvaluationDetail(null, null, $reason); } + return $this->getVariation($this->_offVariation, $reason); } - - public function getOffVariationValue() + + /** + * @param VariationOrRollout $r + * @param LDUser $user + * @param EvaluationReason $reason + * @return EvaluationDetail + */ + private function getValueForVariationOrRollout($r, $user, $reason) { - if ($this->_offVariation === null) { - return null; - } - if ($this->_offVariation >= count($this->_variations)) { - throw new EvaluationException("Invalid offVariation index"); + $index = $r->variationIndexForUser($user, $this->_key, $this->_salt); + if ($index === null) { + return new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); } - return $this->_variations[$this->_offVariation]; + return $this->getVariation($index, $reason); } /** diff --git a/src/LaunchDarkly/FeatureFlagsState.php b/src/LaunchDarkly/FeatureFlagsState.php index 87c84d2b4..3c11338ef 100644 --- a/src/LaunchDarkly/FeatureFlagsState.php +++ b/src/LaunchDarkly/FeatureFlagsState.php @@ -28,15 +28,18 @@ public function __construct($valid, $flagValues = array(), $flagMetadata = array /** * Used internally to build the state map. */ - public function addFlag($flag, $evalResult) + public function addFlag($flag, $detail, $withReason = false) { - $this->_flagValues[$flag->getKey()] = $evalResult->getValue(); + $this->_flagValues[$flag->getKey()] = $detail->getValue(); $meta = array(); - if (!is_null($evalResult->getVariation())) { - $meta['variation'] = $evalResult->getVariation(); + if (!is_null($detail->getVariationIndex())) { + $meta['variation'] = $detail->getVariationIndex(); } $meta['version'] = $flag->getVersion(); $meta['trackEvents'] = $flag->isTrackEvents(); + if ($withReason) { + $meta['reason'] = $detail->getReason(); + } if ($flag->getDebugEventsUntilDate()) { $meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate(); } @@ -55,7 +58,7 @@ public function isValid() /** * Returns the value of an individual feature flag at the time the state was recorded. - * @param $key string + * @param $key string the feature flag key * @return mixed the flag's value; null if the flag returned the default value, or if there was no such flag */ public function getFlagValue($key) @@ -63,6 +66,22 @@ public function getFlagValue($key) return isset($this->_flagValues[$key]) ? $this->_flagValues[$key] : null; } + /** + * Returns the evaluation reason for an individual feature flag (as returned by LDClient.variationDetail()) + * at the time the state was recorded. + * @param $key string the feature flag key + * @return EvaluationReason|null the evaluation reason; null if reasons were not recorded, or if there + * was no such flag + */ + public function getFlagReason($key) + { + if (isset($this->_flagMetadata[$key])) { + $meta = $this->_flagMetadata[$key]; + return isset($meta['reason']) ? $meta['reason'] : null; + } + return null; + } + /** * Returns an associative array of flag keys to flag values. If a flag would have evaluated to the default * value, its value will be null. @@ -88,7 +107,15 @@ public function toValuesMap() public function jsonSerialize() { $ret = array_replace([], $this->_flagValues); - $ret['$flagsState'] = $this->_flagMetadata; + $metaMap = array(); + foreach ($this->_flagMetadata as $key => $meta) { + $meta = array_replace([], $meta); + if (isset($meta['reason'])) { + $meta['reason'] = $meta['reason']->jsonSerialize(); + } + $metaMap[$key] = $meta; + } + $ret['$flagsState'] = $metaMap; $ret['$valid'] = $this->_valid; return $ret; } diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 75b12a142..95c63e1cd 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -12,7 +12,7 @@ class LDClient { const DEFAULT_BASE_URI = 'https://app.launchdarkly.com'; const DEFAULT_EVENTS_URI = 'https://events.launchdarkly.com'; - const VERSION = '3.3.0'; + const VERSION = '3.4.0'; /** @var string */ protected $_sdkKey; @@ -133,11 +133,53 @@ private function getFeatureRequester($sdkKey, array $options) * @return mixed The result of the Feature Flag evaluation, or $default if any errors occurred. */ public function variation($key, $user, $default = false) + { + $detail = $this->variationDetailInternal($key, $user, $default, false); + return $detail->getValue(); + } + + /** + * Calculates the value of a feature flag, and returns an object that describes the way the + * value was determined. The "reason" property in the result will also be included in + * analytics events, if you are capturing detailed event data for this flag. + * + * @param string $key The unique key for the feature flag + * @param LDUser $user The end user requesting the flag + * @param mixed $default The default value of the flag + * + * @return EvaluationDetail An EvaluationDetail object that includes the feature flag value + * and evaluation reason. + */ + public function variationDetail($key, $user, $default = false) + { + return $this->variationDetailInternal($key, $user, $default, true); + } + + /** + * @param string $key + * @param LDUser $user + * @param mixed $default + * @param bool $includeReasonsInEvents + */ + private function variationDetailInternal($key, $user, $default, $includeReasonsInEvents) { $default = $this->_get_default($key, $default); + $errorResult = function ($errorKind) use ($key, $default) { + return new EvaluationDetail($default, null, EvaluationReason::error($errorKind)); + }; + $sendEvent = function ($detail, $flag) use ($key, $user, $default, $includeReasonsInEvents) { + if ($this->isOffline() || !$this->_send_events) { + return; + } + $event = Util::newFeatureRequestEvent($key, $user, $detail->getVariationIndex(), $detail->getValue(), + $default, $flag ? $flag->getVersion() : null, null, + $includeReasonsInEvents ? $detail->getReason() : null); + $this->_eventProcessor->enqueue($event); + }; + if ($this->_offline) { - return $default; + return $errorResult(EvaluationReason::CLIENT_NOT_READY_ERROR); } try { @@ -148,43 +190,44 @@ public function variation($key, $user, $default = false) $flag = $this->_featureRequester->getFeature($key); } catch (UnrecoverableHTTPStatusException $e) { $this->handleUnrecoverableError(); - return $default; + return $errorResult(EvaluationReason::EXCEPTION_ERROR); } if (is_null($flag)) { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default); - return $default; + $result = $errorResult(EvaluationReason::FLAG_NOT_FOUND_ERROR); + $sendEvent($result, null); + return $result; } if (is_null($user) || is_null($user->getKey())) { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default, $flag->getVersion()); + $result = $errorResult(EvaluationReason::USER_NOT_SPECIFIED_ERROR); + $sendEvent($result, $flag); $this->_logger->warning("Variation called with null user or null user key! Returning default value"); - return $default; + return $result; } - $evalResult = $flag->evaluate($user, $this->_featureRequester); + $evalResult = $flag->evaluate($user, $this->_featureRequester, $includeReasonsInEvents); if (!$this->isOffline() && $this->_send_events) { foreach ($evalResult->getPrerequisiteEvents() as $e) { $this->_eventProcessor->enqueue($e); } } - if ($evalResult !== null && $evalResult->getValue() !== null) { - $this->_sendFlagRequestEvent($key, $user, $evalResult->getVariation(), $evalResult->getValue(), $default, $flag->getVersion()); - return $evalResult->getValue(); - } else { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default, $flag->getVersion()); - return $default; + $detail = $evalResult->getDetail(); + if ($detail->isDefaultValue()) { + $detail = new EvaluationDetail($default, null, $detail->getReason()); } + $sendEvent($detail, $flag); + return $detail; } catch (\Exception $e) { $this->_logger->error("Caught $e"); + $result = $errorResult(EvaluationReason::EXCEPTION_ERROR); + try { + $sendEvent($result, null); + } catch (\Exception $e) { + $this->_logger->error("Caught $e"); + } + return $result; } - try { - $this->_sendFlagRequestEvent($key, $user, null, $default, $default); - } catch (\Exception $e) { - $this->_logger->error("Caught $e"); - } - return $default; } - /** @deprecated Use variation() instead. * @param $key * @param $user @@ -284,7 +327,8 @@ public function allFlags($user) * @param $user LDUser the end user requesting the feature flags * @param $options array optional properties affecting how the state is computed; set * 'clientSideOnly' => true to specify that only flags marked for client-side use - * should be included (by default, all flags are included) + * should be included (by default, all flags are included); set 'withReasons' => true + * to include evaluation reasons (see variationDetail) * @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid()) */ public function allFlagsState($user, $options = array()) @@ -308,12 +352,13 @@ public function allFlagsState($user, $options = array()) $state = new FeatureFlagsState(true); $clientOnly = isset($options['clientSideOnly']) && $options['clientSideOnly']; + $withReasons = isset($options['withReasons']) && $options['withReasons']; foreach ($flags as $key => $flag) { if ($clientOnly && !$flag->isClientSide()) { continue; } $result = $flag->evaluate($user, $this->_featureRequester); - $state->addFlag($flag, $result); + $state->addFlag($flag, $result->getDetail(), $withReasons); } return $state; } @@ -343,23 +388,6 @@ public function flush() } } - /** - * @param $key string - * @param $user LDUser - * @param $variation int | null - * @param $value mixed - * @param $default - * @param $version int | null - * @param string | null $prereqOf - */ - protected function _sendFlagRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null) - { - if ($this->isOffline() || !$this->_send_events) { - return; - } - $this->_eventProcessor->enqueue(Util::newFeatureRequestEvent($key, $user, $variation, $value, $default, $version, $prereqOf)); - } - protected function _get_default($key, $default) { if (array_key_exists($key, $this->_defaults)) { diff --git a/src/LaunchDarkly/Rule.php b/src/LaunchDarkly/Rule.php index daf9c6e11..39d1a9622 100644 --- a/src/LaunchDarkly/Rule.php +++ b/src/LaunchDarkly/Rule.php @@ -4,12 +4,15 @@ class Rule extends VariationOrRollout { + /** @var string */ + private $_id = null; /** @var Clause[] */ private $_clauses = array(); - protected function __construct($variation, $rollout, array $clauses) + protected function __construct($variation, $rollout, $id, array $clauses) { parent::__construct($variation, $rollout); + $this->_id = $id; $this->_clauses = $clauses; } @@ -19,6 +22,7 @@ public static function getDecoder() return new Rule( isset($v['variation']) ? $v['variation'] : null, isset($v['rollout']) ? call_user_func(Rollout::getDecoder(), $v['rollout']) : null, + isset($v['id']) ? $v['id'] : null, array_map(Clause::getDecoder(), $v['clauses'])); }; } @@ -37,6 +41,14 @@ public function matchesUser($user, $featureRequester) return true; } + /** + * @return string + */ + public function getId() + { + return $this->_id; + } + /** * @return Clause[] */ diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php index 1fbf79010..e9afd00af 100644 --- a/src/LaunchDarkly/Util.php +++ b/src/LaunchDarkly/Util.php @@ -37,7 +37,7 @@ public static function currentTimeUnixMillis() * @param null $prereqOf string | null * @return array */ - public static function newFeatureRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null) + public static function newFeatureRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null, $reason = null) { $event = array(); $event['user'] = $user; @@ -49,6 +49,9 @@ public static function newFeatureRequestEvent($key, $user, $variation, $value, $ $event['default'] = $default; $event['version'] = $version; $event['prereqOf'] = $prereqOf; + if ($reason !== null) { + $event['reason'] = $reason->jsonSerialize(); + } return $event; } diff --git a/tests/EvaluationReasonTest.php b/tests/EvaluationReasonTest.php new file mode 100644 index 000000000..759dc3ece --- /dev/null +++ b/tests/EvaluationReasonTest.php @@ -0,0 +1,57 @@ +assertEquals(array('kind' => 'OFF'), json_decode($json, true)); + $this->assertEquals('OFF', (string)$reason); + } + + public function testFallthroughReasonSerialization() + { + $reason = EvaluationReason::fallthrough(); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'FALLTHROUGH'), json_decode($json, true)); + $this->assertEquals('FALLTHROUGH', (string)$reason); + } + + public function testTargetMatchReasonSerialization() + { + $reason = EvaluationReason::targetMatch(); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'TARGET_MATCH'), json_decode($json, true)); + $this->assertEquals('TARGET_MATCH', (string)$reason); + } + + public function testRuleMatchReasonSerialization() + { + $reason = EvaluationReason::ruleMatch(0, 'id'); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'), + json_decode($json, true)); + $this->assertEquals('RULE_MATCH(0,id)', (string)$reason); + } + + public function testPrerequisiteFailedReasonSerialization() + { + $reason = EvaluationReason::prerequisiteFailed('key'); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'PREREQUISITE_FAILED', 'prerequisiteKey' => 'key'), + json_decode($json, true)); + $this->assertEquals('PREREQUISITE_FAILED(key)', (string)$reason); + } + + public function testErrorReasonSerialization() + { + $reason = EvaluationReason::error(EvaluationReason::EXCEPTION_ERROR); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'ERROR', 'errorKind' => 'EXCEPTION'), json_decode($json, true)); + $this->assertEquals('ERROR(EXCEPTION)', (string)$reason); + } +} diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index e73531b76..f0f09f02e 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -1,7 +1,10 @@ '' ); $flag = FeatureFlag::decode($flagJson); - $ub = new LDUserBuilder('x'); - $user = $ub->build(); - $result = $flag->evaluate($user, null); - self::assertEquals(1, $result->getVariation()); - self::assertEquals('off', $result->getValue()); + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail('off', 1, EvaluationReason::off()); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } @@ -232,12 +233,56 @@ public function testFlagReturnsNullIfFlagIsOffAndOffVariationIsUnspecified() 'salt' => '' ); $flag = FeatureFlag::decode($flagJson); - $ub = new LDUserBuilder('x'); - $user = $ub->build(); - $result = $flag->evaluate($user, null); - self::assertNull($result->getVariation()); - self::assertNull($result->getValue()); + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail(null, null, EvaluationReason::off()); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfOffVariationIsTooHigh() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 999, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfOffVariationIsNegative() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => -1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + + $result = $flag->evaluate(new LDUser('user'), null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } @@ -264,11 +309,64 @@ public function testFlagReturnsOffVariationIfPrerequisiteIsNotFound() $requester = new MockFeatureRequesterForFeature(); $result = $flag->evaluate($user, $requester); - self::assertEquals(1, $result->getVariation()); - self::assertEquals('off', $result->getValue()); + $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } + public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsOff() + { + $flag0Json = array( + 'key' => 'feature0', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array( + array('key' => 'feature1', 'variation' => 1) + ), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '' + ); + $flag1Json = array( + 'key' => 'feature1', + 'version' => 2, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + // note that even though it returns the desired variation, it is still off and therefore not a match + 'fallthrough' => array('variation' => 0), + 'variations' => array('nogo', 'go'), + 'salt' => '' + ); + $flag0 = FeatureFlag::decode($flag0Json); + $flag1 = FeatureFlag::decode($flag1Json); + $ub = new LDUserBuilder('x'); + $user = $ub->build(); + $requester = new MockFeatureRequesterForFeature(); + $requester->key = $flag1->getKey(); + $requester->val = $flag1; + + $result = $flag0->evaluate($user, $requester); + $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); + self::assertEquals($detail, $result->getDetail()); + + $events = $result->getPrerequisiteEvents(); + self::assertEquals(1, count($events)); + $event = $events[0]; + self::assertEquals('feature', $event['kind']); + self::assertEquals($flag1->getKey(), $event['key']); + self::assertEquals('go', $event['value']); + self::assertEquals($flag1->getVersion(), $event['version']); + self::assertEquals($flag0->getKey(), $event['prereqOf']); + } + public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() { $flag0Json = array( @@ -308,8 +406,8 @@ public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() $requester->val = $flag1; $result = $flag0->evaluate($user, $requester); - self::assertEquals(1, $result->getVariation()); - self::assertEquals('off', $result->getValue()); + $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); + self::assertEquals($detail, $result->getDetail()); $events = $result->getPrerequisiteEvents(); self::assertEquals(1, count($events)); @@ -360,8 +458,8 @@ public function testFlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAn $requester->val = $flag1; $result = $flag0->evaluate($user, $requester); - self::assertEquals(0, $result->getVariation()); - self::assertEquals('fall', $result->getValue()); + $detail = new EvaluationDetail('fall', 0, EvaluationReason::fallthrough()); + self::assertEquals($detail, $result->getDetail()); $events = $result->getPrerequisiteEvents(); self::assertEquals(1, count($events)); @@ -395,12 +493,12 @@ public function testFlagMatchesUserFromTargets() $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(2, $result->getVariation()); - self::assertEquals('on', $result->getValue()); + $detail = new EvaluationDetail('on', 2, EvaluationReason::targetMatch()); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } - public function testFlagMatchesUserFromRules() + private function makeBooleanFlagWithRules(array $rules) { $flagJson = array( 'key' => 'feature', @@ -409,31 +507,111 @@ public function testFlagMatchesUserFromRules() 'on' => true, 'targets' => array(), 'prerequisites' => array(), - 'rules' => array( - array( - 'clauses' => array( - array( - 'attribute' => 'key', - 'op' => 'in', - 'values' => array('userkey'), - 'negate' => false - ) - ), - 'variation' => 2 - ) - ), - 'offVariation' => 1, + 'rules' => $rules, + 'offVariation' => 0, 'fallthrough' => array('variation' => 0), - 'variations' => array('fall', 'off', 'on'), + 'variations' => array(false, true), 'salt' => '' ); - $flag = FeatureFlag::decode($flagJson); + return FeatureFlag::decode($flagJson); + } + + public function testFlagMatchesUserFromRules() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'variation' => 1 + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, 'ruleid')); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleVariationIsTooHigh() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'variation' => 999 + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleVariationIsNegative() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'variation' => -1 + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleHasNoVariationOrRollout() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ) + ) + )); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testFlagReturnsErrorIfRuleHasRolloutWithNoVariations() + { + $flag = $this->makeBooleanFlagWithRules(array( + array( + 'id' => 'ruleid', + 'clauses' => array( + array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) + ), + 'rollout' => array('variations' => array()) + ) + )); $ub = new LDUserBuilder('userkey'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(2, $result->getVariation()); - self::assertEquals('on', $result->getValue()); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } @@ -517,7 +695,7 @@ public function testSegmentMatchClauseRetrievesSegmentFromStore() $result = $feature->evaluate($user, $requester); - self::assertTrue($result->getValue()); + self::assertTrue($result->getDetail()->getValue()); } public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound() @@ -531,7 +709,7 @@ public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound( $result = $feature->evaluate($user, $requester); - self::assertFalse($result->getValue()); + self::assertFalse($result->getDetail()->getValue()); } private function booleanFlagWithClauses($clauses) diff --git a/tests/FeatureFlagsStateTest.php b/tests/FeatureFlagsStateTest.php index 9b97d0a4b..e584f1e13 100644 --- a/tests/FeatureFlagsStateTest.php +++ b/tests/FeatureFlagsStateTest.php @@ -2,7 +2,8 @@ namespace LaunchDarkly\Tests; use InvalidArgumentException; -use LaunchDarkly\EvalResult; +use LaunchDarkly\EvaluationDetail; +use LaunchDarkly\EvaluationReason; use LaunchDarkly\FeatureFlag; use LaunchDarkly\FeatureFlagsState; @@ -42,7 +43,7 @@ public function testCanGetFlagValue() { $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag, new EvalResult(0, 'value1', array())); + $state->addFlag($flag, new EvaluationDetail('value1', 0)); $this->assertEquals('value1', $state->getFlagValue('key1')); } @@ -54,13 +55,38 @@ public function testUnknownFlagReturnsNullValue() $this->assertNull($state->getFlagValue('key1')); } + public function testCanGetFlagReason() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvaluationDetail('value1', 0, EvaluationReason::off()), true); + + $this->assertEquals(EvaluationReason::off(), $state->getFlagReason('key1')); + } + + public function testUnknownFlagReturnsNullReason() + { + $state = new FeatureFlagsState(true); + + $this->assertNull($state->getFlagReason('key1')); + } + + public function testReasonIsNullIfReasonsWereNotRecorded() + { + $flag = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); + $state = new FeatureFlagsState(true); + $state->addFlag($flag, new EvaluationDetail('value1', 0, EvaluationReason::off()), false); + + $this->assertNull($state->getFlagReason('key1')); + } + public function testCanConvertToValuesMap() { $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(0, 'value2', array())); + $state->addFlag($flag1, new EvaluationDetail('value1', 0)); + $state->addFlag($flag2, new EvaluationDetail('value2', 0)); $expected = array('key1' => 'value1', 'key2' => 'value2'); $this->assertEquals($expected, $state->toValuesMap()); @@ -71,8 +97,8 @@ public function testCanConvertToJson() $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + $state->addFlag($flag1, new EvaluationDetail('value1', 0)); + $state->addFlag($flag2, new EvaluationDetail('value2', 1)); $expected = array( 'key1' => 'value1', @@ -100,8 +126,8 @@ public function testJsonEncodeUsesCustomSerializer() $flag1 = FeatureFlag::decode(FeatureFlagsStateTest::$flag1Json); $flag2 = FeatureFlag::decode(FeatureFlagsStateTest::$flag2Json); $state = new FeatureFlagsState(true); - $state->addFlag($flag1, new EvalResult(0, 'value1', array())); - $state->addFlag($flag2, new EvalResult(1, 'value2', array())); + $state->addFlag($flag1, new EvaluationDetail('value1', 0)); + $state->addFlag($flag2, new EvaluationDetail('value2', 1)); $expected = $state->jsonSerialize(); $json = json_encode($state); diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index e523bd3e2..6893a3bd4 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -2,6 +2,7 @@ namespace LaunchDarkly\Tests; use InvalidArgumentException; +use LaunchDarkly\EvaluationReason; use LaunchDarkly\FeatureFlag; use LaunchDarkly\FeatureRequester; use LaunchDarkly\LDClient; @@ -16,10 +17,10 @@ public function testDefaultCtor() $this->assertInstanceOf(LDClient::class, new LDClient("BOGUS_SDK_KEY")); } - public function testVariationReturnsFlagValue() + private function makeOffFlagWithValue($key, $value) { $flagJson = array( - 'key' => 'feature', + 'key' => $key, 'version' => 100, 'deleted' => false, 'on' => false, @@ -28,21 +29,86 @@ public function testVariationReturnsFlagValue() 'rules' => array(), 'offVariation' => 1, 'fallthrough' => array('variation' => 0), - 'variations' => array('fall', 'off', 'on'), + 'variations' => array('FALLTHROUGH', $value), 'salt' => '' ); - $flag = FeatureFlag::decode($flagJson); + return FeatureFlag::decode($flagJson); + } + + private function makeFlagThatEvaluatesToNull($key) + { + $flagJson = array( + 'key' => $key, + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => null, + 'fallthrough' => array('variation' => 0), + 'variations' => array('none'), + 'salt' => '' + ); + return FeatureFlag::decode($flagJson); + } + public function testVariationReturnsFlagValue() + { + $flag = $this->makeOffFlagWithValue('feature', 'value'); MockFeatureRequester::$flags = array('feature' => $flag); $client = new LDClient("someKey", array( 'feature_requester_class' => MockFeatureRequester::class, 'events' => false )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $value = $client->variation('feature', $user, 'default'); - $this->assertEquals('off', $value); + $value = $client->variation('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('value', $value); + } + + public function testVariationDetailReturnsFlagValue() + { + $flag = $this->makeOffFlagWithValue('feature', 'value'); + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $detail = $client->variationDetail('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('value', $detail->getValue()); + $this->assertFalse($detail->isDefaultValue()); + $this->assertEquals(1, $detail->getVariationIndex()); + $this->assertEquals(EvaluationReason::off(), $detail->getReason()); + } + + public function testVariationReturnsDefaultIfFlagEvaluatesToNull() + { + $flag = $this->makeFlagThatEvaluatesToNull('feature'); + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $value = $client->variation('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('default', $value); + } + + public function testVariationDetailReturnsDefaultIfFlagEvaluatesToNull() + { + $flag = $this->makeFlagThatEvaluatesToNull('feature'); + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $detail = $client->variationDetail('feature', new LDUser('userkey'), 'default'); + $this->assertEquals('default', $detail->getValue()); + $this->assertTrue($detail->isDefaultValue()); + $this->assertNull($detail->getVariationIndex()); + $this->assertEquals(EvaluationReason::off(), $detail->getReason()); } public function testVariationReturnsDefaultForUnknownFlag() @@ -53,9 +119,22 @@ public function testVariationReturnsDefaultForUnknownFlag() 'events' => false )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $this->assertEquals('argdef', $client->variation('foo', $user, 'argdef')); + $this->assertEquals('argdef', $client->variation('foo', new LDUser('userkey'), 'argdef')); + } + + public function testVariationDetailReturnsDefaultForUnknownFlag() + { + MockFeatureRequester::$flags = array(); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $detail = $client->variationDetail('foo', new LDUser('userkey'), 'default'); + $this->assertEquals('default', $detail->getValue()); + $this->assertTrue($detail->isDefaultValue()); + $this->assertNull($detail->getVariationIndex()); + $this->assertEquals(EvaluationReason::error(EvaluationReason::FLAG_NOT_FOUND_ERROR), $detail->getReason()); } public function testVariationReturnsDefaultFromConfigurationForUnknownFlag() @@ -67,12 +146,60 @@ public function testVariationReturnsDefaultFromConfigurationForUnknownFlag() 'defaults' => array('foo' => 'fromarray') )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $this->assertEquals('fromarray', $client->variation('foo', $user, 'argdef')); + $this->assertEquals('fromarray', $client->variation('foo', new LDUser('userkey'), 'argdef')); } public function testVariationSendsEvent() + { + $flag = $this->makeOffFlagWithValue('flagkey', 'flagvalue'); + MockFeatureRequester::$flags = array('flagkey' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => true + )); + + $user = new LDUser('userkey'); + $client->variation('flagkey', new LDUser('userkey'), 'default'); + $proc = $this->getPrivateField($client, '_eventProcessor'); + $queue = $this->getPrivateField($proc, '_queue'); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('flagkey', $event['key']); + $this->assertEquals($flag->getVersion(), $event['version']); + $this->assertEquals('flagvalue', $event['value']); + $this->assertEquals(1, $event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertFalse(isset($event['reason'])); + } + + public function testVariationDetailSendsEvent() + { + $flag = $this->makeOffFlagWithValue('FUCKINGWEIRDflagkey', 'flagvalue'); + MockFeatureRequester::$flags = array('FUCKINGWEIRDflagkey' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => true + )); + + $user = new LDUser('userkey'); + $client->variationDetail('FUCKINGWEIRDflagkey', $user, 'default'); + $proc = $this->getPrivateField($client, '_eventProcessor'); + $queue = $this->getPrivateField($proc, '_queue'); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('FUCKINGWEIRDflagkey', $event['key']); + $this->assertEquals($flag->getVersion(), $event['version']); + $this->assertEquals('flagvalue', $event['value']); + $this->assertEquals(1, $event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertEquals(array('kind' => 'OFF'), $event['reason']); + } + + public function testVariationSendsEventForUnknownFlag() { MockFeatureRequester::$flags = array(); $client = new LDClient("someKey", array( @@ -80,12 +207,44 @@ public function testVariationSendsEvent() 'events' => true )); - $builder = new LDUserBuilder(3); - $user = $builder->build(); - $client->variation('foo', $user, 'argdef'); + $user = new LDUser('userkey'); + $client->variation('flagkey', new LDUser('userkey'), 'default'); $proc = $this->getPrivateField($client, '_eventProcessor'); $queue = $this->getPrivateField($proc, '_queue'); $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('flagkey', $event['key']); + $this->assertNull($event['version']); + $this->assertEquals('default', $event['value']); + $this->assertNull($event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertFalse(isset($event['reason'])); + } + + public function testVariationDetailSendsEventForUnknownFlag() + { + MockFeatureRequester::$flags = array(); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => true + )); + + $user = new LDUser('userkey'); + $client->variationDetail('flagkey', new LDUser('userkey'), 'default'); + $proc = $this->getPrivateField($client, '_eventProcessor'); + $queue = $this->getPrivateField($proc, '_queue'); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('feature', $event['kind']); + $this->assertEquals('flagkey', $event['key']); + $this->assertNull($event['version']); + $this->assertEquals('default', $event['value']); + $this->assertNull($event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertEquals(array('kind' => 'ERROR', 'errorKind' => 'FLAG_NOT_FOUND'), $event['reason']); } public function testAllFlagsReturnsFlagValues() @@ -164,6 +323,53 @@ public function testAllFlagsStateReturnsState() $this->assertEquals($expectedState, $state->jsonSerialize()); } + public function testAllFlagsStateReturnsStateWithReasons() + { + $flagJson = array( + 'key' => 'feature', + 'version' => 100, + 'deleted' => false, + 'on' => false, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fall', 'off', 'on'), + 'salt' => '', + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000 + ); + $flag = FeatureFlag::decode($flagJson); + + MockFeatureRequester::$flags = array('feature' => $flag); + $client = new LDClient("someKey", array( + 'feature_requester_class' => MockFeatureRequester::class, + 'events' => false + )); + + $builder = new LDUserBuilder(3); + $user = $builder->build(); + $state = $client->allFlagsState($user, array('withReasons' => true)); + + $this->assertTrue($state->isValid()); + $this->assertEquals(array('feature' => 'off'), $state->toValuesMap()); + $expectedState = array( + 'feature' => 'off', + '$flagsState' => array( + 'feature' => array( + 'variation' => 1, + 'version' => 100, + 'trackEvents' => true, + 'debugEventsUntilDate' => 1000, + 'reason' => array('kind' => 'OFF') + ) + ), + '$valid' => true + ); + $this->assertEquals($expectedState, $state->jsonSerialize()); + } + public function testAllFlagsStateCanFilterForClientSideFlags() { $flagJson = array('key' => 'server-side-1', 'version' => 1, 'on' => false, 'salt' => '', 'deleted' => false,