ServicesÀ proposNotesContact Me contacter →
EN FR
Note

Patterns de sessionisation personnalisée

Comment construire des définitions de session personnalisées à partir d'événements bruts en utilisant LAG et des sommes cumulatives, avec des timeouts configurables, des découpages basés sur les campagnes, et des métriques de session.

Planté
bigqueryga4analyticsdata modeling

GA4 définit les sessions avec un timeout d’inactivité de 30 minutes. C’est une valeur par défaut raisonnable, mais le métier peut avoir besoin de quelque chose de différent. Les applications à fort engagement peuvent nécessiter des timeouts de 15 minutes pour distinguer les sessions de navigation des sessions d’achat. Les sites de contenu peuvent vouloir des timeouts de 45 minutes parce que les lecteurs font des pauses en milieu d’article. Certaines équipes ont besoin de découpages de session basés sur les campagnes, où les nouveaux paramètres UTM déclenchent toujours une nouvelle session, indépendamment du timing.

La sessionisation personnalisée donne un contrôle total sur ce qui constitue une « session » dans les données. La technique utilise des fonctions fenêtre — spécifiquement LAG pour la détection des ruptures et une somme cumulative pour la numérotation des sessions — pour construire des sessions à partir des données brutes au grain événement.

Le pattern de base : détection des ruptures + somme cumulative

Le pattern fonctionne en deux étapes. D’abord, LAG identifie les frontières de session en comparant le timestamp de chaque événement à celui de l’événement précédent. Ensuite, une somme cumulative des drapeaux de frontière attribue les numéros de session.

WITH events_with_gaps AS (
SELECT
user_pseudo_id,
event_timestamp,
event_name,
traffic_source.source AS traffic__source,
traffic_source.medium AS traffic__medium,
CASE
WHEN LAG(event_timestamp) OVER (
PARTITION BY user_pseudo_id ORDER BY event_timestamp
) IS NULL THEN 1 -- Premier événement = nouvelle session
WHEN TIMESTAMP_DIFF(
event_timestamp,
LAG(event_timestamp) OVER (PARTITION BY user_pseudo_id ORDER BY event_timestamp),
MINUTE
) > 30 THEN 1 -- Écart > 30 min = nouvelle session
ELSE 0
END AS event__is_new_session
FROM `project.analytics_XXXXX.events_*`
WHERE _TABLE_SUFFIX BETWEEN '20250101' AND '20250131'
),
sessionized AS (
SELECT
user_pseudo_id,
event_timestamp,
event_name,
traffic__source,
traffic__medium,
event__is_new_session,
SUM(event__is_new_session) OVER (
PARTITION BY user_pseudo_id
ORDER BY event_timestamp
) AS session_number
FROM events_with_gaps
)
SELECT
user_pseudo_id,
event_timestamp,
event_name,
traffic__source,
traffic__medium,
session_number,
CONCAT(user_pseudo_id, '_', session_number) AS custom_session_id
FROM sessionized;

Le premier CTE identifie les frontières : le tout premier événement d’un utilisateur (où LAG retourne NULL) et tout événement qui suit plus de 30 minutes d’inactivité. Le deuxième CTE utilise SUM(event__is_new_session) comme total cumulatif — chaque fois que le drapeau est à 1, le numéro de session s’incrémente de un. Les événements entre les frontières partagent la même valeur cumulative, qui devient l’identifiant de session.

Le CONCAT(user_pseudo_id, '_', session_number) crée un identifiant de session globalement unique. C’est critique — session_number seul n’est pas unique entre utilisateurs.

Configurer le timeout

Le timeout de 30 minutes est simplement une constante. Le changer pour correspondre au métier :

-- Timeout de 15 minutes pour les applications à fort engagement
WHEN TIMESTAMP_DIFF(..., MINUTE) > 15 THEN 1
-- Timeout de 45 minutes pour les sites à fort contenu
WHEN TIMESTAMP_DIFF(..., MINUTE) > 45 THEN 1
-- Timeout de 2 heures pour les plateformes B2B avec de longs workflows
WHEN TIMESTAMP_DIFF(..., HOUR) > 2 THEN 1

Comment choisir le bon timeout : regarder la distribution des écarts inter-événements dans les données. Tracer un histogramme du temps entre des événements consécutifs par utilisateur. On observe typiquement une distribution bimodale — un cluster de courts écarts (activité intra-session) et un cluster de longs écarts (entre sessions). Fixer le timeout à la vallée entre ces clusters.

-- Analyser la distribution des écarts inter-événements
SELECT
CASE
WHEN gap_minutes <= 5 THEN '0-5 min'
WHEN gap_minutes <= 15 THEN '5-15 min'
WHEN gap_minutes <= 30 THEN '15-30 min'
WHEN gap_minutes <= 60 THEN '30-60 min'
WHEN gap_minutes <= 120 THEN '60-120 min'
ELSE '120+ min'
END AS gap_bucket,
COUNT(*) AS event_count
FROM (
SELECT
TIMESTAMP_DIFF(
event_timestamp,
LAG(event_timestamp) OVER (PARTITION BY user_pseudo_id ORDER BY event_timestamp),
MINUTE
) AS gap_minutes
FROM events
)
WHERE gap_minutes IS NOT NULL
GROUP BY 1
ORDER BY 1;

Si les données montrent une chute nette entre 20 et 40 minutes, un timeout de 30 minutes est approprié. Si l’activité se concentre fortement sous 10 minutes avec une longue queue, 15 minutes peut être plus adapté.

