Install Google Analytics 4 on Strapi Sites | OpsBlu Docs

Install Google Analytics 4 on Strapi Sites

How to install GA4 on Strapi-powered sites using Next.js, Gatsby, Nuxt, and other frontend frameworks.

Since Strapi is a headless CMS, Google Analytics 4 is installed on your frontend framework (Next.js, Gatsby, Nuxt.js, etc.), not in Strapi itself. This guide covers GA4 installation for the most common frontend frameworks used with Strapi.

Important: Frontend vs Backend

Remember:

  • Install GA4 in your frontend application (Next.js, Gatsby, etc.)
  • Do NOT try to install GA4 in Strapi admin panel
  • Strapi provides content via API; tracking happens on frontend
  • Use GTM for easier management across all frameworks

Before You Begin

  1. Create a GA4 Property

    • Go to Google Analytics
    • Create a new GA4 property
    • Note your Measurement ID (format: G-XXXXXXXXXX)
  2. Choose Your Implementation Method

    • Direct gtag.js: Simple, framework-specific installation
    • Google Tag Manager: Recommended for easier management
    • Third-party libraries: Framework-specific analytics packages

Method 1: Next.js + Strapi

Next.js is the most popular framework for Strapi-powered sites.

App Router (Next.js 13+)

1. Install GA4 Script Globally

Create a Google Analytics component:

// app/components/GoogleAnalytics.tsx
'use client';

import Script from 'next/script';

export default function GoogleAnalytics({ GA_MEASUREMENT_ID }: { GA_MEASUREMENT_ID: string }) {
  return (
    <>
      <Script
        strategy="afterInteractive"
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
      />
      <Script
        id="google-analytics"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${GA_MEASUREMENT_ID}', {
              page_path: window.location.pathname,
            });
          `,
        }}
      />
    </>
  );
}

2. Add to Root Layout

// app/layout.tsx
import GoogleAnalytics from '@/components/GoogleAnalytics';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <GoogleAnalytics GA_MEASUREMENT_ID={process.env.NEXT_PUBLIC_GA_ID!} />
      </body>
    </html>
  );
}

3. Track Route Changes

// app/components/PageViewTracker.tsx
'use client';

import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';

export default function PageViewTracker() {
  const pathname = usePathname();
  const searchParams = useSearchParams();

  useEffect(() => {
    if (pathname) {
      window.gtag('config', process.env.NEXT_PUBLIC_GA_ID!, {
        page_path: pathname + searchParams.toString(),
      });
    }
  }, [pathname, searchParams]);

  return null;
}

Add to layout:

// app/layout.tsx
import PageViewTracker from '@/components/PageViewTracker';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <GoogleAnalytics GA_MEASUREMENT_ID={process.env.NEXT_PUBLIC_GA_ID!} />
        <Suspense fallback={null}>
          <PageViewTracker />
        </Suspense>
      </body>
    </html>
  );
}

Pages Router (Next.js 12 and earlier)

1. Create Analytics Library

// lib/analytics.js
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;

// Log page views
export const pageview = (url) => {
  if (typeof window.gtag !== 'undefined') {
    window.gtag('config', GA_TRACKING_ID, {
      page_path: url,
    });
  }
};

// Log specific events
export const event = ({ action, category, label, value }) => {
  if (typeof window.gtag !== 'undefined') {
    window.gtag('event', action, {
      event_category: category,
      event_label: label,
      value: value,
    });
  }
};

2. Add GA4 to _app.js

// pages/_app.js
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import Script from 'next/script';
import * as gtag from '../lib/analytics';

function MyApp({ Component, pageProps }) {
  const router = useRouter();

  useEffect(() => {
    const handleRouteChange = (url) => {
      gtag.pageview(url);
    };
    router.events.on('routeChangeComplete', handleRouteChange);
    return () => {
      router.events.off('routeChangeComplete', handleRouteChange);
    };
  }, [router.events]);

  return (
    <>
      {/* Global Site Tag (gtag.js) - Google Analytics */}
      <Script
        strategy="afterInteractive"
        src={`https://www.googletagmanager.com/gtag/js?id=${gtag.GA_TRACKING_ID}`}
      />
      <Script
        id="gtag-init"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${gtag.GA_TRACKING_ID}', {
              page_path: window.location.pathname,
            });
          `,
        }}
      />
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

3. Track Strapi Content Views

// pages/articles/[slug].js
import { useEffect } from 'react';
import * as gtag from '@/lib/analytics';

export default function Article({ article }) {
  useEffect(() => {
    // Track article view with Strapi metadata
    gtag.event({
      action: 'view_content',
      category: 'Article',
      label: article.attributes.title,
      value: article.id,
    });
  }, [article]);

  return (
    <article>
      <h1>{article.attributes.title}</h1>
      {/* Article content */}
    </article>
  );
}

export async function getStaticProps({ params }) {
  const res = await fetch(`${process.env.STRAPI_API_URL}/api/articles?filters[slug][$eq]=${params.slug}&populate=*`);
  const { data } = await res.json();

  return {
    props: { article: data[0] },
    revalidate: 60,
  };
}

Environment Variables

# .env.local
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
STRAPI_API_URL=http://localhost:1337

Method 2: Gatsby + Strapi

Gatsby uses plugins for analytics integration.

Installation

npm install gatsby-plugin-google-gtag

Configuration

// gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-google-gtag`,
      options: {
        trackingIds: [
          process.env.GA_MEASUREMENT_ID, // Google Analytics / GA
        ],
        pluginConfig: {
          head: false, // Put script in <head>
          respectDNT: true, // Respect Do Not Track
          exclude: ['/preview/**', '/do-not-track/me/too/'],
          origin: 'https://www.googletagmanager.com',
          delayOnRouteUpdate: 0,
        },
      },
    },
    {
      resolve: 'gatsby-source-strapi',
      options: {
        apiURL: process.env.STRAPI_API_URL || 'http://localhost:1337',
        accessToken: process.env.STRAPI_TOKEN,
        collectionTypes: ['article', 'category', 'author'],
        singleTypes: ['homepage', 'about'],
      },
    },
  ],
};

