Skip to content

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:

html
<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:

javascript
async function handleCTAClick() {
  await FanFestSDK.trackEvent({
    action: "cta_click",
    description: "Hero CTA",
    externalObjectId: "hero-cta-1",
  });
  navigateToSignup();
}

Product Purchase ​

Declarative:

html
<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:

jsx
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) ​

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 ​

jsx
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.

html
<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 ​

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:

jsx
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 ​

jsx
"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:

jsx
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:

html
<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:

html
<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:

html
<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:

html
<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:

html
<!-- 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:

html
<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:

javascript
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:

jsx
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:

html
<!-- 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:

jsx
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:

typescript
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:

vue
<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:

vue
<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:

typescript
// 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 ​

  1. Define actions in a centralized enum — keeps naming consistent across your codebase
  2. Register all mappings at init — use the bulk record format for registerActionMapping()
  3. Track section views with render events for impression analytics
  4. Track milestones for long-form content (video progress, article scroll depth)
  5. Prevent duplicate tracking with state management or the once modifier
  6. Sync authentication state — call login() and logout() so events are attributed to users
  7. Handle errors gracefully — never let tracking failures block user flows
  8. Use stable object IDs — prefer article-champions-league over button-3
  9. Avoid PII in metadata — do not include email addresses, phone numbers, or similar data
  10. Keep metadata lightweight — include only what is needed for analytics

Next Steps ​

Released under the MIT License.