diff --git a/composer.json b/composer.json index f249058..e9161d1 100644 --- a/composer.json +++ b/composer.json @@ -20,14 +20,14 @@ } ], "require": { - "php": "^7.2 | ^8.0", - "illuminate/support": "^6.0 | ^7.0", + "php": "^8.1.1", + "illuminate/support": "^9.2", "jaybizzle/crawler-detect": "^1.2" }, "require-dev": { "orchestra/testbench": "3.9.*", - "phpunit/phpunit": "^8.0 | ^9.0", - "mockery/mockery": "^1.0" + "phpunit/phpunit": "^9.0", + "mockery/mockery": "^1.3.1" }, "autoload": { "psr-4": { diff --git a/config/config.php b/config/config.php index 186fca1..add65b1 100644 --- a/config/config.php +++ b/config/config.php @@ -25,6 +25,28 @@ 'goals' => [], /* |-------------------------------------------------------------------------- + | Percentage + |-------------------------------------------------------------------------- + | + | Percentage by experiment. + | + | Example: [50, 50] + | + */ + 'percentages' => [], + /* + |-------------------------------------------------------------------------- + | Interval + |-------------------------------------------------------------------------- + | + | Date interval of AB testing. + | + | Example: ['2022-03-31 00:00:00', '2022-04-30 00:00:00'] + | + */ + 'interval' => [], + /* + |-------------------------------------------------------------------------- | Ignore Crawlers |-------------------------------------------------------------------------- | diff --git a/database/migrations/2019_02_02_200315_create_experiments_table.php b/database/migrations/2019_02_02_200315_create_experiments_table.php index 9d26207..067b04b 100644 --- a/database/migrations/2019_02_02_200315_create_experiments_table.php +++ b/database/migrations/2019_02_02_200315_create_experiments_table.php @@ -17,6 +17,7 @@ public function up() $table->increments('id'); $table->string('name')->unique(); $table->integer('visitors'); + $table->double('percentage'); $table->timestamps(); }); } diff --git a/src/AbTesting.php b/src/AbTesting.php index b7b8b03..894c685 100644 --- a/src/AbTesting.php +++ b/src/AbTesting.php @@ -9,6 +9,7 @@ use Ben182\AbTesting\Models\Goal; use Illuminate\Support\Collection; use Jaybizzle\CrawlerDetect\CrawlerDetect; +use Carbon\Carbon; class AbTesting { @@ -31,6 +32,18 @@ protected function start() { $configExperiments = config('ab-testing.experiments'); $configGoals = config('ab-testing.goals'); + $configPercentages = config('ab-testing.percentages'); + $configInterval = config('ab-testing.interval'); + + $configPercentagesNumeric = array_filter($configPercentages, function($percentage) { + return is_numeric($percentage); + }); + + if (count($configPercentagesNumeric) !== count($configPercentages)) { + throw InvalidConfiguration::numericPercentages(); + } + + $totalPercentage = array_sum($configPercentages); if (! count($configExperiments)) { throw InvalidConfiguration::noExperiment(); @@ -39,15 +52,25 @@ protected function start() if (count($configExperiments) !== count(array_unique($configExperiments))) { throw InvalidConfiguration::experiment(); } + + if ($totalPercentage !== 100) { + throw InvalidConfiguration::totalPercentage(); + } + + if (count($configPercentages) !== count($configExperiments)) { + throw InvalidConfiguration::percentage(); + } if (count($configGoals) !== count(array_unique($configGoals))) { throw InvalidConfiguration::goal(); } + - foreach ($configExperiments as $configExperiment) { + foreach ($configExperiments as $index => $configExperiment) { $this->experiments[] = $experiment = Experiment::with('goals')->firstOrCreate([ 'name' => $configExperiment, ], [ + 'percentage' => $configPercentages[$index], 'visitors' => 0, ]); @@ -72,6 +95,14 @@ protected function start() */ public function pageView() { + $configInterval = config('ab-testing.interval'); + + if(count($configInterval) == 1 || + (count($configInterval) == 2 && (!Carbon::createFromFormat('Y-m-d H:i:s', $configInterval[0]) || !Carbon::createFromFormat('Y-m-d H:i:s', $configInterval[1]))) || + (count($configInterval) == 2 && Carbon::createFromFormat('Y-m-d H:i:s', $configInterval[0])->gt(Carbon::createFromFormat('Y-m-d H:i:s', $configInterval[1])))) { + throw InvalidConfiguration::interval(); + } + if (config('ab-testing.ignore_crawlers') && (new CrawlerDetect)->isCrawler()) { return; } @@ -79,6 +110,10 @@ public function pageView() if (session(self::SESSION_KEY_EXPERIMENT)) { return; } + + if (!empty($configInterval) && !$this->inInterval($configInterval)) { + return; + } $this->start(); $this->setNextExperiment(); @@ -87,6 +122,19 @@ public function pageView() return $this->getExperiment(); } + + /** + * Check if the current date is in the interval + * + * @return bool + */ + protected function inInterval($interval) + { + $currentDate = Carbon::now(); + $startDate = Carbon::createFromFormat('Y-m-d H:i:s', $interval[0]); + $endDate = Carbon::createFromFormat('Y-m-d H:i:s', $interval[1]); + return $currentDate->between($startDate,$endDate); + } /** * Calculates a new experiment and sets it to the session. @@ -110,9 +158,19 @@ protected function setNextExperiment() */ protected function getNextExperiment() { - $sorted = $this->experiments->sortBy('visitors'); - - return $sorted->first(); + $experiments = $this->experiments->sortByDesc('percentage'); + + $visitorsSum = $experiments->sum('visitors'); + + $nextExperiment = collect([]); + + if($visitorsSum != 0) + { + $nextExperiment = $experiments->filter(function($experiment) use ($visitorsSum) { + return (($experiment->visitors / $visitorsSum) * 100) < $experiment->percentage; + }); + } + return !$nextExperiment->isEmpty() ? $nextExperiment->first() : $experiments->first(); } /** diff --git a/src/Exceptions/InvalidConfiguration.php b/src/Exceptions/InvalidConfiguration.php index 015c148..5361e9c 100644 --- a/src/Exceptions/InvalidConfiguration.php +++ b/src/Exceptions/InvalidConfiguration.php @@ -20,4 +20,24 @@ public static function goal(): self { return new static('The goal names should be unique.'); } + + public static function percentage(): self + { + return new static('There is no percentage for every experiment'); + } + + public static function totalPercentage(): self + { + return new static('Total percentage should be equal to 100'); + } + + public static function numericPercentages(): self + { + return new static('Percentages should be numeric'); + } + + public static function interval(): self + { + return new static('The elements of interval array must be dates of format Y-m-d H:i:s and the first element less than the second'); + } } diff --git a/src/Models/Experiment.php b/src/Models/Experiment.php index 9973052..c661295 100644 --- a/src/Models/Experiment.php +++ b/src/Models/Experiment.php @@ -11,6 +11,7 @@ class Experiment extends Model protected $fillable = [ 'name', 'visitors', + 'percentage', ]; protected $casts = [