Skip to content

Commit d033ec3

Browse files
Gracefully handle empty entity IDs in traffic allocation (#35)
1 parent 029b1f5 commit d033ec3

File tree

5 files changed

+73
-21
lines changed

5 files changed

+73
-21
lines changed

CONTRIBUTING.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
#Contributing to the Optimizely PHP SDK
1+
# Contributing to the Optimizely PHP SDK
22
We welcome contributions and feedback! All contributors must sign our [Contributor License Agreement (CLA)](https://docs.google.com/a/optimizely.com/forms/d/e/1FAIpQLSf9cbouWptIpMgukAKZZOIAhafvjFCV8hS00XJLWQnWDFtwtA/viewform) to be eligible to contribute. Please read the [README](README.md) to set up your development environment, then read the guidelines below for information on submitting your code.
33

4-
##Development process
4+
## Development process
55

66
1. Create a branch off of `devel`: `git checkout -b YOUR_NAME/branch_name`.
77
2. Commit your changes. Make sure to add tests!
@@ -10,12 +10,12 @@ We welcome contributions and feedback! All contributors must sign our [Contribut
1010
5. Open a pull request from `YOUR_NAME/branch_name` to `devel`.
1111
6. A repository maintainer will review your pull request and, if all goes well, merge it!
1212

13-
##Pull request acceptance criteria
13+
## Pull request acceptance criteria
1414

1515
* **All code must have test coverage.** We use PHPUnit. Changes in functionality should have accompanying unit tests. Bug fixes should have accompanying regression tests.
1616
* Tests are located in `tests` with one file per class.
1717

18-
##License
18+
## License
1919

2020
All contributions are under the CLA mentioned above. For this project, Optimizely uses the Apache 2.0 license, and so asks that by contributing your code, you agree to license your contribution under the terms of the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). Your contributions should also include the following header:
2121

@@ -39,5 +39,5 @@ All contributions are under the CLA mentioned above. For this project, Optimizel
3939

4040
The YEAR above should be the year of the contribution. If work on the file has been done over multiple years, list each year in the section above. Example: Optimizely writes the file and releases it in 2014. No changes are made in 2015. Change made in 2016. YEAR should be “2014, 2016”.
4141

42-
##Contact
42+
## Contact
4343
If you have questions, please contact [email protected].

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,35 @@
1-
#Optimizely PHP SDK
1+
# Optimizely PHP SDK
22
[![Build Status](https://travis-ci.org/optimizely/php-sdk.svg?branch=master)](https://travis-ci.org/optimizely/php-sdk)
33
[![Coverage Status](https://coveralls.io/repos/github/optimizely/php-sdk/badge.svg?branch=master)](https://coveralls.io/github/optimizely/php-sdk?branch=master)
44
[![Total Downloads](https://poser.pugx.org/optimizely/optimizely-sdk/downloads)](https://packagist.org/packages/optimizely/optimizely-sdk)
55
[![Apache 2.0](https://img.shields.io/github/license/nebula-plugins/gradle-extra-configurations-plugin.svg)](http://www.apache.org/licenses/LICENSE-2.0)
66

77
This repository houses the PHP SDK for Optimizely Full Stack.
88

9-
##Getting Started
9+
## Getting Started
1010

11-
###Installing the SDK
11+
### Installing the SDK
1212

1313
The Optimizely PHP SDK can be installed through [Composer](https://getcomposer.org/). Please use the following command:
1414

1515
```
1616
php composer.phar require optimizely/optimizely-sdk
1717
```
1818

19-
###Using the SDK
19+
### Using the SDK
2020
See the Optimizely Full Stack [developer documentation](https://developers.optimizely.com/x/solutions/sdks/reference/?language=php) to learn how to set up your first Full Stack project and use the SDK.
2121

22-
##Development
22+
## Development
2323

24-
###Unit tests
24+
### Unit tests
2525

26-
#####Running all tests
26+
##### Running all tests
2727
You can run all unit tests with:
2828

2929
```
3030
./vendor/bin/phpunit
3131
```
3232

33-
###Contributing
33+
### Contributing
3434

3535
Please see [CONTRIBUTING](CONTRIBUTING.md).

src/Optimizely/Bucketer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ public function bucket(ProjectConfig $config, Experiment $experiment, $userId)
150150
}
151151

152152
$userExperimentId = $this->findBucket($userId, $group->getId(), $group->getTrafficAllocation());
153-
if (is_null($userExperimentId)) {
153+
if (empty($userExperimentId)) {
154154
$this->_logger->log(Logger::INFO, sprintf('User "%s" is in no experiment.', $userId));
155155
return new Variation();
156156
}
@@ -172,7 +172,7 @@ public function bucket(ProjectConfig $config, Experiment $experiment, $userId)
172172

173173
// Bucket user if not in whitelist and in group (if any).
174174
$variationId = $this->findBucket($userId, $experiment->getId(), $experiment->getTrafficAllocation());
175-
if (!is_null($variationId)) {
175+
if (!empty($variationId)) {
176176
$variation = $config->getVariationFromId($experiment->getKey(), $variationId);
177177
$this->_logger->log(Logger::INFO,
178178
sprintf('User "%s" is in variation %s of experiment %s.',

tests/BucketerTest.php

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,28 @@ public function testGenerateBucketValue()
8282
public function testBucketValidExperimentNotInGroup()
8383
{
8484
$bucketer = new TestBucketer($this->loggerMock);
85-
$bucketer->setBucketValues([3000, 7000, 9000]);
85+
$bucketer->setBucketValues([1000, 3000, 7000, 9000]);
8686
// Total calls in this test
87-
$this->loggerMock->expects($this->exactly(6))
87+
$this->loggerMock->expects($this->exactly(8))
8888
->method('log');
8989

90+
// No variation (empty entity ID)
91+
$this->loggerMock->expects($this->at(0))
92+
->method('log')
93+
->with(Logger::DEBUG, 'Assigned bucket 1000 to user "testUserId".');
94+
$this->loggerMock->expects($this->at(1))
95+
->method('log')
96+
->with(Logger::INFO, 'User "testUserId" is in no variation.');
97+
98+
$this->assertEquals(
99+
new Variation(),
100+
$bucketer->bucket(
101+
$this->config,
102+
$this->config->getExperimentFromKey('test_experiment'),
103+
$this->testUserId
104+
)
105+
);
106+
90107
// control
91108
$this->loggerMock->expects($this->at(0))
92109
->method('log')
@@ -145,10 +162,10 @@ public function testBucketValidExperimentInGroup()
145162
{
146163
$bucketer = new TestBucketer($this->loggerMock);
147164
// Total calls in this test
148-
$this->loggerMock->expects($this->exactly(10))
165+
$this->loggerMock->expects($this->exactly(14))
149166
->method('log');
150167

151-
// group_experiment_1 (20% experiment)
168+
// group_experiment_1 (15% experiment)
152169
// variation 1
153170
$bucketer->setBucketValues([1000, 4000]);
154171
$this->loggerMock->expects($this->at(0))
@@ -216,6 +233,41 @@ public function testBucketValidExperimentInGroup()
216233
$this->testUserId
217234
)
218235
);
236+
237+
// User not in any experiment (previously allocated space)
238+
$bucketer->setBucketValues([400]);
239+
$this->loggerMock->expects($this->at(0))
240+
->method('log')
241+
->with(Logger::DEBUG, 'Assigned bucket 400 to user "testUserId".');
242+
$this->loggerMock->expects($this->at(1))
243+
->method('log')
244+
->with(Logger::INFO, 'User "testUserId" is in no experiment.');
245+
246+
$this->assertEquals(
247+
new Variation(),
248+
$bucketer->bucket(
249+
$this->config,
250+
$this->config->getExperimentFromKey('group_experiment_1'),
251+
$this->testUserId
252+
)
253+
);
254+
255+
// User not in any experiment (never allocated space)
256+
$bucketer->setBucketValues([9000]);
257+
$this->loggerMock->expects($this->at(0))
258+
->method('log')
259+
->with(Logger::DEBUG, 'Assigned bucket 9000 to user "testUserId".');
260+
$this->loggerMock->expects($this->at(1))
261+
->method('log')
262+
->with(Logger::INFO, 'User "testUserId" is in no experiment.');
263+
$this->assertEquals(
264+
new Variation(),
265+
$bucketer->bucket(
266+
$this->config,
267+
$this->config->getExperimentFromKey('group_experiment_1'),
268+
$this->testUserId
269+
)
270+
);
219271
}
220272

221273
public function testBucketInvalidExperiment()

tests/TestData.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
define('DATAFILE',
2525
'{"experiments": [{"status": "Running", "key": "test_experiment", "layerId": "7719770039",
26-
"trafficAllocation": [{"entityId": "7722370027", "endOfRange": 4000},
26+
"trafficAllocation": [{"entityId": "", "endOfRange": 1500}, {"entityId": "7722370027", "endOfRange": 4000},
2727
{"entityId": "7721010009", "endOfRange": 8000}], "audienceIds": ["7718080042"],
2828
"variations": [{"id": "7722370027", "key": "control"}, {"id": "7721010009", "key": "variation"}],
2929
"forcedVariations": {"user1": "control"}, "id": "7716830082"}, {"status": "Paused", "key": "paused_experiment", "layerId": "7719779139",
@@ -32,7 +32,7 @@
3232
"variations": [{"id": "7722370427", "key": "control"}, {"id": "7721010509", "key": "variation"}],
3333
"forcedVariations": {}, "id": "7716830585"}], "version": "2",
3434
"audiences": [{"conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"device_type\", \"type\": \"custom_attribute\", \"value\": \"iPhone\"}]], [\"or\", [\"or\", {\"name\": \"location\", \"type\": \"custom_attribute\", \"value\": \"San Francisco\"}]]]", "id": "7718080042", "name": "iPhone users in San Francisco"}],
35-
"groups": [{"policy": "random", "trafficAllocation": [{"entityId": "7723330021", "endOfRange": 2000}, {"entityId": "7718750065", "endOfRange": 6000}], "experiments": [{"status": "Running", "key": "group_experiment_1", "layerId": "7721010011", "trafficAllocation": [{"entityId": "7722260071", "endOfRange": 5000}, {"entityId": "7722360022", "endOfRange": 10000}], "audienceIds": [], "variations": [{"id": "7722260071", "key": "group_exp_1_var_1"}, {"id": "7722360022", "key": "group_exp_1_var_2"}], "forcedVariations": {"user1": "group_exp_1_var_1"}, "id": "7723330021"}, {"status": "Running", "key": "group_experiment_2", "layerId": "7721020020", "trafficAllocation": [{"entityId": "7713030086", "endOfRange": 5000}, {"entityId": "7725250007", "endOfRange": 10000}], "audienceIds": [],
35+
"groups": [{"policy": "random", "trafficAllocation": [{"entityId": "", "endOfRange": 500}, {"entityId": "7723330021", "endOfRange": 2000}, {"entityId": "7718750065", "endOfRange": 6000}], "experiments": [{"status": "Running", "key": "group_experiment_1", "layerId": "7721010011", "trafficAllocation": [{"entityId": "7722260071", "endOfRange": 5000}, {"entityId": "7722360022", "endOfRange": 10000}], "audienceIds": [], "variations": [{"id": "7722260071", "key": "group_exp_1_var_1"}, {"id": "7722360022", "key": "group_exp_1_var_2"}], "forcedVariations": {"user1": "group_exp_1_var_1"}, "id": "7723330021"}, {"status": "Running", "key": "group_experiment_2", "layerId": "7721020020", "trafficAllocation": [{"entityId": "7713030086", "endOfRange": 5000}, {"entityId": "7725250007", "endOfRange": 10000}], "audienceIds": [],
3636
"variations": [{"id": "7713030086", "key": "group_exp_2_var_1"}, {"id": "7725250007", "key": "group_exp_2_var_2"}], "forcedVariations": {}, "id": "7718750065"}], "id": "7722400015"}],
3737
"attributes": [{"id": "7723280020", "key": "device_type"}, {"id": "7723340004", "key": "location"}],
3838
"projectId": "7720880029", "accountId": "1592310167",

0 commit comments

Comments
 (0)