Added split info about bots, non-bots and total visits to the visits stats

This commit is contained in:
Alejandro Celaya 2023-01-02 12:28:34 +01:00
parent e71f6bb528
commit 37c8328eed
8 changed files with 82 additions and 45 deletions

View file

@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Transformer;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary;
use function Functional\invoke;
use function Functional\invoke_if;
@ -33,7 +34,10 @@ class ShortUrlDataTransformer implements DataTransformerInterface
'title' => $shortUrl->title(),
'crawlable' => $shortUrl->crawlable(),
'forwardQuery' => $shortUrl->forwardQuery(),
'visitsSummary' => $this->buildVisitsSummary($shortUrl),
'visitsSummary' => VisitsSummary::fromTotalAndNonBots(
$shortUrl->getVisitsCount(),
$shortUrl->nonBotVisitsCount(),
),
// Deprecated
'visitsCount' => $shortUrl->getVisitsCount(),
@ -52,16 +56,4 @@ class ShortUrlDataTransformer implements DataTransformerInterface
'maxVisits' => $maxVisits,
];
}
private function buildVisitsSummary(ShortUrl $shortUrl): array
{
$totalVisits = $shortUrl->getVisitsCount();
$nonBotVisits = $shortUrl->nonBotVisitsCount();
return [
'total' => $totalVisits,
'nonBots' => $nonBotVisits,
'bots' => $totalVisits - $nonBotVisits,
];
}
}

View file

@ -8,15 +8,34 @@ use JsonSerializable;
final class VisitsStats implements JsonSerializable
{
public function __construct(private int $visitsCount, private int $orphanVisitsCount)
{
private readonly VisitsSummary $nonOrphanVisitsSummary;
private readonly VisitsSummary $orphanVisitsSummary;
public function __construct(
int $nonOrphanVisitsTotal,
int $orphanVisitsTotal,
?int $nonOrphanVisitsNonBots = null,
?int $orphanVisitsNonBots = null,
) {
$this->nonOrphanVisitsSummary = VisitsSummary::fromTotalAndNonBots(
$nonOrphanVisitsTotal,
$nonOrphanVisitsNonBots ?? $nonOrphanVisitsTotal,
);
$this->orphanVisitsSummary = VisitsSummary::fromTotalAndNonBots(
$orphanVisitsTotal,
$orphanVisitsNonBots ?? $orphanVisitsTotal,
);
}
public function jsonSerialize(): array
{
return [
'visitsCount' => $this->visitsCount,
'orphanVisitsCount' => $this->orphanVisitsCount,
'nonOrphanVisits' => $this->nonOrphanVisitsSummary,
'orphanVisits' => $this->orphanVisitsSummary,
// Deprecated
'visitsCount' => $this->nonOrphanVisitsSummary->total,
'orphanVisitsCount' => $this->orphanVisitsSummary->total,
];
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Model;
use JsonSerializable;
final class VisitsSummary implements JsonSerializable
{
private function __construct(public readonly int $total, public readonly int $nonBots)
{
}
public static function fromTotalAndNonBots(int $total, int $nonBots): self
{
return new self($total, $nonBots);
}
public function jsonSerialize(): array
{
return [
'total' => $this->total,
'nonBots' => $this->nonBots,
'bots' => $this->total - $this->nonBots,
];
}
}

View file

@ -12,26 +12,25 @@ use Shlinkio\Shlink\Core\Visit\Repository\VisitRepositoryInterface;
class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
{
public function __construct(private VisitRepositoryInterface $repo, private VisitsParams $params)
public function __construct(private readonly VisitRepositoryInterface $repo, private readonly VisitsParams $params)
{
}
protected function doCount(): int
{
return $this->repo->countOrphanVisits(new VisitsCountFiltering(
$this->params->dateRange,
$this->params->excludeBots,
dateRange: $this->params->dateRange,
excludeBots: $this->params->excludeBots,
));
}
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findOrphanVisits(new VisitsListFiltering(
$this->params->dateRange,
$this->params->excludeBots,
null,
$length,
$offset,
dateRange: $this->params->dateRange,
excludeBots: $this->params->excludeBots,
limit: $length,
offset: $offset,
));
}
}

View file

@ -18,6 +18,6 @@ class VisitsCountFiltering
public static function withApiKey(?ApiKey $apiKey): self
{
return new self(null, false, $apiKey);
return new self(apiKey: $apiKey);
}
}

View file

@ -32,7 +32,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsStatsHelper implements VisitsStatsHelperInterface
{
public function __construct(private EntityManagerInterface $em)
public function __construct(private readonly EntityManagerInterface $em)
{
}
@ -42,13 +42,17 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
$visitsRepo = $this->em->getRepository(Visit::class);
return new VisitsStats(
$visitsRepo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey)),
$visitsRepo->countOrphanVisits(new VisitsCountFiltering()),
nonOrphanVisitsTotal: $visitsRepo->countNonOrphanVisits(VisitsCountFiltering::withApiKey($apiKey)),
orphanVisitsTotal: $visitsRepo->countOrphanVisits(new VisitsCountFiltering()),
nonOrphanVisitsNonBots: $visitsRepo->countNonOrphanVisits(
new VisitsCountFiltering(excludeBots: true, apiKey: $apiKey),
),
orphanVisitsNonBots: $visitsRepo->countOrphanVisits(new VisitsCountFiltering(excludeBots: true)),
);
}
/**
* @return Visit[]|Paginator
* @return Paginator<Visit>
* @throws ShortUrlNotFoundException
*/
public function visitsForShortUrl(

View file

@ -14,6 +14,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
use Shlinkio\Shlink\Core\Visit\Model\VisitsSummary;
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
use Shlinkio\Shlink\Core\Visit\Transformer\OrphanVisitDataTransformer;
@ -63,11 +64,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
'title' => $title,
'crawlable' => false,
'forwardQuery' => true,
'visitsSummary' => [
'total' => 0,
'nonBots' => 0,
'bots' => 0,
],
'visitsSummary' => VisitsSummary::fromTotalAndNonBots(0, 0),
],
'visit' => [
'referer' => '',
@ -144,11 +141,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
'title' => $shortUrl->title(),
'crawlable' => false,
'forwardQuery' => true,
'visitsSummary' => [
'total' => 0,
'nonBots' => 0,
'bots' => 0,
],
'visitsSummary' => VisitsSummary::fromTotalAndNonBots(0, 0),
]], $update->payload);
}
}

View file

@ -53,11 +53,13 @@ class VisitsStatsHelperTest extends TestCase
public function returnsExpectedVisitsStats(int $expectedCount): void
{
$repo = $this->createMock(VisitRepository::class);
$repo->expects($this->once())->method('countNonOrphanVisits')->with(new VisitsCountFiltering())->willReturn(
$expectedCount * 3,
);
$repo->expects($this->once())->method('countOrphanVisits')->with(
$this->isInstanceOf(VisitsCountFiltering::class),
$repo->expects($this->exactly(2))->method('countNonOrphanVisits')->withConsecutive(
[new VisitsCountFiltering()],
[new VisitsCountFiltering(excludeBots: true)],
)->willReturn($expectedCount * 3);
$repo->expects($this->exactly(2))->method('countOrphanVisits')->withConsecutive(
[$this->isInstanceOf(VisitsCountFiltering::class)],
[$this->isInstanceOf(VisitsCountFiltering::class)],
)->willReturn($expectedCount);
$this->em->expects($this->once())->method('getRepository')->with(Visit::class)->willReturn($repo);