Added logic to exclude bots from visits when requested

This commit is contained in:
Alejandro Celaya 2021-05-22 20:49:24 +02:00
parent db3c5a3031
commit 69d72e754f
4 changed files with 52 additions and 12 deletions

View file

@ -53,6 +53,11 @@ final class Visitor
return new self('', '', null, ''); return new self('', '', null, '');
} }
public static function botInstance(): self
{
return new self('cf-facebook', '', null, '');
}
public function getUserAgent(): string public function getUserAgent(): string
{ {
return $this->userAgent; return $this->userAgent;

View file

@ -114,6 +114,10 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb->from(Visit::class, 'v') $qb->from(Visit::class, 'v')
->where($qb->expr()->eq('v.shortUrl', $shortUrlId)); ->where($qb->expr()->eq('v.shortUrl', $shortUrlId));
if ($filtering->excludeBots()) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
// Apply date range filtering // Apply date range filtering
$this->applyDatesInline($qb, $filtering->dateRange()); $this->applyDatesInline($qb, $filtering->dateRange());
@ -144,6 +148,10 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
->join('s.tags', 't') ->join('s.tags', 't')
->where($qb->expr()->eq('t.name', '\'' . $tag . '\'')); // This needs to be concatenated, not bound ->where($qb->expr()->eq('t.name', '\'' . $tag . '\'')); // This needs to be concatenated, not bound
if ($filtering->excludeBots()) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange()); $this->applyDatesInline($qb, $filtering->dateRange());
$this->applySpecification($qb, $filtering->spec(), 'v'); $this->applySpecification($qb, $filtering->spec(), 'v');
@ -158,6 +166,10 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb->from(Visit::class, 'v') $qb->from(Visit::class, 'v')
->where($qb->expr()->isNull('v.shortUrl')); ->where($qb->expr()->isNull('v.shortUrl'));
if ($filtering->excludeBots()) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange()); $this->applyDatesInline($qb, $filtering->dateRange());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset()); return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
@ -165,7 +177,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function countOrphanVisits(VisitsCountFiltering $filtering): int public function countOrphanVisits(VisitsCountFiltering $filtering): int
{ {
return (int) $this->matchSingleScalarResult(new CountOfOrphanVisits($filtering->dateRange())); return (int) $this->matchSingleScalarResult(new CountOfOrphanVisits($filtering));
} }
public function countVisits(?ApiKey $apiKey = null): int public function countVisits(?ApiKey $apiKey = null): int

View file

@ -7,24 +7,30 @@ namespace Shlinkio\Shlink\Core\Visit\Spec;
use Happyr\DoctrineSpecification\Spec; use Happyr\DoctrineSpecification\Spec;
use Happyr\DoctrineSpecification\Specification\BaseSpecification; use Happyr\DoctrineSpecification\Specification\BaseSpecification;
use Happyr\DoctrineSpecification\Specification\Specification; use Happyr\DoctrineSpecification\Specification\Specification;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Spec\InDateRange; use Shlinkio\Shlink\Core\Spec\InDateRange;
use Shlinkio\Shlink\Core\Visit\Persistence\VisitsCountFiltering;
class CountOfOrphanVisits extends BaseSpecification class CountOfOrphanVisits extends BaseSpecification
{ {
private ?DateRange $dateRange; private VisitsCountFiltering $filtering;
public function __construct(?DateRange $dateRange) public function __construct(VisitsCountFiltering $filtering)
{ {
parent::__construct(); parent::__construct();
$this->dateRange = $dateRange; $this->filtering = $filtering;
} }
protected function getSpec(): Specification protected function getSpec(): Specification
{ {
return Spec::countOf(Spec::andX( $conditions = [
Spec::isNull('shortUrl'), Spec::isNull('shortUrl'),
new InDateRange($this->dateRange), new InDateRange($this->filtering->dateRange()),
)); ];
if ($this->filtering->excludeBots()) {
$conditions[] = Spec::eq('potentialBot', false);
}
return Spec::countOf(Spec::andX(...$conditions));
} }
} }

