Edgio

Edge Experiments with Optimizely

This edge function contains Node.js dependencies that require deployment via CDN-as-Code and cannot be deployed through the Edgio Console.
Optimizely is a popular experimentation platform that allows you to run A/B tests, multivariate tests, and personalization campaigns on your website. By integrating Optimizely with your Edgio application, you can leverage the power of experimentation to optimize your user experience and drive better business outcomes.
If you prefer a simpler workflow that does not require the use of Edge Functions or Optimizely, use Experimentation to serve different experiences to your clients.
In this guide, we’ll show you how to use Edgio Edge Functions to integrate Optimizely experiments into your application. We’ll create an edge function that intercepts incoming requests, checks for an Optimizely experiment cookie, and modifies the request based on the experiment configuration.
The following example demonstrates using an Optimizely experiment to determine the text direction of a webpage and modifies the HTML content accordingly before responding to the client.

Prerequisites

Setup requires:

Install the Edgio CLI

If you have not already done so, install the Edgio CLI.
Bash
1npm i -g @edgio/cli@latest

Getting Started

To get started, you’ll need an Optimizely account and an existing experiment that you want to integrate with your Edgio application. If you don’t have an Optimizely account, you can sign up for a free trial.
If you don’t already have an existing Edgio application, you can create one using the Edgio CLI:
Bash
1edgio init --edgioVersion latest
This will create a new Edgio application with the necessary files and configurations to get started.
Next, create the following directories which will be used to store the edge functions and other necessary files:
Bash
1. (project root)
2├── edge-functions
3├── lib
4│ ├── optimizely
5│ └── polyfills
6
7# Create the directories
8mkdir -p edge-functions lib/optimizely lib/polyfills

Install the Optimizely SDK

To integrate Optimizely with your Edgio application, you’ll need to install the Optimizely SDK and some additional polyfills Optimizely depends on. You can do this by adding the following dependencies to your project:
Bash
1npm install @optimizely/optimizely-sdk crypto-js polyfill-crypto.getrandomvalues uuid

Define Required Polyfills

The Optimizely SDK relies on the uuid (has a dependency on crypto) and other timing functions not available in the Edge Functions runtime. To ensure the SDK works correctly, you’ll need to create the following polyfills. These will be used later in the edge function to ensure the SDK functions correctly.

crypto Polyfill

Optimizely requires uuid which has a dependency on crypto. The following polyfill provides the necessary functions for the SDK to work correctly.
JavaScript./lib/polyfills/crypto.js
1import CryptoJS from 'crypto-js';
2import getRandomValues from 'polyfill-crypto.getrandomvalues';
3
4global.crypto = {
5 ...CryptoJS,
6 getRandomValues,
7};

Timer Polyfill

Various dependencies reference standard JavaScript timing functions that are not available in the Edge Function runtime. The following polyfill provides the necessary functions for the SDK to work correctly.
JavaScript./lib/polyfills/timer.js
1let timers = new Map();
2let nextTimerId = 1;
3
4(function (global) {
5 var timerQueue = [];
6 var nextTimerId = 0;
7
8 function runTimers() {
9 var now = Date.now();
10 var nextCheck = null;
11
12 // Run due timers
13 for (var i = 0; i < timerQueue.length; i++) {
14 var timer = timerQueue[i];
15 if (timer.time <= now) {
16 timer.callback.apply(null, timer.args);
17 if (timer.repeating) {
18 timer.time = now + timer.delay; // schedule next run
19 nextCheck =
20 nextCheck !== null ? Math.min(nextCheck, timer.time) : timer.time;
21 } else {
22 timerQueue.splice(i--, 1); // remove non-repeating timer
23 }
24 } else {
25 nextCheck =
26 nextCheck !== null ? Math.min(nextCheck, timer.time) : timer.time;
27 }
28 }
29
30 // Schedule next check
31 if (nextCheck !== null) {
32 var delay = Math.max(nextCheck - Date.now(), 0);
33 setTimeout(runTimers, delay);
34 }
35 }
36
37 global.setTimeout = function (callback, delay, ...args) {
38 var timerId = ++nextTimerId;
39 var timer = {
40 id: timerId,
41 callback: callback,
42 time: Date.now() + delay,
43 args: args,
44 repeating: false,
45 delay: delay,
46 };
47 timerQueue.push(timer);
48 return timerId;
49 };
50
51 global.clearTimeout = function (timerId) {
52 for (var i = 0; i < timerQueue.length; i++) {
53 if (timerQueue[i].id === timerId) {
54 timerQueue.splice(i, 1);
55 break;
56 }
57 }
58 };
59
60 global.queueMicrotask = function (callback) {
61 Promise.resolve()
62 .then(callback)
63 .catch((err) =>
64 setTimeout(() => {
65 throw err;
66 })
67 );
68 };
69
70 setTimeout(runTimers, 0);
71})(global);

