Skip to content

Comprehensive Engagement Tracking

Learn how to implement comprehensive engagement tracking across multiple touchpoints, based on the FanFest FC loyalty demo.

Overview

The FanFest FC demo showcases a complete loyalty and engagement system with 50+ different engagement actions across multiple sections:

  • Games - Interactive mini-games and challenges
  • Watch - Video content with progress tracking
  • Read - Article reading with scroll-based rewards
  • Tickets - Match ticket browsing and purchases
  • Connect - Social media follows and newsletter signups
  • Visit - Location check-ins and experiences
  • Shop - Product browsing and purchases
  • Sponsors - Partner reward claims

Engagement Action Mapping

Complete Action Registry

typescript
// Define all engagement actions
enum EngagementAction {
  // Core Actions
  PURCHASED_ITEM = "PURCHASED_ITEM",
  PODCAST_COMPLETE = "PODCAST_COMPLETE",
  BROWSED_TICKETS = "BROWSED_TICKETS",
  BROWSED_SECTION = "BROWSED_SECTION",
  LOCATION_CHECK_IN = "LOCATION_CHECK_IN",
  NEWSLETTER_SIGNUP = "NEWSLETTER_SIGNUP",
  PURCHASED_TICKET = "PURCHASED_TICKET",
  VIDEO_PROGRESS = "VIDEO_PROGRESS",
  VIDEO_WATCH = "VIDEO_WATCH",
  ARTICLE_READING = "ARTICLE_READING",
  READ_ARTICLE = "READ_ARTICLE",
  PLAYED_GAME = "PLAYED_GAME",

  // Social Actions
  SHARE_VIDEO_ON_X = "SHARE_VIDEO_ON_X",
  SHARE_VIDEO_ON_FACEBOOK = "SHARE_VIDEO_ON_FACEBOOK",
  SHARE_VIDEO_ON_INSTAGRAM = "SHARE_VIDEO_ON_INSTAGRAM",
  SHARE_ARTICLE_ON_X = "SHARE_ARTICLE_ON_X",
  SHARE_ARTICLE_ON_FACEBOOK = "SHARE_ARTICLE_ON_FACEBOOK",
  SHARE_ARTICLE_ON_INSTAGRAM = "SHARE_ARTICLE_ON_INSTAGRAM",

  // Social Media Follows
  FOLLOW_US_ON_INSTAGRAM = "FOLLOW_US_ON_INSTAGRAM",
  FOLLOW_US_ON_X = "FOLLOW_US_ON_X",
  FOLLOW_US_ON_TWITCH = "FOLLOW_US_ON_TWITCH",
  SUSCRIBE_ON_YOUTUBE = "SUSCRIBE_ON_YOUTUBE",

  // Partner Rewards
  CLAIM_MC_DONALD_S_REWARD = "CLAIM_MC_DONALD_S_REWARD",
  CLAIM_NIKE_REWARD = "CLAIM_NIKE_REWARD",
  CLAIM_ACCOR_REWARD = "CLAIM_ACCOR_REWARD",
  CLAIM_QATAR_AIRWAYS_REWARD = "CLAIM_QATAR_AIRWAYS_REWARD",
  CLAIM_EA_SPORTS_REWARD = "CLAIM_EA_SPORTS_REWARD",
  CLAIM_SPOTIFY_REWARD = "CLAIM_SPOTIFY_REWARD",

  // Game-Specific Actions
  PLAYED_GAME_LIVELIKE_SQUAD_BUILDER = "PLAYED_GAME_LIVELIKE_SQUAD_BUILDER",
  PLAYED_GAME_LIVELIKE_MATCH_PREDICTOR = "PLAYED_GAME_LIVELIKE_MATCH_PREDICTOR",
  PLAYED_GAME_LIVELIKE_HISTORY_QUIZ = "PLAYED_GAME_LIVELIKE_HISTORY_QUIZ",
  PLAYED_GAME_FASTORY_PENALTY_SHOOTOUT = "PLAYED_GAME_FASTORY_PENALTY_SHOOTOUT",