View file

@ -91,6 +91,7 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertCount(0, $this->repo->findVisitsByShortCode('invalid', null, new VisitsListFiltering())); self::assertCount(0, $this->repo->findVisitsByShortCode('invalid', null, new VisitsListFiltering()));
self::assertCount(6, $this->repo->findVisitsByShortCode($shortCode, null, new VisitsListFiltering())); self::assertCount(6, $this->repo->findVisitsByShortCode($shortCode, null, new VisitsListFiltering()));
self::assertCount(4, $this->repo->findVisitsByShortCode($shortCode, null, new VisitsListFiltering(null, true)));
self::assertCount(3, $this->repo->findVisitsByShortCode($shortCode, $domain, new VisitsListFiltering())); self::assertCount(3, $this->repo->findVisitsByShortCode($shortCode, $domain, new VisitsListFiltering()));
self::assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, new VisitsListFiltering( self::assertCount(2, $this->repo->findVisitsByShortCode($shortCode, null, new VisitsListFiltering(
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')), DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
@ -125,6 +126,10 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertEquals(0, $this->repo->countVisitsByShortCode('invalid', null, new VisitsCountFiltering())); self::assertEquals(0, $this->repo->countVisitsByShortCode('invalid', null, new VisitsCountFiltering()));
self::assertEquals(6, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering())); self::assertEquals(6, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering()));
self::assertEquals(4, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering(
null,
true,
)));
self::assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain, new VisitsCountFiltering())); self::assertEquals(3, $this->repo->countVisitsByShortCode($shortCode, $domain, new VisitsCountFiltering()));
self::assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering( self::assertEquals(2, $this->repo->countVisitsByShortCode($shortCode, null, new VisitsCountFiltering(
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')), DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
@ -154,6 +159,7 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertCount(0, $this->repo->findVisitsByTag('invalid', new VisitsListFiltering())); self::assertCount(0, $this->repo->findVisitsByTag('invalid', new VisitsListFiltering()));
self::assertCount(18, $this->repo->findVisitsByTag($foo, new VisitsListFiltering())); self::assertCount(18, $this->repo->findVisitsByTag($foo, new VisitsListFiltering()));
self::assertCount(12, $this->repo->findVisitsByTag($foo, new VisitsListFiltering(null, true)));
self::assertCount(6, $this->repo->findVisitsByTag($foo, new VisitsListFiltering( self::assertCount(6, $this->repo->findVisitsByTag($foo, new VisitsListFiltering(
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')), DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
))); )));
@ -175,6 +181,7 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertEquals(0, $this->repo->countVisitsByTag('invalid', new VisitsCountFiltering())); self::assertEquals(0, $this->repo->countVisitsByTag('invalid', new VisitsCountFiltering()));
self::assertEquals(12, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering())); self::assertEquals(12, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering()));
self::assertEquals(8, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering(null, true)));
self::assertEquals(4, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering( self::assertEquals(4, $this->repo->countVisitsByTag($foo, new VisitsCountFiltering(
DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')), DateRange::withStartAndEndDate(Chronos::parse('2016-01-02'), Chronos::parse('2016-01-03')),
))); )));
@ -220,6 +227,7 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist(Visit::forBasePath(Visitor::emptyInstance())); $this->getEntityManager()->persist(Visit::forBasePath(Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forInvalidShortUrl(Visitor::emptyInstance())); $this->getEntityManager()->persist(Visit::forInvalidShortUrl(Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forRegularNotFound(Visitor::emptyInstance())); $this->getEntityManager()->persist(Visit::forRegularNotFound(Visitor::emptyInstance()));
$this->getEntityManager()->persist(Visit::forRegularNotFound(Visitor::botInstance()));
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
@ -227,7 +235,8 @@ class VisitRepositoryTest extends DatabaseTestCase
self::assertEquals(4, $this->repo->countVisits($apiKey1)); self::assertEquals(4, $this->repo->countVisits($apiKey1));
self::assertEquals(5 + 7, $this->repo->countVisits($apiKey2)); self::assertEquals(5 + 7, $this->repo->countVisits($apiKey2));
self::assertEquals(4 + 7, $this->repo->countVisits($domainApiKey)); self::assertEquals(4 + 7, $this->repo->countVisits($domainApiKey));
self::assertEquals(3, $this->repo->countOrphanVisits(new VisitsCountFiltering())); self::assertEquals(4, $this->repo->countOrphanVisits(new VisitsCountFiltering()));
self::assertEquals(3, $this->repo->countOrphanVisits(new VisitsCountFiltering(null, true)));
} }
/** @test */ /** @test */
@ -237,9 +246,10 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist($shortUrl); $this->getEntityManager()->persist($shortUrl);
$this->createVisitsForShortUrl($shortUrl, 7); $this->createVisitsForShortUrl($shortUrl, 7);
$botsCount = 3;
for ($i = 0; $i < 6; $i++) { for ($i = 0; $i < 6; $i++) {
$this->getEntityManager()->persist($this->setDateOnVisit( $this->getEntityManager()->persist($this->setDateOnVisit(
Visit::forBasePath(Visitor::emptyInstance()), Visit::forBasePath($botsCount < 1 ? Visitor::emptyInstance() : Visitor::botInstance()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)), Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
)); ));
$this->getEntityManager()->persist($this->setDateOnVisit( $this->getEntityManager()->persist($this->setDateOnVisit(
@ -250,11 +260,14 @@ class VisitRepositoryTest extends DatabaseTestCase
Visit::forRegularNotFound(Visitor::emptyInstance()), Visit::forRegularNotFound(Visitor::emptyInstance()),
Chronos::parse(sprintf('2020-01-0%s', $i + 1)), Chronos::parse(sprintf('2020-01-0%s', $i + 1)),
)); ));
$botsCount--;
} }
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
self::assertCount(18, $this->repo->findOrphanVisits(new VisitsListFiltering())); self::assertCount(18, $this->repo->findOrphanVisits(new VisitsListFiltering()));
self::assertCount(15, $this->repo->findOrphanVisits(new VisitsListFiltering(null, true)));
self::assertCount(5, $this->repo->findOrphanVisits(new VisitsListFiltering(null, false, null, 5))); self::assertCount(5, $this->repo->findOrphanVisits(new VisitsListFiltering(null, false, null, 5)));
self::assertCount(10, $this->repo->findOrphanVisits(new VisitsListFiltering(null, false, null, 15, 8))); self::assertCount(10, $this->repo->findOrphanVisits(new VisitsListFiltering(null, false, null, 15, 8)));
self::assertCount(9, $this->repo->findOrphanVisits(new VisitsListFiltering( self::assertCount(9, $this->repo->findOrphanVisits(new VisitsListFiltering(
@ -338,13 +351,17 @@ class VisitRepositoryTest extends DatabaseTestCase
return [$shortCode, $domain, $shortUrl]; return [$shortCode, $domain, $shortUrl];
} }
private function createVisitsForShortUrl(ShortUrl $shortUrl, int $amount = 6): void private function createVisitsForShortUrl(ShortUrl $shortUrl, int $amount = 6, int $botsAmount = 2): void
{ {
for ($i = 0; $i < $amount; $i++) { for ($i = 0; $i < $amount; $i++) {
$visit = $this->setDateOnVisit( $visit = $this->setDateOnVisit(
Visit::forValidShortUrl($shortUrl, Visitor::emptyInstance()), Visit::forValidShortUrl(
$shortUrl,
$botsAmount < 1 ? Visitor::emptyInstance() : Visitor::botInstance(),
),
Chronos::parse(sprintf('2016-01-0%s', $i + 1)), Chronos::parse(sprintf('2016-01-0%s', $i + 1)),
); );
$botsAmount--;
$this->getEntityManager()->persist($visit); $this->getEntityManager()->persist($visit);
} }