PostHog avec Next.js : analytics produit, feature flags et session replay (Guide 2026)

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

PostHog est devenu, en 2026, la plateforme produit la plus complète pour les équipes SaaS. Ce qui exigeait Mixpanel pour les analytics, LaunchDarkly pour les feature flags, FullStory pour le session replay, Optimizely pour les tests A/B et Sentry pour les erreurs vit désormais dans une seule plateforme open source, avec une offre gratuite généreuse et la possibilité de l'auto-héberger.

Dans ce tutoriel, vous brancherez PostHog dans un projet Next.js 15 App Router de bout en bout. À la fin, vous suivrez des événements, gérerez des fonctionnalités derrière des flags, enregistrerez des sessions, mènerez une expérimentation et capturerez des exceptions — en utilisant le provider moderne posthog-js/react et les bons patterns SSR/client.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20 ou plus récent
  • Une connaissance de base de Next.js App Router et des React Server Components
  • Un compte PostHog Cloud (l'offre gratuite suffit) — inscription sur posthog.com, ou une instance auto-hébergée
  • Un éditeur de code (VS Code recommandé)

Vous devez aussi être à l'aise avec les variables d'environnement et la différence entre composants serveur et client dans Next.js.

Ce que vous allez construire

Vous construirez un petit dashboard façon SaaS qui :

  • Capture les pageviews et les événements personnalisés côté client et serveur
  • Identifie les utilisateurs authentifiés et y attache des propriétés
  • Affiche conditionnellement une UI "nouveau dashboard" derrière un feature flag
  • Mène un test A/B sur le CTA d'une page tarifs
  • Enregistre des sessions avec masquage des champs de saisie
  • Remonte les exceptions non gérées vers Error Tracking de PostHog

Voici la structure finale du projet :

app/
  layout.tsx
  providers.tsx
  page.tsx
  pricing/page.tsx
  api/track/route.ts
lib/
  posthog-server.ts
.env.local

Étape 1 : créer le projet et installer les dépendances

Démarrez avec un projet Next.js 15 propre (sautez si vous en avez déjà un) :

npx create-next-app@latest posthog-demo --typescript --app --tailwind --eslint
cd posthog-demo

Installez les SDKs PostHog. Deux paquets sont nécessaires : posthog-js pour le navigateur et posthog-node pour la capture côté serveur.

npm install posthog-js posthog-node

En 2026, l'intégration officielle Next.js est documentée autour du provider posthog-js/react, qui propose une API à base de hooks et évite les appels manuels dans useEffect.

Étape 2 : configurer les variables d'environnement

Créez un fichier .env.local à la racine du projet :

NEXT_PUBLIC_POSTHOG_KEY=phc_your_project_api_key
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
POSTHOG_PERSONAL_API_KEY=phx_your_personal_api_key

Quelques détails importants :

  • Les variables NEXT_PUBLIC_* sont exposées au navigateur — c'est volontaire pour la clé projet, qui est un token public destiné à la capture côté client.
  • Utilisez eu.i.posthog.com si votre projet PostHog est en Europe, sinon us.i.posthog.com. Choisir la mauvaise région fait silencieusement disparaître les événements.
  • La personal API key sert uniquement aux tâches admin côté serveur (lire les définitions de flags par exemple) ; elle ne doit jamais arriver dans le navigateur.

Étape 3 : configurer le provider navigateur

Créez app/providers.tsx. Ce fichier est un composant client qui initialise PostHog une seule fois et l'expose à votre arbre via PostHogProvider.

// app/providers.tsx
"use client";
 
import posthog from "posthog-js";
import { PostHogProvider as PHProvider } from "posthog-js/react";
import { useEffect } from "react";
 
export function PostHogProvider({ children }: { children: React.ReactNode }) {
  useEffect(() => {
    posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
      api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
      capture_pageview: false, // géré manuellement pour App Router
      capture_pageleave: true,
      person_profiles: "identified_only",
      session_recording: {
        maskAllInputs: true,
      },
    });
  }, []);
 
  return <PHProvider client={posthog}>{children}</PHProvider>;
}

Deux choix à comprendre :

  • capture_pageview: false — Next.js App Router fait de la navigation soft, donc le pageview automatique du SDK ne se déclenche qu'au chargement initial. On capturera les pageviews manuellement à chaque changement de route.
  • person_profiles: "identified_only" — les visiteurs anonymes ne créent pas de profil, ce qui maintient votre facture MAU au plus bas tant qu'aucun utilisateur réel n'est identifié.

Étape 4 : tracer les pageviews lors des changements de route