Track Strapi Content

// src/templates/article.js
import React, { useEffect } from 'react';
import { graphql } from 'gatsby';

const ArticleTemplate = ({ data }) => {
  const article = data.strapiArticle;

  useEffect(() => {
    // Track article view
    if (typeof window !== 'undefined' && window.gtag) {
      window.gtag('event', 'view_content', {
        content_type: 'article',
        content_id: article.strapiId,
        content_title: article.title,
      });
    }
  }, [article]);

  return (
    <article>
      <h1>{article.title}</h1>
      {/* Article content */}
    </article>
  );
};

export const query = graphql`
  query($slug: String!) {
    strapiArticle(slug: { eq: $slug }) {
      strapiId
      title
      content
      publishedAt
    }
  }
`;

export default ArticleTemplate;

Environment Variables

# .env.production
GA_MEASUREMENT_ID=G-XXXXXXXXXX
STRAPI_API_URL=https://your-strapi.com
STRAPI_TOKEN=your-api-token

Method 3: Nuxt.js + Strapi

Nuxt uses modules for analytics integration.

Installation

npm install @nuxtjs/google-analytics
# or for Nuxt 3
npm install @nuxtjs/gtm

Configuration (Nuxt 2)

// nuxt.config.js
export default {
  modules: [
    '@nuxtjs/google-analytics',
  ],

  googleAnalytics: {
    id: process.env.GA_MEASUREMENT_ID,
    dev: false, // Disable in dev mode
  },

  publicRuntimeConfig: {
    strapiUrl: process.env.STRAPI_URL || 'http://localhost:1337',
  },
};

Configuration (Nuxt 3)

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/gtm'],

  gtm: {
    id: process.env.GA_MEASUREMENT_ID,
    enabled: true,
    debug: false,
  },

  runtimeConfig: {
    public: {
      strapiUrl: process.env.STRAPI_URL || 'http://localhost:1337',
    },
  },
});

Track Strapi Content

<!-- pages/articles/[slug].vue -->
<script setup>
const route = useRoute();
const { data: article } = await useFetch(`/api/articles/${route.params.slug}`);

onMounted(() => {
  // Track article view
  if (process.client && window.gtag) {
    window.gtag('event', 'view_content', {
      content_type: 'article',
      content_id: article.value.id,
      content_title: article.value.attributes.title,
    });
  }
});
</script>

