Prismic Event Tracking with Google Analytics 4 | OpsBlu Docs

Prismic Event Tracking with Google Analytics 4

Track Prismic Slice interactions, preview mode usage, and custom component events with GA4

Prismic's component-based architecture with Slices and preview functionality requires custom event tracking implementation. This guide shows how to track Prismic-specific interactions with GA4.

Prismic Event Categories

Slice Events

  • Slice View - Individual Slice rendered on page
  • Slice Interaction - User interacts with Slice component
  • Slice CTA Click - Call-to-action button in Slice clicked
  • Slice Form Submit - Form within Slice submitted

Preview Events

  • Preview Mode Accessed - Editor enters preview mode
  • Preview Document Viewed - Specific document previewed
  • Preview Exit - User exits preview mode

Content Events

  • Document View - Prismic document rendered
  • Link Click - Internal Prismic link clicked
  • External Link Click - External link in content clicked
  • Media View - Image or media from Prismic loaded

Event Tracking Implementation

Method 1: Tracking Slice Interactions (Next.js)

Create a wrapper component for tracking Slices:

// components/TrackedSlice.js
'use client';

import { useEffect, useRef } from 'react';

export function TrackedSlice({ slice, children }) {
  const sliceRef = useRef(null);
  const hasTracked = useRef(false);

  useEffect(() => {
    if (!sliceRef.current || hasTracked.current) return;

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !hasTracked.current) {
            hasTracked.current = true;

            if (typeof window.gtag !== 'undefined') {
              window.gtag('event', 'view_item', {
                event_category: 'slice',
                event_label: slice.slice_type,
                slice_type: slice.slice_type,
                slice_variation: slice.variation,
                slice_id: slice.id,
              });
            }
          }
        });
      },
      { threshold: 0.5 }
    );

    observer.observe(sliceRef.current);

    return () => observer.disconnect();
  }, [slice]);

  return <div ref={sliceRef}>{children}</div>;
}

Usage in Slice components:

// slices/HeroSlice.js
import { TrackedSlice } from '@/components/TrackedSlice';

export default function HeroSlice({ slice }) {
  return (
    <TrackedSlice slice={slice}>
      <section className="hero">
        <h1>{slice.primary.title}</h1>
        <p>{slice.primary.description}</p>
        <button => {
            if (typeof window.gtag !== 'undefined') {
              window.gtag('event', 'click', {
                event_category: 'slice_cta',
                event_label: 'hero_cta',
                slice_type: slice.slice_type,
                cta_text: slice.primary.cta_text,
              });
            }
          }}
        >
          {slice.primary.cta_text}
        </button>
      </section>
    </TrackedSlice>
  );
}

Method 2: Tracking Preview Mode

Create a preview tracking component:

// components/PrismicPreviewTracking.js
'use client';

import { useEffect } from 'react';
import { usePathname, useSearchParams } from 'next/navigation';

export function PrismicPreviewTracking() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const isPreview = searchParams.get('preview') === 'true';

  useEffect(() => {
    if (isPreview && typeof window.gtag !== 'undefined') {
      // Set preview mode as user property
      window.gtag('set', 'user_properties', {
        preview_mode: 'active',
      });

      // Track preview access
      window.gtag('event', 'prismic_preview_accessed', {
        event_category: 'preview',
        event_label: 'Preview Mode Active',
        page_path: pathname,
      });
    }
  }, [isPreview, pathname]);

  // Track preview exit
  useEffect(() => {
    if (!isPreview && typeof window.gtag !== 'undefined') {
      window.gtag('set', 'user_properties', {
        preview_mode: 'inactive',
      });
    }
  }, [isPreview]);

  return null;
}

Add to layout:

// app/layout.js
import { PrismicPreviewTracking } from '@/components/PrismicPreviewTracking';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <PrismicPreviewTracking />
      </body>
    </html>
  );
}

Method 3: Tracking Prismic Document Views

Track when Prismic documents are viewed:

// In your page component
import { useEffect } from 'react';

export default function PrismicPage({ document }) {
  useEffect(() => {
    if (document && typeof window.gtag !== 'undefined') {
      window.gtag('event', 'view_document', {
        event_category: 'prismic_content',
        event_label: document.type,
        document_id: document.id,
        document_type: document.type,
        document_uid: document.uid,
        document_tags: document.tags.join(','),
        last_published: document.last_publication_date,
        language: document.lang,
      });
    }
  }, [document]);

  return (
    <div>
      {/* Render document content */}
    </div>
  );
}

