Step 9 of 11 (82% complete)

Tracking Events

Step Code

The code for this specific step can be found on the following branch:

Click on a link to view the code for this step on GitHub.

Tracking Events with Optimizely

Once an event has been defined in the Optimizely Feature Experimentation Admin Panel and added as a metric to an A/B test, the next step is to implement the logic for sending this event whenever a user interacts with the relevant UI element - in this case, clicking a CTA button

To achieve this, we need to:

  1. Retrieve the current user ID from cookies.
  2. Use the Optimizely SDK to create a user context.
  3. Call the trackEvent method to send the event with relevant metadata (event tags).
  4. Connect this tracking logic to a button component that triggers the server-side action.

Let’s break this down step by step.


1. Creating the Event Tracking Function

The first step is to build a utility function that encapsulates the logic of sending events to Optimizely.

// lib/optimizely-feature-exp/tracking.ts

import { cookies } from 'next/headers'
import { getOptimizelyInstance } from '.'
import { EventTags } from '@optimizely/optimizely-sdk'
import { COOKIE_NAME_USER_ID } from './cookies'

export async function trackButtonClicked({
  buttonText,
}: {
  buttonText?: string
}) {
  const optimizely = await getOptimizelyInstance()

  if (!optimizely) {
    throw new Error('Failed to create client')
  }

  await optimizely.onReady({ timeout: 500 })

  const cookieStore = await cookies()
  const userdIdValue = cookieStore.get(COOKIE_NAME_USER_ID)?.value
  const userId = userdIdValue
    ? userdIdValue
    : Math.random().toString(36).substring(2)
  const context = optimizely!.createUserContext(userId)

  if (!context) {
    throw new Error('Failed to create user context')
  }

  const properties = {
    Text: buttonText ?? '',
  }

  const tags = {
    $opt_event_properties: properties,
  }

  context.trackEvent('cta-clicked', tags as unknown as EventTags)
}

Explanation:

  • getOptimizelyInstance() → Initializes and retrieves the Optimizely SDK client.

  • cookies() → Reads the user ID from cookies. If no cookie exists, a fallback random ID is generated.

  • createUserContext(userId) → Creates a user context which allows tracking events tied to a specific user.

  • properties → Stores metadata about the event, in this case the button text.

  • trackEvent('cta-clicked', tags) → Sends the event named cta-clicked with the defined tags to Optimizely.

This function is reusable for any button or interaction where we want to send an event.


2. Wrapping the Logic in a Server Action

Because cookies are read server-side, the tracking must also happen on the server. For this, we use a Next.js Server Action.

// lib/actions/track-button-clicked.ts
'use server'

import { trackButtonClicked } from '../optimizely-feature-exp/tracking'

export async function trackButtonClickedAction({
  buttonText,
}: {
  buttonText?: string
}) {
  try {
    await trackButtonClicked({ buttonText })

    return { success: true }
  } catch (error) {
    console.error('Failed to track button click:', error)
    return {
      success: false,
      error: error instanceof Error ? error.message : 'Unknown error',
    }
  }
}

Explanation:

  • Declared with 'use server', making it a server-only function in Next.js.

  • Calls the trackButtonClicked function from step 1.

  • Returns a response object with success: true or success: false for better error handling.

  • This makes the function callable from the client but guarantees execution on the server.


3. Creating a Reusable Tracked Button Component

Now that the tracking logic exists, we need a UI component that integrates it seamlessly.

// components/ui/tracked-button.tsx
'use client'

import * as React from 'react'
import { Button, type buttonVariants } from './button'
import type { VariantProps } from 'class-variance-authority'
import { trackButtonClickedAction } from '@/lib/actions/track-button-clicked'

interface TrackedButtonProps
  extends React.ComponentProps<'button'>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
  trackingText?: string
  colorScheme?: "default" | "primary" | "secondary" 
}

const getVariantForColorScheme = (colorScheme?: string, currentVariant?: string) => {
  // If variant is already specified, use it
  if (currentVariant && currentVariant !== "default") {
    return currentVariant
  }

  // Map colorScheme to appropriate variant for visibility
  switch (colorScheme) {
    case "primary":
      return "secondary" // Use secondary variant on primary background for contrast
    case "secondary":
      return "default" // Use default variant on secondary background
    case "default":
    default:
      return "outline" // Use outline variant on default pink background for better visibility
  }
}

const TrackedButton = React.forwardRef<HTMLButtonElement, TrackedButtonProps>(
  ({ children, onClick, trackingText, colorScheme, variant, asChild = false, ...props }, ref) => {
    const finalVariant = getVariantForColorScheme(colorScheme, variant as string) as VariantProps<
      typeof buttonVariants
    >["variant"]
    
    const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
      // Extract text content for tracking
      const buttonText =
        trackingText ||
        (typeof children === 'string'
          ? children
          : event.currentTarget.textContent || 'Unknown Button')

      try {
        // Track the button click using server action
        const result = await trackButtonClickedAction({ buttonText })
        if (!result.success) {
          console.error('Tracking failed:', result.error)
        }
      } catch (error) {
        console.error('Failed to track button click:', error)
        // Continue with the original click handler even if tracking fails
      }

      // Call the original onClick handler if provided
      if (onClick) {
        onClick(event)   
      }
    }

    if (asChild) {
      return (
        <Button asChild ref={ref} onClick={handleClick} variant={finalVariant} {...props}>
          {children}
        </Button>
      )
    }

    return (
      <Button ref={ref} onClick={handleClick} variant={finalVariant} {...props}>
        {children}
      </Button>
    )
  }
)

TrackedButton.displayName = 'TrackedButton'

export { TrackedButton }

Explanation:

  • TrackedButton extends a standard Button component but automatically adds tracking.

  • handleClick does two things:

    1. Extracts the button label (trackingText or fallback to text content).

    2. Calls the server action trackButtonClickedAction.

  • If tracking fails, the click event still proceeds - ensuring no user experience degradation.

  • Supports both asChild rendering and multiple color schemes via getVariantForColorScheme.

This makes tracking transparent: any button wrapped with TrackedButton automatically reports clicks.


4. Using the Tracked Button in a Component

Finally, integrate the tracked button into a UI block such as a profile card:

{button_cta?.url?.default && (
  <div className="mt-8">
    <TrackedButton
      size="lg"
      className="w-full md:w-auto"
      asChild
      trackingText={button_cta.text ?? ''}
    >
      <Link href={button_cta?.url?.default}>
        {button_cta.text}
      </Link>
    </TrackedButton>
  </div>
)}

Explanation:

  • TrackedButton is used instead of a regular Button.

  • The trackingText ensures a clean label is sent to Optimizely.

  • The button still behaves like a normal link (asChild with <Link>), but tracking is automatically triggered on click.


Summary

With this setup, we have created a reusable tracked button component that:

  • Automatically sends click events to Optimizely.

  • Retrieves user IDs from cookies for consistent experiment attribution.

  • Uses server actions to handle secure, server-side event tracking.

  • Falls back gracefully in case of errors, without affecting user interaction.

This design ensures consistent and reliable tracking across the application, while keeping the codebase clean and easy to extend.

Have questions? I'm here to help!

Contact Me