Disclaimer: This website requires Please enable JavaScript in your browser settings for the best experience.

The availability of features may depend on your plan type. Contact your Customer Success Manager if you have any questions.

Dev GuideAPI Reference
Dev GuideAPI ReferenceUser GuideLegal TermsGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Snippet integrity check with Akamai

How to create a system that verifies the integrity of your Optimizely Web Experimentation snippet using Akamai EdgeWorkers

You can verify the integrity of your Optimizely Web Experimentation snippet using Akamai EdgeWorkers to ensure the snippet is running properly.

The snippet contains two parts.

  • Static code – The core Optimizely library code and functionality. It only changes when Optimizely releases updates to their SDK.
  • Dynamic code – Your specific experiment configurations, variations, custom code, and targeting rules. This changes frequently as you create, update, and conclude experiments.

The integrity check system verifies the static code. You cannot check the dynamic code effectively with a simple hash comparison because the code changes constantly. Instead, you must manage changes to the dynamic code through internal governance processes.

What this solution does

  • Downloads the snippet – Automatically fetches the latest version of your snippet from Optimizely's CDN.
  • Splits the snippet – Divides it into static (core functionality) and dynamic (experiment configuration) parts.
  • Calculates a hash – Creates a cryptographic "fingerprint" (a SHA-256 hash) of the static part.
  • Compares hashes – Checks if the current hash matches a previously stored hash.
  • Tracks revisions – Monitors Optimizely's revision numbers to detect changes.
  • Stores data – Uses Akamai EdgeKV (a key-value store) to save the trusted hash and revision number.
  • Reports results – Provides a clear success or failure response, indicating whether the static part of the snippet is valid.