Create a custom Link component:

// components/PrismicLink.js
'use client';

import { PrismicNextLink } from '@prismicio/next';

export function TrackedPrismicLink({ field, children, ...props }) {
  const handleClick = () => {
    if (typeof window.gtag !== 'undefined') {
      const isExternal = field.link_type === 'Web';

      window.gtag('event', 'click', {
        event_category: isExternal ? 'outbound_link' : 'internal_link',
        event_label: field.url || field.uid,
        link_type: field.link_type,
        link_url: field.url,
        link_uid: field.uid,
      });
    }
  };

  return (
    <PrismicNextLink field={field} {...props}>
      {children}
    </PrismicNextLink>
  );
}

Usage:

import { TrackedPrismicLink } from '@/components/PrismicLink';

export function MyComponent({ slice }) {
  return (
    <TrackedPrismicLink field={slice.primary.link}>
      {slice.primary.link_text}
    </TrackedPrismicLink>
  );
}

Method 5: Tracking Slice Form Submissions

For forms within Slices:

// slices/ContactFormSlice.js
'use client';

import { useState } from 'react';

export default function ContactFormSlice({ slice }) {
  const [formData, setFormData] = useState({});

  const handleSubmit = async (e) => {
    e.preventDefault();

    // Track form submission
    if (typeof window.gtag !== 'undefined') {
      window.gtag('event', 'generate_lead', {
        event_category: 'form',
        event_label: 'contact_form_submit',
        slice_type: slice.slice_type,
        form_name: slice.primary.form_name,
      });
    }

    // Submit form data
    // ... form submission logic
  };

  return (
    <form
      <input
        type="email" => setFormData({ ...formData, email: e.target.value })}
        required
      />
      <button type="submit">Submit</button>
    </form>
  );
}

Method 6: Tracking Media Interactions (Gatsby)

For Gatsby with Prismic:

// components/PrismicImage.js
import { GatsbyImage, getImage } from 'gatsby-plugin-image';
import { useEffect, useRef } from 'react';

export function TrackedPrismicImage({ image, alt }) {
  const imageRef = useRef(null);
  const hasTracked = useRef(false);

  useEffect(() => {
    if (!imageRef.current || hasTracked.current) return;

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !hasTracked.current) {
            hasTracked.current = true;

            if (typeof window.gtag !== 'undefined') {
              window.gtag('event', 'image_view', {
                event_category: 'media',
                event_label: 'prismic_image_loaded',
                image_url: image.url,
                image_dimensions: `${image.dimensions.width}x${image.dimensions.height}`,
              });
            }
          }
        });
      },
      { threshold: 0.1 }
    );

    observer.observe(imageRef.current);

    return () => observer.disconnect();
  }, [image]);

  const imageData = getImage(image);

  return (
    <div ref={imageRef}>
      <GatsbyImage image={imageData} alt={alt} />
    </div>
  );
}

Gatsby-Specific Event Tracking

Track Page Transitions

In gatsby-browser.js:

export const location, prevLocation }) => {
  if (typeof window.gtag !== 'undefined') {
    window.gtag('config', 'G-XXXXXXXXXX', {
      page_path: location.pathname + location.search,
    });

    // Track if coming from Prismic preview
    if (location.search.includes('preview=true')) {
      window.gtag('event', 'preview_page_view', {
        event_category: 'preview',
        event_label: 'Preview Page View',
        page_path: location.pathname,
      });
    }
  }
};

Track Prismic Build Time

// gatsby-node.js
exports.onPostBuild = ({ reporter }) => {
  reporter.info('Prismic build completed');

  // Send build event to GA4 (server-side with Measurement Protocol)
  // This requires additional setup with GA4 Measurement Protocol API
};

Advanced Tracking Patterns

Slice Zone Tracking

Track entire Slice Zone rendering:

// components/TrackedSliceZone.js
'use client';

import { SliceZone } from '@prismicio/react';
import { useEffect } from 'react';
import { components } from '@/slices';

export function TrackedSliceZone({ slices }) {
  useEffect(() => {
    if (slices && slices.length > 0 && typeof window.gtag !== 'undefined') {
      // Track Slice Zone composition
      window.gtag('event', 'slice_zone_loaded', {
        event_category: 'content',
        event_label: 'Slice Zone Rendered',
        slice_count: slices.length,
        slice_types: slices.map(s => s.slice_type).join(','),
      });
    }
  }, [slices]);

  return <SliceZone slices={slices} components={components} />;
}

User Journey Tracking

