Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimizely SDK in EF + Next #138

Merged
merged 12 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading