Aller au contenu
← Retour au blog
Guides 1 août 2026· 9 min de lecture

Ajouter des analytics à une app Laravel sans bandeau cookies

Analytics sans cookie dans Laravel 10/11/12 : directive Blade, conversions mesurées côté serveur, et le piège d'attribution des jobs en file.

Sommaire

Le réflexe est bien rôdé. On veut mesurer l’audience d’une application Laravel, on cherche comment faire, et on tombe invariablement sur des tutoriels de bandeau de consentement : installer un paquet de gestion du consentement, coller la balise Google Analytics dans le layout Blade, écrire le middleware qui ne rend la balise qu’après acceptation, stocker le choix dans un cookie. Dans tous ces guides, le bandeau est le point de départ du travail. C’est pourtant un résultat, et le résultat d’un choix précis : celui d’un outil qui dépose un cookie et envoie les données hors de l’Union européenne. Une mesure qui ne collecte rien de personnel n’a rien à faire accepter, donc rien à demander.

Ce guide déroule l’intégration complète côté Laravel 10 / 11 / 12 : le paquet, la directive Blade, les conversions envoyées depuis PHP, et un piège d’attribution qui, dès qu’un job en file ou un webhook entre dans le tunnel de paiement, fait basculer tout votre chiffre d’affaires sur un seul et même « visiteur » — votre propre serveur.

Pourquoi le bandeau n’est pas une fatalité

Ce qui déclenche l’obligation de consentement, ce n’est pas le mot « analytics ». C’est l’écriture ou la lecture d’informations dans le terminal du visiteur : un cookie, une entrée en stockage local, une empreinte de navigateur reconstituée. Une mesure qui n’écrit rien dans le navigateur et ne cherche à reconnaître personne d’une visite à l’autre sort de ce périmètre. Le second point de friction, le transfert de données personnelles vers des serveurs situés hors de l’Union, disparaît de la même façon dès lors que la collecte et le stockage restent en Europe.

Ce qu’il reste est exactement ce dont on a besoin pour piloter un produit : des agrégats. Nombre de visites, pages consultées, sources de trafic, répartition par pays et par appareil. Aucun profil individuel, aucun identifiant persistant, personne à reconnaître. C’est le parti pris de conception de Takt : pas de cookie, pas d’identification, un service géré et hébergé en Europe. Le contraste complet avec l’approche Google Analytics est détaillé sur notre page comparative.

Côté Laravel, la conséquence pratique est agréablement banale : rien à ajouter à config/session.php, aucun cookie supplémentaire à faire figurer dans votre politique de confidentialité, aucun middleware conditionnel à écrire ni à tester. Et le gain n’est pas que juridique : sans bandeau, plus personne ne refuse ni n’ignore la fenêtre, donc vos chiffres portent sur l’ensemble du trafic et non sur la seule fraction de visiteurs ayant cliqué sur « Accepter ».

Installer les analytics dans Laravel

Avant la ligne de commande, une étape zéro qu’on saute volontiers et qu’on paie ensuite : créez le site dans Takt et notez le domaine que vous y déclarez. C’est cette valeur exacte qui devra figurer dans TAKT_DOMAIN, car c’est le domaine déclaré — et non l’hôte réellement servi — qui rattache les événements à un site. Une préproduction hébergée sur staging.exemple.fr mais déclarant exemple.fr ira donc alimenter les statistiques de la production.

Le bridge Laravel câble ensuite le cœur PHP dans le conteneur de services :

composer require vskstudio/takt-laravel
php artisan vendor:publish --tag=takt-config

La seconde commande dépose config/takt.php, qui lit vos variables d’environnement. Le TaktServiceProvider est auto-découvert : vous n’avez aucun provider à déclarer à la main. Il lie deux objets dans le conteneur d’après cette configuration — le SnippetRenderer, qui produit la balise du runtime navigateur, et le client Takt, qui poste les événements serveur-à-serveur.

# .env
TAKT_DOMAIN=exemple.fr
TAKT_OUTBOUND=true
TAKT_FILES=true

TAKT_OUTBOUND et TAKT_FILES activent deux captures automatiques : les clics vers les liens sortants et les téléchargements. Toutes les autocaptures sont désactivées par défaut ; TAKT_TAGGED (les éléments marqués data-takt-event dans le HTML) et TAKT_NOT_FOUND (les pages 404) complètent la série des interrupteurs. TAKT_FILE_EXTENSIONS n’en est pas un : c’est une restriction posée sur une capture déjà active, la liste des extensions que TAKT_FILES accepte de compter comme téléchargement.

