GA4 Event Tracking for Directus | OpsBlu Docs

GA4 Event Tracking for Directus

Implement custom event tracking for Directus-powered sites including content engagement, user interactions, and custom dimensions.

Track user interactions and content engagement on your Directus-powered site. Since Directus is headless, all event tracking is implemented in your frontend application code.

Directus Content Events

Content View Events

Track when users view Directus content:

// utils/analytics.ts
export function trackContentView(item: any) {
  if (typeof window === 'undefined' || !window.gtag) return;

  window.gtag('event', 'content_view', {
    content_type: item.__collection || 'unknown',
    content_id: item.id,
    title: item.title || item.name,
    status: item.status,
    date_created: item.date_created,
    user_created: item.user_created?.id || 'unknown',
  });
}

// Usage in Next.js component
'use client';

import { useEffect } from 'react';
import { trackContentView } from '@/utils/analytics';

export function ArticlePage({ article }) {
  useEffect(() => {
    trackContentView(article);
  }, [article]);

  return <article>{/* Content */}</article>;
}

Content Engagement Tracking

Track scroll depth and time on page:

// components/ContentEngagementTracker.tsx
'use client';

import { useEffect, useState } from 'react';

export function ContentEngagementTracker({ contentId, contentType }) {
  const [maxScroll, setMaxScroll] = useState(0);
  const [startTime] = useState(Date.now());

  useEffect(() => {
    const handleScroll = () => {
      const scrollPercentage = Math.round(
        (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100
      );

      if (scrollPercentage > maxScroll) {
        setMaxScroll(scrollPercentage);

        // Track scroll milestones
        if ([25, 50, 75, 90].includes(scrollPercentage)) {
          window.gtag?.('event', 'scroll', {
            content_id: contentId,
            content_type: contentType,
            percent_scrolled: scrollPercentage,
          });
        }
      }
    };

    window.addEventListener('scroll', handleScroll, { passive: true });

    // Track time on page when leaving
    const handleBeforeUnload = () => {
      const timeOnPage = Math.round((Date.now() - startTime) / 1000);

      window.gtag?.('event', 'content_engagement', {
        content_id: contentId,
        content_type: contentType,
        time_on_page: timeOnPage,
        max_scroll_depth: maxScroll,
      });
    };

    window.addEventListener('beforeunload', handleBeforeUnload);

    return () => {
      window.removeEventListener('scroll', handleScroll);
      window.removeEventListener('beforeunload', handleBeforeUnload);
    };
  }, [contentId, contentType, maxScroll, startTime]);

  return null;
}

Collection Filtering Events

Track when users filter Directus collections:

// Track collection filters
export function trackCollectionFilter(collection: string, filters: any) {
  window.gtag?.('event', 'filter_content', {
    collection: collection,
    filter_count: Object.keys(filters).length,
    filters: JSON.stringify(filters),
  });
}

// Usage example
const [filters, setFilters] = useState({});

const handleFilterChange = (newFilters) => {
  setFilters(newFilters);
  trackCollectionFilter('articles', newFilters);
};

User Interaction Events

Search Events

Track Directus content searches:

// components/SearchBar.tsx
'use client';

import { useState } from 'react';
import { createDirectus, rest, readItems } from '@directus/sdk';

const directus = createDirectus(process.env.NEXT_PUBLIC_DIRECTUS_URL!).with(rest());

export function SearchBar() {
  const [query, setQuery] = useState('');

  const handleSearch = async (e: React.FormEvent) => {
    e.preventDefault();

    // Track search event
    window.gtag?.('event', 'search', {
      search_term: query,
      search_type: 'directus_content',
    });

    // Perform search
    const results = await directus.request(
      readItems('articles', {
        search: query,
      })
    );

    // Track search results
    window.gtag?.('event', 'view_search_results', {
      search_term: query,
      results_count: results.length,
    });
  };

  return (
    <form
      <input
        value={query} => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <button type="submit">Search</button>
    </form>
  );
}

File Download Tracking

Track downloads from Directus assets:

// components/DownloadButton.tsx
'use client';

export function DownloadButton({ asset, label }) {
  const handleDownload = () => {
    window.gtag?.('event', 'file_download', {
      file_name: asset.filename_download,
      file_type: asset.type,
      file_size: asset.filesize,
      link_text: label,
      link_url: `${process.env.NEXT_PUBLIC_DIRECTUS_URL}/assets/${asset.id}`,
    });
  };

  return (
    <a
      href={`${process.env.NEXT_PUBLIC_DIRECTUS_URL}/assets/${asset.id}`}
      download={asset.filename_download}
    >
      {label || `Download ${asset.title}`}
    </a>
  );
}

Relation Tracking

Track when users navigate related Directus items:

export function trackRelationClick(fromCollection: string, toCollection: string, itemId: string) {
  window.gtag?.('event', 'click_relation', {
    from_collection: fromCollection,
    to_collection: toCollection,
    item_id: itemId,
  });
}

// Usage
<a
  href={`/authors/${article.author.id}`} => trackRelationClick('articles', 'authors', article.author.id)}
>
  {article.author.name}
</a>

Form Events

Directus Form Submissions

Track form submissions that create Directus items:

// components/ContactForm.tsx
'use client';

import { createDirectus, rest, createItem } from '@directus/sdk';

const directus = createDirectus(process.env.NEXT_PUBLIC_DIRECTUS_URL!).with(rest());

