Skip to content
This repository was archived by the owner on Jun 8, 2022. It is now read-only.

Commit 3bb1d06

Browse files
prepare 3.9.0 release (launchdarkly#148)
1 parent e47e184 commit 3bb1d06

14 files changed

+566
-26
lines changed

src/LaunchDarkly/EvaluationReason.php

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,9 @@ public static function off()
100100
* Creates a new instance of the FALLTHROUGH reason.
101101
* @return EvaluationReason
102102
*/
103-
public static function fallthrough()
103+
public static function fallthrough($inExperiment = false)
104104
{
105-
return new EvaluationReason(self::FALLTHROUGH);
105+
return new EvaluationReason(self::FALLTHROUGH, null, null, null, null, $inExperiment);
106106
}
107107

108108
/**
@@ -118,9 +118,9 @@ public static function targetMatch()
118118
* Creates a new instance of the RULE_MATCH reason.
119119
* @return EvaluationReason
120120
*/
121-
public static function ruleMatch($ruleIndex, $ruleId)
121+
public static function ruleMatch($ruleIndex, $ruleId, $inExperiment = false)
122122
{
123-
return new EvaluationReason(self::RULE_MATCH, null, $ruleIndex, $ruleId);
123+
return new EvaluationReason(self::RULE_MATCH, null, $ruleIndex, $ruleId, null, $inExperiment);
124124
}
125125

126126
/**
@@ -141,13 +141,14 @@ public static function error($errorKind)
141141
return new EvaluationReason(self::ERROR, $errorKind);
142142
}
143143

144-
private function __construct($kind, $errorKind = null, $ruleIndex = null, $ruleId = null, $prerequisiteKey = null)
144+
private function __construct($kind, $errorKind = null, $ruleIndex = null, $ruleId = null, $prerequisiteKey = null, $inExperiment = null)
145145
{
146146
$this->_kind = $kind;
147147
$this->_errorKind = $errorKind;
148148
$this->_ruleIndex = $ruleIndex;
149149
$this->_ruleId = $ruleId;
150150
$this->_prerequisiteKey = $prerequisiteKey;
151+
$this->_inExperiment = $inExperiment;
151152
}
152153

153154
/**
@@ -199,6 +200,16 @@ public function getPrerequisiteKey()
199200
return $this->_prerequisiteKey;
200201
}
201202

203+
/**
204+
* Returns true if the evaluation resulted in an experiment rollout *and* served
205+
* one of the variations in the experiment. Otherwise it returns false.
206+
* @return boolean
207+
*/
208+
public function isInExperiment()
209+
{
210+
return !is_null($this->_inExperiment) && $this->_inExperiment;
211+
}
212+
202213
/**
203214
* Returns a simple string representation of this object.
204215
*/
@@ -235,6 +246,9 @@ public function jsonSerialize()
235246
if ($this->_prerequisiteKey !== null) {
236247
$ret['prerequisiteKey'] = $this->_prerequisiteKey;
237248
}
249+
if ($this->_inExperiment !== null && $this->_inExperiment) {
250+
$ret['inExperiment'] = $this->_inExperiment;
251+
}
238252
return $ret;
239253
}
240254
}

src/LaunchDarkly/FeatureFlag.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,18 @@ private function getOffValue($reason)
236236
*/
237237
private function getValueForVariationOrRollout($r, $user, $reason)
238238
{
239-
$index = $r->variationIndexForUser($user, $this->_key, $this->_salt);
239+
$seed = $r->getRollout() ? $r->getRollout()->getSeed() : null;
240+
list($index, $inExperiment) = $r->variationIndexForUser($user, $this->_key, $this->_salt, $seed);
240241
if ($index === null) {
241242
return new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR));
242243
}
244+
if ($inExperiment) {
245+
if ($reason->getKind() === EvaluationReason::FALLTHROUGH) {
246+
$reason = EvaluationReason::fallthrough(true);
247+
} elseif ($reason->getKind() === EvaluationReason::RULE_MATCH) {
248+
$reason = EvaluationReason::ruleMatch($reason->getRuleIndex(), $reason->getRuleId(), true);
249+
}
250+
}
243251
return $this->getVariation($index, $reason);
244252
}
245253

