feat(analytics): Implémente le suivi des visiteurs avec enregistrement des données.

This commit is contained in:
Serreau Jovann
2025-10-16 09:59:18 +02:00
parent f7deb334ee
commit 02fe42c629
7 changed files with 392 additions and 4 deletions

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251016075428 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE analytics_visitor_log (id SERIAL NOT NULL, visitor_id VARCHAR(36) NOT NULL, session_id VARCHAR(36) NOT NULL, ip_address VARCHAR(45) NOT NULL, user_agent TEXT NOT NULL, first_access TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, last_access TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, events_logged JSON NOT NULL, PRIMARY KEY(id))');
$this->addSql('COMMENT ON COLUMN analytics_visitor_log.first_access IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN analytics_visitor_log.last_access IS \'(DC2Type:datetime_immutable)\'');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP TABLE analytics_visitor_log');
}
}

View File

@@ -6,6 +6,8 @@ use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Form\RequestPasswordConfirmType;
use App\Form\RequestPasswordRequestType;
use App\Service\Analytics\AnalyticsVisitor;
use App\Service\Analytics\VisitorLogger;
use App\Service\Mailer\Mailer;
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
use App\Service\ResetPassword\Event\ResetPasswordEvent;
@@ -24,12 +26,16 @@ class HomeController extends AbstractController
{
#[Route(path: '/',name: 'app_login',methods: ['GET', 'POST'])]
public function index(Request $request,AuthenticationUtils $authenticationUtils): Response
public function index(AnalyticsVisitor $analyticsVisitor,Request $request,AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('artemis_dashboard');
}
$analyticsVisitor->recordPageView(
$this->generateUrl('app_login'),
'Login Page'
);
$websiteTitle = "Mainframe";
if($request->getHost() == "espace-client.siteconseil.fr") {
$websiteTitle = "SARL SITECONSEIL";
@@ -48,8 +54,12 @@ class HomeController extends AbstractController
}
#[Route(path: '/forgot-password',name: 'app_forgotpassword',methods: ['GET', 'POST'])]
public function forgotPassword(EventDispatcherInterface $eventDispatcher,Request $request): Response
public function forgotPassword(AnalyticsVisitor $analyticsVisitor,EventDispatcherInterface $eventDispatcher,Request $request): Response
{
$analyticsVisitor->recordPageView(
$this->generateUrl('app_login'),
'Forgot Password'
);
$websiteTitle = "Mainframe";
if($request->getHost() == "espace-client.siteconseil.fr") {
$websiteTitle = "SARL SITECONSEIL";
@@ -68,9 +78,12 @@ class HomeController extends AbstractController
]);
}
#[Route(path: '/forgot-password/confirm/{id}/{token}',name: 'app_forgotpassword_confirm',methods: ['GET', 'POST'])]
public function forgotPasswordConfirm(UserPasswordHasherInterface $userPasswordHasher,EventDispatcherInterface $eventDispatcher,Request $request,string $id,string $token,EntityManagerInterface $entityManager): Response
public function forgotPasswordConfirm(AnalyticsVisitor $analyticsVisitor, UserPasswordHasherInterface $userPasswordHasher,EventDispatcherInterface $eventDispatcher,Request $request,string $id,string $token,EntityManagerInterface $entityManager): Response
{
$analyticsVisitor->recordPageView(
$this->generateUrl('app_login'),
'Forgot Password Confirm'
);
$websiteTitle = "Mainframe";
if($request->getHost() == "espace-client.siteconseil.fr") {
$websiteTitle = "SARL SITECONSEIL";

110
src/Entity/VisitorLog.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
namespace App\Entity;
use App\Repository\VisitorLogRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: VisitorLogRepository::class)]
#[ORM\Table(name: 'analytics_visitor_log')]
class VisitorLog
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 36)]
private string $visitorId;
#[ORM\Column(type: 'string', length: 36)]
private string $sessionId;
#[ORM\Column(type: 'string', length: 45)]
private string $ipAddress;
#[ORM\Column(type: 'text')]
private string $userAgent;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $firstAccess;
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $lastAccess;
// Nous stockons les événements comme un JSON ou un ARRAY sérialisé
#[ORM\Column(type: 'json')]
private array $eventsLogged = [];
public function setVisitorId(string $visitorId): self { $this->visitorId = $visitorId; return $this; }
public function setSessionId(string $sessionId): self { $this->sessionId = $sessionId; return $this; }
public function setIpAddress(string $ipAddress): self { $this->ipAddress = $ipAddress; return $this; }
public function setUserAgent(string $userAgent): self { $this->userAgent = $userAgent; return $this; }
public function setFirstAccess(\DateTimeImmutable $firstAccess): self { $this->firstAccess = $firstAccess; return $this; }
public function setLastAccess(\DateTimeImmutable $lastAccess): self { $this->lastAccess = $lastAccess; return $this; }
public function setEventsLogged(array $eventsLogged): self { $this->eventsLogged = $eventsLogged; return $this; }
/**
* @return int|null
*/
public function getId(): ?int
{
return $this->id;
}
/**
* @return array
*/
public function getEventsLogged(): array
{
return $this->eventsLogged;
}
/**
* @return \DateTimeImmutable
*/
public function getFirstAccess(): \DateTimeImmutable
{
return $this->firstAccess;
}
/**
* @return string
*/
public function getIpAddress(): string
{
return $this->ipAddress;
}
/**
* @return \DateTimeImmutable
*/
public function getLastAccess(): \DateTimeImmutable
{
return $this->lastAccess;
}
/**
* @return string
*/
public function getSessionId(): string
{
return $this->sessionId;
}
/**
* @return string
*/
public function getUserAgent(): string
{
return $this->userAgent;
}
/**
* @return string
*/
public function getVisitorId(): string
{
return $this->visitorId;
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\VisitorLog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<VisitorLog>
*/
class VisitorLogRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, VisitorLog::class);
}
// /**
// * @return VisitorLog[] Returns an array of VisitorLog objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('v')
// ->andWhere('v.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('v.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?VisitorLog
// {
// return $this->createQueryBuilder('v')
// ->andWhere('v.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -0,0 +1,114 @@
<?php
namespace App\Service\Analytics;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Uid\Uuid;
use DateTimeImmutable;
use DateInterval;
class AnalyticsVisitor
{
// --- Core Identification ---
public string $visitorId;
public string $sessionId;
public string $ipAddress;
public string $userAgent;
// --- Time and Activity Tracking ---
private DateTimeImmutable $firstAccess;
private DateTimeImmutable $lastAccess;
private int $pageViews = 0;
private array $eventsLogged = [];
/**
* The RequestStack is injected by Symfony to get the current Request object.
*/
public function __construct(RequestStack $requestStack)
{
$request = $requestStack->getCurrentRequest();
// Use the Request object for reliable data collection
$this->ipAddress = $request ? $this->getClientIp($request) : '0.0.0.0';
$this->userAgent = $request ? $request->headers->get('User-Agent', 'Unknown/Bot') : 'Unknown/Bot';
// Session and Visitor IDs would typically come from a cookie or session.
// For simplicity here, we generate a UUID.
$this->visitorId = Uuid::v4()->toRfc4122();
$this->sessionId = Uuid::v4()->toRfc4122();
$this->firstAccess = new DateTimeImmutable();
$this->lastAccess = $this->firstAccess;
}
/**
* A more robust way to get the client IP using the Symfony Request object.
* * @param \Symfony\Component\HttpFoundation\Request $request
*/
private function getClientIp(\Symfony\Component\HttpFoundation\Request $request): string
{
// getClientIp() handles X-Forwarded-For and other headers securely
// if your Symfony trusted proxies are configured correctly.
return $request->getClientIp() ?? '0.0.0.0';
}
// ----------------------------------------------------------------------
// --- Public Methods for Tracking ---
// ----------------------------------------------------------------------
/**
* Records a new page view for the visitor.
*/
public function recordPageView(string $url, string $title): void
{
$this->pageViews++;
$this->lastAccess = new DateTimeImmutable();
$this->eventsLogged[] = [
'type' => 'page_view',
'timestamp' => $this->lastAccess->format(\DateTimeInterface::ISO8601),
'url' => $url,
'title' => $title,
];
}
/**
* Records a custom event.
*/
public function recordEvent(string $eventName, array $details = []): void
{
$this->lastAccess = new DateTimeImmutable();
$this->eventsLogged[] = [
'type' => 'custom_event',
'timestamp' => $this->lastAccess->format(\DateTimeInterface::ISO8601),
'event_name' => $eventName,
'details' => $details,
];
}
// ----------------------------------------------------------------------
// --- Data Export (for Storage) ---
// ----------------------------------------------------------------------
/**
* Returns an array representation of the visitor object for storage.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
// Calculate duration in seconds
$duration = $this->lastAccess->getTimestamp() - $this->firstAccess->getTimestamp();
return [
'visitor_id' => $this->visitorId,
'session_id' => $this->sessionId,
'ip_address' => $this->ipAddress,
'user_agent' => $this->userAgent,
'first_access' => $this->firstAccess->format(\DateTimeInterface::ISO8601),
'last_access' => $this->lastAccess->format(\DateTimeInterface::ISO8601),
'session_duration_seconds' => $duration,
'page_views' => $this->pageViews,
'events_logged' => $this->eventsLogged,
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Service\Analytics;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\TerminateEvent;
use Symfony\Component\HttpKernel\KernelEvents;
#[AsEventListener(event: KernelEvents::TERMINATE,method: 'onKernelTerminate')]
class VisitorLogSubscriber
{
public function __construct(private VisitorLogger $visitorLogger)
{}
public function onKernelTerminate(TerminateEvent $event): void
{
// Ne pas enregistrer les requêtes qui ont échoué
if (!$event->isMainRequest() || $event->getResponse()->getStatusCode() >= 500) {
return;
}
// Appeler le service de sauvegarde
$this->visitorLogger->logCurrentVisitor();
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Service\Analytics;
use App\Entity\VisitorLog;
use Doctrine\ORM\EntityManagerInterface;
class VisitorLogger
{
private EntityManagerInterface $entityManager;
private AnalyticsVisitor $visitor;
/**
* L'EntityManager de Doctrine et l'objet AnalyticsVisitor sont injectés par Symfony.
*/
public function __construct(EntityManagerInterface $entityManager, AnalyticsVisitor $visitor)
{
$this->entityManager = $entityManager;
// On récupère l'instance du visiteur qui a collecté toutes les actions
$this->visitor = $visitor;
}
/**
* Enregistre l'ensemble des données de l'objet AnalyticsVisitor dans la BDD.
*/
public function logCurrentVisitor(): void
{
// 1. Récupérer les données structurées
$data = $this->visitor->toArray();
// 2. Créer l'entité et mapper les données
$log = new VisitorLog();
$log->setVisitorId($data['visitor_id']);
$log->setSessionId($data['session_id']);
$log->setIpAddress($data['ip_address']);
$log->setUserAgent($data['user_agent']);
// Convertir les chaînes de date ISO8601 en objets DateTimeImmutable
$log->setFirstAccess(new \DateTimeImmutable($data['first_access']));
$log->setLastAccess(new \DateTimeImmutable($data['last_access']));
// Sauvegarder l'historique des événements
$log->setEventsLogged($data['events_logged']);
// 3. Persister et flusher
$this->entityManager->persist($log);
$this->entityManager->flush();
}
}