Vous remarquerez deux absences dans ce fichier. Il n’y a pas de clé d’API : elle ne sert qu’aux envois serveur-à-serveur, et on l’ajoutera au moment des conversions — une application qui ne mesure que des pages vues n’en a pas besoin. Et il n’y a pas d’adresse d’ingestion : TAKT_ENDPOINT vise le service Takt hébergé — jamais un serveur à vous — et vous n’avez de raison de le redéfinir que si vous servez la mesure derrière un proxy first-party sur votre propre domaine. Si votre version du paquet réclame malgré tout cette variable, la référence Laravel fait foi sur la valeur attendue.

La directive Blade

Le paquet ajoute une directive @takt. Elle se rend dans le <head> du layout, une seule fois pour toute l’application :

{{-- resources/views/layouts/app.blade.php --}}
<head>
  <meta charset="utf-8">
  @takt
</head>

C’est tout pour la mesure des pages vues. Par défaut, le runtime est embarqué directement dans la balise plutôt que chargé depuis une URL : cela évite une requête réseau supplémentaire au chargement de chaque page. La directive accepte par ailleurs des surcharges inline, mais préférez piloter les captures depuis la configuration : un layout unique et une configuration centralisée valent mieux qu’une option dispersée dans les gabarits.

Livewire et Inertia : rien à câbler

Le runtime enveloppe history.pushState et écoute popstate. Or wire:navigate de Livewire et le routeur d’Inertia passent l’un comme l’autre par pushState, et le bouton « retour » du navigateur déclenche un popstate : une application Laravel bâtie sur l’un ou l’autre est donc correctement mesurée dès l’installation, sans configuration, sans écouteur à enregistrer et sans appel à déclencher à chaque changement d’écran. Une seule précaution, et elle vaut pour les deux : la directive doit rester dans le <head> du layout et nulle part ailleurs — l’article sur l’intégration Symfony détaille pourquoi un snippet rendu dans le corps de page finit par multiplier les pages vues.

Suivre les conversions Laravel côté serveur

La mesure côté navigateur suffit pour les pages vues. Elle devient nettement moins fiable dès qu’il s’agit d’argent. Un bloqueur de publicité, un onglet fermé pendant la redirection de retour du prestataire de paiement, un tunnel qui se termine sur un domaine tiers : autant de commandes bien réelles qui n’apparaîtront jamais dans vos statistiques. C’est la raison pour laquelle le chiffre d’affaires affiché par un outil qui s’arrête au navigateur ne recoupe jamais tout à fait le back-office. Un outil capable de recevoir des événements depuis votre serveur mesure la conversion au moment où la commande change d’état, pas au moment où un navigateur veut bien exécuter un script. C’est aussi ce qui règle le cas d’un front découplé : les pages vues continuent d’être mesurées dans le navigateur, comme le détaille l’article sur l’intégration SvelteKit, pendant que les conversions partent de l’API Laravel.

Les envois depuis PHP s’authentifient avec une clé d’API. Elle n’était pas nécessaire jusqu’ici ; ajoutez-la maintenant, à la suite des lignes déjà présentes dans votre .env :

# .env — ligne à ajouter aux précédentes
TAKT_API_KEY=votre_cle_ingest   # requis seulement pour le S2S

Ne sautez pas cette ligne. C’est cette clé qui authentifie les envois serveur-à-serveur, et le client ne vous remontera pas un rejet : en mode par défaut, seul un 202 vaut succès et tout le reste est avalé plutôt que propagé. Un envoi refusé faute de clé valide ne lèvera donc rien dans votre code appelant — il se verra uniquement à des conversions qui n’arrivent jamais dans le tableau de bord.

La façade fait le reste, depuis un contrôleur, un service, un observateur de modèle — l’endroit de votre tunnel où la commande est confirmée :

use Vskstudio\Takt\Laravel\Facades\Takt;
use Vskstudio\Takt\Revenue;

Takt::event('Signup', ['plan' => 'pro']);

Takt::event('Purchase', ['plan' => 'pro'], new Revenue(amount: '29', currency: 'EUR'));

Le montant du Revenue est une chaîne décimale, pas un flottant : c’est délibéré, cela tient les arrondis binaires à distance des sommes d’argent. Les envois sont en fire-and-forget : un 202 vaut succès, et une panne de transport est avalée plutôt que propagée, parce que l’analytics ne doit jamais faire tomber un tunnel de paiement.

Un détail mérite d’être retenu, parce que toute la section suivante repose dessus : la façade résout le client Takt du conteneur, et ce client forwarde l’IP et l’User-Agent du visiteur depuis l’Illuminate\Http\Request courante. C’est ce forward, et lui seul, qui rattache la conversion au bon visiteur — donc à sa source de trafic, à son pays, à sa campagne.

