takt

PHP

Le cœur PHP vskstudio/takt-core-php (Packagist, v0.6.0) est agnostique au framework. Il couvre deux besoins côté serveur : rendre le snippet du runtime navigateur dans ton HTML, et envoyer des événements serveur-à-serveur (S2S) directement depuis PHP. Les bridges Laravel et Symfony s'appuient dessus.

composer require vskstudio/takt-core-php

PHP 8.1+. SnippetRenderer n’a besoin d’aucune dépendance HTTP : cette ligne suffit pour rendre le snippet.

Le client S2S Takt découvre en revanche son transport via PSR-18 / PSR-17 (php-http/discovery) et le paquet ne déclare que les interfaces. Tu apportes l’implémentation :

composer require vskstudio/takt-core-php guzzlehttp/guzzle
# ou : composer require vskstudio/takt-core-php symfony/http-client nyholm/psr7

Sans implémentation PSR-18 / PSR-17 installée, new Takt(...) lève dès la construction (la découverte a lieu dans le constructeur, hors du filet fire-and-forget).

SnippetRenderer : le runtime navigateur

SnippetRenderer produit le bloc <script> à déposer dans le <head>. Il se configure avec un objet Options en lecture seule.

use Vskstudio\Takt\SnippetRenderer;
use Vskstudio\Takt\Options;
use Vskstudio\Takt\Mode;

$renderer = new SnippetRenderer(new Options(
    domain: 'exemple.fr',
    outbound: true,
    files: true,
    tagged: true,
    notFound: true,
    fileExtensions: ['pdf', 'zip'],
));

echo $renderer->render(); // à placer dans <head>
ChampDéfautRôle
domain(requis)Domaine du site
endpointhttps://taktlytics.com/api/eventEndpoint d’ingestion, par défaut l’origine Takt hébergée (un setup nu marche direct) ; passe /api/event pour un proxy same-origin
scriptOriginnullOrigine first-party pour servir le runtime + dériver l’endpoint (contourne les ad-blockers ; endpoint prime)
outboundfalseSuit les liens sortants (token outbound)
filesfalseSuit les téléchargements (token downloads)
fileExtensions[]Restreint les téléchargements à ces extensions (data-downloads-ext) ; vide = liste par défaut
taggedfalseSuit les éléments marqués data-takt-event (token tagged)
notFoundfalseSuit les pages 404 (token 404)
excludeLocalhosttrueIgnore les événements localhost
noncenullNonce CSP pour la balise <script>
sampleRatenullN’envoie que cette fraction (0 à 1) des hits (data-sample-rate)
trackQuerynullConserve la query string + le hash (data-track-query) ; off = strippé
queryParams[]Allowlist de paramètres de query conservés (data-query-params). Sans effet si trackQuery est actif, qui garde tout
respectDntnullfalse cesse de respecter Do-Not-Track (data-respect-dnt)
enablednullfalse = coupe-circuit, snippet no-op (data-enabled)
scrubUrlnullFonction JS brute de réécriture d’URL (requiert Mode::Sdk)
exclude[]Préfixes de chemin jamais suivis (borné au segment), requiert Mode::Sdk
redactRoutes[]Motifs de routes sensibles envoyés sous la forme du motif, voir Masquage de routes (requiert Mode::Sdk)
routeTemplatesfalseEnvoie chaque page sous la forme de son gabarit de route (requiert Mode::Sdk)
routeTemplatenullGabarit de la route de la page rendue, lu quand routeTemplates est actif
modeMode::InlineSource du runtime (voir ci-dessous)

enum Mode :

  • Inline : embarque le bundle takt.auto.js vendoré dans la balise (zéro requête en plus, compatible CSP avec un nonce).
  • Cdn : émet un loader <script defer src="https://cdn.jsdelivr.net/npm/@vskstudio/[email protected]/dist/takt.auto.js">.
  • Asset : defer src="/takt/takt.auto.js" pointant vers une copie auto-hébergée, ou {scriptOrigin}/takt/takt.auto.js si scriptOrigin est défini.
  • Sdk : émet un <script type="module">import{init}…;init({…})</script> qui boote le SDK complet. Seul mode capable d’exprimer scrubUrl ; le module est chargé depuis {scriptOrigin}/takt/takt.esm.js si scriptOrigin est défini, sinon depuis jsDelivr. Le paquet ne vendore que takt.auto.js : takt.esm.js n’est pas fourni, c’est à toi de le déposer à cette URL, sinon le module renvoie un 404 et rien n’est mesuré.