  // Ticket Purchases
  PURCHASED_TICKETS_FANFESTFC_VS_RIVALS = "PURCHASED_TICKETS_FANFESTFC_VS_RIVALS",
  PURCHASED_TICKETS_HOME_TEAM_VS_FANFESTFC = "PURCHASED_TICKETS_HOME_TEAM_VS_FANFESTFC",
  PURCHASED_TICKETS_AWAY_TEAM_VS_FANFESTFC = "PURCHASED_TICKETS_AWAY_TEAM_VS_FANFESTFC",
}

// Map to channel action IDs
const engagementActionToChannelActionId: Record<EngagementAction, string> = {
  [EngagementAction.PURCHASED_ITEM]: "fanfestfc_PURCHASED_ITEM",
  [EngagementAction.PODCAST_COMPLETE]: "fanfestfc_PODCAST_COMPLETE",
  [EngagementAction.BROWSED_TICKETS]: "fanfestfc_BROWSED_TICKETS",
  [EngagementAction.LOCATION_CHECK_IN]: "fanfestfc_LOCATION_CHECK_IN",
  [EngagementAction.NEWSLETTER_SIGNUP]: "fanfestfc_NEWSLETTER_SIGNUP",
  [EngagementAction.VIDEO_WATCH]: "fanfestfc_VIDEO_WATCH",
  [EngagementAction.ARTICLE_READING]: "fanfestfc_ARTICLE_READING",
  [EngagementAction.PLAYED_GAME]: "fanfestfc_PLAYED_GAME",
  [EngagementAction.SHARE_VIDEO_ON_X]: "fanfestfc_SHARE_VIDEO_ON_X",
  [EngagementAction.SHARE_ARTICLE_ON_FACEBOOK]:
    "fanfestfc_SHARE_ARTICLE_ON_FACEBOOK",
  [EngagementAction.FOLLOW_US_ON_INSTAGRAM]: "fanfestfc_FOLLOW_US_ON_INSTAGRAM",
  [EngagementAction.CLAIM_MC_DONALD_S_REWARD]:
    "fanfestfc_CLAIM_MC_DONALD_S_REWARD",
  [EngagementAction.PLAYED_GAME_FASTORY_PENALTY_SHOOTOUT]:
    "fanfestfc_PLAYED_GAME_FASTORY_PENALTY_SHOOTOUT",
  [EngagementAction.PURCHASED_TICKETS_FANFESTFC_VS_RIVALS]:
    "fanfestfc_PURCHASED_TICKETS_FANFESTFC_VS_RIVALS",
  // ... map all actions
};

// Register with SDK
FanFestSDK.registerActionMapping(engagementActionToChannelActionId);

Section-Based Tracking

Video Section with Progress Tracking

vue
<template>
  <!-- Video section with render tracking -->
  <div
    :data-fanfest-track="EngagementAction.BROWSED_VIDEOS"
    :data-fanfest-description="EngagementAction.BROWSED_VIDEOS"
    data-fanfest-experience-id="BROWSED_VIDEOS"
    data-fanfest-on="render"
  >
    <!-- Video player with progress tracking -->
    <video
      ref="mainVideoPlayer"
      @timeupdate="trackVideoProgress"
      class="w-full h-full"
      controls
    >
      <source :src="activeVideo.landscapeUrl" type="video/mp4" />
    </video>
  </div>
</template>

<script setup>
const videoTracking = reactive({
  video25: false,
  video50: false,
  video100: false,
});