Ajoutez un petit composant client qui écoute la navigation App Router et déclenche un événement $pageview à chaque changement de chemin.

// app/posthog-pageview.tsx
"use client";
 
import { usePathname, useSearchParams } from "next/navigation";
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
 
export function PostHogPageview() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const posthog = usePostHog();
 
  useEffect(() => {
    if (!pathname || !posthog) return;
    let url = window.origin + pathname;
    const search = searchParams?.toString();
    if (search) url += `?${search}`;
    posthog.capture("$pageview", { $current_url: url });
  }, [pathname, searchParams, posthog]);
 
  return null;
}

Branchez maintenant le tout dans le layout racine :

// app/layout.tsx
import { Suspense } from "react";
import { PostHogProvider } from "./providers";
import { PostHogPageview } from "./posthog-pageview";
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="fr">
      <body>
        <PostHogProvider>
          <Suspense fallback={null}>
            <PostHogPageview />
          </Suspense>
          {children}
        </PostHogProvider>
      </body>
    </html>
  );
}

L'enrobage Suspense est obligatoire car useSearchParams provoque un bailout CSR — sans lui, toute la route bascule en rendu côté client.

Étape 5 : capturer des événements personnalisés

Vous pouvez désormais capturer des événements depuis n'importe quel composant client via le hook usePostHog :

// app/page.tsx
"use client";
 
import { usePostHog } from "posthog-js/react";
 
export default function Home() {
  const posthog = usePostHog();
 
  return (
    <main className="p-12">
      <h1 className="text-3xl font-bold">Bienvenue</h1>
      <button
        className="mt-6 rounded bg-black px-4 py-2 text-white"
        onClick={() =>
          posthog.capture("cta_clicked", {
            location: "homepage_hero",
            variant: "primary",
          })
        }
      >
        Commencer
      </button>
    </main>
  );
}

Deux règles d'or pour la conception d'événements :

  • Les noms d'événements utilisent snake_case et un verbe au passé : signup_completed, invoice_downloaded, chat_message_sent. Évitez les noms génériques comme click ou submit.
  • Les propriétés sont plates, typées et bornées. N'envoyez pas des objets entiers ; choisissez les 3 à 8 champs sur lesquels vous filtrerez réellement plus tard.

Étape 6 : identifier les utilisateurs authentifiés

Les événements anonymes sont utiles pour les funnels, mais la vraie valeur arrive en reliant les événements à un utilisateur connu. Appelez identify une fois après la connexion :

"use client";
 
import { usePostHog } from "posthog-js/react";
import { useEffect } from "react";
 
type User = { id: string; email: string; plan: "free" | "pro" | "team" };
 
export function IdentifyUser({ user }: { user: User | null }) {
  const posthog = usePostHog();
 
  useEffect(() => {
    if (!user || !posthog) return;
    posthog.identify(user.id, {
      email: user.email,
      plan: user.plan,
    });
    posthog.group("plan", user.plan);
  }, [user, posthog]);
 
  return null;
}

À la déconnexion, appelez posthog.reset() pour détacher le distinct ID et démarrer une session anonyme propre.

Étape 7 : capturer des événements côté serveur

Certains événements ne devraient jamais vivre dans le navigateur — succès de paiement, réception de webhook, fin d'inférence IA. Pour ceux-là, utilisez posthog-node.

Créez lib/posthog-server.ts :

// lib/posthog-server.ts
import { PostHog } from "posthog-node";
 
let client: PostHog | null = null;
 
export function getPostHogServer() {
  if (!client) {
    client = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
      host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
      flushAt: 1,
      flushInterval: 0,
    });
  }
  return client;
}

Les paramètres de flush agressifs sont essentiels en serverless : avec le batching par défaut, les événements capturés dans une fonction Vercel seraient perdus quand le lambda gèle avant le flush.

Utilisez-le dans un route handler :

// app/api/track/route.ts
import { NextResponse } from "next/server";
import { getPostHogServer } from "@/lib/posthog-server";
 
export async function POST(req: Request) {
  const { userId, plan } = await req.json();
  const posthog = getPostHogServer();
 
  posthog.capture({
    distinctId: userId,
    event: "subscription_started",
    properties: { plan, source: "stripe_webhook" },
  });
 
  await posthog.shutdown();
  return NextResponse.json({ ok: true });
}

Faites toujours un await posthog.shutdown() (ou au moins posthog.flush()) avant de retourner depuis un handler serverless. Sinon l'événement reste dans une queue en mémoire qui disparaît avec le lambda.

Étape 8 : livrer une fonctionnalité derrière un flag

