Add redirect_url field to track where a visitor is redirected for a visit

This commit is contained in:
Alejandro Celaya 2024-11-24 12:53:49 +01:00
parent fef512a7a3
commit 8274525f75
9 changed files with 73 additions and 0 deletions

View File

@ -247,6 +247,11 @@
"type": "string",
"nullable": true,
"description": "The originally visited URL that triggered the tracking of this visit"
},
"redirectUrl": {
"type": "string",
"nullable": true,
"description": "The URL to which the visitor was redirected"
}
},
"example": {

View File

@ -25,6 +25,10 @@
"visitedUrl": {
"type": ["string", "null"],
"description": "The originally visited URL that triggered the tracking of this visit"
},
"redirectUrl": {
"type": ["string", "null"],
"description": "The URL to which the visitor was redirected"
}
}
}

View File

@ -75,4 +75,10 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
->columnName('potential_bot')
->option('default', false)
->build();
fieldWithUtf8Charset($builder->createField('redirectUrl', Types::STRING), $emConfig)
->columnName('redirect_url')
->length(Visitor::REDIRECT_URL_MAX_LENGTH)
->nullable()
->build();
};

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
final class Version20241124112257 extends AbstractMigration
{
private const COLUMN_NAME = 'redirect_url';
public function up(Schema $schema): void
{
$visits = $schema->getTable('visits');
$this->skipIf($visits->hasColumn(self::COLUMN_NAME));
$visits->addColumn('redirected_url', Types::STRING, [
'length' => 2048,
'notnull' => false,
'default' => null,
]);
}
public function down(Schema $schema): void
{
$visits = $schema->getTable('visits');
$this->skipIf(! $visits->hasColumn(self::COLUMN_NAME));
$visits->dropColumn(self::COLUMN_NAME);
}
public function isTransactional(): bool
{
return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
}
}

View File

@ -28,6 +28,7 @@ class Visit extends AbstractEntity implements JsonSerializable
public readonly bool $potentialBot,
public readonly string|null $remoteAddr = null,
public readonly string|null $visitedUrl = null,
public readonly string|null $redirectUrl = null,
private VisitLocation|null $visitLocation = null,
public readonly Chronos $date = new Chronos(),
) {
@ -68,6 +69,7 @@ class Visit extends AbstractEntity implements JsonSerializable
potentialBot: $visitor->potentialBot,
remoteAddr: self::processAddress($visitor->remoteAddress, $anonymize),
visitedUrl: $visitor->visitedUrl,
redirectUrl: null, // TODO
visitLocation: $geolocation !== null ? VisitLocation::fromGeolocation($geolocation) : null,
);
}
@ -156,6 +158,7 @@ class Visit extends AbstractEntity implements JsonSerializable
'visitLocation' => $this->visitLocation,
'potentialBot' => $this->potentialBot,
'visitedUrl' => $this->visitedUrl,
'redirectUrl' => $this->redirectUrl,
];
if (! $this->isOrphan()) {
return $base;

View File

@ -19,6 +19,7 @@ final readonly class Visitor
public const REFERER_MAX_LENGTH = 1024;
public const REMOTE_ADDRESS_MAX_LENGTH = 256;
public const VISITED_URL_MAX_LENGTH = 2048;
public const REDIRECT_URL_MAX_LENGTH = 2048;
private function __construct(
public string $userAgent,
@ -27,6 +28,7 @@ final readonly class Visitor
public string $visitedUrl,
public bool $potentialBot,
public Location|null $geolocation,
public string $redirectUrl,
) {
}
@ -36,6 +38,7 @@ final readonly class Visitor
string|null $remoteAddress = null,
string $visitedUrl = '',
Location|null $geolocation = null,
string $redirectUrl = '',
): self {
return new self(
userAgent: self::cropToLength($userAgent, self::USER_AGENT_MAX_LENGTH),
@ -46,6 +49,7 @@ final readonly class Visitor
visitedUrl: self::cropToLength($visitedUrl, self::VISITED_URL_MAX_LENGTH),
potentialBot: isCrawler($userAgent),
geolocation: $geolocation,
redirectUrl: self::cropToLength($redirectUrl, self::REDIRECT_URL_MAX_LENGTH),
);
}
@ -62,6 +66,8 @@ final readonly class Visitor
remoteAddress: ipAddressFromRequest($request),
visitedUrl: $request->getUri()->__toString(),
geolocation: geolocationFromRequest($request),
// TODO
redirectUrl: '',
);
}
@ -85,6 +91,7 @@ final readonly class Visitor
// Keep the fact that the visit was a potential bot, even if we no longer save the user agent
potentialBot: $this->potentialBot,
geolocation: $this->geolocation,
redirectUrl: $this->redirectUrl,
);
}
}

View File

@ -80,6 +80,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
'date' => $visit->date->toAtomString(),
'potentialBot' => false,
'visitedUrl' => '',
'redirectUrl' => null,
],
], $update->payload);
}
@ -105,6 +106,7 @@ class PublishingUpdatesGeneratorTest extends TestCase
'potentialBot' => false,
'visitedUrl' => $orphanVisit->visitedUrl,
'type' => $orphanVisit->type->value,
'redirectUrl' => null,
],
], $update->payload);
}

View File

@ -34,6 +34,7 @@ class VisitTest extends TestCase
'visitLocation' => null,
'potentialBot' => $expectedToBePotentialBot,
'visitedUrl' => $visit->visitedUrl,
'redirectUrl' => $visit->redirectUrl,
], $visit->jsonSerialize());
}
@ -67,6 +68,7 @@ class VisitTest extends TestCase
'potentialBot' => false,
'visitedUrl' => '',
'type' => VisitType::BASE_URL->value,
'redirectUrl' => null,
],
];
yield 'invalid short url visit' => [
@ -83,6 +85,7 @@ class VisitTest extends TestCase
'potentialBot' => false,
'visitedUrl' => 'https://example.com/foo',
'type' => VisitType::INVALID_SHORT_URL->value,
'redirectUrl' => null,
],
];
yield 'regular 404 visit' => [
@ -101,6 +104,7 @@ class VisitTest extends TestCase
'potentialBot' => false,
'visitedUrl' => 'https://s.test/foo/bar',
'type' => VisitType::REGULAR_404->value,
'redirectUrl' => null,
],
];
}

View File

@ -21,6 +21,7 @@ class OrphanVisitsTest extends ApiTestCase
'potentialBot' => true,
'visitedUrl' => 'foo.com',
'type' => 'invalid_short_url',
'redirectUrl' => null,
];
private const REGULAR_NOT_FOUND = [
'referer' => 'https://s.test/foo/bar',
@ -30,6 +31,7 @@ class OrphanVisitsTest extends ApiTestCase
'potentialBot' => false,
'visitedUrl' => '',
'type' => 'regular_404',
'redirectUrl' => null,
];
private const BASE_URL = [
'referer' => 'https://s.test',
@ -39,6 +41,7 @@ class OrphanVisitsTest extends ApiTestCase
'potentialBot' => false,
'visitedUrl' => '',
'type' => 'base_url',
'redirectUrl' => null,
];
#[Test, DataProvider('provideQueries')]