mirror of
https://github.com/shlinkio/shlink.git
synced 2025-12-10 11:05:50 -06:00
Add redirect_url field to track where a visitor is redirected for a visit
This commit is contained in:
parent
fef512a7a3
commit
8274525f75
@ -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": {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
39
module/Core/migrations/Version20241124112257.php
Normal file
39
module/Core/migrations/Version20241124112257.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -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')]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user