feat(subscriptions): pin the winning variant via env
Add SUBSCRIPTION_EXPERIMENT_FORCE_VARIANT: a Pennant before() override that serves one variant to every user, so once a winner is chosen the split ends with an env flip (no deploy, stored assignments untouched). Invalid values are ignored and fall through to normal assignment.
This commit is contained in:
parent
f1d1fb0f71
commit
051ef165f6
|
|
@ -13,9 +13,8 @@ use Carbon\CarbonImmutable;
|
|||
* registered on or after the start is split evenly into the three variants by a
|
||||
* stable hash of their id, so the bucket never changes for a given user.
|
||||
*
|
||||
* The split is deterministic (crc32(id) % 3); the funnel report mirrors the same
|
||||
* formula in SQL. Manual overrides via `feature:enable` are honoured at runtime
|
||||
* but are not reflected in that report — keep overrides to QA accounts.
|
||||
* The split is deterministic (crc32(id) % 3) and persisted by Pennant; the funnel
|
||||
* report reads that persisted value so it always matches what the user was served.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
|
|
@ -29,6 +28,21 @@ class SubscriptionExperiment
|
|||
|
||||
public const PAY_NOW = 'pay_now';
|
||||
|
||||
/**
|
||||
* In-memory override that pins every user to the winning variant once the
|
||||
* experiment is decided. Returning non-null skips both storage and resolve,
|
||||
* so flipping SUBSCRIPTION_EXPERIMENT_FORCE_VARIANT rolls the winner out to
|
||||
* everyone without a deploy and without rewriting stored assignments.
|
||||
*/
|
||||
public function before(?User $user): ?string
|
||||
{
|
||||
$forced = config('subscriptions.experiment.force_variant');
|
||||
|
||||
return in_array($forced, [self::CONTROL, self::REDUCED_TRIAL, self::PAY_NOW], true)
|
||||
? $forced
|
||||
: null;
|
||||
}
|
||||
|
||||
public function resolve(?User $user): string
|
||||
{
|
||||
$startedAt = config('subscriptions.experiment.started_at');
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ return [
|
|||
|
||||
'experiment' => [
|
||||
'started_at' => env('SUBSCRIPTION_EXPERIMENT_STARTED_AT'),
|
||||
// Once a winner is chosen, set this to control / reduced_trial / pay_now
|
||||
// to give every user that variant and end the split (env-only, no deploy).
|
||||
'force_variant' => env('SUBSCRIPTION_EXPERIMENT_FORCE_VARIANT'),
|
||||
'reduced_trial' => [
|
||||
'monthly' => (int) env('SUBSCRIPTION_EXPERIMENT_REDUCED_TRIAL_MONTHLY', 3),
|
||||
'yearly' => (int) env('SUBSCRIPTION_EXPERIMENT_REDUCED_TRIAL_YEARLY', 7),
|
||||
|
|
|
|||
|
|
@ -36,6 +36,26 @@ it('treats everyone as legacy while the experiment is off', function () {
|
|||
expect(app(ExperimentOffer::class)->variantFor($user))->toBe(SubscriptionExperiment::LEGACY);
|
||||
});
|
||||
|
||||
it('pins every user to the forced winner variant', function () {
|
||||
config(['subscriptions.experiment.force_variant' => SubscriptionExperiment::PAY_NOW]);
|
||||
$offer = app(ExperimentOffer::class);
|
||||
|
||||
$legacy = User::factory()->create(['created_at' => CarbonImmutable::parse('2026-05-01')]);
|
||||
$fresh = User::factory()->create(['created_at' => CarbonImmutable::parse('2026-06-10')]);
|
||||
|
||||
expect($offer->variantFor($legacy))->toBe(SubscriptionExperiment::PAY_NOW)
|
||||
->and($offer->variantFor($fresh))->toBe(SubscriptionExperiment::PAY_NOW)
|
||||
->and($offer->trialDaysFor($fresh, 'monthly'))->toBe(0);
|
||||
});
|
||||
|
||||
it('ignores an invalid forced variant', function () {
|
||||
config(['subscriptions.experiment.force_variant' => 'bogus']);
|
||||
|
||||
$user = User::factory()->create(['created_at' => CarbonImmutable::parse('2026-05-01')]);
|
||||
|
||||
expect(app(ExperimentOffer::class)->variantFor($user))->toBe(SubscriptionExperiment::LEGACY);
|
||||
});
|
||||
|
||||
it('splits post-start users across all three variants and stays stable per user', function () {
|
||||
$offer = app(ExperimentOffer::class);
|
||||
$variants = [];
|
||||
|
|
|
|||
Loading…
Reference in New Issue