src/LaunchDarkly/Impl/EventFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ private static function contextKind($user)
148148
private static function isExperiment($flag, $reason)
149149
{
150150
if ($reason) {
151+
if ($reason->isInExperiment()) {
152+
return true;
153+
}
151154
switch ($reason->getKind()) {
152155
case 'RULE_MATCH':
153156
$i = $reason->getRuleIndex();

src/LaunchDarkly/Rollout.php

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,34 @@
1111
*/
1212
class Rollout
1313
{
14+
const KIND_EXPERIMENT = 'experiment';
15+
1416
/** @var WeightedVariation[] */
1517
private $_variations = array();
1618
/** @var string */
1719
private $_bucketBy = null;
20+
/** @var string */
21+
private $_kind = null;
22+
/** @var int|null */
23+
private $_seed = null;
1824

19-
protected function __construct(array $variations, $bucketBy)
25+
protected function __construct(array $variations, $bucketBy, $kind = null, $seed = null)
2026
{
2127
$this->_variations = $variations;
2228
$this->_bucketBy = $bucketBy;
29+
$this->_kind = $kind;
30+
$this->_seed = $seed;
2331
}
2432

2533
public static function getDecoder()
2634
{
2735
return function ($v) {
2836
return new Rollout(
2937
array_map(WeightedVariation::getDecoder(), $v['variations']),
30-
isset($v['bucketBy']) ? $v['bucketBy'] : null);
38+
isset($v['bucketBy']) ? $v['bucketBy'] : null,
39+
isset($v['kind']) ? $v['kind'] : null,
40+
isset($v['seed']) ? $v['seed'] : null
41+
);
3142
};
3243
}
3344

@@ -46,4 +57,20 @@ public function getBucketBy()
4657
{
4758
return $this->_bucketBy;
4859
}
60+
61+
/**
62+
* @return int|null
63+
*/
64+
public function getSeed()
65+
{
66+
return $this->_seed;
67+
}
68+
69+
/**
70+
* @return boolean
71+
*/
72+
public function isExperiment()
73+
{
74+
return $this->_kind === self::KIND_EXPERIMENT;
75+
}
4976
}

src/LaunchDarkly/SegmentRule.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function matchesUser($user, $segmentKey, $segmentSalt)
4848
}
4949
// All of the clauses are met. See if the user buckets in
5050
$bucketBy = ($this->_bucketBy === null) ? "key" : $this->_bucketBy;
51-
$bucket = VariationOrRollout::bucketUser($user, $segmentKey, $bucketBy, $segmentSalt);
51+
$bucket = VariationOrRollout::bucketUser($user, $segmentKey, $bucketBy, $segmentSalt, null);
5252
$weight = $this->_weight / 100000.0;
5353
return $bucket < $weight;
5454
}

src/LaunchDarkly/VariationOrRollout.php

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,41 +54,43 @@ public function getRollout()
5454
* @param $user LDUser
5555
* @param $_key string
5656
* @param $_salt string
57-
* @return int|null
57+
* @return array(int|null, boolean)
5858
*/
5959
public function variationIndexForUser($user, $_key, $_salt)
6060
{
6161
if ($this->_variation !== null) {
62-
return $this->_variation;
62+
return array($this->_variation, false);
6363
}
6464
$rollout = $this->_rollout;
6565
if ($rollout === null) {
66-
return null;
66+
return array(null, false);
6767
}
6868
$variations = $rollout->getVariations();
6969
if ($variations) {
7070
$bucketBy = $this->_rollout->getBucketBy() === null ? "key" : $this->_rollout->getBucketBy();
71-
$bucket = self::bucketUser($user, $_key, $bucketBy, $_salt);
71+
$bucket = self::bucketUser($user, $_key, $bucketBy, $_salt, $rollout->getSeed());
7272
$sum = 0.0;
7373
foreach ($variations as $wv) {
7474
$sum += $wv->getWeight() / 100000.0;
7575
if ($bucket < $sum) {
76-
return $wv->getVariation();
76+
return array($wv->getVariation(), $this->_rollout->isExperiment() && !$wv->isUntracked());
7777
}
7878
}
79-
return $variations[count($variations) - 1]->getVariation();
79+
$lastVariation = $variations[count($variations) - 1];
80+
return array($lastVariation->getVariation(), $this->_rollout->isExperiment() && !$lastVariation->isUntracked());
8081
}
81-
return null;
82+
return array(null, false);
8283
}
8384

