Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
add new version of allFlags() that captures more metadata
  • Loading branch information
eli-darkly committed Aug 20, 2018
commit 0d4fc33e9a381e4efe8eccd0ba3227061fd89cec
2 changes: 1 addition & 1 deletion src/LaunchDarkly/EvalResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function __construct($variation, $value, array $prerequisiteEvents)
}

/**
* @return int
* @return int | null
*/
public function getVariation()
{
Expand Down
34 changes: 32 additions & 2 deletions src/LaunchDarkly/FeatureFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ class FeatureFlag
protected $_variations = array();
/** @var bool */
protected $_deleted = false;
/** @var bool */
protected $_trackEvents = false;
/** @var int | null */
protected $_debugEventsUntilDate = null;
// Note, trackEvents and debugEventsUntilDate are not used in EventProcessor, because
// the PHP client doesn't do summary events. However, we need to capture them in case
// they want to pass the flag data to the front end with allFlagsState().

protected function __construct($key,
$version,
Expand All @@ -38,7 +45,9 @@ protected function __construct($key,
$fallthrough,
$offVariation,
array $variations,
$deleted)
$deleted,
$trackEvents,
$debugEventsUntilDate)
{
$this->_key = $key;
$this->_version = $version;
Expand All @@ -51,6 +60,8 @@ protected function __construct($key,
$this->_offVariation = $offVariation;
$this->_variations = $variations;
$this->_deleted = $deleted;
$this->_trackEvents = $trackEvents;
$this->_debugEventsUntilDate = $debugEventsUntilDate;
}

public static function getDecoder()
Expand All @@ -67,7 +78,10 @@ public static function getDecoder()
call_user_func(VariationOrRollout::getDecoder(), $v['fallthrough']),
$v['offVariation'],
$v['variations'] ?: [],
$v['deleted']);
$v['deleted'],
$v['trackEvents'],
$v['debugEventsUntilDate']
);
};
}

Expand Down Expand Up @@ -222,4 +236,20 @@ public function isDeleted()
{
return $this->_deleted;
}

/**
* @return boolean
*/
public function isTrackEvents()
{
return $this->_trackEvents;
}

/**
* @return int | null
*/
public function getDebugEventsUntilDate()
{
return $this->_debugEventsUntilDate;
}
}
90 changes: 90 additions & 0 deletions src/LaunchDarkly/FeatureFlagsState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
namespace LaunchDarkly;

