Step 27 of 36 (75% complete)

Architecture Overview

Szymon Uryga photo

Our implementation uses:

Setting Up the Foundation

1. Environment Variables

OPTIMIZELY_FEATURE_EXP_API_KEY=your_sdk_key_here
OPTIMIZELY_WEBHOOK_SECRET=your_webhook_secret_here

2. Core Optimizely Integration

The foundation of our implementation is the Optimizely instance creation and datafile management:

// lib/optimizely-feature-exp/index.ts
export async function fetchDatafileFromCDN() {
  const sdkKey = process.env.OPTIMIZELY_FEATURE_EXP_API_KEY;
  
  try {
    const response = await fetch(`https://cdn.optimizely.com/datafiles/${sdkKey}.json`, {
      next: { tags: [OPTIMIZELY_DATAFILE_TAG] }
    });
    return await response.json();
  } catch (error) {
    console.log(error);
  }
}

Key Benefits:

  • Caching: Uses Next.js cache tags for efficient datafile management
  • Error Handling: Graceful fallback when Optimizely is unavailable
  • Performance: CDN-based datafile delivery for fast loading

Experiment 1: Search Provider Selection

The Problem

You want to test whether Shopify's native search or Optimizely Graph provides better search results and user experience.

Implementation

export const shouldUseShopifySearchFlag = flag<boolean>({
  key: 'use_shopify_search',
  defaultValue: false,
  description: 'Flag for using Shopify search',
  options: [
    { value: false, label: 'Use Optimizely Graph for Search' },
    { value: true, label: 'Use Shopify for Search' }
  ],
  async decide({ cookies }) {
    const optimizely = await getOptimizelyInstance();
    let flag = false;

    try {
      if (!optimizely) {
        throw new Error("Failed to create client");
      }

      await optimizely.onReady({ timeout: 500 });
      const userId = getUserId(cookies);
      const context = optimizely.createUserContext(userId);
      
      const decision = context.decide("use_shopify_search");
      flag = decision.enabled;
    } catch (error) {
      console.error("Optimizely error:", error);
    } finally {
      reportValue('use_shopify_search', flag);
      return flag;
    }
  }
});

Usage in Components

export default async function SearchPage({ searchParams, params }) {
  const useShopifySearch = await shouldUseShopifySearchFlag(
    resolvedParams.code, 
    precomputeFlags
  );

  let products;
  if (useShopifySearch) {
    products = await getShopifySearchProducts(searchValue, locale, reverse, sortKey);
  } else {
    products = await getOptiSearchProducts(searchValue ?? '', locale);
  }

  return (
    <div>
      <p>Source: {useShopifySearch ? 'Shopify' : 'Optimizely Graph'}</p>
      {/* Rest of component */}
    </div>
  );
}

What to Measure

  • Search Result Relevance: Click-through rates on search results
  • Performance: Search response times
  • User Engagement: Time spent on search results pages
  • Conversion: Purchase rates from search traffic

Experiment 2: Promotional Banner Display

The Problem

You want to test whether showing a promotional banner increases conversions or creates banner blindness.

Implementation

export const showPromoBannerFlag = flag<boolean>({
  key: 'show_promo_banner',
  defaultValue: false,
  description: 'Flag for showing promo banner on search page',
  options: [
    { value: false, label: 'Hide' },
    { value: true, label: 'Show' }
  ],
  async decide({ cookies }) {
    const optimizely = await getOptimizelyInstance();
    let flag = false;

    try {
      if (!optimizely) {
        throw new Error("Failed to create client");
      }

      await optimizely.onReady({ timeout: 500 });
      const userId = getUserId(cookies);
      const context = optimizely.createUserContext(userId);
      
      const decision = context.decide("show_promo_banner");
      flag = decision.enabled;
    } catch (error) {
      console.error("Optimizely error:", error);
    } finally {
      reportValue('show_promo_banner', flag);
      return flag;
    }
  }
});

Usage in Components

export default async function SearchPage({ searchParams, params }) {
  const showPromoBanner = await showPromoBannerFlag(
    resolvedParams.code, 
    precomputeFlags
  );

  return (
    <div className="container min-h-screen-100">
      {showPromoBanner && (
        <section className="mx-4 flex w-full justify-center rounded-lg bg-amber-300 py-4 font-bold md:mx-6">
          Optimizely One Masterclass! Get 20% off all products.
        </section>
      )}
      {/* Rest of component */}
    </div>
  );
}

What to Measure

  • Click-Through Rate: Banner clicks vs impressions
  • Conversion Impact: Purchase rates with/without banner
  • User Experience: Bounce rates and session duration
  • Banner Fatigue: Performance over time

Advanced Features

1. Real-Time Datafile Updates

Implement webhook handling for instant experiment updates:

// app/api/revalidate/datafile/route.ts
export async function POST(req: NextRequest) {
  try {
    const text = await req.text();
    const isVerified = await verifyOptimizelyWebhook(req.headers, text);

    if (!isVerified) {
      return NextResponse.json(
        { success: false, message: "Invalid webhook request" },
        { status: 401 }
      );
    }

    revalidateTag(OPTIMIZELY_DATAFILE_TAG);
    console.log("Revalidating Optimizely datafile tag");
    return NextResponse.json({ success: true }, { status: 200 });
  } catch (error) {
    console.error("Error processing webhook:", error);
    return NextResponse.json(
      { success: false, message: "Internal server error" },
      { status: 500 }
    );
  }
}

2. User Context Management

Consistent user identification across sessions:

const getUserId = (cookies: any) => {
  const existingUserId = cookies.get(COOKIE_NAME_USER_ID)?.value;
  return existingUserId || Math.floor(Math.random() * (10000 - 1000) + 1000).toString();
};

Best Practices

1. Error Handling

  • Always provide fallback values
  • Log errors for debugging
  • Don't let experiments break core functionality

2. Performance

  • Use appropriate timeouts
  • Cache datafiles efficiently

3. User Experience

  • Ensure consistent experiences within sessions
  • Avoid flickering between variations
  • Test with real user scenarios

Conclusion

Optimizely Feature Experimentation with Next.js provides a powerful platform for data-driven development. By implementing proper error handling, performance optimization, and measurement strategies, you can:

  • Reduce deployment risks
  • Improve user experiences
  • Make informed product decisions
  • Optimize business metrics

The combination of server-side rendering, real-time updates, and robust experimentation creates a foundation for continuous improvement and innovation.

Next Steps

  1. Set up your Optimizely project and experiments
  2. Implement the flag system in your Next.js application
  3. Configure webhooks for real-time updates
  4. Define success metrics and start measuring
  5. Iterate based on experiment results

Remember: The goal isn't just to run experiments, but to build a culture of data-driven decision making that continuously improves your product and user experience.

Vercel Template with Feature Experimentation

Have questions? I'm here to help!

Contact Me