Tracking Examples
This page consolidates real-world tracking patterns organized by use case. Each section shows both declarative (HTML attribute) and imperative (JavaScript API) approaches where applicable.
Click Tracking
Basic Button Click
Declarative:
<button
data-fanfest-track="cta_click"
data-fanfest-on="click"
data-fanfest-description="Hero CTA"
data-fanfest-object-id="hero-cta-1"
>
Get Started
</button>Imperative:
async function handleCTAClick() {
await FanFestSDK.trackEvent({
action: "cta_click",
description: "Hero CTA",
externalObjectId: "hero-cta-1",
});
navigateToSignup();
}Product Purchase
Declarative:
<button
data-fanfest-track="purchase"
data-fanfest-on="click"
data-fanfest-description="Product Purchase"
data-fanfest-object-id="product-123"
data-fanfest-meta-amount="99.99"
data-fanfest-meta-currency="USD"
>
Buy Now - $99.99
</button>Imperative:
function ProductCard({ product }) {
const handlePurchase = async () => {
await FanFestSDK.trackEvent({
action: "purchase",
description: "Product Purchase",
externalObjectId: product.id,
metadata: {
amount: product.price,
currency: "USD",
category: product.category,
},
});
navigateToCheckout(product.id);
};
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={handlePurchase}>Buy Now</button>
</div>
);
}Dynamic Social Sharing (Vue)
<template>
<button
v-for="item in socialShare"
:key="item.name"
:data-fanfest-track="item.action"
data-fanfest-on="click"
:data-fanfest-description="`Shared on ${item.name}`"
:data-fanfest-experience-id="`SHARE_ARTICLE_ON_${item.name.toUpperCase()}`"
>
<Icon :name="item.icon" />
</button>
</template>
<script setup>
const socialShare = [
{ name: "Facebook", icon: "mdi:facebook", action: "SHARE_ARTICLE_ON_FACEBOOK" },
{ name: "X", icon: "mdi:twitter", action: "SHARE_ARTICLE_ON_X" },
{ name: "Instagram", icon: "mdi:instagram", action: "SHARE_ARTICLE_ON_INSTAGRAM" },
];
</script>Conditional Tracking Based on Auth State
function ConditionalCTA({ user, product }) {
const handleClick = async () => {
const action = user.isLoggedIn
? "authenticated_cta_click"
: "anonymous_cta_click";
await FanFestSDK.trackEvent({
action,
externalObjectId: product.id,
metadata: { userType: user.isLoggedIn ? "authenticated" : "anonymous" },
});
handleCTAClick();
};
return (
<button onClick={handleClick}>
{user.isLoggedIn ? "Add to Cart" : "Sign Up to Purchase"}
</button>
);
}Page View Tracking
Section Visibility with render
Use the render event to track when a section becomes visible on the page. The SDK fires the action immediately when the element is discovered in the DOM.
<div
data-fanfest-track="BROWSED_TICKETS"
data-fanfest-on="render"
data-fanfest-description="Browsed ticket section"
data-fanfest-experience-id="BROWSED_TICKETS"
data-fanfest-object-id="tickets-hero"
>
<h2>Available Tickets</h2>
<!-- Ticket content -->
</div>Section Tracking with Vue
<template>
<div
:data-fanfest-track="EngagementAction.BROWSED_VIDEOS"
:data-fanfest-description="EngagementAction.BROWSED_VIDEOS"
data-fanfest-experience-id="BROWSED_VIDEOS"
data-fanfest-on="render"
>
<!-- Video content -->
</div>
</template>React Router Page Views
Track page views on route changes using the imperative API:
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
function App() {
const location = useLocation();
useEffect(() => {
FanFestSDK.trackEvent({
action: "page_view",
description: document.title,
externalObjectId: location.pathname,
metadata: {
route: location.pathname,
search: location.search,
},
});
}, [location]);
return <div className="app">{/* App content */}</div>;
}Next.js App Router
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
export default function RootLayout({ children }) {
const pathname = usePathname();
useEffect(() => {
FanFestSDK.trackEvent({
action: "page_view",
description: document.title,
externalObjectId: pathname,
});
}, [pathname]);
return (
<html>
<body>{children}</body>
</html>
);
}Preventing Double Fires (SSR)
When using server-side rendering, guard against tracking during the server render pass:
function PageViewTracker() {
const [hasTracked, setHasTracked] = useState(false);
useEffect(() => {
if (typeof window !== "undefined" && !hasTracked) {
FanFestSDK.trackEvent({
action: "page_view",
description: document.title,
externalObjectId: window.location.pathname,
});
setHasTracked(true);
}
}, []);
return null;
}Event Modifiers
Event modifiers control listener behavior for declarative tracking. Append them to the event name with colons. See the declarative tracking reference for the full modifier list.
Prevent Default Form Submission
Track a form submission while preventing the browser's default submit behavior:
<form
data-fanfest-track="newsletter_signup"
data-fanfest-on="submit:preventDefault"
data-fanfest-description="Newsletter Signup"
data-fanfest-object-id="newsletter-form"
>
<input type="email" placeholder="Enter your email" required />
<button type="submit">Subscribe</button>
</form>Fire Only Once
Track only the first click on an element:
<button
data-fanfest-track="first_click"
data-fanfest-on="click:once"
data-fanfest-description="First Interaction"
>
Welcome Offer
</button>Stop Event Propagation
Prevent the tracking event from bubbling to parent handlers:
<button
data-fanfest-track="isolated_click"
data-fanfest-on="click:stop"
data-fanfest-description="Isolated Click"
>
Isolated Button
</button>Capture Phase
Attach the listener in the capture phase so it fires before bubble-phase handlers:
<div
data-fanfest-track="early_capture"
data-fanfest-on="click:capture"
data-fanfest-description="Early Capture"
>
Captured Area
</div>Combined Modifiers
Chain multiple modifiers for complex behavior:
<!-- Prevent default and fire only once -->
<form
data-fanfest-track="signup"
data-fanfest-on="submit:preventDefault:once"
data-fanfest-description="One-time Newsletter Signup"
>
<input type="email" placeholder="Enter your email" required />
<button type="submit">Subscribe</button>
</form>
<!-- Stop propagation and prevent default -->
<button data-fanfest-on="click:stop:preventDefault">Isolated Submit</button>
<!-- All four modifiers -->
<button data-fanfest-on="click:preventDefault:once:stop:capture">
Full Control
</button>Metadata and Deduplication
Declarative Metadata
Add custom key-value pairs using data-fanfest-meta-* attributes:
<button
data-fanfest-track="purchase"
data-fanfest-on="click"
data-fanfest-object-id="product-123"
data-fanfest-meta-campaign="BlackFriday"
data-fanfest-meta-amount="99.99"
data-fanfest-meta-currency="USD"
>
Buy Now
</button>Imperative Metadata
The imperative API supports typed values (numbers, booleans) — not just strings:
await FanFestSDK.trackEvent({
action: "purchase",
externalObjectId: product.id,
metadata: {
campaign: campaign.name,
amount: product.price, // number
currency: "USD", // string
discount: campaign.discount, // number
isFirstPurchase: true, // boolean
},
});A/B Testing Metadata
Track experiment variants to measure conversion:
function ABTestButton({ variant, testId }) {
const handleClick = async () => {
await FanFestSDK.trackEvent({
action: "ab_test_click",
externalObjectId: `${testId}-${variant}`,
metadata: {
testId,
variant,
conversionGoal: "button_click",
},
});
};
return <button onClick={handleClick}>Test Button</button>;
}Stable Object IDs
Use meaningful, stable identifiers for consistent analytics:
<!-- Good: stable, meaningful identifier -->
<button data-fanfest-object-id="hero-cta-2024-blackfriday">Get Started</button>
<!-- Bad: DOM-dependent identifier -->
<button data-fanfest-object-id="button-1">Get Started</button>Client-Side Deduplication
Prevent rapid-fire or duplicate events in JavaScript:
function DeduplicatedButton() {
const [hasTracked, setHasTracked] = useState(false);
const [lastTracked, setLastTracked] = useState(null);
const handleClick = async () => {
const now = Date.now();
// Prevent rapid-fire clicks (1-second cooldown)
if (lastTracked && now - lastTracked < 1000) {
return;
}
if (!hasTracked) {
await FanFestSDK.trackEvent({
action: "first_click",
externalObjectId: "deduplicated-button",
});
setHasTracked(true);
}
setLastTracked(now);
};
return <button onClick={handleClick}>Click Me</button>;
}Declarative Alternative
For one-time tracking in HTML, use the once modifier instead of writing deduplication logic: data-fanfest-on="click:once"
Server-Side Deduplication
The FanFest backend also deduplicates events based on:
- Session ID
- Event name
- Object ID
- Timestamp window (configurable)
Client-side deduplication is still recommended to reduce unnecessary network requests.
Comprehensive Engagement Patterns
Centralized Action Registry
For large integrations with many tracked actions, define a centralized enum and register all mappings at init:
enum EngagementAction {
// Content
PURCHASED_ITEM = "PURCHASED_ITEM",
PODCAST_COMPLETE = "PODCAST_COMPLETE",
BROWSED_TICKETS = "BROWSED_TICKETS",
NEWSLETTER_SIGNUP = "NEWSLETTER_SIGNUP",
VIDEO_WATCH = "VIDEO_WATCH",
ARTICLE_READING = "ARTICLE_READING",
PLAYED_GAME = "PLAYED_GAME",
// Social
SHARE_VIDEO_ON_X = "SHARE_VIDEO_ON_X",
SHARE_ARTICLE_ON_FACEBOOK = "SHARE_ARTICLE_ON_FACEBOOK",
FOLLOW_US_ON_INSTAGRAM = "FOLLOW_US_ON_INSTAGRAM",
// Location
LOCATION_CHECK_IN = "LOCATION_CHECK_IN",
// Partner Rewards
CLAIM_MC_DONALD_S_REWARD = "CLAIM_MC_DONALD_S_REWARD",
CLAIM_NIKE_REWARD = "CLAIM_NIKE_REWARD",
}
// Map action names to FanFest Channel Action IDs (bulk record format)
const actionMapping: Record<string, string> = {
[EngagementAction.PURCHASED_ITEM]: "fanfestfc_PURCHASED_ITEM",
[EngagementAction.PODCAST_COMPLETE]: "fanfestfc_PODCAST_COMPLETE",
[EngagementAction.BROWSED_TICKETS]: "fanfestfc_BROWSED_TICKETS",
// ... map all actions to their channel action IDs
};
FanFestSDK.registerActionMapping(actionMapping);Video Progress Milestones
Track video watch milestones using the imperative API:
<script setup>
import { reactive, ref } from "vue";
const mainVideoPlayer = ref(null);
const activeVideo = ref({ id: "video-123" });
const videoTracking = reactive({
video25: false,
video50: false,
video100: false,
});
function trackVideoProgress() {
const video = mainVideoPlayer.value;
if (!video) return;
const progress = Math.round((video.currentTime / video.duration) * 100);
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;
}
}
async function triggerVideoMilestone(milestone) {
await FanFestSDK.trackEvent({
action: "VIDEO_PROGRESS",
description: milestone,
externalObjectId: activeVideo.value.id,
metadata: { milestone, videoId: activeVideo.value.id },
});
}
</script>
<template>
<video
ref="mainVideoPlayer"
@timeupdate="trackVideoProgress"
src="/videos/highlight.mp4"
/>
</template>INFO
Video progress tracking uses the imperative API because timeupdate is not one of the 3 supported declarative events. The timeupdate DOM event triggers the Vue handler, which then calls trackEvent().
Experience Check-ins with Location
Track location-based check-ins at physical experiences:
<script setup>
function handleCheckIn(experience) {
if (experience.qrCode) {
// Show QR modal for scanning
modalData.value = experience;
showModal.value = true;
} else {
FanFestSDK.trackEvent({
action: "EXPERIENCE_CHECK_IN",
description: experience.title,
externalExperienceId: experience.id,
externalObjectId: experience.id,
});
}
}
</script>Authentication State Sync
Sync your app's authentication state with the FanFest SDK so tracked events are attributed to identified users:
// Auth store with FanFest SDK integration (Vue/Pinia)
export const useAuthStore = defineStore("auth", () => {
const { loggedIn: isLoggedIn, login, logout } = useOidcAuth();
const isFanFestSDKLoaded = ref(false);
watch(
() => [isLoggedIn.value, isFanFestSDKLoaded.value],
([_, sdkLoaded]) => {
if (typeof window === "undefined") return;
if (!sdkLoaded || !window.FanFestSDK) return;
if (isLoggedIn.value) {
window.FanFestSDK.login();
} else {
window.FanFestSDK.logout();
}
},
);
return { isLoggedIn, isFanFestSDKLoaded, login, logout };
});Best Practices
- Define actions in a centralized enum — keeps naming consistent across your codebase
- Register all mappings at init — use the bulk record format for
registerActionMapping() - Track section views with
renderevents for impression analytics - Track milestones for long-form content (video progress, article scroll depth)
- Prevent duplicate tracking with state management or the
oncemodifier - Sync authentication state — call
login()andlogout()so events are attributed to users - Handle errors gracefully — never let tracking failures block user flows
- Use stable object IDs — prefer
article-champions-leagueoverbutton-3 - Avoid PII in metadata — do not include email addresses, phone numbers, or similar data
- Keep metadata lightweight — include only what is needed for analytics
Next Steps
- Declarative Tracking — Full HTML data attribute reference
- Imperative Tracking —
trackEvent()TypeScript interface - SDK Methods Reference — Complete method documentation
