mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 04:03:12 +08:00
Extend and normalize output from visits console commands
This commit is contained in:
@@ -19,6 +19,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||||||
|
|
||||||
Instead, if you have more than 1 proxy in front of Shlink, you should provide `TRUSTED_PROXIES` env var, with either a comma-separated list of the IP addresses of your proxies, or a number indicating how many proxies are there in front of Shlink.
|
Instead, if you have more than 1 proxy in front of Shlink, you should provide `TRUSTED_PROXIES` env var, with either a comma-separated list of the IP addresses of your proxies, or a number indicating how many proxies are there in front of Shlink.
|
||||||
|
|
||||||
|
* [#2311](https://github.com/shlinkio/shlink/issues/2311) All visits-related commands now return more information, and columns are arranged slightly differently.
|
||||||
|
|
||||||
|
Among other things, they now always return the type of the visit, region, visited URL, redirected URL and whether the visit comes from a potential bot or not.
|
||||||
|
|
||||||
* [#2540](https://github.com/shlinkio/shlink/issues/2540) Update Symfony packages to 8.0.
|
* [#2540](https://github.com/shlinkio/shlink/issues/2540) Update Symfony packages to 8.0.
|
||||||
* [#2512](https://github.com/shlinkio/shlink/issues/2512) Make all remaining console commands invokable.
|
* [#2512](https://github.com/shlinkio/shlink/issues/2512) Make all remaining console commands invokable.
|
||||||
|
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ return [
|
|||||||
],
|
],
|
||||||
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
Command\Visit\DeleteOrphanVisitsCommand::class => [Visit\VisitsDeleter::class],
|
Command\Visit\DeleteOrphanVisitsCommand::class => [Visit\VisitsDeleter::class],
|
||||||
Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
|
||||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
||||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||||
@@ -119,11 +119,11 @@ return [
|
|||||||
Command\Tag\ListTagsCommand::class => [TagService::class],
|
Command\Tag\ListTagsCommand::class => [TagService::class],
|
||||||
Command\Tag\RenameTagCommand::class => [TagService::class],
|
Command\Tag\RenameTagCommand::class => [TagService::class],
|
||||||
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
||||||
Command\Tag\GetTagVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
Command\Tag\GetTagVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
|
||||||
Command\Domain\ListDomainsCommand::class => [DomainService::class],
|
Command\Domain\ListDomainsCommand::class => [DomainService::class],
|
||||||
Command\Domain\DomainRedirectsCommand::class => [DomainService::class],
|
Command\Domain\DomainRedirectsCommand::class => [DomainService::class],
|
||||||
Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
|
||||||
Command\RedirectRule\ManageRedirectRulesCommand::class => [
|
Command\RedirectRule\ManageRedirectRulesCommand::class => [
|
||||||
ShortUrl\ShortUrlResolver::class,
|
ShortUrl\ShortUrlResolver::class,
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ namespace Shlinkio\Shlink\CLI\Command\Domain;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils;
|
use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils;
|
||||||
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Symfony\Component\Console\Attribute\Argument;
|
use Symfony\Component\Console\Attribute\Argument;
|
||||||
@@ -22,10 +20,8 @@ class GetDomainVisitsCommand extends Command
|
|||||||
{
|
{
|
||||||
public const string NAME = 'domain:visits';
|
public const string NAME = 'domain:visits';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(private readonly VisitsStatsHelperInterface $visitsHelper)
|
||||||
private readonly VisitsStatsHelperInterface $visitsHelper,
|
{
|
||||||
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,17 +32,8 @@ class GetDomainVisitsCommand extends Command
|
|||||||
#[MapInput] VisitsListInput $input,
|
#[MapInput] VisitsListInput $input,
|
||||||
): int {
|
): int {
|
||||||
$paginator = $this->visitsHelper->visitsForDomain($domain, new VisitsParams($input->dateRange()));
|
$paginator = $this->visitsHelper->visitsForDomain($domain, new VisitsParams($input->dateRange()));
|
||||||
VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...));
|
VisitsCommandUtils::renderOutput($io, $input, $paginator);
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
protected function mapExtraFields(Visit $visit): array
|
|
||||||
{
|
|
||||||
$shortUrl = $visit->shortUrl;
|
|
||||||
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ namespace Shlinkio\Shlink\CLI\Command\Tag;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils;
|
use Shlinkio\Shlink\CLI\Command\Visit\VisitsCommandUtils;
|
||||||
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
|
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Symfony\Component\Console\Attribute\Argument;
|
use Symfony\Component\Console\Attribute\Argument;
|
||||||
@@ -24,10 +22,8 @@ class GetTagVisitsCommand extends Command
|
|||||||
{
|
{
|
||||||
public const string NAME = 'tag:visits';
|
public const string NAME = 'tag:visits';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(private readonly VisitsStatsHelperInterface $visitsHelper)
|
||||||
private readonly VisitsStatsHelperInterface $visitsHelper,
|
{
|
||||||
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,17 +43,8 @@ class GetTagVisitsCommand extends Command
|
|||||||
domain: $domain,
|
domain: $domain,
|
||||||
));
|
));
|
||||||
|
|
||||||
VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...));
|
VisitsCommandUtils::renderOutput($io, $input, $paginator);
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
private function mapExtraFields(Visit $visit): array
|
|
||||||
{
|
|
||||||
$shortUrl = $visit->shortUrl;
|
|
||||||
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
|
use Shlinkio\Shlink\Core\Visit\Model\WithDomainVisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Symfony\Component\Console\Attribute\AsCommand;
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
@@ -21,10 +19,8 @@ class GetNonOrphanVisitsCommand extends Command
|
|||||||
{
|
{
|
||||||
public const string NAME = 'visit:non-orphan';
|
public const string NAME = 'visit:non-orphan';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(private readonly VisitsStatsHelperInterface $visitsHelper)
|
||||||
private readonly VisitsStatsHelperInterface $visitsHelper,
|
{
|
||||||
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
|
||||||
) {
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,17 +38,8 @@ class GetNonOrphanVisitsCommand extends Command
|
|||||||
dateRange: $input->dateRange(),
|
dateRange: $input->dateRange(),
|
||||||
domain: $domain,
|
domain: $domain,
|
||||||
));
|
));
|
||||||
VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...));
|
VisitsCommandUtils::renderOutput($io, $input, $paginator);
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
private function mapExtraFields(Visit $visit): array
|
|
||||||
{
|
|
||||||
$shortUrl = $visit->shortUrl;
|
|
||||||
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
use Shlinkio\Shlink\CLI\Input\VisitsListInput;
|
||||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
|
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitsParams;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitType;
|
use Shlinkio\Shlink\Core\Visit\Model\OrphanVisitType;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
@@ -42,16 +41,8 @@ class GetOrphanVisitsCommand extends Command
|
|||||||
domain: $domain,
|
domain: $domain,
|
||||||
type: $type,
|
type: $type,
|
||||||
));
|
));
|
||||||
VisitsCommandUtils::renderOutput($io, $input, $paginator, $this->mapExtraFields(...));
|
VisitsCommandUtils::renderOutput($io, $input, $paginator);
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array<string, string>
|
|
||||||
*/
|
|
||||||
private function mapExtraFields(Visit $visit): array
|
|
||||||
{
|
|
||||||
return ['type' => $visit->type->value];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,16 +13,12 @@ use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtils;
|
|||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
use function array_keys;
|
|
||||||
use function array_map;
|
use function array_map;
|
||||||
use function Shlinkio\Shlink\Core\ArrayUtils\select_keys;
|
|
||||||
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
|
|
||||||
|
|
||||||
class VisitsCommandUtils
|
class VisitsCommandUtils
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Paginator<Visit> $paginator
|
* @param Paginator<Visit> $paginator
|
||||||
* @param null|callable(Visit $visits): array<string, string> $mapExtraFields
|
|
||||||
*/
|
*/
|
||||||
public static function renderOutput(
|
public static function renderOutput(
|
||||||
OutputInterface $output,
|
OutputInterface $output,
|
||||||
@@ -36,25 +32,21 @@ class VisitsCommandUtils
|
|||||||
}
|
}
|
||||||
|
|
||||||
match ($inputData->format) {
|
match ($inputData->format) {
|
||||||
VisitsListFormat::CSV => self::renderCSVOutput($output, $paginator, $mapExtraFields),
|
VisitsListFormat::CSV => self::renderCSVOutput($output, $paginator),
|
||||||
default => self::renderHumanFriendlyOutput($output, $paginator, $mapExtraFields),
|
default => self::renderHumanFriendlyOutput($output, $paginator),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Paginator<Visit> $paginator
|
* @param Paginator<Visit> $paginator
|
||||||
* @param null|callable(Visit $visits): array<string, string> $mapExtraFields
|
|
||||||
*/
|
*/
|
||||||
private static function renderCSVOutput(
|
private static function renderCSVOutput(OutputInterface $output, Paginator $paginator): void
|
||||||
OutputInterface $output,
|
{
|
||||||
Paginator $paginator,
|
|
||||||
callable|null $mapExtraFields,
|
|
||||||
): void {
|
|
||||||
$page = 1;
|
$page = 1;
|
||||||
do {
|
do {
|
||||||
$paginator->setCurrentPage($page);
|
$paginator->setCurrentPage($page);
|
||||||
|
|
||||||
[$rows, $headers] = self::resolveRowsAndHeaders($paginator, $mapExtraFields);
|
[$rows, $headers] = self::resolveRowsAndHeaders($paginator);
|
||||||
$csv = Writer::fromString();
|
$csv = Writer::fromString();
|
||||||
if ($page === 1) {
|
if ($page === 1) {
|
||||||
$csv->insertOne($headers);
|
$csv->insertOne($headers);
|
||||||
@@ -69,19 +61,15 @@ class VisitsCommandUtils
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Paginator<Visit> $paginator
|
* @param Paginator<Visit> $paginator
|
||||||
* @param null|callable(Visit $visits): array<string, string> $mapExtraFields
|
|
||||||
*/
|
*/
|
||||||
private static function renderHumanFriendlyOutput(
|
private static function renderHumanFriendlyOutput(OutputInterface $output, Paginator $paginator): void
|
||||||
OutputInterface $output,
|
{
|
||||||
Paginator $paginator,
|
|
||||||
callable|null $mapExtraFields,
|
|
||||||
): void {
|
|
||||||
$page = 1;
|
$page = 1;
|
||||||
do {
|
do {
|
||||||
$paginator->setCurrentPage($page);
|
$paginator->setCurrentPage($page);
|
||||||
$page++;
|
$page++;
|
||||||
|
|
||||||
[$rows, $headers] = self::resolveRowsAndHeaders($paginator, $mapExtraFields);
|
[$rows, $headers] = self::resolveRowsAndHeaders($paginator);
|
||||||
ShlinkTable::default($output)->render(
|
ShlinkTable::default($output)->render(
|
||||||
$headers,
|
$headers,
|
||||||
$rows,
|
$rows,
|
||||||
@@ -92,35 +80,38 @@ class VisitsCommandUtils
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Paginator<Visit> $paginator
|
* @param Paginator<Visit> $paginator
|
||||||
* @param null|callable(Visit $visits): array<string, string> $mapExtraFields
|
|
||||||
*/
|
*/
|
||||||
private static function resolveRowsAndHeaders(Paginator $paginator, callable|null $mapExtraFields): array
|
private static function resolveRowsAndHeaders(Paginator $paginator): array
|
||||||
{
|
{
|
||||||
$extraKeys = null;
|
$headers = [
|
||||||
$mapExtraFields ??= static fn (Visit $_) => [];
|
'Date',
|
||||||
|
'Potential bot',
|
||||||
$rows = array_map(function (Visit $visit) use (&$extraKeys, $mapExtraFields) {
|
'User agent',
|
||||||
$extraFields = $mapExtraFields($visit);
|
'Referer',
|
||||||
$extraKeys ??= array_keys($extraFields);
|
'Country',
|
||||||
|
'Region',
|
||||||
$rowData = [
|
'City',
|
||||||
'referer' => $visit->referer,
|
'Visited URL',
|
||||||
'date' => $visit->date->toAtomString(),
|
'Redirect URL',
|
||||||
'userAgent' => $visit->userAgent,
|
'Type',
|
||||||
'potentialBot' => $visit->potentialBot,
|
|
||||||
'country' => $visit->getVisitLocation()->countryName ?? 'Unknown',
|
|
||||||
'city' => $visit->getVisitLocation()->cityName ?? 'Unknown',
|
|
||||||
...$extraFields,
|
|
||||||
];
|
|
||||||
|
|
||||||
// Filter out unknown keys
|
|
||||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]);
|
|
||||||
}, [...$paginator->getCurrentPageResults()]);
|
|
||||||
$extra = array_map(camelCaseToHumanFriendly(...), $extraKeys ?? []);
|
|
||||||
|
|
||||||
return [
|
|
||||||
$rows,
|
|
||||||
['Referer', 'Date', 'User agent', 'Country', 'City', ...$extra],
|
|
||||||
];
|
];
|
||||||
|
$rows = array_map(function (Visit $visit) {
|
||||||
|
$visitLocation = $visit->visitLocation;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'date' => $visit->date->toAtomString(),
|
||||||
|
'potentialBot' => $visit->potentialBot ? 'Potential bot' : '',
|
||||||
|
'userAgent' => $visit->userAgent,
|
||||||
|
'referer' => $visit->referer,
|
||||||
|
'country' => $visitLocation->countryName ?? 'Unknown',
|
||||||
|
'region' => $visitLocation->regionName ?? 'Unknown',
|
||||||
|
'city' => $visitLocation->cityName ?? 'Unknown',
|
||||||
|
'visitedUrl' => $visit->visitedUrl ?? 'Unknown',
|
||||||
|
'redirectUrl' => $visit->redirectUrl ?? 'Unknown',
|
||||||
|
'type' => $visit->type->value,
|
||||||
|
];
|
||||||
|
}, [...$paginator->getCurrentPageResults()]);
|
||||||
|
|
||||||
|
return [$rows, $headers];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||||
@@ -24,16 +24,11 @@ class GetDomainVisitsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
$this->commandTester = CliTestUtils::testerForCommand(new GetDomainVisitsCommand($this->visitsHelper));
|
||||||
|
|
||||||
$this->commandTester = CliTestUtils::testerForCommand(
|
|
||||||
new GetDomainVisitsCommand($this->visitsHelper, $this->stringifier),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
@@ -48,22 +43,22 @@ class GetDomainVisitsCommandTest extends TestCase
|
|||||||
$domain,
|
$domain,
|
||||||
$this->anything(),
|
$this->anything(),
|
||||||
)->willReturn(new Paginator(new ArrayAdapter([$visit])));
|
)->willReturn(new Paginator(new ArrayAdapter([$visit])));
|
||||||
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn(
|
|
||||||
'the_short_url',
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->commandTester->execute(['domain' => $domain]);
|
$this->commandTester->execute(['domain' => $domain]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
$type = VisitType::VALID_SHORT_URL->value;
|
||||||
|
|
||||||
self::assertEquals(
|
self::assertEquals(
|
||||||
|
// phpcs:disable Generic.Files.LineLength
|
||||||
<<<OUTPUT
|
<<<OUTPUT
|
||||||
+---------+---------------------------+------------+---------+--------+---------------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| Referer | Date | User agent | Country | City | Short Url |
|
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
|
||||||
+---------+---------------------------+------------+---------+--------+---------------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
|
||||||
+---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+
|
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
|
||||||
|
|
||||||
OUTPUT,
|
OUTPUT,
|
||||||
|
// phpcs:enable
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,20 +107,22 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
yield 'regular' => [
|
yield 'regular' => [
|
||||||
VisitsListFormat::FULL,
|
VisitsListFormat::FULL,
|
||||||
|
// phpcs:disable Generic.Files.LineLength
|
||||||
static fn (Chronos $date) => <<<OUTPUT
|
static fn (Chronos $date) => <<<OUTPUT
|
||||||
+---------+---------------------------+------------+---------+--------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| Referer | Date | User agent | Country | City |
|
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
|
||||||
+---------+---------------------------+------------+---------+--------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| foo | {$date->toAtomString()} | bar | Spain | Madrid |
|
| {$date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | valid_short_url |
|
||||||
+---------+------------------ Page 1 of 1 ---------+---------+--------+
|
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
|
||||||
|
|
||||||
OUTPUT,
|
OUTPUT,
|
||||||
|
// phpcs:enable
|
||||||
];
|
];
|
||||||
yield 'CSV' => [
|
yield 'CSV' => [
|
||||||
VisitsListFormat::CSV,
|
VisitsListFormat::CSV,
|
||||||
static fn (Chronos $date) => <<<OUTPUT
|
static fn (Chronos $date) => <<<OUTPUT
|
||||||
Referer,Date,"User agent",Country,City
|
Date,"Potential bot","User agent",Referer,Country,Region,City,"Visited URL","Redirect URL",Type
|
||||||
foo,{$date->toAtomString()},bar,Spain,Madrid
|
{$date->toAtomString()},,bar,foo,Spain,,Madrid,,Unknown,valid_short_url
|
||||||
|
|
||||||
OUTPUT,
|
OUTPUT,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||||
@@ -24,16 +24,11 @@ class GetTagVisitsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
$this->commandTester = CliTestUtils::testerForCommand(new GetTagVisitsCommand($this->visitsHelper));
|
||||||
|
|
||||||
$this->commandTester = CliTestUtils::testerForCommand(
|
|
||||||
new GetTagVisitsCommand($this->visitsHelper, $this->stringifier),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
@@ -47,20 +42,22 @@ class GetTagVisitsCommandTest extends TestCase
|
|||||||
$this->visitsHelper->expects($this->once())->method('visitsForTag')->with($tag, $this->anything())->willReturn(
|
$this->visitsHelper->expects($this->once())->method('visitsForTag')->with($tag, $this->anything())->willReturn(
|
||||||
new Paginator(new ArrayAdapter([$visit])),
|
new Paginator(new ArrayAdapter([$visit])),
|
||||||
);
|
);
|
||||||
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn('the_short_url');
|
|
||||||
|
|
||||||
$this->commandTester->execute(['tag' => $tag]);
|
$this->commandTester->execute(['tag' => $tag]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
$type = VisitType::VALID_SHORT_URL->value;
|
||||||
|
|
||||||
self::assertEquals(
|
self::assertEquals(
|
||||||
|
// phpcs:disable Generic.Files.LineLength
|
||||||
<<<OUTPUT
|
<<<OUTPUT
|
||||||
+---------+---------------------------+------------+---------+--------+---------------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| Referer | Date | User agent | Country | City | Short Url |
|
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
|
||||||
+---------+---------------------------+------------+---------+--------+---------------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
|
||||||
+---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+
|
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
|
||||||
|
|
||||||
OUTPUT,
|
OUTPUT,
|
||||||
|
// phpcs:enable
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Shlinkio\Shlink\CLI\Command\Visit\GetNonOrphanVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\GetNonOrphanVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||||
@@ -24,16 +24,11 @@ class GetNonOrphanVisitsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
private CommandTester $commandTester;
|
private CommandTester $commandTester;
|
||||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
$this->commandTester = CliTestUtils::testerForCommand(new GetNonOrphanVisitsCommand($this->visitsHelper));
|
||||||
|
|
||||||
$this->commandTester = CliTestUtils::testerForCommand(
|
|
||||||
new GetNonOrphanVisitsCommand($this->visitsHelper, $this->stringifier),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
@@ -46,20 +41,22 @@ class GetNonOrphanVisitsCommandTest extends TestCase
|
|||||||
$this->visitsHelper->expects($this->once())->method('nonOrphanVisits')->withAnyParameters()->willReturn(
|
$this->visitsHelper->expects($this->once())->method('nonOrphanVisits')->withAnyParameters()->willReturn(
|
||||||
new Paginator(new ArrayAdapter([$visit])),
|
new Paginator(new ArrayAdapter([$visit])),
|
||||||
);
|
);
|
||||||
$this->stringifier->expects($this->once())->method('stringify')->with($shortUrl)->willReturn('the_short_url');
|
|
||||||
|
|
||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
$type = VisitType::VALID_SHORT_URL->value;
|
||||||
|
|
||||||
self::assertEquals(
|
self::assertEquals(
|
||||||
|
// phpcs:disable Generic.Files.LineLength
|
||||||
<<<OUTPUT
|
<<<OUTPUT
|
||||||
+---------+---------------------------+------------+---------+--------+---------------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| Referer | Date | User agent | Country | City | Short Url |
|
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
|
||||||
+---------+---------------------------+------------+---------+--------+---------------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+-----------------+
|
||||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
|
||||||
+---------+-------------------------- Page 1 of 1 -+---------+--------+---------------+
|
+---------------------------+---------------+------------+------- Page 1 of 1 --------+--------+-------------+--------------+-----------------+
|
||||||
|
|
||||||
OUTPUT,
|
OUTPUT,
|
||||||
|
// phpcs:enable
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,16 +48,19 @@ class GetOrphanVisitsCommandTest extends TestCase
|
|||||||
|
|
||||||
$this->commandTester->execute($args);
|
$this->commandTester->execute($args);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
$type = OrphanVisitType::BASE_URL->value;
|
||||||
|
|
||||||
self::assertEquals(
|
self::assertEquals(
|
||||||
|
// phpcs:disable Generic.Files.LineLength
|
||||||
<<<OUTPUT
|
<<<OUTPUT
|
||||||
+---------+---------------------------+------------+---------+--------+----------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+
|
||||||
| Referer | Date | User agent | Country | City | Type |
|
| Date | Potential bot | User agent | Referer | Country | Region | City | Visited URL | Redirect URL | Type |
|
||||||
+---------+---------------------------+------------+---------+--------+----------+
|
+---------------------------+---------------+------------+---------+---------+--------+--------+-------------+--------------+----------+
|
||||||
| foo | {$visit->date->toAtomString()} | bar | Spain | Madrid | base_url |
|
| {$visit->date->toAtomString()} | | bar | foo | Spain | | Madrid | | Unknown | {$type} |
|
||||||
+---------+----------------------- Page 1 of 1 ----+---------+--------+----------+
|
+---------------------------+---------------+------------+--- Page 1 of 1 ---+--------+--------+-------------+--------------+----------+
|
||||||
|
|
||||||
OUTPUT,
|
OUTPUT,
|
||||||
|
// phpcs:enable
|
||||||
$output,
|
$output,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\ArrayUtils;
|
namespace Shlinkio\Shlink\Core\ArrayUtils;
|
||||||
|
|
||||||
use function array_filter;
|
|
||||||
use function array_reduce;
|
|
||||||
use function in_array;
|
use function in_array;
|
||||||
|
|
||||||
use const ARRAY_FILTER_USE_KEY;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @param T $value
|
* @param T $value
|
||||||
@@ -20,18 +16,6 @@ function contains(mixed $value, array $array): bool
|
|||||||
return in_array($value, $array, strict: true);
|
return in_array($value, $array, strict: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array[] $multiArray
|
|
||||||
*/
|
|
||||||
function flatten(array $multiArray): array
|
|
||||||
{
|
|
||||||
return array_reduce(
|
|
||||||
$multiArray,
|
|
||||||
static fn (array $carry, array $value) => [...$carry, ...$value],
|
|
||||||
initial: [],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a callback returns true for at least one item in a collection.
|
* Checks if a callback returns true for at least one item in a collection.
|
||||||
* @param callable(mixed $value, mixed $key): bool $callback
|
* @param callable(mixed $value, mixed $key): bool $callback
|
||||||
@@ -62,21 +46,6 @@ function every(iterable $collection, callable $callback): bool
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array containing only those entries in the array whose key is in the supplied keys.
|
|
||||||
*/
|
|
||||||
function select_keys(array $array, array $keys): array
|
|
||||||
{
|
|
||||||
return array_filter(
|
|
||||||
$array,
|
|
||||||
static fn (string $key) => contains(
|
|
||||||
$key,
|
|
||||||
$keys,
|
|
||||||
),
|
|
||||||
ARRAY_FILTER_USE_KEY,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @template T
|
||||||
* @template R
|
* @template R
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ readonly class MatomoVisitSender implements MatomoVisitSenderInterface
|
|||||||
->setUrlReferrer($visit->referer)
|
->setUrlReferrer($visit->referer)
|
||||||
->setForceVisitDateTime($visit->date->setTimezone('UTC')->toDateTimeString());
|
->setForceVisitDateTime($visit->date->setTimezone('UTC')->toDateTimeString());
|
||||||
|
|
||||||
$location = $visit->getVisitLocation();
|
$location = $visit->visitLocation;
|
||||||
if ($location !== null) {
|
if ($location !== null) {
|
||||||
$tracker
|
$tracker
|
||||||
->setCity($location->cityName)
|
->setCity($location->cityName)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
public readonly string|null $remoteAddr = null,
|
public readonly string|null $remoteAddr = null,
|
||||||
public readonly string|null $visitedUrl = null,
|
public readonly string|null $visitedUrl = null,
|
||||||
public readonly string|null $redirectUrl = null,
|
public readonly string|null $redirectUrl = null,
|
||||||
private VisitLocation|null $visitLocation = null,
|
private(set) VisitLocation|null $visitLocation = null,
|
||||||
public readonly Chronos $date = new Chronos(),
|
public readonly Chronos $date = new Chronos(),
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -124,11 +124,6 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
return ! empty($this->remoteAddr);
|
return ! empty($this->remoteAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getVisitLocation(): VisitLocation|null
|
|
||||||
{
|
|
||||||
return $this->visitLocation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function locate(VisitLocation $visitLocation): self
|
public function locate(VisitLocation $visitLocation): self
|
||||||
{
|
{
|
||||||
$this->visitLocation = $visitLocation;
|
$this->visitLocation = $visitLocation;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ readonly class VisitLocator implements VisitLocatorInterface
|
|||||||
|
|
||||||
private function locateVisit(Visit $visit, VisitLocation $location, VisitGeolocationHelperInterface $helper): void
|
private function locateVisit(Visit $visit, VisitLocation $location, VisitGeolocationHelperInterface $helper): void
|
||||||
{
|
{
|
||||||
$prevLocation = $visit->getVisitLocation();
|
$prevLocation = $visit->visitLocation;
|
||||||
|
|
||||||
$visit->locate($location);
|
$visit->locate($location);
|
||||||
$this->em->persist($visit);
|
$this->em->persist($visit);
|
||||||
|
|||||||
Reference in New Issue
Block a user