11-audience-measurement.md 6422 octets

11. Mesure d'audience

Comment brancher un script de suivi sur une application Rustwarden, et comment migrer un projet qui l'avait câblé en dur.

Pour la référence exhaustive de l'API, voir SERVICES_REFERENCE.md §23.


Le principe

Rustwarden ne connaît aucun fournisseur d'analytics. Il modélise le suivi comme une URL de script plus des attributs data-*, ce qui suffit à décrire Rybbit, Plausible, Umami, Matomo-tag et leurs équivalents. Chaque projet décide s'il mesure son audience, avec quel script, et sous quel identifiant de site.

Par défaut, rien n'est activé : aucune balise rendue, CSP inchangée.

Trois responsabilités sont réparties ainsi :

QuoiOùPourquoi
L'URL du scriptvariable d'environnementElle alimente la CSP, figée au démarrage
Activation, identifiant, attributspanneau d'administrationModifiables sans redéploiement
L'endroit où la balise apparaîtvos templatesLe framework ne l'injecte pas tout seul

Mise en place

1. Déclarer le script

# .env
ANALYTICS_SCRIPT_URL=https://stats.example.org/api/script.js
ANALYTICS_SITE_ID=1d870795f7ae
ANALYTICS_OPT_OUT_STORAGE_KEY=disable-rybbit
ANALYTICS_ENABLED=true

L'attribut portant l'identifiant dépend du fournisseur :

FournisseurANALYTICS_SITE_ID_ATTRIBUTE
Rybbit, Matomo-tagdata-site-id (défaut, rien à poser)
Umamidata-website-id
Plausibledata-domain

2. Câbler le builder

use rustwarden_core::prelude::*;

let app = RustwardenApp::builder()
    .from_env()
    .analytics(AnalyticsConfig::from_env()?)
    .build()
    .await?;

Ou en dur, sans passer par l'environnement :

.analytics(
    AnalyticsConfig::default()
        .with_script_url("https://stats.example.org/api/script.js")
        .with_site_id("1d870795f7ae")
        .with_opt_out_storage_key("disable-rybbit")
        .enabled(true),
)

Vous n'avez rien à faire pour la CSP. build() ajoute l'origine du script à script-src et connect-src. L'ordre des appels .analytics(..) et .security_headers(..) est indifférent.

3. Poser la balise dans vos pages

Le framework ne touche pas à votre layout : c'est à vous d'appeler le helper, une fois, dans votre template de base.

// Handler
pub async fn ma_page(
    analytics: AnalyticsContext,
    // ..
) -> Result<impl IntoResponse, AppError> {
    Ok(MaPageTemplate { analytics, /* .. */ })
}
<!-- base.html, avant </head> -->
{% if analytics.is_enabled() %}{{ analytics.snippet|safe }}{% endif %}

Le |safe est légitime ici, et seulement ici : la valeur est produite par le framework après validation par liste blanche et échappement HTML. N'y mettez jamais de saisie utilisateur.

4. Offrir l'opposition au visiteur

let analytics_opt_out =
    AnalyticsOptOutContext::build(&analytics, &i18n_service, locale.as_str()).await;
<!-- Dans votre page « vie privée » -->
{% include "components/analytics_opt_out.html" %}

Le composant ne rend rien si aucune clé d'opposition n'est configurée : vous pouvez l'inclure sans condition. Le choix du visiteur est stocké dans son navigateur et prend effet au chargement de page suivant, parce que la plupart des scripts ne lisent ce drapeau qu'à leur initialisation.

5. Piloter au quotidien

/admin/settings expose une section « Mesure d'audience » : interrupteur, identifiant de site, attributs additionnels. Les changements prennent effet immédiatement, sans redémarrage.

L'URL du script y apparaît en lecture seule. Ce n'est pas un oubli : la CSP est construite une fois au démarrage, donc une URL modifiable à chaud produirait un script bloqué par le navigateur. Cela évite aussi qu'un compte d'administration compromis puisse élargir la CSP vers un domaine arbitraire.


Migrer un projet qui l'avait en dur

Exemple type, tel qu'on le trouve dans un projet consommateur avant migration : un partiel pm_analytics.html inclus dans une vingtaine de templates, un composant d'opposition avec ses traductions écrites en ligne, et un middleware qui réécrit la CSP entière pour autoriser le domaine du tracker.

Ce qui disparaît

À supprimerRemplacé par
Le partiel contenant la balise en dur{{ analytics.snippet|safe }} dans le layout
Les {% include %} répétés page par pageUn seul appel dans le template de base
Le middleware qui surcharge la CSPbuild(), automatiquement
La clé localStorage écrite en durANALYTICS_OPT_OUT_STORAGE_KEY
Les traductions inline du composant d'oppositionanalytics.opt_out.*

Marche à suivre

  1. Relever la configuration existante dans le partiel : URL du script, identifiant de site, attributs supplémentaires (data-consent…).
  2. Les porter en variables d'environnement, selon l'étape 1 ci-dessus.
  3. Câbler .analytics(..) sur le builder.
  4. Supprimer le partiel et tous ses {% include %}, et poser l'appel unique dans le layout. C'est l'étape qui supprime le risque d'oubli sur une page.
  5. Retirer la surcharge de CSP. Attention : si ce middleware ajoutait autre chose que le domaine du tracker — typiquement 'unsafe-eval' pour Alpine.js — ne le supprimez pas aveuglément. Portez ce qui reste dans un SecurityHeadersConfig explicite, et laissez build() s'occuper du seul domaine d'analytics.
  6. Remplacer le composant d'opposition par components/analytics_opt_out.html.
  7. Vérifier, application lancée :
curl -sI http://localhost:3000/login | grep -i content-security-policy

L'origine doit apparaître dans script-src et connect-src. Puis, dans le navigateur : la balise est présente, l'interrupteur d'/admin/settings la fait disparaître sans redémarrage, et le bouton d'opposition empêche le script de s'initialiser au rechargement suivant.


Limites

  • Un seul fournisseur à la fois.
  • Pas de bannière de consentement : un fournisseur qui en attend une se pilote par un attribut (data-consent), le recueil restant à la charge de l'application.
  • Pas de collecte côté serveur : le framework émet une balise, il ne mesure rien.
  • Changer de domaine de collecte demande un redéploiement.