Track content discovery path:

// hooks/usePrismicJourney.js
'use client';

import { useEffect, useRef } from 'react';
import { usePathname } from 'next/navigation';

export function usePrismicJourney(document) {
  const pathname = usePathname();
  const journeyRef = useRef([]);

  useEffect(() => {
    if (document) {
      journeyRef.current.push({
        type: document.type,
        uid: document.uid,
        timestamp: Date.now(),
      });

      if (typeof window.gtag !== 'undefined') {
        window.gtag('event', 'content_journey', {
          event_category: 'engagement',
          event_label: 'Content Path',
          journey_depth: journeyRef.current.length,
          content_types: journeyRef.current.map(d => d.type).join(' > '),
        });
      }
    }
  }, [document]);
}

Rich Text Interaction Tracking

Track interactions with Prismic Rich Text fields:

// components/TrackedRichText.js
'use client';

import { PrismicRichText } from '@prismicio/react';
import { useRef, useEffect } from 'react';

export function TrackedRichText({ field }) {
  const contentRef = useRef(null);

  useEffect(() => {
    if (!contentRef.current) return;

    const handleLinkClick = (e) => {
      const link = e.target.closest('a');
      if (link && typeof window.gtag !== 'undefined') {
        window.gtag('event', 'click', {
          event_category: 'rich_text_link',
          event_label: link.href,
          link_text: link.textContent,
        });
      }
    };

    contentRef.current.addEventListener('click', handleLinkClick);

    return () => {
      contentRef.current?.removeEventListener('click', handleLinkClick);
    };
  }, []);

  return (
    <div ref={contentRef}>
      <PrismicRichText field={field} />
    </div>
  );
}

Create in GA4 Console

Navigate to GA4 → Configure → Events → Create Event:

  1. slice_interaction

    • Event name: click
    • Match condition: event_category = slice_cta
  2. preview_usage

    • Event name: prismic_preview_accessed
    • Parameter: event_category = preview
  3. document_engagement

    • Event name: view_document
    • Parameter: document_type exists
  4. high_engagement

    • Event name: slice_zone_loaded
    • Match condition: slice_count >= 5

Custom Dimensions

Set up in GA4 → Configure → Custom Definitions:

Dimension Name Parameter Scope
Slice Type slice_type Event
Slice Variation slice_variation Event
Document Type document_type Event
Document UID document_uid Event
Preview Mode preview_mode User
Language language Event

Performance Considerations

Throttle Scroll Tracking

Prevent excessive events:

function useThrottledTracking(callback, delay = 1000) {
  const timeoutRef = useRef(null);
  const lastCallRef = useRef(0);

  return (...args) => {
    const now = Date.now();
    if (now - lastCallRef.current < delay) return;

    clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => {
      callback(...args);
      lastCallRef.current = now;
    }, 100);
  };
}

Batch Events

Consolidate multiple events:

const eventQueue = [];
const BATCH_SIZE = 5;

function queueEvent(eventName, params) {
  eventQueue.push({ name: eventName, params });

  if (eventQueue.length >= BATCH_SIZE) {
    flushEvents();
  }
}

function flushEvents() {
  if (typeof window.gtag !== 'undefined') {
    eventQueue.forEach(({ name, params }) => {
      window.gtag('event', name, params);
    });
  }
  eventQueue.length = 0;
}

// Flush on page unload
window.addEventListener('beforeunload', flushEvents);

Testing Event Tracking

1. DebugView

Enable debug mode:

gtag('config', 'G-XXXXXXXXXX', {
  'debug_mode': true
});

Check GA4 → Admin → DebugView to see events in real-time.

2. Browser Console

// Log all dataLayer pushes
window.dataLayer.push = new Proxy(window.dataLayer.push, {
  apply(target, thisArg, args) {
    console.log('GA4 Event:', args);
    return target.apply(thisArg, args);
  }
});

3. Google Tag Assistant

Install Tag Assistant and validate events fire correctly.

Common Issues

Events Not Firing in Preview

Cause: Preview mode may block scripts

Solution: Ensure gtag loads in preview:

// Don't exclude preview from analytics
if (isPreview || typeof window.gtag !== 'undefined') {
  window.gtag('event', 'preview_accessed');
}

Duplicate Slice Events

Cause: Slice re-renders trigger multiple events

Solution: Use hasTracked ref (see TrackedSlice example above)

Events Fire Too Frequently

Cause: No throttling on scroll/interaction events

Solution: Implement throttling (see Performance section)

Next Steps