```
✨ feat(SitePerformance): Ajoute la collecte des métriques web vitales. 🐛 fix(caddy): Corrige la redirection du script Trustpilot. 📦 chore: Ajoute web-vitals comme dépendance et adapte package.json. ```
This commit is contained in:
@@ -20,7 +20,9 @@ intranet.ludikevent.fr, signature.ludikevent.fr, reservation.ludikevent.fr {
|
||||
handle_path /utm_reserve.js {
|
||||
redir https://tools-security.esy-web.dev/script.js
|
||||
}
|
||||
|
||||
handle_path /ts.js {
|
||||
redir https://widget.trustpilot.com/bootstrap/v5/tp.widget.bootstrap.min.js
|
||||
}
|
||||
# --- BLOC HEADER AVEC CSP ---
|
||||
header {
|
||||
X-Content-Type-Options "nosniff"
|
||||
|
||||
@@ -2,6 +2,7 @@ import './reserve.scss';
|
||||
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
||||
import { CookieBanner } from "./tools/CookieBanner.js";
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
import {onLCP, onINP, onCLS} from 'web-vitals';
|
||||
|
||||
// --- DÉTECTION BOT / PERFORMANCE ---
|
||||
const isLighthouse = () => {
|
||||
@@ -53,7 +54,6 @@ const initImageLoader = () => {
|
||||
const images = mainContainer.querySelectorAll('img:not(.loaded)');
|
||||
|
||||
|
||||
console.log(images);
|
||||
images.forEach(img => {
|
||||
// Sécurité : si l'image est déjà chargée (cache), on marque et on skip
|
||||
if (img.complete) {
|
||||
@@ -195,8 +195,33 @@ const initRegisterLogic = () => {
|
||||
updateSiretVisibility();
|
||||
};
|
||||
|
||||
const sendToAnalytics = ({ name, delta, id }) => {
|
||||
// On ne veut pas polluer les stats avec les tests Lighthouse
|
||||
if (isLighthouse()) return;
|
||||
|
||||
const body = JSON.stringify({
|
||||
name, // 'LCP', 'INP', ou 'CLS'
|
||||
value: delta, // La valeur de la mesure
|
||||
id, // ID unique de la session de page (pour éviter les doublons)
|
||||
path: window.location.pathname // Pour savoir quelle page est lente
|
||||
});
|
||||
|
||||
const url = '/reservation/web-vitals';
|
||||
|
||||
// sendBeacon est idéal pour les stats car il ne bloque pas le thread principal
|
||||
if (navigator.sendBeacon) {
|
||||
navigator.sendBeacon(url, body);
|
||||
} else {
|
||||
fetch(url, { body, method: 'POST', keepalive: true });
|
||||
}
|
||||
};
|
||||
|
||||
// --- INITIALISATION GLOBALE ---
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
onLCP(sendToAnalytics);
|
||||
onINP(sendToAnalytics);
|
||||
onCLS(sendToAnalytics);
|
||||
initLoader();
|
||||
initImageLoader();
|
||||
// Enregistrement Custom Elements
|
||||
|
||||
33
migrations/Version20260127223021.php
Normal file
33
migrations/Version20260127223021.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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 Version20260127223021 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 site_performance (id SERIAL NOT NULL, name VARCHAR(255) NOT NULL, value DOUBLE PRECISION NOT NULL, path VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
|
||||
$this->addSql('COMMENT ON COLUMN site_performance.created_at 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 site_performance');
|
||||
}
|
||||
}
|
||||
32
migrations/Version20260127223100.php
Normal file
32
migrations/Version20260127223100.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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 Version20260127223100 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('ALTER TABLE site_performance ADD metric_id VARCHAR(50) DEFAULT NULL');
|
||||
}
|
||||
|
||||
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('ALTER TABLE site_performance DROP metric_id');
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tom-select": "^2.4.3",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-favicon": "^1.0.8"
|
||||
"vite-plugin-favicon": "^1.0.8",
|
||||
"web-vitals": "^5.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerTracking;
|
||||
use App\Entity\Product;
|
||||
use App\Entity\SitePerformance;
|
||||
use App\Form\RequestPasswordConfirmType;
|
||||
use App\Form\RequestPasswordRequestType;
|
||||
use App\Logger\AppLogger;
|
||||
@@ -62,6 +63,35 @@ class ReserverController extends AbstractController
|
||||
'products' => $products
|
||||
]);
|
||||
}
|
||||
#[Route('/reservation/web-vitals', name: 'reservation_web-vitals', methods: ['POST'])]
|
||||
public function webVitals(Request $request, EntityManagerInterface $em): Response
|
||||
{
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
if (!$data || !isset($data['name'], $data['value'])) {
|
||||
return new Response('Invalid data', Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// On vérifie si cet ID de métrique existe déjà pour éviter les doublons
|
||||
// (web-vitals peut renvoyer plusieurs fois la même métrique si elle s'affine)
|
||||
$existing = $em->getRepository(SitePerformance::class)->findOneBy(['metricId' => $data['id']]);
|
||||
|
||||
$perf = $existing ?? new SitePerformance();
|
||||
|
||||
$perf->setName($data['name']);
|
||||
$perf->setValue((float)$data['value']);
|
||||
$perf->setPath($data['path'] ?? '/');
|
||||
$perf->setMetricId($data['id'] ?? null);
|
||||
$perf->setUpdatedAt(new \DateTime());
|
||||
|
||||
if (!$existing) {
|
||||
$em->persist($perf);
|
||||
}
|
||||
|
||||
$em->flush();
|
||||
|
||||
return new Response('', Response::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
#[Route('/reservation/umami', name: 'reservation_umami', methods: ['POST'])]
|
||||
public function umami(
|
||||
|
||||
95
src/Entity/SitePerformance.php
Normal file
95
src/Entity/SitePerformance.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\SitePerformanceRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: SitePerformanceRepository::class)]
|
||||
class SitePerformance
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?float $value = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $path = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $metricId = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue(): ?float
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function setValue(float $value): static
|
||||
{
|
||||
$this->value = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPath(): ?string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function setPath(string $path): static
|
||||
{
|
||||
$this->path = $path;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeImmutable $createdAt): static
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMetricId(): ?string
|
||||
{
|
||||
return $this->metricId;
|
||||
}
|
||||
|
||||
public function setMetricId(?string $metricId): static
|
||||
{
|
||||
$this->metricId = $metricId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
43
src/Repository/SitePerformanceRepository.php
Normal file
43
src/Repository/SitePerformanceRepository.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\SitePerformance;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<SitePerformance>
|
||||
*/
|
||||
class SitePerformanceRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, SitePerformance::class);
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return SitePerformance[] Returns an array of SitePerformance objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('s')
|
||||
// ->andWhere('s.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('s.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?SitePerformance
|
||||
// {
|
||||
// return $this->createQueryBuilder('s')
|
||||
// ->andWhere('s.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
@@ -75,6 +75,7 @@
|
||||
<script data-host-url="https://tools-security.esy-web.dev" nonce="{{ csp_nonce('script') }}" defer src="/utm_reserve.js" data-website-id="bc640e0d-43fb-4c3a-bb17-1ac01cec9643"></script>
|
||||
{% endif %}
|
||||
|
||||
<script nonce="{{ csp_nonce('script') }}" src="/ts.js"></script>
|
||||
{{ vite_asset('reserve.js',{}) }}
|
||||
{% block stylesheets %}{% endblock %}
|
||||
</head>
|
||||
|
||||
Reference in New Issue
Block a user