Overview
Google Analytics 4 (GA4) represents a fundamental shift from Universal Analytics, built on an event-based data model designed for cross-platform tracking. GA4 uses machine learning to provide predictive insights and works seamlessly across web and mobile app properties.
Key Architecture:
- Data Streams: Individual sources (web, iOS, Android)
- Events: All interactions tracked as events with parameters
- User Properties: Persistent attributes describing users
- Measurement Protocol: Server-side event ingestion
- Firebase Integration: Unified mobile & web analytics
GA4 vs Universal Analytics:
- Event-based vs session-based model
- Enhanced measurement (scrolls, outbound clicks, site search) enabled by default
- Cross-platform identity resolution
- Predictive metrics and audiences
- BigQuery export available on free tier
Prerequisites
1. GA4 Property Setup
Before installation:
- GA4 Property Created: In Google Analytics, create a new GA4 property (not Universal Analytics)
- Measurement ID: Located under Admin → Data Streams → Your Stream (format:
G-XXXXXXXXXX) - API Secret: Required for Measurement Protocol (server-side tracking)
- Data Streams: Separate streams for web, iOS, Android
2. Google Tag Manager Setup (Recommended)
For maximum flexibility:
- GTM Container: Create container at tagmanager.google.com
- Container ID: Format
GTM-XXXXXXX - Workspace: Development workspace for testing
- Environment Variables: Configure for staging/production
3. Consent Management Planning
GA4 requires consent configuration:
- Consent Mode: Determine if using Google Consent Mode v2
- Default Consent State:
grantedordeniedforanalytics_storageandad_storage - Geolocation Rules: Different consent defaults by region (EU vs non-EU)
- CMP Integration: OneTrust, Cookiebot, or custom solution
4. Enhanced Measurement Decisions
Configure automatic event tracking:
- Page Views: Always tracked
- Scrolls: Track 90% scroll depth
- Outbound Clicks: Track external link clicks
- Site Search: Automatically detect search parameters
- Video Engagement: YouTube video tracking
- File Downloads: PDF, Excel, etc.
5. Data Layer Architecture
Plan your data structure:
window.dataLayer = window.dataLayer || [];
- Event Names: Follow GA4 naming conventions (lowercase, underscores)
- Parameters: Align with GA4 reserved names when possible
- E-commerce: Use GA4 recommended e-commerce schema
- User Properties: Maximum 25 custom user properties
Installation Methods
Method 1: Google Tag Manager (Recommended)
GTM provides the most flexible, maintainable GA4 implementation.
Step 1: Install GTM Container
Add GTM snippet to all pages:
<!DOCTYPE html>
<html>
<head>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
<!-- End Google Tag Manager -->
</head>
<body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<!-- Your content -->
</body>
</html>
Replace GTM-XXXXXXX with your container ID.
Step 2: Create GA4 Configuration Tag
- In GTM, go to Tags → New
- Tag Type: Google Analytics: GA4 Configuration
- Configuration:
- Measurement ID:
G-XXXXXXXXXX(or use GTM variable) - Send a page view event when this configuration loads: Checked (for initial setup)
- Measurement ID:
Step 3: Configure Consent Mode
Add consent defaults BEFORE the GA4 configuration:
- Create tag: Tag Type → Custom HTML
- Add consent initialization:
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Default consent to denied
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'wait_for_update': 500
});
// Region-specific defaults (EU requires opt-in)
gtag('consent', 'default', {
'analytics_storage': 'granted',
'ad_storage': 'granted',
'region': ['US', 'CA', 'AU']
});
</script>
- Trigger: Consent Initialization - All Pages
- Fire Priority: 100 (highest priority)
Step 4: Update Consent on User Choice
Create trigger and tag for consent updates:
<script>
gtag('consent', 'update', {
'analytics_storage': 'granted',
'ad_storage': 'granted',
'ad_user_data': 'granted',
'ad_personalization': 'granted'
});
</script>
Trigger: Custom Event → consent_granted
Step 5: Create GTM Variables
Create User-Defined Variables:
Measurement ID Variable:
- Variable Type: Lookup Table
- Input Variable:
{{Page Hostname}} - Output:
localhost→G-DEV123456staging.yourdomain.com→G-STAGING123yourdomain.com→G-PROD789
User ID Variable:
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
userId
Page Type Variable:
- Variable Type: Data Layer Variable
- Data Layer Variable Name:
pageType
Step 6: Track Custom Events
Create GA4 Event tags:
- Tag Type: Google Analytics: GA4 Event
- Configuration Tag: Select your GA4 Configuration tag
- Event Name:
{{Event Name}}(from Data Layer) - Event Parameters:
event_category:{{Event Category}}event_label:{{Event Label}}value:{{Event Value}}
Trigger: Custom Event (match your data layer events)
Step 7: E-commerce Tracking
For purchase tracking:
// Data Layer push for purchase
dataLayer.push({
event: 'purchase',
ecommerce: {
transaction_id: 'T12345',
value: 149.99,
currency: 'USD',
tax: 12.00,
shipping: 5.99,
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
item_category: 'Subscription',
item_variant: 'Annual',
price: 149.99,
quantity: 1
}
]
}
});
GA4 Event Tag:
- Event Name:
purchase - Send E-commerce data: Checked
- Source: Data Layer
- Trigger: Custom Event
purchase
Method 2: gtag.js Direct Implementation
For simpler sites without GTM.
Basic Installation
Add global site tag to <head>:
<!DOCTYPE html>
<html>
<head>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
</head>
<body>
<!-- Your content -->
</body>
</html>
With Consent Mode
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
// Set default consent
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'wait_for_update': 500
});
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<!-- After user consents -->
<script>
gtag('consent', 'update', {
'analytics_storage': 'granted',
'ad_storage': 'granted'
});
</script>
Environment-Specific Configuration
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
// Determine Measurement ID by environment
const measurementId = window.location.hostname === 'yourdomain.com'
? 'G-PROD789'
: window.location.hostname.includes('staging')
? 'G-STAGING123'
: 'G-DEV456';
gtag('config', measurementId, {
'send_page_view': true,
'anonymize_ip': true,
'cookie_flags': 'SameSite=None;Secure'
});
</script>
Tracking Custom Events
<script>
// Simple event
gtag('event', 'signup_button_click');
// Event with parameters
gtag('event', 'purchase', {
transaction_id: 'T12345',
value: 149.99,
currency: 'USD',
tax: 12.00,
shipping: 5.99,
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
price: 149.99,
quantity: 1
}
]
});
// Custom event
gtag('event', 'custom_event_name', {
event_category: 'engagement',
event_label: 'button_click',
value: 1
});
</script>
User Identification
// Set User ID
gtag('config', 'G-XXXXXXXXXX', {
'user_id': 'USER-12345'
});
// Set User Properties
gtag('set', 'user_properties', {
'membership_level': 'premium',
'signup_date': '2024-01-15'
});
Method 3: NPM Package (@analytics/google-analytics)
For modern JavaScript applications.
Installation
npm install analytics @analytics/google-analytics
# or
yarn add analytics @analytics/google-analytics
Basic Configuration
import Analytics from 'analytics'
import googleAnalytics from '@analytics/google-analytics'
const analytics = Analytics({
app: 'your-app-name',
plugins: [
googleAnalytics({
measurementIds: ['G-XXXXXXXXXX']
})
]
})
// Track page
analytics.page()
// Track event
analytics.track('button_clicked', {
category: 'engagement',
label: 'cta_button'
})
// Identify user
analytics.identify('user-123', {
email: 'user@example.com',
membership: 'premium'
})
Advanced Configuration
import Analytics from 'analytics'
import googleAnalytics from '@analytics/google-analytics'
const analytics = Analytics({
app: 'your-app',
plugins: [
googleAnalytics({
measurementIds: [
process.env.REACT_APP_GA_MEASUREMENT_ID
],
gtagConfig: {
'anonymize_ip': true,
'cookie_flags': 'SameSite=None;Secure',
'send_page_view': false // Manual page tracking
},
customDimensions: {
'dimension1': 'user_type',
'dimension2': 'page_type'
}
})
]
})
export default analytics
Framework Integrations
React
Using Custom Hook
// hooks/useAnalytics.js
import { useEffect } from 'react'
export function usePageTracking() {
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'page_view', {
page_path: window.location.pathname,
page_title: document.title
})
}
}, [])
}
// Component.jsx
import { usePageTracking } from './hooks/useAnalytics'
function MyPage() {
usePageTracking()
const handleClick = () => {
gtag('event', 'button_click', {
event_category: 'engagement',
event_label: 'signup_cta'
})
}
return <button Up</button>
}
React Router Integration
// App.js
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
function App() {
const location = useLocation()
useEffect(() => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'page_view', {
page_path: location.pathname + location.search,
page_title: document.title
})
}
}, [location])
return <YourRoutes />
}
React Context Provider
// context/AnalyticsContext.js
import React, { createContext, useContext } from 'react'
const AnalyticsContext = createContext()
export function AnalyticsProvider({ children, measurementId }) {
const trackEvent = (eventName, parameters) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', eventName, parameters)
}
}
const trackPage = (pagePath, pageTitle) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'page_view', {
page_path: pagePath,
page_title: pageTitle
})
}
}
const setUserId = (userId) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', measurementId, {
'user_id': userId
})
}
}
return (
<AnalyticsContext.Provider value={{ trackEvent, trackPage, setUserId }}>
{children}
</AnalyticsContext.Provider>
)
}
export function useAnalytics() {
return useContext(AnalyticsContext)
}
// Usage in component
import { useAnalytics } from './context/AnalyticsContext'
function Component() {
const { trackEvent } = useAnalytics()
const handlePurchase = () => {
trackEvent('purchase', {
transaction_id: 'T123',
value: 99.99,
currency: 'USD'
})
}
return <button Now</button>
}
Next.js
App Router (Next.js 13+)
// app/GoogleAnalytics.js
'use client'
import Script from 'next/script'
export default function GoogleAnalytics({ measurementId }) {
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${measurementId}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${measurementId}', {
page_path: window.location.pathname,
});
`}
</Script>
</>
)
}
// app/layout.js
import GoogleAnalytics from './GoogleAnalytics'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<GoogleAnalytics measurementId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID} />
{children}
</body>
</html>
)
}
Pages Router
// pages/_app.js
import Script from 'next/script'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
function MyApp({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
const handleRouteChange = (url) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
})
}
}
router.events.on('routeChangeComplete', handleRouteChange)
return () => {
router.events.off('routeChangeComplete', handleRouteChange)
}
}, [router.events])
return (
<>
<Script
src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
strategy="afterInteractive"
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${GA_MEASUREMENT_ID}', {
page_path: window.location.pathname,
});
`}
</Script>
<Component {...pageProps} />
</>
)
}
export default MyApp
Utility Functions
// utils/gtag.js
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
export const pageview = (url) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', GA_MEASUREMENT_ID, {
page_path: url,
})
}
}
export const event = ({ action, category, label, value }) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
})
}
}
Vue.js
Vue 3 Plugin
// plugins/analytics.js
export default {
install: (app, options) => {
app.config.globalProperties.$gtag = (...args) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag(...args)
}
}
app.provide('gtag', app.config.globalProperties.$gtag)
}
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import analyticsPlugin from './plugins/analytics'
const app = createApp(App)
app.use(analyticsPlugin)
app.mount('#app')
Vue Router Integration
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [/* your routes */]
})
router.afterEach((to, from) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', 'page_view', {
page_path: to.fullPath,
page_title: to.meta.title || document.title
})
}
})
export default router
Angular
Service Implementation
// services/analytics.service.ts
import { Injectable } from '@angular/core'
declare global {
interface Window {
gtag?: (...args: any[]) => void
dataLayer?: any[]
}
}
@Injectable({
providedIn: 'root'
})
export class AnalyticsService {
private measurementId: string
constructor() {
this.measurementId = environment.gaMeasurementId
}
init() {
const script1 = document.createElement('script')
script1.async = true
script1.src = `https://www.googletagmanager.com/gtag/js?id=${this.measurementId}`
document.head.appendChild(script1)
const script2 = document.createElement('script')
script2.innerHTML = `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${this.measurementId}');
`
document.head.appendChild(script2)
}
trackEvent(eventName: string, parameters?: any) {
if (window.gtag) {
window.gtag('event', eventName, parameters)
}
}
trackPage(pagePath: string, pageTitle: string) {
if (window.gtag) {
window.gtag('event', 'page_view', {
page_path: pagePath,
page_title: pageTitle
})
}
}
setUserId(userId: string) {
if (window.gtag) {
window.gtag('config', this.measurementId, {
'user_id': userId
})
}
}
}
Router Integration
// app.component.ts
import { Component, OnInit } from '@angular/core'
import { Router, NavigationEnd } from '@angular/router'
import { filter } from 'rxjs/operators'
import { AnalyticsService } from './services/analytics.service'
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
constructor(
private router: Router,
private analytics: AnalyticsService
) {}
ngOnInit() {
this.analytics.init()
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.analytics.trackPage(event.urlAfterRedirects, document.title)
})
}
}
Mobile SDK Installation
iOS (Firebase SDK)
Installation via CocoaPods
# Podfile
platform :ios, '13.0'
target 'YourApp' do
use_frameworks!
pod 'Firebase/Analytics'
pod 'Firebase/Core'
end
pod install
Swift Package Manager
- In Xcode: File → Add Packages
- Enter:
https://github.com/firebase/firebase-ios-sdk - Select
FirebaseAnalytics
Configuration
// AppDelegate.swift
import UIKit
import Firebase
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure Firebase
FirebaseApp.configure()
// Enable debug mode
#if DEBUG
Analytics.setAnalyticsCollectionEnabled(true)
#endif
return true
}
}
Tracking Events
import Firebase
// Log custom event
Analytics.logEvent("button_tap", parameters: [
"button_name": "signup_cta",
"screen_name": "home"
])
// Log screen view
Analytics.logEvent(AnalyticsEventScreenView, parameters: [
AnalyticsParameterScreenName: "Home",
AnalyticsParameterScreenClass: "HomeViewController"
])
// Log purchase
Analytics.logEvent(AnalyticsEventPurchase, parameters: [
AnalyticsParameterTransactionID: "T12345",
AnalyticsParameterValue: 99.99,
AnalyticsParameterCurrency: "USD",
AnalyticsParameterItems: [
[
AnalyticsParameterItemID: "SKU-001",
AnalyticsParameterItemName: "Premium Subscription",
AnalyticsParameterPrice: 99.99,
AnalyticsParameterQuantity: 1
]
]
])
// Set User ID
Analytics.setUserID("user-123")
// Set User Property
Analytics.setUserProperty("premium", forName: "membership_level")
Android (Firebase SDK)
Installation
// build.gradle (project level)
buildscript {
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}
// build.gradle (app level)
plugins {
id 'com.google.gms.google-services'
}
dependencies {
implementation platform('com.google.firebase:firebase-bom:32.0.0')
implementation 'com.google.firebase:firebase-analytics'
}
Configuration
Add google-services.json to app/ directory.
// Application.kt
import android.app.Application
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase
class MyApplication : Application() {
private lateinit var firebaseAnalytics: FirebaseAnalytics
override fun onCreate() {
super.onCreate()
// Initialize Firebase Analytics
firebaseAnalytics = Firebase.analytics
// Enable debug mode
firebaseAnalytics.setAnalyticsCollectionEnabled(true)
}
}
Tracking Events
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.analytics.ktx.logEvent
import com.google.firebase.ktx.Firebase
val firebaseAnalytics = Firebase.analytics
// Log custom event
firebaseAnalytics.logEvent("button_tap") {
param("button_name", "signup_cta")
param("screen_name", "home")
}
// Log screen view
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
param(FirebaseAnalytics.Param.SCREEN_NAME, "Home")
param(FirebaseAnalytics.Param.SCREEN_CLASS, "HomeActivity")
}
// Log purchase
firebaseAnalytics.logEvent(FirebaseAnalytics.Event.PURCHASE) {
param(FirebaseAnalytics.Param.TRANSACTION_ID, "T12345")
param(FirebaseAnalytics.Param.VALUE, 99.99)
param(FirebaseAnalytics.Param.CURRENCY, "USD")
param(FirebaseAnalytics.Param.ITEMS, arrayOf(
Bundle().apply {
putString(FirebaseAnalytics.Param.ITEM_ID, "SKU-001")
putString(FirebaseAnalytics.Param.ITEM_NAME, "Premium Subscription")
putDouble(FirebaseAnalytics.Param.PRICE, 99.99)
putLong(FirebaseAnalytics.Param.QUANTITY, 1)
}
))
}
// Set User ID
firebaseAnalytics.setUserId("user-123")
// Set User Property
firebaseAnalytics.setUserProperty("membership_level", "premium")
Server-Side Tracking (Measurement Protocol)
Node.js Implementation
const axios = require('axios')
const GA_MEASUREMENT_ID = 'G-XXXXXXXXXX'
const GA_API_SECRET = 'your-api-secret'
async function sendGA4Event(clientId, events) {
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`
const payload = {
client_id: clientId,
events: events
}
try {
const response = await axios.post(url, payload)
return response.status === 204
} catch (error) {
console.error('GA4 tracking error:', error)
return false
}
}
// Track purchase
await sendGA4Event('client-123', [
{
name: 'purchase',
params: {
transaction_id: 'T12345',
value: 149.99,
currency: 'USD',
tax: 12.00,
shipping: 5.99,
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
price: 149.99,
quantity: 1
}
]
}
}
])
// Track custom event
await sendGA4Event('client-123', [
{
name: 'server_action',
params: {
action_type: 'email_sent',
email_template: 'welcome',
user_id: 'user-123'
}
}
])
Python Implementation
import requests
import json
GA_MEASUREMENT_ID = 'G-XXXXXXXXXX'
GA_API_SECRET = 'your-api-secret'
def send_ga4_event(client_id, events):
url = f'https://www.google-analytics.com/mp/collect?measurement_id={GA_MEASUREMENT_ID}&api_secret={GA_API_SECRET}'
payload = {
'client_id': client_id,
'events': events
}
try:
response = requests.post(url, json=payload)
return response.status_code == 204
except Exception as e:
print(f'GA4 tracking error: {e}')
return False
# Track purchase
send_ga4_event('client-123', [
{
'name': 'purchase',
'params': {
'transaction_id': 'T12345',
'value': 149.99,
'currency': 'USD',
'items': [
{
'item_id': 'SKU-001',
'item_name': 'Premium Subscription',
'price': 99.99,
'quantity': 1
}
]
}
}
])
Validation Endpoint
Use the validation server to test events:
// Validation URL (doesn't send to GA4)
const validationUrl = `https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`
const response = await axios.post(validationUrl, payload)
console.log('Validation response:', response.data)
E-commerce Implementation
GA4 E-commerce Events
// View item list
gtag('event', 'view_item_list', {
item_list_id: 'category_page',
item_list_name: 'Premium Products',
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
item_category: 'Subscriptions',
price: 99.99,
quantity: 1,
index: 0
}
]
})
// Select item
gtag('event', 'select_item', {
item_list_id: 'category_page',
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
price: 99.99
}
]
})
// View item details
gtag('event', 'view_item', {
currency: 'USD',
value: 99.99,
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
item_category: 'Subscriptions',
price: 99.99,
quantity: 1
}
]
})
// Add to cart
gtag('event', 'add_to_cart', {
currency: 'USD',
value: 99.99,
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
price: 99.99,
quantity: 1
}
]
})
// Begin checkout
gtag('event', 'begin_checkout', {
currency: 'USD',
value: 99.99,
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
price: 99.99,
quantity: 1
}
]
})
// Add payment info
gtag('event', 'add_payment_info', {
currency: 'USD',
value: 99.99,
payment_type: 'Credit Card',
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
price: 99.99,
quantity: 1
}
]
})
// Purchase
gtag('event', 'purchase', {
transaction_id: 'T12345',
value: 149.99,
currency: 'USD',
tax: 12.00,
shipping: 5.99,
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
item_category: 'Subscriptions',
price: 99.99,
quantity: 1
}
]
})
// Refund
gtag('event', 'refund', {
transaction_id: 'T12345',
value: 149.99,
currency: 'USD',
items: [
{
item_id: 'SKU-001',
item_name: 'Premium Subscription',
price: 99.99,
quantity: 1
}
]
})
Verification & Debugging
1. DebugView (Real-Time)
Enable DebugView:
Web:
gtag('config', 'G-XXXXXXXXXX', {
'debug_mode': true
})
iOS:
# Enable debug mode
-FIRDebugEnabled
Android:
# Enable debug logging
adb shell setprop debug.firebase.analytics.app com.your.package
Access DebugView: GA4 Property → Admin → DebugView
2. Google Tag Assistant
- Install Tag Assistant Chrome Extension
- Navigate to your site
- Click extension icon
- Enable recording
- Interact with your site
- Review tag firing and data
3. Network Inspector
Check network requests:
gtag.js:
- Script:
https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX - Events:
https://www.google-analytics.com/g/collect
GTM:
- Container:
https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXXX - Events:
https://www.google-analytics.com/g/collect
4. Realtime Reports
View live data:
- Navigate to Reports → Realtime
- Trigger events on your site
- Verify events appear within 30 seconds
- Check event parameters are correct
Troubleshooting
Events Not Showing
Problem: Events not appearing in GA4
Solutions:
- Check Measurement ID is correct
- Verify data stream is active
- Ensure events sent with valid parameters
- Check browser console for errors
- Use DebugView to see event validation errors
- Verify ad blockers aren't blocking GA4
Consent Mode Issues
Problem: Consent not updating properly
Solutions:
- Ensure consent set BEFORE gtag loads
- Verify consent update fires on user action
- Check consent parameters match GA4 requirements
- Test in incognito to verify cookie behavior
User ID Not Persisting
Problem: User ID lost across sessions
Solutions:
- Call
gtag('set', {'user_id': 'USER_ID'})on every page - Ensure User-ID set before pageview event
- Check cookie settings allow persistence
- Verify User-ID feature enabled in GA4
Duplicate Events
Problem: Same event tracked multiple times
Solutions:
- Check GTM tags not firing multiple times
- Verify only one GA4 configuration tag active
- Don't mix gtag.js and GTM on same page
- Check for duplicate script tags
Security Best Practices
1. Measurement ID Protection
// Good - Use environment variables
const MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
// Acceptable - Client-side exposure is expected
// (Measurement IDs are designed to be public)
gtag('config', 'G-XXXXXXXXXX')
2. API Secret Protection
// Good - Server-side only
const GA_API_SECRET = process.env.GA_API_SECRET
// Bad - NEVER expose API secret client-side
// API secrets must remain server-side only
3. PII Protection
// Bad - Sending PII
gtag('event', 'signup', {
email: 'user@example.com',
name: 'John Doe'
})
// Good - Hash or omit PII
gtag('event', 'signup', {
user_type: 'premium',
signup_method: 'google'
})
4. Content Security Policy
<meta http-equiv="Content-Security-Policy"
content="script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com 'unsafe-inline';
connect-src 'self' https://www.google-analytics.com https://region1.google-analytics.com;
img-src 'self' https://www.google-analytics.com https://www.googletagmanager.com data:;">
Validation Checklist
Before production:
- Correct Measurement ID per environment
- Consent Mode configured and tested
- Enhanced Measurement settings verified
- Custom events appear in DebugView
- E-commerce events structured correctly
- User-ID implementation tested
- Cross-domain tracking works if needed
- Server-side events validated
- No PII sent in event parameters
- Page load performance acceptable
- GTM container published to production
- All team members have GA4 access
- Data retention settings configured
- Filters configured (internal traffic, etc.)
Configuration Recommendations
These are the key decisions to lock down before going to production:
Consent Management: Use Google's Consent Mode v2 with your CMP (OneTrust, Cookiebot, or CookieYes). Set the default consent state to denied for analytics_storage and ad_storage in regions requiring consent (EU/EEA, UK). Consent mode still collects cookieless pings for behavioral modeling when consent is denied.
Enhanced Measurement: Enable all Enhanced Measurement events in GA4 Admin → Data Streams unless you have a specific reason not to. File download tracking captures .pdf, .docx, .xlsx, .csv, .zip by default — add custom extensions in the stream settings if needed. Disable scroll tracking only if you implement custom scroll depth (25/50/75/100%) via GTM.
E-commerce: Implement the full funnel (view_item → add_to_cart → begin_checkout → purchase) for meaningful funnel analysis. Track view_item_list and select_item only if product list click-through analysis matters for your merchandising team.
User Identification: Use User-ID when you have authenticated users — set it server-side at login with gtag('config', 'G-XXX', { user_id: 'USER_ID' }). Client-ID handles anonymous users automatically. Cross-device tracking requires User-ID; Client-ID is device-scoped.
Data Quality: Exclude internal traffic using GA4's built-in IP filter (Admin → Data Streams → Configure Tag Settings → Define Internal Traffic). Create a data filter in Admin → Data Settings → Data Filters set to "active" for the internal traffic definition.