Créez un feature flag booléen dans l'UI PostHog appelé new-dashboard. Configurez le rollout sur "100 pourcent des utilisateurs avec la propriété plan = pro" pour que seuls les clients payants le voient.

Côté client, le hook useFeatureFlagEnabled lit le flag mis en cache :

// app/dashboard/page.tsx
"use client";
 
import { useFeatureFlagEnabled } from "posthog-js/react";
 
export default function Dashboard() {
  const newDashboard = useFeatureFlagEnabled("new-dashboard");
 
  if (newDashboard) {
    return <NewDashboard />;
  }
 
  return <LegacyDashboard />;
}

Il y a un souci UX subtil : au tout premier rendu, le flag vaut undefined, puis bascule à true ou false, ce qui crée un flicker visible. Corrigez-le en évaluant le flag côté serveur et en passant la valeur résolue dans l'arbre.

// app/dashboard/page.tsx
import { getPostHogServer } from "@/lib/posthog-server";
import { cookies } from "next/headers";
 
export default async function DashboardPage() {
  const posthog = getPostHogServer();
  const distinctId = (await cookies()).get("ph_distinct_id")?.value ?? "anonymous";
 
  const newDashboard = await posthog.isFeatureEnabled("new-dashboard", distinctId);
 
  return newDashboard ? <NewDashboard /> : <LegacyDashboard />;
}

Pour que l'évaluation côté serveur ait du sens, votre distinct ID doit être stable entre les requêtes. Le plus simple est de le lire depuis un cookie posé après identify, ou d'utiliser l'ID utilisateur de votre fournisseur d'auth.

Étape 9 : mener un test A/B

Les flags multivariés alimentent les expérimentations. Dans l'UI PostHog, créez une expérimentation pricing_cta_test avec deux variantes : control (texte "Démarrer un essai gratuit") et bold (texte "Lancez-vous gratuitement, sans carte bancaire").

// app/pricing/page.tsx
"use client";
 
import { useFeatureFlagVariantKey, usePostHog } from "posthog-js/react";
 
export default function PricingPage() {
  const variant = useFeatureFlagVariantKey("pricing_cta_test");
  const posthog = usePostHog();
 
  const ctaText = variant === "bold"
    ? "Lancez-vous gratuitement, sans carte bancaire"
    : "Démarrer un essai gratuit";
 
  return (
    <button
      onClick={() => {
        posthog.capture("pricing_cta_clicked", { variant });
      }}
    >
      {ctaText}
    </button>
  );
}

Incluez toujours la valeur variant comme propriété de l'événement de conversion. PostHog peut alors calculer le lift, la significativité statistique et les intervalles crédibles automatiquement — et vous aurez les données brutes dans la vue warehouse si vous voulez les découper vous-même.

Étape 10 : activer le session replay avec masquage

Le session replay a déjà été activé à l'étape 3 avec maskAllInputs: true. Cela couvre l'essentiel, mais vous devriez être plus précis sur ce qui est enregistré. Ajoutez des classes CSS sur les éléments à ne jamais capturer :

<input
  type="text"
  className="ph-no-capture"
  placeholder="Numéro fiscal"
/>
 
<div className="ph-mask">
  <p>Termes contractuels sensibles ici.</p>
</div>

Trois classes sont reconnues par le recorder :

  • ph-no-capture — l'élément est traité comme s'il n'existait pas dans le snapshot DOM
  • ph-mask — l'élément est remplacé par un rectangle noir de même taille
  • ph-mask-text — le texte est remplacé par des astérisques mais la mise en page est préservée

Combinez ces classes avec maskAllInputs: true pour livrer le replay en toute sécurité, même dans des secteurs régulés. Pour une conformité RGPD complète, conditionnez aussi la capture de replay au consentement utilisateur avant d'appeler posthog.startSessionRecording().

Étape 11 : capturer les exceptions

Le produit Error Tracking de PostHog se branche sur le même SDK. Activez-le en autorisant l'autocapture des exceptions dans l'init :

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
  // ... config existante
  capture_exceptions: true,
});

Vous pouvez aussi remonter manuellement des erreurs avec stack trace depuis une error boundary Next.js :

// app/error.tsx
"use client";
 
import { useEffect } from "react";
import { usePostHog } from "posthog-js/react";
 
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
  const posthog = usePostHog();
 
  useEffect(() => {
    posthog.captureException(error, {
      digest: (error as Error & { digest?: string }).digest,
    });
  }, [error, posthog]);
 
  return (
    <div>
      <h2>Quelque chose a mal tourné</h2>
      <button onClick={reset}>Réessayer</button>
    </div>
  );
}

