diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index d96d940..418b075 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -3,6 +3,9 @@ name: Integration on: pull_request: +env: + DO_NOT_TRACK: 1 + jobs: build: runs-on: ubuntu-24.04 diff --git a/Controller/InstallController.php b/Controller/InstallController.php index d3ebd2a..05c3f4e 100644 --- a/Controller/InstallController.php +++ b/Controller/InstallController.php @@ -9,6 +9,8 @@ use Shopware\WebInstaller\Services\RecoveryManager; use Shopware\WebInstaller\Services\ReleaseInfoProvider; use Shopware\WebInstaller\Services\StreamedCommandResponseGenerator; +use Shopware\WebInstaller\Services\TrackingEvent; +use Shopware\WebInstaller\Services\TrackingService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; @@ -28,6 +30,7 @@ public function __construct( private readonly ReleaseInfoProvider $releaseInfoProvider, private readonly ProjectComposerJsonUpdater $projectComposerJsonUpdater, private readonly LanguageProvider $languageProvider, + private readonly TrackingService $trackingService, ) {} #[Route('/install', name: 'install', defaults: ['step' => 2])] @@ -45,6 +48,12 @@ public function index(): Response public function run(Request $request): StreamedResponse { $shopwareVersion = $request->query->get('shopwareVersion', ''); + $trackingId = $request->getSession()->get('trackingId', ''); + + $this->trackingService->track(TrackingEvent::InstallStarted, $trackingId, [ + 'shopware_version' => $shopwareVersion, + ]); + $folder = $this->recoveryManager->getProjectDir(); $fs = new Filesystem(); @@ -61,7 +70,11 @@ public function run(Request $request): StreamedResponse $shopwareVersion ); - $finish = function (Process $process) use ($request): void { + $finish = function (Process $process) use ($request, $shopwareVersion, $trackingId): void { + $this->trackingService->track($process->isSuccessful() ? TrackingEvent::InstallCompleted : TrackingEvent::InstallFailed, $trackingId, [ + 'shopware_version' => $shopwareVersion, + ]); + $data = [ 'success' => $process->isSuccessful(), ]; diff --git a/Controller/UpdateController.php b/Controller/UpdateController.php index e41432e..fbf1ecc 100644 --- a/Controller/UpdateController.php +++ b/Controller/UpdateController.php @@ -13,6 +13,8 @@ use Shopware\WebInstaller\Services\RecoveryManager; use Shopware\WebInstaller\Services\ReleaseInfoProvider; use Shopware\WebInstaller\Services\StreamedCommandResponseGenerator; +use Shopware\WebInstaller\Services\TrackingEvent; +use Shopware\WebInstaller\Services\TrackingService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -31,6 +33,7 @@ public function __construct( private readonly StreamedCommandResponseGenerator $streamedCommandResponseGenerator, private readonly ProjectComposerJsonUpdater $projectComposerJsonUpdater, private readonly LanguageProvider $languageProvider, + private readonly TrackingService $trackingService, ) {} #[Route('/update', name: 'update', defaults: ['step' => 2], methods: ['GET'])] @@ -77,7 +80,15 @@ public function run(Request $request): Response $version = $request->query->get('shopwareVersion', ''); $shopwarePath = $this->recoveryManager->getShopwareLocation(); + $currentVersion = $this->recoveryManager->getCurrentShopwareVersion($shopwarePath); $composerJsonPath = $shopwarePath . '/composer.json'; + $trackingId = $request->getSession()->get('trackingId', ''); + + $this->trackingService->track(TrackingEvent::UpdateStarted, $trackingId, [ + 'shopware_version_from' => $currentVersion, + 'shopware_version_to' => $version, + 'is_flex_project' => $this->recoveryManager->isFlexProject($shopwarePath), + ]); $composerJsonBackup = new FileBackup($composerJsonPath); $composerJsonBackup->backup(); @@ -103,10 +114,15 @@ public function run(Request $request): Response '--no-scripts', '-v', '--with-all-dependencies', // update all packages - ], function (Process $process) use ($composerJsonBackup): void { + ], function (Process $process) use ($composerJsonBackup, $trackingId, $currentVersion, $version): void { $process->isSuccessful() ? $composerJsonBackup->remove() : $composerJsonBackup->restore(); + + $this->trackingService->track($process->isSuccessful() ? TrackingEvent::UpdateCompleted : TrackingEvent::UpdateFailed, $trackingId, [ + 'shopware_version_from' => $currentVersion, + 'shopware_version_to' => $version, + ]); }); } diff --git a/Listener/TrackingListener.php b/Listener/TrackingListener.php new file mode 100644 index 0000000..fcd4436 --- /dev/null +++ b/Listener/TrackingListener.php @@ -0,0 +1,55 @@ +getRequest(); + + if (!$event->isMainRequest()) { + return; + } + + $session = $request->getSession(); + + if ($session->has('trackingId')) { + return; + } + + $trackingId = bin2hex(random_bytes(16)); + $session->set('trackingId', $trackingId); + + $referer = $request->headers->get('referer', ''); + $source = 'direct'; + + if ($referer !== '') { + $path = parse_url($referer, \PHP_URL_PATH); + + if (\is_string($path) && str_contains($path, '/admin')) { + $source = 'admin'; + } + } + + $this->trackingService->track(TrackingEvent::Visit, $trackingId, [ + 'source' => $source, + 'language' => $request->getLocale(), + 'php_version' => \PHP_VERSION, + 'os' => \PHP_OS_FAMILY, + ]); + } +} diff --git a/Services/TrackingEvent.php b/Services/TrackingEvent.php new file mode 100644 index 0000000..b9377d8 --- /dev/null +++ b/Services/TrackingEvent.php @@ -0,0 +1,19 @@ +socket = @socket_create(\AF_INET, \SOCK_DGRAM, \SOL_UDP); + + $domain = Platform::getEnv('SHOPWARE_TRACKING_DOMAIN'); + $this->domain = $domain !== false ? $domain : self::DEFAULT_TRACKING_DOMAIN; + } + + /** + * @param array $tags + */ + public function track(TrackingEvent $eventName, string $userId, array $tags = []): void + { + if (Platform::getEnv('DO_NOT_TRACK') !== false) { + return; + } + + if ($this->socket === false) { + return; + } + + $payload = json_encode([ + 'event' => 'web_installer.' . $eventName->value, + 'tags' => $tags, + 'user_id' => $userId, + 'timestamp' => (new \DateTime())->format(\DateTimeInterface::ATOM), + ], \JSON_THROW_ON_ERROR); + + @socket_sendto($this->socket, $payload, \strlen($payload), 0, $this->domain, 9000); + } +} diff --git a/Tests/Controller/InstallControllerTest.php b/Tests/Controller/InstallControllerTest.php index 2b089f8..42aaa46 100644 --- a/Tests/Controller/InstallControllerTest.php +++ b/Tests/Controller/InstallControllerTest.php @@ -13,6 +13,7 @@ use Shopware\WebInstaller\Services\RecoveryManager; use Shopware\WebInstaller\Services\ReleaseInfoProvider; use Shopware\WebInstaller\Services\StreamedCommandResponseGenerator; +use Shopware\WebInstaller\Services\TrackingService; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; @@ -39,7 +40,7 @@ public function testStartPage(): void $responseGenerator = $this->createMock(StreamedCommandResponseGenerator::class); $responseGenerator->method('runJSON')->willReturn(new StreamedResponse()); - $controller = new InstallController($recovery, $responseGenerator, $this->createMock(ReleaseInfoProvider::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class)); + $controller = new InstallController($recovery, $responseGenerator, $this->createMock(ReleaseInfoProvider::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), $this->createMock(TrackingService::class)); $controller->setContainer($this->buildContainer()); $response = $controller->index(); @@ -75,7 +76,7 @@ public function testInstall(): void ]) ->willReturn(new StreamedResponse()); - $controller = new InstallController($recovery, $responseGenerator, $this->createMock(ReleaseInfoProvider::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class)); + $controller = new InstallController($recovery, $responseGenerator, $this->createMock(ReleaseInfoProvider::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), $this->createMock(TrackingService::class)); $controller->setContainer($this->buildContainer()); $request = new Request(); @@ -190,7 +191,8 @@ private function createInstallControllerAndRequestAndTemporaryDirectory( $responseGenerator, $this->createMock(ReleaseInfoProvider::class), $this->createMock(ProjectComposerJsonUpdater::class), - $this->createMock(LanguageProvider::class) + $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $installController->setContainer($this->buildContainer()); diff --git a/Tests/Controller/UpdateControllerTest.php b/Tests/Controller/UpdateControllerTest.php index f3f8552..11cf0e1 100644 --- a/Tests/Controller/UpdateControllerTest.php +++ b/Tests/Controller/UpdateControllerTest.php @@ -16,6 +16,7 @@ use Shopware\WebInstaller\Services\RecoveryManager; use Shopware\WebInstaller\Services\ReleaseInfoProvider; use Shopware\WebInstaller\Services\StreamedCommandResponseGenerator; +use Shopware\WebInstaller\Services\TrackingService; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Request; @@ -48,6 +49,7 @@ public function testRedirectWhenNotInstalled(): void $this->createMock(StreamedCommandResponseGenerator::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -74,6 +76,7 @@ public function testRedirectToFinishWhenNoUpdateThere(): void $this->createMock(StreamedCommandResponseGenerator::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -99,6 +102,7 @@ public function testUpdateThereRendersTemplate(): void $this->createMock(StreamedCommandResponseGenerator::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -145,6 +149,7 @@ public function testMigrateFlex(): void $this->createMock(StreamedCommandResponseGenerator::class), $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -182,6 +187,7 @@ public function testPrepare(): void $responseGenerator, $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -220,6 +226,7 @@ public function testFinishUpdate(): void $responseGenerator, $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -277,6 +284,7 @@ public function testUpdateChangesComposerJSON(string $shopwareVersion): void $responseGenerator, $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -334,6 +342,7 @@ public function testUpdateToRC(): void $responseGenerator, $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -407,6 +416,7 @@ public function testUpdateChangesComposerJSONInTestMode(): void $responseGenerator, $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer()); @@ -457,6 +467,7 @@ public function testResetConfig(): void $responseGenerator, $this->createMock(ProjectComposerJsonUpdater::class), $this->createMock(LanguageProvider::class), + $this->createMock(TrackingService::class), ); $controller->setContainer($this->buildContainer());