Back to Guides
    integrationsadvanced60 minutesJan 9, 2025

    Server-Side Tracking Implementation

    Implement server-side tracking to bypass ad blockers and iOS restrictions. Get 100% reliable data for AI analytics with backend event tracking.

    Server-Side Tracking Implementation

    TL;DR

    Implement server-side tracking to bypass ad blockers and iOS restrictions, achieving 100% reliable data collection by sending events from your backend to GA4 and Meta.

    What you'll accomplish:

    • Move critical event tracking from browser to backend server
    • Send events directly to GA4 Measurement Protocol and Meta Conversions API
    • Bypass ad blockers (affecting 20-40% of users) and browser privacy restrictions
    • Implement event deduplication for hybrid client/server tracking
    • Achieve 100% reliable conversion tracking for AI-powered optimization

    Time required: 60 minutes | Difficulty: Advanced | Prerequisites: Backend application access, developer resources, GA4 and Meta Pixel installed, basic server-side coding

    Quick Start: Identify critical conversion events (purchases, signups) → Implement server-side event sending on successful transactions → Test with small percentage of traffic → Gradually migrate events.


    Related Resources

    Essential guides for complete tracking infrastructure:


    Question

    How do I implement server-side tracking to get more reliable data for Cogny's AI?

    Answer

    Move tracking from browser to your backend server. Send events directly from your application to GA4, Meta, and your data warehouse. Bypass ad blockers, privacy restrictions, and browser limitations.

    Get 100% reliable tracking for better AI insights.

    Quick Tip: Start with purchase events for server-side tracking—highest ROI with minimal implementation complexity. Once stable, expand to signups and other critical conversions.

    Why Server-Side Tracking?

    Browser-based tracking is dying.

    Problems with browser tracking:

    Ad blockers: Block 20-40% of events

    • Users with uBlock Origin, AdBlock Plus, Brave browser
    • Events never fire
    • You're blind to these users

    Browser privacy settings:

    • Safari ITP (Intelligent Tracking Prevention)
    • Firefox ETP (Enhanced Tracking Protection)
    • Chrome Privacy Sandbox
    • Cookies blocked or limited

    User behavior:

    • Close page before event fires
    • Network timeout
    • JavaScript errors
    • Incognito mode

    Result: You're making decisions on 60-70% of actual data.

    Server-side tracking solves this:

    • ✅ 100% reliable delivery
    • ✅ Bypasses ad blockers
    • ✅ No browser dependencies
    • ✅ Works for all users
    • ✅ Better data quality
    • ✅ Reduced client-side payload

    What You'll Build

    After this guide:

    Server-side event pipeline Events flow from backend → GA4 Measurement Protocol → BigQuery → Cogny

    Multi-platform tracking Same events to GA4, Meta Conversions API, your data warehouse

    Reliable conversion tracking Critical events (purchases, signups) never missed

    Enhanced user matching Server has more context: email, user ID, order details

    Privacy-compliant tracking Control exactly what data is sent, when, and to whom


    Note: Server-side tracking requires backend development. Budget 2-4 developer days for initial implementation or use a managed service like Segment. Start with critical events only (purchases, signups) before expanding.

    Architecture Overview

    Browser-based (old way):

    User action → Browser JavaScript → gtag.js → GA4 → BigQuery → Cogny
                                      → fbq() → Meta
    
    Problems:
    - Ad blockers can stop entire chain
    - Browser restrictions limit data
    - User leaving page = lost events
    

    Server-side (new way):

    User action → Your backend → GA4 Measurement Protocol → GA4 → BigQuery → Cogny
                              → Meta Conversions API → Meta Ads Manager
                              → Direct to BigQuery
    
    Benefits:
    - Nothing can block it
    - Complete control
    - Rich server context
    - Guaranteed delivery
    

    Hybrid (best):

    User action → Browser (for immediate context) → GA4
               → Your backend (for reliability) → GA4 + Meta + BigQuery
    
    Browser + Server events deduplicated using event_id
    

    Prerequisites

    You need:

    1. Backend application (Node.js, Python, Ruby, PHP, etc.)
    2. GA4 property with Measurement Protocol enabled
    3. BigQuery dataset (if direct-to-warehouse)
    4. Meta Conversions API access token (optional)
    5. Developer skills (or a developer on your team)

    Step 1: Understand GA4 Measurement Protocol

    What is it?

    HTTP API for sending events to GA4 from anywhere.

    Endpoint:

    POST https://www.google-analytics.com/mp/collect?measurement_id=G-XXXXXXXXXX&api_secret=YOUR_SECRET
    

    Required parameters:

    • measurement_id: Your GA4 Measurement ID
    • api_secret: Secret key for authentication
    • client_id: Anonymous user identifier
    • events: Array of events to send

    Time: 5 minutes to understand


    Step 2: Generate GA4 API Secret

    Log into analytics.google.com

    Select your GA4 property.

    Click Admin (gear icon) → Data Streams

    Click your web data stream.

    Scroll to Measurement Protocol API secrets

    Click Create

    Nickname: server-side-tracking

    Click Create

    Copy the secret value. You won't see it again.

    IMPORTANT: Keep this secret. Never expose in client-side code or public repos.

    Time: 3 minutes


    Step 3: Set Up Server-Side Event Sending

    Example: Node.js / Express

    Install axios:

    npm install axios
    

    Create helper function:

    // lib/analytics.js
    const axios = require('axios');
    
    const GA4_MEASUREMENT_ID = process.env.GA4_MEASUREMENT_ID; // G-XXXXXXXXXX
    const GA4_API_SECRET = process.env.GA4_API_SECRET;
    
    /**
     * Send event to GA4 Measurement Protocol
     */
    async function sendGA4Event(clientId, eventName, eventParams = {}, userProperties = {}) {
      const url = `https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`;
    
      const payload = {
        client_id: clientId, // Anonymous ID (from GA cookie or generated)
        user_id: userProperties.user_id, // Logged-in user ID (optional but recommended)
        timestamp_micros: Date.now() * 1000, // Current time in microseconds
        user_properties: userProperties,
        events: [{
          name: eventName,
          params: {
            ...eventParams,
            engagement_time_msec: 100, // Required for some events
            session_id: userProperties.session_id || 'unknown'
          }
        }]
      };
    
      try {
        await axios.post(url, payload, {
          headers: {
            'Content-Type': 'application/json'
          },
          timeout: 5000 // 5 second timeout
        });
        console.log(`✅ GA4 event sent: ${eventName}`);
      } catch (error) {
        console.error(`❌ GA4 event failed: ${eventName}`, error.message);
        // Don't throw - tracking failure shouldn't break user experience
      }
    }
    
    module.exports = { sendGA4Event };
    

    Usage in your app:

    const { sendGA4Event } = require('./lib/analytics');
    
    // When user completes purchase
    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
    
      // Send to GA4
      await sendGA4Event(
        req.cookies._ga || generateClientId(), // GA client ID
        'purchase',
        {
          transaction_id: order.id,
          value: order.total,
          currency: 'USD',
          tax: order.tax,
          shipping: order.shipping,
          items: order.items.map(item => ({
            item_id: item.sku,
            item_name: item.name,
            price: item.price,
            quantity: item.quantity
          }))
        },
        {
          user_id: req.user.id,
          session_id: req.sessionID,
          email: req.user.email
        }
      );
    
      res.json({ success: true, orderId: order.id });
    });
    

    Time: 20 minutes


    Example: Python / Flask

    # lib/analytics.py
    import os
    import requests
    import time
    import json
    
    GA4_MEASUREMENT_ID = os.getenv('GA4_MEASUREMENT_ID')
    GA4_API_SECRET = os.getenv('GA4_API_SECRET')
    
    def send_ga4_event(client_id, event_name, event_params=None, user_properties=None):
        """Send event to GA4 Measurement Protocol"""
    
        url = f'https://www.google-analytics.com/mp/collect?measurement_id={GA4_MEASUREMENT_ID}&api_secret={GA4_API_SECRET}'
    
        payload = {
            'client_id': client_id,
            'user_id': user_properties.get('user_id') if user_properties else None,
            'timestamp_micros': int(time.time() * 1000000),
            'user_properties': user_properties or {},
            'events': [{
                'name': event_name,
                'params': {
                    **(event_params or {}),
                    'engagement_time_msec': 100,
                    'session_id': user_properties.get('session_id', 'unknown') if user_properties else 'unknown'
                }
            }]
        }
    
        try:
            response = requests.post(
                url,
                json=payload,
                headers={'Content-Type': 'application/json'},
                timeout=5
            )
            print(f'✅ GA4 event sent: {event_name}')
        except Exception as e:
            print(f'❌ GA4 event failed: {event_name} - {str(e)}')
            # Don't raise - tracking failure shouldn't break user experience
    
    # Usage
    from lib.analytics import send_ga4_event
    
    @app.route('/api/purchase', methods=['POST'])
    def purchase():
        order = create_order(request.json)
    
        send_ga4_event(
            client_id=request.cookies.get('_ga', generate_client_id()),
            event_name='purchase',
            event_params={
                'transaction_id': order.id,
                'value': order.total,
                'currency': 'USD',
                'items': [{
                    'item_id': item.sku,
                    'item_name': item.name,
                    'price': item.price,
                    'quantity': item.quantity
                } for item in order.items]
            },
            user_properties={
                'user_id': current_user.id,
                'session_id': session.id,
                'email': current_user.email
            }
        )
    
        return jsonify({'success': True, 'orderId': order.id})
    

    Time: 20 minutes


    Step 4: Get Client ID from Browser

    GA4 creates client_id in browser (_ga cookie).

    You need this same ID for server-side tracking to connect browser and server events.

    Extract from cookie:

    // JavaScript - send to backend
    function getGA4ClientId() {
      const cookies = document.cookie.split(';');
      const gaCookie = cookies.find(c => c.trim().startsWith('_ga='));
    
      if (gaCookie) {
        // Format: _ga=G-XXXXXXXXXX.CLIENT_ID
        const parts = gaCookie.split('.');
        return parts.slice(2).join('.'); // CLIENT_ID portion
      }
    
      return null;
    }
    
    // Send with purchase request
    fetch('/api/purchase', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-GA-Client-ID': getGA4ClientId() // Send in header
      },
      body: JSON.stringify(orderData)
    });
    

    Backend extracts it:

    app.post('/api/purchase', async (req, res) => {
      const clientId = req.headers['x-ga-client-id'] || generateClientId();
    
      await sendGA4Event(clientId, 'purchase', {...});
    });
    

    Fallback if no cookie:

    function generateClientId() {
      return `${Date.now()}.${Math.random().toString(36).substring(2, 11)}`;
    }
    

    Time: 10 minutes


    Step 5: Implement Event Deduplication

    Problem: Same event sent from browser AND server = duplicate counting.

    Solution: Use event_id to deduplicate.

    Browser event:

    // When user clicks "Purchase" button
    const eventId = `purchase_${orderId}_${Date.now()}`;
    
    // Send via gtag (browser)
    gtag('event', 'purchase', {
      transaction_id: orderId,
      value: orderTotal,
      currency: 'USD'
    }, {
      event_id: eventId // Add this
    });
    
    // Also send event_id to backend
    fetch('/api/purchase', {
      method: 'POST',
      body: JSON.stringify({
        orderId: orderId,
        eventId: eventId // Include same ID
      })
    });
    

    Server event:

    app.post('/api/purchase', async (req, res) => {
      const { orderId, eventId } = req.body;
    
      // Send to GA4 with SAME event_id
      await sendGA4Event(
        clientId,
        'purchase',
        {
          transaction_id: orderId,
          value: orderTotal,
          currency: 'USD',
          event_id: eventId // Same ID as browser event
        }
      );
    });
    

    GA4 automatically deduplicates events with matching event_id.

    Time: 10 minutes


    Step 6: Add Meta Conversions API

    Send same events to Meta for better ad optimization.

    See our Meta Pixel Attribution Setup for detailed Conversions API implementation. For browser-based event tracking coordination, check our Google Tag Manager Integration guide.

    Quick example:

    const crypto = require('crypto');
    
    async function sendMetaConversion(eventName, eventData, userData) {
      const pixelId = process.env.META_PIXEL_ID;
      const accessToken = process.env.META_ACCESS_TOKEN;
    
      const url = `https://graph.facebook.com/v18.0/${pixelId}/events`;
    
      const hashData = (data) => {
        return data ? crypto.createHash('sha256').update(data.toLowerCase()).digest('hex') : null;
      };
    
      const payload = {
        data: [{
          event_name: eventName,
          event_time: Math.floor(Date.now() / 1000),
          event_id: eventData.event_id,
          event_source_url: eventData.source_url,
          action_source: 'website',
          user_data: {
            em: hashData(userData.email),
            ph: hashData(userData.phone),
            fn: hashData(userData.firstName),
            ln: hashData(userData.lastName),
            external_id: userData.userId,
            client_ip_address: userData.ip,
            client_user_agent: userData.userAgent,
            fbc: userData.fbc,
            fbp: userData.fbp
          },
          custom_data: {
            value: eventData.value,
            currency: eventData.currency,
            content_ids: eventData.content_ids
          }
        }],
        access_token: accessToken
      };
    
      await axios.post(url, payload);
    }
    
    // Usage - send to both GA4 and Meta
    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
    
      const eventId = `purchase_${order.id}_${Date.now()}`;
    
      // Send to GA4
      await sendGA4Event(clientId, 'purchase', {
        transaction_id: order.id,
        value: order.total,
        currency: 'USD',
        event_id: eventId
      });
    
      // Send to Meta
      await sendMetaConversion('Purchase', {
        event_id: eventId, // Same ID for deduplication
        source_url: `${req.protocol}://${req.get('host')}/order-confirmation`,
        value: order.total,
        currency: 'USD',
        content_ids: order.items.map(i => i.sku)
      }, {
        email: req.user.email,
        phone: req.user.phone,
        firstName: req.user.firstName,
        lastName: req.user.lastName,
        userId: req.user.id,
        ip: req.ip,
        userAgent: req.headers['user-agent'],
        fbc: req.cookies._fbc,
        fbp: req.cookies._fbp
      });
    
      res.json({ success: true });
    });
    

    Time: 15 minutes (if already implemented Meta Pixel)


    Step 7: Send Directly to BigQuery

    Bypass GA4 entirely for critical events.

    Why?

    • Instant availability (no 24-hour delay)
    • No GA4 processing/sampling
    • Full control over schema
    • Can include sensitive data (that shouldn't go to GA4/Meta)

    Install BigQuery client:

    npm install @google-cloud/bigquery
    

    Set up BigQuery client:

    const { BigQuery } = require('@google-cloud/bigquery');
    
    const bigquery = new BigQuery({
      projectId: process.env.GCP_PROJECT_ID,
      credentials: {
        client_email: process.env.GCP_CLIENT_EMAIL,
        private_key: process.env.GCP_PRIVATE_KEY.replace(/\\n/g, '\n')
      }
    });
    
    async function sendToBigQuery(datasetId, tableId, rows) {
      try {
        await bigquery
          .dataset(datasetId)
          .table(tableId)
          .insert(rows);
    
        console.log(`✅ Inserted ${rows.length} rows to BigQuery`);
      } catch (error) {
        console.error('❌ BigQuery insert failed:', error);
      }
    }
    
    // Usage
    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
    
      // Send to BigQuery immediately
      await sendToBigQuery('ecommerce', 'purchases', [{
        timestamp: new Date().toISOString(),
        order_id: order.id,
        user_id: req.user.id,
        email: req.user.email,
        total: order.total,
        currency: 'USD',
        items: JSON.stringify(order.items),
        payment_method: order.paymentMethod,
        shipping_address: JSON.stringify(order.shippingAddress),
        utm_source: req.cookies.utm_source,
        utm_medium: req.cookies.utm_medium,
        utm_campaign: req.cookies.utm_campaign,
        referrer: req.headers.referer,
        ip_address: req.ip,
        user_agent: req.headers['user-agent']
      }]);
    
      res.json({ success: true });
    });
    

    Create BigQuery table first:

    CREATE TABLE `project.ecommerce.purchases` (
      timestamp TIMESTAMP,
      order_id STRING,
      user_id STRING,
      email STRING,
      total FLOAT64,
      currency STRING,
      items STRING, -- JSON
      payment_method STRING,
      shipping_address STRING, -- JSON
      utm_source STRING,
      utm_medium STRING,
      utm_campaign STRING,
      referrer STRING,
      ip_address STRING,
      user_agent STRING
    );
    

    Time: 20 minutes


    Step 8: Implement Error Handling and Retries

    Tracking shouldn't break your app.

    async function sendGA4Event(clientId, eventName, eventParams, userProperties, retries = 3) {
      for (let attempt = 1; attempt <= retries; attempt++) {
        try {
          await axios.post(url, payload, { timeout: 5000 });
          console.log(`✅ GA4 event sent: ${eventName}`);
          return; // Success
        } catch (error) {
          console.error(`❌ GA4 event failed (attempt ${attempt}/${retries}): ${eventName}`, error.message);
    
          if (attempt < retries) {
            // Exponential backoff: 100ms, 200ms, 400ms
            await sleep(100 * Math.pow(2, attempt - 1));
          }
        }
      }
    
      // All retries failed - log to error tracking (but don't throw)
      console.error(`❌ GA4 event permanently failed after ${retries} attempts: ${eventName}`);
    }
    
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }
    

    Send tracking async (don't block user):

    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
    
      // Return response immediately
      res.json({ success: true, orderId: order.id });
    
      // Send tracking in background (don't await)
      sendGA4Event(clientId, 'purchase', {...}).catch(err => {
        console.error('Tracking failed:', err);
      });
    });
    

    Time: 15 minutes


    Step 9: Test Server-Side Events

    Verify events reach GA4:

    Use GA4 DebugView:

    1. In GA4: Configure → DebugView
    2. Add debug_mode: true to your event params:
    await sendGA4Event(clientId, 'purchase', {
      transaction_id: orderId,
      value: orderTotal,
      debug_mode: true // Only for testing
    });
    
    1. Trigger event from your backend
    2. See event appear in DebugView in real-time

    If event doesn't appear:

    Check:

    • Measurement ID correct?
    • API secret valid?
    • client_id in correct format?
    • Event name valid (no spaces, special chars)?
    • Network connectivity?

    Verify events reach BigQuery:

    Wait 24-48 hours.

    Query BigQuery:

    SELECT
      event_name,
      event_timestamp,
      user_id,
      (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'transaction_id') as transaction_id,
      (SELECT value.double_value FROM UNNEST(event_params) WHERE key = 'value') as value
    FROM `project.analytics_XXXXXX.events_*`
    WHERE event_name = 'purchase'
      AND _TABLE_SUFFIX = FORMAT_DATE('%Y%m%d', CURRENT_DATE())
    ORDER BY event_timestamp DESC
    LIMIT 10
    

    Time: 15 minutes


    Step 10: Connect to Cogny

    If BigQuery already connected to Cogny, you're done.

    Cogny automatically:

    1. Discovers new events
    2. Merges browser and server events (via event_id)
    3. Analyzes complete dataset
    4. Generates insights

    Time: Already done if BigQuery connected


    Advanced: User Session Tracking

    Problem: Server-side tracking doesn't automatically track sessions.

    Solution: Generate session_id on first page load, store in cookie, include in all server events.

    Client-side:

    // Generate or retrieve session ID
    function getSessionId() {
      let sessionId = sessionStorage.getItem('session_id');
    
      if (!sessionId) {
        sessionId = `${Date.now()}_${Math.random().toString(36).substring(2)}`;
        sessionStorage.setItem('session_id', sessionId);
      }
    
      return sessionId;
    }
    
    // Send with requests
    fetch('/api/purchase', {
      method: 'POST',
      headers: {
        'X-Session-ID': getSessionId()
      },
      body: JSON.stringify(orderData)
    });
    

    Server-side:

    app.post('/api/purchase', async (req, res) => {
      const sessionId = req.headers['x-session-id'];
    
      await sendGA4Event(clientId, 'purchase', {
        transaction_id: orderId,
        value: orderTotal,
        session_id: sessionId // Include in event
      });
    });
    

    Time: 15 minutes


    Advanced: Offline Event Tracking

    Track events that happen outside the browser:

    Phone orders:

    // Admin creates order via phone
    app.post('/admin/phone-order', async (req, res) => {
      const order = await createPhoneOrder(req.body);
    
      await sendGA4Event(
        `phone_${order.customerId}`, // Special client ID for offline
        'purchase',
        {
          transaction_id: order.id,
          value: order.total,
          currency: 'USD',
          traffic_source: 'phone'
        },
        {
          user_id: order.customerId
        }
      );
    
      // Also to Meta Conversions API
      await sendMetaConversion('Purchase', {
        event_id: `purchase_${order.id}`,
        source_url: 'https://yoursite.com/admin/phone-orders',
        value: order.total,
        currency: 'USD'
      }, {
        email: order.customerEmail,
        phone: order.customerPhone,
        action_source: 'phone_call' // Indicates offline
      });
    });
    

    In-store purchases (if you have physical locations):

    // POS system sends to your backend
    app.post('/api/pos-purchase', async (req, res) => {
      const sale = await recordPOSSale(req.body);
    
      await sendGA4Event(
        `store_${sale.customerId}`,
        'purchase',
        {
          transaction_id: sale.id,
          value: sale.total,
          currency: 'USD',
          traffic_source: 'physical_store',
          store_location: sale.storeId
        }
      );
    });
    

    Time: 20 minutes


    Advanced: Data Enrichment

    Problem: Browser only knows what user does. Server knows MORE.

    Enrich events with server-side data:

    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
      const user = await getUser(req.user.id);
    
      // Enrich with data browser doesn't have
      await sendGA4Event(clientId, 'purchase', {
        transaction_id: order.id,
        value: order.total,
        currency: 'USD',
    
        // Enrichment from server
        customer_lifetime_orders: user.totalOrders,
        customer_lifetime_value: user.totalSpent,
        customer_account_age_days: daysSince(user.createdAt),
        customer_is_vip: user.isVIP,
        customer_segment: user.segment, // 'high_value', 'at_risk', etc.
        order_profit_margin: calculateMargin(order.items),
        order_has_discount: order.discountAmount > 0,
        first_time_buyer: user.totalOrders === 1
      });
    });
    

    Cogny's AI uses this enrichment to find patterns:

    "First-time buyers who use 20%+ discount:

    • 6-month LTV: $95

    First-time buyers with no discount:

    • 6-month LTV: $287

    Recommendation: Replace deep first-order discounts with loyalty rewards."

    Time: 20 minutes


    Best Practices

    1. Always include user_id for logged-in users

    Enables cross-device tracking and better attribution.

    2. Use consistent event naming

    // Good
    'trial_start'
    'purchase'
    'subscription_renewed'
    
    // Bad
    'trialStarted'
    'PURCHASE'
    'subscription-renewed'
    

    3. Track both successes and failures

    try {
      const order = await createOrder(req.body);
      await sendGA4Event(clientId, 'purchase', {...});
    } catch (error) {
      // Track failed purchase too!
      await sendGA4Event(clientId, 'purchase_failed', {
        error_message: error.message,
        error_code: error.code,
        attempted_value: req.body.total
      });
    }
    

    4. Use environment-specific measurement IDs

    const GA4_MEASUREMENT_ID = process.env.NODE_ENV === 'production'
      ? process.env.GA4_PROD_MEASUREMENT_ID
      : process.env.GA4_DEV_MEASUREMENT_ID;
    

    Avoid polluting production data with test events.

    5. Log tracking failures (but don't throw)

    Tracking is auxiliary. Never let tracking failures break user experience.

    try {
      await sendGA4Event(...);
    } catch (error) {
      console.error('Tracking failed:', error);
      // Log to error monitoring (Sentry, etc.)
      // But don't throw or return error to user
    }
    

    6. Batch events when possible

    GA4 Measurement Protocol supports up to 25 events per request.

    async function sendGA4Events(clientId, events) {
      const url = `https://www.google-analytics.com/mp/collect?measurement_id=${GA4_MEASUREMENT_ID}&api_secret=${GA4_API_SECRET}`;
    
      const payload = {
        client_id: clientId,
        events: events // Array of up to 25 events
      };
    
      await axios.post(url, payload);
    }
    
    // Usage
    await sendGA4Events(clientId, [
      { name: 'begin_checkout', params: {...} },
      { name: 'add_payment_info', params: {...} },
      { name: 'purchase', params: {...} }
    ]);
    

    Common Issues

    "Events sent but not appearing in GA4"

    Check:

    1. Measurement ID correct?
    2. API secret valid?
    3. Waiting 24-48 hours for BigQuery export?
    4. Using DebugView to see real-time events?

    "Duplicate events in GA4"

    Not using event_id for deduplication.

    Both browser and server sending same event without matching event_id.

    "User_id not connecting sessions"

    Make sure:

    1. user_id is same format across all events
    2. Included in ALL events (browser and server)
    3. Not changing between sessions

    "High latency when sending events"

    Don't await tracking in your response handler:

    // Bad - user waits for tracking
    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
      await sendGA4Event(...); // User waits here
      res.json({ success: true });
    });
    
    // Good - tracking happens in background
    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
      res.json({ success: true }); // Return immediately
      sendGA4Event(...); // Fire and forget
    });
    

    "BigQuery insert quota exceeded"

    Too many events.

    Solutions:

    1. Batch inserts (up to 10,000 rows per request)
    2. Use streaming inserts (10,000 rows/sec limit)
    3. Increase quota (contact Google Cloud support)

    Pro Tips

    1. Use server-side tracking for critical events only

    Don't server-track everything.

    Server-side (critical):

    • Purchases
    • Signups
    • Subscriptions
    • High-value conversions

    Browser-side (engagement):

    • Page views
    • Scrolls
    • Clicks
    • Video views

    2. Include request metadata

    await sendGA4Event(clientId, 'purchase', {
      transaction_id: orderId,
      value: orderTotal,
    
      // Metadata
      server_timestamp: new Date().toISOString(),
      request_id: req.id,
      api_version: 'v2',
      user_ip: req.ip,
      user_agent: req.headers['user-agent']
    });
    

    Helps debugging and analysis.

    3. Monitor tracking health

    Send tracking metrics to your monitoring system:

    async function sendGA4Event(...) {
      const startTime = Date.now();
    
      try {
        await axios.post(url, payload);
    
        // Track success
        metrics.increment('ga4.events.sent');
        metrics.timing('ga4.events.duration', Date.now() - startTime);
      } catch (error) {
        // Track failure
        metrics.increment('ga4.events.failed');
        console.error('GA4 tracking failed:', error);
      }
    }
    

    Alert when failure rate spikes.

    4. Create event queue for reliability

    For critical events, use message queue (Redis, RabbitMQ, etc.):

    // Publish event to queue
    await queue.publish('analytics_events', {
      type: 'ga4',
      clientId: clientId,
      eventName: 'purchase',
      eventParams: {...}
    });
    
    // Worker consumes queue and sends to GA4
    queue.subscribe('analytics_events', async (message) => {
      await sendGA4Event(
        message.clientId,
        message.eventName,
        message.eventParams
      );
    });
    

    Benefits:

    • Retry failed events automatically
    • Rate limiting
    • Don't lose events if GA4 API is down

    5. Implement consent management

    Respect user privacy preferences:

    app.post('/api/purchase', async (req, res) => {
      const order = await createOrder(req.body);
    
      // Check user's consent preferences
      const consent = await getUserConsent(req.user.id);
    
      if (consent.analytics) {
        await sendGA4Event(...);
      }
    
      if (consent.advertising) {
        await sendMetaConversion(...);
      }
    
      // Always allowed - your own data warehouse
      await sendToBigQuery(...);
    
      res.json({ success: true });
    });
    

    6. Version your event schemas

    await sendGA4Event(clientId, 'purchase', {
      transaction_id: orderId,
      value: orderTotal,
      schema_version: '2.0', // Track which schema version
    
      // v2 fields (added later)
      profit_margin: calculateMargin(order),
      customer_ltv: user.ltv
    });
    

    Makes it easier to evolve tracking over time.


    What You Can Do Now

    Immediate actions:

    1. Implement for critical events

      • Start with purchases
      • Add signups
      • Track subscriptions
    2. Add event deduplication

      • Use event_id consistently
      • Prevent duplicate counting
      • Verify in GA4
    3. Enrich with server data

      • Add LTV
      • Include customer segment
      • Send profit margin
    4. Monitor tracking health

      • Set up error logging
      • Track success rate
      • Alert on failures

    Next Steps

    Immediate actions:

    • Implement server-side tracking for purchases and signups first
    • Add event deduplication to prevent double-counting

    Advanced implementation:

    Need help? Contact support


    FAQ

    Q: Should I replace browser tracking entirely?

    No. Use hybrid approach:

    Browser: Immediate context, user behavior, engagement Server: Critical conversions, enriched data, reliability

    Both together = best data quality.

    Q: Will this increase server costs?

    Minimal.

    Each event = 1 HTTP request (~1-2KB) 1,000 purchases/day = ~2MB/day in outgoing bandwidth

    Negligible impact.

    Q: How do I track events if user isn't logged in?

    Use client_id from GA cookie. Pass from browser to backend.

    For truly anonymous events (no cookies), generate unique client_id per session.

    Q: Can I send historical events?

    Yes.

    Include timestamp in past:

    await sendGA4Event(clientId, 'purchase', {
      transaction_id: historicalOrder.id,
      value: historicalOrder.total,
      timestamp_micros: new Date(historicalOrder.createdAt).getTime() * 1000
    });
    

    But GA4 may reject events older than 72 hours.

    For historical data, write directly to BigQuery instead.

    Q: What about GDPR?

    Server-side tracking still requires consent if you're processing personal data.

    Implement consent checks before sending to GA4/Meta.

    Your own data warehouse: Generally OK (you're data controller), but still follow privacy policy.

    Q: How many events can I send?

    GA4 Measurement Protocol:

    • No documented hard limit
    • Recommended: <25 events per request
    • Rate limit: ~1,000 requests/second per property

    Meta Conversions API:

    • 1,000 events per request
    • Rate limit: Varies by account

    BigQuery:

    • Streaming inserts: 100,000 rows/second per table
    • Batch inserts: No limit (but 10GB per request recommended)

    For most businesses: No practical limit.

    Q: Can I track mobile app events this way?

    Yes.

    Same GA4 Measurement Protocol works for:

    • Web
    • Mobile apps
    • Desktop apps
    • IoT devices
    • Anywhere you can make HTTP requests

    Ready for 100% Reliable Tracking?

    Browser-based tracking loses 20-40% of events.

    You're optimizing campaigns with incomplete data. Making decisions based on partial reality.

    Server-side tracking gives you the full picture.

    Every conversion tracked. Every user journey complete. AI insights based on 100% of data, not 60%.

    Not implemented yet?

    Schedule a demo and we'll show you exactly what data you're missing and how to capture it.


    About This Guide

    Written by the Cogny team—built by the founders who created AI optimization systems for Netflix, Zalando, and Momondo at Campanja, and scaled growth for Kry, Epidemic Sound, and Yubico through GrowthHackers.se.

    We've implemented server-side tracking for hundreds of applications. This is the proven architecture.

    Last Updated: January 9, 2025

    Ready to Get Started?

    See Cogny in Action

    Schedule a demo to see how AI can transform your marketing analytics and automate your growth optimization.

    Schedule Demo