Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 55 additions & 8 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
matrix:
php-version:
- "7.4"
- "8.0"
- "8.1"
- "8.2"

Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -102,6 +118,7 @@ jobs:
matrix:
php-version:
- "7.4"
- "8.0"
- "8.1"
- "8.2"

Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
1 change: 0 additions & 1 deletion composer-require-checker.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"string",
"true",
"void",
"str_starts_with",
"Setono\\Consent\\Consents",
"Setono\\ConsentBundle\\SetonoConsentBundle",
"Setono\\Consent\\ConsentCheckerInterface"
Expand Down
12 changes: 12 additions & 0 deletions composer-unused.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

use ComposerUnused\ComposerUnused\Configuration\Configuration;
use ComposerUnused\ComposerUnused\Configuration\NamedFilter;

return static function (Configuration $config): Configuration {
return $config
->addNamedFilter(NamedFilter::fromString('setono/tag-bag-bundle'))
;
};
18 changes: 10 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,30 @@
"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",
"symfony/config": "^5.4 || ^6.0",
"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"
},
Expand Down
24 changes: 24 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
15 changes: 14 additions & 1 deletion src/DependencyInjection/SetonoGoogleAnalyticsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<string>, hostnames: list<string>}},
* consent: array{enabled: bool}
* } $config
*/
$config = $this->processConfiguration($this->getConfiguration([], $container), $configs);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
Expand All @@ -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.');
Expand Down Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions src/EventSubscriber/Tag/EventSubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@
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;

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
Expand All @@ -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);
}
}
13 changes: 10 additions & 3 deletions src/EventSubscriber/Tag/LibrarySubscriber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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;
}

Expand Down
16 changes: 16 additions & 0 deletions src/Filter/ClientSide/ClientSideFilterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Setono\GoogleAnalyticsBundle\Filter\ClientSide;

interface ClientSideFilterInterface
{
/**
* If the method returns false, the client side tags/scripts are not rendered on your page.
* You can use this to avoid rendering Google Analytics tags on your admin pages for example
*
* @param array<string, mixed> $context
*/
public function filter(array $context = []): bool;
}
24 changes: 24 additions & 0 deletions src/Filter/ClientSide/CompositeClientSideFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Setono\GoogleAnalyticsBundle\Filter\ClientSide;

use Setono\CompositeCompilerPass\CompositeService;

/**
* @extends CompositeService<ClientSideFilterInterface>
*/
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;
}
}
Loading