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

  1. Copy the entire n8n_GA4_Data_Quality_Workflow.json file

  2. In n8n: Click “+” → “Import from File”

  3. Paste JSON content

  4. Click “Import”

Method 2: Manual Setup See “Manual Build Instructions” section below

Step 3: Set Up Google Analytics Connection

Create OAuth2 Credentials:

  1. Create new project (or select existing)

  2. Enable “Google Analytics Data API”

  3. Create OAuth 2.0 credentials:

    • Application type: Web application

    • Authorized redirect URIs: https://your-n8n-instance.com/rest/oauth2-credential/callback

  4. Copy Client ID and Client Secret

In n8n:

  1. Click on “Get Yesterday’s Data” node

  2. Click “Create New Credential”

  3. Select “Google Analytics OAuth2”

  4. Paste Client ID and Client Secret

  5. Click “Connect my account”

  6. 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:

  1. GA4 → Admin → Property Settings

  2. Copy “Property ID” (format: 123456789)

How to find Slack Channel ID:

  1. Slack → Right-click channel → “View channel details”

  2. Scroll to bottom → Copy Channel ID

Step 5: Set Up Email

If using Gmail:

  1. Enable 2FA on Google account

  2. In n8n SMTP settings:

If using other provider:

  • Use their SMTP settings

Step 6: Set Up Slack Connection(Optional)

Create Slack App:

  1. Click “Create New App” → “From scratch”

  2. Name: “GA4 Monitor”

  3. Select your workspace

  4. OAuth & Permissions → Add scopes:

    • chat:write

    • chat:write.public

  5. Install to workspace

  6. Copy “Bot User OAuth Token”

In n8n:

  1. Click on “Send Slack Alert” node

  2. Click “Create New Credential”

  3. Select “Slack API”

  4. Paste Bot Token

  5. 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:

  1. Click “Execute Workflow” button

  2. Check each node completes successfully

  3. 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:

  1. Click “Schedule Daily 9AM” node

  2. 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”:

  1. Add “Google Sheets” node

  2. Operation: “Append”

  3. Spreadsheet: Your tracking sheet

  4. 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_ID for each

  • Different Slack channels per property

Option 2: Loop through multiple properties

  1. Add “Code” node at start with property IDs array

  2. Add “Split In Batches” node

  3. Use ={{ $json.propertyId }} in GA4 nodes

  4. Include 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

  1. Start with higher thresholds (30%/50%) and tighten over time as you understand your baseline variance.

  2. Create separate workflows for weekday vs weekend monitoring (different thresholds).

  3. Add a “grace period” - Don’t alert on first occurrence, only if issue persists 2+ days.

  4. Log to Google Sheets for historical trending and pattern analysis.

  5. Set up PagerDuty instead of Slack for critical issues (24/7 on-call).

  6. Monitor specific event names separately (e.g., purchase events, sign-up events).

  7. 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.