Cela alimente l'onglet Errors de PostHog avec une stack trace, les breadcrumbs des événements précédents et un lien vers le replay où l'erreur s'est produite — typiquement la fonctionnalité phare qui justifie la migration depuis Sentry.

Étape 12 : contourner les ad blockers via un reverse proxy

En production, environ 30 pour cent des visiteurs utilisent un ad blocker qui bloque *.posthog.com. Le remède est un reverse proxy via votre propre domaine. Dans Next.js 15, ajoutez ceci à next.config.ts :

const nextConfig = {
  async rewrites() {
    return [
      {
        source: "/ingest/static/:path*",
        destination: "https://eu-assets.i.posthog.com/static/:path*",
      },
      {
        source: "/ingest/:path*",
        destination: "https://eu.i.posthog.com/:path*",
      },
    ];
  },
  skipTrailingSlashRedirect: true,
};

Puis changez l'init du SDK pour utiliser votre propre origine :

posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
  api_host: "/ingest",
  ui_host: "https://eu.posthog.com",
});

Récupérer les événements du trafic bloqué augmente typiquement les données de conversion de 10 à 30 pour cent — largement rentable pour cinq minutes de configuration.

Tester votre intégration

Vérifiez que tout fonctionne avant de crier victoire :

  1. Ouvrez l'onglet Activity de PostHog et chargez votre page d'accueil dans une fenêtre incognito. Vous devriez voir un événement $pageview en quelques secondes.
  2. Cliquez sur le bouton "Commencer" et confirmez que cta_clicked arrive avec les propriétés location et variant.
  3. Connectez-vous à l'application et vérifiez que le prochain événement porte l'email et le plan de l'utilisateur en propriétés de personne.
  4. Basculez le flag new-dashboard dans l'UI et rechargez la route dashboard — le composant rendu doit changer sans redéploiement.
  5. Ouvrez Session Replay et confirmez que les champs de saisie sont masqués.
  6. Déclenchez une exception non gérée et vérifiez qu'elle apparaît dans Errors avec une stack trace.

Si les événements mettent plus d'une minute à apparaître, la cause la plus fréquente est un mauvais réglage de région dans NEXT_PUBLIC_POSTHOG_HOST.

Dépannage

Les événements partent en local mais pas en production. Presque toujours un souci de Content Security Policy ou un ad blocker. Utilisez le reverse proxy de l'étape 12 et vérifiez l'onglet Network pour les requêtes /ingest bloquées.

Le flag retourne undefined indéfiniment. Le SDK n'a pas terminé son bootstrap. Soit attendez posthog.onFeatureFlags(), soit faites une évaluation côté serveur comme à l'étape 8.

Les événements serveur n'arrivent jamais. Oubli du await posthog.shutdown() à la fin du route handler. Le serverless gèle la fonction, le batch en mémoire est perdu.

La facture MAU explose après le lancement. Vous avez oublié person_profiles: "identified_only" et PostHog crée un profil pour chaque visiteur anonyme. Changez le réglage et contactez le support pour réinitialiser les compteurs de profils.

Prochaines étapes

  • Envoyez les événements PostHog dans votre data warehouse via les destinations BigQuery, Snowflake ou Postgres
  • Combinez avec les emails transactionnels Resend pour déclencher des emails depuis des cohortes PostHog
  • Ajoutez Better Auth et identifiez les utilisateurs juste après la connexion
  • Superposez du tracing OpenTelemetry pour une observabilité full-stack côté backend

Conclusion

PostHog transforme cinq abonnements SaaS distincts en une seule plateforme open source que vous pouvez auto-héberger si besoin. Avec les étapes ci-dessus, vous avez analytics, feature flags, expérimentations, session replay et error tracking dans une app Next.js 15 App Router — correctement rendue côté serveur, résiliente aux ad blockers et respectueuse de la vie privée.

Le vrai levier se manifeste sur les semaines suivantes : chaque décision produit a désormais un chiffre derrière elle, chaque fonctionnalité part derrière un flag actionnable en quelques secondes, et chaque rapport de bug arrive avec un replay attaché. C'est ce changement de workflow qui justifie la demi-journée d'installation.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Medusa.js 2.0 — Construire une boutique e-commerce Headless avec Next.js (2026).

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes

Construire un Starter Kit SaaS avec Next.js 15, Stripe et Auth.js v5

Apprenez a construire une application SaaS prete pour la production avec Next.js 15, Stripe pour la facturation par abonnement, et Auth.js v5 pour l'authentification. Ce tutoriel pas a pas couvre la configuration du projet, la connexion OAuth, les plans tarifaires, la gestion des webhooks et les routes protegees.

35 min read·

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·