Skip to content

Commit 1dc3408

Browse files
authored
Merge pull request #38 from launchdarkly/eb/ch32307/experiment
add experimentation event overrides for rules and fallthrough
2 parents 9c589d1 + 1f18eb0 commit 1dc3408

File tree

8 files changed

+322
-98
lines changed

8 files changed

+322
-98
lines changed

src/LaunchDarkly/FeatureFlag.php

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class FeatureFlag
2929
protected $_deleted = false;
3030
/** @var bool */
3131
protected $_trackEvents = false;
32+
/** @var bool */
33+
protected $_trackEventsFallthrough = false;
3234
/** @var int | null */
3335
protected $_debugEventsUntilDate = null;
3436
/** @var bool */
@@ -50,6 +52,7 @@ protected function __construct($key,
5052
array $variations,
5153
$deleted,
5254
$trackEvents,
55+
$trackEventsFallthrough,
5356
$debugEventsUntilDate,
5457
$clientSide)
5558
{
@@ -65,6 +68,7 @@ protected function __construct($key,
6568
$this->_variations = $variations;
6669
$this->_deleted = $deleted;
6770
$this->_trackEvents = $trackEvents;
71+
$this->_trackEventsFallthrough = $trackEventsFallthrough;
6872
$this->_debugEventsUntilDate = $debugEventsUntilDate;
6973
$this->_clientSide = $clientSide;
7074
}
@@ -85,6 +89,7 @@ public static function getDecoder()
8589
$v['variations'] ?: [],
8690
$v['deleted'],
8791
isset($v['trackEvents']) && $v['trackEvents'],
92+
isset($v['trackEventsFallthrough']) && $v['trackEventsFallthrough'],
8893
isset($v['debugEventsUntilDate']) ? $v['debugEventsUntilDate'] : null,
8994
isset($v['clientSide']) && $v['clientSide']
9095
);
@@ -104,30 +109,30 @@ public function isOn()
104109
/**
105110
* @param LDUser $user
106111
* @param FeatureRequester $featureRequester
107-
* @param bool $includeReasonsInEvents
112+
* @param Impl\EventFactory $eventFactory
108113
* @return EvalResult
109114
*/
110-
public function evaluate($user, $featureRequester, $includeReasonsInEvents = false)
115+
public function evaluate($user, $featureRequester, $eventFactory)
111116
{
112117
$prereqEvents = array();
113-
$detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $includeReasonsInEvents);
118+
$detail = $this->evaluateInternal($user, $featureRequester, $prereqEvents, $eventFactory);
114119
return new EvalResult($detail, $prereqEvents);
115120
}
116121

