mirror of
https://github.com/shlinkio/shlink.git
synced 2024-10-22 20:25:35 +03:00
Add logic to send visits to a matomo instance
This commit is contained in:
parent
0edb3e5c2c
commit
9dbd15bc0c
13 changed files with 341 additions and 147 deletions
|
@ -10,7 +10,7 @@ return [
|
|||
'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(false),
|
||||
'base_url' => EnvVars::MATOMO_BASE_URL->loadFromEnv(),
|
||||
'site_id' => EnvVars::MATOMO_SITE_ID->loadFromEnv(),
|
||||
'token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(),
|
||||
'api_token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -92,6 +92,9 @@ return [
|
|||
Importer\ImportedLinksProcessor::class => ConfigAbstractFactory::class,
|
||||
|
||||
Crawling\CrawlingHelper::class => ConfigAbstractFactory::class,
|
||||
|
||||
Matomo\MatomoOptions::class => [ValinorConfigFactory::class, 'config.matomo'],
|
||||
Matomo\MatomoTrackerBuilder::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
|
@ -100,6 +103,8 @@ return [
|
|||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Matomo\MatomoTrackerBuilder::class => [Matomo\MatomoOptions::class],
|
||||
|
||||
ErrorHandler\NotFoundTypeResolverMiddleware::class => ['config.router.base_path'],
|
||||
ErrorHandler\NotFoundTrackerMiddleware::class => [Visit\RequestTracker::class],
|
||||
ErrorHandler\NotFoundRedirectHandler::class => [
|
||||
|
|
|
@ -11,6 +11,7 @@ use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper;
|
|||
use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
|
||||
use Shlinkio\Shlink\Common\Mercure\MercureOptions;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocator;
|
||||
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelper;
|
||||
use Shlinkio\Shlink\EventDispatcher\Listener\EnabledListenerCheckerInterface;
|
||||
|
@ -18,152 +19,177 @@ use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
|||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
||||
return [
|
||||
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
'events' => [
|
||||
'regular' => [
|
||||
EventDispatcher\Event\UrlVisited::class => [
|
||||
EventDispatcher\LocateVisit::class,
|
||||
],
|
||||
EventDispatcher\Event\GeoLiteDbCreated::class => [
|
||||
EventDispatcher\LocateUnlocatedVisits::class,
|
||||
],
|
||||
return (static function (): array {
|
||||
$regularEvents = [
|
||||
EventDispatcher\Event\UrlVisited::class => [
|
||||
EventDispatcher\LocateVisit::class,
|
||||
],
|
||||
'async' => [
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class,
|
||||
],
|
||||
EventDispatcher\Event\ShortUrlCreated::class => [
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class,
|
||||
],
|
||||
EventDispatcher\Event\GeoLiteDbCreated::class => [
|
||||
EventDispatcher\LocateUnlocatedVisits::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
$asyncEvents = [
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class,
|
||||
],
|
||||
EventDispatcher\Event\ShortUrlCreated::class => [
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class,
|
||||
],
|
||||
];
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
||||
// Send visits to matomo asynchronously if the runtime allows it
|
||||
if (runningInRoadRunner() || runningInOpenswoole()) {
|
||||
$asyncEvents[EventDispatcher\Event\VisitLocated::class][] = EventDispatcher\Matomo\SendVisitToMatomo::class;
|
||||
} else {
|
||||
$regularEvents[EventDispatcher\Event\VisitLocated::class] = [EventDispatcher\Matomo\SendVisitToMatomo::class];
|
||||
}
|
||||
|
||||
EventDispatcher\Helper\EnabledListenerChecker::class => ConfigAbstractFactory::class,
|
||||
return [
|
||||
|
||||
'events' => [
|
||||
'regular' => $regularEvents,
|
||||
'async' => $asyncEvents,
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
EnabledListenerCheckerInterface::class => EventDispatcher\Helper\EnabledListenerChecker::class,
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
||||
|
||||
EventDispatcher\Helper\EnabledListenerChecker::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
EnabledListenerCheckerInterface::class => EventDispatcher\Helper\EnabledListenerChecker::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
ConfigAbstractFactory::class => [
|
||||
EventDispatcher\LocateVisit::class => [
|
||||
IpLocationResolverInterface::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
DbUpdater::class,
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
'httpClient',
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\WebhookOptions::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
Options\AppOptions::class,
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => [
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
ShortUrlStringifier::class,
|
||||
Matomo\MatomoOptions::class,
|
||||
Matomo\MatomoTrackerBuilder::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [
|
||||
GeolocationDbUpdater::class,
|
||||
'Logger_Shlink',
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
|
||||
EventDispatcher\Helper\EnabledListenerChecker::class => [
|
||||
Options\RabbitMqOptions::class,
|
||||
'config.redis.pub_sub_enabled',
|
||||
MercureOptions::class,
|
||||
Options\WebhookOptions::class,
|
||||
GeoLite2Options::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
EventDispatcher\LocateVisit::class => [
|
||||
IpLocationResolverInterface::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
DbUpdater::class,
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
'httpClient',
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\WebhookOptions::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
Options\AppOptions::class,
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [
|
||||
GeolocationDbUpdater::class,
|
||||
'Logger_Shlink',
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
|
||||
EventDispatcher\Helper\EnabledListenerChecker::class => [
|
||||
Options\RabbitMqOptions::class,
|
||||
'config.redis.pub_sub_enabled',
|
||||
MercureOptions::class,
|
||||
Options\WebhookOptions::class,
|
||||
GeoLite2Options::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
];
|
||||
})();
|
||||
|
|
|
@ -9,17 +9,19 @@ use Shlinkio\Shlink\EventDispatcher\Util\JsonUnserializable;
|
|||
|
||||
abstract class AbstractVisitEvent implements JsonSerializable, JsonUnserializable
|
||||
{
|
||||
final public function __construct(public readonly string $visitId)
|
||||
{
|
||||
final public function __construct(
|
||||
public readonly string $visitId,
|
||||
public readonly ?string $originalIpAddress = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return ['visitId' => $this->visitId];
|
||||
return ['visitId' => $this->visitId, 'originalIpAddress' => $this->originalIpAddress];
|
||||
}
|
||||
|
||||
public static function fromPayload(array $payload): self
|
||||
{
|
||||
return new static($payload['visitId'] ?? '');
|
||||
return new static($payload['visitId'] ?? '', $payload['originalIpAddress'] ?? null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,4 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
|
|||
|
||||
final class UrlVisited extends AbstractVisitEvent
|
||||
{
|
||||
private ?string $originalIpAddress = null;
|
||||
|
||||
public static function withOriginalIpAddress(string $visitId, ?string $originalIpAddress): self
|
||||
{
|
||||
$instance = new self($visitId);
|
||||
$instance->originalIpAddress = $originalIpAddress;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function originalIpAddress(): ?string
|
||||
{
|
||||
return $this->originalIpAddress;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,8 +41,8 @@ class LocateVisit
|
|||
return;
|
||||
}
|
||||
|
||||
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
|
||||
$this->eventDispatcher->dispatch(new VisitLocated($visitId));
|
||||
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit);
|
||||
$this->eventDispatcher->dispatch(new VisitLocated($visitId, $shortUrlVisited->originalIpAddress));
|
||||
}
|
||||
|
||||
private function locateVisit(string $visitId, ?string $originalIpAddress, Visit $visit): void
|
||||
|
|
88
module/Core/src/EventDispatcher/Matomo/SendVisitToMatomo.php
Normal file
88
module/Core/src/EventDispatcher/Matomo/SendVisitToMatomo.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\Matomo;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoTrackerBuilderInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Throwable;
|
||||
|
||||
class SendVisitToMatomo
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ShortUrlStringifier $shortUrlStringifier,
|
||||
private readonly MatomoOptions $matomoOptions,
|
||||
private readonly MatomoTrackerBuilderInterface $trackerBuilder,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(VisitLocated $visitLocated): void
|
||||
{
|
||||
if (! $this->matomoOptions->enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$visitId = $visitLocated->visitId;
|
||||
|
||||
/** @var Visit|null $visit */
|
||||
$visit = $this->em->find(Visit::class, $visitId);
|
||||
if ($visit === null) {
|
||||
$this->logger->warning('Tried to send visit with id "{visitId}" to matomo, but it does not exist.', [
|
||||
'visitId' => $visitId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$tracker = $this->trackerBuilder->buildMatomoTracker();
|
||||
|
||||
$tracker
|
||||
->setUrl($this->resolveUrlToTrack($visit))
|
||||
->setCustomTrackingParameter('type', $visit->type()->value)
|
||||
->setUserAgent($visit->userAgent());
|
||||
|
||||
$location = $visit->getVisitLocation();
|
||||
if ($location !== null) {
|
||||
$tracker
|
||||
->setCity($location->getCityName())
|
||||
->setCountry($location->getCountryName())
|
||||
->setLatitude($location->getLatitude())
|
||||
->setLongitude($location->getLongitude());
|
||||
}
|
||||
|
||||
// Set not obfuscated IP if possible, as matomo handles obfuscation itself
|
||||
$ip = $visitLocated->originalIpAddress ?? $visit->getRemoteAddr();
|
||||
if ($ip !== null) {
|
||||
$tracker->setIp($ip);
|
||||
}
|
||||
|
||||
if ($visit->isOrphan()) {
|
||||
$tracker->setCustomTrackingParameter('orphan', 'true');
|
||||
}
|
||||
|
||||
// Send empty document title to avoid different actions to be created by matomo
|
||||
$tracker->doTrackPageView('');
|
||||
} catch (Throwable $e) {
|
||||
// Capture all exceptions to make sure this does not interfere with the regular execution
|
||||
$this->logger->error('An error occurred while trying to send visit to Matomo. {e}', ['e' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
public function resolveUrlToTrack(Visit $visit): string
|
||||
{
|
||||
$shortUrl = $visit->getShortUrl();
|
||||
if ($shortUrl === null) {
|
||||
return $visit->visitedUrl() ?? '';
|
||||
}
|
||||
|
||||
return $this->shortUrlStringifier->stringify($shortUrl);
|
||||
}
|
||||
}
|
27
module/Core/src/Matomo/MatomoOptions.php
Normal file
27
module/Core/src/Matomo/MatomoOptions.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Matomo;
|
||||
|
||||
class MatomoOptions
|
||||
{
|
||||
public function __construct(
|
||||
public readonly bool $enabled,
|
||||
public readonly ?string $baseUrl,
|
||||
/** @var numeric-string|int|null */
|
||||
private readonly string|int|null $siteId,
|
||||
public readonly ?string $apiToken,
|
||||
) {
|
||||
}
|
||||
|
||||
public function siteId(): ?int
|
||||
{
|
||||
if ($this->siteId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We enforce site ID to be hydrated as a numeric string or int, so it's safe to cast to int here
|
||||
return (int) $this->siteId;
|
||||
}
|
||||
}
|
39
module/Core/src/Matomo/MatomoTrackerBuilder.php
Normal file
39
module/Core/src/Matomo/MatomoTrackerBuilder.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Matomo;
|
||||
|
||||
use MatomoTracker;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
|
||||
class MatomoTrackerBuilder implements MatomoTrackerBuilderInterface
|
||||
{
|
||||
public function __construct(private readonly MatomoOptions $options)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException If there's any missing matomo parameter
|
||||
*/
|
||||
public function buildMatomoTracker(): MatomoTracker
|
||||
{
|
||||
$siteId = $this->options->siteId();
|
||||
if ($siteId === null || $this->options->baseUrl === null || $this->options->apiToken === null) {
|
||||
throw new RuntimeException(
|
||||
'Cannot create MatomoTracker. Either site ID, base URL or api token are not defined',
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new MatomoTracker on every request, because it infers request info during construction
|
||||
$tracker = new MatomoTracker($siteId, $this->options->baseUrl);
|
||||
// Token required to set the IP and location
|
||||
$tracker->setTokenAuth($this->options->apiToken);
|
||||
// We don't want to bulk send, as every request to Shlink will create a new tracker
|
||||
$tracker->disableBulkTracking();
|
||||
// Ensure params are not sent in the URL, for security reasons
|
||||
$tracker->setRequestMethodNonBulk('POST');
|
||||
|
||||
return $tracker;
|
||||
}
|
||||
}
|
16
module/Core/src/Matomo/MatomoTrackerBuilderInterface.php
Normal file
16
module/Core/src/Matomo/MatomoTrackerBuilderInterface.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Matomo;
|
||||
|
||||
use MatomoTracker;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
|
||||
interface MatomoTrackerBuilderInterface
|
||||
{
|
||||
/**
|
||||
* @throws RuntimeException If there's any missing matomo parameter
|
||||
*/
|
||||
public function buildMatomoTracker(): MatomoTracker;
|
||||
}
|
|
@ -188,6 +188,11 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||
return $this->date;
|
||||
}
|
||||
|
||||
public function userAgent(): string
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return [
|
||||
|
|
|
@ -75,6 +75,6 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||
$this->em->persist($visit);
|
||||
$this->em->flush();
|
||||
|
||||
$this->eventDispatcher->dispatch(UrlVisited::withOriginalIpAddress($visit->getId(), $visitor->remoteAddress));
|
||||
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ class LocateVisitTest extends TestCase
|
|||
{
|
||||
$ipAddr = $originalIpAddress ?? $visit->getRemoteAddr();
|
||||
$location = new Location('', '', '', '', 0.0, 0.0, '');
|
||||
$event = UrlVisited::withOriginalIpAddress('123', $originalIpAddress);
|
||||
$event = new UrlVisited('123', $originalIpAddress);
|
||||
|
||||
$this->em->expects($this->once())->method('find')->with(Visit::class, '123')->willReturn($visit);
|
||||
$this->em->expects($this->once())->method('flush');
|
||||
|
|
Loading…
Reference in a new issue