<template>
  <article>
    <h1>{{ article.attributes.title }}</h1>
    <!-- Article content -->
  </article>
</template>

Method 4: React SPA + Strapi

For simple React single-page applications.

Installation

npm install react-ga4

Implementation

// src/analytics.js
import ReactGA from 'react-ga4';

export const initGA = () => {
  ReactGA.initialize(process.env.REACT_APP_GA_ID);
};

export const logPageView = () => {
  ReactGA.send({ hitType: 'pageview', page: window.location.pathname });
};

export const logEvent = (category, action, label) => {
  ReactGA.event({
    category: category,
    action: action,
    label: label,
  });
};
// src/App.js
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { initGA, logPageView } from './analytics';

function App() {
  const location = useLocation();

  useEffect(() => {
    initGA();
  }, []);

  useEffect(() => {
    logPageView();
  }, [location]);

  return (
    <div className="App">
      {/* Your app content */}
    </div>
  );
}

export default App;

Track Strapi Content

// src/components/Article.js
import { useEffect } from 'react';
import { logEvent } from '../analytics';

function Article({ article }) {
  useEffect(() => {
    logEvent('Content', 'View Article', article.attributes.title);
  }, [article]);

  return (
    <article>
      <h1>{article.attributes.title}</h1>
      {/* Article content */}
    </article>
  );
}

GTM provides the easiest management across all frameworks.

See Install Google Tag Manager on Strapi Sites for complete GTM implementation, which is recommended over direct GA4 installation.

Benefits:

  • Framework-agnostic implementation
  • Easier to update without code changes
  • Centralized tag management
  • Better for teams with non-technical marketers

SSR/SSG Considerations

Server-Side Rendering (SSR)

Problem: GA4 scripts should only run on client, not server.

Solution: Always check for window object:

// Only initialize on client
if (typeof window !== 'undefined') {
  initializeGA();
}

Static Site Generation (SSG)

Problem: Scripts run during build can cause issues.

Solution: Use dynamic imports or lazy loading:

// Next.js example
useEffect(() => {
  import('../lib/analytics').then((mod) => {
    mod.initGA();
  });
}, []);

Hybrid Rendering

For sites using both SSR and SSG:

// Detect environment
const isClient = typeof window !== 'undefined';
const isProduction = process.env.NODE_ENV === 'production';

if (isClient && isProduction) {
  initializeGA();
}

Verification & Testing

1. Check GA4 Realtime Reports

  • Open GA4 → ReportsRealtime
  • Navigate your Strapi-powered site
  • Verify events appear within 30 seconds

2. Use Browser Console

// Check if GA4 is loaded
console.log(window.gtag);
console.log(window.dataLayer);

// Test event manually
gtag('event', 'test_event', { test_parameter: 'test_value' });

3. Use GA4 DebugView

Enable debug mode:

gtag('config', 'G-XXXXXXXXXX', {
  debug_mode: true
});

Then check AdminDebugView in GA4.

4. Test Different Page Types

Test across your Strapi content structure:

  • Homepage (Single Type)
  • Blog posts (Collection Type)
  • Category pages (Collection Type)
  • Dynamic routes (SSR/ISR)
  • Static pages (SSG)

5. Verify Route Changes

For SPAs, ensure page views fire on navigation:

// Should fire on each route change
router.events.on('routeChangeComplete', (url) => {
  console.log('Route changed to:', url);
  gtag('config', GA_ID, { page_path: url });
});

Common Issues

GA4 Not Loading in Development

Solution: GA4 often disabled in dev mode. Either:

  • Test in production build
  • Remove dev environment check temporarily
  • Use GA4 debug mode

Duplicate Page Views

Cause: Both automatic and manual page view tracking.

Solution: Disable automatic tracking:

gtag('config', 'G-XXXXXXXXXX', {
  send_page_view: false
});

Events Not Firing on Client Navigation

Cause: SPA route changes not tracked.

Solution: Implement route change listener (shown in framework examples above).

SSR Hydration Errors

Cause: GA4 script running on server.

Solution: Use useEffect or onMounted to ensure client-only execution.

Troubleshooting

For detailed troubleshooting, see:

