From atum-stack-web
Web analytics setup pattern library — Google Analytics 4 with gtag and Measurement Protocol (server-side events), Plausible Analytics (privacy-friendly, no cookies, EU-hostable), Matomo (self-hosted GDPR-compliant alternative), Hotjar / Microsoft Clarity (heatmaps + session recordings), PostHog (product analytics + feature flags), consent management with cookie banners (GDPR-compliant opt-in before loading trackers), event taxonomy best practices (noun_verb naming, standardized properties), funnel tracking, goal conversions, and UTM parameter handling. Use when setting up analytics on a web project: GA4 installation, cookie consent flow, custom event tracking, attribution setup, privacy-friendly analytics alternatives, or server-side tracking to bypass ad blockers. Differentiates from generic observability (Sentry, Datadog) by focusing on marketing/product analytics and user behavior tracking.
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-stack-webThis skill uses the workspace's default tool permissions.
Patterns pour mettre en place du tracking analytics sur un projet web de manière conforme GDPR et performante.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agent creation for Claude Code plugins with file templates, frontmatter specs (name, description, model), triggering examples, system prompts, and best practices.
Patterns pour mettre en place du tracking analytics sur un projet web de manière conforme GDPR et performante.
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
const gaId = process.env.NEXT_PUBLIC_GA_ID
return (
<html>
<head>
{gaId && (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${gaId}`}
strategy="afterInteractive"
/>
<Script id="ga-init" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${gaId}', {
anonymize_ip: true,
cookie_flags: 'SameSite=None;Secure',
});
`}
</Script>
</>
)}
</head>
<body>{children}</body>
</html>
)
}
// app/components/ConsentBanner.tsx
'use client'
declare global {
interface Window {
gtag: (...args: any[]) => void
dataLayer: any[]
}
}
export function setConsent(granted: {
analytics: boolean
marketing: boolean
functional: boolean
}) {
if (typeof window === 'undefined' || !window.gtag) return
window.gtag('consent', 'update', {
analytics_storage: granted.analytics ? 'granted' : 'denied',
ad_storage: granted.marketing ? 'granted' : 'denied',
ad_user_data: granted.marketing ? 'granted' : 'denied',
ad_personalization: granted.marketing ? 'granted' : 'denied',
functionality_storage: granted.functional ? 'granted' : 'denied',
personalization_storage: granted.functional ? 'granted' : 'denied',
})
localStorage.setItem('consent', JSON.stringify(granted))
}
Règle GDPR : appeler gtag('consent', 'default', { ... 'denied' ... }) avant le script GA4, puis 'update' ... 'granted' après l'opt-in explicite de l'utilisateur.
// Default deny (avant le script GA4)
gtag('consent', 'default', {
analytics_storage: 'denied',
ad_storage: 'denied',
ad_user_data: 'denied',
ad_personalization: 'denied',
wait_for_update: 2000, // attend l'update du banner 2s max
})
// app/lib/analytics.ts
export function trackEvent(
eventName: string,
params: Record<string, string | number | boolean> = {}
) {
if (typeof window === 'undefined' || !window.gtag) return
window.gtag('event', eventName, {
event_category: params.category ?? 'engagement',
event_label: params.label,
value: params.value,
...params,
})
}
// Usage
trackEvent('sign_up', { method: 'email' })
trackEvent('add_to_cart', { currency: 'EUR', value: 29.99, items: [{ item_id: 'SKU-123' }] })
trackEvent('purchase', { transaction_id: 'T12345', currency: 'EUR', value: 59.98 })
Pour by-pass les ad blockers ou tracker des events côté serveur (webhooks Stripe, jobs background) :
// Next.js API route
export async function POST(request: Request) {
const event = await request.json()
await fetch(
`https://www.google-analytics.com/mp/collect?measurement_id=${process.env.GA_MEASUREMENT_ID}&api_secret=${process.env.GA_API_SECRET}`,
{
method: 'POST',
body: JSON.stringify({
client_id: event.clientId,
user_id: event.userId,
events: [
{
name: 'purchase_completed_server',
params: {
currency: 'EUR',
value: event.amount,
transaction_id: event.orderId,
},
},
],
}),
}
)
return new Response(null, { status: 204 })
}
// app/layout.tsx
<Script
defer
data-domain="example.com"
src="https://plausible.io/js/script.js"
strategy="afterInteractive"
/>
Avantages Plausible :
// Plausible.js expose une fonction globale
export function trackPlausible(eventName: string, props?: Record<string, string | number>) {
if (typeof window === 'undefined' || !(window as any).plausible) return
;(window as any).plausible(eventName, { props })
}
trackPlausible('Signup', { plan: 'pro' })
trackPlausible('Checkout', { value: 99 })
<Script id="matomo" strategy="afterInteractive">
{`
var _paq = window._paq = window._paq || [];
_paq.push(['disableCookies']); // GDPR mode
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="https://matomo.example.com/";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '1']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
})();
`}
</Script>
disableCookies rend Matomo cookie-less → pas de consent banner nécessaire.
<Script id="clarity" strategy="afterInteractive">
{`
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "${process.env.NEXT_PUBLIC_CLARITY_ID}");
`}
</Script>
<Script id="hotjar" strategy="afterInteractive">
{`
(function(h,o,t,j,a,r){
h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
h._hjSettings={hjid:${process.env.NEXT_PUBLIC_HOTJAR_ID},hjsv:6};
a=o.getElementsByTagName('head')[0];
r=o.createElement('script');r.async=1;
r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
a.appendChild(r);
})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
`}
</Script>
IMPORTANT : Hotjar et Clarity enregistrent des session recordings → obligatoire d'avoir un consent banner explicite avant de les charger (données personnelles visibles).
// app/providers/PostHogProvider.tsx
'use client'
import posthog from 'posthog-js'
import { PostHogProvider as OriginalProvider } from 'posthog-js/react'
import { useEffect } from 'react'
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://eu.posthog.com',
capture_pageview: false, // on gère manuellement pour Next.js App Router
persistence: 'localStorage+cookie',
opt_out_capturing_by_default: true, // default deny GDPR
})
}
export function PostHogProvider({ children }: { children: React.ReactNode }) {
return <OriginalProvider client={posthog}>{children}</OriginalProvider>
}
// Identification user
posthog.identify(user.id, { email: user.email, plan: user.plan })
// Capture event
posthog.capture('feature_used', { feature: 'export_pdf' })
// Feature flag
const isNewUIEnabled = posthog.isFeatureEnabled('new-ui')
// app/components/ConsentBanner.tsx
'use client'
import { useEffect, useState } from 'react'
import { setConsent } from '@/lib/consent'
export function ConsentBanner() {
const [visible, setVisible] = useState(false)
useEffect(() => {
const stored = localStorage.getItem('consent')
if (!stored) setVisible(true)
}, [])
if (!visible) return null
return (
<div className="fixed bottom-0 left-0 right-0 bg-gray-900 p-4 text-white">
<p>
We use analytics cookies to improve your experience. See our{' '}
<a href="/privacy" className="underline">privacy policy</a>.
</p>
<div className="mt-2 flex gap-2">
<button
onClick={() => {
setConsent({ analytics: false, marketing: false, functional: true })
setVisible(false)
}}
className="rounded border border-white px-4 py-2"
>
Reject
</button>
<button
onClick={() => {
setConsent({ analytics: true, marketing: true, functional: true })
setVisible(false)
}}
className="rounded bg-white px-4 py-2 text-black"
>
Accept all
</button>
</div>
</div>
)
}
noun_verb en snake_caseBON :
product_viewedcart_item_addedcheckout_startedpayment_completedsubscription_cancelledMAUVAIS :
click (trop générique)PVWR-UPDATE (illisible)userClickedBuyButton (verbeux, pas consistant)Utiliser des clés standardisées sur tous les events :
const standardProps = {
currency: 'EUR',
value: 29.99,
item_id: 'SKU-123',
item_name: 'T-shirt Blue',
item_category: 'Apparel',
item_brand: 'MyBrand',
item_variant: 'M',
quantity: 1,
}
GA4 + PostHog + Plausible comprennent tous ce schéma.
https://example.com/?utm_source=newsletter&utm_medium=email&utm_campaign=summer_sale&utm_content=cta_button&utm_term=t-shirts
| Paramètre | Usage |
|---|---|
utm_source | Plateforme : google, facebook, newsletter, linkedin |
utm_medium | Canal : cpc, email, social, organic, affiliate |
utm_campaign | Campagne : spring_2026, black_friday, launch_v2 |
utm_content | Variant (A/B) : cta_top, cta_bottom |
utm_term | Keyword (pour SEM) : t-shirts, sneakers |
Règle canonique : lowercase, snake_case, et UTM strippés du URL canonical pour éviter les duplicates en SEO.
anonymize_ip: true sur GA4)SameSite=None; Secure en prodevent_label → violation GDPRanonymize_ip sur GA4 en EUseo-technical (dans ce plugin)sentry-* skills ou posthog-instrumentationshopify-expert (dans atum-cms-ecom) — utilise l'API Pixels Shopify