Skip to content

Commit

Permalink
Merge pull request #138 from edgio-docs/COMSUP-338_v7_optimizely
Browse files Browse the repository at this point in the history
Optimizely SDK in EF + Next
  • Loading branch information
Tristan Lee authored May 15, 2024
2 parents e9abcef + 13684b7 commit 158f2ae
Show file tree
Hide file tree
Showing 16 changed files with 12,612 additions and 0 deletions.
1 change: 1 addition & 0 deletions examples/v7-optimizely-edge/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPTIMIZELY_SDK_KEY=your_sdk_key
1 change: 1 addition & 0 deletions examples/v7-optimizely-edge/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
5 changes: 5 additions & 0 deletions examples/v7-optimizely-edge/.github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
**NOTICE TO CONTRIBUTORS**

This repository is not actively monitored and any pull requests made to this repository will be closed/ignored.

Please submit the pull request to [edgio-docs/edgio-examples](https://github.com/edgio-docs/edgio-examples) instead.
18 changes: 18 additions & 0 deletions examples/v7-optimizely-edge/.github/workflows/edgio.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Deploy to Edgio

on:
workflow_dispatch:
push:

jobs:
deploy-to-edgio:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: if [ -f yarn.lock ]; then yarn install; else npm ci; fi
- run: if [ -f yarn.lock ]; then yarn edgio:deploy -- --token=$EDGIO_DEPLOY_TOKEN; else npm run edgio:deploy -- --token=$EDGIO_DEPLOY_TOKEN; fi
env:
EDGIO_DEPLOY_TOKEN: ${{secrets.EDGIO_DEPLOY_TOKEN}}
23 changes: 23 additions & 0 deletions examples/v7-optimizely-edge/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# Edgio generated build directory
/.edgio
/node_modules
42 changes: 42 additions & 0 deletions examples/v7-optimizely-edge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Edge-side Experiments with Optimizely

This example demonstrates how you may use edge functions to run Optimizely experiments.

**Preview**: [https://edgio-community-examples-v7-optimizely-edge-live.glb.edgio.link/](https://edgio-community-examples-v7-optimizely-edge-live.glb.edgio.link/)

The request workflow is as follows:

- The client makes a request to `/` which is handled by an edge function.
- The edge function fetches the Optimizely experiment variant and decides which text direction to use based on the variant.
- The edge function makes another fetch request to the Wikipedia homepage and gets the HTML content.
- The edge function modifies the HTML content to include the experiment variant.
- The edge function sets the experiment variant cookie and returns the page to the client.
- Depending on the experiment variant, the page may render normal or mirrored based on the applied transformation.

## Getting Started

1. Clone the repository:

```bash
git clone ...
```

2. Install the dependencies:

```bash
npm install
```

3. Create a `.env` file in the root of the project and add the following environment variables:

```bash
OPTIMIZELY_SDK_KEY=...
```

4. Start the development server:

```bash
npm run edgio:dev
```

5. Open [http://localhost:3000/](http://localhost:3000/) with your browser to see the result. Note: The page may render normally or mirrored based on the experiment variant.
73 changes: 73 additions & 0 deletions examples/v7-optimizely-edge/edge-functions/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import '../lib/polyfills'; // Necessary polyfills for the edge function scope
import {
createInstance,
eventDispatcher,
} from '@optimizely/optimizely-sdk/dist/optimizely.lite.min.js';
import optimizelyDatafile from '../lib/optimizely/datafile.json';
import { v4 as uuidv4 } from 'uuid';

// Constants for Optimizely client configuration
const CLIENT_ENGINE = 'EDGIO_EF';
const COOKIE_NAME = 'optimizely_visitor_id';

/**
* Handles incoming HTTP requests and applies A/B testing using Optimizely.
*
* @param {Request} request - The incoming HTTP request.
* @param {Object} context - The context for this handler
* @returns {Response} The HTTP response after applying A/B testing logic.
*/
export async function handleHttpRequest(request, context) {
// Retrieve or generate a unique user ID from cookies
const userId =
request.headers
.get('Cookie')
?.split(';')
.find((cookie) => cookie.trim().startsWith(`${COOKIE_NAME}=`))
?.split('=')[1] || uuidv4();

// Create an Optimizely instance with the preloaded datafile and configuration.
// This edge function uses the Optimizely SDK Lite which requires a preloaded datafile.
const instance = createInstance({
datafile: optimizelyDatafile,
clientEngine: CLIENT_ENGINE,
eventDispatcher,
});

// Early exit if the Optimizely instance isn't properly created
if (!instance) {
return new Response('Optimizely instance unavailable.', { status: 500 });
}

await instance.onReady(); // Ensures the Optimizely instance is ready before proceeding

// Create a user context for the retrieved or generated user ID
const userContext = instance.createUserContext(userId.toString());

// Make a decision using Optimizely for the 'text_direction' feature
const decision = userContext.decide('text_direction');
const textDir = decision.enabled ? 1 : -1; // Determine text direction based on decision

console.log(`[OPTIMIZELY] User ID: ${userId}, Text Direction: ${textDir}`);

// Fetch the homepage of Wikipedia
const url = new URL('https://en.wikipedia.org');
const response = await fetch(url.toString(), {
edgio: { origin: 'wikipedia' },
});

// Update the `<body>` tag with the text direction based on the Optimizely decision
const bodyTagRegex = /<body([^>]*)>/i;
const bodyTagReplacement = `<body style="transform: scaleX(${textDir});"$1>`;
const body = await response.text();
const updatedBody = body.replace(bodyTagRegex, bodyTagReplacement);

// Create a new response with the updated body content
const updatedResponse = new Response(updatedBody, response);

// Add the user ID to the response headers as a cookie to ensure the user experience consistency
// const cookie = `${COOKIE_NAME}=${userId}; Path=/; Max-Age=31536000; SameSite=Lax`;
// updatedResponse.headers.append('Set-Cookie', cookie);

return updatedResponse;
}
59 changes: 59 additions & 0 deletions examples/v7-optimizely-edge/edgio.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// This file was automatically added by edgio init.
// You should commit this file to source control.
// Learn more about this file at https://docs.edg.io/guides/edgio_config
module.exports = {
// The name of the site in Edgio to which this app should be deployed.
// name: 'my-site-name',

// The name of the organization in Edgio to which this app should be deployed.
// organization: 'my-organization-name',

// Overrides the default path to the routes file. The path should be relative to the root of your app.
// routes: 'routes.js',

// When set to true or omitted entirely, Edgio includes the deployment number in the cache key,
// effectively purging the cache each time you deploy.
// purgeCacheOnDeploy: false,

// If you need to proxy some URLs to an origin instead of your Next.js app, you can configure the origins here:
origins: [
{
name: 'wikipedia',
override_host_header: 'en.wikipedia.org',
hosts: [
{
location: 'en.wikipedia.org',
},
],
tls_verify: {
use_sni: true,
allow_self_signed_certs: true,
sni_hint_and_strict_san_check: 'en.wikipedia.org',
},
},
],

// Options for hosting serverless functions on Edgio
// serverless: {
// // Set to true to include all packages listed in the dependencies property of package.json when deploying to Edgio.
// // This option generally isn't needed as Edgio automatically includes all modules imported by your code in the bundle that
// // is uploaded during deployment
// includeNodeModules: true,
//
// // Include additional paths that are dynamically loaded by your app at runtime here when building the serverless bundle.
// include: ['views/**/*'],
// },

// The maximum number of URLs that will be concurrently prerendered during deployment when static prerendering is enabled.
// Defaults to 200, which is the maximum allowed value.
// prerenderConcurrency: 200,

// A list of glob patterns identifying which prerenderConcurrency source files should be uploaded when running edgio deploy --includeSources.
// This option is primarily used to share source code with Edgio support personnel for the purpose of debugging. If omitted,
// edgio deploy --includeSources will result in all files which are not gitignored being uploaded to Edgio.
//
// sources : [
// '**/*', // include all files
// '!(**/secrets/**/*)', // except everything in the secrets directory
// ],
};
1 change: 1 addition & 0 deletions examples/v7-optimizely-edge/lib/optimizely/datafile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"accountId":"29260950578","projectId":"29260950578","revision":"7","attributes":[],"audiences":[{"id":"$opt_dummy_audience","name":"Optimizely-Generated Audience for Backwards Compatibility","conditions":"[\"or\", {\"match\": \"exact\", \"name\": \"$opt_dummy_attribute\", \"type\": \"custom_attribute\", \"value\": \"$opt_dummy_value\"}]"}],"version":"4","events":[],"integrations":[],"anonymizeIP":true,"botFiltering":false,"typedAudiences":[],"variables":[],"environmentKey":"development","sdkKey":"2imW2dz6yoT3fzLKWFDmz","featureFlags":[{"id":"179326","key":"text_direction","rolloutId":"rollout-179326-29212200416","experimentIds":["9300000941392"],"variables":[]}],"rollouts":[{"id":"rollout-179326-29212200416","experiments":[{"id":"default-rollout-179326-29212200416","key":"default-rollout-179326-29212200416","status":"Running","layerId":"rollout-179326-29212200416","variations":[{"id":"587821","key":"off","featureEnabled":false,"variables":[]}],"trafficAllocation":[{"entityId":"587821","endOfRange":10000}],"forcedVariations":{},"audienceIds":[],"audienceConditions":[]}]}],"experiments":[{"id":"9300000941392","key":"text_direction","status":"Running","layerId":"9300000714177","variations":[{"id":"587821","key":"off","featureEnabled":false,"variables":[]},{"id":"587822","key":"on","featureEnabled":true,"variables":[]}],"trafficAllocation":[{"entityId":"587821","endOfRange":5000},{"entityId":"587822","endOfRange":10000}],"forcedVariations":{},"audienceIds":[],"audienceConditions":[]}],"groups":[]}
7 changes: 7 additions & 0 deletions examples/v7-optimizely-edge/lib/polyfills/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import CryptoJS from 'crypto-js';
import getRandomValues from 'polyfill-crypto.getrandomvalues';

global.crypto = {
...CryptoJS,
getRandomValues,
};
2 changes: 2 additions & 0 deletions examples/v7-optimizely-edge/lib/polyfills/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import './timer';
import './crypto';
71 changes: 71 additions & 0 deletions examples/v7-optimizely-edge/lib/polyfills/timer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
let timers = new Map();
let nextTimerId = 1;

(function (global) {
var timerQueue = [];
var nextTimerId = 0;

function runTimers() {
var now = Date.now();
var nextCheck = null;

// Run due timers
for (var i = 0; i < timerQueue.length; i++) {
var timer = timerQueue[i];
if (timer.time <= now) {
timer.callback.apply(null, timer.args);
if (timer.repeating) {
timer.time = now + timer.delay; // schedule next run
nextCheck =
nextCheck !== null ? Math.min(nextCheck, timer.time) : timer.time;
} else {
timerQueue.splice(i--, 1); // remove non-repeating timer
}
} else {
nextCheck =
nextCheck !== null ? Math.min(nextCheck, timer.time) : timer.time;
}
}

// Schedule next check
if (nextCheck !== null) {
var delay = Math.max(nextCheck - Date.now(), 0);
setTimeout(runTimers, delay);
}
}

global.setTimeout = function (callback, delay, ...args) {
var timerId = ++nextTimerId;
var timer = {
id: timerId,
callback: callback,
time: Date.now() + delay,
args: args,
repeating: false,
delay: delay,
};
timerQueue.push(timer);
return timerId;
};

global.clearTimeout = function (timerId) {
for (var i = 0; i < timerQueue.length; i++) {
if (timerQueue[i].id === timerId) {
timerQueue.splice(i, 1);
break;
}
}
};

global.queueMicrotask = function (callback) {
Promise.resolve()
.then(callback)
.catch((err) =>
setTimeout(() => {
throw err;
})
);
};

setTimeout(runTimers, 0);
})(global);
Loading

0 comments on commit 158f2ae

Please sign in to comment.