const trackVideoProgress = () => {
  const video = mainVideoPlayer.value;
  if (!video) return;

  const currentTime = video.currentTime;
  const duration = video.duration;
  const progress = Math.round((currentTime / duration) * 100);

  // Trigger rewards at milestones
  if (progress >= 25 && !videoTracking.video25) {
    triggerVideoMilestone("Video 25% Watched");
    videoTracking.video25 = true;
  } else if (progress >= 50 && !videoTracking.video50) {
    triggerVideoMilestone("Video 50% Watched");
    videoTracking.video50 = true;
  } else if (progress >= 90 && !videoTracking.video100) {
    triggerVideoMilestone("Video Completed");
    videoTracking.video100 = true;
  }
};

const triggerVideoMilestone = async (milestone) => {
  // Track video progress milestone
  await FanFestSDK.trackEvent({
    action: EngagementAction.VIDEO_PROGRESS,
    description: milestone,
    externalObjectId: activeVideo.value.id,
    metadata: {
      milestone: milestone,
      videoId: activeVideo.value.id,
      progress: Math.round((video.currentTime / video.duration) * 100),
    },
  });
};
</script>

News Section with Reading Tracking

vue
<template>
  <!-- News section with render tracking -->
  <div
    class="bg-gray-100"
    :data-fanfest-track="EngagementAction.BROWSED_NEWS"
    :data-fanfest-description="EngagementAction.BROWSED_NEWS"
    data-fanfest-experience-id="BROWSED_NEWS"
    data-fanfest-on="render"
  >
    <!-- Article content with scroll tracking -->
    <div ref="articleContent" class="p-6">
      <h1>{{ mainArticle.title }}</h1>
      <div v-html="mainArticle.content"></div>
    </div>

    <!-- Social sharing buttons -->
    <button
      v-for="value in socialShare"
      :key="value.name"
      :data-fanfest-track="value.dataFanfestTrack"
      data-fanfest-on="click"
      :data-fanfest-description="`Shared on ${value.name}`"
      :data-fanfest-experience-id="`SHARE_ARTICLE_ON_${value.name.toUpperCase()}`"
    >
      <Icon :name="value.icon" />
    </button>
  </div>
</template>

<script setup>
const socialShare = [
  {
    name: "Facebook",
    icon: "mdi:facebook",
    dataFanfestTrack: "SHARE_ARTICLE_ON_FACEBOOK",
  },
  {
    name: "X",
    icon: "mdi:twitter",
    dataFanfestTrack: "SHARE_ARTICLE_ON_X",
  },
  {
    name: "Instagram",
    icon: "mdi:instagram",
    dataFanfestTrack: "SHARE_ARTICLE_ON_INSTAGRAM",
  },
];

// Track reading progress
const trackReadingProgress = () => {
  const articleElement = articleContent.value;
  if (!articleElement) return;

  const scrollTop = window.scrollY;
  const elementTop = articleElement.offsetTop;
  const elementHeight = articleElement.offsetHeight;
  const windowHeight = window.innerHeight;

  const scrollProgress = Math.min(
    (scrollTop - elementTop + windowHeight) / elementHeight,
    1,
  );

  if (scrollProgress >= 0.8 && !readingComplete.value) {
    // Track article reading completion
    FanFestSDK.trackEvent({
      action: EngagementAction.ARTICLE_READING,
      description: "Article Reading Complete",
      externalObjectId: mainArticle.value.id,
      metadata: {
        articleId: mainArticle.value.id,
        readingTime: mainArticle.value.readingTime,
        progress: Math.round(scrollProgress * 100),
      },
    });
    readingComplete.value = true;
  }
};
</script>

Experience Check-ins

vue
<template>
  <!-- Experience check-in with QR code support -->
  <div
    v-for="experience in experiences"
    :key="experience.id"
    @click="handleClick(experience)"
  >
    <h3>{{ experience.title }}</h3>
    <button>{{ experience.buttonLabel }}</button>
  </div>

  <!-- QR Code Modal -->
  <div v-if="showModal && modalData" class="modal">
    <img :src="modalData.qrCode" alt="QR Code" />
    <button @click="closeModal">Close</button>
  </div>
</template>

