Key Features:
✅ Compares yesterday’s metrics vs 7-day average
✅ Detects anomalies (sessions, users, events, conversions)
✅ Sends Slack or Email alerts when issues found
✅ Configurable thresholds (20% warning, 40% critical)
✅ Detects tracking failures (zero events)
✅ Daily summary even when everything is OK
📋 What It Monitors
Metric | What It Checks | Alert If |
|---|---|---|
Sessions | Daily session count | ±20% from 7-day avg |
Users | Daily user count | ±20% from 7-day avg |
Events | Total event volume | ±20% from 7-day avg OR zero events |
Conversions | Key event conversions | ±20% from 7-day avg |
Example Alert Scenarios:
Sessions drop 45% → CRITICAL alert
Conversions up 25% → WARNING (investigate spike)
Zero events tracked → CRITICAL (tracking broken)
Events down 60% → WARNING (possible tracking issue)
🚀 Setup Instructions
Step 1: Prerequisites
You need:
[ ] n8n instance (cloud or self-hosted)
[ ] GA4 property with data
[ ] Google Analytics API access
[ ] Slack workspace (or email only)
Step 2: Import Workflow
Method 1: Direct Import
Copy the entire
n8n_GA4_Data_Quality_Workflow.jsonfileIn n8n: Click “+” → “Import from File”
Paste JSON content
Click “Import”
Method 2: Manual Setup See “Manual Build Instructions” section below
Step 3: Set Up Google Analytics Connection
Create OAuth2 Credentials:
Go to Google Cloud Console
Create new project (or select existing)
Enable “Google Analytics Data API”
Create OAuth 2.0 credentials:
Application type: Web application
Authorized redirect URIs:
https://your-n8n-instance.com/rest/oauth2-credential/callback
Copy Client ID and Client Secret
In n8n:
Click on “Get Yesterday’s Data” node
Click “Create New Credential”
Select “Google Analytics OAuth2”
Paste Client ID and Client Secret
Click “Connect my account”
Authorize access
Step 4: Set Environment Variables
In n8n, set these environment variables:
GA4_PROPERTY_ID=123456789 # Your GA4 property IDSLACK_CHANNEL_ID=C01234567 # Your Slack channel [email protected]
ALERT_TO_EMAIL=[email protected]
How to find GA4 Property ID:
GA4 → Admin → Property Settings
Copy “Property ID” (format: 123456789)
How to find Slack Channel ID:
Slack → Right-click channel → “View channel details”
Scroll to bottom → Copy Channel ID
Step 5: Set Up Email
If using Gmail:
Enable 2FA on Google account
Create App Password: myaccount.google.com/apppasswords
In n8n SMTP settings:
Host: smtp.gmail.com
Port: 587
User: [email protected]
Password: [app password]
If using other provider:
Use their SMTP settings
Step 6: Set Up Slack Connection(Optional)
Create Slack App:
Go to api.slack.com/apps
Click “Create New App” → “From scratch”
Name: “GA4 Monitor”
Select your workspace
OAuth & Permissions → Add scopes:
chat:writechat:write.public
Install to workspace
Copy “Bot User OAuth Token”
In n8n:
Click on “Send Slack Alert” node
Click “Create New Credential”
Select “Slack API”
Paste Bot Token
Test connection
Step 7: Configure Alert Thresholds
In the “Calculate Variance & Detect Issues” node, adjust these values:
const alertThreshold = 20; // Warning if variance > 20%const criticalThreshold = 40; // Critical if variance > 40%
Recommended thresholds by business type:
Business Type | Warning | Critical | Reason |
|---|---|---|---|
E-commerce | 15% | 30% | High daily variance |
SaaS | 20% | 40% | Stable traffic patterns |
B2B | 25% | 50% | Low daily volume |
Media/Blog | 30% | 60% | High content variance |
Step 8: Test the Workflow
Test Run:
Click “Execute Workflow” button
Check each node completes successfully
Verify Slack/Email received
Expected Test Output:
✅ Yesterday’s data fetched
✅ 7-day average calculated
✅ Variance computed
✅ Alert sent (if issues found) OR OK status sent
Step 9: Schedule It
The workflow is set to run daily at 9:00 AM by default.
To change schedule:
Click “Schedule Daily 9AM” node
Modify cron expression:
Daily 9 AM:
0 9 * * *Daily 8 AM:
0 8 * * *Twice daily (9 AM, 5 PM):
0 9,17 * * *Hourly:
0 * * * *
📊 Sample Alert Examples
⚠️ WARNING Alert:
🚨 GA4 Data Quality Alert
Date: 2025-12-11
Issues Found: 2
Critical Issues: 0
Issue Details:
⚠️ Sessions
Yesterday: 2,431
7-Day Avg: 2,950
Variance: -17.6%
⚠️ Conversions
Yesterday: 45
7-Day Avg: 56
Variance: -19.6%
Full Metrics Summary:
• Sessions: 2,431 (-17.6%)
• Users: 1,890 (-12.3%)
• Events: 18,540 (-8.2%)
• Conversions: 45 (-19.6%)🚨 GA4 Data Quality Alert Date: 2025-12-11 Issues Found: 2 Critical Issues: 0 Issue Details: ⚠️ Sessions Yesterday: 2,431 7-Day Avg: 2,950 Variance: -17.6% ⚠️ Conversions Yesterday: 45 7-Day Avg: 56 Variance: -19.6% Full Metrics Summary: • Sessions: 2,431 (-17.6%) • Users: 1,890 (-12.3%) • Events: 18,540 (-8.2%) • Conversions: 45 (-19.6%)
🔴 CRITICAL Alert:
🚨 GA4 Data Quality Alert
Date: 2025-12-11
Issues Found: 1
Critical Issues: 1
Issue Details:
🔴 Event Tracking
Yesterday: 0
7-Day Avg: 20,150
Variance: -100%
Note: No events tracked yesterday - possible tracking failure!
Full Metrics Summary:
• Sessions: 0 (-100%)
• Users: 0 (-100%)
• Events: 0 (-100%)
• Conversions: 0 (-100%)🚨 GA4 Data Quality Alert Date: 2025-12-11 Issues Found: 1 Critical Issues: 1 Issue Details: 🔴 Event Tracking Yesterday: 0 7-Day Avg: 20,150 Variance: -100% Note: No events tracked yesterday - possible tracking failure! Full Metrics Summary: • Sessions: 0 (-100%) • Users: 0 (-100%) • Events: 0 (-100%) • Conversions: 0 (-100%)
✅ No Issues:
✅ GA4 Daily Report - No Issues
Date: 2025-12-11
Metrics Summary:
• Sessions: 2,980 (+1.0%)
• Users: 2,340 (+2.1%)
• Events: 22,150 (-3.2%)
• Conversions: 58 (+3.6%)
All metrics within normal range ✓✅ GA4 Daily Report - No Issues Date: 2025-12-11 Metrics Summary: • Sessions: 2,980 (+1.0%) • Users: 2,340 (+2.1%) • Events: 22,150 (-3.2%) • Conversions: 58 (+3.6%) All metrics within normal range ✓
🔧 Customization Options
Add More Metrics
In “Get Yesterday’s Data” and “Get 7-Day Average” nodes, add metrics:
"metrics": [
"sessions", "totalUsers", "eventCount", "conversions", "averageSessionDuration", // Add this "bounceRate", // Add this "ecommercePurchases" // Add this (e-commerce only)]
Then update the variance calculation code accordingly.
Add Revenue Tracking (E-commerce)
Add this to the metrics array:
"totalRevenue"
Then in variance calculation, add:
const yesterdayRevenue = parseFloat(yesterday.totalRevenue);const avg7Revenue = parseFloat(avg7day.totalRevenue) / 7;const revenueVariance = ((yesterdayRevenue - avg7Revenue) / avg7Revenue) * 100;if (Math.abs(revenueVariance) > alertThreshold) {
issues.push({
severity: Math.abs(revenueVariance) > criticalThreshold ? 'CRITICAL' : 'WARNING', metric: 'Revenue', yesterday: '$' + yesterdayRevenue.toLocaleString(), average: '$' + Math.round(avg7Revenue).toLocaleString(), variance: revenueVariance.toFixed(2) + '%' });}
Change Comparison Period
Current: Yesterday vs 7-day average
To compare vs same day last week:
Change “Get 7-Day Average” date range to:
"startDate": "8daysAgo","endDate": "8daysAgo"
To compare vs last month same day:
"startDate": "1monthAgo","endDate": "1monthAgo"
Add Google Sheets Logging
Add a new node after “Calculate Variance & Detect Issues”:
Add “Google Sheets” node
Operation: “Append”
Spreadsheet: Your tracking sheet
Map fields:
Date:
={{ $json.summary.date }}Sessions:
={{ $json.metrics.sessions.yesterday }}Session Variance:
={{ $json.metrics.sessions.variance }}Issues:
={{ $json.summary.issueCount }}
This creates a historical log of all checks.
🐛 Troubleshooting
Error: “Property ID not found”
Solution: Double-check GA4_PROPERTY_ID environment variable matches your property ID exactly.
Error: “Insufficient permissions”
Solution: Make sure Google Analytics OAuth has “Read & Analyze” permissions.
No data returned
Solution:
Check property has data for yesterday
Verify date range format is correct
Test in GA4 UI first with same date range
Alerts not sending to Slack
Solution:
Verify Slack channel ID is correct
Check bot is added to channel
Test Slack credential connection
Too many false positives
Solution:
Increase threshold (try 30% for warning, 50% for critical)
Switch to “same day last week” comparison instead of 7-day average
Exclude weekends if your traffic varies significantly
Workflow errors on weekends
Solution: Add a condition to skip weekends:
const today = new Date();const dayOfWeek = today.getDay();// Skip if Saturday (6) or Sunday (0)if (dayOfWeek === 0 || dayOfWeek === 6) {
return { skipped: true, reason: 'Weekend' };}
📈 Advanced: Multi-Property Monitoring
To monitor multiple GA4 properties:
Option 1: Duplicate workflow for each property
Clone entire workflow
Change
GA4_PROPERTY_IDfor eachDifferent Slack channels per property
Option 2: Loop through multiple properties
Add “Code” node at start with property IDs array
Add “Split In Batches” node
Use
={{ $json.propertyId }}in GA4 nodesInclude property name in alerts
📝 Maintenance Checklist
Weekly:
[ ] Review alerts for patterns
[ ] Adjust thresholds if too many false positives
[ ] Check workflow execution history
Monthly:
[ ] Review Google Analytics API quota usage
[ ] Update alert recipient list if needed
[ ] Test workflow still working correctly
Quarterly:
[ ] Review what metrics to monitor
[ ] Consider adding new metrics
[ ] Optimize alert thresholds based on historical data
💡 Pro Tips
Start with higher thresholds (30%/50%) and tighten over time as you understand your baseline variance.
Create separate workflows for weekday vs weekend monitoring (different thresholds).
Add a “grace period” - Don’t alert on first occurrence, only if issue persists 2+ days.
Log to Google Sheets for historical trending and pattern analysis.
Set up PagerDuty instead of Slack for critical issues (24/7 on-call).
Monitor specific event names separately (e.g., purchase events, sign-up events).
Compare hour-by-hour instead of daily if you need real-time monitoring.
Full JSON Workflow:
{
"name": "GA4 Data Quality Monitor",
"nodes": [
{
"parameters": {
"mode": "combine",
"combineBy": "combineAll",
"options": {}
},
"id": "2150b2c3-a7a4-43b6-9e51-35469248f8af",
"name": "Merge Both Datasets",
"type": "n8n-nodes-base.merge",
"typeVersion": 3,
"position": [
144,
896
]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\\n\\nconst formatNumber = (num) => num.toString().replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g, ',');\\n\\nconst getChangeColor = (change) => {\\n const value = parseFloat(change);\\n if (value > 0) return '#10b981';\\n if (value < 0) return '#ef4444';\\n return '#6b7280';\\n};\\n\\nconst getChangeIcon = (change) => {\\n const value = parseFloat(change);\\n if (value > 0) return '📈';\\n if (value < 0) return '📉';\\n return '➡️';\\n};\\n\\n// Generate HTML email for ALERT\\nconst html = `\\n<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <style>\\n body { \\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; \\n line-height: 1.6; \\n color: #333; \\n max-width: 650px; \\n margin: 0 auto; \\n padding: 20px; \\n background-color: #f5f5f5; \\n }\\n .container { \\n background-color: white; \\n border-radius: 12px; \\n padding: 35px; \\n box-shadow: 0 4px 12px rgba(0,0,0,0.1); \\n }\\n .header { \\n background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);\\n color: white; \\n padding: 25px; \\n border-radius: 8px; \\n margin-bottom: 30px;\\n text-align: center;\\n }\\n .header h1 { \\n margin: 0; \\n font-size: 26px; \\n font-weight: 700;\\n }\\n .header p { \\n margin: 8px 0 0 0; \\n opacity: 0.95; \\n font-size: 15px;\\n }\\n .badge { \\n display: inline-block; \\n padding: 5px 14px; \\n border-radius: 15px; \\n font-size: 12px; \\n font-weight: 600; \\n margin-top: 8px;\\n }\\n .badge-critical { \\n background: #fff; \\n color: #dc3545; \\n }\\n .badge-warning { \\n background: #fff3cd; \\n color: #856404; \\n }\\n .summary-box { \\n background: #fff3cd; \\n border-left: 4px solid #ffc107; \\n padding: 18px; \\n margin: 20px 0; \\n border-radius: 6px;\\n }\\n .summary-box h3 { \\n margin: 0 0 12px 0; \\n color: #856404; \\n font-size: 16px;\\n }\\n .summary-stats { \\n display: grid; \\n grid-template-columns: repeat(3, 1fr); \\n gap: 10px; \\n font-size: 14px;\\n }\\n .summary-stats div { \\n background: white; \\n padding: 8px; \\n border-radius: 4px; \\n text-align: center;\\n }\\n .summary-stats strong { \\n display: block; \\n font-size: 20px; \\n color: #856404;\\n }\\n .issue-card { \\n border-radius: 8px; \\n padding: 18px; \\n margin: 15px 0; \\n border-left: 5px solid;\\n }\\n .issue-critical { \\n background: #fee2e2; \\n border-left-color: #dc3545;\\n }\\n .issue-warning { \\n background: #fef3c7; \\n border-left-color: #ffc107;\\n }\\n .issue-card h4 { \\n margin: 0 0 10px 0; \\n font-size: 16px;\\n }\\n .issue-critical h4 { color: #991b1b; }\\n .issue-warning h4 { color: #92400e; }\\n .metric-row { \\n display: flex; \\n justify-content: space-between; \\n padding: 6px 0; \\n font-size: 14px;\\n }\\n .metric-label { \\n color: #6b7280; \\n font-weight: 500;\\n }\\n .metric-value { \\n font-weight: 600;\\n }\\n .metrics-grid { \\n display: grid; \\n grid-template-columns: repeat(2, 1fr); \\n gap: 15px; \\n margin: 25px 0;\\n }\\n .metric-box { \\n background: #f8fafc; \\n border-radius: 8px; \\n padding: 18px; \\n border: 2px solid #e5e7eb;\\n }\\n .metric-box h4 { \\n margin: 0 0 12px 0; \\n color: #64748b; \\n font-size: 13px; \\n text-transform: uppercase; \\n font-weight: 600; \\n letter-spacing: 0.5px;\\n }\\n .metric-big { \\n font-size: 28px; \\n font-weight: 700; \\n color: #1e293b; \\n margin: 8px 0;\\n }\\n .metric-change { \\n font-size: 14px; \\n font-weight: 600; \\n margin-top: 5px;\\n }\\n .metric-avg { \\n font-size: 13px; \\n color: #64748b; \\n margin-top: 8px;\\n }\\n .footer { \\n margin-top: 35px; \\n padding-top: 25px; \\n border-top: 2px solid #e5e7eb; \\n font-size: 13px; \\n color: #6b7280; \\n text-align: center;\\n }\\n .footer p { margin: 5px 0; }\\n .section-title { \\n font-size: 18px; \\n font-weight: 700; \\n color: #1e40af; \\n margin: 30px 0 15px 0; \\n padding-bottom: 8px; \\n border-bottom: 2px solid #e0e7ff;\\n }\\n </style>\\n</head>\\n<body>\\n <div class=\\"container\\">\\n <div class=\\"header\\">\\n <h1>🚨 GA4 Data Quality Alert</h1>\\n <p><strong>Date:</strong> ${data.summary.date}</p>\\n ${data.summary.criticalCount > 0 ? \\n `<span class=\\"badge badge-critical\\">${data.summary.criticalCount} CRITICAL ISSUE${data.summary.criticalCount > 1 ? 'S' : ''}</span>` : \\n `<span class=\\"badge badge-warning\\">${data.summary.issueCount} WARNING${data.summary.issueCount > 1 ? 'S' : ''}</span>`\\n }\\n </div>\\n\\n <div class=\\"summary-box\\">\\n <h3>⚡ Quick Summary</h3>\\n <div class=\\"summary-stats\\">\\n <div>\\n <strong>${data.summary.issueCount}</strong>\\n <span>Total Issues</span>\\n </div>\\n <div>\\n <strong>${data.summary.criticalCount}</strong>\\n <span>Critical</span>\\n </div>\\n <div>\\n <strong>${data.summary.issueCount - data.summary.criticalCount}</strong>\\n <span>Warnings</span>\\n </div>\\n </div>\\n </div>\\n\\n <div class=\\"section-title\\">🔍 Issue Details</div>\\n ${data.issues.map(issue => `\\n <div class=\\"issue-card ${issue.severity === 'CRITICAL' ? 'issue-critical' : 'issue-warning'}\\">\\n <h4>${issue.severity === 'CRITICAL' ? '🔴' : '⚠️'} ${issue.metric}</h4>\\n <div class=\\"metric-row\\">\\n <span class=\\"metric-label\\">Yesterday:</span>\\n <span class=\\"metric-value\\">${formatNumber(issue.yesterday)}</span>\\n </div>\\n <div class=\\"metric-row\\">\\n <span class=\\"metric-label\\">7-Day Average:</span>\\n <span class=\\"metric-value\\">${formatNumber(issue.average)}</span>\\n </div>\\n <div class=\\"metric-row\\">\\n <span class=\\"metric-label\\">Variance:</span>\\n <span class=\\"metric-value\\" style=\\"color: ${parseFloat(issue.variance) > 0 ? '#10b981' : '#ef4444'}\\">\\n ${issue.variance}\\n </span>\\n </div>\\n ${issue.message ? `\\n <div style=\\"margin-top: 10px; padding: 10px; background: white; border-radius: 4px; font-size: 13px;\\">\\n <strong>Note:</strong> ${issue.message}\\n </div>\\n ` : ''}\\n </div>\\n `).join('')}\\n\\n <div class=\\"section-title\\">📊 Full Metrics Summary</div>\\n <div class=\\"metrics-grid\\">\\n <div class=\\"metric-box\\">\\n <h4>Sessions</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.sessions.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.sessions.variance)}\\">\\n ${getChangeIcon(data.metrics.sessions.variance)} ${data.metrics.sessions.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.sessions.avg7day)}</div>\\n </div>\\n\\n <div class=\\"metric-box\\">\\n <h4>Users</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.users.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.users.variance)}\\">\\n ${getChangeIcon(data.metrics.users.variance)} ${data.metrics.users.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.users.avg7day)}</div>\\n </div>\\n\\n <div class=\\"metric-box\\">\\n <h4>Events</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.events.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.events.variance)}\\">\\n ${getChangeIcon(data.metrics.events.variance)} ${data.metrics.events.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.events.avg7day)}</div>\\n </div>\\n\\n <div class=\\"metric-box\\">\\n <h4>Conversions</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.conversions.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.conversions.variance)}\\">\\n ${getChangeIcon(data.metrics.conversions.variance)} ${data.metrics.conversions.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.conversions.avg7day)}</div>\\n </div>\\n </div>\\n\\n <div class=\\"footer\\">\\n <p><strong>🤖 Automated GA4 Data Quality Monitor</strong></p>\\n <p>Generated on ${new Date().toLocaleString()}</p>\\n <p style=\\"margin-top: 15px; font-size: 12px;\\">This is an automated alert. Please investigate the issues above.</p>\\n </div>\\n </div>\\n</body>\\n</html>\\n`;\\n\\nreturn { \\n html, \\n rawData: data,\\n subject: `🚨 GA4 Alert: ${data.summary.issueCount} Issue${data.summary.issueCount > 1 ? 's' : ''} Detected - ${data.summary.date}`\\n};"
},
"id": "3fba3b65-71f8-4f7f-82e1-570a536b0096",
"name": "Format Alert Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
816,
800
]
},
{
"parameters": {
"sendTo": "={{ $env.ALERT_EMAIL }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {}
},
"id": "d99bd2fd-265e-472d-b126-f99a0701b594",
"name": "Send Alert Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1040,
800
],
"webhookId": "WEBHOOK_ID_PLACEHOLDER",
"credentials": {
"gmailOAuth2": {
"id": "YOUR_GMAIL_CREDENTIAL_ID",
"name": "Gmail OAuth2 Credential"
}
}
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\\n\\nconst formatNumber = (num) => num.toString().replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g, ',');\\n\\nconst getChangeColor = (change) => {\\n const value = parseFloat(change);\\n if (value > 0) return '#10b981';\\n if (value < 0) return '#ef4444';\\n return '#6b7280';\\n};\\n\\nconst getChangeIcon = (change) => {\\n const value = parseFloat(change);\\n if (value > 0) return '📈';\\n if (value < 0) return '📉';\\n return '➡️';\\n};\\n\\n// Generate HTML email for OK status\\nconst html = `\\n<!DOCTYPE html>\\n<html>\\n<head>\\n <meta charset=\\"UTF-8\\">\\n <meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">\\n <style>\\n body { \\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; \\n line-height: 1.6; \\n color: #333; \\n max-width: 650px; \\n margin: 0 auto; \\n padding: 20px; \\n background-color: #f5f5f5; \\n }\\n .container { \\n background-color: white; \\n border-radius: 12px; \\n padding: 35px; \\n box-shadow: 0 4px 12px rgba(0,0,0,0.1); \\n }\\n .header { \\n background: linear-gradient(135deg, #10b981 0%, #059669 100%);\\n color: white; \\n padding: 25px; \\n border-radius: 8px; \\n margin-bottom: 30px;\\n text-align: center;\\n }\\n .header h1 { \\n margin: 0; \\n font-size: 26px; \\n font-weight: 700;\\n }\\n .header p { \\n margin: 8px 0 0 0; \\n opacity: 0.95; \\n font-size: 15px;\\n }\\n .badge { \\n display: inline-block; \\n padding: 5px 14px; \\n border-radius: 15px; \\n font-size: 12px; \\n font-weight: 600; \\n background: white;\\n color: #059669;\\n margin-top: 8px;\\n }\\n .summary-box { \\n background: #d1fae5; \\n border-left: 4px solid #10b981; \\n padding: 18px; \\n margin: 20px 0; \\n border-radius: 6px;\\n }\\n .summary-box h3 { \\n margin: 0 0 8px 0; \\n color: #065f46; \\n font-size: 16px;\\n }\\n .summary-box p { \\n margin: 5px 0; \\n color: #047857; \\n font-size: 14px;\\n }\\n .metrics-grid { \\n display: grid; \\n grid-template-columns: repeat(2, 1fr); \\n gap: 15px; \\n margin: 25px 0;\\n }\\n .metric-box { \\n background: #f8fafc; \\n border-radius: 8px; \\n padding: 18px; \\n border: 2px solid #e5e7eb;\\n }\\n .metric-box h4 { \\n margin: 0 0 12px 0; \\n color: #64748b; \\n font-size: 13px; \\n text-transform: uppercase; \\n font-weight: 600; \\n letter-spacing: 0.5px;\\n }\\n .metric-big { \\n font-size: 28px; \\n font-weight: 700; \\n color: #1e293b; \\n margin: 8px 0;\\n }\\n .metric-change { \\n font-size: 14px; \\n font-weight: 600; \\n margin-top: 5px;\\n }\\n .metric-avg { \\n font-size: 13px; \\n color: #64748b; \\n margin-top: 8px;\\n }\\n .footer { \\n margin-top: 35px; \\n padding-top: 25px; \\n border-top: 2px solid #e5e7eb; \\n font-size: 13px; \\n color: #6b7280; \\n text-align: center;\\n }\\n .footer p { margin: 5px 0; }\\n .section-title { \\n font-size: 18px; \\n font-weight: 700; \\n color: #1e40af; \\n margin: 30px 0 15px 0; \\n padding-bottom: 8px; \\n border-bottom: 2px solid #e0e7ff;\\n }\\n </style>\\n</head>\\n<body>\\n <div class=\\"container\\">\\n <div class=\\"header\\">\\n <h1>✅ GA4 Daily Report</h1>\\n <p><strong>Date:</strong> ${data.summary.date}</p>\\n <span class=\\"badge\\">ALL SYSTEMS NORMAL</span>\\n </div>\\n\\n <div class=\\"summary-box\\">\\n <h3>✓ No Issues Detected</h3>\\n <p>All metrics are within normal range compared to the 7-day average.</p>\\n <p>Your GA4 tracking is functioning properly.</p>\\n </div>\\n\\n <div class=\\"section-title\\">📊 Daily Metrics Summary</div>\\n <div class=\\"metrics-grid\\">\\n <div class=\\"metric-box\\">\\n <h4>Sessions</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.sessions.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.sessions.variance)}\\">\\n ${getChangeIcon(data.metrics.sessions.variance)} ${data.metrics.sessions.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.sessions.avg7day)}</div>\\n </div>\\n\\n <div class=\\"metric-box\\">\\n <h4>Users</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.users.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.users.variance)}\\">\\n ${getChangeIcon(data.metrics.users.variance)} ${data.metrics.users.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.users.avg7day)}</div>\\n </div>\\n\\n <div class=\\"metric-box\\">\\n <h4>Events</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.events.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.events.variance)}\\">\\n ${getChangeIcon(data.metrics.events.variance)} ${data.metrics.events.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.events.avg7day)}</div>\\n </div>\\n\\n <div class=\\"metric-box\\">\\n <h4>Conversions</h4>\\n <div class=\\"metric-big\\">${formatNumber(data.metrics.conversions.yesterday)}</div>\\n <div class=\\"metric-change\\" style=\\"color: ${getChangeColor(data.metrics.conversions.variance)}\\">\\n ${getChangeIcon(data.metrics.conversions.variance)} ${data.metrics.conversions.variance}\\n </div>\\n <div class=\\"metric-avg\\">7-day avg: ${formatNumber(data.metrics.conversions.avg7day)}</div>\\n </div>\\n </div>\\n\\n <div class=\\"footer\\">\\n <p><strong>🤖 Automated GA4 Data Quality Monitor</strong></p>\\n <p>Generated on ${new Date().toLocaleString()}</p>\\n <p style=\\"margin-top: 15px; font-size: 12px;\\">Daily monitoring keeps your data quality in check.</p>\\n </div>\\n </div>\\n</body>\\n</html>\\n`;\\n\\nreturn { \\n html,\\n subject: `✅ GA4 Daily Report: No Issues - ${data.summary.date}`\\n};"
},
"id": "5f40e6cb-0844-4a50-86b9-8ab5e2d64d69",
"name": "Format OK Email",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
816,
992
]
},
{
"parameters": {
"sendTo": "={{ $env.ALERT_EMAIL }}",
"subject": "={{ $json.subject }}",
"message": "={{ $json.html }}",
"options": {}
},
"id": "158d39f3-3797-473f-a094-c4b2aa08f02b",
"name": "Send OK Email",
"type": "n8n-nodes-base.gmail",
"typeVersion": 2.1,
"position": [
1040,
992
],
"webhookId": "WEBHOOK_ID_PLACEHOLDER",
"credentials": {
"gmailOAuth2": {
"id": "YOUR_GMAIL_CREDENTIAL_ID",
"name": "Gmail OAuth2 Credential"
}
}
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 9 * * *"
}
]
}
},
"id": "432909e3-0e9c-43c8-a559-d267eb7d020f",
"name": "Schedule Daily 9AM1",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
-304,
896
]
},
{
"parameters": {
"propertyId": {
"__rl": true,
"value": "={{ $env.GA4_PROPERTY_ID }}",
"mode": "id"
},
"dateRange": "yesterday",
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
},
{
"listName": "eventCount"
},
{}
]
},
"dimensionsGA4": {
"dimensionValues": [
{}
]
},
"limit": 100,
"additionalFields": {}
},
"id": "b6364bfa-7d5d-42a2-90ef-580423a90346",
"name": "Get Yesterday's Data1",
"type": "n8n-nodes-base.googleAnalytics",
"typeVersion": 2,
"position": [
-80,
800
],
"credentials": {
"googleAnalyticsOAuth2": {
"id": "YOUR_GA4_CREDENTIAL_ID",
"name": "Google Analytics OAuth2 Credential"
}
}
},
{
"parameters": {
"propertyId": {
"__rl": true,
"value": "={{ $env.GA4_PROPERTY_ID }}",
"mode": "id"
},
"metricsGA4": {
"metricValues": [
{
"listName": "sessions"
},
{
"listName": "eventCount"
},
{}
]
},
"dimensionsGA4": {
"dimensionValues": [
{}
]
},
"limit": 100,
"additionalFields": {}
},
"id": "286cdfd6-9171-4f0b-b701-325d6487c2ac",
"name": "Get 7-Day Average1",
"type": "n8n-nodes-base.googleAnalytics",
"typeVersion": 2,
"position": [
-80,
992
],
"credentials": {
"googleAnalyticsOAuth2": {
"id": "YOUR_GA4_CREDENTIAL_ID",
"name": "Google Analytics OAuth2 Credential"
}
}
},
{
"parameters": {
"jsCode": "// CORRECTED CODE FOR \\"Calculate Variance & Detect Issues\\" NODE\\n// This handles the actual GA4 API response structure\\n\\n// Access merged data - n8n merge creates an array of all items\\nconst allItems = $input.all();\\n\\nconsole.log('Total items received:', allItems.length);\\nconsole.log('Sample item:', JSON.stringify(allItems[0], null, 2));\\n\\n// GA4 returns multiple items (one per day)\\n// We need to separate yesterday's data from the 7-day data\\n// The Merge node combines both datasets into a single array\\n\\n// First 8 items are from \\"Get Yesterday's Data\\" (1 day)\\n// Next items are from \\"Get 7-Day Average\\" (7 days)\\n\\n// Find yesterday's data (should be the most recent date)\\nconst sortedByDate = allItems.sort((a, b) => {\\n const dateA = a.json.date || '';\\n const dateB = b.json.date || '';\\n return dateB.localeCompare(dateA);\\n});\\n\\n// Yesterday is the most recent date\\nconst yesterday = sortedByDate[0].json;\\n\\n// Calculate 7-day totals from all items (excluding yesterday)\\nlet totalSessions = 0;\\nlet totalUsers = 0;\\nlet totalEvents = 0;\\nlet totalConversions = 0;\\nlet dayCount = 0;\\n\\n// Sum up the 7-day period data (dates before yesterday)\\nallItems.forEach(item => {\\n const itemDate = item.json.date || '';\\n const yesterdayDate = yesterday.date || '';\\n \\n // Only include dates before yesterday for the average\\n if (itemDate < yesterdayDate) {\\n totalSessions += parseInt(item.json.sessions || 0);\\n totalUsers += parseInt(item.json.totalUsers || 0);\\n totalEvents += parseInt(item.json.eventCount || 0);\\n totalConversions += parseInt(item.json.conversions || 0);\\n dayCount++;\\n }\\n});\\n\\n// If we don't have 7 days of data, use what we have\\nif (dayCount === 0) {\\n // Fallback: use all items except the first one\\n allItems.slice(1).forEach(item => {\\n totalSessions += parseInt(item.json.sessions || 0);\\n totalUsers += parseInt(item.json.totalUsers || 0);\\n totalEvents += parseInt(item.json.eventCount || 0);\\n totalConversions += parseInt(item.json.conversions || 0);\\n dayCount++;\\n });\\n}\\n\\n// Calculate averages\\nconst avg7Sessions = dayCount > 0 ? totalSessions / dayCount : 0;\\nconst avg7Users = dayCount > 0 ? totalUsers / dayCount : 0;\\nconst avg7Events = dayCount > 0 ? totalEvents / dayCount : 0;\\nconst avg7Conversions = dayCount > 0 ? totalConversions / dayCount : 0;\\n\\n// Extract yesterday's metrics\\nconst yesterdaySessions = parseInt(yesterday.sessions || 0);\\nconst yesterdayUsers = parseInt(yesterday.totalUsers || 0);\\nconst yesterdayEvents = parseInt(yesterday.eventCount || 0);\\nconst yesterdayConversions = parseInt(yesterday.conversions || 0);\\n\\n// Calculate variance percentages\\nconst calculateVariance = (current, average) => {\\n if (average === 0) return 0;\\n return ((current - average) / average) * 100;\\n};\\n\\nconst sessionVariance = calculateVariance(yesterdaySessions, avg7Sessions);\\nconst userVariance = calculateVariance(yesterdayUsers, avg7Users);\\nconst eventVariance = calculateVariance(yesterdayEvents, avg7Events);\\nconst conversionVariance = calculateVariance(yesterdayConversions, avg7Conversions);\\n\\n// Define thresholds (configurable)\\nconst alertThreshold = 20;\\nconst criticalThreshold = 40;\\n\\n// Check for anomalies\\nconst issues = [];\\n\\n// Check sessions\\nif (Math.abs(sessionVariance) > criticalThreshold) {\\n issues.push({\\n severity: 'CRITICAL',\\n metric: 'Sessions',\\n yesterday: yesterdaySessions,\\n average: Math.round(avg7Sessions),\\n variance: sessionVariance.toFixed(2) + '%'\\n });\\n} else if (Math.abs(sessionVariance) > alertThreshold) {\\n issues.push({\\n severity: 'WARNING',\\n metric: 'Sessions',\\n yesterday: yesterdaySessions,\\n average: Math.round(avg7Sessions),\\n variance: sessionVariance.toFixed(2) + '%'\\n });\\n}\\n\\n// Check users\\nif (Math.abs(userVariance) > criticalThreshold) {\\n issues.push({\\n severity: 'CRITICAL',\\n metric: 'Users',\\n yesterday: yesterdayUsers,\\n average: Math.round(avg7Users),\\n variance: userVariance.toFixed(2) + '%'\\n });\\n} else if (Math.abs(userVariance) > alertThreshold) {\\n issues.push({\\n severity: 'WARNING',\\n metric: 'Users',\\n yesterday: yesterdayUsers,\\n average: Math.round(avg7Users),\\n variance: userVariance.toFixed(2) + '%'\\n });\\n}\\n\\n// Check conversions\\nif (Math.abs(conversionVariance) > criticalThreshold) {\\n issues.push({\\n severity: 'CRITICAL',\\n metric: 'Conversions',\\n yesterday: yesterdayConversions,\\n average: Math.round(avg7Conversions),\\n variance: conversionVariance.toFixed(2) + '%'\\n });\\n} else if (Math.abs(conversionVariance) > alertThreshold) {\\n issues.push({\\n severity: 'WARNING',\\n metric: 'Conversions',\\n yesterday: yesterdayConversions,\\n average: Math.round(avg7Conversions),\\n variance: conversionVariance.toFixed(2) + '%'\\n });\\n}\\n\\n// Check for zero events\\nif (yesterdayEvents === 0) {\\n issues.push({\\n severity: 'CRITICAL',\\n metric: 'Event Tracking',\\n yesterday: 0,\\n average: Math.round(avg7Events),\\n variance: '-100%',\\n message: 'No events tracked yesterday - possible tracking failure!'\\n });\\n} else if (yesterdayEvents < avg7Events * 0.5 && yesterdayEvents > 0) {\\n issues.push({\\n severity: 'WARNING',\\n metric: 'Event Volume',\\n yesterday: yesterdayEvents,\\n average: Math.round(avg7Events),\\n variance: eventVariance.toFixed(2) + '%',\\n message: 'Event volume dropped significantly'\\n });\\n}\\n\\n// Format date for display\\nconst reportDate = yesterday.date || new Date().toISOString().split('T')[0];\\nconst formattedDate = reportDate.substring(0, 4) + '-' + reportDate.substring(4, 6) + '-' + reportDate.substring(6, 8);\\n\\n// Return structured output\\nreturn {\\n summary: {\\n date: formattedDate,\\n hasIssues: issues.length > 0,\\n issueCount: issues.length,\\n criticalCount: issues.filter(i => i.severity === 'CRITICAL').length\\n },\\n metrics: {\\n sessions: {\\n yesterday: Math.round(yesterdaySessions),\\n avg7day: Math.round(avg7Sessions),\\n variance: sessionVariance.toFixed(2) + '%'\\n },\\n users: {\\n yesterday: Math.round(yesterdayUsers),\\n avg7day: Math.round(avg7Users),\\n variance: userVariance.toFixed(2) + '%'\\n },\\n events: {\\n yesterday: Math.round(yesterdayEvents),\\n avg7day: Math.round(avg7Events),\\n variance: eventVariance.toFixed(2) + '%'\\n },\\n conversions: {\\n yesterday: Math.round(yesterdayConversions),\\n avg7day: Math.round(avg7Conversions),\\n variance: conversionVariance.toFixed(2) + '%'\\n }\\n },\\n issues: issues,\\n debug: {\\n totalItemsReceived: allItems.length,\\n daysInAverage: dayCount,\\n yesterdayDate: formattedDate\\n }\\n};"
},
"id": "8d8870d2-2aca-471b-9886-2b4d713936b1",
"name": "Calculate Variance & Detect Issues1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
368,
896
]
},
{
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.summary.hasIssues }}",
"value2": true
}
]
}
},
"id": "6c03a649-8016-4163-b0c1-d55fc767cbaf",
"name": "Has Issues?1",
"type": "n8n-nodes-base.if",
"typeVersion": 1,
"position": [
592,
896
]
}
],
"pinData": {},
"connections": {
"Merge Both Datasets": {
"main": [
[
{
"node": "Calculate Variance & Detect Issues1",
"type": "main",
"index": 0
}
]
]
},
"Format Alert Email": {
"main": [
[
{
"node": "Send Alert Email",
"type": "main",
"index": 0
}
]
]
},
"Format OK Email": {
"main": [
[
{
"node": "Send OK Email",
"type": "main",
"index": 0
}
]
]
},
"Schedule Daily 9AM1": {
"main": [
[
{
"node": "Get Yesterday's Data1",
"type": "main",
"index": 0
},
{
"node": "Get 7-Day Average1",
"type": "main",
"index": 0
}
]
]
},
"Get Yesterday's Data1": {
"main": [
[
{
"node": "Merge Both Datasets",
"type": "main",
"index": 0
}
]
]
},
"Get 7-Day Average1": {
"main": [
[
{
"node": "Merge Both Datasets",
"type": "main",
"index": 1
}
]
]
},
"Calculate Variance & Detect Issues1": {
"main": [
[
{
"node": "Has Issues?1",
"type": "main",
"index": 0
}
]
]
},
"Has Issues?1": {
"main": [
[
{
"node": "Format Alert Email",
"type": "main",
"index": 0
}
],
[
{
"node": "Format OK Email",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "eb5efc4f-95cc-4b2d-ab2f-65576918e0f2",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "e17e98e5cd488ce03435ef2ccd78b6d3a873b06fb28b92e23b60b9f2a5b97c40"
},
"id": "j7Pa8KUkX7kvlwkC",
"tags": []
}
You now have automated GA4 data quality monitoring running 24/7! 🎉
Any issues? The workflow will catch them before you notice in reports.