export function ContactForm() {
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);

    window.gtag?.('event', 'generate_lead', {
      value: 0,
      currency: 'USD',
      form_name: 'contact_form',
    });

    try {
      await directus.request(
        createItem('contacts', {
          name: formData.get('name'),
          email: formData.get('email'),
          message: formData.get('message'),
        })
      );

      window.gtag?.('event', 'form_submit', {
        form_name: 'contact_form',
        success: true,
      });
    } catch (error) {
      window.gtag?.('event', 'form_submit', {
        form_name: 'contact_form',
        success: false,
        error_message: error.message,
      });
    }
  };

  return (
    <form
      {/* Form fields */}
    </form>
  );
}

Custom Dimensions & Metrics

Set Up Custom Dimensions

Track Directus-specific data as custom dimensions:

// Set user properties based on Directus data
export function setUserPreferences(user: any) {
  window.gtag?.('set', 'user_properties', {
    user_role: user.role?.name || 'guest',
    user_status: user.status,
    preferred_language: user.language,
  });
}

// Set custom dimensions on page view
export function trackPageWithDimensions(item: any) {
  window.gtag?.('event', 'page_view', {
    // Custom dimensions
    collection_name: item.__collection,
    item_status: item.status,
    date_created: item.date_created,
    date_updated: item.date_updated,
    user_created: item.user_created?.id,
    translations_available: item.translations?.length || 0,
  });
}

E-commerce Events (Directus Commerce)

If using Directus for e-commerce:

View Item

export function trackProductView(product: any) {
  window.gtag?.('event', 'view_item', {
    currency: 'USD',
    value: product.price,
    items: [
      {
        item_id: product.id || product.sku,
        item_name: product.name,
        item_brand: product.brand,
        item_category: product.category?.name,
        price: product.price,
        quantity: 1,
      },
    ],
  });
}

// Usage in product page
'use client';

export function ProductPage({ product }) {
  useEffect(() => {
    trackProductView(product);
  }, [product]);

  return <div>{/* Product details */}</div>;
}

Add to Cart

export function AddToCartButton({ product, quantity = 1 }) {
  const handleAddToCart = async () => {
    window.gtag?.('event', 'add_to_cart', {
      currency: 'USD',
      value: product.price * quantity,
      items: [
        {
          item_id: product.id || product.sku,
          item_name: product.name,
          item_brand: product.brand,
          item_category: product.category?.name,
          price: product.price,
          quantity: quantity,
        },
      ],
    });

    // Add to cart logic...
  };

  return <button to Cart</button>;
}

Framework-Specific Implementations

Next.js Event Wrapper

Create a reusable event tracking hook:

// hooks/useAnalytics.ts
import { useCallback } from 'react';

export function useAnalytics() {
  const trackEvent = useCallback((
    eventName: string,
    eventParams?: Record<string, any>
  ) => {
    if (typeof window !== 'undefined' && window.gtag) {
      window.gtag('event', eventName, eventParams);
    }
  }, []);

  const trackDirectusItem = useCallback((item: any) => {
    trackEvent('content_view', {
      collection: item.__collection,
      item_id: item.id,
      title: item.title || item.name,
      status: item.status,
    });
  }, [trackEvent]);

  return { trackEvent, trackDirectusItem };
}

// Usage
const { trackEvent, trackDirectusItem } = useAnalytics();

useEffect(() => {
  trackDirectusItem(directusItem);
}, [directusItem, trackDirectusItem]);

Vue/Nuxt Composable

// composables/useAnalytics.ts
export const useAnalytics = () => {
  const trackEvent = (eventName: string, params?: any) => {
    if (process.client && window.gtag) {
      window.gtag('event', eventName, params);
    }
  };

  const trackDirectusItem = (item: any) => {
    trackEvent('content_view', {
      collection: item.__collection,
      item_id: item.id,
      title: item.title,
    });
  };

  return {
    trackEvent,
    trackDirectusItem,
  };
};

Usage in component:

<script setup>
const { trackDirectusItem } = useAnalytics();

onMounted(() => {
  trackDirectusItem(item.value);
});
</script>

Testing Events

GA4 DebugView

Enable debug mode to test events:

// Enable debug mode in development
const debugMode = process.env.NODE_ENV === 'development';

gtag('config', GA_MEASUREMENT_ID, {
  debug_mode: debugMode,
});

View events:

  1. Go to GA4 AdminDebugView
  2. Navigate your site
  3. See events appear in real-time with full parameters

Console Logging

Log events during development:

function trackEvent(eventName: string, params?: any) {
  if (process.env.NODE_ENV === 'development') {
    console.log('GA4 Event:', eventName, params);
  }

  if (window.gtag) {
    window.gtag('event', eventName, params);
  }
}

Best Practices

1. Consistent Event Naming

Use standard GA4 event names when possible:

  • page_view - Page views
  • search - Searches
  • sign_up - User signups
  • generate_lead - Lead generation
  • view_item - Content views
  • file_download - Downloads

2. Avoid PII

Never send personally identifiable information:

// Bad
window.gtag('event', 'sign_up', {
  email: user.email,      // DON'T
  name: user.name,        // DON'T
});

// Good
window.gtag('event', 'sign_up', {
  user_id: hashUserId(user.id),  // Hashed
  user_role: user.role.name,
});

3. Track Directus Status Changes

export function trackStatusChange(collection: string, itemId: string, oldStatus: string, newStatus: string) {
  window.gtag?.('event', 'status_change', {
    collection: collection,
    item_id: itemId,
    old_status: oldStatus,
    new_status: newStatus,
  });
}

Next Steps

For general GA4 event concepts, see GA4 Event Tracking Guide.