Integrate Matomo tracking for A/B Testing (#2)

* Adds new fields to pages.
* Adds new event handler and integration to add necessary JS to track
  experiment with variant.
This commit is contained in:
Daniel Siepmann 2023-03-06 14:39:21 +01:00 committed by GitHub
parent 7ace7e2625
commit 9f0e6cd6c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 311 additions and 6 deletions

View file

@ -24,16 +24,21 @@ declare(strict_types=1);
namespace WerkraumMedia\ABTest\Hook; namespace WerkraumMedia\ABTest\Hook;
use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController as Typo3TypoScriptFrontendController; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController as Typo3TypoScriptFrontendController;
use WerkraumMedia\ABTest\MatomoTracker;
use WerkraumMedia\ABTest\Switcher; use WerkraumMedia\ABTest\Switcher;
class TypoScriptFrontendController class TypoScriptFrontendController
{ {
private Switcher $switcher; private Switcher $switcher;
private MatomoTracker $matomoTracker;
public function __construct( public function __construct(
Switcher $switcher Switcher $switcher,
MatomoTracker $matomoTracker
) { ) {
$this->switcher = $switcher; $this->switcher = $switcher;
$this->matomoTracker = $matomoTracker;
} }
public function determineIdPostProc( public function determineIdPostProc(
@ -43,8 +48,16 @@ class TypoScriptFrontendController
$this->switcher->switch($frontendController); $this->switcher->switch($frontendController);
} }
public function contentPostProcAll(
array $params,
Typo3TypoScriptFrontendController $frontendController
): void {
$frontendController->content = $this->matomoTracker->addScriptToHtmlMarkup($frontendController->content);
}
public static function register(): void public static function register(): void
{ {
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'][self::class] = self::class . '->determineIdPostProc'; $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['determineId-PostProc'][self::class] = self::class . '->determineIdPostProc';
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_fe.php']['contentPostProc-all'][self::class] = self::class . '->contentPostProcAll';
} }
} }

62
Classes/MatomoTracker.php Normal file
View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ABTest;
use WerkraumMedia\ABTest\Events\SwitchedToVariant;
class MatomoTracker
{
private string $experiment = '';
private string $variation = '';
public function handleVariant(SwitchedToVariant $event): void
{
$this->experiment = $event->getOriginalPage()['tx_abtest_matomo_experiment_id'] ?? '';
$this->variation = $event->getVariantPage()['tx_abtest_matomo_variant_id'] ?? '';
}
public function addScriptToHtmlMarkup(string $markup): string
{
if ($this->experiment === '' || $this->variation === '') {
return $markup;
}
$script = $this->generateScript();
return str_replace('</body>', $script . '</body>', $markup);
}
private function generateScript(): string
{
$experiment = htmlspecialchars($this->experiment);
$variation = htmlspecialchars($this->variation);
return '<script>'
. 'var _paq = window._paq = window._paq || [];'
. "_paq.push(['AbTesting::enter', {experiment: '$experiment', variation: '$variation'}]);"
. '</script>'
. PHP_EOL
;
}
}

View file

@ -23,7 +23,6 @@ declare(strict_types=1);
namespace WerkraumMedia\ABTest\TCA; namespace WerkraumMedia\ABTest\TCA;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Domain\Repository\PageRepository; use TYPO3\CMS\Core\Domain\Repository\PageRepository;
class VariantFilter class VariantFilter
@ -36,7 +35,7 @@ class VariantFilter
$this->pageRepository = $pageRepository; $this->pageRepository = $pageRepository;
} }
public function doFilter(array $parameters, DataHandler $dataHandler): array public function doFilter(array $parameters): array
{ {
return array_filter($parameters['values'], [$this, 'filterPage']); return array_filter($parameters['values'], [$this, 'filterPage']);
} }

View file

@ -7,6 +7,7 @@ namespace DanielSiepmann\Configuration;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use WerkraumMedia\ABTest\Events\SwitchedToVariant; use WerkraumMedia\ABTest\Events\SwitchedToVariant;
use WerkraumMedia\ABTest\Hook\TypoScriptFrontendController; use WerkraumMedia\ABTest\Hook\TypoScriptFrontendController;
use WerkraumMedia\ABTest\MatomoTracker;
use WerkraumMedia\ABTest\Middleware\SetCookie; use WerkraumMedia\ABTest\Middleware\SetCookie;
use WerkraumMedia\ABTest\TCA\VariantFilter; use WerkraumMedia\ABTest\TCA\VariantFilter;
@ -26,4 +27,8 @@ return static function (ContainerConfigurator $containerConfigurator) {
'method' => 'handleVariant', 'method' => 'handleVariant',
'event' => SwitchedToVariant::class, 'event' => SwitchedToVariant::class,
]); ]);
$services->set(MatomoTracker::class)->tag('event.listener', [
'method' => 'handleVariant',
'event' => SwitchedToVariant::class,
]);
}; };

