From 3371a36b043cef8d7563a709f8b92dbbec7a2129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Fri, 14 Jul 2023 14:09:28 +0200 Subject: [PATCH] Add filters --- .github/workflows/build.yaml | 63 ++++++++++++++++--- README.md | 3 + composer-require-checker.json | 1 - composer-unused.php | 12 ++++ composer.json | 18 +++--- src/DependencyInjection/Configuration.php | 24 +++++++ .../SetonoGoogleAnalyticsExtension.php | 15 ++++- src/EventSubscriber/Tag/EventSubscriber.php | 14 ++++- src/EventSubscriber/Tag/LibrarySubscriber.php | 13 +++- .../ClientSide/ClientSideFilterInterface.php | 16 +++++ .../ClientSide/CompositeClientSideFilter.php | 24 +++++++ .../HostnameBasedClientSideFilter.php | 61 ++++++++++++++++++ .../ClientSide/PathBasedClientSideFilter.php | 63 +++++++++++++++++++ src/Filter/Matcher/EqualityMatcher.php | 18 ++++++ src/Filter/Matcher/FnMatcher.php | 18 ++++++ src/Filter/Matcher/GlobMatcher.php | 21 +++++++ src/Filter/Matcher/MatcherInterface.php | 18 ++++++ src/Filter/Matcher/RegExpMatcher.php | 18 ++++++ src/Resources/config/services.xml | 2 + .../conditional/library_event_subscriber.xml | 1 + .../config/services/event_subscriber.xml | 1 + src/Resources/config/services/filter.xml | 31 +++++++++ src/SetonoGoogleAnalyticsBundle.php | 9 +++ src/ValueObject/Property.php | 5 +- .../SetonoGoogleAnalyticsExtensionTest.php | 12 ++++ .../HostnameBasedClientSideFilterTest.php | 44 +++++++++++++ .../PathBasedClientSideFilterTest.php | 58 +++++++++++++++++ 27 files changed, 558 insertions(+), 25 deletions(-) create mode 100644 composer-unused.php create mode 100644 src/Filter/ClientSide/ClientSideFilterInterface.php create mode 100644 src/Filter/ClientSide/CompositeClientSideFilter.php create mode 100644 src/Filter/ClientSide/HostnameBasedClientSideFilter.php create mode 100644 src/Filter/ClientSide/PathBasedClientSideFilter.php create mode 100644 src/Filter/Matcher/EqualityMatcher.php create mode 100644 src/Filter/Matcher/FnMatcher.php create mode 100644 src/Filter/Matcher/GlobMatcher.php create mode 100644 src/Filter/Matcher/MatcherInterface.php create mode 100644 src/Filter/Matcher/RegExpMatcher.php create mode 100644 src/Resources/config/services/filter.xml create mode 100644 tests/Unit/Filter/ClientSide/HostnameBasedClientSideFilterTest.php create mode 100644 tests/Unit/Filter/ClientSide/PathBasedClientSideFilterTest.php diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e097ee1..3263817 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -54,6 +54,7 @@ jobs: matrix: php-version: - "7.4" + - "8.0" - "8.1" - "8.2" @@ -62,11 +63,26 @@ jobs: symfony: - "^5.4" - - "^6.0" + - "~6.0.0" + - "~6.1.0" + - "~6.2.0" + - "~6.3.0" exclude: - php-version: "7.4" - symfony: "^6.0" + symfony: "~6.0.0" + - php-version: "7.4" + symfony: "~6.1.0" + - php-version: "7.4" + symfony: "~6.2.0" + - php-version: "7.4" + symfony: "~6.3.0" + - php-version: "8.0" + symfony: "~6.1.0" + - php-version: "8.0" + symfony: "~6.2.0" + - php-version: "8.0" + symfony: "~6.3.0" steps: - name: "Checkout" @@ -91,7 +107,7 @@ jobs: run: "composer-require-checker check --config-file=$(pwd)/composer-require-checker.json" - name: "Run composer-unused/composer-unused" - run: "composer-unused || true" # todo remove when https://github.com/shivammathur/setup-php/issues/703 is fixed + run: "composer-unused" static-code-analysis: name: "Static Code Analysis" @@ -102,6 +118,7 @@ jobs: matrix: php-version: - "7.4" + - "8.0" - "8.1" - "8.2" @@ -110,11 +127,26 @@ jobs: symfony: - "^5.4" - - "^6.0" + - "~6.0.0" + - "~6.1.0" + - "~6.2.0" + - "~6.3.0" exclude: - php-version: "7.4" - symfony: "^6.0" + symfony: "~6.0.0" + - php-version: "7.4" + symfony: "~6.1.0" + - php-version: "7.4" + symfony: "~6.2.0" + - php-version: "7.4" + symfony: "~6.3.0" + - php-version: "8.0" + symfony: "~6.1.0" + - php-version: "8.0" + symfony: "~6.2.0" + - php-version: "8.0" + symfony: "~6.3.0" steps: - name: "Checkout" @@ -147,20 +179,35 @@ jobs: matrix: php-version: - "7.4" + - "8.0" - "8.1" - "8.2" dependencies: - - "lowest" - "highest" symfony: - "^5.4" - - "^6.0" + - "~6.0.0" + - "~6.1.0" + - "~6.2.0" + - "~6.3.0" exclude: - php-version: "7.4" - symfony: "^6.0" + symfony: "~6.0.0" + - php-version: "7.4" + symfony: "~6.1.0" + - php-version: "7.4" + symfony: "~6.2.0" + - php-version: "7.4" + symfony: "~6.3.0" + - php-version: "8.0" + symfony: "~6.1.0" + - php-version: "8.0" + symfony: "~6.2.0" + - php-version: "8.0" + symfony: "~6.3.0" steps: - name: "Checkout" diff --git a/README.md b/README.md index 434c1b8..3eade6c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Latest Version][ico-version]][link-packagist] [![Software License][ico-license]](LICENSE) [![Build Status][ico-github-actions]][link-github-actions] +[![Code Coverage][ico-code-coverage]][link-code-coverage] Use Google Analytics in your Symfony application. Under the hood this bundle integrates the [Google Analytics measurement protocol](https://github.com/Setono/google-analytics-measurement-protocol) library. @@ -30,6 +31,8 @@ TODO [ico-version]: https://poser.pugx.org/setono/google-analytics-bundle/v/stable [ico-license]: https://poser.pugx.org/setono/google-analytics-bundle/license [ico-github-actions]: https://github.com/Setono/GoogleAnalyticsBundle/workflows/build/badge.svg +[ico-code-coverage]: https://codecov.io/gh/Setono/GoogleAnalyticsBundle/branch/master/graph/badge.svg [link-packagist]: https://packagist.org/packages/setono/google-analytics-bundle [link-github-actions]: https://github.com/Setono/GoogleAnalyticsBundle/actions +[link-code-coverage]: https://codecov.io/gh/Setono/GoogleAnalyticsBundle diff --git a/composer-require-checker.json b/composer-require-checker.json index 507ee90..21fa35f 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -15,7 +15,6 @@ "string", "true", "void", - "str_starts_with", "Setono\\Consent\\Consents", "Setono\\ConsentBundle\\SetonoConsentBundle", "Setono\\Consent\\ConsentCheckerInterface" diff --git a/composer-unused.php b/composer-unused.php new file mode 100644 index 0000000..dfd990a --- /dev/null +++ b/composer-unused.php @@ -0,0 +1,12 @@ +addNamedFilter(NamedFilter::fromString('setono/tag-bag-bundle')) + ; +}; diff --git a/composer.json b/composer.json index 5521b16..343ef6f 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "ext-json": "*", "psr/event-dispatcher": "^1.0", "psr/log": "^1.1 || ^2.0 || ^3.0", + "setono/composite-compiler-pass": "^1.1", "setono/google-analytics-measurement-protocol": "^1.0.0-alpha.8", "setono/tag-bag": "^2.2", "setono/tag-bag-bundle": "^3.0", @@ -21,21 +22,22 @@ "symfony/dependency-injection": "^5.4 || ^6.0", "symfony/event-dispatcher": "^5.4 || ^6.0", "symfony/event-dispatcher-contracts": "^2.5 || ^3.2", + "symfony/filesystem": "^5.4 || ^6.0", "symfony/http-foundation": "^5.4 || ^6.0", "symfony/http-kernel": "^5.4 || ^6.0", "symfony/messenger": "^5.4 || ^6.0", - "webmozart/assert": "^1.11" + "webmozart/assert": "^1.11", + "webmozart/glob": "^4.6" }, "require-dev": { - "kriswallsmith/buzz": "^1.2", - "matthiasnoback/symfony-dependency-injection-test": "^4.3", + "kriswallsmith/buzz": "^1.2.1", + "matthiasnoback/symfony-dependency-injection-test": "^4.3.1", "nyholm/psr7": "^1.5", "nyholm/symfony-bundle-test": "^2.0", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^9.5", - "psalm/plugin-phpunit": "^0.18", - "psalm/plugin-symfony": "^5.0", - "roave/security-advisories": "dev-latest", + "phpspec/prophecy-phpunit": "^2.0.2", + "phpunit/phpunit": "^9.6.10", + "psalm/plugin-phpunit": "^0.18.4", + "psalm/plugin-symfony": "^5.0.3", "setono/code-quality-pack": "^2.4", "setono/consent-bundle": "^1.0" }, diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 749dd58..f62c506 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -47,6 +47,30 @@ public function getConfigTreeBuilder(): TreeBuilder ->info('The container id. Looks something like: GTM-WMF5KF') ->isRequired() ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('filters') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('client_side') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('paths') + ->scalarPrototype() + ->info('A path to exclude from client side tracking. You can use glob matching (see https://github.com/webmozarts/glob), regular expressions (must start with #) or exact matching. If you want to exclude your admin area located at /admin, you could input #/admin(/.*)?# here.') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->end() + ->arrayNode('hostnames') + ->scalarPrototype() + ->info('A hostname to exclude from client side tracking. You can use fn matching (see https://www.php.net/manual/en/function.fnmatch.php), regular expressions (must start with #) or exact matching. If you want to exclude all subdomains for example.com you would add *.example.com.') + ->isRequired() + ->cannotBeEmpty() ; $consentNode = $rootNode->children()->arrayNode('consent'); diff --git a/src/DependencyInjection/SetonoGoogleAnalyticsExtension.php b/src/DependencyInjection/SetonoGoogleAnalyticsExtension.php index db8567a..a92a618 100644 --- a/src/DependencyInjection/SetonoGoogleAnalyticsExtension.php +++ b/src/DependencyInjection/SetonoGoogleAnalyticsExtension.php @@ -4,6 +4,7 @@ namespace Setono\GoogleAnalyticsBundle\DependencyInjection; +use Setono\GoogleAnalyticsBundle\Filter\ClientSide\ClientSideFilterInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -17,7 +18,13 @@ public function load(array $configs, ContainerBuilder $container): void /** * @psalm-suppress PossiblyNullArgument * - * @var array{inject_library: bool, gtag: array{enabled: bool, properties: array}, tag_manager: array{enabled: bool, containers: array}, consent: array{enabled: bool}} $config + * @var array{ + * inject_library: bool, + * gtag: array{enabled: bool, properties: array}, + * tag_manager: array{enabled: bool, containers: array}, + * filters: array{client_side: array{paths: list, hostnames: list}}, + * consent: array{enabled: bool} + * } $config */ $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); @@ -28,6 +35,8 @@ public function load(array $configs, ContainerBuilder $container): void $container->setParameter('setono_google_analytics.consent_enabled', $config['consent']['enabled']); $container->setParameter('setono_google_analytics.gtag_enabled', $config['gtag']['enabled']); $container->setParameter('setono_google_analytics.tag_manager_enabled', $config['tag_manager']['enabled']); + $container->setParameter('setono_google_analytics.filters.client_side.paths', $config['filters']['client_side']['paths']); + $container->setParameter('setono_google_analytics.filters.client_side.hostnames', $config['filters']['client_side']['hostnames']); if (true === $config['tag_manager']['enabled'] && true === $config['gtag']['enabled']) { throw new \InvalidArgumentException('You cannot enable both gtag and tag_manager at the same time.'); @@ -56,6 +65,10 @@ public function load(array $configs, ContainerBuilder $container): void } else { $loader->load('services/conditional/consent_disabled.xml'); } + + $container->registerForAutoconfiguration(ClientSideFilterInterface::class) + ->addTag('setono_google_analytics.client_side_filter') + ; } public function prepend(ContainerBuilder $container): void diff --git a/src/EventSubscriber/Tag/EventSubscriber.php b/src/EventSubscriber/Tag/EventSubscriber.php index 3eaa0a9..f4115c4 100644 --- a/src/EventSubscriber/Tag/EventSubscriber.php +++ b/src/EventSubscriber/Tag/EventSubscriber.php @@ -5,6 +5,7 @@ namespace Setono\GoogleAnalyticsBundle\EventSubscriber\Tag; use Setono\GoogleAnalyticsBundle\Event\ClientSideEvent; +use Setono\GoogleAnalyticsBundle\Filter\ClientSide\ClientSideFilterInterface; use Setono\GoogleAnalyticsBundle\Strategy\CollectionStrategyInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -12,9 +13,14 @@ final class EventSubscriber implements EventSubscriberInterface { private CollectionStrategyInterface $collectionStrategy; - public function __construct(CollectionStrategyInterface $collectionStrategy) - { + private ClientSideFilterInterface $clientSideFilter; + + public function __construct( + CollectionStrategyInterface $collectionStrategy, + ClientSideFilterInterface $clientSideFilter + ) { $this->collectionStrategy = $collectionStrategy; + $this->clientSideFilter = $clientSideFilter; } public static function getSubscribedEvents(): array @@ -26,6 +32,10 @@ public static function getSubscribedEvents(): array public function add(ClientSideEvent $clientSideEvent): void { + if (!$this->clientSideFilter->filter(['caller' => self::class, 'event' => $clientSideEvent])) { + return; + } + $this->collectionStrategy->addEvent($clientSideEvent->event); } } diff --git a/src/EventSubscriber/Tag/LibrarySubscriber.php b/src/EventSubscriber/Tag/LibrarySubscriber.php index b05a8b2..75d69cd 100644 --- a/src/EventSubscriber/Tag/LibrarySubscriber.php +++ b/src/EventSubscriber/Tag/LibrarySubscriber.php @@ -4,6 +4,7 @@ namespace Setono\GoogleAnalyticsBundle\EventSubscriber\Tag; +use Setono\GoogleAnalyticsBundle\Filter\ClientSide\ClientSideFilterInterface; use Setono\GoogleAnalyticsBundle\Strategy\CollectionStrategyInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\RequestEvent; @@ -13,11 +14,17 @@ final class LibrarySubscriber implements EventSubscriberInterface { private CollectionStrategyInterface $collectionStrategy; + private ClientSideFilterInterface $clientSideFilter; + private bool $injectLibrary; - public function __construct(CollectionStrategyInterface $collectionStrategy, bool $injectLibrary) - { + public function __construct( + CollectionStrategyInterface $collectionStrategy, + ClientSideFilterInterface $clientSideFilter, + bool $injectLibrary + ) { $this->collectionStrategy = $collectionStrategy; + $this->clientSideFilter = $clientSideFilter; $this->injectLibrary = $injectLibrary; } @@ -30,7 +37,7 @@ public static function getSubscribedEvents(): array public function add(RequestEvent $event): void { - if (!$this->injectLibrary || !$event->isMainRequest()) { + if (!$this->injectLibrary || !$event->isMainRequest() || !$this->clientSideFilter->filter(['caller' => self::class])) { return; } diff --git a/src/Filter/ClientSide/ClientSideFilterInterface.php b/src/Filter/ClientSide/ClientSideFilterInterface.php new file mode 100644 index 0000000..88a52e7 --- /dev/null +++ b/src/Filter/ClientSide/ClientSideFilterInterface.php @@ -0,0 +1,16 @@ + $context + */ + public function filter(array $context = []): bool; +} diff --git a/src/Filter/ClientSide/CompositeClientSideFilter.php b/src/Filter/ClientSide/CompositeClientSideFilter.php new file mode 100644 index 0000000..e1acd8d --- /dev/null +++ b/src/Filter/ClientSide/CompositeClientSideFilter.php @@ -0,0 +1,24 @@ + + */ +final class CompositeClientSideFilter extends CompositeService implements ClientSideFilterInterface +{ + public function filter(array $context = []): bool + { + foreach ($this->services as $service) { + if (!$service->filter($context)) { + return false; + } + } + + return true; + } +} diff --git a/src/Filter/ClientSide/HostnameBasedClientSideFilter.php b/src/Filter/ClientSide/HostnameBasedClientSideFilter.php new file mode 100644 index 0000000..3a7a7a9 --- /dev/null +++ b/src/Filter/ClientSide/HostnameBasedClientSideFilter.php @@ -0,0 +1,61 @@ + */ + private array $matchers; + + private RequestStack $requestStack; + + /** @var list */ + private array $hostnames; + + /** + * @param list $hostnames + */ + public function __construct(RequestStack $requestStack, array $hostnames) + { + $this->matchers = [ + new EqualityMatcher(), + new FnMatcher(), + new RegExpMatcher(), + ]; + + $this->requestStack = $requestStack; + $this->hostnames = $hostnames; + } + + public function filter(array $context = []): bool + { + $request = $this->requestStack->getMainRequest(); + if (null === $request) { + return true; + } + + $hostname = $request->getHttpHost(); + + foreach ($this->hostnames as $excludedHostname) { + foreach ($this->matchers as $matcher) { + if ($matcher->supports($excludedHostname) && $matcher->matches($hostname, $excludedHostname)) { + return false; + } + } + } + + return true; + } +} diff --git a/src/Filter/ClientSide/PathBasedClientSideFilter.php b/src/Filter/ClientSide/PathBasedClientSideFilter.php new file mode 100644 index 0000000..ae72899 --- /dev/null +++ b/src/Filter/ClientSide/PathBasedClientSideFilter.php @@ -0,0 +1,63 @@ + */ + private array $matchers; + + private RequestStack $requestStack; + + /** @var list */ + private array $paths; + + /** + * @param list $paths + */ + public function __construct(RequestStack $requestStack, array $paths) + { + $this->matchers = [ + new EqualityMatcher(), + new GlobMatcher(), + new RegExpMatcher(), + ]; + + $this->requestStack = $requestStack; + $this->paths = $paths; + } + + public function filter(array $context = []): bool + { + $request = $this->requestStack->getMainRequest(); + if (null === $request) { + return true; + } + + $pathWithoutTrailingSlash = rtrim($request->getPathInfo(), '/'); + $pathWithTrailingSlash = $pathWithoutTrailingSlash . '/'; + + foreach ($this->paths as $excludedPath) { + foreach ($this->matchers as $matcher) { + if ($matcher->supports($excludedPath) && ($matcher->matches($pathWithoutTrailingSlash, $excludedPath) || $matcher->matches($pathWithTrailingSlash, $excludedPath))) { + return false; + } + } + } + + return true; + } +} diff --git a/src/Filter/Matcher/EqualityMatcher.php b/src/Filter/Matcher/EqualityMatcher.php new file mode 100644 index 0000000..731e3e4 --- /dev/null +++ b/src/Filter/Matcher/EqualityMatcher.php @@ -0,0 +1,18 @@ +isAbsolutePath($pattern); + } + + public function matches(string $value, string $pattern): bool + { + return Glob::match($value, $pattern); + } +} diff --git a/src/Filter/Matcher/MatcherInterface.php b/src/Filter/Matcher/MatcherInterface.php new file mode 100644 index 0000000..5c311c5 --- /dev/null +++ b/src/Filter/Matcher/MatcherInterface.php @@ -0,0 +1,18 @@ + + + diff --git a/src/Resources/config/services/conditional/library_event_subscriber.xml b/src/Resources/config/services/conditional/library_event_subscriber.xml index 2dd4d3a..d3458bb 100644 --- a/src/Resources/config/services/conditional/library_event_subscriber.xml +++ b/src/Resources/config/services/conditional/library_event_subscriber.xml @@ -6,6 +6,7 @@ + %setono_google_analytics.inject_library% diff --git a/src/Resources/config/services/event_subscriber.xml b/src/Resources/config/services/event_subscriber.xml index 70ff3b0..1e6e7fc 100644 --- a/src/Resources/config/services/event_subscriber.xml +++ b/src/Resources/config/services/event_subscriber.xml @@ -6,6 +6,7 @@ + diff --git a/src/Resources/config/services/filter.xml b/src/Resources/config/services/filter.xml new file mode 100644 index 0000000..5572464 --- /dev/null +++ b/src/Resources/config/services/filter.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + %setono_google_analytics.filters.client_side.paths% + + + + + + + %setono_google_analytics.filters.client_side.hostnames% + + + + + diff --git a/src/SetonoGoogleAnalyticsBundle.php b/src/SetonoGoogleAnalyticsBundle.php index 48de992..d16b5be 100644 --- a/src/SetonoGoogleAnalyticsBundle.php +++ b/src/SetonoGoogleAnalyticsBundle.php @@ -4,8 +4,17 @@ namespace Setono\GoogleAnalyticsBundle; +use Setono\CompositeCompilerPass\CompositeCompilerPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; final class SetonoGoogleAnalyticsBundle extends Bundle { + public function build(ContainerBuilder $container): void + { + $container->addCompilerPass(new CompositeCompilerPass( + 'setono_google_analytics.filter.client_side.composite', + 'setono_google_analytics.client_side_filter' + )); + } } diff --git a/src/ValueObject/Property.php b/src/ValueObject/Property.php index ab8f5be..6264881 100644 --- a/src/ValueObject/Property.php +++ b/src/ValueObject/Property.php @@ -17,8 +17,9 @@ final class Property public function __construct(string $measurementId, string $apiSecret = null) { - Assert::true( - str_starts_with($measurementId, 'G-'), + Assert::startsWith( + $measurementId, + 'G-', sprintf('The measurement id does not start with "G-". The given input was: "%s"', $measurementId), ); diff --git a/tests/Unit/DependencyInjection/SetonoGoogleAnalyticsExtensionTest.php b/tests/Unit/DependencyInjection/SetonoGoogleAnalyticsExtensionTest.php index 1a5a62a..c3545fe 100644 --- a/tests/Unit/DependencyInjection/SetonoGoogleAnalyticsExtensionTest.php +++ b/tests/Unit/DependencyInjection/SetonoGoogleAnalyticsExtensionTest.php @@ -37,6 +37,16 @@ public function it_has_parameters_set(): void ], ], ], + 'filters' => [ + 'client_side' => [ + 'paths' => [ + '/admin', + ], + 'hostnames' => [ + '*.github.com', + ], + ], + ], ]); $this->assertContainerBuilderHasParameter('setono_google_analytics.properties', [ @@ -53,6 +63,8 @@ public function it_has_parameters_set(): void $this->assertContainerBuilderHasParameter('setono_google_analytics.consent_enabled', true); $this->assertContainerBuilderHasParameter('setono_google_analytics.gtag_enabled', true); $this->assertContainerBuilderHasParameter('setono_google_analytics.tag_manager_enabled', false); + $this->assertContainerBuilderHasParameter('setono_google_analytics.filters.client_side.paths', ['/admin']); + $this->assertContainerBuilderHasParameter('setono_google_analytics.filters.client_side.hostnames', ['*.github.com']); } /** diff --git a/tests/Unit/Filter/ClientSide/HostnameBasedClientSideFilterTest.php b/tests/Unit/Filter/ClientSide/HostnameBasedClientSideFilterTest.php new file mode 100644 index 0000000..5c54d28 --- /dev/null +++ b/tests/Unit/Filter/ClientSide/HostnameBasedClientSideFilterTest.php @@ -0,0 +1,44 @@ + $excludedHostnames + */ + public function it_filters(string $hostname, array $excludedHostnames, bool $expectedFilterResult): void + { + $request = $this->prophesize(Request::class); + $request->getHttpHost()->willReturn($hostname); + + $requestStack = new RequestStack(); + $requestStack->push($request->reveal()); + $filter = new HostnameBasedClientSideFilter($requestStack, $excludedHostnames); + + self::assertSame($expectedFilterResult, $filter->filter()); + } + + /** + * @return \Generator, 2: bool}> + */ + public function providePaths(): \Generator + { + yield ['github.com', ['api.github.com'], true]; + yield ['github.com', ['*.github.com'], true]; + yield ['www.github.com', ['*.github.com'], false]; + } +} diff --git a/tests/Unit/Filter/ClientSide/PathBasedClientSideFilterTest.php b/tests/Unit/Filter/ClientSide/PathBasedClientSideFilterTest.php new file mode 100644 index 0000000..a7a5ef6 --- /dev/null +++ b/tests/Unit/Filter/ClientSide/PathBasedClientSideFilterTest.php @@ -0,0 +1,58 @@ + $excludedPaths + */ + public function it_filters(string $path, array $excludedPaths, bool $expectedFilterResult): void + { + $request = $this->prophesize(Request::class); + $request->getPathInfo()->willReturn($path); + + $requestStack = new RequestStack(); + $requestStack->push($request->reveal()); + $filter = new PathBasedClientSideFilter($requestStack, $excludedPaths); + + self::assertSame($expectedFilterResult, $filter->filter()); + } + + /** + * @return \Generator, 2: bool}> + */ + public function providePaths(): \Generator + { + yield ['/', ['/admin', '/api'], true]; + yield ['/admin', ['/admin', '/api'], false]; + yield ['/admin/', ['/admin', '/api'], false]; + yield ['/admin2', ['/admin', '/api'], true]; + + yield ['/sub1/sub2/sub3', ['#/sub1/.*#'], false]; + + yield ['/sub1/sub2/sub3', ['/sub1/su[ab][0123]/**/'], false]; + + yield ['/sub1/sub2/sub3', ['/sub1/**/'], false]; + yield ['/sub1/sub2/sub3/', ['/sub1/**/'], false]; + + // Example of excluding a whole subfolder with the same regexp + yield ['/admin', ['#/admin(/.*)?#'], false]; + yield ['/admin/', ['#/admin(/.*)?#'], false]; + yield ['/admin/google-analytics', ['#/admin(/.*)?#'], false]; + yield ['/admin/google-analytics/settings', ['#/admin(/.*)?#'], false]; + } +}