Skip to content
Open
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
311 changes: 243 additions & 68 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@ The AEM Experimentation plugin helps you quickly set up experimentation and segm
It is currently available to customers in collaboration with AEM Engineering via co-innovation VIP Projects.
To implement experimentation or personalization use-cases, please reach out to the AEM Engineering team in the Slack channel dedicated to your project.

> **Note:** We are adding new support for the contextual experimentation rail UI. This is still under development. Feel free to reach out if you have any questions via email: aem-contextual-experimentation@adobe.com.

## Features

The AEM Experimentation plugin supports:
- :busts_in_silhouette: serving different content variations to different audiences, including custom audience definitions for your project that can be either resolved directly in-browser or against a trusted backend API.
- :money_with_wings: serving different content variations based on marketing campaigns you are running, so that you can easily track email and/or social campaigns
- :chart_with_upwards_trend: running A/B test experiments on a set of variants to measure and improve the conversion on your site. This works particularly with our :chart: [RUM conversion tracking plugin](https://github.com/adobe/franklin-rum-conversion).
- :shield: privacy-compliant experimentation with built-in consent management support for GDPR, CCPA, and other privacy regulations
- :rocket: easy simulation of each experience and basic reporting leveraging in-page overlays

## Installation

Add the plugin to your AEM project by running:

```sh
git subtree add --squash --prefix plugins/experimentation git@github.com:adobe/aem-experimentation.git v2
```

If you later want to pull the latest changes and update your local copy of the plugin
If you later want to pull the latest changes and update your local copy of the plugin:

```sh
git subtree pull --squash --prefix plugins/experimentation git@github.com:adobe/aem-experimentation.git v2
```
Expand All @@ -28,85 +33,85 @@ If you prefer using `https` links you'd replace `git@github.com:adobe/aem-experi

## Project instrumentation

### On top of a regular boilerplate project
### Key Files to Add or Modify

Typically, you'd know you don't have the plugin system if you don't see a reference to `window.aem.plugins` or `window.hlx.plugins` in your `scripts.js`. In that case, you can still manually instrument this plugin in your project by falling back to a more manual instrumentation. To properly connect and configure the plugin for your project, you'll need to edit your `scripts.js` in your AEM project and add the following:
1. **plugins/experimentation** - Add this folder containing the experimentation engine plugins (see Installation section above)
2. **scripts/experiment-loader.js** - Add this script to handle experiment loading
3. **scripts/scripts.js** - Modify this script with the configuration

1. at the start of the file:
```js
const experimentationConfig = {
prodHost: 'www.my-site.com',
audiences: {
mobile: () => window.innerWidth < 600,
desktop: () => window.innerWidth >= 600,
// define your custom audiences here as needed
}
};
### Step 1: Create `scripts/experiment-loader.js`

let runExperimentation;
let showExperimentationOverlay;
const isExperimentationEnabled = document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"],[property^="campaign:"],[property^="audience:"]')
|| [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i));
if (isExperimentationEnabled) {
({
loadEager: runExperimentation,
loadLazy: showExperimentationOverlay,
} = await import('../plugins/experimentation/src/index.js'));
}
```
2. Early in the `loadEager` method you'll need to add:
```js
async function loadEager(doc) {
// Add below snippet early in the eager phase
if (runExperimentation) {
await runExperimentation(document, experimentationConfig);
}
}
```
This needs to be done as early as possible since this will be blocking the eager phase and impacting your LCP, so we want this to execute as soon as possible.
3. Finally at the end of the `loadLazy` method you'll have to add:
```js
async function loadLazy(doc) {
// Add below snippet at the end of the lazy phase
if (showExperimentationOverlay) {
await showExperimentationOverlay(document, experimentationConfig);
Create a new file `scripts/experiment-loader.js` with the following content:

```js
/**
* Checks if experimentation is enabled.
* @returns {boolean} True if experimentation is enabled, false otherwise.
*/
const isExperimentationEnabled = () => document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"],[property^="campaign:"],[property^="audience:"]')
|| [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i));