Le piège : des conversions attribuées à votre propre serveur

Relisez la fin de la section précédente : la façade forwarde l’IP et l’User-Agent « de la requête courante ». Elle suppose donc qu’il y ait une requête, et que ce soit celle du visiteur. Trois endroits parfaitement ordinaires d’une application Laravel manquent à l’une de ces conditions, ou aux deux.

Un job en file s’exécute dans un worker, un processus qui n’a jamais vu la requête HTTP à l’origine de la commande. Une commande Artisan — la relance d’abonnements du planificateur, le script de rattrapage lancé à la main après un incident — tourne hors de tout contexte web. Le chemin qui y mène est d’ailleurs des plus discrets : la ligne Takt::event(...) que quelqu’un déplace un jour du contrôleur vers un job, parce que l’envoi ralentissait la réponse. Le code n’a pas changé d’une virgule ; sa signification, si.

La conséquence est documentée : sans withVisitor(), un événement serveur-à-serveur est attribué au serveur applicatif, pas au visiteur réel. L’attribution est dérivée côté serveur de l’IP et de l’User-Agent ; à défaut de ceux du visiteur, ce sont ceux de la machine qui émet. Toutes vos conversions s’effondrent alors sur un unique visiteur, assis dans le centre de données de votre hébergeur, sans source de trafic et sans pays exploitable.

Le troisième endroit, le webhook de paiement, est le plus vicieux, parce qu’il ne ressemble pas aux deux autres. Il y a bien une requête HTTP en cours, la façade a donc bien quelque chose à forwarder, et rien n’a l’air anormal — sauf que cette requête est celle de Stripe, pas celle de votre client. $request->ip() renvoie une IP de Stripe, $request->userAgent() la chaîne de son client HTTP. Vous n’obtenez pas une attribution manquante, qui se verrait : vous obtenez une attribution fausse et parfaitement bien formée.

Et rien ne vous préviendra. L’envoi, ici, ne rate même pas : il réussit, la réponse est un 202, il est simplement attribué à la mauvaise personne. Aucune exception n’est levée, aucun test ne casse. Le seul symptôme est dans le tableau de bord, souvent des semaines plus tard, quand quelqu’un remarque que le profil de vos acheteurs ne ressemble plus du tout à celui de vos visiteurs.

La correction consiste à capturer ce qui identifie le visiteur pendant sa requête, puis à le transporter jusqu’au point d’envoi. Au moment de dispatcher le job, on a encore la requête sous la main :

// Pendant la requête du visiteur : on capture ce qui l'identifie.
ConfirmOrder::dispatch($order, $request->ip(), $request->userAgent());

Encore faut-il que $request->ip() renvoie bien l’adresse du visiteur. Derrière nginx, un répartiteur de charge ou Cloudflare, elle renvoie celle du proxy tant que l’application n’a pas reçu la liste des proxys auxquels faire confiance — sur Laravel 11 et 12 via ->withMiddleware(...) dans bootstrap/app.php, sur les versions antérieures via le middleware TrustProxies. La valeur à y mettre dépend de votre infrastructure et ne se devine pas ; la documentation Laravel en donne les formes. Sans cela, le correctif ci-dessous s’appliquera sans rien corriger : vos conversions continueront de s’écraser sur un visiteur unique, votre edge cette fois.

Le job lui-même part du squelette produit par php artisan make:job ConfirmOrder, dans app/Jobs/ConfirmOrder.php ; il n’y reste qu’à ajouter le constructeur et l’envoi. On y récupère le client Takt par la signature de handle() plutôt que par la façade — mais c’est un choix de style, pas un changement de comportement : les deux résolvent le même service du conteneur, et Takt::withVisitor($ip, $ua)->event(...) ferait exactement la même chose. Ce qui corrige le bug, dans les deux cas, c’est withVisitor(), et rien d’autre.

<?php

namespace App\Jobs;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Vskstudio\Takt\Revenue;
use Vskstudio\Takt\Takt;

class ConfirmOrder implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        private Order $order,
        private ?string $ip,
        private ?string $userAgent,
    ) {}

    public function handle(Takt $takt): void
    {
        $takt->withVisitor($this->ip, $this->userAgent)
            ->event('Purchase', ['plan' => $this->order->plan],
                new Revenue(amount: (string) $this->order->total, currency: 'EUR'));
    }
}

