diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 46123f8a4..32d85fb29 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -29,6 +29,8 @@ class FeatureFlag protected $_deleted = false; /** @var bool */ protected $_trackEvents = false; + /** @var bool */ + protected $_trackEventsFallthrough = false; /** @var int | null */ protected $_debugEventsUntilDate = null; /** @var bool */ @@ -50,6 +52,7 @@ protected function __construct($key, array $variations, $deleted, $trackEvents, + $trackEventsFallthrough, $debugEventsUntilDate, $clientSide) { @@ -65,6 +68,7 @@ protected function __construct($key, $this->_variations = $variations; $this->_deleted = $deleted; $this->_trackEvents = $trackEvents; + $this->_trackEventsFallthrough = $trackEventsFallthrough; $this->_debugEventsUntilDate = $debugEventsUntilDate; $this->_clientSide = $clientSide; } @@ -85,6 +89,7 @@ public static function getDecoder() $v['variations'] ?: [], $v['deleted'], isset($v['trackEvents']) && $v['trackEvents'], + isset($v['trackEventsFallthrough']) && $v['trackEventsFallthrough'], isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null, isset($v['clientSide']) && $v['clientSide'] ); @@ -104,13 +109,13 @@ public function isOn() /** * @param LDUser $user * @param FeatureRequester $featureRequester - * @param bool $includeReasonsInEvents + * @param Impl\EventFactory $eventFactory * @return EvalResult */ - public function evaluate($user, $featureRequester, $includeReasonsInEvents = false) + public function evaluate($user, $featureRequester, $eventFactory) { $prereqEvents = array(); - $detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $includeReasonsInEvents); + $detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $eventFactory); return new EvalResult($detail, $prereqEvents); } @@ -118,16 +123,16 @@ public function evaluate($user, $featureRequester, $includeReasonsInEvents = fal * @param LDUser $user * @param FeatureRequester $featureRequester * @param array $events - * @param bool $includeReasonsInEvents + * @param Impl\EventFactory $eventFactory * @return EvaluationDetail */ - private function evaluateInternal($user, $featureRequester, &$events, $includeReasonsInEvents) + private function evaluateInternal($user, $featureRequester, &$events, $eventFactory) { if (!$this->isOn()) { return $this->getOffValue(EvaluationReason::off()); } - $prereqFailureReason = $this->checkPrerequisites($user, $featureRequester, $events, $includeReasonsInEvents); + $prereqFailureReason = $this->checkPrerequisites($user, $featureRequester, $events, $eventFactory); if ($prereqFailureReason !== null) { return $this->getOffValue($prereqFailureReason); } @@ -158,10 +163,10 @@ private function evaluateInternal($user, $featureRequester, &$events, $includeRe * @param LDUser $user * @param FeatureRequester $featureRequester * @param array $events - * @param bool $includeReasonsInEvents + * @param Impl\EventFactory $eventFactory * @return EvaluationReason|null */ - private function checkPrerequisites($user, $featureRequester, &$events, $includeReasonsInEvents) + private function checkPrerequisites($user, $featureRequester, &$events, $eventFactory) { if ($this->_prerequisites != null) { foreach ($this->_prerequisites as $prereq) { @@ -172,16 +177,12 @@ private function checkPrerequisites($user, $featureRequester, &$events, $include if ($prereqFeatureFlag == null) { $prereqOk = false; } else { - $prereqEvalResult = $prereqFeatureFlag->evaluateInternal($user, $featureRequester, $events, $includeReasonsInEvents); + $prereqEvalResult = $prereqFeatureFlag->evaluateInternal($user, $featureRequester, $events, $eventFactory); $variation = $prereq->getVariation(); if (!$prereqFeatureFlag->isOn() || $prereqEvalResult->getVariationIndex() !== $variation) { $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, $eventFactory->newEvalEvent($prereqFeatureFlag, $user, $prereqEvalResult, null, $this)); } } catch (EvaluationException $e) { $prereqOk = false; @@ -258,6 +259,14 @@ public function isDeleted() return $this->_deleted; } + /** + * @return array + */ + public function getRules() + { + return $this->_rules; + } + /** * @return boolean */ @@ -266,6 +275,14 @@ public function isTrackEvents() return $this->_trackEvents; } + /** + * @return boolean + */ + public function isTrackEventsFallthrough() + { + return $this->_trackEventsFallthrough; + } + /** * @return int | null */ diff --git a/src/LaunchDarkly/Impl/EventFactory.php b/src/LaunchDarkly/Impl/EventFactory.php new file mode 100644 index 000000000..068ca4e51 --- /dev/null +++ b/src/LaunchDarkly/Impl/EventFactory.php @@ -0,0 +1,127 @@ +_withReasons = $withReasons; + } + + public function newEvalEvent($flag, $user, $detail, $default, $prereqOfFlag = null) + { + $addExperimentData = static::isExperiment($flag, $detail->getReason()); + $e = array( + 'kind' => 'feature', + 'creationDate' => Util::currentTimeUnixMillis(), + 'key' => $flag->getKey(), + 'user' => $user, + 'variation' => $detail->getVariationIndex(), + 'value' => $detail->getValue(), + 'default' => $default, + 'version' => $flag->getVersion() + ); + // the following properties are handled separately so we don't waste bandwidth on unused keys + if ($addExperimentData || $flag->isTrackEvents()) { + $e['trackEvents'] = true; + } + if ($flag->getDebugEventsUntilDate()) { + $e['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate(); + } + if ($prereqOfFlag) { + $e['prereqOf'] = $prereqOfFlag->getKey(); + } + if (($addExperimentData || $this->_withReasons) && $detail->getReason()) { + $e['reason'] = $detail->getReason()->jsonSerialize(); + } + return $e; + } + + public function newDefaultEvent($flag, $user, $detail) + { + $e = array( + 'kind' => 'feature', + 'creationDate' => Util::currentTimeUnixMillis(), + 'key' => $flag->getKey(), + 'user' => $user, + 'value' => $detail->getValue(), + 'default' => $detail->getValue(), + 'version' => $flag->getVersion() + ); + // the following properties are handled separately so we don't waste bandwidth on unused keys + if ($flag->isTrackEvents()) { + $e['trackEvents'] = true; + } + if ($flag->getDebugEventsUntilDate()) { + $e['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate(); + } + if ($this->_withReasons && $detail->getReason()) { + $e['reason'] = $detail->getReason()->jsonSerialize(); + } + return $e; + } + + public function newUnknownFlagEvent($key, $user, $detail) + { + $e = array( + 'kind' => 'feature', + 'creationDate' => Util::currentTimeUnixMillis(), + 'key' => $key, + 'user' => $user, + 'value' => $detail->getValue(), + 'default' => $detail->getValue() + ); + // the following properties are handled separately so we don't waste bandwidth on unused keys + if ($this->_withReasons && $detail->getReason()) { + $e['reason'] = $detail->getReason()->jsonSerialize(); + } + return $e; + } + + public function newIdentifyEvent($user) + { + return array( + 'kind' => 'identify', + 'creationDate' => Util::currentTimeUnixMillis(), + 'key' => strval($user->getKey()), + 'user' => $user + ); + } + + public function newCustomEvent($eventName, $user, $data, $metricValue) + { + $e = array( + 'kind' => 'custom', + 'creationDate' => Util::currentTimeUnixMillis(), + 'key' => $eventName, + 'user' => $user + ); + if (isset($data)) { + $e['data'] = $data; + } + if (isset($metricValue)) { + $e['metricValue'] = $metricValue; + } + return $e; + } + + private static function isExperiment($flag, $reason) + { + if ($reason) { + switch ($reason->getKind()) { + case 'RULE_MATCH': + $i = $reason->getRuleIndex(); + $rules = $flag->getRules(); + return isset($i) && $i >= 0 && $i < count($rules) && $rules[$i]->isTrackEvents(); + case 'FALLTHROUGH': + return $flag->isTrackEventsFallthrough(); + } + } + return false; + } +} diff --git a/src/LaunchDarkly/Impl/NullEventProcessor.php b/src/LaunchDarkly/Impl/NullEventProcessor.php new file mode 100644 index 000000000..36e9d7a6d --- /dev/null +++ b/src/LaunchDarkly/Impl/NullEventProcessor.php @@ -0,0 +1,14 @@ +_logger = $options['logger']; - $this->_eventProcessor = new EventProcessor($sdkKey, $options); + $this->_eventFactoryDefault = new EventFactory(false); + $this->_eventFactoryWithReasons = new EventFactory(true); + + if (isset($options['event_processor'])) { + $ep = $options['event_processor']; + if (is_callable($ep)) { + $ep = $ep($sdkKey, $options); + } + $this->_eventProcessor = $ep; + } elseif ($this->_offline || !$this->_send_events) { + $this->_eventProcessor = new EventProcessor($sdkKey, $options); + } else { + $this->_eventProcessor = new EventProcessor($sdkKey, $options); + } $this->_featureRequester = $this->getFeatureRequester($sdkKey, $options); } @@ -141,7 +160,7 @@ private function getFeatureRequester($sdkKey, array $options) */ public function variation($key, $user, $default = false) { - $detail = $this->variationDetailInternal($key, $user, $default, false); + $detail = $this->variationDetailInternal($key, $user, $default, $this->_eventFactoryDefault); return $detail->getValue(); } @@ -159,29 +178,28 @@ public function variation($key, $user, $default = false) */ public function variationDetail($key, $user, $default = false) { - return $this->variationDetailInternal($key, $user, $default, true); + return $this->variationDetailInternal($key, $user, $default, $this->_eventFactoryWithReasons); } /** * @param string $key * @param LDUser $user * @param mixed $default - * @param bool $includeReasonsInEvents + * @param EventFactory $eventFactory */ - private function variationDetailInternal($key, $user, $default, $includeReasonsInEvents) + private function variationDetailInternal($key, $user, $default, $eventFactory) { $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; + $sendEvent = function ($detail, $flag) use ($key, $user, $default, $eventFactory) { + if ($flag) { + $event = $eventFactory->newEvalEvent($flag, $user, $detail, $default); + } else { + $event = $eventFactory->newUnknownFlagEvent($key, $user, $detail); } - $event = Util::newFeatureRequestEvent($key, $user, $detail->getVariationIndex(), $detail->getValue(), - $default, $flag ? $flag->getVersion() : null, null, - $includeReasonsInEvents ? $detail->getReason() : null); $this->_eventProcessor->enqueue($event); }; @@ -211,11 +229,9 @@ private function variationDetailInternal($key, $user, $default, $includeReasonsI $this->_logger->warning("Variation called with null user or null user key! Returning default value"); return $result; } - $evalResult = $flag->evaluate($user, $this->_featureRequester, $includeReasonsInEvents); - if (!$this->isOffline() && $this->_send_events) { - foreach ($evalResult->getPrerequisiteEvents() as $e) { - $this->_eventProcessor->enqueue($e); - } + $evalResult = $flag->evaluate($user, $this->_featureRequester, $eventFactory); + foreach ($evalResult->getPrerequisiteEvents() as $e) { + $this->_eventProcessor->enqueue($e); } $detail = $evalResult->getDetail(); if ($detail->isDefaultValue()) { @@ -261,27 +277,18 @@ public function isOffline() * * @param $eventName string The name of the event * @param $user LDUser The user that performed the event - * @param $data mixed + * @param $data mixed Optional additional information to associate with the event + * @param $metricValue number A numeric value used by the LaunchDarkly experimentation feature in + * numeric custom metrics. Can be omitted if this event is used by only non-numeric metrics. This + * field will also be returned as part of the custom event for Data Export. */ - public function track($eventName, $user, $data) + public function track($eventName, $user, $data = null, $metricValue = null) { - if ($this->isOffline()) { - return; - } if (is_null($user) || $user->isKeyBlank()) { $this->_logger->warning("Track called with null user or null/empty user key!"); return; } - - $event = array(); - $event['user'] = $user; - $event['kind'] = "custom"; - $event['creationDate'] = Util::currentTimeUnixMillis(); - $event['key'] = $eventName; - if (isset($data)) { - $event['data'] = $data; - } - $this->_eventProcessor->enqueue($event); + $this->_eventProcessor->enqueue($this->_eventFactoryDefault->newCustomEvent($eventName, $user, $data, $metricValue)); } /** @@ -289,20 +296,11 @@ public function track($eventName, $user, $data) */ public function identify($user) { - if ($this->isOffline()) { - return; - } if (is_null($user) || $user->isKeyBlank()) { $this->_logger->warning("Track called with null user or null/empty user key!"); return; } - - $event = array(); - $event['user'] = $user; - $event['kind'] = "identify"; - $event['creationDate'] = Util::currentTimeUnixMillis(); - $event['key'] = strval($user->getKey()); - $this->_eventProcessor->enqueue($event); + $this->_eventProcessor->enqueue($this->_eventFactoryDefault->newIdentifyEvent($user)); } /** Returns an array mapping Feature Flag keys to their evaluated results for a given user. @@ -370,7 +368,7 @@ public function allFlagsState($user, $options = array()) if ($clientOnly && !$flag->isClientSide()) { continue; } - $result = $flag->evaluate($user, $preloadedRequester); + $result = $flag->evaluate($user, $preloadedRequester, $this->_eventFactoryDefault); $state->addFlag($flag, $result->getDetail(), $withReasons, $detailsOnlyIfTracked); } return $state; diff --git a/src/LaunchDarkly/Rule.php b/src/LaunchDarkly/Rule.php index 39d1a9622..5b60a24c1 100644 --- a/src/LaunchDarkly/Rule.php +++ b/src/LaunchDarkly/Rule.php @@ -8,12 +8,15 @@ class Rule extends VariationOrRollout private $_id = null; /** @var Clause[] */ private $_clauses = array(); + /** @var boolean */ + private $_trackEvents; - protected function __construct($variation, $rollout, $id, array $clauses) + protected function __construct($variation, $rollout, $id, array $clauses, $trackEvents) { parent::__construct($variation, $rollout); $this->_id = $id; $this->_clauses = $clauses; + $this->_trackEvents = $trackEvents; } public static function getDecoder() @@ -23,7 +26,8 @@ public static function getDecoder() 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'])); + array_map(Clause::getDecoder(), $v['clauses']), + isset($v['trackEvents']) ? $v['trackEvents'] : false); }; } @@ -56,4 +60,12 @@ public function getClauses() { return $this->_clauses; } + + /** + * @return boolean + */ + public function isTrackEvents() + { + return $this->_trackEvents; + } } diff --git a/src/LaunchDarkly/Util.php b/src/LaunchDarkly/Util.php index e9afd00af..b3619decc 100644 --- a/src/LaunchDarkly/Util.php +++ b/src/LaunchDarkly/Util.php @@ -7,7 +7,6 @@ class Util { - /** * @param $dateTime DateTime * @return int @@ -27,34 +26,6 @@ public static function currentTimeUnixMillis() return Util::dateTimeToUnixMillis(new DateTime('now', new DateTimeZone("UTC"))); } - - /** - * @param $key string - * @param $user LDUser - * @param $value - * @param $default - * @param null $version int | null - * @param null $prereqOf string | null - * @return array - */ - public static function newFeatureRequestEvent($key, $user, $variation, $value, $default, $version = null, $prereqOf = null, $reason = null) - { - $event = array(); - $event['user'] = $user; - $event['variation'] = $variation; - $event['value'] = $value; - $event['kind'] = "feature"; - $event['creationDate'] = Util::currentTimeUnixMillis(); - $event['key'] = $key; - $event['default'] = $default; - $event['version'] = $version; - $event['prereqOf'] = $prereqOf; - if ($reason !== null) { - $event['reason'] = $reason->jsonSerialize(); - } - return $event; - } - /** * @param $status int * @return boolean diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 19c22e324..e6dd3f94d 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -4,6 +4,7 @@ use LaunchDarkly\EvaluationDetail; use LaunchDarkly\EvaluationReason; use LaunchDarkly\FeatureFlag; +use LaunchDarkly\Impl\EventFactory; use LaunchDarkly\LDUser; use LaunchDarkly\LDUserBuilder; use LaunchDarkly\Segment; @@ -189,6 +190,13 @@ class FeatureFlagTest extends \PHPUnit_Framework_TestCase \"deleted\": false }"; + private static $eventFactory; + + public static function setUpBeforeClass() + { + static::$eventFactory = new EventFactory(false); + } + public function testDecode() { $this->assertInstanceOf(FeatureFlag::class, FeatureFlag::decode(\GuzzleHttp\json_decode(FeatureFlagTest::$json1, true))); @@ -259,7 +267,7 @@ public function testFlagReturnsOffVariationIfFlagIsOff() ); $flag = FeatureFlag::decode($flagJson); - $result = $flag->evaluate(new LDUser('user'), null); + $result = $flag->evaluate(new LDUser('user'), null, static::$eventFactory); $detail = new EvaluationDetail('off', 1, EvaluationReason::off()); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -282,7 +290,7 @@ public function testFlagReturnsNullIfFlagIsOffAndOffVariationIsUnspecified() ); $flag = FeatureFlag::decode($flagJson); - $result = $flag->evaluate(new LDUser('user'), null); + $result = $flag->evaluate(new LDUser('user'), null, static::$eventFactory); $detail = new EvaluationDetail(null, null, EvaluationReason::off()); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -305,7 +313,7 @@ public function testFlagReturnsErrorIfOffVariationIsTooHigh() ); $flag = FeatureFlag::decode($flagJson); - $result = $flag->evaluate(new LDUser('user'), null); + $result = $flag->evaluate(new LDUser('user'), null, static::$eventFactory); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -328,7 +336,7 @@ public function testFlagReturnsErrorIfOffVariationIsNegative() ); $flag = FeatureFlag::decode($flagJson); - $result = $flag->evaluate(new LDUser('user'), null); + $result = $flag->evaluate(new LDUser('user'), null, static::$eventFactory); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -356,7 +364,7 @@ public function testFlagReturnsOffVariationIfPrerequisiteIsNotFound() $user = $ub->build(); $requester = new MockFeatureRequesterForFeature(); - $result = $flag->evaluate($user, $requester); + $result = $flag->evaluate($user, $requester, static::$eventFactory); $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -401,7 +409,7 @@ public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsOff() $requester->key = $flag1->getKey(); $requester->val = $flag1; - $result = $flag0->evaluate($user, $requester); + $result = $flag0->evaluate($user, $requester, static::$eventFactory); $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); self::assertEquals($detail, $result->getDetail()); @@ -453,7 +461,7 @@ public function testFlagReturnsOffVariationAndEventIfPrerequisiteIsNotMet() $requester->key = $flag1->getKey(); $requester->val = $flag1; - $result = $flag0->evaluate($user, $requester); + $result = $flag0->evaluate($user, $requester, static::$eventFactory); $detail = new EvaluationDetail('off', 1, EvaluationReason::prerequisiteFailed('feature1')); self::assertEquals($detail, $result->getDetail()); @@ -505,7 +513,7 @@ public function testFlagReturnsFallthroughVariationAndEventIfPrerequisiteIsMetAn $requester->key = $flag1->getKey(); $requester->val = $flag1; - $result = $flag0->evaluate($user, $requester); + $result = $flag0->evaluate($user, $requester, static::$eventFactory); $detail = new EvaluationDetail('fall', 0, EvaluationReason::fallthrough()); self::assertEquals($detail, $result->getDetail()); @@ -540,7 +548,7 @@ public function testFlagMatchesUserFromTargets() $ub = new LDUserBuilder('userkey'); $user = $ub->build(); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); $detail = new EvaluationDetail('on', 2, EvaluationReason::targetMatch()); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -551,7 +559,7 @@ public function testFlagMatchesUserFromRules() global $defaultUser; $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => 1)))); - $result = $flag->evaluate($defaultUser, null); + $result = $flag->evaluate($defaultUser, null, static::$eventFactory); $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -562,7 +570,7 @@ public function testFlagReturnsErrorIfRuleVariationIsTooHigh() global $defaultUser; $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => 999)))); - $result = $flag->evaluate($defaultUser, null); + $result = $flag->evaluate($defaultUser, null, static::$eventFactory); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -573,7 +581,7 @@ public function testFlagReturnsErrorIfRuleVariationIsNegative() global $defaultUser; $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => -1)))); - $result = $flag->evaluate($defaultUser, null); + $result = $flag->evaluate($defaultUser, null, static::$eventFactory); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -584,7 +592,7 @@ public function testFlagReturnsErrorIfRuleHasNoVariationOrRollout() global $defaultUser; $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array()))); - $result = $flag->evaluate($defaultUser, null); + $result = $flag->evaluate($defaultUser, null, static::$eventFactory); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -596,7 +604,7 @@ public function testFlagReturnsErrorIfRuleHasRolloutWithNoVariations() $rollout = array('variations' => array()); $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('rollout' => $rollout)))); - $result = $flag->evaluate($defaultUser, null); + $result = $flag->evaluate($defaultUser, null, static::$eventFactory); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -613,7 +621,7 @@ public function testRolloutCalculationBucketsByUserKeyByDefault() ); $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); } @@ -631,7 +639,7 @@ public function testRolloutCalculationCanBucketBySpecificAttribute() ); $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); } @@ -648,7 +656,7 @@ public function testRolloutCalculationIncludesSecondaryKey() ); $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -667,7 +675,7 @@ public function testRolloutCalculationCoercesSecondaryKeyToString() ); $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -681,7 +689,7 @@ public function testClauseCanMatchBuiltInAttribute() $ub->name('Bob'); $user = $ub->build(); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); self::assertEquals(true, $result->getDetail()->getValue()); } @@ -693,7 +701,7 @@ public function testClauseCanMatchCustomAttribute() $ub->customAttribute('legs', 4); $user = $ub->build(); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); self::assertEquals(true, $result->getDetail()->getValue()); } @@ -704,7 +712,7 @@ public function testClauseReturnsFalseForMissingAttribute() $ub = new LDUserBuilder('userkey'); $user = $ub->build(); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); self::assertEquals(false, $result->getDetail()->getValue()); } @@ -716,7 +724,7 @@ public function testClauseCanBeNegated() $ub->name('Bob'); $user = $ub->build(); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); self::assertEquals(false, $result->getDetail()->getValue()); } @@ -728,7 +736,7 @@ public function testClauseWithUnknownOperatorDoesNotMatch() $ub->name('Bob'); $user = $ub->build(); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($user, null, static::$eventFactory); self::assertEquals(false, $result->getDetail()->getValue()); } @@ -752,7 +760,7 @@ public function testSegmentMatchClauseRetrievesSegmentFromStore() $feature = makeBooleanFlagWithClauses(array(makeSegmentMatchClause('segkey'))); - $result = $feature->evaluate($defaultUser, $requester); + $result = $feature->evaluate($defaultUser, $requester, static::$eventFactory); self::assertTrue($result->getDetail()->getValue()); } @@ -764,7 +772,7 @@ public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound( $feature = makeBooleanFlagWithClauses(array(makeSegmentMatchClause('segkey'))); - $result = $feature->evaluate($defaultUser, $requester); + $result = $feature->evaluate($defaultUser, $requester, static::$eventFactory); self::assertFalse($result->getDetail()->getValue()); } diff --git a/tests/FileDataFeatureRequesterTest.php b/tests/FileDataFeatureRequesterTest.php index 1e0e51a2b..7cbdef77c 100644 --- a/tests/FileDataFeatureRequesterTest.php +++ b/tests/FileDataFeatureRequesterTest.php @@ -32,7 +32,7 @@ public function testShortcutFlagCanBeEvaluated() $fr = Files::featureRequester("./tests/filedata/all-properties.json"); $flag2 = $fr->getFeature("flag2"); $this->assertEquals("flag2", $flag2->getKey()); - $result = $flag2->evaluate(new LDUser("user"), null); + $result = $flag2->evaluate(new LDUser("user"), null, null); $this->assertEquals("value2", $result->getDetail()->getValue()); } } diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 25921a2cf..15faa024e 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -53,14 +53,20 @@ private function makeFlagThatEvaluatesToNull($key) return FeatureFlag::decode($flagJson); } + private function makeClient($overrideOptions = array()) + { + $options = array( + 'feature_requester_class' => MockFeatureRequester::class, + 'event_processor' => new MockEventProcessor() + ); + return new LDClient("someKey", array_merge($options, $overrideOptions)); + } + 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 - )); + $client = $this->makeClient(); $value = $client->variation('feature', new LDUser('userkey'), 'default'); $this->assertEquals('value', $value); @@ -70,10 +76,7 @@ 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 - )); + $client = $this->makeClient(); $detail = $client->variationDetail('feature', new LDUser('userkey'), 'default'); $this->assertEquals('value', $detail->getValue()); @@ -86,10 +89,7 @@ public function testVariationReturnsDefaultIfFlagEvaluatesToNull() { $flag = $this->makeFlagThatEvaluatesToNull('feature'); MockFeatureRequester::$flags = array('feature' => $flag); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $value = $client->variation('feature', new LDUser('userkey'), 'default'); $this->assertEquals('default', $value); @@ -99,10 +99,7 @@ public function testVariationDetailReturnsDefaultIfFlagEvaluatesToNull() { $flag = $this->makeFlagThatEvaluatesToNull('feature'); MockFeatureRequester::$flags = array('feature' => $flag); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $detail = $client->variationDetail('feature', new LDUser('userkey'), 'default'); $this->assertEquals('default', $detail->getValue()); @@ -114,10 +111,7 @@ public function testVariationDetailReturnsDefaultIfFlagEvaluatesToNull() public function testVariationReturnsDefaultForUnknownFlag() { MockFeatureRequester::$flags = array(); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $this->assertEquals('argdef', $client->variation('foo', new LDUser('userkey'), 'argdef')); } @@ -125,10 +119,7 @@ public function testVariationReturnsDefaultForUnknownFlag() public function testVariationDetailReturnsDefaultForUnknownFlag() { MockFeatureRequester::$flags = array(); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $detail = $client->variationDetail('foo', new LDUser('userkey'), 'default'); $this->assertEquals('default', $detail->getValue()); @@ -140,11 +131,7 @@ public function testVariationDetailReturnsDefaultForUnknownFlag() public function testVariationReturnsDefaultFromConfigurationForUnknownFlag() { MockFeatureRequester::$flags = array(); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false, - 'defaults' => array('foo' => 'fromarray') - )); + $client = $this->makeClient(array('defaults' => array('foo' => 'fromarray'))); $this->assertEquals('fromarray', $client->variation('foo', new LDUser('userkey'), 'argdef')); } @@ -153,15 +140,12 @@ 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 - )); + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); $user = new LDUser('userkey'); $client->variation('flagkey', new LDUser('userkey'), 'default'); - $proc = $this->getPrivateField($client, '_eventProcessor'); - $queue = $this->getPrivateField($proc, '_queue'); + $queue = $ep->getEvents(); $this->assertEquals(1, sizeof($queue)); $event = $queue[0]; $this->assertEquals('feature', $event['kind']); @@ -171,6 +155,7 @@ public function testVariationSendsEvent() $this->assertEquals(1, $event['variation']); $this->assertEquals($user, $event['user']); $this->assertEquals('default', $event['default']); + $this->assertFalse(isset($event['trackEvents'])); $this->assertFalse(isset($event['reason'])); } @@ -178,15 +163,12 @@ public function testVariationDetailSendsEvent() { $flag = $this->makeOffFlagWithValue('flagkey', 'flagvalue'); MockFeatureRequester::$flags = array('flagkey' => $flag); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => true - )); + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); $user = new LDUser('userkey'); $client->variationDetail('flagkey', $user, 'default'); - $proc = $this->getPrivateField($client, '_eventProcessor'); - $queue = $this->getPrivateField($proc, '_queue'); + $queue = $ep->getEvents(); $this->assertEquals(1, sizeof($queue)); $event = $queue[0]; $this->assertEquals('feature', $event['kind']); @@ -196,28 +178,115 @@ public function testVariationDetailSendsEvent() $this->assertEquals(1, $event['variation']); $this->assertEquals($user, $event['user']); $this->assertEquals('default', $event['default']); + $this->assertFalse(isset($event['trackEvents'])); $this->assertEquals(array('kind' => 'OFF'), $event['reason']); } + public function testVariationForcesTrackingWhenMatchedRuleHasTrackEventsSet() + { + $flagJson = array( + 'key' => 'flagkey', + 'version' => 100, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array( + array( + 'clauses' => array( + array( + 'attribute' => 'key', + 'op' => 'in', + 'values' => array('userkey'), + 'negate' => false + ) + ), + 'id' => 'rule-id', + 'variation' => 1, + 'trackEvents' => true + ) + ), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fellthrough', 'flagvalue'), + 'salt' => '' + ); + $flag = FeatureFlag::decode($flagJson); + + MockFeatureRequester::$flags = array('flagkey' => $flag); + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); + + $user = new LDUser('userkey'); + $client->variation('flagkey', new LDUser('userkey'), 'default'); + $queue = $ep->getEvents(); + $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->assertTrue($event['trackEvents']); + $this->assertEquals(array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'rule-id'), $event['reason']); + } + + public function testVariationForcesTrackingForFallthroughWhenTrackEventsFallthroughIsSet() + { + $flagJson = array( + 'key' => 'flagkey', + 'version' => 100, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => array('variation' => 0), + 'variations' => array('fellthrough', 'flagvalue'), + 'salt' => '', + 'trackEventsFallthrough' => true + ); + $flag = FeatureFlag::decode($flagJson); + + MockFeatureRequester::$flags = array('flagkey' => $flag); + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); + + $user = new LDUser('userkey'); + $client->variation('flagkey', new LDUser('userkey'), 'default'); + $queue = $ep->getEvents(); + $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('fellthrough', $event['value']); + $this->assertEquals(0, $event['variation']); + $this->assertEquals($user, $event['user']); + $this->assertEquals('default', $event['default']); + $this->assertTrue($event['trackEvents']); + $this->assertEquals(array('kind' => 'FALLTHROUGH'), $event['reason']); + } + public function testVariationSendsEventForUnknownFlag() { MockFeatureRequester::$flags = array(); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => true - )); + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); $user = new LDUser('userkey'); $client->variation('flagkey', new LDUser('userkey'), 'default'); - $proc = $this->getPrivateField($client, '_eventProcessor'); - $queue = $this->getPrivateField($proc, '_queue'); + $queue = $ep->getEvents(); $this->assertEquals(1, sizeof($queue)); $event = $queue[0]; $this->assertEquals('feature', $event['kind']); $this->assertEquals('flagkey', $event['key']); - $this->assertNull($event['version']); + $this->assertFalse(isset($event['version'])); $this->assertEquals('default', $event['value']); - $this->assertNull($event['variation']); + $this->assertFalse(isset($event['variation'])); $this->assertEquals($user, $event['user']); $this->assertEquals('default', $event['default']); $this->assertFalse(isset($event['reason'])); @@ -226,22 +295,19 @@ public function testVariationSendsEventForUnknownFlag() public function testVariationDetailSendsEventForUnknownFlag() { MockFeatureRequester::$flags = array(); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => true - )); + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); $user = new LDUser('userkey'); $client->variationDetail('flagkey', new LDUser('userkey'), 'default'); - $proc = $this->getPrivateField($client, '_eventProcessor'); - $queue = $this->getPrivateField($proc, '_queue'); + $queue = $ep->getEvents(); $this->assertEquals(1, sizeof($queue)); $event = $queue[0]; $this->assertEquals('feature', $event['kind']); $this->assertEquals('flagkey', $event['key']); - $this->assertNull($event['version']); + $this->assertFalse(isset($event['version'])); $this->assertEquals('default', $event['value']); - $this->assertNull($event['variation']); + $this->assertFalse(isset($event['variation'])); $this->assertEquals($user, $event['user']); $this->assertEquals('default', $event['default']); $this->assertEquals(array('kind' => 'ERROR', 'errorKind' => 'FLAG_NOT_FOUND'), $event['reason']); @@ -265,10 +331,7 @@ public function testAllFlagsReturnsFlagValues() $flag = FeatureFlag::decode($flagJson); MockFeatureRequester::$flags = array('feature' => $flag); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $builder = new LDUserBuilder(3); $user = $builder->build(); @@ -297,10 +360,7 @@ public function testAllFlagsStateReturnsState() $flag = FeatureFlag::decode($flagJson); MockFeatureRequester::$flags = array('feature' => $flag); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $builder = new LDUserBuilder(3); $user = $builder->build(); @@ -343,10 +403,7 @@ public function testAllFlagsStateReturnsStateWithReasons() $flag = FeatureFlag::decode($flagJson); MockFeatureRequester::$flags = array('feature' => $flag); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $builder = new LDUserBuilder(3); $user = $builder->build(); @@ -388,10 +445,7 @@ public function testAllFlagsStateCanFilterForClientSideFlags() MockFeatureRequester::$flags = array( $flag1->getKey() => $flag1, $flag2->getKey() => $flag2, $flag3->getKey() => $flag3, $flag4->getKey() => $flag4 ); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $builder = new LDUserBuilder(3); $user = $builder->build(); @@ -451,10 +505,7 @@ public function testAllFlagsStateCanOmitDetailsForUntrackedFlags() $flag3 = FeatureFlag::decode($flag3Json); MockFeatureRequester::$flags = array('flag1' => $flag1, 'flag2' => $flag2, 'flag3' => $flag3); - $client = new LDClient("someKey", array( - 'feature_requester_class' => MockFeatureRequester::class, - 'events' => false - )); + $client = $this->makeClient(); $builder = new LDUserBuilder(3); $user = $builder->build(); @@ -488,6 +539,60 @@ public function testAllFlagsStateCanOmitDetailsForUntrackedFlags() $this->assertEquals($expectedState, $state->jsonSerialize()); } + public function testTrackSendsEvent() + { + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); + + $user = new LDUser('userkey'); + $client->track('eventkey', $user); + $queue = $ep->getEvents(); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('custom', $event['kind']); + $this->assertEquals('eventkey', $event['key']); + $this->assertEquals($user, $event['user']); + $this->assertFalse(isset($event['data'])); + $this->assertFalse(isset($event['metricValue'])); + } + + public function testTrackSendsEventWithData() + { + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); + $data = array('thing' => 'stuff'); + + $user = new LDUser('userkey'); + $client->track('eventkey', $user, $data); + $queue = $ep->getEvents(); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('custom', $event['kind']); + $this->assertEquals('eventkey', $event['key']); + $this->assertEquals($user, $event['user']); + $this->assertEquals($data, $event['data']); + $this->assertFalse(isset($event['metricValue'])); + } + + public function testTrackSendsEventWithDataAndMetricValue() + { + $ep = new MockEventProcessor(); + $client = $this->makeClient(array('event_processor' => $ep)); + $data = array('thing' => 'stuff'); + $metricValue = 1.5; + + $user = new LDUser('userkey'); + $client->track('eventkey', $user, $data, $metricValue); + $queue = $ep->getEvents(); + $this->assertEquals(1, sizeof($queue)); + $event = $queue[0]; + $this->assertEquals('custom', $event['kind']); + $this->assertEquals('eventkey', $event['key']); + $this->assertEquals($user, $event['user']); + $this->assertEquals($data, $event['data']); + $this->assertEquals($metricValue, $event['metricValue']); + } + public function testOnlyValidFeatureRequester() { $this->setExpectedException(InvalidArgumentException::class); diff --git a/tests/MockEventProcessor.php b/tests/MockEventProcessor.php new file mode 100644 index 000000000..542a4208a --- /dev/null +++ b/tests/MockEventProcessor.php @@ -0,0 +1,27 @@ +_events = array(); + } + + public function enqueue($event) + { + $this->_events[] = $event; + return true; + } + + public function flush() + { + } + + public function getEvents() + { + return $this->_events; + } +}