Skip to content

Commit d9ecd57

Browse files
committed
Add dashboard widget
1 parent a5d525d commit d9ecd57

5 files changed

Lines changed: 235 additions & 3 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<ui-widget title="{{ __('A/B Experiments') }}" icon="labs-idea-experimental-flask">
2+
@if ($experiments->isEmpty())
3+
<div class="flex flex-col items-center justify-center gap-2 py-10 text-center">
4+
<ui-description>{{ __('No experiments are currently running.') }}</ui-description>
5+
<a href="{{ $indexUrl }}" class="text-sm">{{ __('Create an experiment') }} &rarr;</a>
6+
</div>
7+
@else
8+
<ui-table class="px-4 mt-2">
9+
<ui-table-columns>
10+
<ui-table-column>{{ __('Experiment') }}</ui-table-column>
11+
<ui-table-column>{{ __('Hits') }}</ui-table-column>
12+
<ui-table-column>{{ __('Leader') }}</ui-table-column>
13+
</ui-table-columns>
14+
<ui-table-rows>
15+
@foreach ($experiments as $exp)
16+
<ui-table-row>
17+
<ui-table-cell>
18+
<a href="{{ $exp['url'] }}" class="font-medium">{{ $exp['title'] }}</a>
19+
</ui-table-cell>
20+
<ui-table-cell>{{ number_format($exp['total_hits']) }}</ui-table-cell>
21+
<ui-table-cell>
22+
@if ($exp['leader_label'] !== null)
23+
<span>{{ $exp['leader_label'] }}</span>
24+
<ui-badge color="green" class="ml-2">{{ $exp['leader_rate'] }}%</ui-badge>
25+
@else
26+
<span>&mdash;</span>
27+
@endif
28+
</ui-table-cell>
29+
</ui-table-row>
30+
@endforeach
31+
</ui-table-rows>
32+
</ui-table>
33+
34+
<template #footer>
35+
<div class="px-4 py-3 border-t border-gray-200 dark:border-gray-700 text-right">
36+
<a href="{{ $indexUrl }}" class="text-sm">{{ __('View all experiments') }} &rarr;</a>
37+
</div>
38+
</template>
39+
@endif
40+
</ui-widget>

src/Widgets/ABTesterWidget.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace Thoughtco\StatamicABTester\Widgets;
4+
5+
use Illuminate\Support\Facades\DB;
6+
use Statamic\Widgets\Widget;
7+
use Thoughtco\StatamicABTester\Facades\Experiment;
8+
9+
class ABTesterWidget extends Widget
10+
{
11+
protected static $handle = 'ab_tester';
12+
13+
public function html()
14+
{
15+
$activeExperiments = Experiment::query()
16+
->whereNull('completed_at')
17+
->where('published', true)
18+
->where(fn ($q) => $q->whereNull('start_at')->orWhere('start_at', '<=', now()))
19+
->where(fn ($q) => $q->whereNull('end_at')->orWhere('end_at', '>=', now()))
20+
->get();
21+
22+
$experiments = $activeExperiments->map(function ($experiment) {
23+
$variantResults = $experiment->resultsQuery()
24+
->select('variation', DB::raw('count(*) as total'))
25+
->groupBy('variation')
26+
->get()
27+
->map(function ($row) use ($experiment) {
28+
$successes = $experiment->resultsQuery()
29+
->where('variation', $row->variation)
30+
->where('type', 'success')
31+
->count();
32+
33+
return [
34+
'label' => $row->variation,
35+
'hits' => $row->total,
36+
'rate' => $row->total > 0 ? round($successes / $row->total * 100, 1) : 0,
37+
];
38+
});
39+
40+
$leader = $variantResults->sortByDesc('rate')->first();
41+
42+
return [
43+
'title' => $experiment->title(),
44+
'url' => cp_route('ab.experiments.show', $experiment->id()),
45+
'total_hits' => $variantResults->sum('hits'),
46+
'leader_label' => $leader ? $leader['label'] : null,
47+
'leader_rate' => $leader ? $leader['rate'] : null,
48+
];
49+
});
50+
51+
return view('ab::widgets.ab-tester', [
52+
'activeCount' => $activeExperiments->count(),
53+
'experiments' => $experiments,
54+
'indexUrl' => cp_route('ab.experiments.index'),
55+
])->render();
56+
}
57+
}

tests/Experiment/Stache/ExperimentRepositoryTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
uses(\Thoughtco\StatamicABTester\Tests\TestCase::class);
44

55
use Statamic\Facades\File;
6-
use Thoughtco\StatamicABTester\Experiment\Stache\Experiment;
7-
use Thoughtco\StatamicABTester\Experiment\Stache\ExperimentQueryBuilder;
6+
use Thoughtco\StatamicABTester\Events\Experiment\Stache\Experiment;
7+
use Thoughtco\StatamicABTester\Events\Experiment\Stache\ExperimentQueryBuilder;
88
use Thoughtco\StatamicABTester\Facades\Experiment as ExperimentApi;
99