Strapi Plugin Ecosystem for Analytics

While GA4 is installed on your frontend, Strapi's plugin ecosystem can enhance your analytics setup.

Available Strapi Plugins

1. Strapi Plugin Analytics (Community)

Track content operations in Strapi admin panel:

npm install strapi-plugin-analytics

Configuration:

// config/plugins.js
module.exports = {
  analytics: {
    enabled: true,
    config: {
      providers: [
        {
          name: 'google-analytics',
          enabled: true,
          config: {
            trackingId: process.env.GA_MEASUREMENT_ID,
          },
        },
      ],
    },
  },
};

2. Strapi Plugin Sitemap

Auto-generate sitemaps to improve SEO and tracking:

npm install strapi-plugin-sitemap

Configuration:

// config/plugins.js
module.exports = {
  sitemap: {
    enabled: true,
    config: {
      autoGenerate: true,
      allowedFields: ['title', 'slug', 'publishedAt'],
      excludeDrafts: true,
    },
  },
};

3. Custom Analytics Plugin

Create a custom plugin to track content changes:

# Create plugin
cd plugins
npx create-strapi-plugin analytics-tracker

Plugin code:

// plugins/analytics-tracker/server/bootstrap.js
module.exports = ({ strapi }) => {
  strapi.db.lifecycles.subscribe({
    models: ['api::article.article'],

    async afterCreate(event) {
      const { result } = event;

      // Send to GA4 Measurement Protocol
      await strapi.plugin('analytics-tracker').service('tracker').trackEvent({
        name: 'content_created',
        params: {
          content_type: 'article',
          content_id: result.id,
          content_title: result.title,
        },
      });
    },

    async afterUpdate(event) {
      const { result } = event;

      await strapi.plugin('analytics-tracker').service('tracker').trackEvent({
        name: 'content_updated',
        params: {
          content_type: 'article',
          content_id: result.id,
        },
      });
    },
  });
};

Content Type Tracking and Data Layer Setup

Track specific Strapi content types with structured data.

Define Content Type Data Layer

Create a standardized data layer structure for your Strapi content:

// lib/strapiDataLayer.ts
interface StrapiContentData {
  id: number;
  contentType: string;
  title: string;
  slug: string;
  category?: string;
  author?: string;
  publishedAt?: string;
  tags?: string[];
}

export function buildContentDataLayer(
  strapiData: any,
  contentType: string
): StrapiContentData {
  const attributes = strapiData.attributes || strapiData;

  return {
    id: strapiData.id,
    contentType,
    title: attributes.title || attributes.name || 'Untitled',
    slug: attributes.slug,
    category: attributes.category?.data?.attributes?.name,
    author: attributes.author?.data?.attributes?.name,
    publishedAt: attributes.publishedAt,
    tags: attributes.tags?.data?.map((tag: any) => tag.attributes.name) || [],
  };
}

export function pushContentToDataLayer(contentData: StrapiContentData) {
  if (typeof window !== 'undefined' && window.dataLayer) {
    window.dataLayer.push({
      event: 'view_content',
      content: contentData,
    });
  }

  // Also track with gtag if available
  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('event', 'view_content', {
      content_type: contentData.contentType,
      content_id: contentData.id.toString(),
      item_id: contentData.slug,
      items: [{
        item_id: contentData.slug,
        item_name: contentData.title,
        item_category: contentData.category,
      }],
    });
  }
}

Track Different Content Types

Articles:

// components/ArticleView.tsx
'use client';

import { useEffect } from 'react';
import { buildContentDataLayer, pushContentToDataLayer } from '@/lib/strapiDataLayer';

export function ArticleView({ article }: { article: any }) {
  useEffect(() => {
    const contentData = buildContentDataLayer(article, 'article');
    pushContentToDataLayer(contentData);
  }, [article.id]);

  return (
    <article>
      <h1>{article.attributes.title}</h1>
      {/* Article content */}
    </article>
  );
}

Products (for e-commerce sites):

// components/ProductView.tsx
'use client';

import { useEffect } from 'react';