L’autocapture est opt-in : outbound, files, tagged et notFound ajoutent chacun un token à un unique attribut data-auto que lit le bundle takt.auto.js ; fileExtensions restreint les téléchargements comptés.

Options avancées

Chaque option avancée vaut null par défaut (« non définie » : le défaut du tracker s’applique) ; seule une valeur non-défaut est rendue. sampleRate, trackQuery, queryParams, respectDnt et enabled se reflètent en attributs data-* dans les modes Inline, Cdn et Asset (parité snippet, voir Configuration) ; en Mode::Sdk aucun attribut data-* n’est émis, tout part dans l’objet passé à init(). scrubUrl est une fonction JS brute injectée verbatim dans la page : elle ne s’exprime qu’en Mode::Sdk (la passer dans un autre mode lève une exception) et reste contrôlée par le dev. Ne la construis jamais à partir d’entrées utilisateur. exclude vit lui aussi uniquement dans le SDK complet (le snippet minimal ≤ 1 kB ne l’embarque pas) : il requiert Mode::Sdk et lève une exception dans les autres modes plutôt que de laisser filer silencieusement des chemins qu’on croyait exclus.

new Options(
    domain: 'exemple.fr',
    mode: Mode::Sdk,
    sampleRate: 0.5,
    queryParams: ['utm_source'],
    exclude: ['/app', '/account'],
    scrubUrl: '(u) => u.split("?")[0]',
);
Le suivi de navigation SPA est toujours actif (pas d'option spa). Le respect de Do-Not-Track est actif par défaut ; ne passe respectDnt: false (ou data-respect-dnt="false") que si tu sais exactement pourquoi.

Options::fromArray(array $a): self construit les options depuis un tableau et accepte en plus les alias snake_case (exclude_localhost, not_found, file_extensions, sample_rate, track_query, query_params, respect_dnt, scrub_url, redact_routes, route_templates, route_template). C’est ce qui rend les bridges Laravel et Symfony configurables par fichier de config. Deux constantes évitent de recopier les URLs : Options::HOSTED_ORIGIN (https://taktlytics.com) et Options::HOSTED_ENDPOINT (https://taktlytics.com/api/event). Si tu assembles toi-même une balise inline, SnippetRenderer::neutralizeScriptClose(string $js): string est publique : elle échappe les fermetures de balise script contenues dans un bundle.

Masquage de routes

Le nettoyage des URLs retire la query et le hash, mais le chemin part en clair : /verify/abc123 transmet le jeton. Depuis la 0.6.0, trois options remplacent ces chemins par le motif de la route, avec les règles du SDK navigateur (voir Masquage de routes). Elles vivent dans le SDK complet et requièrent Mode::Sdk : poser redactRoutes ou routeTemplates dans un autre mode lève une InvalidArgumentException à la construction du SnippetRenderer, puisque le snippet minimal ne sait pas masquer. Mode::Sdk charge @vskstudio/takt-core 0.10.0, première version qui gère ces options.

new Options(
    domain: 'exemple.fr',
    mode: Mode::Sdk,
    redactRoutes: ['/verify/{token}', '/reset/:code', '/factures/[id].pdf'],
);

redactRoutes liste les routes qui portent un secret. Un chemin qui correspond à l’un des motifs part sous la forme du motif, tous les autres gardent leur chemin réel. Motifs acceptés :

SyntaxeExempleSens
[p]/verify/[token]Un segment
[[p]]/blog/[[page]]Segment optionnel
[...p]/docs/[...rest]Reste du chemin
(groupe)/(app)/verify/[token]Groupe ignoré
:p, :p?/reset/:codeSegment, optionnel avec ?
*, **/files/**Joker de segment, reste du chemin
{p}, {p?}/users/{id}Syntaxe Laravel / Symfony, optionnel avec ?
{p<regex>}/users/{id<\d+>}Segment qui doit satisfaire la regex, voir Contraintes ci-dessous

Un segment peut mêler texte et paramètre ([id].pdf). Le motif envoyé est normalisé : les groupes disparaissent, ainsi que les contraintes et valeurs par défaut ({id<\d+>?1} devient {id}).

Contraintes : {id<\d+>} ne correspond qu’à un segment entier qui satisfait la regex, sans franchir / et sans tenir compte de la casse. La regex suit une grammaire restreinte, pas PCRE : alternation au premier niveau seulement, littéraux, ., \d \D \w \W \s \S, classes [...] sans imbrication, quantificateurs * + ? {n} {n,} {n,m} bornés à 100, sans quantificateur paresseux ni possessif, au plus deux quantificateurs variables et 100 caractères au plus. Les groupes, lookarounds, rétroréférences, ancres et / sont refusés. \d, \w et \s ne lisent que l’ASCII, et la regex compte des octets, pas des caractères.

Une route est inutilisable dans ces cas :

  • une contrainte sort de la grammaire ou de ses bornes, un segment porte plus d’un paramètre contraint, ou un { ou } reste orphelin dans un segment ;
  • son degré d’ambiguïté dépasse 1. Un segment compte ses variables moins 1, avec un plancher à zéro : chaque paramètre sans contrainte compte une variable, et un paramètre contraint compte les quantificateurs variables de sa regex. Un segment optionnel (/{a?}) compte une variable de plus avant ce calcul. On somme les segments : /{a?}/{b}{c} donne 1 pour {a?} et 1 pour {b}{c}, soit 2, donc elle est refusée ; /{a?}/{b} donne 1 et passe ;
  • elle contient plus d’un segment REST (**, *nom, :nom*, :nom+, [...nom]).

Exemples : /{a}{b}/{c} est acceptée, /{a?}/{b} aussi, alors que /{a}{b}/{c}{d}, /{p<a*a*>}/{q}{r} et /{a?}/{b}{c} sont refusées. /a/**/b est acceptée, /a/**/b/** est refusée. Une route inutilisable masque en /[redacted] tout chemin de même forme ; les autres chemins ne sont pas touchés. Un segment contraint de plus de 256 octets est masqué avant la vérification.

Le SDK navigateur ne lit pas la syntaxe à accolades : le renderer lui transmet {token} sous la forme [token] et {page?} sous la forme [[page]]. Une pageview du navigateur remonte donc /verify/[token], alors qu’un événement envoyé par le client serveur Takt remonte /verify/{token}. Écris le motif entre crochets si les deux doivent tomber sur la même ligne du classement Pages.

new Options(
    domain: 'exemple.fr',
    mode: Mode::Sdk,
    routeTemplates: true,
    routeTemplate: '/users/{id}',
);

routeTemplates: true envoie chaque page sous la forme de son gabarit : /users/42 devient /users/{id}. PHP rend chaque page côté serveur, passe donc le gabarit de la route courante dans routeTemplate : il est émis comme résolveur constant (routeTemplate: () => "/users/{id}"), sous forme canonique ({id?} devient {id}). Ce gabarit n’est pas traduit en crochets : avec routeTemplates, la pageview du navigateur et l’événement serveur remontent tous deux /users/{id}. Sans routeTemplate, redactRoutes s’applique quand même, sinon le chemin réel part.

Options::withRouteTemplate(?string $routeTemplate): self et SnippetRenderer::withRouteTemplate(?string $routeTemplate): self renvoient une copie qui porte le gabarit de la requête courante, sans reconstruire toute la configuration :

echo $renderer->withRouteTemplate($routeCourante)->render();

Les bridges Laravel et Symfony remplissent ce gabarit pour toi. Le plugin WordPress n’expose pas ces options.

Le masquage du snippet et celui du client serveur se règlent séparément : redactRoutes d’Options ne s’applique pas au client Takt, qui prend sa propre liste (voir Masquer les routes en S2S).

Takt : le client serveur-à-serveur

Takt poste des événements sur POST /api/event avec une clé d’API portant la permission events:write (préfixe takt_ik_), liée au site dont le domaine correspond exactement à domain. Sinon l’ingest répond 401. Idéal pour tracer des actions qui n’ont pas lieu dans le navigateur (webhook de paiement, job en file…).

L’endpoint du client S2S accepte l’origine (Options::HOSTED_ORIGIN, ou celle de ton proxy first-party) comme l’URL de collecte complète (Options::HOSTED_ENDPOINT) : /api/event n’est ajouté que s’il manque, jamais en double. Il doit être absolu.

use Vskstudio\Takt\Options;
use Vskstudio\Takt\Revenue;
use Vskstudio\Takt\Takt;

// Forwarde l'IP + l'User-Agent du visiteur réel pour l'attribution
// (adapte la lecture de la requête à ton framework) :
$takt = (new Takt(
    endpoint: Options::HOSTED_ORIGIN, // https://taktlytics.com
    domain: 'exemple.fr',
    apiKey: $_ENV['TAKT_API_KEY'],
))->withVisitor($_SERVER['REMOTE_ADDR'] ?? null, $_SERVER['HTTP_USER_AGENT'] ?? null);

$takt->event('Signup', ['plan' => 'pro'], null, 'https://exemple.fr/inscription');

$takt->event(
    'Purchase',
    ['plan' => 'pro'],
    new Revenue(amount: '29.00', currency: 'EUR'),
    'https://exemple.fr/merci',
);

$takt->pageview('https://exemple.fr/merci');
  • __construct(string $endpoint, string $domain, ?string $apiKey = null, ?ClientInterface $httpClient = null, ?RequestFactoryInterface $requestFactory = null, ?StreamFactoryInterface $streamFactory = null, array $redactRoutes = [], bool $redactUndecodablePaths = true) : $httpClient, $requestFactory et $streamFactory injectent le transport PSR (indispensable pour tester) ; laissés à null, ils sont découverts. $redactRoutes liste les routes sensibles, voir Masquer les routes en S2S. $redactUndecodablePaths (défaut true) règle les chemins illisibles, voir Chemins illisibles.
  • withVisitor(?string $ip, ?string $userAgent): self : lie le visiteur réel avant l’envoi (renvoie une instance depuis laquelle envoyer)
  • event(string $name, array $props = [], ?Revenue $revenue = null, ?string $url = null, ?string $referrer = null, ?string $route = null): void : $props est un array<string,scalar> dont chaque valeur est castée en chaîne (true part en '1')
  • pageview(?string $url = null, ?string $referrer = null, ?string $route = null): void
  • withRoute(string|\Closure|null $route): self : gabarit de route par défaut de tous les appels du clone renvoyé
  • strict(): self : renvoie un clone qui lève sur erreur de transport ou sur toute réponse ≠ 202 (pratique en test)
  • Fire-and-forget : en mode par défaut, erreurs de transport comme réponses d’erreur sont avalées (l’analytics ne doit jamais casser l’app). Un 202 signifie « accepté », pas « enregistré » : un User-Agent classé bot ou un opt-out (DNT / GPC) est jeté côté serveur, avec un 202 malgré tout.
Passe $url en URL absolue http(s) pour rattacher l'événement à la bonne page. Omise, l'événement est rattaché à la page d'accueil du site (https:// suivi de domain, puis /) : c'est utile pour un hook sans contexte de page (achat WooCommerce, job en file), mais toutes ces actions tombent alors sur la même page.
L'attribution visiteur est dérivée côté serveur de l'IP et de l'User-Agent. Sans withVisitor(), les events S2S sont attribués au serveur applicatif, pas au visiteur réel. Forwarde l'IP / l'User-Agent depuis la requête courante.

Revenue est un value object en lecture seule (amount, currency) : le montant est une chaîne décimale, la devise un code à 3 lettres majuscules (miroir du SDK JS). Les deux sont validés à la construction : '29,00' ou 'eur' lèvent une InvalidArgumentException.

Masquer les routes en S2S

Le client Takt masque les routes avec les mêmes motifs que le snippet (voir Masquage de routes), mais il se règle à part :

  • redactRoutes, passé au constructeur, s’applique à chaque appel : l’URL de la page et le referrer du même site partent sous la forme du motif quand ils correspondent.
  • redactUndecodablePaths, le dernier argument du constructeur (depuis 0.7.0, défaut true), règle le sort des chemins illisibles : voir Chemins illisibles. Il ne règle que les envois serveur : le snippet rendu par PHP garde le défaut, masqué.
  • L’argument route de pageview() et event() envoie cet appel sous le gabarit fourni.
  • withRoute() renvoie un clone dont tous les appels partent sous un gabarit par défaut. Il prend une chaîne, ou une closure évaluée à l’envoi (une closure qui lève ou ne renvoie pas de chaîne est ignorée). Un route explicite prime sur ce défaut.
$takt = new Takt(
    endpoint: Options::HOSTED_ORIGIN,
    domain: 'exemple.fr',
    apiKey: $_ENV['TAKT_API_KEY'],
    redactRoutes: ['/verify/{token}'],
);

$takt->pageview('https://exemple.fr/verify/abc123');

$takt->event('Verified', url: 'https://exemple.fr/users/42', route: '/users/{id}');

$parRequete = $takt->withRoute(fn (): ?string => $router->currentTemplate());

La première URL part en https://exemple.fr/verify/{token}, la deuxième en https://exemple.fr/users/{id}. Côté serveur, les accolades ne sont pas traduites en crochets. Avec un gabarit (route ou withRoute()), un referrer du même site est réduit à l’origine, puisque son gabarit n’est pas connu.