diff --git a/Classes/Hook/TypoScriptFrontendController.php b/Classes/Hook/TypoScriptFrontendController.php index 0d69e16..a42e4d1 100644 --- a/Classes/Hook/TypoScriptFrontendController.php +++ b/Classes/Hook/TypoScriptFrontendController.php @@ -24,16 +24,21 @@ declare(strict_types=1); namespace WerkraumMedia\ABTest\Hook; use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController as Typo3TypoScriptFrontendController; +use WerkraumMedia\ABTest\MatomoTracker; use WerkraumMedia\ABTest\Switcher; class TypoScriptFrontendController { private Switcher $switcher; + private MatomoTracker $matomoTracker; + public function __construct( - Switcher $switcher + Switcher $switcher, + MatomoTracker $matomoTracker ) { $this->switcher = $switcher; + $this->matomoTracker = $matomoTracker; } public function determineIdPostProc( @@ -43,8 +48,16 @@ class TypoScriptFrontendController $this->switcher->switch($frontendController); } + public function contentPostProcAll( + array $params, + Typo3TypoScriptFrontendController $frontendController + ): void { + $frontendController->content = $this->matomoTracker->addScriptToHtmlMarkup($frontendController->content); + } + 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']['contentPostProc-all'][self::class] = self::class . '->contentPostProcAll'; } } diff --git a/Classes/MatomoTracker.php b/Classes/MatomoTracker.php new file mode 100644 index 0000000..ec47c4d --- /dev/null +++ b/Classes/MatomoTracker.php @@ -0,0 +1,62 @@ + + * + * 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('', $script . '', $markup); + } + + private function generateScript(): string + { + $experiment = htmlspecialchars($this->experiment); + $variation = htmlspecialchars($this->variation); + + return '' + . PHP_EOL + ; + } +} diff --git a/Classes/TCA/VariantFilter.php b/Classes/TCA/VariantFilter.php index 577bbd5..66ef0a1 100644 --- a/Classes/TCA/VariantFilter.php +++ b/Classes/TCA/VariantFilter.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace WerkraumMedia\ABTest\TCA; -use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\Domain\Repository\PageRepository; class VariantFilter @@ -36,7 +35,7 @@ class VariantFilter $this->pageRepository = $pageRepository; } - public function doFilter(array $parameters, DataHandler $dataHandler): array + public function doFilter(array $parameters): array { return array_filter($parameters['values'], [$this, 'filterPage']); } diff --git a/Configuration/Services.php b/Configuration/Services.php index 85de5c9..4646a45 100644 --- a/Configuration/Services.php +++ b/Configuration/Services.php @@ -7,6 +7,7 @@ namespace DanielSiepmann\Configuration; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use WerkraumMedia\ABTest\Events\SwitchedToVariant; use WerkraumMedia\ABTest\Hook\TypoScriptFrontendController; +use WerkraumMedia\ABTest\MatomoTracker; use WerkraumMedia\ABTest\Middleware\SetCookie; use WerkraumMedia\ABTest\TCA\VariantFilter; @@ -26,4 +27,8 @@ return static function (ContainerConfigurator $containerConfigurator) { 'method' => 'handleVariant', 'event' => SwitchedToVariant::class, ]); + $services->set(MatomoTracker::class)->tag('event.listener', [ + 'method' => 'handleVariant', + 'event' => SwitchedToVariant::class, + ]); }; diff --git a/Configuration/TCA/Overrides/pages.php b/Configuration/TCA/Overrides/pages.php index 56b1b40..1575ccd 100644 --- a/Configuration/TCA/Overrides/pages.php +++ b/Configuration/TCA/Overrides/pages.php @@ -10,6 +10,7 @@ 'tx_abtest_variant' => [ 'exclude' => 1, 'label' => $languagePath . 'tx_abtest_variant', + 'description' => $languagePath . 'tx_abtest_variant.description', 'config' => [ 'type' => 'group', 'allowed' => 'pages', @@ -31,6 +32,7 @@ 'tx_abtest_cookie_time' => [ 'exclude' => 1, 'label' => $languagePath . 'tx_abtest_cookie_time', + 'description' => $languagePath . 'tx_abtest_cookie_time.description', 'config' => [ 'type' => 'input', 'eval' => 'int', @@ -50,17 +52,52 @@ 'tx_abtest_counter' => [ 'exclude' => 1, 'label' => $languagePath . 'tx_abtest_counter', + 'description' => $languagePath . 'tx_abtest_counter.description', 'config' => [ 'type' => 'input', 'eval' => 'int', '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( $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' ); diff --git a/README.md b/README.md index 71e7182..0036bb8 100644 --- a/README.md +++ b/README.md @@ -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) +### 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 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. diff --git a/Resources/Private/Language/de.locallang_db.xlf b/Resources/Private/Language/de.locallang_db.xlf index fe87edf..d379614 100644 --- a/Resources/Private/Language/de.locallang_db.xlf +++ b/Resources/Private/Language/de.locallang_db.xlf @@ -11,10 +11,18 @@ Variant Variante + + The selected page will be used as Variation of this page. + Die ausgewählte Seite wird als Variante dieser Seite genutzt. + Cookie Lifetime Cookie Lebenszeit + + 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. + Definiert wie lange ein Besucher die ausgewählte Variante sehen soll. Nach Ablauf der Zeit wird die anzuzeigende Variante neu bestimmt. + 1 month 1 Monat @@ -43,6 +51,35 @@ Counter Zähler + + Only technical. defines how often the page was shown to visitors. Used to determine whether to show this or a variant to next visitor. + Rein technisch. Definiert wie viele Besucher diese Seite gesehen haben. Wird genutzt um zu bestimmen ob diese Seite oder eine Variante angezeigt werden soll. + + + + Matomo + Matomo + + + Matomo Experiment ID + Matomo Experiment ID + + + Either the name of the experiment, or the technical ID to not expose the name in source code. + 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. + + + Matomo Variant ID + Matomo Varianten ID + + + Either the name of the variant, or the technical ID to not expose the name in source code. + 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. + + + Original + Original + diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index d5ed083..06ecaaf 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -9,9 +9,15 @@ Variant + + The selected page will be used as Variation of this page. + Cookie Lifetime + + 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. + 1 month @@ -33,6 +39,28 @@ Counter + + Only technical. Defines how often the page was shown to visitors. Used to determine whether to show this or a variant to next visitor. + + + + Matomo + + + Matomo Experiment ID + + + Either the name of the experiment, or the technical ID to not expose the name in source code. + + + Matomo Variant ID + + + Either the name of the variant, or the technical ID to not expose the name in source code. + + + Original + diff --git a/Tests/Fixtures/BasicMatomoDatabase.csv b/Tests/Fixtures/BasicMatomoDatabase.csv new file mode 100644 index 0000000..5d5f76f --- /dev/null +++ b/Tests/Fixtures/BasicMatomoDatabase.csv @@ -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","" diff --git a/Tests/Functional/MatomoTrackingTest.php b/Tests/Functional/MatomoTrackingTest.php new file mode 100644 index 0000000..d666aa1 --- /dev/null +++ b/Tests/Functional/MatomoTrackingTest.php @@ -0,0 +1,104 @@ + + * + * 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()); + } +} diff --git a/ext_tables.sql b/ext_tables.sql index 15a3037..8d126a1 100644 --- a/ext_tables.sql +++ b/ext_tables.sql @@ -1,5 +1,8 @@ CREATE TABLE pages ( tx_abtest_variant int(11) DEFAULT '0' 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, );