Fixes #6007 -- Implement Google Analytics Measurement V4.

This commit is contained in:
Buster Neece 2023-01-14 02:04:13 -06:00
parent 415e19e15d
commit 6ef72c643e
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
10 changed files with 251 additions and 42 deletions

View File

@ -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",

49
composer.lock generated
View File

@ -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",

View File

@ -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' => [],
],

View File

@ -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: {

View File

@ -0,0 +1,42 @@
<template>
<b-form-group>
<div class="form-row">
<b-wrapped-form-group
id="form_config_api_secret"
class="col-md-6"
:field="form.config.api_secret"
>
<template #label>
{{ $gettext('Measurement Protocol API Secret') }}
</template>
<template #description>
{{ $gettext('This can be generated in the "Events" section for a measurement.') }}
</template>
</b-wrapped-form-group>
<b-wrapped-form-group
id="form_config_measurement_id"
class="col-md-6"
:field="form.config.measurement_id"
>
<template #label>
{{ $gettext('Measurement ID') }}
</template>
<template #description>
{{ $gettext('A unique identifier (i.e. "G-A1B2C3D4") for this measurement stream.') }}
</template>
</b-wrapped-form-group>
</div>
</b-form-group>
</template>
<script setup>
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
const props = defineProps({
form: {
type: Object,
required: true
}
});
</script>

View File

@ -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;

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace App\Webhook\Connector;
use App\Entity\Repository\ListenerRepository;
use App\Entity\Station;
use App\Nginx\CustomUrls;
use GuzzleHttp\Client;
use Monolog\Logger;
abstract class AbstractGoogleAnalyticsConnector extends AbstractConnector
{
public function __construct(
Logger $logger,
Client $httpClient,
protected readonly ListenerRepository $listenerRepo
) {
parent::__construct($logger, $httpClient);
}
protected function buildListenUrls(Station $station): array
{
$listenBaseUrl = CustomUrls::getListenUrl($station);
$hlsBaseUrl = CustomUrls::getHlsUrl($station);
$mountUrls = [];
foreach ($station->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;
}
}

View File

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

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Webhook\Connector;
use App\Entity\Api\NowPlaying\NowPlaying;
use App\Entity\Station;
use App\Entity\StationWebhook;
use Br33f\Ga4\MeasurementProtocol\Dto\Event\BaseEvent;
use Br33f\Ga4\MeasurementProtocol\Dto\Request\BaseRequest;
use Br33f\Ga4\MeasurementProtocol\HttpClient as Ga4HttpClient;
use Br33f\Ga4\MeasurementProtocol\Service;
final class GoogleAnalyticsV4 extends AbstractGoogleAnalyticsConnector
{
public const NAME = 'google_analytics_v4';
/**
* @inheritDoc
*/
public function dispatch(
Station $station,
StationWebhook $webhook,
NowPlaying $np,
array $triggers
): void {
$config = $webhook->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
)
);
}
}
}