feat(features): support percentage rollouts in feature:enable (#592)
## What `feature:enable <feature> <target>` now accepts a percentage like `25%` as the target, in addition to a user email or `all`. ```bash php artisan feature:enable AiConsentSettings 25% ``` This activates the feature for a random `ceil(total * N/100)` sample of the **current** users, persisted in Pennant's `features` table — no need to edit the feature class and redeploy for a percentage-based rollout. ## Notes - It targets the current user base. New users keep the feature's default (`false`) unless the command is run again. - Percentage is validated to be between 1 and 100. - `feature:disable` was left unchanged — disabling for a random subset has no clear use case. ## Test plan - [x] `feature:enable AiConsentSettings 40%` enables the feature for exactly 4 of 10 users - [x] An out-of-range percentage (`0%`) fails
This commit is contained in:
parent
9a458b1031
commit
f72e2a64ca
|
|
@ -11,7 +11,7 @@ class FeatureEnableCommand extends Command
|
|||
{
|
||||
use ResolvesFeatures;
|
||||
|
||||
protected $signature = 'feature:enable {feature : The feature name (class name or string-based feature)} {target : User email or "all" for everyone}';
|
||||
protected $signature = 'feature:enable {feature : The feature name (class name or string-based feature)} {target : User email, "all" for everyone, or a percentage like "25%" for a random rollout}';
|
||||
|
||||
protected $description = 'Enable a feature for a specific user or all users';
|
||||
|
||||
|
|
@ -36,6 +36,10 @@ class FeatureEnableCommand extends Command
|
|||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (preg_match('/^(\d{1,3})%$/', $target, $matches)) {
|
||||
return $this->enableForPercentage($featureClass, $featureName, (int) $matches[1]);
|
||||
}
|
||||
|
||||
$user = User::where('email', $target)->first();
|
||||
|
||||
if (! $user) {
|
||||
|
|
@ -49,4 +53,23 @@ class FeatureEnableCommand extends Command
|
|||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function enableForPercentage(string $featureClass, string $featureName, int $percentage): int
|
||||
{
|
||||
if ($percentage < 1 || $percentage > 100) {
|
||||
$this->error('Percentage must be between 1 and 100.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$count = (int) ceil(User::count() * $percentage / 100);
|
||||
|
||||
User::inRandomOrder()
|
||||
->take($count)
|
||||
->each(fn (User $user) => Feature::for($user)->activate($featureClass));
|
||||
|
||||
$this->info("Feature '{$featureName}' enabled for {$count} users (~{$percentage}% of current users).");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
use App\Features\AiConsentSettings;
|
||||
use App\Models\User;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
test('enables a feature for a percentage of users', function () {
|
||||
$users = User::factory()->count(10)->create();
|
||||
|
||||
$this->artisan('feature:enable', ['feature' => 'AiConsentSettings', 'target' => '40%'])
|
||||
->expectsOutputToContain('enabled for 4 users')
|
||||
->assertSuccessful();
|
||||
|
||||
$enabled = $users->filter(fn (User $user) => Feature::for($user)->active(AiConsentSettings::class));
|
||||
|
||||
expect($enabled)->toHaveCount(4);
|
||||
});
|
||||
|
||||
test('rejects an out-of-range percentage', function () {
|
||||
User::factory()->create();
|
||||
|
||||
$this->artisan('feature:enable', ['feature' => 'AiConsentSettings', 'target' => '0%'])
|
||||
->assertFailed();
|
||||
});
|
||||
Loading…
Reference in New Issue