takt

Laravel

Le bridge Laravel vskstudio/takt-laravel câble le cœur PHP dans le conteneur : une directive Blade pour le snippet, une façade pour le S2S et une config publiable. 0.5.x — PHP 8.1+, Laravel 10 / 11 / 12.

composer require vskstudio/takt-laravel

Le TaktServiceProvider est auto-découvert : SnippetRenderer (rendu du snippet) et Takt (client S2S) sont liés dans le conteneur d’après la config, et l’IP / l’User-Agent sont forwardés au client S2S lorsqu’une requête HTTP est disponible.

Cette page est la référence des options. Pour l’intégration de bout en bout et son piège principal — l’attribution des envois S2S émis hors requête visiteur, dans un job en file, une commande Artisan ou un webhook — voir le guide analytics sans cookie dans une application Laravel.

Configuration

php artisan vendor:publish --tag=takt-config

takt-config est le seul tag publiable du paquet. config/takt.php lit les variables d’environnement :

TAKT_DOMAIN=exemple.fr
TAKT_API_KEY=tk_…               # requis seulement pour le S2S
TAKT_MODE=inline                # inline (défaut) | cdn | asset | sdk
TAKT_ENDPOINT=https://taktlytics.com/api/event   # voir « Endpoint et origine first-party »
TAKT_SCRIPT_ORIGIN=https://analytics.exemple.fr  # origine first-party servant le tracker
TAKT_EXCLUDE_LOCALHOST=false    # défaut : true — rien n'est mesuré depuis localhost
TAKT_NONCE=# nonce CSP de la balise script
TAKT_OUTBOUND=true
TAKT_FILES=true
TAKT_TAGGED=true
TAKT_NOT_FOUND=true
TAKT_FILE_EXTENSIONS=pdf,zip,docx

# Options avancées — laisser vide garde le défaut du tracker
TAKT_SAMPLE_RATE=0.5            # n'envoie qu'une fraction (0–1) des hits
TAKT_TRACK_QUERY=true           # conserve la query string + le hash (défaut : strippés)
TAKT_QUERY_PARAMS=utm_source,utm_medium  # allowlist quand track_query est off
TAKT_EXCLUDE=/app,/account      # préfixes de chemin jamais suivis — requiert TAKT_MODE=sdk
TAKT_RESPECT_DNT=false          # cesse de respecter Do-Not-Track
TAKT_ENABLED=false              # coupe-circuit : snippet no-op

TAKT_EXCLUDE_LOCALHOST vaut true par défaut : en développement local, rien n’est mesuré tant que tu ne l’as pas passé à false.

Modes de rendu

TAKT_MODE choisit la source du runtime navigateur :

TAKT_MODERendu
inline (défaut)Le bundle takt.auto.js vendoré est embarqué dans une balise <script> inline : aucune requête en plus, mais du JS inline dans chaque page — sous CSP stricte, renseigne TAKT_NONCE
cdn<script defer src="https://cdn.jsdelivr.net/npm/@vskstudio/[email protected]/dist/takt.auto.js">
asset<script defer src="/takt/takt.auto.js"> — copie auto-hébergée, préfixée de TAKT_SCRIPT_ORIGIN si l’origine est définie
sdk<script type="module">import{init}…;init({…})</script> — SDK complet, chargé depuis jsDelivr ou depuis /takt/takt.esm.js sur TAKT_SCRIPT_ORIGIN

TAKT_OUTBOUND, TAKT_FILES, TAKT_TAGGED et TAKT_NOT_FOUND ajoutent chacun un token (outbound, downloads, tagged, 404) à l’unique attribut data-auto que lit le bundle, en modes inline, cdn et asset ; en mode sdk ce sont des clés booléennes (outbound, files, tagged, notFound) de l’objet passé à init().

Le paquet ne publie que sa configuration : aucune commande ne dépose le bundle dans public/. Pour TAKT_MODE=asset, copie vendor/vskstudio/takt-core-php/resources/takt.auto.js vers public/takt/takt.auto.js — par exemple depuis un script post-update-cmd de ton composer.json, pour que la copie suive les mises à jour du paquet.

Options réservées au mode sdk

TAKT_SCRUB_URL — une fonction JS brute de réécriture d’URL, injectée verbatim dans la page, à réserver au dev : ne la construis jamais à partir d’entrées utilisateur — et TAKT_EXCLUDE n’existent que dans le SDK complet. Posées dans un autre mode, elles ne sont pas ignorées : la construction du SnippetRenderer lève une InvalidArgumentException, donc une 500 sur toutes les pages qui rendent le snippet.

