```
✨ 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 {
|
handle_path /utm_reserve.js {
|
||||||
redir https://tools-security.esy-web.dev/script.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 ---
|
# --- BLOC HEADER AVEC CSP ---
|
||||||
header {
|
header {
|
||||||
X-Content-Type-Options "nosniff"
|
X-Content-Type-Options "nosniff"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import './reserve.scss';
|
|||||||
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
import { UtmEvent, UtmAccount } from "./tools/UtmEvent.js";
|
||||||
import { CookieBanner } from "./tools/CookieBanner.js";
|
import { CookieBanner } from "./tools/CookieBanner.js";
|
||||||
import * as Turbo from "@hotwired/turbo";
|
import * as Turbo from "@hotwired/turbo";
|
||||||
|
import {onLCP, onINP, onCLS} from 'web-vitals';
|
||||||
|
|
||||||
// --- DÉTECTION BOT / PERFORMANCE ---
|
// --- DÉTECTION BOT / PERFORMANCE ---
|
||||||
const isLighthouse = () => {
|
const isLighthouse = () => {
|
||||||
@@ -53,7 +54,6 @@ const initImageLoader = () => {
|
|||||||
const images = mainContainer.querySelectorAll('img:not(.loaded)');
|
const images = mainContainer.querySelectorAll('img:not(.loaded)');
|
||||||
|
|
||||||
|
|
||||||
console.log(images);
|
|
||||||
images.forEach(img => {
|
images.forEach(img => {
|
||||||
// Sécurité : si l'image est déjà chargée (cache), on marque et on skip
|
// Sécurité : si l'image est déjà chargée (cache), on marque et on skip
|
||||||
if (img.complete) {
|
if (img.complete) {
|
||||||
@@ -195,8 +195,33 @@ const initRegisterLogic = () => {
|
|||||||
updateSiretVisibility();
|
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 ---
|
// --- INITIALISATION GLOBALE ---
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
|
onLCP(sendToAnalytics);
|
||||||
|
onINP(sendToAnalytics);
|
||||||
|
onCLS(sendToAnalytics);
|
||||||
initLoader();
|
initLoader();
|
||||||
initImageLoader();
|
initImageLoader();
|
||||||
// Enregistrement Custom Elements
|
// 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",
|
"tailwindcss": "^4.1.18",
|
||||||
"tom-select": "^2.4.3",
|
"tom-select": "^2.4.3",
|
||||||
"vite-plugin-compression": "^0.5.1",
|
"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\Customer;
|
||||||
use App\Entity\CustomerTracking;
|
use App\Entity\CustomerTracking;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
|
use App\Entity\SitePerformance;
|
||||||
use App\Form\RequestPasswordConfirmType;
|
use App\Form\RequestPasswordConfirmType;
|
||||||
use App\Form\RequestPasswordRequestType;
|
use App\Form\RequestPasswordRequestType;
|
||||||
use App\Logger\AppLogger;
|
use App\Logger\AppLogger;
|
||||||
@@ -62,6 +63,35 @@ class ReserverController extends AbstractController
|
|||||||
'products' => $products
|
'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'])]
|
#[Route('/reservation/umami', name: 'reservation_umami', methods: ['POST'])]
|
||||||
public function umami(
|
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>
|
<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 %}
|
{% endif %}
|
||||||
|
|
||||||
|
<script nonce="{{ csp_nonce('script') }}" src="/ts.js"></script>
|
||||||
{{ vite_asset('reserve.js',{}) }}
|
{{ vite_asset('reserve.js',{}) }}
|
||||||
{% block stylesheets %}{% endblock %}
|
{% block stylesheets %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Reference in New Issue
Block a user