File: /home/xedaptot/ai.naniguide.com/app/Model/AdAbTestEvaluationLog.php
<?php
namespace App\Model;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property int $ad_ab_test_id
* @property array<int,array{id:int,name:string,weight:int,impressions:int,clicks:int,spend:float,conversions:int,ctr:float}> $variants_snapshot
* @property string $method
* @property float|null $test_statistic
* @property float|null $p_value
* @property float|null $confidence
* @property bool $winner_declared
* @property int|null $winner_variant_id
* @property string $gate_result
* @property string|null $notes
* @property \Carbon\Carbon|null $created_at
* @property \Carbon\Carbon|null $updated_at
*
* R14 — single audit row per evaluator run.
*
* Every time the AdAbTestEvaluator scores a running test, it writes one of
* these rows. Customers reading their A/B test history can see the full
* timeline of "test was evaluated at T1 with confidence 85% (no winner),
* T2 with confidence 92% (no winner), T3 with confidence 96% (winner = B)"
* — answers the "why isn't this finishing?" support question without
* digging into application logs.
*
* Snapshot semantics: variants_snapshot is a frozen-at-evaluation copy of
* each variant's metrics. Storing it means later metric pulls (R7) don't
* retroactively change the audit row's numbers.
*
* Statistical-method values:
* two_proportion_z — z-test on CTR (clicks / impressions), the default
* for almost every A/B test
* welch_t — Welch's t-test on CPA (spend / conversions); used
* when both variants have ≥30 conversions and the
* customer flagged the test for cost optimization
* min_sample_not_met — evaluator skipped the test (insufficient data);
* test_statistic + p_value + confidence are null
*
* Gate-result values:
* passed — min-sample requirements met; statistical
* test ran
* insufficient_conversions — fewer than 100 conversions per variant
* AND test ran for less than 7 days
* insufficient_duration — has 100 conversions per variant but the
* 7-day floor hasn't elapsed (rare; only
* hits ultra-high-traffic experiments)
* inconclusive — test ran 7+ days but confidence below
* threshold; evaluator suggests stopping
*/
class AdAbTestEvaluationLog extends Model
{
public const METHOD_TWO_PROPORTION_Z = 'two_proportion_z';
public const METHOD_WELCH_T = 'welch_t';
public const METHOD_MIN_SAMPLE_NOT_MET = 'min_sample_not_met';
public const METHODS = [
self::METHOD_TWO_PROPORTION_Z,
self::METHOD_WELCH_T,
self::METHOD_MIN_SAMPLE_NOT_MET,
];
public const GATE_PASSED = 'passed';
public const GATE_INSUFFICIENT_CONVERSIONS = 'insufficient_conversions';
public const GATE_INSUFFICIENT_DURATION = 'insufficient_duration';
public const GATE_INCONCLUSIVE = 'inconclusive';
public const GATES = [
self::GATE_PASSED,
self::GATE_INSUFFICIENT_CONVERSIONS,
self::GATE_INSUFFICIENT_DURATION,
self::GATE_INCONCLUSIVE,
];
protected $table = 'ad_ab_test_evaluation_logs';
protected $fillable = [
'ad_ab_test_id',
'variants_snapshot',
'method',
'test_statistic',
'p_value',
'confidence',
'winner_declared',
'winner_variant_id',
'gate_result',
'notes',
];
protected $casts = [
'variants_snapshot' => 'json',
'test_statistic' => 'decimal:4',
'p_value' => 'decimal:8',
'confidence' => 'decimal:2',
'winner_declared' => 'boolean',
];
public function abTest(): BelongsTo
{
return $this->belongsTo(AdAbTest::class, 'ad_ab_test_id');
}
public function winnerVariant(): BelongsTo
{
return $this->belongsTo(AdAbVariant::class, 'winner_variant_id');
}
}