Découpage de session basé sur les campagnes

Parfois, le timing n’est pas la seule frontière de session. Un utilisateur qui clique sur une nouvelle publicité (paramètres UTM différents) commence sans doute une nouvelle session même s’il ne s’est écoulé que 5 minutes depuis son dernier événement. Cela compte pour la modélisation d’attribution car cela détermine quelle session reçoit le crédit pour une conversion.

Ajouter le découpage basé sur les campagnes à la détection des frontières :

CASE
WHEN LAG(event_timestamp) OVER (w) IS NULL THEN 1
WHEN TIMESTAMP_DIFF(event_timestamp, LAG(event_timestamp) OVER (w), MINUTE) > 30 THEN 1
-- Nouvelle source/medium UTM = nouvelle session
WHEN traffic_source.source != LAG(traffic_source.source) OVER (w)
OR traffic_source.medium != LAG(traffic_source.medium) OVER (w)
THEN 1
ELSE 0
END AS event__is_new_session

Attention à la gestion des NULL ici. Si traffic_source.source est NULL pour le trafic organique, la comparaison != retourne NULL (pas TRUE). Utiliser IFNULL ou COALESCE pour gérer les données d’attribution manquantes :

WHEN IFNULL(traffic_source.source, 'none') != IFNULL(LAG(traffic_source.source) OVER (w), 'none') THEN 1

Métriques de session

Une fois les sessions disponibles, agréger les données au niveau événement en métriques au niveau session dans un CTE suivant :

, session_metrics AS (
SELECT
custom_session_id,
user_pseudo_id,
MIN(event_timestamp) AS session__started_at,
MAX(event_timestamp) AS session__ended_at,
TIMESTAMP_DIFF(MAX(event_timestamp), MIN(event_timestamp), SECOND) AS session__duration_seconds,
COUNT(*) AS session__events,
COUNTIF(event_name = 'page_view') AS session__pageviews
FROM sessionized
GROUP BY custom_session_id, user_pseudo_id
)

Métriques courantes au niveau session à calculer :

  • Durée : TIMESTAMP_DIFF(MAX, MIN, SECOND) — noter que c’est 0 pour les sessions à un seul événement, ce qui est aussi une particularité GA4
  • Pages vues : COUNTIF(event_name = 'page_view')
  • Engagement : définir sa propre règle (ex. durée > 10 secondes OU pages vues > 1 OU événement de conversion présent)
  • Rebond : l’inverse de la définition d’engagement
  • Page d’entrée : utiliser FIRST_VALUE(page_location) OVER (PARTITION BY session_id ORDER BY event_timestamp) avant d’agréger
  • Page de sortie : idem avec LAST_VALUE et un frame explicite ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING

Pourquoi ne pas simplement utiliser ga_session_id ?

Le ga_session_id de GA4 (extrait depuis les event_params) fonctionne dans de nombreux cas, mais il a des limites :

  1. Il utilise le timeout de 30 minutes de Google, qu’on ne peut pas changer
  2. Il se réinitialise à minuit dans le fuseau horaire de l’utilisateur dans certaines implémentations, découpant les sessions qui s’étendent sur minuit
  3. Il n’est pas globalement unique — plusieurs utilisateurs qui commencent des sessions à la même seconde partagent le même ga_session_id
  4. On ne peut pas ajouter de règles de frontière personnalisées comme le découpage basé sur les campagnes

Si la définition de session de GA4 correspond aux besoins, utiliser CONCAT(user_pseudo_id, '.', ga_session_id) comme clé de session et éviter la sessionisation personnalisée. Construire sa propre sessionisation uniquement quand la logique métier l’exige.

Assemblage inter-appareils des sessions

Quand les utilisateurs ont un user_id (état connecté), on peut construire des sessions sur plusieurs appareils :

PARTITION BY COALESCE(user_id, user_pseudo_id) ORDER BY event_timestamp

Cela regroupe les événements par utilisateur authentifié quand disponible, revenant à l’utilisateur anonyme sinon. Le compromis : les sessions anonymes d’une même personne sur différents appareils restent séparées (on ne peut pas assembler ce qu’on ne peut pas identifier), et dès qu’un utilisateur se connecte, ses événements pré-connexion sur cet appareil rejoignent la session authentifiée.

Pour une résolution d’identité plus sophistiquée — faire correspondre des sessions anonymes à des utilisateurs connus rétrospectivement — il faut un graphe d’identité séparé, qui va au-delà de ce que la sessionisation seule peut gérer.

Considérations pour la production

Matérialisation : les requêtes de sessionisation personnalisée sont coûteuses car elles scannent tous les événements dans la plage de dates et calculent des fonctions fenêtre sur eux. Matérialiser comme un modèle dbt incrémental avec insert_overwrite sur la partition de date d’événement. Retraiter les 2-3 derniers jours pour gérer les événements arrivant en retard.

Tests : valider que les sessions personnalisées produisent des métriques raisonnables. Comparer les comptages de sessions, la durée moyenne et les taux de rebond avec les métriques de session intégrées de GA4. Des écarts importants (>20%) suggèrent un timeout ou une règle de frontière qui ne correspond pas au comportement des utilisateurs.

Nommage : distinguer les sessions personnalisées des sessions GA4 dans les noms de colonnes. Utiliser custom_session_id plutôt que session_id, et documenter le timeout et les règles de frontière dans la description YAML du modèle. Le futur soi remerciera le soi présent lors du débogage d’un écart dans un dashboard.