Google Analytics 4 (GA4) is an event-based analytics platform that replaced Universal Analytics. Unlike its predecessor, GA4 uses a flexible event model where everything is an event -- page views, clicks, scrolls, purchases, and custom actions. This flexibility is powerful but dangerous: without deliberate configuration, you end up with a property full of automatically collected noise and none of the business-specific events that actually matter. Enhanced Measurement captures some useful events automatically, but it also creates conflicts with custom tracking if you are not careful about what to enable and what to disable.
Why Proper Implementation Matters
The Enhanced Measurement Trap
GA4 enables "Enhanced Measurement" by default, which automatically tracks:
- Page views
- Scrolls (90% depth)
- Outbound clicks
- Site search
- Video engagement (YouTube embeds)
- File downloads
The problem: Enhanced Measurement events cannot carry custom parameters. If you need to track scroll depth at 25/50/75/100% (not just 90%), or need to capture the search query with product results count, you must disable the Enhanced Measurement version and implement your own. Running both creates duplicate events with different parameter structures.
Event Naming is Permanent
GA4 event names and parameter names are case-sensitive and cannot be renamed retroactively:
page_viewandPage_Vieware different events in GA4- Once custom dimensions are created from parameters, renaming the parameter creates a new dimension with zero historical data
- Google's recommended event names (like
purchase,add_to_cart,generate_lead) must match exactly for e-commerce reports to work
Data Retention is Limited
GA4 standard properties retain detailed event data for 2 months (14 months with Google Analytics 360). After that, only aggregated reports remain. This means:
- If your tracking is broken for the first 2 months, that detailed data is gone permanently
- BigQuery export is the only way to preserve raw event-level data indefinitely
- Export must be set up before data is needed -- it is not retroactive
Pre-Implementation Planning
Access and Permissions
Google Analytics Account:
- Sign in at
analytics.google.com - Verify you have Editor or Admin access to the property
- Admin access is required for data stream creation, BigQuery linking, and user management
Google Tag Manager (if using GTM):
- Verify Publish access on the GTM container
- Confirm the container is installed on all pages
- Coordinate with other GTM users to avoid tag conflicts
Required IDs:
| ID | Where to Find | Format |
|---|---|---|
| Measurement ID | GA4 > Admin > Data Streams > Web | G-XXXXXXXXXX |
| Stream ID | GA4 > Admin > Data Streams > Web | Numeric |
| API Secret | GA4 > Admin > Data Streams > Measurement Protocol | Alphanumeric string |
| GTM Container ID | GTM > Container overview | GTM-XXXXXXX |
Data Stream Configuration
- In GA4, go to Admin > Data Streams
- Click Add Stream > Web
- Enter your website URL and stream name
- Note the Measurement ID (G-XXXXXXXXXX)
- Configure Enhanced Measurement:
| Feature | Enable? | When to Disable |
|---|---|---|
| Page views | Yes (always) | Never -- always use this |
| Scrolls | Depends | Disable if implementing custom scroll tracking |
| Outbound clicks | Yes | Disable if tracking outbound clicks with custom parameters |
| Site search | Yes | Disable if implementing custom search tracking with more parameters |
| Video engagement | Yes | Only works for YouTube embeds; disable if tracking custom video players |
| File downloads | Yes | Disable if you need custom file download parameters |
Event Architecture
Design your event structure before implementation:
Automatically Collected Events (cannot disable):
first_visit,session_start,user_engagementpage_view(if Enhanced Measurement enabled)
Recommended Events (use Google's exact names for built-in reports):
- E-commerce:
view_item,add_to_cart,begin_checkout,purchase - Lead gen:
generate_lead,sign_up - Content:
search,share
Custom Events (your business-specific events):
- Name with
snake_case, max 40 characters - Up to 500 distinct event names per property
- Up to 25 custom parameters per event
Implementation Walkthrough
Step 1: Install GA4 Tag
Option A: Google Tag Manager (Recommended)
In GTM, create a new tag:
- Tag Type: Google Tag
- Tag ID: Your Measurement ID (G-XXXXXXXXXX)
- Trigger: All Pages
Configure consent defaults (if using consent management):
// In a Custom HTML tag that fires BEFORE the Google Tag, on All Pages
gtag('consent', 'default', {
'analytics_storage': 'denied',
'ad_storage': 'denied',
'ad_user_data': 'denied',
'ad_personalization': 'denied',
'region': ['EU', 'EEA', 'GB'] // Apply restrictions to EU users only
});
Option B: gtag.js Direct Installation
<!-- 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());
// Consent defaults (if applicable)
gtag('consent', 'default', {
'analytics_storage': 'granted',
'ad_storage': 'denied'
});
gtag('config', 'G-XXXXXXXXXX', {
'send_page_view': true,
'cookie_domain': 'auto',
'cookie_flags': 'SameSite=None;Secure'
});
</script>
Step 2: Configure Custom Events
E-commerce Events (must use exact Google names):
// View item (product page)
gtag('event', 'view_item', {
currency: 'USD',
value: 49.99,
items: [{
item_id: 'SKU-12345',
item_name: 'Premium Widget',
item_brand: 'Acme',
item_category: 'Widgets',
item_category2: 'Premium',
price: 49.99,
quantity: 1
}]
});
// Add to cart
gtag('event', 'add_to_cart', {
currency: 'USD',
value: 49.99,
items: [{
item_id: 'SKU-12345',
item_name: 'Premium Widget',
price: 49.99,
quantity: 1
}]
});
// Begin checkout
gtag('event', 'begin_checkout', {
currency: 'USD',
value: 129.98,
coupon: 'SAVE10',
items: [{
item_id: 'SKU-12345',
item_name: 'Premium Widget',
price: 49.99,
quantity: 1
}, {
item_id: 'SKU-67890',
item_name: 'Deluxe Gadget',
price: 79.99,
quantity: 1
}]
});
// Purchase (critical event)
gtag('event', 'purchase', {
transaction_id: 'ORD-2024-12345',
value: 129.98,
tax: 10.40,
shipping: 5.99,
currency: 'USD',
coupon: 'SAVE10',
items: [{
item_id: 'SKU-12345',
item_name: 'Premium Widget',
affiliation: 'Online Store',
item_brand: 'Acme',
item_category: 'Widgets',
price: 49.99,
quantity: 1
}, {
item_id: 'SKU-67890',
item_name: 'Deluxe Gadget',
affiliation: 'Online Store',
item_brand: 'Acme',
item_category: 'Gadgets',
price: 79.99,
quantity: 1
}]
});
Lead Generation Events:
// Form submission
gtag('event', 'generate_lead', {
value: 50.00,
currency: 'USD',
form_name: 'Contact Us',
form_id: 'contact-main'
});
// Sign up
gtag('event', 'sign_up', {
method: 'Google' // or 'Email', 'Apple', etc.
});
Custom Business Events:
// Feature usage (SaaS)
gtag('event', 'feature_used', {
feature_name: 'export_report',
feature_category: 'analytics',
export_format: 'csv'
});
// Content engagement
gtag('event', 'article_read', {
article_id: 'ART-123',
article_category: 'tutorials',
read_percentage: 100,
word_count: 2500
});
Step 3: GTM Data Layer Implementation
For GTM-based deployments, push structured data to the data layer:
// On all pages
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'page_data_ready',
'page_type': 'product', // home, category, product, cart, checkout, confirmation
'user_logged_in': true,
'user_id': 'USR-12345',
'user_type': 'returning'
});
// On product pages
dataLayer.push({
'event': 'view_item',
'ecommerce': {
'currency': 'USD',
'value': 49.99,
'items': [{
'item_id': 'SKU-12345',
'item_name': 'Premium Widget',
'item_brand': 'Acme',
'item_category': 'Widgets',
'price': 49.99,
'quantity': 1
}]
}
});
// On purchase confirmation
dataLayer.push({
'event': 'purchase',
'ecommerce': {
'transaction_id': 'ORD-2024-12345',
'value': 129.98,
'tax': 10.40,
'shipping': 5.99,
'currency': 'USD',
'items': [/* item array */]
}
});
GTM Tag Configuration:
- Create a GA4 Event tag for each custom event
- Use the data layer variable for event parameters
- Set triggers based on data layer
eventvalues
Step 4: Configure Custom Dimensions and Metrics
In GA4 Admin, register custom parameters as dimensions or metrics:
- Go to Admin > Custom Definitions
- Click Create Custom Dimension:
| Parameter | Scope | Description |
|---|---|---|
| page_type | Event | Type of page (product, category, home) |
| user_type | User | New vs. returning customer |
| form_name | Event | Name of submitted form |
| feature_name | Event | Product feature used |
| article_category | Event | Content category |
- Click Create Custom Metric:
| Parameter | Scope | Unit | Description |
|---|---|---|---|
| word_count | Event | Standard | Article word count |
| read_percentage | Event | Standard | Article read depth |
Limits:
- 50 custom dimensions (event scope)
- 25 custom dimensions (user scope)
- 50 custom metrics
- Plan allocations carefully; deleting a custom definition loses historical data
Step 5: Set Up Measurement Protocol (Server-Side)
For backend events (subscription renewals, CRM actions, offline conversions):
const MEASUREMENT_ID = 'G-XXXXXXXXXX';
const API_SECRET = 'your_api_secret';
async function sendServerEvent(clientId, userId, eventName, params) {
const payload = {
client_id: clientId, // Required: GA4 client ID (_ga cookie value)
user_id: userId, // Optional: your internal user ID
events: [{
name: eventName,
params: {
...params,
engagement_time_msec: 1 // Required for events to show in reports
}
}]
};
const url = `https://www.google-analytics.com/mp/collect?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`;
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(payload)
});
// Measurement Protocol returns 204 No Content on success
return response.status === 204;
}
// Example: Track subscription renewal
await sendServerEvent(
'GA1.1.123456789.1234567890', // client_id from _ga cookie
'USR-12345',
'subscription_renewed',
{
value: 99.00,
currency: 'USD',
plan_type: 'pro',
billing_period: 'annual'
}
);
Validation endpoint (use during testing):
https://www.google-analytics.com/debug/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_SECRET
This returns validation messages instead of actually recording events.
Step 6: Enable BigQuery Export
For raw event-level data preservation (highly recommended):
- In GA4, go to Admin > BigQuery Links
- Click Link
- Select your Google Cloud project
- Choose export frequency:
- Daily: Batch export of previous day's data
- Streaming: Near-real-time export (additional cost)
- Select data to export: Events, Users, or both
- BigQuery dataset is created automatically in format:
analytics_PROPERTY_ID
Cost consideration: BigQuery storage is cheap (~$0.02/GB/month), but query costs can add up. Partition tables by date and use WHERE _TABLE_SUFFIX filters.
Verification and QA
DebugView
- In GA4, go to Admin > DebugView
- Enable debug mode on your browser:
- GTM: Enable Preview mode
- gtag.js: Add
'debug_mode': trueto config - Chrome extension: Install "Google Analytics Debugger"
- Navigate through your site and verify:
- Events appear in real-time
- Event parameters are populated
- User properties are set
- E-commerce items array is structured correctly
Real-Time Report
- Go to Reports > Realtime
- Verify:
- Active users count increases when you visit
- Events appear with correct names
- Conversions fire on appropriate pages
GTM Preview Mode
- Enable Preview in GTM
- Navigate through conversion funnel
- For each page/action, check:
- Google Tag fires on All Pages
- Event tags fire on correct triggers
- Variable values are populated correctly
- No duplicate tags
Common Issues
| Issue | Cause | Fix |
|---|---|---|
| Events not in reports | Wrong Measurement ID | Check G-XXXXXXXXXX in tag configuration |
| Duplicate page_views | Enhanced Measurement + custom tracking | Disable Enhanced Measurement page views if using custom |
| E-commerce reports empty | Wrong event names or missing items array | Use exact Google event names: purchase, add_to_cart |
| Custom dimensions show "(not set)" | Parameter name mismatch | Verify parameter name in tag matches custom dimension registration |
| Cross-domain identity breaks | Missing linker configuration | Set up cross-domain measurement in data stream settings |
| Consent blocking all tracking | Default consent too restrictive | Verify consent defaults only apply to regulated regions |
| Measurement Protocol events missing | Missing engagement_time_msec |
Add engagement_time_msec: 1 to all server-side events |
Deployment Artifacts
- Measurement ID and API Secret: Central reference for all implementations
- Tracking plan: Events, parameters, and expected values
- GTM container documentation: Tags, triggers, variables, and version history
- Custom dimensions/metrics registry: Allocated dimensions with descriptions and owners
- Data layer specification: Expected data layer structure per page type
- BigQuery export configuration: Project, dataset, export frequency
- Consent mode configuration: Default settings, CMP integration, regional rules
- Environment matrix: Measurement IDs, GTM containers, and debug settings per environment
Linked Runbooks
- Install or Embed the Tag or SDK -- gtag.js and GTM installation
- Event Tracking -- Custom and recommended event implementation
- Data Layer Setup -- Data layer structure for GA4 events
- Cross-Domain Tracking -- Cross-domain measurement configuration
- Server-Side vs Client-Side -- Measurement Protocol and server-side GTM
Change Log and Owners
- Track who publishes GTM containers and who manages GA4 Admin settings
- Document custom dimension/metric allocations with owners
- Record Enhanced Measurement configuration changes
- Maintain BigQuery export status and query cost monitoring
- Review monthly: event volume, data quality, custom dimension utilization