Skip to content

Commit 2477953

Browse files
Group tests, enable Google Optimize (#12)
1 parent 96fa4e0 commit 2477953

7 files changed

Lines changed: 253 additions & 25 deletions

File tree

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,97 @@ To do this use the `track_experiments` helper.
6767
SdcExperiments::googleTagManagerSetTrackingVars();
6868
```
6969
This will send through experiment data to GTM for reporting.
70+
71+
### Optionally setup Google Optimize Server Side tracking
72+
As an additional extra you can also track your experiment via Google Optimize with their Server Side tracking configuration.
73+
More information about the required experiment settings can be found in the official [Google Optimize docs](https://developers.google.com/optimize/devguides/experiments).
74+
75+
Assuming you have followed the steps and have an experiment setup and ready to run you can now define how the A/B tests you have setup should be grouped and reported.
76+
77+
Only 2 buckets are supported at this tim for each a/b test, 0=control, 1=test
78+
79+
#### Scenario - A/B test
80+
81+
in `config/experiments.php` define a new group with a readable key (this can be the same key that you used for the a/b test itself)
82+
83+
```php
84+
[
85+
'tests' => [
86+
'ab_test' => [],
87+
],
88+
'groups' => [
89+
'ab_test_nice_group_name' => [
90+
'id' => 'GOOGLE_OPTIMIZE_EXPERIMENT_ID',
91+
'variations' => [
92+
'ab_test',
93+
],
94+
],
95+
],
96+
]
97+
```
98+
99+
Assuming the user is in the control group
100+
The GTM tracking vars created for this will appear as follows (other vars snipped for clarity):
101+
```js
102+
window.dataLayer = [
103+
{
104+
"experiments": {
105+
"ab_test": "control"
106+
},
107+
"ga_optimize_exp": "GOOGLE_OPTIMIZE_EXPERIMENT_ID.0",
108+
"experiments_running": "ab_test_nice_group_name.0"
109+
}
110+
];
111+
```
112+
113+
#### Scenario - Multivariant test
114+
115+
building upon the previous example, assuming we want to run a multivariant test (multiple a/b tests simultaneously with similar or connected changes)
116+
117+
```php
118+
[
119+
'tests' => [
120+
'ab_test' => [],
121+
'large_font' => [],
122+
'obnoxious_colours' => [],
123+
],
124+
'groups' => [
125+
'ab_test_nice_group_name' => [
126+
'id' => 'GOOGLE_OPTIMIZE_EXPERIMENT_ID',
127+
'variations' => [
128+
'ab_test',
129+
],
130+
],
131+
'style_test' => [
132+
'id' => 'MULTIVARIANT_GOOGLE_OPTIMIZE_EXPERIMENT_ID',
133+
'variations' => [
134+
'large_font',
135+
'obnoxious_colours',
136+
],
137+
],
138+
],
139+
]
140+
```
141+
142+
Assuming the user is in the `control` group for `ab_test`, `test` group for `large_font`, and `control` group for `obnoxious_colours`
143+
The GTM tracking vars created for this will appear as follows (other vars snipped for clarity):
144+
```js
145+
window.dataLayer = [
146+
{
147+
"experiments": {
148+
"ab_test": "control",
149+
"large_font": "test",
150+
"obnoxious_colours": "control"
151+
},
152+
"ga_optimize_exp": "GOOGLE_OPTIMIZE_EXPERIMENT_ID.0!MULTIVARIANT_GOOGLE_OPTIMIZE_EXPERIMENT_ID.1-0",
153+
"experiments_running": "ab_test_nice_group_name.0!style_test.1-0"
154+
}
155+
];
156+
```
157+
158+
#### GTM Setup
159+
GA needs the value of `ga_optimize_exp` from the dataLayer to be set to the `exp` field.
160+
Running GA directly it would look a bit like `ga('set', 'exp', 'GOOGLE_OPTIMIZE_EXPERIMENT_ID.0!MULTIVARIANT_GOOGLE_OPTIMIZE_EXPERIMENT_ID.1-0');'`
161+
Via GTM, in the Google Analytics settings var, under `Fields to Set`, set the field name `exp` to the value of the dataLayer variable `ga_optimize_exp`
162+
The other vars pushed to the dataLayer are convenience vars that you could use to segment user traffic, call additional GTM scripts, etc
163+
Access specific experiment values via `experiments.obnoxious_colours` (for example)

config/experiments.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,29 @@
11
<?php
22

33
return [
4-
'your-experiment-key' => [
5-
'control' => 'control-key',
6-
'test' => 'test-key',
4+
'tests' => [
5+
// example format
6+
//'experiment_1' => [
7+
// 'control' => 'control-key',
8+
// 'test' => 'test-key',
9+
//],
10+
//'experiment_2' => [],
11+
//'experiment_3' => [],
12+
],
13+
'groups' => [
14+
// example format
15+
//'experiment_collection_name' => [
16+
// 'id' => '', // Google Optimize experiment ID
17+
// 'variations' => [
18+
// 'experiment_1',
19+
// 'experiment_2',
20+
// ],
21+
//],
22+
//'another_experiment_collection_name' => [
23+
// 'id' => '', // Google Optimize experiment ID
24+
// 'variations' => [
25+
// 'experiment_3',
26+
// ],
27+
//],
728
],
829
];

src/Experiments.php

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Illuminate\Support\Collection;
66
use Illuminate\Support\Facades\Cookie;
77
use Illuminate\Support\Str;
8-
use Spatie\GoogleTagManager\GoogleTagManagerFacade;
8+
use Spatie\GoogleTagManager\GoogleTagManagerFacade as GoogleTagManager;
99

1010
class Experiments
1111
{
@@ -93,7 +93,7 @@ function ($assignment) {
9393
public function availableExperiments(): array
9494
{
9595
return array_keys(
96-
config('experiments') ?: []
96+
config('experiments.tests') ?: []
9797
);
9898
}
9999

@@ -176,7 +176,86 @@ function ($assignment, $experiment) {
176176

177177
public function googleTagManagerSetTrackingVars()
178178
{
179-
GoogleTagManagerFacade::set(
179+
$experiments = collect(config('experiments.groups') ?: [])
180+
->map(
181+
function ($experiment, $experimentName) {
182+
if (!$experiment['id']) {
183+
return null;
184+
}
185+
186+
$hasVariation = false;
187+
foreach ($experiment['variations'] as $variation) {
188+
if (!is_null($this->getExperiment($variation))) {
189+
$hasVariation = true;
190+
}
191+
}
192+
if (!$hasVariation) {
193+
return null;
194+
}
195+
196+
$variations = [];
197+
foreach ($experiment['variations'] as $variation) {
198+
switch ($this->getOrStartExperiment($variation)) {
199+
case 'test':
200+
$variations[] = '1';
201+
break;
202+
case 'internal':
203+
$variations[] = '2';
204+
break;
205+
case 'control':
206+
case null:
207+
default:
208+
$variations[] = '0';
209+
break;
210+
}
211+
}
212+
213+
return [
214+
'id' => $experiment['id'],
215+
'name' => $experimentName,
216+
'variations' => $variations,
217+
];
218+
219+
return sprintf(
220+
'%s.%s',
221+
$experiment['id'],
222+
implode('-', $variations)
223+
);
224+
}
225+
)
226+
->filter();
227+
228+
GoogleTagManager::set(
229+
'ga_optimize_exp',
230+
$experiments
231+
->map(
232+
function ($experiment) {
233+
return sprintf(
234+
'%s.%s',
235+
$experiment['id'],
236+
implode('-', $experiment['variations'])
237+
);
238+
}
239+
)
240+
->implode('!')
241+
);
242+
243+
GoogleTagManager::set(
244+
'experiments_running',
245+
$experiments
246+
->map(
247+
function ($experiment) {
248+
return sprintf(
249+
'%s.%s',
250+
$experiment['name'],
251+
implode('-', $experiment['variations'])
252+
);
253+
}
254+
)
255+
->implode('!')
256+
);
257+
258+
GoogleTagManager::set(
180259
'experiments',
181260
$this->userExperiments()
182261
);

src/ExperimentsServiceProvider.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ public function boot()
2626

2727
public function register()
2828
{
29-
// do not merge config from base package, this can cause a/b test inheritance from the example config
30-
//$this->mergeConfigFrom($this->configPath(), 'experiments');
29+
$this->mergeConfigFrom($this->configPath(), 'experiments');
3130

3231
$this->app->singleton('experiments', function ($app) {
3332
return $app->make(Experiments::class);

tests/TestCase.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,26 @@ protected function getEnvironmentSetUp($app)
1919
config(
2020
[
2121
'experiments' => [
22-
'recommend' => [
23-
'control' => 'personalize',
24-
'test' => 'alice',
22+
'tests' => [
23+
'recommend' => [
24+
'control' => 'personalize',
25+
'test' => 'alice',
26+
],
27+
],
28+
'groups' => [
29+
'recommend_group' => [
30+
'id' => 'GOOGLE_OPTIMIZE_EXPERIMENT_ID',
31+
'variations' => [
32+
'recommend',
33+
],
34+
],
35+
'another_group_not_called_recommend' => [
36+
'id' => 'GOOGLE_OPTIMIZE_EXPERIMENT_ID_2',
37+
'variations' => [
38+
'recommend',
39+
'recommend', // repeat the same one here for
40+
],
41+
],
2542
],
2643
],
2744
]

tests/Unit/Helpers/ExperimentsTest.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,25 @@ public function testTrackExperiments()
1414

1515
SdcExperiments::googleTagManagerSetTrackingVars();
1616

17+
$assignmentIndex = $assignment === 'control' ? 0 : 1;
18+
1719
$this->assertEquals(
1820
[
1921
'experiments' => [
2022
'recommend' => $assignment,
2123
],
24+
'ga_optimize_exp' => sprintf(
25+
'GOOGLE_OPTIMIZE_EXPERIMENT_ID.%s!GOOGLE_OPTIMIZE_EXPERIMENT_ID_2.%s-%s',
26+
$assignmentIndex,
27+
$assignmentIndex,
28+
$assignmentIndex
29+
),
30+
'experiments_running' => sprintf(
31+
'recommend_group.%s!another_group_not_called_recommend.%s-%s',
32+
$assignmentIndex,
33+
$assignmentIndex,
34+
$assignmentIndex
35+
),
2236
],
2337
GoogleTagManager::getDataLayer()->toArray()
2438
);
@@ -58,8 +72,10 @@ public function testExperimentGroup()
5872
config(
5973
[
6074
'experiments' => [
61-
'experiment-1' => [],
62-
'experiment-2' => [],
75+
'tests' => [
76+
'experiment-1' => [],
77+
'experiment-2' => [],
78+
],
6379
],
6480
]
6581
);

tests/Unit/Http/Middleware/SetExperimentTest.php

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function () {
3131
in_array(
3232
$experiment['experiment_recommend'],
3333
array_keys(
34-
config('experiments.recommend')
34+
config('experiments.tests.recommend')
3535
)
3636
)
3737
);
@@ -42,13 +42,15 @@ public function testSuccessSeveralExperiments()
4242
config(
4343
[
4444
'experiments' => [
45-
'recommend' => [
46-
'control' => 'personalize',
47-
'test' => 'alice',
48-
],
49-
'recommend_2' => [
50-
'control' => 'personalize',
51-
'test' => 'alice',
45+
'tests' => [
46+
'recommend' => [
47+
'control' => 'personalize',
48+
'test' => 'alice',
49+
],
50+
'recommend_2' => [
51+
'control' => 'personalize',
52+
'test' => 'alice',
53+
],
5254
],
5355
],
5456
]
@@ -70,7 +72,7 @@ function () {
7072
in_array(
7173
$experiment['experiment_recommend'],
7274
array_keys(
73-
config('experiments.recommend')
75+
config('experiments.tests.recommend')
7476
)
7577
)
7678
);
@@ -79,7 +81,7 @@ function () {
7981
in_array(
8082
$experiment['experiment_recommend_2'],
8183
array_keys(
82-
config('experiments.recommend_2')
84+
config('experiments.tests.recommend_2')
8385
)
8486
)
8587
);
@@ -152,7 +154,7 @@ function () {
152154
*/
153155
public function testPresetExperiments()
154156
{
155-
config(['experiments.recommend_2' => []]);
157+
config(['experiments.tests.recommend_2' => []]);
156158

157159
// this ensures control always get chosen
158160
mt_srand(0);
@@ -191,7 +193,7 @@ function () {
191193
}
192194

193195
protected function setExperiments() {
194-
foreach (array_keys(config('experiments')) as $experiment) {
196+
foreach (array_keys(config('experiments.tests')) as $experiment) {
195197
SdcExperiments::getOrStartExperiment($experiment);
196198
}
197199
}

0 commit comments

Comments
 (0)