export function ProductView({ product }: { product: any }) {
  useEffect(() => {
    if (window.gtag) {
      window.gtag('event', 'view_item', {
        currency: 'USD',
        value: product.attributes.price,
        items: [{
          item_id: product.attributes.sku,
          item_name: product.attributes.name,
          item_category: product.attributes.category?.data?.attributes?.name,
          price: product.attributes.price,
        }],
      });
    }
  }, [product.id]);

  return <div>{/* Product UI */}</div>;
}

Categories/Collections:

// components/CategoryView.tsx
'use client';

export function CategoryView({ category, articles }: any) {
  useEffect(() => {
    if (window.gtag) {
      window.gtag('event', 'view_item_list', {
        item_list_id: category.id.toString(),
        item_list_name: category.attributes.name,
        items: articles.map((article: any, index: number) => ({
          item_id: article.attributes.slug,
          item_name: article.attributes.title,
          index,
        })),
      });
    }
  }, [category.id]);

  return <div>{/* Category UI */}</div>;
}

GraphQL Data Layer Structure

When using Strapi's GraphQL API:

// lib/graphqlDataLayer.ts
import { gql } from '@apollo/client';

export const ARTICLE_TRACKING_FRAGMENT = gql`
  fragment ArticleTracking on Article {
    id
    attributes {
      title
      slug
      publishedAt
      category {
        data {
          attributes {
            name
          }
        }
      }
      author {
        data {
          attributes {
            name
            email
          }
        }
      }
      tags {
        data {
          attributes {
            name
          }
        }
      }
    }
  }
`;

export const GET_ARTICLE_FOR_TRACKING = gql`
  ${ARTICLE_TRACKING_FRAGMENT}
  query GetArticle($slug: String!) {
    articles(filters: { slug: { eq: $slug } }) {
      data {
        ...ArticleTracking
      }
    }
  }
`;

Usage:

// pages/articles/[slug].tsx
import { useQuery } from '@apollo/client';
import { GET_ARTICLE_FOR_TRACKING } from '@/lib/graphqlDataLayer';

export function ArticlePage({ slug }: { slug: string }) {
  const { data } = useQuery(GET_ARTICLE_FOR_TRACKING, {
    variables: { slug },
  });

  useEffect(() => {
    if (data?.articles?.data?.[0]) {
      const article = data.articles.data[0];
      pushContentToDataLayer(buildContentDataLayer(article, 'article'));
    }
  }, [data]);

  // ... rest of component
}

Webhook-Based Event Tracking for Content Changes

Track content changes server-side using Strapi webhooks.

Set Up Strapi Webhooks

1. Configure Webhook in Strapi Admin

Go to SettingsWebhooksCreate new webhook

  • Name: GA4 Content Tracking
  • URL: https://your-site.com/api/webhook/strapi-tracking
  • Events: Select:
    • entry.create
    • entry.update
    • entry.publish
    • entry.unpublish
    • entry.delete

2. Create Webhook Handler (Next.js)

// app/api/webhook/strapi-tracking/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  try {
    const payload = await request.json();

    // Verify webhook signature (recommended)
    const signature = request.headers.get('x-strapi-signature');
    if (!verifySignature(signature, payload)) {
      return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
    }

    // Extract event data
    const { event, model, entry } = payload;

    // Send to GA4 Measurement Protocol
    await sendToGA4({
      name: mapStrapiEventToGA4(event),
      params: {
        content_type: model,
        content_id: entry.id.toString(),
        content_title: entry.title || entry.name,
        event_source: 'strapi_webhook',
        timestamp: new Date().toISOString(),
      },
    });

    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('Webhook error:', error);
    return NextResponse.json({ error: 'Webhook processing failed' }, { status: 500 });
  }
}

function mapStrapiEventToGA4(event: string): string {
  const eventMap: Record<string, string> = {
    'entry.create': 'content_created',
    'entry.update': 'content_updated',
    'entry.publish': 'content_published',
    'entry.unpublish': 'content_unpublished',
    'entry.delete': 'content_deleted',
  };

  return eventMap[event] || 'content_modified';
}