View file

@ -10,6 +10,7 @@
'tx_abtest_variant' => [ 'tx_abtest_variant' => [
'exclude' => 1, 'exclude' => 1,
'label' => $languagePath . 'tx_abtest_variant', 'label' => $languagePath . 'tx_abtest_variant',
'description' => $languagePath . 'tx_abtest_variant.description',
'config' => [ 'config' => [
'type' => 'group', 'type' => 'group',
'allowed' => 'pages', 'allowed' => 'pages',
@ -31,6 +32,7 @@
'tx_abtest_cookie_time' => [ 'tx_abtest_cookie_time' => [
'exclude' => 1, 'exclude' => 1,
'label' => $languagePath . 'tx_abtest_cookie_time', 'label' => $languagePath . 'tx_abtest_cookie_time',
'description' => $languagePath . 'tx_abtest_cookie_time.description',
'config' => [ 'config' => [
'type' => 'input', 'type' => 'input',
'eval' => 'int', 'eval' => 'int',
@ -50,17 +52,52 @@
'tx_abtest_counter' => [ 'tx_abtest_counter' => [
'exclude' => 1, 'exclude' => 1,
'label' => $languagePath . 'tx_abtest_counter', 'label' => $languagePath . 'tx_abtest_counter',
'description' => $languagePath . 'tx_abtest_counter.description',
'config' => [ 'config' => [
'type' => 'input', 'type' => 'input',
'eval' => 'int', 'eval' => 'int',
'size' => 10, 'size' => 10,
], ],
], ],
'tx_abtest_matomo_experiment_id' => [
'exclude' => 1,
'label' => $languagePath . 'tx_abtest_matomo_experiment_id',
'description' => $languagePath . 'tx_abtest_matomo_experiment_id.description',
'config' => [
'type' => 'input',
'eval' => 'nospace',
],
],
'tx_abtest_matomo_variant_id' => [
'exclude' => 1,
'label' => $languagePath . 'tx_abtest_matomo_variant_id',
'description' => $languagePath . 'tx_abtest_matomo_variant_id.description',
'config' => [
'type' => 'input',
'eval' => 'nospace',
'valuePicker' => [
'items' => [
[$languagePath . 'tx_abtest_matomo_variant_id.original', 'original'],
],
],
],
],
]); ]);
$GLOBALS['TCA'][$tableName]['palettes']['tx_abtest_matomo'] = [
'showitem' => 'tx_abtest_matomo_experiment_id, tx_abtest_matomo_variant_id',
];
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes( \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes(
$tableName, $tableName,
'--div--;' . $languagePath . 'div_title,tx_abtest_variant,tx_abtest_cookie_time,tx_abtest_counter', implode(',', [
'--div--;' . $languagePath . 'div_title',
'tx_abtest_variant',
'tx_abtest_cookie_time',
'tx_abtest_counter',
'--palette--;' . $languagePath . 'palette_tx_abtest_matomo;tx_abtest_matomo',
]),
'', '',
'after:content_from_pid' 'after:content_from_pid'
); );

View file

@ -21,8 +21,17 @@ Additional header information may be specified both for the original version as
![Demo](https://raw.githubusercontent.com/werkraum-media/abtest/master/Documentation/Images/demo.gif) ![Demo](https://raw.githubusercontent.com/werkraum-media/abtest/master/Documentation/Images/demo.gif)
### Matomo A/B integration
Provides an integration for "A/B Testing - Experiments" https://matomo.org/a-b-testing/.
This is currently enabled out of the box and integrated into this extension.
That is because we need this for one of our customers.
We didn't think it is worth it to split it up into this own extension right now.
You can disable the corresponding event listener and hide the corresponding fields.
### Known issues ### Known issues
This extension currently does not support typeNum. This extension currently does not support typeNum.
It always checks requested page for a variant. It always checks requested page for a variant, and it always adds the tracking code.

View file

@ -11,10 +11,18 @@
<source>Variant</source> <source>Variant</source>
<target>Variante</target> <target>Variante</target>
</trans-unit> </trans-unit>
<trans-unit id="pages.tx_abtest_variant.description" approved="yes">
<source>The selected page will be used as Variation of this page.</source>
<target>Die ausgewählte Seite wird als Variante dieser Seite genutzt.</target>
</trans-unit>
<trans-unit id="pages.tx_abtest_cookie_time" approved="yes"> <trans-unit id="pages.tx_abtest_cookie_time" approved="yes">
<source>Cookie Lifetime</source> <source>Cookie Lifetime</source>
<target>Cookie Lebenszeit</target> <target>Cookie Lebenszeit</target>
</trans-unit> </trans-unit>
<trans-unit id="pages.tx_abtest_cookie_time.description" approved="yes">
<source>Defines how long the selected variation should be saved on the visitors end. The visitor will see the same variation until end of this period.</source>
<target>Definiert wie lange ein Besucher die ausgewählte Variante sehen soll. Nach Ablauf der Zeit wird die anzuzeigende Variante neu bestimmt.</target>
</trans-unit>
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_month" approved="yes"> <trans-unit id="pages.tx_abtest_cookie_time.cookie_1_month" approved="yes">
<source>1 month</source> <source>1 month</source>
<target>1 Monat</target> <target>1 Monat</target>
@ -43,6 +51,35 @@
<source>Counter</source> <source>Counter</source>
<target>Zähler</target> <target>Zähler</target>
</trans-unit> </trans-unit>
<trans-unit id="pages.tx_abtest_counter.description" approved="yes">
<source>Only technical. defines how often the page was shown to visitors. Used to determine whether to show this or a variant to next visitor.</source>
<target>Rein technisch. Definiert wie viele Besucher diese Seite gesehen haben. Wird genutzt um zu bestimmen ob diese Seite oder eine Variante angezeigt werden soll.</target>
</trans-unit>
<trans-unit id="pages.palette_tx_abtest_matomo" approved="yes">
<source>Matomo</source>
<target>Matomo</target>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_experiment_id" approved="yes">
<source>Matomo Experiment ID</source>
<target>Matomo Experiment ID</target>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_experiment_id.description" approved="yes">
<source>Either the name of the experiment, or the technical ID to not expose the name in source code.</source>
<target>Entweder der Name ohne Leerzeichen, so wie er in der Matomo UI angezeigt wird. Oder die technische ID, um den Namen nicht im Quellcode auszugeben.</target>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_variant_id" approved="yes">
<source>Matomo Variant ID</source>
<target>Matomo Varianten ID</target>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_variant_id.description" approved="yes">
<source>Either the name of the variant, or the technical ID to not expose the name in source code.</source>
<target>Entweder der Name ohne Leerzeichen, so wie er in der Matomo UI angezeigt wird. Oder die technische ID, um den Namen nicht im Quellcode auszugeben.</target>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_variant_id.original" approved="yes">
<source>Original</source>
<target>Original</target>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -9,9 +9,15 @@
<trans-unit id="pages.tx_abtest_variant" approved="yes"> <trans-unit id="pages.tx_abtest_variant" approved="yes">
<source>Variant</source> <source>Variant</source>
</trans-unit> </trans-unit>
<trans-unit id="pages.tx_abtest_variant.description" approved="yes">
<source>The selected page will be used as Variation of this page.</source>
</trans-unit>
<trans-unit id="pages.tx_abtest_cookie_time" approved="yes"> <trans-unit id="pages.tx_abtest_cookie_time" approved="yes">
<source>Cookie Lifetime</source> <source>Cookie Lifetime</source>
</trans-unit> </trans-unit>
<trans-unit id="pages.tx_abtest_cookie_time.description" approved="yes">
<source>Defines how long the selected variation should be saved on the visitors end. The visitor will see the same variation until end of this period.</source>
</trans-unit>
<trans-unit id="pages.tx_abtest_cookie_time.cookie_1_month" approved="yes"> <trans-unit id="pages.tx_abtest_cookie_time.cookie_1_month" approved="yes">
<source>1 month</source> <source>1 month</source>
</trans-unit> </trans-unit>
@ -33,6 +39,28 @@
<trans-unit id="pages.tx_abtest_counter" approved="yes"> <trans-unit id="pages.tx_abtest_counter" approved="yes">
<source>Counter</source> <source>Counter</source>
</trans-unit> </trans-unit>
<trans-unit id="pages.tx_abtest_counter.description" approved="yes">
<source>Only technical. Defines how often the page was shown to visitors. Used to determine whether to show this or a variant to next visitor.</source>
</trans-unit>
<trans-unit id="pages.palette_tx_abtest_matomo" approved="yes">
<source>Matomo</source>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_experiment_id" approved="yes">
<source>Matomo Experiment ID</source>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_experiment_id.description" approved="yes">
<source>Either the name of the experiment, or the technical ID to not expose the name in source code.</source>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_variant_id" approved="yes">
<source>Matomo Variant ID</source>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_variant_id.description" approved="yes">
<source>Either the name of the variant, or the technical ID to not expose the name in source code.</source>
</trans-unit>
<trans-unit id="pages.tx_abtest_matomo_variant_id.original" approved="yes">
<source>Original</source>
</trans-unit>
</body> </body>
</file> </file>
</xliff> </xliff>

View file

@ -0,0 +1,8 @@
"pages"
,"uid","pid","slug","title",tx_abtest_variant,hidden,tx_abtest_cookie_time,tx_abtest_matomo_experiment_id,tx_abtest_matomo_variant_id
,1,0,"/","Page 1 Title (No Variant)",0,0,604800,,
,2,1,"/page-2","Page 2 Title (Variant A)",3,0,604800,TestForDevelopment,VariationA
,3,1,"/page-3","Page 3 Title (Variant B)",0,0,604800,TestForDevelopment,VariationB
"sys_template"
,"uid","pid","root","clear","constants","config"
,1,1,1,3,"databasePlatform = mysql","<INCLUDE_TYPOSCRIPT: source=""FILE:EXT:abtest/Tests/Fixtures/FrontendRendering.typoscript"">"
Can't render this file because it contains an unexpected character in line 2 and column 2.

View file

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2023 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ABTest\Tests\Functional;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalRequest;
use TYPO3\TestingFramework\Core\Functional\Framework\Frontend\InternalResponse;
use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
class MatomoTrackingTest extends FunctionalTestCase
{
protected $testExtensionsToLoad = [
'typo3conf/ext/abtest',
];
protected $pathsToLinkInTestInstance = [
'typo3conf/ext/abtest/Tests/Fixtures/Sites' => 'typo3conf/sites',
];
protected function setUp(): void
{
parent::setUp();
$this->setUpBackendUserFromFixture(1);
$this->importCSVDataSet(__DIR__ . '/../Fixtures/BasicMatomoDatabase.csv');
}
/**
* @test
*/
public function rendersPageWithoutVariantWithoutMatomo(): void
{
$request = new InternalRequest();
$request = $request->withPageId(1);
$response = $this->executeFrontendRequest($request);
self::assertSame(200, $response->getStatusCode());
self::assertStringContainsString('Page 1 Title (No Variant)', $response->getBody()->__toString());
$this->assertNoMatomoTrackingCode($response);
}
/**
* @test
*/
public function rendersVariantAWithMatomo(): void
{
$request = new InternalRequest();
$request = $request->withPageId(2);
$response = $this->executeFrontendRequest($request);
self::assertSame(200, $response->getStatusCode());
self::assertStringContainsString('Page 2 Title (Variant A)', $response->getBody()->__toString());
$this->assertMatomoTrackingCode($response, 'TestForDevelopment', 'VariationA');
}
/**
* @test
*/
public function rendersVariantBWithMatomo(): void
{
$request = new InternalRequest();
$request = $request->withPageId(2);
$request = $request->withAddedHeader('Cookie', 'ab-2=3');
$response = $this->executeFrontendRequest($request);
self::assertSame(200, $response->getStatusCode());
self::assertStringContainsString('Page 3 Title (Variant B)', $response->getBody()->__toString());
$this->assertMatomoTrackingCode($response, 'TestForDevelopment', 'VariationB');
}
private function assertNoMatomoTrackingCode(InternalResponse $response): void
{
self::assertStringNotContainsString('_paq.push', $response->getBody()->__toString());
}
private function assertMatomoTrackingCode(
InternalResponse $response,
string $experiment,
string $variation
): void {
self::assertStringContainsString("_paq.push(['AbTesting::enter', {experiment: '$experiment', variation: '$variation'}]);", $response->getBody()->__toString());
}
}

View file

@ -1,5 +1,8 @@
CREATE TABLE pages ( CREATE TABLE pages (
tx_abtest_variant int(11) DEFAULT '0' NOT NULL, tx_abtest_variant int(11) DEFAULT '0' NOT NULL,
tx_abtest_cookie_time int(11) DEFAULT 604800 NOT NULL, tx_abtest_cookie_time int(11) DEFAULT 604800 NOT NULL,
tx_abtest_counter int(11) DEFAULT '0' NOT NULL tx_abtest_counter int(11) DEFAULT '0' NOT NULL,
tx_abtest_matomo_experiment_id varchar(255) DEFAULT '' NOT NULL,
tx_abtest_matomo_variant_id varchar(255) DEFAULT '' NOT NULL,
); );