Storyblok Event Tracking with Google Analytics 4 | OpsBlu Docs

Storyblok Event Tracking with Google Analytics 4

Track Storyblok component interactions, Visual Editor usage, and custom events with GA4

Storyblok's component-based architecture and Visual Editor require custom event tracking implementation. This guide shows how to track Storyblok-specific interactions with GA4.

Storyblok Event Categories

Component Events

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

Visual Editor Events

  • Editor Accessed - User enters Visual Editor mode
  • Editor Story Viewed - Specific story viewed in editor
  • Editor Preview - Preview button clicked in editor

Story Events

  • Story View - Storyblok story rendered
  • Link Click - Internal story link clicked
  • External Link Click - External link clicked

Event Tracking Implementation

Method 1: Tracking Component Views (Nuxt 3)

Create a composable for tracking:

// composables/useComponentTracking.ts
export const useComponentTracking = (blok: any) => {
  const hasTracked = ref(false);
  const elementRef = ref<HTMLElement | null>(null);

  onMounted(() => {
    if (!elementRef.value || hasTracked.value) return;

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

            if (typeof window.gtag !== 'undefined') {
              window.gtag('event', 'view_item', {
                event_category: 'component',
                event_label: blok.component,
                component_type: blok.component,
                component_uid: blok._uid,
              });
            }
          }
        });
      },
      { threshold: 0.5 }
    );

    observer.observe(elementRef.value);

    onUnmounted(() => observer.disconnect());
  });

  return { elementRef };
};

Usage in component:

<!-- components/HeroSection.vue -->
<template>
  <div ref="elementRef" v-editable="blok" class="hero">
    <h1>{{ blok.title }}</h1>
    <button @click="handleCTAClick">{{ blok.cta_text }}</button>
  </div>
</template>

<script setup>
const { blok } = defineProps(['blok']);
const { elementRef } = useComponentTracking(blok);

const handleCTAClick = () => {
  if (typeof window.gtag !== 'undefined') {
    window.gtag('event', 'click', {
      event_category: 'component_cta',
      event_label: 'hero_cta',
      component_type: blok.component,
      cta_text: blok.cta_text,
    });
  }
};
</script>

Method 2: Tracking Visual Editor Usage (Nuxt 3)

// plugins/storyblok-editor-tracking.client.ts
export default defineNuxtPlugin(() => {
  const route = useRoute();

  // Detect Visual Editor
  const isEditor = computed(() => route.query._storyblok !== undefined);

  watch(isEditor, (inEditor) => {
    if (inEditor && typeof window.gtag !== 'undefined') {
      window.gtag('set', 'user_properties', {
        editor_mode: 'active',
      });

      window.gtag('event', 'visual_editor_accessed', {
        event_category: 'editor',
        event_label: 'Storyblok Visual Editor Active',
      });
    }
  }, { immediate: true });

  // Track editor story changes
  if (window.storyblok) {
    window.storyblok.on(['input'], (event) => {
      if (typeof window.gtag !== 'undefined') {
        window.gtag('event', 'editor_content_changed', {
          event_category: 'editor',
          event_label: 'Content Modified in Editor',
          story_id: event.story.id,
        });
      }
    });
  }
});

Method 3: Tracking Story Views

<!-- pages/[...slug].vue (Nuxt 3) -->
<script setup>
const { slug } = useRoute().params;
const story = await useAsyncStoryblok(slug.join('/'), { version: 'draft' });

onMounted(() => {
  if (story.value && typeof window.gtag !== 'undefined') {
    window.gtag('event', 'view_story', {
      event_category: 'storyblok_content',
      event_label: story.value.content.component,
      story_id: story.value.id,
      story_name: story.value.name,
      story_slug: story.value.slug,
      created_at: story.value.created_at,
      published_at: story.value.published_at,
    });
  }
});
</script>
// components/StoryblokLink.js
'use client';

import Link from 'next/link';
import { storyblokEditable } from '@storyblok/react';

export default function StoryblokLink({ blok }) {
  const handleClick = () => {
    const isExternal = blok.link.linktype === 'url';

    if (typeof window.gtag !== 'undefined') {
      window.gtag('event', 'click', {
        event_category: isExternal ? 'outbound_link' : 'internal_link',
        event_label: blok.link.cached_url || blok.link.url,
        link_type: blok.link.linktype,
        component_type: blok.component,
      });
    }
  };

  return (
    <Link
      href={blok.link.cached_url || blok.link.url}
      {...storyblokEditable(blok)}
    >
      {blok.text}
    </Link>
  );
}

Method 5: Tracking Form Submissions

<!-- components/ContactForm.vue (Nuxt 3) -->
<template>
  <form @submit.prevent="handleSubmit" v-editable="blok">
    <input v-model="email" type="email" required />
    <button type="submit">Submit</button>
  </form>
</template>

<script setup>
const { blok } = defineProps(['blok']);
const email = ref('');

const handleSubmit = async () => {
  if (typeof window.gtag !== 'undefined') {
    window.gtag('event', 'generate_lead', {
      event_category: 'form',
      event_label: 'contact_form_submit',
      component_type: blok.component,
      form_name: blok.form_name,
    });
  }

  // Submit form logic
};
</script>

Create in GA4 Console

Navigate to GA4 → Configure → Events → Create Event:

  1. component_interaction

    • Event name: click
    • Match condition: event_category = component_cta
  2. editor_usage

    • Event name: visual_editor_accessed
    • Parameter: event_category = editor
  3. story_engagement

    • Event name: view_story
    • Parameter: story_id exists

Custom Dimensions

Set up in GA4 → Configure → Custom Definitions:

Dimension Name Parameter Scope
Component Type component_type Event
Component UID component_uid Event
Story ID story_id Event
Story Name story_name Event
Editor Mode editor_mode User

Advanced Tracking Patterns

Track Content Journey

// composables/useStoryblokJourney.ts
export const useStoryblokJourney = () => {
  const journey = ref<Array<any>>([]);

  const trackStoryView = (story: any) => {
    journey.value.push({
      id: story.id,
      name: story.name,
      component: story.content.component,
      timestamp: Date.now(),
    });

    if (typeof window.gtag !== 'undefined') {
      window.gtag('event', 'content_journey', {
        event_category: 'engagement',
        event_label: 'Story Path',
        journey_depth: journey.value.length,
        story_sequence: journey.value.map(s => s.name).join(' > '),
      });
    }
  };

  return { trackStoryView };
};

Performance Considerations

Throttle Component Tracking

const useThrottledTracking = (callback: Function, delay = 1000) => {
  const timeoutRef = ref<number | null>(null);
  const lastCallRef = ref(0);

  return (...args: any[]) => {
    const now = Date.now();
    if (now - lastCallRef.value < delay) return;

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

Testing Event Tracking

1. DebugView

Enable debug mode:

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

Check GA4 → Admin → DebugView for real-time events.

2. Browser Console

// Test event manually
gtag('event', 'test_component_view', {
  component_type: 'hero',
  test: true,
});

// Verify dataLayer
console.log(window.dataLayer);

Next Steps