117122
/**
118123
* @param LDUser $user
119124
* @param FeatureRequester $featureRequester
120125
* @param array $events
121-
* @param bool $includeReasonsInEvents
126+
* @param Impl\EventFactory $eventFactory
122127
* @return EvaluationDetail
123128
*/
124-
private function evaluateInternal($user, $featureRequester, &$events, $includeReasonsInEvents)
129+
private function evaluateInternal($user, $featureRequester, &$events, $eventFactory)
125130
{
126131
if (!$this->isOn()) {
127132
return $this->getOffValue(EvaluationReason::off());
128133
}
129134

130-
$prereqFailureReason = $this->checkPrerequisites($user, $featureRequester, $events, $includeReasonsInEvents);
135+
$prereqFailureReason = $this->checkPrerequisites($user, $featureRequester, $events, $eventFactory);
131136
if ($prereqFailureReason !== null) {
132137
return $this->getOffValue($prereqFailureReason);
133138
}
@@ -158,10 +163,10 @@ private function evaluateInternal($user, $featureRequester, &$events, $includeRe
158163
* @param LDUser $user
159164
* @param FeatureRequester $featureRequester
160165
* @param array $events
161-
* @param bool $includeReasonsInEvents
166+
* @param Impl\EventFactory $eventFactory
162167
* @return EvaluationReason|null
163168
*/
164-
private function checkPrerequisites($user, $featureRequester, &$events, $includeReasonsInEvents)
169+
private function checkPrerequisites($user, $featureRequester, &$events, $eventFactory)
165170
{
166171
if ($this->_prerequisites != null) {
167172
foreach ($this->_prerequisites as $prereq) {
@@ -172,16 +177,12 @@ private function checkPrerequisites($user, $featureRequester, &$events, $include
172177
if ($prereqFeatureFlag == null) {
173178
$prereqOk = false;
174179
} else {
175-
$prereqEvalResult = $prereqFeatureFlag->evaluateInternal($user, $featureRequester, $events, $includeReasonsInEvents);
180+
$prereqEvalResult = $prereqFeatureFlag->evaluateInternal($user, $featureRequester, $events, $eventFactory);
176181
$variation = $prereq->getVariation();
177182
if (!$prereqFeatureFlag->isOn() || $prereqEvalResult->getVariationIndex() !== $variation) {
178183
$prereqOk = false;
179184
}
180-
array_push($events, Util::newFeatureRequestEvent($prereq->getKey(), $user,
181-
$prereqEvalResult->getVariationIndex(), $prereqEvalResult->getValue(),
182-
null, $prereqFeatureFlag->getVersion(), $this->_key,
183-
($includeReasonsInEvents && $prereqEvalResult) ? $prereqEvalResult->getReason() : null
184-
));
185+
array_push($events, $eventFactory->newEvalEvent($prereqFeatureFlag, $user, $prereqEvalResult, null, $this));
185186
}
186187
} catch (EvaluationException $e) {
187188
$prereqOk = false;
@@ -258,6 +259,14 @@ public function isDeleted()
258259
return $this->_deleted;
259260
}
260261

262+
/**
263+
* @return array
264+
*/
265+
public function getRules()
266+
{
267+
return $this->_rules;
268+
}
269+
261270
/**
262271
* @return boolean
263272
*/
@@ -266,6 +275,14 @@ public function isTrackEvents()
266275
return $this->_trackEvents;
267276
}
268277

278+
/**
279+
* @return boolean
280+
*/
281+
public function isTrackEventsFallthrough()
282+
{
283+
return $this->_trackEventsFallthrough;
284+
}
285+
269286
/**
270287
* @return int | null
271288
*/
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
namespace LaunchDarkly\Impl;
3+
4+
use LaunchDarkly\Util;
5+
6+
class EventFactory
7+
{
8+
/** @var boolean */
9+
private $_withReasons;
10+
11+
public function __construct($withReasons)
12+
{
13+
$this->_withReasons = $withReasons;
14+
}
15+
16+
public function newEvalEvent($flag, $user, $detail, $default, $prereqOfFlag = null)
17+
{
18+
$addExperimentData = static::isExperiment($flag, $detail->getReason());
19+
$e = array(
20+
'kind' => 'feature',
21+
'creationDate' => Util::currentTimeUnixMillis(),
22+
'key' => $flag->getKey(),
23+
'user' => $user,
24+
'variation' => $detail->getVariationIndex(),
25+
'value' => $detail->getValue(),
26+
'default' => $default,
27+
'version' => $flag->getVersion()
28+
);
29+
// the following properties are handled separately so we don't waste bandwidth on unused keys
30+
if ($addExperimentData || $flag->isTrackEvents()) {
31+
$e['trackEvents'] = true;
32+
}
33+
if ($flag->getDebugEventsUntilDate()) {
34+
$e['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate();
35+
}
36+
if ($prereqOfFlag) {
37+
$e['prereqOf'] = $prereqOfFlag->getKey();
38+
}
39+
if (($addExperimentData || $this->_withReasons) && $detail->getReason()) {
40+
$e['reason'] = $detail->getReason()->jsonSerialize();
41+
}
42+
return $e;
43+
}
44+
45+
public function newDefaultEvent($flag, $user, $detail)
46+
{
47+
$e = array(
48+
'kind' => 'feature',
49+
'creationDate' => Util::currentTimeUnixMillis(),
50+
'key' => $flag->getKey(),
51+
'user' => $user,
52+
'value' => $detail->getValue(),
53+
'default' => $detail->getValue(),
54+
'version' => $flag->getVersion()
55+
);
56+
// the following properties are handled separately so we don't waste bandwidth on unused keys
57+
if ($flag->isTrackEvents()) {
58+
$e['trackEvents'] = true;
59+
}
60+
if ($flag->getDebugEventsUntilDate()) {
61+
$e['debugEventsUntilDate'] = $flag->getDebugEventsUntilDate();
62+
}
63+
if ($this->_withReasons && $detail->getReason()) {
64+
$e['reason'] = $detail->getReason()->jsonSerialize();
65+
}
66+
return $e;
67+
}
68+
69+
public function newUnknownFlagEvent($key, $user, $detail)
70+
{
71+
$e = array(
72+
'kind' => 'feature',
73+
'creationDate' => Util::currentTimeUnixMillis(),
74+
'key' => $key,
75+
'user' => $user,
76+
'value' => $detail->getValue(),
77+
'default' => $detail->getValue()
78+
);
79+
// the following properties are handled separately so we don't waste bandwidth on unused keys
80+
if ($this->_withReasons && $detail->getReason()) {
81+
$e['reason'] = $detail->getReason()->jsonSerialize();
82+
}
83+
return $e;
84+
}
85+
86+
public function newIdentifyEvent($user)
87+
{
88+
return array(
89+
'kind' => 'identify',
90+
'creationDate' => Util::currentTimeUnixMillis(),
91+
'key' => $user->getKey(),
92+
'user' => $user
93+
);
94+
}
95+
96+
public function newCustomEvent($eventName, $user, $data)
97+
{
98+
$e = array(
99+
'kind' => 'custom',
100+
'creationDate' => Util::currentTimeUnixMillis(),
101+
'key' => $eventName,
102+
'user' => $user
103+
);
104+
if (isset($data)) {
105+
$e['data'] = $data;
106+
}
107+
return $e;
108+
}
109+
110+
private static function isExperiment($flag, $reason)
111+
{
112+
if ($reason) {
113+
switch ($reason->getKind()) {
114+
case 'RULE_MATCH':
115+
$i = $reason->getRuleIndex();
116+
$rules = $flag->getRules();
117+
return isset($i) && $i >= 0 && $i < count($rules) && $rules[$i]->isTrackEvents();
118+
case 'FALLTHROUGH':
119+
return $flag->isTrackEventsFallthrough();
120+
}
121+
}
122+
return false;
123+
}
124+
}

src/LaunchDarkly/LDClient.php

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22
namespace LaunchDarkly;
33

4+
use LaunchDarkly\Impl\EventFactory;
45
use LaunchDarkly\Integrations\Guzzle;
56
use Monolog\Handler\ErrorLogHandler;
67
use Monolog\Logger;
@@ -33,6 +34,10 @@ class LDClient
3334
protected $_logger;
3435
/** @var FeatureRequester */
3536
protected $_featureRequester;
37+
/** @var EventFactory */
38+
protected $_eventFactoryDefault;
39+
/** @var EventFactory */
40+
protected $_eventFactoryWithReasons;
3641

3742
/**
3843
* Creates a new client instance that connects to LaunchDarkly.
@@ -99,6 +104,9 @@ public function __construct($sdkKey, $options = array())
99104
}
100105
$this->_logger = $options['logger'];
101106

107+
$this->_eventFactoryDefault = new EventFactory(false);
108+
$this->_eventFactoryWithReasons = new EventFactory(true);
109+
102110
$this->_eventProcessor = new EventProcessor($sdkKey, $options);
103111

104112
$this->_featureRequester = $this->getFeatureRequester($sdkKey, $options);
@@ -141,7 +149,7 @@ private function getFeatureRequester($sdkKey, array $options)
141149
*/
142150
public function variation($key, $user, $default = false)
143151
{
144-
$detail = $this->variationDetailInternal($key, $user, $default, false);
152+
$detail = $this->variationDetailInternal($key, $user, $default, $this->_eventFactoryDefault);
145153
return $detail->getValue();
146154
}
147155

@@ -159,29 +167,31 @@ public function variation($key, $user, $default = false)
159167
*/
160168
public function variationDetail($key, $user, $default = false)
161169
{
162-
return $this->variationDetailInternal($key, $user, $default, true);
170+
return $this->variationDetailInternal($key, $user, $default, $this->_eventFactoryWithReasons);
163171
}
164172

165173
/**
166174
* @param string $key
167175
* @param LDUser $user
168176
* @param mixed $default
169-
* @param bool $includeReasonsInEvents
177+
* @param EventFactory $eventFactory
170178
*/
171-
private function variationDetailInternal($key, $user, $default, $includeReasonsInEvents)
179+
private function variationDetailInternal($key, $user, $default, $eventFactory)
172180
{
173181
$default = $this->_get_default($key, $default);
174182

175183
$errorResult = function ($errorKind) use ($key, $default) {
176184
return new EvaluationDetail($default, null, EvaluationReason::error($errorKind));
177185
};
178-
$sendEvent = function ($detail, $flag) use ($key, $user, $default, $includeReasonsInEvents) {
186+
$sendEvent = function ($detail, $flag) use ($key, $user, $default, $eventFactory) {
179187
if ($this->isOffline() || !$this->_send_events) {
180188
return;
181189
}
182-
$event = Util::newFeatureRequestEvent($key, $user, $detail->getVariationIndex(), $detail->getValue(),
183-
$default, $flag ? $flag->getVersion() : null, null,
184-
$includeReasonsInEvents ? $detail->getReason() : null);
190+
if ($flag) {
191+
$event = $eventFactory->newEvalEvent($flag, $user, $detail, $default);
192+
} else {
193+
$event = $eventFactory->newUnknownFlagEvent($key, $user, $detail);
194+
}
185195
$this->_eventProcessor->enqueue($event);
186196
};
187197

@@ -211,7 +221,7 @@ private function variationDetailInternal($key, $user, $default, $includeReasonsI
211221
$this->_logger->warning("Variation called with null user or null user key! Returning default value");
212222
return $result;
213223
}
214-
$evalResult = $flag->evaluate($user, $this->_featureRequester, $includeReasonsInEvents);
224+
$evalResult = $flag->evaluate($user, $this->_featureRequester, $eventFactory);
215225
if (!$this->isOffline() && $this->_send_events) {
216226
foreach ($evalResult->getPrerequisiteEvents() as $e) {
217227
$this->_eventProcessor->enqueue($e);
@@ -271,16 +281,7 @@ public function track($eventName, $user, $data)
271281
if (is_null($user) || $user->isKeyBlank()) {
272282
$this->_logger->warning("Track called with null user or null/empty user key!");
273283
}
274-
275-
$event = array();
276-
$event['user'] = $user;
277-
$event['kind'] = "custom";
278-
$event['creationDate'] = Util::currentTimeUnixMillis();
279-
$event['key'] = $eventName;
280-
if (isset($data)) {
281-
$event['data'] = $data;
282-
}
283-
$this->_eventProcessor->enqueue($event);
284+
$this->_eventProcessor->enqueue($this->_eventFactoryDefault->newCustomEvent($eventName, $user, $data));
284285
}
285286

286287
/**
@@ -294,13 +295,7 @@ public function identify($user)
294295
if (is_null($user) || $user->isKeyBlank()) {
295296
$this->_logger->warning("Track called with null user or null/empty user key!");
296297
}
297-
298-
$event = array();
299-
$event['user'] = $user;
300-
$event['kind'] = "identify";
301-
$event['creationDate'] = Util::currentTimeUnixMillis();
302-
$event['key'] = $user->getKey();
303-
$this->_eventProcessor->enqueue($event);
298+
$this->_eventProcessor->enqueue($this->_eventFactoryDefault->newIdentifyEvent($user));
304299
}
305300

306301
/** Returns an array mapping Feature Flag keys to their evaluated results for a given user.
@@ -368,7 +363,7 @@ public function allFlagsState($user, $options = array())
368363
if ($clientOnly && !$flag->isClientSide()) {
369364
continue;
370365
}
371-
$result = $flag->evaluate($user, $preloadedRequester);
366+
$result = $flag->evaluate($user, $preloadedRequester, $this->_eventFactoryDefault);
372367
$state->addFlag($flag, $result->getDetail(), $withReasons, $detailsOnlyIfTracked);
373368
}
374369
return $state;

0 commit comments

Comments
 (0)