8485
/**
8586
* @param $user LDUser
8687
* @param $_key string
8788
* @param $attr string
8889
* @param $_salt string
90+
* @param $seed int|null
8991
* @return float
9092
*/
91-
public static function bucketUser($user, $_key, $attr, $_salt)
93+
public static function bucketUser($user, $_key, $attr, $_salt, $seed)
9294
{
9395
$userValue = $user->getValueForEvaluation($attr);
9496
$idHash = null;
@@ -98,10 +100,15 @@ public static function bucketUser($user, $_key, $attr, $_salt)
98100
}
99101
if (is_string($userValue)) {
100102
$idHash = $userValue;
103+
if (isset($seed)) {
104+
$prefix = (string) $seed;
105+
} else {
106+
$prefix = $_key . "." . $_salt;
107+
}
101108
if ($user->getSecondary() !== null) {
102109
$idHash = $idHash . "." . strval($user->getSecondary());
103110
}
104-
$hash = substr(sha1($_key . "." . $_salt . "." . $idHash), 0, 15);
111+
$hash = substr(sha1($prefix . "." . $idHash), 0, 15);
105112
$longVal = base_convert($hash, 16, 10);
106113
$result = $longVal / self::$LONG_SCALE;
107114

src/LaunchDarkly/WeightedVariation.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,24 @@ class WeightedVariation
1515
private $_variation = null;
1616
/** @var int */
1717
private $_weight = null;
18+
/** @var boolean */
19+
private $_untracked = false;
1820

19-
private function __construct($variation, $weight)
21+
private function __construct($variation, $weight, $untracked)
2022
{
2123
$this->_variation = $variation;
2224
$this->_weight = $weight;
25+
$this->_untracked = $untracked;
2326
}
2427

2528
public static function getDecoder()
2629
{
2730
return function ($v) {
28-
return new WeightedVariation($v['variation'], $v['weight']);
31+
return new WeightedVariation(
32+
$v['variation'],
33+
$v['weight'],
34+
isset($v['untracked']) ? $v['untracked'] : false
35+
);
2936
};
3037
}
3138

@@ -44,4 +51,12 @@ public function getWeight()
4451
{
4552
return $this->_weight;
4653
}
54+
55+
/**
56+
* @return boolean
57+
*/
58+
public function isUntracked()
59+
{
60+
return $this->_untracked;
61+
}
4762
}

tests/EvaluationReasonTest.php

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ public function testFallthroughReasonSerialization()
2121
$this->assertEquals('FALLTHROUGH', (string)$reason);
2222
}
2323

24+
public function testFallthroughReasonNotInExperimentSerialization()
25+
{
26+
$reason = EvaluationReason::fallthrough(false);
27+
$json = json_encode($reason);
28+
$this->assertEquals(array('kind' => 'FALLTHROUGH'), json_decode($json, true));
29+
$this->assertEquals('FALLTHROUGH', (string)$reason);
30+
}
31+
32+
public function testFallthroughReasonInExperimentSerialization()
33+
{
34+
$reason = EvaluationReason::fallthrough(true);
35+
$json = json_encode($reason);
36+
$this->assertEquals(array('kind' => 'FALLTHROUGH', 'inExperiment' => true), json_decode($json, true));
37+
$this->assertEquals('FALLTHROUGH', (string)$reason);
38+
}
39+
2440
public function testTargetMatchReasonSerialization()
2541
{
2642
$reason = EvaluationReason::targetMatch();
@@ -33,17 +49,43 @@ public function testRuleMatchReasonSerialization()
3349
{
3450
$reason = EvaluationReason::ruleMatch(0, 'id');
3551
$json = json_encode($reason);
36-
$this->assertEquals(array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'),
37-
json_decode($json, true));
52+
$this->assertEquals(
53+
array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'),
54+
json_decode($json, true)
55+
);
56+
$this->assertEquals('RULE_MATCH(0,id)', (string)$reason);
57+
}
58+
59+
public function testRuleMatchReasonNotInExperimentSerialization()
60+
{
61+
$reason = EvaluationReason::ruleMatch(0, 'id', false);
62+
$json = json_encode($reason);
63+
$this->assertEquals(
64+
array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'),
65+
json_decode($json, true)
66+
);
67+
$this->assertEquals('RULE_MATCH(0,id)', (string)$reason);
68+
}
69+
70+
public function testRuleMatchReasonInExperimentSerialization()
71+
{
72+
$reason = EvaluationReason::ruleMatch(0, 'id', true);
73+
$json = json_encode($reason);
74+
$this->assertEquals(
75+
array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id', 'inExperiment' => true),
76+
json_decode($json, true)
77+
);
3878
$this->assertEquals('RULE_MATCH(0,id)', (string)$reason);
3979
}
4080

4181
public function testPrerequisiteFailedReasonSerialization()
4282
{
4383
$reason = EvaluationReason::prerequisiteFailed('key');
4484
$json = json_encode($reason);
45-
$this->assertEquals(array('kind' => 'PREREQUISITE_FAILED', 'prerequisiteKey' => 'key'),
46-
json_decode($json, true));
85+
$this->assertEquals(
86+
array('kind' => 'PREREQUISITE_FAILED', 'prerequisiteKey' => 'key'),
87+
json_decode($json, true)
88+
);
4789
$this->assertEquals('PREREQUISITE_FAILED(key)', (string)$reason);
4890
}
4991

0 commit comments

Comments
 (0)