/**
* Loads the experimentation module (eager).
* @param {Document} document The document object.
* @param {Object} config The experimentation configuration.
* @returns {Promise<void>} A promise that resolves when the experimentation module is loaded.
*/
export async function runExperimentation(document, config) {
if (!isExperimentationEnabled()) {
window.addEventListener('message', async (event) => {
if (event.data?.type === 'hlx:experimentation-get-config') {
event.source.postMessage({
type: 'hlx:experimentation-config',
config: { experiments: [], audiences: [], campaigns: [] },
source: 'no-experiments'
}, '*');
}
}
```
This is mostly used for the authoring overlay, and as such isn't essential to the page rendering, so having it at the end of the lazy phase is good enough.
});
return null;
}

try {
const { loadEager } = await import(
'../plugins/experimentation/src/index.js'
);
return loadEager(document, config);
} catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to load experimentation module (eager):', error);
return null;
}
}

### On top of the plugin system (deprecated)
```

The easiest way to add the plugin is if your project is set up with the plugin system extension in the boilerplate.
You'll know you have it if either `window.aem.plugins` or `window.hlx.plugins` is defined on your page.
### Step 2: Update `scripts/scripts.js`

If you don't have it, you can follow the proposal in https://github.com/adobe/aem-lib/pull/23 and https://github.com/adobe/aem-boilerplate/pull/275 and apply the changes to your `aem.js`/`lib-franklin.js` and `scripts.js`.
Add the following import and configuration at the top of your `scripts/scripts.js`:

Once you have confirmed this, you'll need to edit your `scripts.js` in your AEM project and add the following at the start of the file:
```js
import {
runExperimentation,
} from './experiment-loader.js';

const experimentationConfig = {
prodHost: 'www.my-site.com',
prodHost: 'www.mysite.com', // add your prodHost here, otherwise we will show mock data
audiences: {
mobile: () => window.innerWidth < 600,
desktop: () => window.innerWidth >= 600,
// define your custom audiences here as needed
}
},
};
```

window.aem.plugins.add('experimentation', { // use window.hlx instead of your project has this
condition: () =>
// page level metadata
document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"]')
// decorated section metadata
|| document.querySelector('.section[class*=experiment],.section[class*=audience],.section[class*=campaign]')
// undecorated section metadata
|| [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i)),
options: experimentationConfig,
url: '/plugins/experimentation/src/index.js',
});
Then, add the following line early in your `loadEager()` function:

```js
async function loadEager(doc) {
// ... existing code ...
await runExperimentation(doc, experimentationConfig);
// ... rest of your code ...
}
```

### Increasing sampling rate for low traffic pages
Expand Down Expand Up @@ -137,14 +142,14 @@ If this is not present, please apply the following changes to the file: https://

### Custom options

There are various aspects of the plugin that you can configure via options you are passing to the 2 main methods above (`runEager`/`runLazy`).
There are various aspects of the plugin that you can configure via the `experimentationConfig` object.
You have already seen the `audiences` option in the examples above, but here is the full list we support:

```js
runEager.call(document, {
const experimentationConfig = {
// Lets you configure the prod environment.
// (prod environments do not get the pill overlay)
prodHost: 'www.my-website.com',
prodHost: 'www.mysite.com',
// if you have several, or need more complex logic to toggle pill overlay, you can use
isProd: () => !window.location.hostname.endsWith('hlx.page')
&& window.location.hostname !== ('localhost'),
Expand Down Expand Up @@ -176,7 +181,7 @@ runEager.call(document, {
buildBlock(el);
decorateBlock(el);
}
});
};
```

For detailed implementation instructions on the different features, please read the dedicated pages we have on those topics:
Expand All @@ -200,6 +205,175 @@ The plugin exposes experiment data through two mechanisms:

### Available APIs

#### Consent Management

The plugin provides consent management APIs for privacy compliance. Experiments can be configured to require user consent before running.

**APIs:**

```javascript
import {
isUserConsentGiven,
updateUserConsent
} from './plugins/experimentation/src/index.js';

// Check if user has consented to experimentation
const hasConsent = isUserConsentGiven();

// Integrate this with your consent management platform events to track the user's choice
updateUserConsent(true); // or false to revoke consent
```

**Requiring consent for an experiment:**

Add the `Experiment Requires Consent` metadata property:

| Metadata | |
|-----------------------|--------------------------------------------------------------|
| Experiment | Hero Test |
| Experiment Variants | /variant-1, /variant-2 |
| Experiment Requires Consent | true |

**Implementation:**

You can integrate consent management in two ways:

1. **In your `experiment-loader.js`** (recommended) - keeps all experimentation code together
2. **In your `scripts.js`** - if you need consent for other purposes beyond experimentation

<details>
<summary>Recommended: Integrate in experiment-loader.js</summary>

```javascript
// experiment-loader.js
import {
updateUserConsent,
isUserConsentGiven,
} from '../plugins/experimentation/src/index.js';

/**
* Initialize consent management
* Choose ONE of the setup functions based on your CMP (Consent Management Platform)
*
* IMPORTANT: These are example implementations. Please:
* 1. Verify the consent categories match your OneTrust/Cookiebot configuration
* 2. Test thoroughly in your environment
* 3. Consult with your legal/privacy team about consent requirements
*/
function initConsent() {
// OPTION 1: OneTrust
function setupOneTrustConsent() {
// Step 1: Bridge OneTrust's callback to dispatch a custom event
window.OptanonWrapper = function() {
const activeGroups = window.OnetrustActiveGroups || '';
const groups = activeGroups.split(',').filter(g => g);
window.dispatchEvent(new CustomEvent('consent.onetrust', {
detail: groups
}));
};

// Step 2: Listen for the custom event
function consentEventHandler(ev) {
const groups = ev.detail;
const hasConsent = groups.includes('C0003') // Functional Cookies
|| groups.includes('C0004'); // Targeting Cookies
updateUserConsent(hasConsent);
}
window.addEventListener('consent.onetrust', consentEventHandler);
}

// OPTION 2: Cookiebot
function setupCookiebotConsent() {
function handleCookiebotConsent() {
const preferences = window.Cookiebot?.consent?.preferences || false;
const marketing = window.Cookiebot?.consent?.marketing || false;
updateUserConsent(preferences || marketing);
}
window.addEventListener('CookiebotOnConsentReady', handleCookiebotConsent);
window.addEventListener('CookiebotOnAccept', handleCookiebotConsent);
}

// OPTION 3: Custom Consent Banner
function setupCustomConsent() {
document.addEventListener('consent-updated', (event) => {
updateUserConsent(event.detail.experimentation);
});
}

// Choose ONE:
setupOneTrustConsent(); // or setupCookiebotConsent() or setupCustomConsent()
}

export async function runExperimentation(document, config) {
if (!isExperimentationEnabled()) {
return null;
}

// Initialize consent BEFORE loading experimentation
initConsent();

const { loadEager } = await import('../plugins/experimentation/src/index.js');
return loadEager(document, config);
}

// Export consent functions for use elsewhere if needed
export { updateUserConsent, isUserConsentGiven };
```

Your `scripts.js` stays clean - no consent code needed there!

</details>

<details>
<summary>Integrate in scripts.js</summary>

```javascript
// scripts.js
import {
updateUserConsent,
isUserConsentGiven,
} from '../plugins/experimentation/src/index.js';

import { runExperimentation } from './experiment-loader.js';

// Setup consent (choose ONE based on your CMP)
function setupOneTrustConsent() {
// Step 1: Bridge OneTrust's callback to dispatch a custom event
window.OptanonWrapper = function() {
const activeGroups = window.OnetrustActiveGroups || '';
const groups = activeGroups.split(',').filter(g => g);
window.dispatchEvent(new CustomEvent('consent.onetrust', {
detail: groups
}));
};

// Step 2: Listen for the custom event
function consentEventHandler(ev) {
const groups = ev.detail;
const hasConsent = groups.includes('C0003') // Functional Cookies
|| groups.includes('C0004'); // Targeting Cookies
updateUserConsent(hasConsent);
}
window.addEventListener('consent.onetrust', consentEventHandler);
}

async function loadEager(doc) {
document.documentElement.lang = 'en';
decorateTemplateAndTheme();

// Initialize consent BEFORE running experiments
setupOneTrustConsent();

await runExperimentation(doc, experimentationConfig);

// ... rest of your code
}
```

</details>

For detailed usage instructions and more examples, see the [Experiments documentation](/documentation/experiments.md#consent-based-experiments).

#### Events

Listen for the `aem:experimentation` event to react when experiments, campaigns, or audiences are applied:
Expand Down Expand Up @@ -508,6 +682,7 @@ Here's the complete experiment config structure available in `window.hlx.experim
variantNames: ["control", "challenger-1"],
audiences: ["mobile", "desktop"],
resolvedAudiences: ["mobile"],
requiresConsent: false, // whether this experiment requires user consent
run: true,
variants: {
control: { percentageSplit: "0.5", pages: ["/current"], label: "Control" },
Expand Down
Loading