async function sendToGA4(event: { name: string; params: any }) {
  const measurementId = process.env.GA_MEASUREMENT_ID!;
  const apiSecret = process.env.GA_API_SECRET!;

  const response = await fetch(
    `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
    {
      method: 'POST',
      body: JSON.stringify({
        client_id: 'strapi-webhook',
        events: [event],
      }),
    }
  );

  if (!response.ok) {
    throw new Error(`GA4 API error: ${response.statusText}`);
  }

  return response;
}

function verifySignature(signature: string | null, payload: any): boolean {
  // Implement signature verification based on your security needs
  // Example using HMAC:
  const crypto = require('crypto');
  const secret = process.env.STRAPI_WEBHOOK_SECRET!;

  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  const expectedSignature = hmac.digest('hex');

  return signature === expectedSignature;
}

3. Configure Environment Variables

# .env.local
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GA_API_SECRET=your_api_secret_from_ga4
STRAPI_WEBHOOK_SECRET=your_webhook_secret

Get GA4 API Secret:

  1. Go to GA4 Admin → Data Streams
  2. Select your web stream
  3. Scroll to "Measurement Protocol API secrets"
  4. Click "Create" to generate a new secret

Advanced Webhook Tracking

Track Content Performance:

// app/api/webhook/content-performance/route.ts
export async function POST(request: NextRequest) {
  const { event, entry } = await request.json();

  if (event === 'entry.publish') {
    // Track when content goes live
    await sendToGA4({
      name: 'content_published',
      params: {
        content_type: entry.__component || 'article',
        content_id: entry.id.toString(),
        publish_date: new Date().toISOString(),
        author_id: entry.author?.id,
        category: entry.category?.name,
        word_count: countWords(entry.content),
      },
    });
  }

  return NextResponse.json({ success: true });
}

function countWords(content: string): number {
  return content?.split(/\s+/).length || 0;
}

Track Editorial Workflow:

// Track content through editorial stages
export async function POST(request: NextRequest) {
  const { event, entry } = await request.json();

  const workflowStages: Record<string, string> = {
    'entry.create': 'draft_created',
    'entry.update': 'draft_revised',
    'entry.publish': 'published',
    'entry.unpublish': 'unpublished',
  };

  await sendToGA4({
    name: 'editorial_workflow',
    params: {
      workflow_stage: workflowStages[event],
      content_id: entry.id.toString(),
      content_type: entry.__component,
      time_to_publish: calculateTimeToPublish(entry),
    },
  });

  return NextResponse.json({ success: true });
}

REST API Webhook Implementation

For non-Next.js setups:

// Node.js/Express example
const express = require('express');
const app = express();

app.post('/webhook/strapi-tracking', async (req, res) => {
  const { event, model, entry } = req.body;

  try {
    // Send to GA4
    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',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          client_id: 'strapi-server',
          events: [{
            name: event.replace('.', '_'),
            params: {
              content_type: model,
              content_id: entry.id,
            },
          }],
        }),
      }
    );

    res.json({ success: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: 'Failed to process webhook' });
  }
});

Webhook Security Best Practices

1. Verify Webhook Source

// Verify requests come from your Strapi instance
function verifyWebhookSource(request: NextRequest): boolean {
  const allowedIPs = process.env.STRAPI_SERVER_IPS?.split(',') || [];
  const clientIP = request.headers.get('x-forwarded-for')?.split(',')[0];

  return allowedIPs.includes(clientIP || '');
}

2. Use Webhook Signatures

// Add signature to Strapi webhook
// In Strapi: plugins/strapi-plugin-webhooks/server/services/webhook.js
const crypto = require('crypto');

function signPayload(payload) {
  const secret = process.env.WEBHOOK_SECRET;
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  return hmac.digest('hex');
}

3. Rate Limiting

// Implement rate limiting for webhook endpoints
import rateLimit from 'express-rate-limit';

const webhookLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // Limit each IP to 100 requests per windowMs
  message: 'Too many webhook requests',
});

app.use('/api/webhook', webhookLimiter);

Environment Variable Configuration

Comprehensive environment variable setup for all frameworks.

Next.js Environment Variables

# .env.local (Development)
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GTM_ID=GTM-XXXXXX
GA_API_SECRET=your_api_secret
STRAPI_API_URL=http://localhost:1337
STRAPI_TOKEN=your_strapi_api_token
STRAPI_WEBHOOK_SECRET=your_webhook_secret

# .env.production (Production)
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_GTM_ID=GTM-XXXXXX
GA_API_SECRET=your_production_api_secret
STRAPI_API_URL=https://api.yoursite.com
STRAPI_TOKEN=your_production_strapi_token
STRAPI_WEBHOOK_SECRET=your_production_webhook_secret

Access in code:

// Client-side (NEXT_PUBLIC_ prefix required)
const GA_ID = process.env.NEXT_PUBLIC_GA_ID;

// Server-side (no prefix needed)
const API_SECRET = process.env.GA_API_SECRET;

Gatsby Environment Variables

# .env.development
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GTM_ID=GTM-XXXXXX
STRAPI_API_URL=http://localhost:1337
STRAPI_TOKEN=your_token

# .env.production
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GTM_ID=GTM-XXXXXX
STRAPI_API_URL=https://api.yoursite.com
STRAPI_TOKEN=your_production_token

Load in gatsby-config.js:

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`,
});