<script setup>
const experiences = [
  {
    id: "stadium-tour",
    title: "Stadium Tour",
    buttonLabel: "Check In",
    qrCode: "/img/experiences/qr-code-stadium-tour.png",
  },
  {
    id: "store",
    title: "Official FanFest FC Store",
    buttonLabel: "Check In",
    qrCode: "/img/experiences/qr-code-official-fanfest-store.png",
  },
  {
    id: "pub-watch-party",
    title: "FanFest FC London Supporter Club Watch Party",
    buttonLabel: "Check In",
    qrCode: "/img/experiences/qr-code-london-supporter-club.png",
  },
];

function handleClick(experience) {
  if (experience.qrCode) {
    // Show QR code modal
    modalData.value = experience;
    showModal.value = true;
  } else {
    // Track experience check-in
    FanFestSDK.trackEvent({
      action: EngagementAction.EXPERIENCE_CHECK_IN,
      description: experience.title,
      externalExperienceId: experience.id,
      externalObjectId: experience.id,
      metadata: {
        experienceType: experience.header,
        hasQRCode: !!experience.qrCode,
      },
    });
  }
}
</script>

Authentication Integration

OIDC Authentication Setup

typescript
// Nuxt.js configuration
export default defineNuxtConfig({
  runtimeConfig: {
    auth: {
      oidc: {
        session: {
          name: "fanfestfc-oidc",
          password: process.env.NUXT_OIDC_AUTH_SESSION_SECRET,
          maxAge: 60 * 60 * 24 * 30, // 30 days
          secure: false,
          sameSite: "lax",
        },
      },
      auth0: {
        clientId: process.env.NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_ID,
        clientSecret: process.env.NUXT_OIDC_PROVIDERS_AUTH0_CLIENT_SECRET,
        baseUrl: process.env.NUXT_OIDC_PROVIDERS_AUTH0_BASE_URL,
      },
    },
    public: {
      apiUrl: process.env.API_URL,
      apiKey: process.env.API_KEY,
      appOrigin: process.env.APP_ORIGIN ?? "https://fanfest.vip",
    },
  },
});

Authentication State Management

typescript
// Auth store with SDK integration
export const useAuthStore = defineStore("auth", () => {
  const {
    loggedIn: isLoggedIn,
    loading: isLoading,
    user,
    login,
    logout,
  } = useOidcAuth();

  const isFanFestSDKLoaded = ref(false);

  // Watch for authentication changes
  watch(
    () => [isLoggedIn.value, isFanFestSDKLoaded.value],
    ([_, sdkLoaded], __, onCleanup) => {
      if (typeof window === "undefined") return;
      if (!sdkLoaded || !window.FanFestSDK) return;
      if (isLoggedIn.value === null) return;

      const timeout = setTimeout(() => {
        if (isLoggedIn.value) {
          window.FanFestSDK.login();
        } else {
          window.FanFestSDK.logout();
        }
      }, 100);

      onCleanup(() => {
        clearTimeout(timeout);
      });
    },
  );

  return {
    isLoggedIn,
    isFanFestSDKLoaded,
    user,
    login,
    logout,
  };
});

Best Practices

1. Comprehensive Action Mapping

  • Define all engagement actions in a centralized enum
  • Map to channel action IDs for consistent tracking
  • Use descriptive names that clearly indicate the action

2. Section-Based Tracking

  • Track section views with render events
  • Use stable section IDs for consistent tracking
  • Include relevant metadata for analytics

3. Progress Tracking

  • Track milestones for long-form content
  • Use percentage-based triggers for consistent rewards
  • Prevent duplicate tracking with state management

4. Authentication Integration

  • Watch authentication state changes
  • Sync with SDK login/logout methods
  • Handle anonymous users gracefully

5. Error Handling

  • Graceful degradation when SDK fails to load
  • Retry logic for failed tracking calls
  • User feedback for important actions

Next Steps

Released under the MIT License.