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
- Click Tracking - Basic interaction tracking
- Page Views - Navigation tracking
- Event Modifiers - Advanced event handling
- Metadata & Deduping - Data management
