Step 9 of 11 (82% complete)
Tracking Events
Step Code
The code for this specific step can be found on the following branch:
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:
- Retrieve the current user ID from cookies.
- Use the Optimizely SDK to create a user context.
- Call the
trackEventmethod to send the event with relevant metadata (event tags). - 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: trueorsuccess: falsefor 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:
-
TrackedButtonextends a standardButtoncomponent but automatically adds tracking. -
handleClick does two things:
-
Extracts the button label (
trackingTextor fallback to text content). -
Calls the server action
trackButtonClickedAction.
-
-
If tracking fails, the click event still proceeds - ensuring no user experience degradation.
-
Supports both
asChildrendering 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:
-
TrackedButtonis used instead of a regularButton. -
The
trackingTextensures a clean label is sent to Optimizely. -
The button still behaves like a normal link (
asChildwith<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!