Prerequisites

  • An Akamai account with EdgeWorkers and EdgeKV enabled.
  • The Akamai CLI installed and configured.
  • Your Optimizely Project ID (found in your Optimizely snippet's filename).
  • Familiarity with Akamai Property Manager.

Steps

Configure the project

  1. Create a directory for your EdgeWorker.

    Bash  
    mkdir optimizely-integrity-check  
    cd optimizely-integrity-check
    
  2. Initialize an EdgeWorkers bundle. Use the hello world template as a starting point.

    Bash  
    akamai edgeworkers --contract <contractId> --accountkey <accountSwitchKey> init
    
  3. Follow the prompts. Choose a descriptive name, such as optimizely-integrity-check. Choose JavaScript as the language.

Create an EdgeKV namespace and database

  1. Log in to the Akamai Control Center.
  2. Go to CDN > EdgeKV.
  3. Create a namespace (like optimizely). Set the retention period appropriately.
  4. Create a database (like integrity-check) within the namespace. Note the namespace and database names for later.
  5. Grant the Read and Write permissions to the database.

Configure the bundle.json

Open the bundle.json file (created during akamai edgeworkers init) in your project directory and update it. It should look similar to the following sample:

{
  "edgeworker-version": "0.1.0",
  "description": "Optimizely Snippet Integrity Check",
  "name": "optimizely-integrity-check",
  "groupId": "YOUR_GROUP_ID",  // Replace with your Akamai Group ID
  "productId": "prd_Fresca",
  "contractId": "1-ABCDE",
  "version": "0.2.0",
  "notes": "",
  "source-location": "dist/workspaces/default/optimizely-integrity-check-0.2.0.tgz"
}
  • Add "edgekv": {} to the edgeworker.json.
  • Replace YOUR_GROUP_ID with your Akamai Group ID.

Create the EdgeWorker code (main.js)

Create a file named main.js inside the src directory and paste the following code:

// src/main.js
import { httpRequest } from 'http-request';
import { createResponse } from 'create-response';
import { EdgeKV } from './edgekv.js'; // Import the EdgeKV helper
import { TextEncoder, TextDecoder } from 'text-encoding';
import { crypto } from 'crypto';

// --- Configuration ---
const OPTIMIZELY_CDN_URL = "https://cdn.optimizely.com/js/";
const OPTIMIZELY_PROJECT_ID = "YOUR_PROJECT_ID";  // Replace with *your* Project ID
const EDGEKV_NAMESPACE = "optimizely"; // Replace with your EdgeKV namespace
const EDGEKV_DATABASE = "integrity-check"; // Replace with your EdgeKV database

// --- Helper Functions ---

async function sha256(message) {
    const msgUint8 = new TextEncoder().encode(message);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
    return hashHex;
}

function splitOptimizelySnippet(snippet) {
    const splitPoint = `"layers": [`;
    const splitIndex = snippet.indexOf(splitPoint);
    if (splitIndex === -1) {
        throw new Error("Could not find 'layers' marker in Optimizely snippet.");
    }
    return {
        staticCode: snippet.substring(0, splitIndex),
        dynamicCode: snippet.substring(splitIndex)
    };
}

function extractRevision(snippet) {
    const revisionRegex = /"revision":\s*"(\d+)"/;
    const match = snippet.match(revisionRegex);
    return (match && match[1]) ? match[1] : null;
}

// --- EdgeKV Interaction ---
const edgeKv = new EdgeKV({namespace: EDGEKV_NAMESPACE, database: EDGEKV_DATABASE});

async function getStoredHashAndRevision() {
    try {
        const hash = await edgeKv.getText({item: "staticHash", default_value: null});
        const revision = await edgeKv.getText({item: "revision", default_value: null });
        return { hash, revision };
    } catch (error) {
      if (error instanceof Error && error.message.includes("No item")) {
        return { hash: null, revision: null }; // Handle item not found
      }
      throw error; // Re-throw other errors
    }
}

async function storeHashAndRevision(hash, revision) {
    await edgeKv.putText({item: "staticHash", value: hash});
    await edgeKv.putText({item: "revision", value: revision});
}

// --- Main Event Handler ---
export async function onClientRequest(request) {
    try {
        const snippetUrl = `${OPTIMIZELY_CDN_URL}${OPTIMIZELY_PROJECT_ID}.js`;
        const snippetResponse = await httpRequest(snippetUrl);

        if (snippetResponse.status !== 200) {
            throw new Error(`Failed to fetch snippet: ${snippetResponse.status}`);
        }
       const snippetText = await snippetResponse.text();

        const currentRevision = extractRevision(snippetText);
        if (!currentRevision) {
            throw new Error("Could not extract revision number.");
        }

        const { staticCode } = splitOptimizelySnippet(snippetText);
        const currentStaticHash = await sha256(staticCode);

        const { hash: previousStaticHash, revision: previousRevision } = await getStoredHashAndRevision();

        let status = "success";
        let message = "Integrity check passed.";

        if (!previousStaticHash) {
            message = "Baseline established. First-time run.";
            await storeHashAndRevision(currentStaticHash, currentRevision);
        } else if (currentStaticHash !== previousStaticHash) {
            status = "failure";
            message = "Integrity check FAILED. Static code has changed.";
        }  else if (currentRevision === previousRevision) {
            message = "No changes detected (same revision).";
        }else {
            message = "Dynamic content updated (new revision). Static code integrity verified.";
            await storeHashAndRevision(currentStaticHash, currentRevision);
        }

        createResponse(
            200,
            {'Content-Type': ['application/json']},
            JSON.stringify({ status, message, revision: currentRevision, currentStaticHash, previousStaticHash, snippetUrl })
        );

    } catch (error) {
        createResponse(
            500,
            {'Content-Type': ['application/json']},
            JSON.stringify({ status: "error", message: error.message })
        );
    }
}
  • Replace YOUR_PROJECT_ID with your Optimizely Project ID.
  • Replace EDGEKV_NAMESPACE and EDGEKV_DATABASE with the names you chose.

Create edgekv.js (Required)

Create a file named edgekv.js in your src directory. This file provides a simplified interface for interacting with EdgeKV (provided by Akamai). Paste the following code into it:

// src/edgekv.js
import { httpRequest } from 'http-request';
import { createResponse } from 'create-response';
import { TextEncoder, TextDecoder } from 'text-encoding';

const ALGO = "JWT-HS256";
const TYP  = "JWT";
export class EdgeKV {

	constructor(options) {
		if (typeof options === "undefined") {
			options = {};
		}
		this.namespace = options.namespace || "default";
		this.group = options.group || "default";
		this.token = options.token || "edgekv_access_token";
		this.timeout = typeof options.timeout === "undefined" ? 5000 : options.timeout;
	}

	async getJwt() {
		let jwt = "";
		if (typeof env[this.token] === "undefined") {
            		throw new Error(`Missing EdgeKV access token. 
			You can use environment variables to inject the token into the EdgeWorker.
			Set variable ${this.token} to a valid EdgeKV access token.`);
		} else {
			try {
				let decoded = JSON.parse(new TextDecoder().decode(base64decode(env[this.token].split('.')[1])));
				if (decoded.exp - Math.floor(Date.now()/1000) <= 300) {
					throw new Error("EdgeKV access token has expired or will expire within 5 minutes")
				}
				jwt = env[this.token];
			} catch (error) {
				throw new Error("Issue decoding EdgeKV access token. Please ensure a valid token is being used. " + error);
			}
		}
		return jwt;
	}

	async request(method, item, value, callback) {
		let uri = `https://edgekv.akamai-edge-svcs.net/api/v1/namespaces/<span class="math-inline">\{this\.namespace\}/groups/</span>{this.group}/items/${item}`;
		let body = typeof value === "undefined" ? undefined : value;
		let jwt = await this.getJwt();
		let headers = {
			"X-Akamai-EdgeDB-Auth": [ `Bearer ${jwt}` ]
		};
		try {
            let response = await httpRequest(uri, {method:method, body:body, headers:headers, timeout:this.timeout});
            let response_body = await response.text();
            if (response.status >= 400) {
                throw {status: response.status, body: response_body};
            }
            let result = typeof callback === "undefined" ? response_body : callback(response_body);
            return result;

        } catch (error) {
            throw new Error(`Failed to ${method} EdgeKV ${error.status} : ${error.body}`);
        }
	}

	put(item, value, callback) {
        if (typeof item === "undefined") {
            throw new Error("Item can not be undefined");
        }
		return this.request("PUT", item, JSON.stringify(value), callback);
	}

	get(item, callback) {
        if (typeof item === "undefined") {
            throw new Error("Item can not be undefined");
        }
		return this.request("GET", item, undefined, callback);
	}

	delete(item, callback) {
        if (typeof item === "undefined") {
            throw new Error("Item can not be undefined");
        }
		return this.request("DELETE", item, undefined, callback);
	}

	putMetadata(item, value, callback) {
        if (typeof item === "undefined") {
            throw new Error("Item can not be undefined");
        }
		let value_json = {};
		try {
			value_json = JSON.parse(value);
		} catch (error) {
			throw new Error("Metadata value is not valid JSON: " + error);
		}
		return this.request("PUT", item, value, callback);
	}

	getMetadata(item, callback) {
        if (typeof item === "undefined") {
            throw new Error("Item can not be undefined");
        }
		return this.request("GET", item, undefined, callback);
	}

	// Convenience Methods

	putText(options) {
		let callback = typeof options.callback === "undefined" ? undefined : options.callback;
		return this.put(options.item, options.value, callback);
	}
	
	async getText(options) {
        let result = await this.get(options.item);
		return result;
	}

	putJson(options) {
		let callback = typeof options.callback === "undefined" ? undefined : options.callback;
		return this.put(options.item, options.value, callback);
	}
	
	async getJson(options) {
		let result = await this.get(options.item);
		if (result == null) return options.default_value;
		try {
			return JSON.parse(result)
		} catch {
			throw new Error("Retrieved value is not valid JSON")
		}
	}

	deleteText(options) {
		let callback = typeof options.callback === "undefined" ? undefined : options.callback;
		return this.delete(options.item, callback);
	}

	deleteJson(options) {
		let callback = typeof options.callback === "undefined" ? undefined : options.callback;
		return this.delete(options.item, callback);
	}
}

function base64decode(base64) {
	var result = [];
	var digitNum;
	var cur;
	var prev;

	for (var i = 23, len = base64.length; i < len; i++) {
		cur = decodeMap[base64.charCodeAt(i)];
		digitNum = (prev << 6) | cur;
		result.push(digitNum >> 4);

		if (cur !== 64) {
			digitNum = (prev << 12) | (cur << 6);
			result.push((digitNum & 0xff00) >> 8);
			result.push(digitNum & 0xff);
		}
		prev = cur;
	}
	return new Uint8Array(result);
}

var decodeMap = [
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1,
	-1, 0, -1, -1, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
	15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1,	26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
	45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1
];

How the code works

  • Configuration – Sets the URL for Optimizely's CDN, your project ID, and the EdgeKV namespace and database names.
  • sha256() – Calculates the SHA-256 hash (a cryptographic fingerprint) of a given string.
  • splitOptimizelySnippet() – Splits the Optimizely snippet into static and dynamic parts based on the "layers": [ marker.
  • extractRevision() – Finds the revision number within the snippet using a regular expression.
  • EdgeKV Interaction – The EdgeKV class (in edgekv.js) handles communication with your EdgeKV database. getStoredHashAndRevision() and storeHashAndRevision() functions use this class to read and write the trusted hash and revision.
  • onClientRequest() – This is the main function that runs when the EdgeWorker is triggered.
    • Fetches the Optimizely snippet.
    • Extracts the revision number.
    • Splits the snippet.
    • Calculates the hash of the static code.
    • Retrieves the previously stored hash and revision from EdgeKV.
    • Compares the current hash and revision with the stored values.
    • Returns a JSON response indicating success or failure, along with relevant details.
    • Handles the first run (baseline establishment) by storing the initial hash and revision.

Build and deploy

  1. Build the EdgeWorker.
    Bash  
    akamai edgeworkers build ./
    
    This creates a dist folder containing the deployable bundle (.tgz file).
  2. Deploy the EdgeWorker. You have a few options for deployment.
    1. Use the Akamai CLI.
      Bash
      akamai edgeworkers deploy dist/workspaces/default/<your_edgeworker_name_and_version>.tgz --contract <contractId> --accountkey <accountSwitchKey> --network STAGING
      
    2. Use the Akamai Control Center.
      1. Go to CDN > EdgeWorkers.
      2. Click Add EdgeWorker ID.
      3. Enter a name and select your group.
      4. Upload the .tgz file from the dist folder.
      5. Activate the EdgeWorker on the Staging network.
  3. Create an EdgeWorker ID or use an existing EdgeWorker ID in the Akamai Control center.
  4. Associate with a Property.
    1. Go to your Property Manager configuration in the Akamai Control Center.
    2. Add the EdgeWorkers behavior to the rule where you want to trigger the integrity check. The default rule is recommended.
    3. Configure the EdgeWorker behavior to use the EdgeWorker ID you created/deployed. You should trigger it on the Client Request event. You must configure the property to not cache the response from the EdgeWorker. Set the caching option to Bypass Cache or No Store for the rule that triggers the EdgeWorker. If you cache the response, you defeat the purpose of the integrity check.

Establish the initial baseline

After deploying your Optimizely snippet and activating the Akamai EdgeWorker, follow these steps:

  1. Ensure you deploy your initial Optimizely snippet through your standard, secure deployment process. This is the snippet you are trusting.
  2. Make a request to your website that triggers the EdgeWorker (based on your Property Manager configuration). You might need to flush Akamai's cache to ensure the EdgeWorker runs.
  3. Examine the response headers or body, depending on your EdgeWorker configuration. You might need to add custom headers or log the response to see the output. You should see:
    • status – success
    • message – Baseline established. First-time run."
    • The currentStaticHash and revision values. These are now your baseline.
  4. (Optional but recommended) Manually download the snippet from the snippetUrl in the response and compare it to what you expect.

Ongoing use

  1. Webhook integration (separate system) – You need a separate system (such as another EdgeWorker, a serverless function, or a script) to listen for Optimizely's snippet change webhooks. When a webhook is received, this system should trigger your integrity check EdgeWorker.
    There are several ways to do this:
    • Modify onClientRequest – The simplest approach is to modify the onClientRequest function in your EdgeWorker to check the request path or headers. If the request is from your webhook listener (like a specific URL), then perform the integrity check. Otherwise, just pass the request through.
    • Separate EdgeWorker – Create a second EdgeWorker that listens for the webhook and then makes an internal request (using httpRequest) to trigger the integrity check EdgeWorker. This is cleaner but more complex.
    • Use a different technology – Use cloud functions or similar to invoke the EdgeWorker.
  2. Deployment integration – Your deployment pipeline should only deploy new Optimizely snippets if the integrity check EdgeWorker returns a success response. How you achieve this depends on your deployment tools.
  3. Alerting – Configure alerts to notify you if the EdgeWorker reports a failure (integrity violation of the static code). You can use Akamai's logging and alerting features, or integrate with an external monitoring system.

Dynamic code governance

Because the dynamic part of the snippet changes frequently, you need a strong internal governance process to manage these changes:

  • Change control – Implement a formal process for reviewing and approving changes to experiments, targeting rules, and custom code within Optimizely.
  • Auditing – Use Optimizely's built-in audit logs (if available) to track who made changes and when.
  • Testing – Test all changes thoroughly in a staging environment before deploying them to production.
  • Approvals – Require multiple approvals for significant changes to experiments.
  • Documentation – Maintain clear documentation of your experiment configurations and custom code.
  • Staging environment – Use a staging or preview environment before pushing into production.

Legitimate static code updates

If you intentionally change the static part of your Optimizely snippet, like when Optimizely releases a new SDK version, follow these steps:

  • Make changes. Update your Optimizely snippet as needed.
  • Expect failure. The integrity check (correctly) fails because the static code's hash changes.
  • Re-establish the basline after carefully verifying your changes.
    • Option 1 (Recommended) – Temporarily disable the integrity check in your deployment pipeline (such as by commenting out the EdgeWorkers behavior in your Property Manager configuration), deploy the new snippet, and trigger the worker again. This updates the stored hash. Re-enable the integrity check.
    • Option 2 – Manually update the staticHash value in your Akamai EdgeKV storage after you verify the new snippet. You can do this via the Akamai CLI or Control Center.