1010
it('can make an experiment', function () {

tests/Tags/ABTagsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
uses(\Thoughtco\StatamicABTester\Tests\TestCase::class);
44

55
use Statamic\Facades;
6-
use Thoughtco\StatamicABTester\Experiment\Stache\Experiment;
6+
use Thoughtco\StatamicABTester\Events\Experiment\Stache\Experiment;
77

88
it('returns a variant', function () {
99
(new Experiment)
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
uses(\Thoughtco\StatamicABTester\Tests\TestCase::class);
4+
5+
use Thoughtco\StatamicABTester\Facades\Experiment;
6+
use Thoughtco\StatamicABTester\Models\AbTestResult;
7+
use Thoughtco\StatamicABTester\Widgets\ABTesterWidget;
8+
9+
function makeWidget(): ABTesterWidget
10+
{
11+
$widget = new ABTesterWidget;
12+
$widget->setConfig([]);
13+
14+
return $widget;
15+
}
16+
17+
describe('ABTesterWidget', function () {
18+
it('is registered with the correct handle', function () {
19+
expect(ABTesterWidget::handle())->toBe('ab_tester');
20+
});
21+
22+
it('renders HTML', function () {
23+
$html = makeWidget()->html();
24+
25+
expect($html)->toBeString()->not->toBeEmpty();
26+
});
27+
28+
it('shows a count of zero when there are no active experiments', function () {
29+
$html = makeWidget()->html();
30+
31+
expect($html)->toContain('0');
32+
expect($html)->toContain('No experiments are currently running.');
33+
});
34+
35+
it('counts active experiments', function () {
36+
tap(Experiment::make('active-1')->title('Active One')->published(true))->save();
37+
tap(Experiment::make('active-2')->title('Active Two')->published(true))->save();
38+
39+
$html = makeWidget()->html();
40+
41+
expect($html)->toContain('2');
42+
expect($html)->toContain('Active One');
43+
expect($html)->toContain('Active Two');
44+
});
45+
46+
it('excludes completed experiments', function () {
47+
tap(Experiment::make('active')->title('Active')->published(true))->save();
48+
49+
tap(Experiment::make('done')->title('Done')->published(true))
50+
->completedAt(now())
51+
->save();
52+
53+
$html = makeWidget()->html();
54+
55+
expect($html)->toContain('1');
56+
expect($html)->toContain('Active');
57+
expect($html)->not->toContain('Done');
58+
});
59+
60+
it('excludes unpublished experiments', function () {
61+
tap(Experiment::make('published')->title('Published')->published(true))->save();
62+
tap(Experiment::make('draft')->title('Draft')->published(false))->save();
63+
64+
$html = makeWidget()->html();
65+
66+
expect($html)->toContain('1');
67+
expect($html)->toContain('Published');
68+
expect($html)->not->toContain('Draft');
69+
});
70+
71+
it('excludes experiments that have not started yet', function () {
72+
tap(Experiment::make('future')->title('Future')->published(true))
73+
->startAt(now()->addDay())
74+
->save();
75+
76+
$html = makeWidget()->html();
77+
78+
expect($html)->toContain('0');
79+
expect($html)->not->toContain('Future');
80+
});
81+
82+
it('excludes experiments past their end date', function () {
83+
tap(Experiment::make('expired')->title('Expired')->published(true))
84+
->endAt(now()->subDay())
85+
->save();
86+
87+
$html = makeWidget()->html();
88+
89+
expect($html)->toContain('0');
90+
expect($html)->not->toContain('Expired');
91+
});
92+
93+
it('shows total hits for each experiment', function () {
94+
$experiment = tap(Experiment::make('hit-test')->title('Hit Test')->published(true))->save();
95+
96+
AbTestResult::create(['experiment_id' => $experiment->id(), 'variation' => '1', 'type' => 'hit', 'data' => []]);
97+
AbTestResult::create(['experiment_id' => $experiment->id(), 'variation' => '1', 'type' => 'hit', 'data' => []]);
98+
AbTestResult::create(['experiment_id' => $experiment->id(), 'variation' => '2', 'type' => 'hit', 'data' => []]);
99+
100+
$html = makeWidget()->html();
101+
102+
expect($html)->toContain('3');
103+
});
104+
105+
it('shows the leading variant label', function () {
106+
$experiment = tap(Experiment::make('leader-test')->title('Leader Test')->published(true))->save();
107+
108+
// variant 1: 1 hit, 1 success = 100% rate
109+
AbTestResult::create(['experiment_id' => $experiment->id(), 'variation' => 'control', 'type' => 'hit', 'data' => []]);
110+
AbTestResult::create(['experiment_id' => $experiment->id(), 'variation' => 'control', 'type' => 'success', 'data' => []]);
111+
// variant 2: 10 hits, 0 successes = 0% rate
112+
for ($i = 0; $i < 10; $i++) {
113+
AbTestResult::create(['experiment_id' => $experiment->id(), 'variation' => 'variant_a', 'type' => 'hit', 'data' => []]);
114+
}
115+
116+
$html = makeWidget()->html();
117+
118+
// control has a higher conversion rate so it should appear as the leader
119+
expect($html)->toContain('control');
120+
});
121+
122+
it('links each experiment to its show page', function () {
123+
$experiment = tap(Experiment::make('link-test')->title('Link Test')->published(true))->save();
124+
125+
$html = makeWidget()->html();
126+
127+
expect($html)->toContain(cp_route('ab.experiments.show', $experiment->id()));
128+
});
129+
130+
it('includes a link to the experiments index', function () {
131+
$html = makeWidget()->html();
132+
133+
expect($html)->toContain(cp_route('ab.experiments.index'));
134+
});
135+
});

0 commit comments

Comments
 (0)