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

chore: experimentation plugin version increment #577

Open
wants to merge 76 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
d9f233c
chore: initial commit
ramboz Aug 31, 2023
d951ec4
doc: update readme
ramboz Aug 31, 2023
3532070
doc: add code of conduct
ramboz Aug 31, 2023
e6d9d01
doc: update readme
ramboz Aug 31, 2023
50eea0f
doc: update readme
ramboz Aug 31, 2023
74889ca
Update README.md
meryllblanchet Sep 5, 2023
ae1395e
fix: possible error when selected variant got deleted
ramboz Sep 5, 2023
066cbc9
fix: audience parsing
ramboz Sep 7, 2023
ee3cd15
fix: preview.css pass issues
ramboz Sep 7, 2023
84eabff
fix: audience parsing
ramboz Sep 7, 2023
c873eb6
fix: experiment variant attribution edge case
ramboz Sep 7, 2023
93902a0
fix: manifest parsing of experiment name
ramboz Sep 7, 2023
9accda7
doc: update readme
ramboz Sep 8, 2023
044d33e
chore: fix linting issues
ramboz Sep 8, 2023
a30d42d
fix: support variants that are index.docx files
ramboz Sep 14, 2023
9ae55e8
fix: support variants that are index.docx files
ramboz Sep 14, 2023
9b28002
fix: split percentage parsing
ramboz Sep 14, 2023
c84235a
fix: experiment split metadata parsing
ramboz Sep 14, 2023
5e1c34a
fix: audience parsing
ramboz Sep 14, 2023
80f6997
fix: css load path if prefix is used, and minor css adjustments to pill
ramboz Sep 18, 2023
40977e3
doc: rebranding to aem
ramboz Sep 26, 2023
e2d61a8
chore: minor fixes and rebranding
ramboz Oct 3, 2023
c7717e5
chore: minor fixes and rebranding
ramboz Oct 3, 2023
1ee2ee2
doc: update readme
ramboz Oct 3, 2023
f602282
fix: fix linting issues and style leaks
ramboz Oct 3, 2023
a77433f
style: properly inherit text color in the overlay
ramboz Oct 4, 2023
44a3aa4
fix: support installation in sub-directories
ramboz Oct 4, 2023
4438f52
doc: update readme
ramboz Oct 4, 2023
4467d3e
fix: block-level experiments resolution
ramboz Oct 9, 2023
92e2abb
fix: campaigns parsing
ramboz Oct 24, 2023
7fbef36
feat: limit the sampling rate (#1)
ramboz Oct 24, 2023
4789afd
feat: limit the sampling rate
ramboz Oct 24, 2023
a486023
fix: audience parsing
ramboz Oct 25, 2023
0071dbd
fix: audience pill activation
ramboz Oct 25, 2023
162aab2
fix: improve anonymization for better gdpr/hippa compliance (#3)
ramboz Oct 25, 2023
9028520
fix: possible css leaking into pill overlay
ramboz Oct 26, 2023
287e35f
feat: adopt the plugin api (#2)
ramboz Oct 27, 2023
be2e07d
doc: update README.md
ramboz Oct 27, 2023
84444c7
chore: rename the plugin for GA (#4)
ramboz Oct 30, 2023
f3d6de1
chore: rename the plugin for GA (#5)
ramboz Oct 30, 2023
fc522cd
fix(fullExperiment): replaceInner page index (#7)
ilievlad73 Oct 31, 2023
5706df2
doc: update readme
ramboz Oct 31, 2023
effa50e
feat: expose the served url in the respective apis (#8)
ramboz Oct 31, 2023
9d1d32a
feat: add support for direct experiment manfiest url in the metadata
ramboz Nov 6, 2023
1e34eae
fix: batch of minor fixes
ramboz Nov 27, 2023
f0904be
fix: typo in readme
ramboz Nov 27, 2023
d2028dc
fix: simulation should work even if audience is not resolved
ramboz Nov 27, 2023
b4d3dab
fix: track rum when selected variant is control (#9)
chicharr Dec 4, 2023
b033075
doc: add warning about recent sampleRUM
ramboz Dec 4, 2023
06a92c5
feat: update plugin to leverage v3 RUM APIs and domain key (#10)
ramboz Jan 5, 2024
162896d
Update README.md
ramboz Jan 5, 2024
d6318cf
fix: gracefully handle projects where block-level experiments aren't …
ramboz Jan 15, 2024
02617d2
fix: sanitize experiment and variant names for proper css class injec…
ramboz Jan 16, 2024
4ff6246
fix: leaking heading styles into the pill
ramboz Jan 16, 2024
46e6a48
Update README.md
ramboz Jan 18, 2024
fee6692
Update README.md
ramboz Jan 22, 2024
53696f4
fix: RUM endpoint url returns empty results
ramboz Jan 29, 2024
2034373
fix: pill does not show any results even if RUM endpoint contains data
ramboz Jan 29, 2024
21890bb
fix: improve production host detection to better cover edge cases
ramboz Jan 30, 2024
e79706d
fix: make sure the pill is not shown on live and prod hosts
ramboz Feb 6, 2024
9e7b01a
fix: expose resolved audiences when running campaigns
ramboz Feb 28, 2024
d9f3d01
fix: issue 11 (#12)
ivanviviani Feb 28, 2024
3ec48c9
fix: invalid url when given a path for variant (#14)
buuhuu Mar 6, 2024
0b293f1
fix: always await replaceInner (#18)
buuhuu Mar 15, 2024
4b6fa0d
feat: add support for setting experiment status, start date and end date
ramboz Mar 15, 2024
4b4a853
fix: active status sanitization
ramboz Mar 18, 2024
e04a21c
fix: fetch without plain selector (#19)
buuhuu Mar 19, 2024
f54a6ca
doc: add error handling steps for git subtree pull command
ramboz Apr 4, 2024
c8d6c4a
Removing existing plugin to fix squashing issue
iamtarun99 Apr 4, 2024
86befb0
Add 'plugins/experimentation/' from commit 'f54a6cafda373ab388d7dd225…
iamtarun99 Apr 4, 2024
6f33207
fix: variants are not properly decorated
ramboz Apr 8, 2024
54c17e5
feat: add support for code-only experiments
ramboz Apr 9, 2024
b9ba2ec
Merge commit '54c17e596bf269690dbe06bb5575af59a006ad77' into experime…
iamtarun99 Apr 9, 2024
b6e2bfd
Squashed 'plugins/experimentation/' changes from f54a6ca..3814bb9
ramboz Apr 9, 2024
4e12fa9
Merge commit 'b6e2bfdd727716b84f7f81690f16cd0daf8688cc' into sites-20…
ramboz Apr 9, 2024
9a979fc
Merge branch 'main' into SITES-20990-exp-plugin-version-increment
solaris007 Apr 24, 2024
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
31 changes: 23 additions & 8 deletions plugins/experimentation/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# AEM Edge Delivery Services Experimenation
# AEM Edge Delivery Services Experimentation

The AEM Experimentation plugin helps you quickly set up experimentation and segmentation on your AEM project.
It is currently available to customers in collaboration with AEM Engineering via co-innovation VIP Projects.
Expand Down Expand Up @@ -26,14 +26,22 @@ git subtree pull --squash --prefix plugins/experimentation [email protected]:adobe/

If you prefer using `https` links you'd replace `[email protected]:adobe/aem-experimentation.git` in the above commands by `https://github.com/adobe/aem-experimentation.git`.

If the `subtree pull` command is failing with an error like:
```
fatal: can't squash-merge: 'plugins/experimentation' was never added
```
you can just delete the folder and re-add the plugin via the `git subtree add` command above.

## Project instrumentation

:warning: The plugin requires that you have a recent RUM instrumentation from the AEM boilerplate that supports `sampleRUM.always`. If you are getting errors that `.on` cannot be called on an `undefined` object, please apply the changes from https://github.com/adobe/aem-boilerplate/pull/247/files to your `lib-franklin.js`.

### On top of the plugin system

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 `window.hlx.plugins` is defined on your page.

If you don't have it, you can follow the proposal in https://github.com/adobe/aem-lib/pull/23 and apply the changes to your `aem.js`/`lib-franklin.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`.

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
Expand All @@ -52,9 +60,9 @@ window.hlx.plugins.add('experimentation', {
});
```

### Without the plugin system
### On top of a regular boilerplate project

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:
Typically, you'd know you don't have the plugin system if you don't see a reference to `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. at the start of the file:
```js
Expand Down Expand Up @@ -131,13 +139,16 @@ There are various aspects of the plugin that you can configure via options you a
You have already seen the `audiences` option in the examples above, but here is the full list we support:

```js
runEager.call(pluginContext, {
runEager.call(document, {
// Overrides the base path if the plugin was installed in a sub-directory
basePath: '',
// Lets you configure if we are in a prod environment or not

// Lets you configure the prod environment.
// (prod environments do not get the pill overlay)
prodHost: 'www.my-website.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')
|| window.location.hostname === ('localhost'),

/* Generic properties */
// RUM sampling rate on regular AEM pages is 1 out of 100 page views
Expand All @@ -146,6 +157,10 @@ runEager.call(pluginContext, {
// short durations of those campaigns/experiments
rumSamplingRate: 10,

// the storage type used to persist data between page views
// (for instance to remember what variant in an experiment the user was served)
storage: window.SessionStorage,

/* Audiences related properties */
// See more details on the dedicated Audiences page linked below
audiences: {},
Expand All @@ -163,7 +178,7 @@ runEager.call(pluginContext, {
experimentsConfigFile: 'manifest.json',
experimentsMetaTag: 'experiment',
experimentsQueryParameter: 'experiment',
});
}, pluginContext);
```

For detailed implementation instructions on the different features, please read the dedicated pages we have on those topics:
Expand Down
134 changes: 92 additions & 42 deletions plugins/experimentation/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,29 +75,29 @@ export async function getResolvedAudiences(applicableAudiences, options, context
/**
* Replaces element with content from path
* @param {string} path
* @param {HTMLElement} element
* @param {boolean} isBlock
* @param {HTMLElement} main
* @return Returns the path that was loaded or null if the loading failed
*/
async function replaceInner(path, element) {
const plainPath = path.endsWith('/')
? `${path}index.plain.html`
: `${path}.plain.html`;
async function replaceInner(path, main) {
try {
const resp = await fetch(plainPath);
const resp = await fetch(path);
if (!resp.ok) {
// eslint-disable-next-line no-console
console.log('error loading content:', resp);
return false;
return null;
}
const html = await resp.text();
// eslint-disable-next-line no-param-reassign
element.innerHTML = html;
return true;
// parse with DOMParser to guarantee valid HTML, and no script execution(s)
const dom = new DOMParser().parseFromString(html, 'text/html');
// do not use replaceWith API here since this would replace the main reference
// in scripts.js as well and prevent proper decoration of the sections/blocks
main.innerHTML = dom.querySelector('main').innerHTML;
return path;
} catch (e) {
// eslint-disable-next-line no-console
console.log(`error loading content: ${plainPath}`, e);
console.log(`error loading content: ${path}`, e);
}
return false;
return null;
}

/**
Expand Down Expand Up @@ -223,7 +223,7 @@ function inferEmptyPercentageSplits(variants) {
* @param {string} instantExperiment The list of varaints
* @returns {object} the experiment manifest
*/
export function getConfigForInstantExperiment(
function getConfigForInstantExperiment(
experimentId,
instantExperiment,
pluginOptions,
Expand All @@ -233,13 +233,18 @@ export function getConfigForInstantExperiment(
const config = {
label: `Instant Experiment: ${experimentId}`,
audiences: audience ? audience.split(',').map(context.toClassName) : [],
status: 'Active',
status: context.getMetadata(`${pluginOptions.experimentsMetaTag}-status`) || 'Active',
startDate: context.getMetadata(`${pluginOptions.experimentsMetaTag}-start-date`),
endDate: context.getMetadata(`${pluginOptions.experimentsMetaTag}-end-date`),
id: experimentId,
variants: {},
variantNames: [],
};

const pages = instantExperiment.split(',').map((p) => new URL(p.trim()).pathname);
const nbOfVariants = Number(instantExperiment);
const pages = Number.isNaN(nbOfVariants)
? instantExperiment.split(',').map((p) => new URL(p.trim(), window.location).pathname)
: new Array(nbOfVariants).fill(window.location.pathname);

const splitString = context.getMetadata(`${pluginOptions.experimentsMetaTag}-split`);
const splits = splitString
Expand Down Expand Up @@ -287,8 +292,15 @@ export function getConfigForInstantExperiment(
* @param {object} pluginOptions The plugin options
* @returns {object} containing the experiment manifest
*/
export async function getConfigForFullExperiment(experimentId, pluginOptions, context) {
const path = `${pluginOptions.experimentsRoot}/${experimentId}/${pluginOptions.experimentsConfigFile}`;
async function getConfigForFullExperiment(experimentId, pluginOptions, context) {
let path;
if (experimentId.includes(`/${pluginOptions.experimentsConfigFile}`)) {
path = new URL(experimentId, window.location.origin).href;
// eslint-disable-next-line no-param-reassign
[experimentId] = path.split('/').splice(-2, 1);
} else {
path = `${pluginOptions.experimentsRoot}/${experimentId}/${pluginOptions.experimentsConfigFile}`;
}
try {
const resp = await fetch(path);
if (!resp.ok) {
Expand All @@ -307,6 +319,7 @@ export async function getConfigForFullExperiment(experimentId, pluginOptions, co
config.manifest = path;
config.basePath = `${pluginOptions.experimentsRoot}/${experimentId}`;
inferEmptyPercentageSplits(Object.values(config.variants));
config.status = context.getMetadata(`${pluginOptions.experimentsMetaTag}-status`) || config.status;
return config;
} catch (e) {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -336,7 +349,7 @@ function getDecisionPolicy(config) {
return decisionPolicy;
}

export async function getConfig(experiment, instantExperiment, pluginOptions, context) {
async function getConfig(experiment, instantExperiment, pluginOptions, context) {
const usp = new URLSearchParams(window.location.search);
const [forcedExperiment, forcedVariant] = usp.has(pluginOptions.experimentsQueryParameter)
? usp.get(pluginOptions.experimentsQueryParameter).split('/')
Expand All @@ -357,23 +370,22 @@ export async function getConfig(experiment, instantExperiment, pluginOptions, co
: null;

experimentConfig.resolvedAudiences = await getResolvedAudiences(
experimentConfig.audiences,
experimentConfig.audiences.map(context.toClassName),
pluginOptions,
context,
);
experimentConfig.run = (
// experiment is active or forced
(context.toCamelCase(experimentConfig.status) === 'active' || forcedExperiment)
(['active', 'on', 'true'].includes(context.toClassName(experimentConfig.status)) || forcedExperiment)
// experiment has resolved audiences if configured
&& (!experimentConfig.resolvedAudiences || experimentConfig.resolvedAudiences.length)
// forced audience resolves if defined
&& (!forcedAudience || experimentConfig.audiences.includes(forcedAudience))
&& (!experimentConfig.startDate || new Date(experimentConfig.startDate) <= Date.now())
&& (!experimentConfig.endDate || new Date(experimentConfig.endDate) > Date.now())
);

window.hlx = window.hlx || {};
if (!experimentConfig.run) {
return false;
}
window.hlx.experiment = experimentConfig;

// eslint-disable-next-line no-console
Expand Down Expand Up @@ -413,10 +425,26 @@ export async function runExperiment(document, options, context) {
console.warn('Invalid experiment config. Please review your metadata, sheet and parser.');
return false;
}

const usp = new URLSearchParams(window.location.search);
const forcedVariant = usp.has(pluginOptions.experimentsQueryParameter)
? usp.get(pluginOptions.experimentsQueryParameter).split('/')[1]
: null;
if (!experimentConfig.run && !forcedVariant) {
// eslint-disable-next-line no-console
console.warn('Experiment will not run. It is either not active or its configured audiences are not resolved.');
return false;
}
// eslint-disable-next-line no-console
console.debug(`running experiment (${window.hlx.experiment.id}) -> ${window.hlx.experiment.selectedVariant}`);

if (experimentConfig.selectedVariant === experimentConfig.variantNames[0]) {
document.body.classList.add(`experiment-${context.toClassName(experimentConfig.id)}`);
document.body.classList.add(`variant-${context.toClassName(experimentConfig.selectedVariant)}`);
context.sampleRUM('experiment', {
source: experimentConfig.id,
target: experimentConfig.selectedVariant,
});
return false;
}

Expand All @@ -428,18 +456,24 @@ export async function runExperiment(document, options, context) {
const currentPath = window.location.pathname;
const control = experimentConfig.variants[experimentConfig.variantNames[0]];
const index = control.pages.indexOf(currentPath);
if (index < 0 || pages[index] === currentPath) {
if (index < 0) {
return false;
}

// Fullpage content experiment
document.body.classList.add(`experiment-${experimentConfig.id}`);
const result = await replaceInner(pages[0], document.querySelector('main'));
document.body.classList.add(`experiment-${context.toClassName(experimentConfig.id)}`);
let result;
if (pages[index] !== currentPath) {
result = await replaceInner(pages[index], document.querySelector('main'));
} else {
result = currentPath;
}
experimentConfig.servedExperience = result || currentPath;
if (!result) {
// eslint-disable-next-line no-console
console.debug(`failed to serve variant ${window.hlx.experiment.selectedVariant}. Falling back to ${experimentConfig.variantNames[0]}.`);
}
document.body.classList.add(`variant-${result ? experimentConfig.selectedVariant : experimentConfig.variantNames[0]}`);
document.body.classList.add(`variant-${context.toClassName(result ? experimentConfig.selectedVariant : experimentConfig.variantNames[0])}`);
context.sampleRUM('experiment', {
source: experimentConfig.id,
target: result ? experimentConfig.selectedVariant : experimentConfig.variantNames[0],
Expand All @@ -463,9 +497,10 @@ export async function runCampaign(document, options, context) {
}

let audiences = context.getMetadata(`${pluginOptions.campaignsMetaTagPrefix}-audience`);
let resolvedAudiences = null;
if (audiences) {
audiences = audiences.split(',').map(context.toClassName);
const resolvedAudiences = await getResolvedAudiences(audiences, pluginOptions, context);
resolvedAudiences = await getResolvedAudiences(audiences, pluginOptions, context);
if (!!resolvedAudiences && !resolvedAudiences.length) {
return false;
}
Expand All @@ -481,9 +516,15 @@ export async function runCampaign(document, options, context) {
return false;
}

window.hlx.campaign = { selectedCampaign: campaign };
if (resolvedAudiences) {
window.hlx.campaign.resolvedAudiences = window.hlx.campaign;
}

try {
const url = new URL(urlString);
const result = replaceInner(url.pathname, document.querySelector('main'));
const result = await replaceInner(url.pathname, document.querySelector('main'));
window.hlx.campaign.servedExperience = result || window.location.pathname;
if (!result) {
// eslint-disable-next-line no-console
console.debug(`failed to serve campaign ${campaign}. Falling back to default content.`);
Expand Down Expand Up @@ -513,7 +554,7 @@ export async function serveAudience(document, options, context) {
}

const audiences = await getResolvedAudiences(
Object.keys(configuredAudiences),
Object.keys(configuredAudiences).map(context.toClassName),
pluginOptions,
context,
);
Expand All @@ -526,17 +567,21 @@ export async function serveAudience(document, options, context) {
? context.toClassName(usp.get(pluginOptions.audiencesQueryParameter))
: null;

const urlString = configuredAudiences[forcedAudience || audiences[0]];
const selectedAudience = forcedAudience || audiences[0];
const urlString = configuredAudiences[selectedAudience];
if (!urlString) {
return false;
}

window.hlx.audience = { selectedAudience };

try {
const url = new URL(urlString);
const result = replaceInner(url.pathname, document.querySelector('main'));
const result = await replaceInner(url.pathname, document.querySelector('main'));
window.hlx.audience.servedExperience = result || window.location.pathname;
if (!result) {
// eslint-disable-next-line no-console
console.debug(`failed to serve audience ${forcedAudience || audiences[0]}. Falling back to default content.`);
console.debug(`failed to serve audience ${selectedAudience}. Falling back to default content.`);
}
document.body.classList.add(audiences.map((audience) => `audience-${audience}`));
context.sampleRUM('audiences', {
Expand All @@ -551,7 +596,7 @@ export async function serveAudience(document, options, context) {
}
}

window.hlx.patchBlockConfig.push((config) => {
window.hlx.patchBlockConfig?.push((config) => {
const { experiment } = window.hlx;

// No experiment is running
Expand Down Expand Up @@ -652,11 +697,16 @@ export async function loadLazy(document, options, context) {
...DEFAULT_OPTIONS,
...(options || {}),
};
if (window.location.hostname.endsWith('hlx.page')
|| window.location.hostname === ('localhost')
|| (typeof options.isProd === 'function' && !options.isProd())) {
// eslint-disable-next-line import/no-cycle
const preview = await import('./preview.js');
preview.default(document, pluginOptions, { ...context, getResolvedAudiences });
}
// do not show the experimentation pill on prod domains
if (window.location.hostname.endsWith('.live')
|| (typeof options.isProd === 'function' && options.isProd())
|| (options.prodHost
&& (options.prodHost === window.location.host
|| options.prodHost === window.location.hostname
|| options.prodHost === window.location.origin))) {
return;
}
// eslint-disable-next-line import/no-cycle
const preview = await import('./preview.js');
preview.default(document, pluginOptions, { ...context, getResolvedAudiences });
}
Loading
Loading