Obtain the Optimizely Datafile

The Optimizely SDK requires a datafile that contains the configuration for your experiments. We recommend that you export the datafile from the Optimizely dashboard and save it as a JSON file in your project’s lib/optimizely directory.
Export Datafile

Create the Edge Function

Next, you’ll need to create an edge function that intercepts incoming requests and modifies the response based on the Optimizely experiment configuration. The edge function will check for an Optimizely experiment cookie in the request and use the Optimizely SDK to determine the appropriate variation to serve.
Optimizely’s SDK Lite is used in this example to reduce the size of the code bundle. The SDK Lite requires a preloaded datafile, which is referenced in the guide. You may choose to fetch the datafile dynamically if your experiment configuration changes frequently.
Create a new file named optimizely-experiment.js in your project’s edge-functions directory and add the following code:
JavaScript./edge-functions/optimizely-experiment.js
1// Necessary polyfills for the edge function runtime
2import '../lib/polyfills/crypto.js';
3import '../lib/polyfills/timer.js';
4
5import {
6 createInstance,
7 eventDispatcher,
8} from '@optimizely/optimizely-sdk/dist/optimizely.lite.min.js';
9import optimizelyDatafile from '../lib/optimizely/datafile.json';
10
11import {v4 as uuidv4} from 'uuid';
12
13// Constants for Optimizely client configuration
14const CLIENT_ENGINE = 'node-sdk';
15const COOKIE_NAME = 'experiment-cookie-name';
16
17/**
18 * Handles incoming HTTP requests and applies A/B testing using Optimizely.
19 *
20 * @param {Request} request - The incoming HTTP request.
21 * @param {Object} context - The context for this handler
22 * @returns {Response} The HTTP response after applying A/B testing logic.
23 */
24export async function handleHttpRequest(request, context) {
25 // Retrieve or generate a unique user ID from cookies
26 const userId =
27 request.headers
28 .get('Cookie')
29 ?.split(';')
30 .find((cookie) => cookie.trim().startsWith(`${COOKIE_NAME}=`))
31 ?.split('=')[1] || uuidv4();
32
33 // Create an Optimizely instance with the preloaded datafile and configuration.
34 // This edge function uses the Optimizely SDK Lite which requires a preloaded datafile.
35 const instance = createInstance({
36 datafile: optimizelyDatafile,
37 clientEngine: CLIENT_ENGINE,
38 eventDispatcher,
39 });
40
41 // Early exit if the Optimizely instance isn't properly created
42 if (!instance) {
43 return new Response('Optimizely instance unavailable.', {status: 500});
44 }
45
46 // Ensures the Optimizely instance is ready before proceeding
47 await instance.onReady();
48
49 // Create a user context for the retrieved or generated user ID
50 const userContext = instance.createUserContext(userId.toString());
51
52 // Your logic based on the Optimizely experiment variation
53 const decision = userContext.decide('your_experiment_flag');
54 // ...
55
56 // Fetch the original response from the origin
57 const response = await fetch(request.url, {
58 edgio: {origin: 'your-origin'},
59 });
60
61 // Modify the response based on the Optimizely experiment variation
62 const updatedResponse = new Response(response.body, response);
63 // ...
64
65 // Add the user ID to the response headers as a cookie to ensure the user experience consistency
66 const cookie = `${COOKIE_NAME}=${userId}; Path=/; Max-Age=31536000; SameSite=Lax`;
67 updatedResponse.headers.append('Set-Cookie', cookie);
68
69 // Return the modified response to the client
70 return updatedResponse;
71}
See our Optimizely example repo for a complete implementation of the edge function.

Routing

With the edge function created, you’ll need to define a route that maps incoming requests to the edge function. You can do this by updating the routes.[js|ts] file in your project’s root directory:
JavaScriptroutes.js
1// This file was automatically added by edg init.
2// You should commit this file to source control.
3
4const {Router} = require('@edgio/core/router');
5
6export default new Router().get('/optimizely-experiment', {
7 edge_function: './edge-functions/optimizely-experiment.js',
8});
9// Additional routes...
With this configuration, any incoming requests to /optimizely-experiment will be processed by the optimizely-experiment.js edge function. Here you may add other features such as cache rules.

Running Locally

Test on your local machine by running the following command in your project’s root directory:
Bash
1edgio dev
Once the development server is running, you can access your app at http://localhost:3000. Test the Optimizely experiment by navigating to the /optimizely-experiment route.

Deploying

Deploy your app to Edgio by running the following command in your project’s root directory:
Bash
1edgio deploy
Your initial CDN-as-code deployment will generate system-defined origin configurations along with those defined within your edgio.config.js. Learn more about system-defined origins.
See Deployments for more information.