module.exports = {
  plugins: [
    {
      resolve: 'gatsby-plugin-google-gtag',
      options: {
        trackingIds: [process.env.GA_MEASUREMENT_ID],
      },
    },
    {
      resolve: 'gatsby-source-strapi',
      options: {
        apiURL: process.env.STRAPI_API_URL,
        accessToken: process.env.STRAPI_TOKEN,
      },
    },
  ],
};

Nuxt Environment Variables

# .env
GA_MEASUREMENT_ID=G-XXXXXXXXXX
GTM_ID=GTM-XXXXXX
STRAPI_URL=http://localhost:1337
STRAPI_TOKEN=your_token

Access in nuxt.config.ts:

export default defineNuxtConfig({
  runtimeConfig: {
    // Private keys (server-side only)
    strapiToken: process.env.STRAPI_TOKEN,

    public: {
      // Public keys (client-side accessible)
      gaId: process.env.GA_MEASUREMENT_ID,
      gtmId: process.env.GTM_ID,
      strapiUrl: process.env.STRAPI_URL,
    },
  },
});

React/Vue SPA Environment Variables

# .env (React - must start with REACT_APP_)
REACT_APP_GA_ID=G-XXXXXXXXXX
REACT_APP_GTM_ID=GTM-XXXXXX
REACT_APP_STRAPI_URL=http://localhost:1337

# .env (Vite/Vue - must start with VITE_)
VITE_GA_ID=G-XXXXXXXXXX
VITE_GTM_ID=GTM-XXXXXX
VITE_STRAPI_URL=http://localhost:1337

Environment-Specific Configuration

Disable tracking in development:

// lib/analytics.ts
const isDevelopment = process.env.NODE_ENV === 'development';
const isProduction = process.env.NODE_ENV === 'production';

export function initializeAnalytics() {
  if (!isProduction) {
    console.log('Analytics disabled in development');
    return;
  }

  if (typeof window !== 'undefined' && window.gtag) {
    window.gtag('config', process.env.NEXT_PUBLIC_GA_ID!, {
      debug_mode: isDevelopment,
      send_page_view: true,
    });
  }
}

Staging environment configuration:

# .env.staging
NEXT_PUBLIC_GA_ID=G-STAGING-ID
NEXT_PUBLIC_GTM_ID=GTM-STAGING
STRAPI_API_URL=https://staging-api.yoursite.com
NODE_ENV=staging

Security Best Practices

1. Never expose API secrets client-side:

// BAD - Exposes secret to client
const apiSecret = process.env.NEXT_PUBLIC_GA_API_SECRET;

// GOOD - Server-side only
// app/api/track/route.ts
const apiSecret = process.env.GA_API_SECRET;

2. Use different credentials per environment:

# Development
STRAPI_TOKEN=dev_token_12345

# Staging
STRAPI_TOKEN=staging_token_67890

# Production
STRAPI_TOKEN=prod_token_abcde

3. Validate environment variables at build:

// lib/env.ts
const requiredEnvVars = [
  'NEXT_PUBLIC_GA_ID',
  'NEXT_PUBLIC_GTM_ID',
  'STRAPI_API_URL',
];

requiredEnvVars.forEach((envVar) => {
  if (!process.env[envVar]) {
    throw new Error(`Missing required environment variable: ${envVar}`);
  }
});

Next Steps

For general GA4 concepts, see Google Analytics 4 Guide.