Repository: shlinkio/shlink (language: php)

Issue:
Shlink sends country names to Matomo instead of country codes, causing DB errors (CHAR(3) limit) and "Unknown" country
### Shlink version

4.4.5

### PHP version

8.4.4

### How do you serve Shlink

Docker image

### Database engine

MariaDB

### Database version

10.5.28

### Current behavior

Shlink sends visit data to Matomo, but instead of sending a country code (e.g., "US" or "DE"), it sends the full country name (e.g., "United States" or "Germany"). Matomo expects a two- or three-letter country code, and since the database column for storing the country is defined as CHAR(3), the longer country names cause an error. As a result, Matomo sets the country to "Unknown".

### Expected behavior

Shlink should send the country code instead of the full country name to ensure proper logging in Matomo. It already provides this from the geolocation.

Changing
`                ->setCountry($location->countryName)`
to
`                ->setCountry($location->countryCode)`
in the file [module/Core/src/Matomo/MatomoVisitSender.php](https://github.com/shlinkio/shlink/blob/5eb1808217f2cebbf7ee12a55bc63f2c063c15a7/module/Core/src/Matomo/MatomoVisitSender.php#L63) resolves this issue.
I'm not sure if this is the only necessary change or if other changes are required.

### Minimum steps to reproduce

<!--
Connect shlink to matomo
-->



Discussion / hints:
Thanks!

Since you already identified the source of the issue, do you want to provide a PR?

> I'm not sure if this is the only necessary change or if other changes are required.

That should be the only change needed.

Fix patch:
diff --git a/composer.json b/composer.json
index d23ec7cf1..4a8c4e87a 100644
--- a/composer.json
+++ b/composer.json
@@ -24,7 +24,7 @@
         "doctrine/migrations": "^3.8",
         "doctrine/orm": "^3.3",
         "donatj/phpuseragentparser": "^1.10",
-        "endroid/qr-code": "^6.0",
+        "endroid/qr-code": "<6.0.4",
         "friendsofphp/proxy-manager-lts": "^1.0",
         "geoip2/geoip2": "^3.1",
         "guzzlehttp/guzzle": "^7.9",
diff --git a/module/Core/src/Matomo/MatomoVisitSender.php b/module/Core/src/Matomo/MatomoVisitSender.php
index 9fc0176a5..6a32c2a52 100644
--- a/module/Core/src/Matomo/MatomoVisitSender.php
+++ b/module/Core/src/Matomo/MatomoVisitSender.php
@@ -11,6 +11,8 @@
 use Shlinkio\Shlink\Core\Visit\Repository\VisitIterationRepositoryInterface;
 use Throwable;
 
+use function strtolower;
+
 readonly class MatomoVisitSender implements MatomoVisitSenderInterface
 {
     public function __construct(
@@ -60,7 +62,7 @@ public function sendVisit(Visit $visit, string|null $originalIpAddress = null):
         if ($location !== null) {
             $tracker
                 ->setCity($location->cityName)
-                ->setCountry($location->countryName)
+                ->setCountry(strtolower($location->countryCode))
                 ->setLatitude($location->latitude)
                 ->setLongitude($location->longitude);
         }


Test patch:
diff --git a/module/Core/test/Matomo/MatomoVisitSenderTest.php b/module/Core/test/Matomo/MatomoVisitSenderTest.php
index 0acccd1d0..7d66d8680 100644
--- a/module/Core/test/Matomo/MatomoVisitSenderTest.php
+++ b/module/Core/test/Matomo/MatomoVisitSenderTest.php
@@ -43,6 +43,9 @@ protected function setUp(): void
     }
 
     #[Test, DataProvider('provideTrackerMethods')]
+    /**
+     * @param array<string, string[]> $invokedMethods
+     */
     public function visitIsSentToMatomo(Visit $visit, string|null $originalIpAddress, array $invokedMethods): void
     {
         $tracker = $this->createMock(MatomoTracker::class);
@@ -66,8 +69,8 @@ public function visitIsSentToMatomo(Visit $visit, string|null $originalIpAddress
             )->willReturn($tracker);
         }
 
-        foreach ($invokedMethods as $invokedMethod) {
-            $tracker->expects($this->once())->method($invokedMethod)->willReturn($tracker);
+        foreach ($invokedMethods as $invokedMethod => $args) {
+            $tracker->expects($this->once())->method($invokedMethod)->with(...$args)->willReturn($tracker);
         }
 
         $this->trackerBuilder->expects($this->once())->method('buildMatomoTracker')->willReturn($tracker);
@@ -81,18 +84,28 @@ public static function provideTrackerMethods(): iterable
         yield 'located regular visit' => [
             Visit::forValidShortUrl(ShortUrl::withLongUrl('https://shlink.io'), Visitor::empty())
                 ->locate(VisitLocation::fromGeolocation(new Location(
-                    countryCode: 'countryCode',
+                    countryCode: 'US',
                     countryName: 'countryName',
                     regionName: 'regionName',
                     city: 'city',
                     latitude: 123,
-                    longitude: 123,
+                    longitude: 456,
                     timeZone: 'timeZone',
                 ))),
             '1.2.3.4',
-            ['setCity', 'setCountry', 'setLatitude', 'setLongitude', 'setIp'],
+            [
+                'setCity' => ['city'],
+                'setCountry' => ['us'],
+                'setLatitude' => [123],
+                'setLongitude' => [456],
+                'setIp' => ['1.2.3.4'],
+            ],
+        ];
+        yield 'fallback IP' => [
+            Visit::forBasePath(Visitor::fromParams(remoteAddress: '5.6.7.8')),
+            null,
+            ['setIp' => ['5.6.7.0']],
         ];
-        yield 'fallback IP' => [Visit::forBasePath(Visitor::fromParams(remoteAddress: '1.2.3.4')), null, ['setIp']];
     }
 
     #[Test, DataProvider('provideUrlsToTrack')]