/**
* A snapshot of the state of all feature flags with regard to a specific user, generated by
* calling LDClient.allFlagsState().
*/
class FeatureFlagsState
{
/** @var bool */
protected $_valid = false;

/** @var array */
protected $_flagValues;

/** @var array */
protected $_flagMetadata;

public function __construct($valid, $flagValues = array(), $flagMetadata = array())
{
$this->_valid = $valid;
$this->_flagValues = array();
$this->_flagMetadata = array();
}

/**
* Used internally to build the state map.
*/
public function addFlag($flag, $evalResult)
{
$this->_flagValues[$flag->getKey()] = $evalResult->getValue();
$meta = array();
if (!is_null($evalResult->getVariation())) {
$meta['variation'] = $evalResult->getVariation();
}
$meta['version'] = $flag->getVersion();
$meta['trackEvents'] = $flag->isTrackEvents();
if ($flag->getDebugEventsUntilDate()) {
$meta['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate();
}
$this->_flagMetadata[$flag->getKey()] = $meta;
}

/**
* Returns true if this object contains a valid snapshot of feature flag state, or false if the
* state could not be computed (for instance, because the client was offline or there was no user).
* @return bool true if the state is valid
*/
public function isValid()
{
return $this->_valid;
}

/**
* Returns the value of an individual feature flag at the time the state was recorded.
* @param $key string
* @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)
{
return $this->_flagValues[$key];
}

/**
* 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.
* <p>
* Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
* Instead, use toJson().
* @return array an associative array of flag keys to JSON values
*/
public function toValuesMap()
{
return $this->_flagValues;
}

/**
* Returns a JSON representation of the entire state map (as an associative array), in the format used
* by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end in
* order to "bootstrap" the JavaScript client.
*
* @return array an associative array suitable for passing as a JSON object
*/
public function toJson()
{
$ret = array_replace([], $this->_flagValues);
$ret['$flagsState'] = $this->_flagMetadata;
return $ret;
}
}
47 changes: 33 additions & 14 deletions src/LaunchDarkly/LDClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,37 +261,56 @@ public function identify($user)
* <p>
* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service.
*
* @deprecated Use allFlagsState() instead. Current versions of the client-side SDK will not
* generate analytics events correctly if you pass the result of allFlags().
* @param $user LDUser the end user requesting the feature flags
* @return array()|null Mapping of feature flag keys to their evaluated results for $user
*/
public function allFlags($user)
{
if (is_null($user) || is_null($user->getKey())) {
$this->_logger->warn("allFlags called with null user or null/empty user key! Returning null");
$state = $this->allFlagsState($user);
if (!$state->isValid()) {
return null;
}
return $state->toValuesMap();
}

/**
* Returns an object that encapsulates the state of all feature flags for a given user, including the flag
* values and also metadata that can be used on the front end. This method does not send analytics events
* back to LaunchDarkly.
* <p>
* The most common use case for this method is to bootstrap a set of client-side feature flags from a back-end service.
* To convert the state object into a JSON data structure, call its toJson() method.
*
* @param $user LDUser the end user requesting the feature flags
* @return FeatureFlagsState a FeatureFlagsState object (will never be null; see FeatureFlagsState.isValid())
*/
public function allFlagsState($user)
{
if (is_null($user) || is_null($user->getKey())) {
$this->_logger->warn("allFlagsState called with null user or null/empty user key! Returning empty state");
return new FeatureFlagsState(false);
}
if ($this->isOffline()) {
return null;
return new FeatureFlagsState(false);
}
try {
$flags = $this->_featureRequester->getAllFeatures();
} catch (UnrecoverableHTTPStatusException $e) {
$this->handleUnrecoverableError();
return null;
return new FeatureFlagsState(false);
}
if ($flags === null) {
return null;
return new FeatureFlagsState(false);
}

/**
* @param $flag FeatureFlag
* @return mixed|null
*/
$eval = function ($flag) use ($user) {
return $flag->evaluate($user, $this->_featureRequester)->getValue();
};

return array_map($eval, $flags);
$state = new FeatureFlagsState(true);
foreach ($flags as $key => $flag) {
$result = $flag->evaluate($user, $this->_featureRequester);
$state->addFlag($flag, $result);
}
return $state;
}

/** Generates an HMAC sha256 hash for use in Secure mode: https://github.com/launchdarkly/js-client#secure-mode
Expand Down
117 changes: 111 additions & 6 deletions tests/LDClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
namespace LaunchDarkly\Tests;

use InvalidArgumentException;
use LaunchDarkly\FeatureFlag;
use LaunchDarkly\FeatureRequester;
use LaunchDarkly\LDClient;
use LaunchDarkly\LDUser;
Expand All @@ -15,9 +16,38 @@ public function testDefaultCtor()
$this->assertInstanceOf(LDClient::class, new LDClient("BOGUS_SDK_KEY"));
}

public function testToggleDefault()
public function testVariationReturnsFlagValue()
{
MockFeatureRequester::$val = null;
$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' => ''
);
$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();
$value = $client->variation('feature', $user, 'default');
$this->assertEquals('off', $value);
}

public function testVariationReturnsDefaultForUnknownFlag()
{
MockFeatureRequester::$flags = array();
$client = new LDClient("someKey", array(
'feature_requester_class' => MockFeatureRequester::class,
'events' => false
Expand All @@ -28,9 +58,9 @@ public function testToggleDefault()
$this->assertEquals('argdef', $client->variation('foo', $user, 'argdef'));
}

public function testToggleFromArray()
public function testVariationReturnsDefaultFromConfigurationForUnknownFlag()
{
MockFeatureRequester::$val = null;
MockFeatureRequester::$flags = array();
$client = new LDClient("someKey", array(
'feature_requester_class' => MockFeatureRequester::class,
'events' => false,
Expand All @@ -42,9 +72,9 @@ public function testToggleFromArray()
$this->assertEquals('fromarray', $client->variation('foo', $user, 'argdef'));
}

public function testToggleEvent()
public function testVariationSendsEvent()
{
MockFeatureRequester::$val = null;
MockFeatureRequester::$flags = array();
$client = new LDClient("someKey", array(
'feature_requester_class' => MockFeatureRequester::class,
'events' => true
Expand All @@ -58,6 +88,81 @@ public function testToggleEvent()
$this->assertEquals(1, sizeof($queue));
}

public function testAllFlagsReturnsFlagValues()
{
$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' => ''
);
$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();
$values = $client->allFlags($user);

$this->assertEquals(array('feature' => 'off'), $values);
}

public function testAllFlagsStateReturnsState()
{
$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);

$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
)
)
);
$this->assertEquals($expectedState, $state->toJson());
}

public function testOnlyValidFeatureRequester()
{
$this->setExpectedException(InvalidArgumentException::class);
Expand Down
Loading