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.