From 6ef72c643e1c76708fc1fefd600e43fdfac050ee Mon Sep 17 00:00:00 2001 From: Buster Neece Date: Sat, 14 Jan 2023 02:04:13 -0600 Subject: [PATCH] Fixes #6007 -- Implement Google Analytics Measurement V4. --- composer.json | 1 + composer.lock | 49 +++++++++++++- config/webhooks.php | 12 +++- .../Stations/Webhooks/EditModal.vue | 16 ++++- ...gleAnalytics.vue => GoogleAnalyticsV3.vue} | 0 .../Webhooks/Form/GoogleAnalyticsV4.vue | 42 ++++++++++++ src/Entity/StationHlsStream.php | 2 +- .../AbstractGoogleAnalyticsConnector.php | 66 ++++++++++++++++++ ...gleAnalytics.php => GoogleAnalyticsV3.php} | 38 +---------- src/Webhook/Connector/GoogleAnalyticsV4.php | 67 +++++++++++++++++++ 10 files changed, 251 insertions(+), 42 deletions(-) rename frontend/vue/components/Stations/Webhooks/Form/{GoogleAnalytics.vue => GoogleAnalyticsV3.vue} (100%) create mode 100644 frontend/vue/components/Stations/Webhooks/Form/GoogleAnalyticsV4.vue create mode 100644 src/Webhook/Connector/AbstractGoogleAnalyticsConnector.php rename src/Webhook/Connector/{GoogleAnalytics.php => GoogleAnalyticsV3.php} (58%) create mode 100644 src/Webhook/Connector/GoogleAnalyticsV4.php diff --git a/composer.json b/composer.json index 8f0d28a62..f417438a9 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "azuracast/nowplaying": "dev-main", "bacon/bacon-qr-code": "^2.0", "beberlei/doctrineextensions": "^1.2", + "br33f/php-ga4-mp": "^0.1.2", "brick/math": "^0.10", "composer/ca-bundle": "^1.2", "doctrine/annotations": "^1.6", diff --git a/composer.lock b/composer.lock index 2e16afe60..0baadbace 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4c004f346864c42ace26a2bc076ca89c", + "content-hash": "99fa37d49611c4023e50cbe3c4ce9dee", "packages": [ { "name": "aws/aws-crt-php", @@ -331,6 +331,53 @@ }, "time": "2020-11-29T07:37:23+00:00" }, + { + "name": "br33f/php-ga4-mp", + "version": "v0.1.2", + "source": { + "type": "git", + "url": "https://github.com/br33f/php-GA4-Measurement-Protocol.git", + "reference": "e9b95e5b0cf4daf05c3739d989867f1103835acb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/br33f/php-GA4-Measurement-Protocol/zipball/e9b95e5b0cf4daf05c3739d989867f1103835acb", + "reference": "e9b95e5b0cf4daf05c3739d989867f1103835acb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/guzzle": "^6.5.5 || ^7.0.0", + "php": ">=7.1" + }, + "require-dev": { + "fakerphp/faker": "^1.14", + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Br33f\\Ga4\\MeasurementProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Damian Zamojski", + "email": "damian.zamojski1@gmail.com" + } + ], + "description": "PHP GoogleAnalytics4 Measurement Protocol Library", + "support": { + "issues": "https://github.com/br33f/php-GA4-Measurement-Protocol/issues", + "source": "https://github.com/br33f/php-GA4-Measurement-Protocol/tree/v0.1.2" + }, + "time": "2022-08-25T12:01:16+00:00" + }, { "name": "brick/math", "version": "0.10.2", diff --git a/config/webhooks.php b/config/webhooks.php index 8edea86b5..44cc08e47 100644 --- a/config/webhooks.php +++ b/config/webhooks.php @@ -70,9 +70,15 @@ return [ 'description' => __('Automatically publish to a Mastodon instance.'), 'triggers' => $allTriggersExceptListeners, ], - Connector\GoogleAnalytics::NAME => [ - 'class' => Connector\GoogleAnalytics::class, - 'name' => __('Google Analytics Integration'), + Connector\GoogleAnalyticsV3::NAME => [ + 'class' => Connector\GoogleAnalyticsV3::class, + 'name' => __('Google Analytics V3 Integration'), + 'description' => __('Send stream listener details to Google Analytics.'), + 'triggers' => [], + ], + Connector\GoogleAnalyticsV4::NAME => [ + 'class' => Connector\GoogleAnalyticsV4::class, + 'name' => __('Google Analytics V4 Integration'), 'description' => __('Send stream listener details to Google Analytics.'), 'triggers' => [], ], diff --git a/frontend/vue/components/Stations/Webhooks/EditModal.vue b/frontend/vue/components/Stations/Webhooks/EditModal.vue index 40b804dbc..1eca37a64 100644 --- a/frontend/vue/components/Stations/Webhooks/EditModal.vue +++ b/frontend/vue/components/Stations/Webhooks/EditModal.vue @@ -51,7 +51,8 @@ import Tunein from "./Form/Tunein"; import Discord from "./Form/Discord"; import Telegram from "./Form/Telegram"; import Twitter from "./Form/Twitter"; -import GoogleAnalytics from "./Form/GoogleAnalytics"; +import GoogleAnalyticsV3 from "./Form/GoogleAnalyticsV3"; +import GoogleAnalyticsV4 from "./Form/GoogleAnalyticsV4"; import MatomoAnalytics from "./Form/MatomoAnalytics"; import Mastodon from "./Form/Mastodon"; import {baseEditModalProps, useBaseEditModal} from "~/functions/useBaseEditModal"; @@ -294,7 +295,7 @@ const webhookConfig = { } }, 'google_analytics': { - component: GoogleAnalytics, + component: GoogleAnalyticsV3, validations: { tracking_id: {required} }, @@ -302,6 +303,17 @@ const webhookConfig = { tracking_id: '' } }, + 'google_analytics_v4': { + component: GoogleAnalyticsV4, + validations: { + api_secret: {required}, + measurement_id: {required} + }, + defaultConfig: { + api_secret: '', + measurement_id: '' + } + }, 'matomo_analytics': { component: MatomoAnalytics, validations: { diff --git a/frontend/vue/components/Stations/Webhooks/Form/GoogleAnalytics.vue b/frontend/vue/components/Stations/Webhooks/Form/GoogleAnalyticsV3.vue similarity index 100% rename from frontend/vue/components/Stations/Webhooks/Form/GoogleAnalytics.vue rename to frontend/vue/components/Stations/Webhooks/Form/GoogleAnalyticsV3.vue diff --git a/frontend/vue/components/Stations/Webhooks/Form/GoogleAnalyticsV4.vue b/frontend/vue/components/Stations/Webhooks/Form/GoogleAnalyticsV4.vue new file mode 100644 index 000000000..8cc37a4e9 --- /dev/null +++ b/frontend/vue/components/Stations/Webhooks/Form/GoogleAnalyticsV4.vue @@ -0,0 +1,42 @@ + + + diff --git a/src/Entity/StationHlsStream.php b/src/Entity/StationHlsStream.php index 06e2e4ff5..2b76c311c 100644 --- a/src/Entity/StationHlsStream.php +++ b/src/Entity/StationHlsStream.php @@ -30,7 +30,7 @@ class StationHlsStream implements protected int $station_id; #[ - ORM\ManyToOne(inversedBy: 'mounts'), + ORM\ManyToOne(inversedBy: 'hls_streams'), ORM\JoinColumn(name: 'station_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE') ] protected Station $station; diff --git a/src/Webhook/Connector/AbstractGoogleAnalyticsConnector.php b/src/Webhook/Connector/AbstractGoogleAnalyticsConnector.php new file mode 100644 index 000000000..388914f11 --- /dev/null +++ b/src/Webhook/Connector/AbstractGoogleAnalyticsConnector.php @@ -0,0 +1,66 @@ +getMounts() as $mount) { + $mountUrls[$mount->getIdRequired()] = $listenBaseUrl . $mount->getName(); + } + + $remoteUrls = []; + foreach ($station->getRemotes() as $remote) { + $remoteUrls[$remote->getIdRequired()] = $listenBaseUrl . '/remote' . $remote->getMount(); + } + + $hlsUrls = []; + foreach ($station->getHlsStreams() as $hlsStream) { + $hlsUrls[$hlsStream->getIdRequired()] = $hlsBaseUrl . '/' . $hlsStream->getName(); + } + + return [ + 'mounts' => $mountUrls, + 'remotes' => $remoteUrls, + 'hls' => $hlsUrls, + ]; + } + + protected function getListenUrl( + array $listener, + array $listenUrls + ): ?string { + if (!empty($listener['mount_id'])) { + return $listenUrls['mounts'][$listener['mount_id']] ?? null; + } + if (!empty($listener['remote_id'])) { + return $listenUrls['remotes'][$listener['remote_id']] ?? null; + } + if (!empty($listener['hls_stream_id'])) { + return $listenUrls['hls'][$listener['hls_stream_id']] ?? null; + } + + return null; + } +} diff --git a/src/Webhook/Connector/GoogleAnalytics.php b/src/Webhook/Connector/GoogleAnalyticsV3.php similarity index 58% rename from src/Webhook/Connector/GoogleAnalytics.php rename to src/Webhook/Connector/GoogleAnalyticsV3.php index 1522957eb..f06eb72e1 100644 --- a/src/Webhook/Connector/GoogleAnalytics.php +++ b/src/Webhook/Connector/GoogleAnalyticsV3.php @@ -5,27 +5,15 @@ declare(strict_types=1); namespace App\Webhook\Connector; use App\Entity\Api\NowPlaying\NowPlaying; -use App\Entity\Repository\ListenerRepository; use App\Entity\Station; use App\Entity\StationWebhook; -use GuzzleHttp\Client; -use GuzzleHttp\Psr7\Uri; -use Monolog\Logger; use TheIconic\Tracking\GoogleAnalytics\Analytics; use TheIconic\Tracking\GoogleAnalytics\Network\HttpClient; -final class GoogleAnalytics extends AbstractConnector +final class GoogleAnalyticsV3 extends AbstractGoogleAnalyticsConnector { public const NAME = 'google_analytics'; - public function __construct( - Logger $logger, - Client $httpClient, - private readonly ListenerRepository $listenerRepo - ) { - parent::__construct($logger, $httpClient); - } - /** * @inheritDoc */ @@ -41,21 +29,7 @@ final class GoogleAnalytics extends AbstractConnector } // Get listen URLs for each mount point. - $radioPort = $station->getFrontendConfig()->getPort(); - - $mountUrls = []; - foreach ($station->getMounts() as $mount) { - $mountUrl = (new Uri()) - ->withPath('/radio/' . $radioPort . $mount->getName()); - $mountUrls[$mount->getId()] = (string)$mountUrl; - } - - $remoteUrls = []; - foreach ($station->getRemotes() as $remote) { - $remoteUrl = (new Uri()) - ->withPath('/radio/remote' . $remote->getMount()); - $remoteUrls[$remote->getId()] = (string)$remoteUrl; - } + $listenUrls = $this->buildListenUrls($station); // Build analytics $httpClient = new HttpClient(); @@ -72,13 +46,7 @@ final class GoogleAnalytics extends AbstractConnector $i = 0; foreach ($liveListeners as $listener) { - $listenerUrl = null; - if (!empty($listener['mount_id'])) { - $listenerUrl = $mountUrls[$listener['mount_id']] ?? null; - } elseif (!empty($listener['remote_id'])) { - $listenerUrl = $remoteUrls[$listener['remote_id']] ?? null; - } - + $listenerUrl = $this->getListenUrl($listener, $listenUrls); if (null === $listenerUrl) { continue; } diff --git a/src/Webhook/Connector/GoogleAnalyticsV4.php b/src/Webhook/Connector/GoogleAnalyticsV4.php new file mode 100644 index 000000000..3349e22e8 --- /dev/null +++ b/src/Webhook/Connector/GoogleAnalyticsV4.php @@ -0,0 +1,67 @@ +getConfig(); + + if (empty($config['api_secret']) || empty($config['measurement_id'])) { + throw $this->incompleteConfigException(self::NAME); + } + + // Get listen URLs for each mount point. + $listenUrls = $this->buildListenUrls($station); + + // Build analytics + $gaHttpClient = new Ga4HttpClient(); + $gaHttpClient->setClient($this->httpClient); + + $ga4Service = new Service($config['api_secret'], $config['measurement_id']); + $ga4Service->setHttpClient($gaHttpClient); + + // Get all current listeners + $liveListeners = $this->listenerRepo->iterateLiveListenersArray($station); + + foreach ($liveListeners as $listener) { + $listenerUrl = $this->getListenUrl($listener, $listenUrls); + if (null === $listenerUrl) { + continue; + } + + $event = new BaseEvent('page_view'); + $event->setParamValue('page_location', $listenerUrl) + ->setParamValue('page_title', $listenerUrl) + ->setParamValue('ip', $listener['listener_ip']) + ->setParamValue('user_agent', $listener['listener_user_agent']); + + $ga4Service->send( + new BaseRequest( + (string)$listener['listener_uid'], + $event + ) + ); + } + } +}