Construire un outil d'analyse du trafic simple, performant et anonyme pour vos sites web
Vous venez de lancer votre site web et la première question qui vous taraude immédiatement est bien sûr : combien de millions de visiteurs vais-je avoir... Pour cela, rien de plus simple : on branche Google Analytics et voilà !
Sauf que, ça c'était avant.
Pour commencer, Google Analytics présente de nombreux défauts :
C'est de plus en plus pénible à installer
Au niveau du look et de l'ergonomie, comment dire... beurk.
Ils semblent tout enregistrer mais difficile de s'y retrouver
C'est une dépendance de plus sur un projet informatique, ce qui implique des coûts de maintenance non maîtrisables.
Point de vue respect de la vie privée de vos visiteurs, c'est lamentable.
Les données sont stockées et dupliquées ailleurs sur des serveurs de Google, bof.
Bien sûr, il existe plusieurs alternatives stylées, mais la plupart sont payantes.
Alors nous allons nous retrousser les manches, réfléchir à une solution sympa, et économiser 10 euros par mois.
Ce que nous voulons :
Rester le plus simple possible et rester maître des données.
Limiter au maximum les échanges de données entre le serveur et le client.
Enregistrer les visites par URL ou URI (c'est-à-dire le chemin d'accès aux différentes pages d'un site donné) et par "session" (c'est-à-dire, un même utilisateur)
Enregistrer les informations sur le type d'appareil, le système, etc., des visiteurs
Anonymiser les visiteurs, tout en restant capable d'identifier un visiteur dans le temps
Construire une solution simple et pérenne capable d'être efficace jusqu'à 100 000 visites par mois..
Ce que nous ne voulons pas :
Enregistrer des données personnelles comme des IP ou des cookies.
Utiliser la géolocalisation, parce que c'est assez gadget dans notre cas, que ce n'est pas fiable et que ça nécessite de créer une nouvelle dépendance à une API dédiée.
Du coup le plan se dessine presque tout seul :
Il faut stocker l'information dans une base de données pour la simplicité et la rapidité (un fichier plat posera trop vite des problèmes de performance)...
On stockera cette base sur le même serveur que le site car c'est plus simple et plus cohérent.
On stockera tous les champs nécessaires dans une seule table, toujours dans un soucis de simplicité.
On utilisera le middleware pour enregistrer la plupart des informations, et nous limiterons le tracking du côté "client" au temps passé sur la page.
Et maintenant passons à la pratique !
J'utilise Statamic pour déployer les sites de mes clients car c'est super simple à utiliser tant pour moi que pour mes clients, c'est sécurisé et robuste, et le résultat est beau ! Statamic utilisant php et Laravel, nous allons donc rester dans cet univers, mais la logique est exactement la même dans un autre langage :-)
Première étape, créer la table
Bien sûr vous aurez vérifié que MySQL est installé sur le serveur au préalable, et vous aurez configuré votre site pour qu'il puisse se connecter à MySQL
On définit le schéma de la base de données :
php artisan make:migration create_visits_table
Puis dans le fichier nouvellement créé :
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('visits', function (Blueprint $table) {
$table->id();
$table->string('session_id')->nullable(); // id unique pour identifier les sessions
$table->text('url')->nullable(); // le chemin de la page
$table->string('user_agent')->nullable();
$table->string('browser_name')->nullable();
$table->string('os_name')->nullable();
$table->string('device_type')->nullable();
$table->string('ip_hash')->nullable(); // ici on stockera l'ip reçue et on la 'hash' pour obtenir une information unique et cohérente dans le temps, mais indéchiffrable donc anonymisée
$table->text('previous_url')->nullable(); // le chemin de la page précédemment visitée (si sur le site évidemment)
$table->integer('time_spent_on_page')->nullable();
$table->json('extra')->nullable(); // juste au cas où...
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('visits');
}
};
et on effectue la "migration".
php artisan migrate
Comme on utilise Laravel, on va maintenant créer un modèle pour interagir plus naturellement avec cette base de données. Le modèle est extrêmement simple, il n'y a qu'une table donc pas de relation à gérer.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Visit extends Model
{
protected $table = 'visits';
protected $fillable = [
'session_id',
'url',
'user_agent',
'browser_name',
'os_name',
'device_type',
'ip_hash',
'previous_url',
'time_spent_on_page',
'extra',
];
}
Maintenant, l'idée est simple, nous allons utiliser le fait que la page web est constituée de pages avec des urls différentes. A chaque fois que le visiteur voudra aller sur une page, il effectuera une requête auprès de notre serveur qui lui renverra la page à afficher. Ces requêtes passent par différents "middleware", et justement, nous allons créer un nouveau middleware pour utiliser les informations de ces requêtes.
<?php
namespace App\Http\Middleware;
use Closure;
use App\Models\Visit;
use Jenssegers\Agent\Agent;
use Illuminate\Http\Request;
class LogVisitorData
{
public function handle(Request $request, Closure $next)
{
$response = $next($request); // on récupère les informations envoyées avec la requête du visiteur
$agent = new Agent(); // ici on utilise une dépendance à installer pour analyser simplement les infos du 'user_agent'.
$device_type = '';
if ($agent->isRobot()) {
$device_type = 'is_robot';
} elseif ($agent->isDesktop()) {
$device_type = 'desktop';
} elseif ($agent->isPhone()) {
$device_type = 'phone';
} elseif ($agent->isTablet()) {
$device_type = 'tablet';
} else {
$device_type = 'unknown';
}
// On récupère l'URL demandée par le visiteur
$url = $request->fullUrl();
// On récupère l'URL d'où vient le visiteur sur le site
$previous_url = url()->previous();
// On récupère l'IP du visiteur (donnée personnelle) qu'on anonimise via un 'hash', de cette façon ce n'est plus une donnée personnelle mais on pourra analyser les retours de ce même individu dans le temps
$ip_hash = hash('sha256',$request->ip());
// on prépare les données à sauvegarder
$sessionData = [
'session_id' => $request->session()->getId(),
'url' => $url,
'user_agent' => $agent->isRobot() ? 'is_robot' : $request->userAgent(),
'browser_name' => $agent->browser() ?? 'unknown',
'os_name' => $agent->platform() ?? 'unknown',
'device_type' => $device_type,
'ip_hash' => $ip_hash,
'previous_url' => $previous_url,
// 'extra' => $request,
];
// On crée la nouvelle ligne dans la base de données
Visit::create($sessionData);
return $response;
}
}
Parfait !
Maintenant allons nous balader sur le site web pour vérifier que tout fonctionne côté base de données...
et bim !
Ce qu'il nous manque maintenant, c'est de calculer le temps passé sur la page par l'utilisateur.
En théorie pure, on pourrait calculer le temps passé entre chaque page côté serveur (puisqu'on récupère le temps écoulé entre chaque requête serveur) mais malheureusement cette théorie ne résiste pas à la vraie vie, où les requêtes serveurs peuvent parfois être doublées ou triplées, pour de multiples raisons, et ce calcul ne sera en réalité pas fiable.
Le plus simple, c'est de mettre un petit script javascript côté client (c'est à dire sur la page qui se lance dans le navigateur du visiteur). Le travail du script sera de compter les secondes qui s'écoulent quand la page est active... et, de temps en temps (ici toutes les 10 secondes), d'informer le serveur du dernier temps enregistré.
On va donc sur la page (ou les pages) où l'on souhaite pouvoir enregistrer le temps passé, et on ajoute ce script à la fin.
Il ne reste maintenant plus qu'à créer une route côté serveur et un contrôleur pour recevoir et traiter cette information et mettre à jour la donnée.
class VisitorController extends Controller
{
public function updateTime(Request $request)
{
// Validation des données reçues
$validatedData = $request->validate([
'duration' => 'required|integer',
'sessionId' => 'required|string',
'url' => 'required|string'
]);
// Parfois les url contiennent des / en trop, on nettoie au cas où.
$normalizedUrl = rtrim($validatedData['url'], '/');
$model = Visit::query()
->where('visits.session_id','=',$validatedData['sessionId'])
->where('visits.url','=',$normalizedUrl)
->orderByDesc('created_at')
->first();
// Si la ligne existe en base, on ajoute le temps (ici 10 secondes) au temps déjà enregistré
if($model) {
$baseTime = $model->time_spent_on_page;
$model->update([
'time_spent_on_page' => $baseTime + $validatedData['duration'],
]);
}
}
}
Et voilà on enregistre maintenant une information plutôt fiable en base.
Nous allons maintenant pouvoir passer à l'interface graphique du tableau de bord, mais ce sera pour un autre article :-)