Trois choses méritent un mot dans ce bloc. Le jeu de traits est celui du stub Laravel 10 ; sur Laravel 11 et 12, make:job les regroupe sous l’unique Illuminate\Foundation\Queue\Queueable, et la forme explicite ci-dessus reste valide sur les trois versions. SerializesModels n’est pas décoratif : sans lui, l’Order entier part dans la charge utile du job au lieu d’être sérialisé par sa clé et rechargé à l’exécution. Et (string) $this->order->total suppose que la colonne soit castée en decimal:2 sur le modèle : le Revenue attend une chaîne décimale, alors que caster un flottant PHP produirait 29.9 là où vous vouliez 29.90.

Un dernier point de vigilance : withVisitor() renvoie une nouvelle instance. L’envoi doit être enchaîné dessus, comme ci-dessus. Écrire l’appel sur deux instructions séparées — $takt->withVisitor(...) d’un côté, $takt->event(...) de l’autre — rend le premier appel parfaitement inutile, et parfaitement silencieux : on retombe exactement sur le bug qu’on cherchait à corriger, avec le code qui semble le corriger juste au-dessus.

Pour le webhook, la même logique s’applique un cran plus tôt. Quand Stripe rappelle votre application, la requête du visiteur est terminée depuis longtemps : il n’y a plus rien à capturer au moment de l’envoi. L’IP et l’User-Agent doivent donc être persistés sur la commande au passage en caisse, dans deux colonnes ordinaires, puis relus par le contrôleur de webhook pour alimenter withVisitor(). C’est un peu de plomberie, mais c’est le seul moyen d’avoir à la fois l’exhaustivité du webhook et l’attribution du contrôleur de retour. Notez que vous stockez alors une donnée que votre application ne conservait peut-être pas : traitez-la comme telle, et purgez-la avec le reste de la commande.

Vérifier que la mesure fonctionne

Déployez, puis ouvrez le site en ligne avec l’onglet Réseau des outils de développement. Vous devez voir une requête POST vers /api/event à chaque chargement de page, et exactement une par navigation wire:navigate ou Inertia — cliquez sur quatre ou cinq liens d’affilée et comptez.

En local, vous ne verrez rien remonter : localhost et les IP privées sont exclus par défaut, au même titre que les visiteurs ayant activé Do Not Track ou Global Privacy Control. C’est le comportement voulu, pas une configuration ratée. Pour un test manuel depuis un environnement public, la console suffit :

// depuis la console du navigateur, sur le site déployé
window.takt('test_event');

Le piège d’attribution, lui, ne se teste pas dans le navigateur, puisque l’envoi n’en part pas. Il se lit dans la répartition par pays de votre trafic, celle-là même, sans filtre. Le raisonnement tient en deux temps. Sans withVisitor(), tous vos envois serveur-à-serveur portent la même IP et le même User-Agent, ceux de votre serveur applicatif : ils se replient donc sur un seul et même visiteur, localisé au pays de votre hébergeur. Ce visiteur fantôme apparaît alors dans la répartition globale sous la forme d’un pays qui n’a rien à faire dans votre audience, et qui y pèse à peu près votre nombre de commandes sur la période. Une boutique aux visiteurs français chez qui l’Allemagne surgit à hauteur de son volume de ventes ne s’est pas mise à vendre en Allemagne : elle mesure le centre de données de son hébergeur. Les répartitions par appareil et par navigateur portent la même signature, celle d’un client HTTP et pas d’un public. Une fois le correctif en place, ce pic disparaît et les conversions se répartissent sur les vrais visiteurs.

Terminez par une vraie commande de bout en bout, et vérifiez que le Purchase correspondant remonte une seule fois. Si vous émettez à la fois depuis le contrôleur de retour et depuis le webhook, vous comptez chaque vente deux fois, et le chiffre d’affaires de votre tableau de bord vaudra le double de celui de votre back-office.

En résumé

Un paquet, un domaine à déclarer, une directive dans le <head> du layout : une application Laravel 10, 11 ou 12 est mesurée sans cookie ni bandeau, Livewire et Inertia compris, et la façade Takt envoie les conversions depuis PHP, hors de portée des bloqueurs. Le seul point réellement piégeux est l’attribution : la façade ne peut forwarder l’IP et l’User-Agent que s’il existe une requête visiteur en cours, ce qui n’est le cas ni dans un job en file, ni dans une commande Artisan, ni dans un webhook où la requête appartient au prestataire de paiement — et l’échec est silencieux, puisqu’un 202 est renvoyé quoi qu’il arrive. Capturez l’IP et l’User-Agent pendant la requête du visiteur, transportez-les jusqu’au point d’envoi, et enchaînez l’envoi sur l’instance renvoyée par withVisitor(). La référence complète des options est dans la documentation Laravel.

Passer à l'action

Mesurez votre audience sans bannière.

Voyez Takt en action, puis installez une analytics sans cookies sur votre site.

Partager