TAKT_MODE=sdk
TAKT_SCRUB_URL="(u) => u.split('#')[0]"
TAKT_EXCLUDE=/app,/account

Endpoint et origine first-party

TAKT_ENDPOINT alimente deux consommateurs qui ne lisent pas la même chose dans la valeur :

  • le snippet rendu par @takt la reçoit telle quelle (attribut data-endpoint, ou clé endpoint de l’appel init() en mode sdk) et POSTe sur cette URL exacte ;
  • la façade Takt la traite comme une origine de base et lui ajoute /api/event à chaque envoi.
TAKT_ENDPOINTLe snippet POSTe surLa façade POSTe sur
https://taktlytics.com (défaut du paquet)https://taktlytics.com — hors point d’ingestionhttps://taktlytics.com/api/event
https://taktlytics.com/api/eventhttps://taktlytics.com/api/eventhttps://taktlytics.com/api/event/api/event — hors point d’ingestion

Aucune valeur unique ne convient aux deux usages. Choisis selon ce que tu utilises :

  • snippet seul (pas de clé d’API) : TAKT_ENDPOINT=https://taktlytics.com/api/event ;
  • S2S seul : garde le défaut https://taktlytics.com ;
  • les deux : garde l’URL complète pour le snippet et rebinde le client S2S sur l’origine, dans le register() de ton AppServiceProvider :
use Vskstudio\Takt\Takt;

$this->app->scoped(Takt::class, function ($app) {
    $takt = new Takt('https://taktlytics.com', config('takt.domain'), config('takt.api_key'));
    $request = $app['request'] ?? null;

    return $request === null ? $takt : $takt->withVisitor($request->ip(), $request->userAgent());
});

TAKT_SCRIPT_ORIGIN est l’origine first-party que tu proxifies vers Takt pour esquiver les bloqueurs. Elle est rendue en data-script-origin et sert le fichier du tracker en modes asset et sdk. Le tracker n’en dérive le point d’ingestion — l’origine suivie de /api/event — que si aucun endpoint n’est rendu, ce qui suppose TAKT_ENDPOINT=https://taktlytics.com/api/event. Avec toute autre valeur, data-endpoint est rendu et prime sur l’origine, qui ne sert alors qu’à charger le fichier.

Directive Blade @takt

Place @takt dans le <head> de ton layout pour rendre le snippet :

<head>
  <meta charset="utf-8">
  @takt
</head>

La directive ne prend pas d’argument : elle compile un appel fixe à SnippetRenderer::render(), et toute expression écrite entre parenthèses est ignorée sans erreur. Le snippet est donc rendu à partir de la seule configuration.

Pour un rendu différent — typiquement un nonce CSP, propre à chaque requête alors que le SnippetRenderer est un singleton — rebinde le renderer avant le rendu de la vue, depuis un middleware :

use Illuminate\Support\Facades\App;
use Vskstudio\Takt\Options;
use Vskstudio\Takt\SnippetRenderer;

App::bind(SnippetRenderer::class, fn () => new SnippetRenderer(Options::fromArray(
    ['nonce' => $nonce, 'scriptOrigin' => config('takt.script_origin')] + config('takt')
)));

Options::fromArray() lit les autres clés en snake_case, comme la config ; scriptOrigin est la seule attendue en camelCase.

Façade Takt (serveur-à-serveur)

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'));

Takt::pageview('https://exemple.fr/merci');
  • event(string $name, array $props = [], ?Revenue $revenue = null, ?string $url = null, ?string $referrer = null): void
  • pageview(?string $url = null, ?string $referrer = null): void
  • Sans $url, l’événement part avec une URL vide : il est comptabilisé, mais rattaché à aucune page.
  • Fire-and-forget : un 202 vaut succès ; par défaut les erreurs de transport et les statuts non-202 sont avalés, l’analytics ne devant jamais casser l’app. Aucune exception ne remonte — valide l’ingestion depuis le dashboard. Takt::strict() renvoie une instance qui lève sur non-202 : réserve-la aux tests.

La façade résout le service Takt du conteneur, lié par requête. L’IP et l’User-Agent proviennent de la requête courante via $request->ip() et $request->userAgent().

Derrière un proxy ou un load-balancer, configure les proxys de confiance (App\Http\Middleware\TrustProxies en Laravel 10, trustProxies() dans bootstrap/app.php en Laravel 11 / 12) : sans cela, Takt reçoit l'IP du proxy et toute l'audience S2S est attribuée à une poignée d'adresses d'infrastructure. Hors requête HTTP — job en file, commande Artisan, webhook — il n'y a pas de visiteur à forwarder : passe l'IP et l'User-Agent réels toi-même avec withVisitor().