Skip to content

Commit

Permalink
Squashed 'plugins/experimentation/' changes from d2028dc..e79706d
Browse files Browse the repository at this point in the history
e79706d fix: make sure the pill is not shown on live and prod hosts
21890bb fix: improve production host detection to better cover edge cases
2034373 fix: pill does not show any results even if RUM endpoint contains data
53696f4 fix: RUM endpoint url returns empty results
fee6692 Update README.md
46e6a48 Update README.md
4ff6246 fix: leaking heading styles into the pill
02617d2 fix: sanitize experiment and variant names for proper css class injection
d6318cf fix: gracefully handle projects where block-level experiments aren't instrumented
162896d Update README.md
06a92c5 feat: update plugin to leverage v3 RUM APIs and domain key (hlxsites#10)
b033075 doc: add warning about recent sampleRUM
b4d3dab fix: track rum when selected variant is control (hlxsites#9)

git-subtree-dir: plugins/experimentation
git-subtree-split: e79706d94597806c70bd32a50e76b49ea5ee169a
  • Loading branch information
Vlad Ilie committed Feb 26, 2024
1 parent 309ff1c commit 8faa1fb
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 29 deletions.
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ If you prefer using `https` links you'd replace `git@github.com:adobe/aem-experi

## 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.
Expand All @@ -52,9 +54,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 +133,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 +151,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 +172,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
29 changes: 19 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,10 @@ export async function runExperiment(document, options, context) {
console.debug(`running experiment (${window.hlx.experiment.id}) -> ${window.hlx.experiment.selectedVariant}`);

if (experimentConfig.selectedVariant === experimentConfig.variantNames[0]) {
context.sampleRUM('experiment', {
source: experimentConfig.id,
target: experimentConfig.selectedVariant,
});
return false;
}

Expand All @@ -447,14 +451,14 @@ export async function runExperiment(document, options, context) {
}

// Fullpage content experiment
document.body.classList.add(`experiment-${experimentConfig.id}`);
document.body.classList.add(`experiment-${context.toClassName(experimentConfig.id)}`);
const result = await replaceInner(pages[index], document.querySelector('main'));
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 Down Expand Up @@ -573,7 +577,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 @@ -674,11 +678,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 });
}
5 changes: 4 additions & 1 deletion src/preview.css
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
background-color: #222;
border-radius: 16px 16px 0 0;
padding: 24px 16px;
gap: 0 16px;
}

.hlx-popup-header-label {
Expand All @@ -132,6 +133,7 @@
.hlx-popup h5 {
font-family: system-ui, sans-serif;
margin: 0;
color: inherit;
}

.hlx-popup h4 {
Expand Down Expand Up @@ -188,6 +190,7 @@
margin: 16px;
padding: 16px;
border-radius: 16px;
gap: 0 16px;
}

.hlx-popup-item-label {
Expand Down Expand Up @@ -217,5 +220,5 @@
.hlx-preview-overlay {
flex-flow: row-reverse wrap-reverse;
justify-content: flex-start;
}
}
}
83 changes: 71 additions & 12 deletions src/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* governing permissions and limitations under the License.
*/

const DOMAIN_KEY_NAME = 'aem-domainkey';

function createPreviewOverlay(cls) {
const overlay = document.createElement('div');
overlay.className = cls;
Expand All @@ -27,20 +29,30 @@ function createButton(label) {

function createPopupItem(item) {
const actions = typeof item === 'object'
? item.actions.map((action) => `<div class="hlx-button"><a href="${action.href}">${action.label}</a></div>`)
? item.actions.map((action) => (action.href
? `<div class="hlx-button"><a href="${action.href}">${action.label}</a></div>`
: `<div class="hlx-button"><a href="#">${action.label}</a></div>`))
: [];
const div = document.createElement('div');
div.className = `hlx-popup-item${item.isSelected ? ' is-selected' : ''}`;
div.innerHTML = `
<h5 class="hlx-popup-item-label">${typeof item === 'object' ? item.label : item}</h5>
${item.description ? `<div class="hlx-popup-item-description">${item.description}</div>` : ''}
${actions.length ? `<div class="hlx-popup-item-actions">${actions}</div>` : ''}`;
const buttons = [...div.querySelectorAll('.hlx-button a')];
item.actions.forEach((action, index) => {
if (action.onclick) {
buttons[index].addEventListener('click', action.onclick);
}
});
return div;
}

function createPopupDialog(header, items = []) {
const actions = typeof header === 'object'
? (header.actions || []).map((action) => `<div class="hlx-button"><a href="${action.href}">${action.label}</a></div>`)
? (header.actions || []).map((action) => (action.href
? `<div class="hlx-button"><a href="${action.href}">${action.label}</a></div>`
: `<div class="hlx-button"><a href="#">${action.label}</a></div>`))
: [];
const popup = document.createElement('div');
popup.className = 'hlx-popup hlx-hidden';
Expand All @@ -55,6 +67,12 @@ function createPopupDialog(header, items = []) {
items.forEach((item) => {
list.append(createPopupItem(item));
});
const buttons = [...popup.querySelectorAll('.hlx-popup-header-actions .hlx-button a')];
header.actions.forEach((action, index) => {
if (action.onclick) {
buttons[index].addEventListener('click', action.onclick);
}
});
return popup;
}

Expand Down Expand Up @@ -144,21 +162,36 @@ function createVariant(experiment, variantName, config, options) {
}

async function fetchRumData(experiment, options) {
if (!options.domainKey) {
// eslint-disable-next-line no-console
console.warn('Cannot show RUM data. No `domainKey` configured.');
return null;
}
if (!options.prodHost && (typeof options.isProd !== 'function' || !options.isProd())) {
// eslint-disable-next-line no-console
console.warn('Cannot show RUM data. No `prodHost` configured or custom `isProd` method provided.');
return null;
}

// the query is a bit slow, so I'm only fetching the results when the popup is opened
const resultsURL = new URL('https://helix-pages.anywhere.run/helix-services/run-query@v2/rum-experiments');
resultsURL.searchParams.set(options.experimentsQueryParameter, experiment);
if (window.hlx.sidekickConfig && window.hlx.sidekickConfig.host) {
// restrict results to the production host, this also reduces query cost
resultsURL.searchParams.set('domain', window.hlx.sidekickConfig.host);
const resultsURL = new URL('https://helix-pages.anywhere.run/helix-services/run-query@v3/rum-experiments');
// restrict results to the production host, this also reduces query cost
if (typeof options.isProd === 'function' && options.isProd()) {
resultsURL.searchParams.set('url', window.location.host);
} else if (options.prodHost) {
resultsURL.searchParams.set('url', options.prodHost);
}
resultsURL.searchParams.set('domainkey', options.domainKey);
resultsURL.searchParams.set('experiment', experiment);

const response = await fetch(resultsURL.href);
if (!response.ok) {
return null;
}

const { results } = await response.json();
if (!results.length) {
const { data } = results;
if (!data.length) {
return null;
}

Expand All @@ -168,7 +201,7 @@ async function fetchRumData(experiment, options) {
return o;
}, {});

const variantsAsNums = results.map(numberify);
const variantsAsNums = data.map(numberify);
const totals = Object.entries(
variantsAsNums.reduce((o, v) => {
Object.entries(v).forEach(([k, val]) => {
Expand All @@ -185,7 +218,7 @@ async function fetchRumData(experiment, options) {
const vkey = k.replace(/^(variant|control)_/, 'variant_');
const ckey = k.replace(/^(variant|control)_/, 'control_');
const tkey = k.replace(/^(variant|control)_/, 'total_');
if (o[ckey] && o[vkey]) {
if (!Number.isNaN(o[ckey]) && !Number.isNaN(o[vkey])) {
o[tkey] = o[ckey] + o[vkey];
}
return o;
Expand Down Expand Up @@ -282,6 +315,7 @@ async function decorateExperimentPill(overlay, options, context) {
// eslint-disable-next-line no-console
console.log('preview experiment', experiment);

const domainKey = window.localStorage.getItem(DOMAIN_KEY_NAME);
const pill = createPopupButton(
`Experiment: ${config.id}`,
{
Expand All @@ -296,7 +330,32 @@ async function decorateExperimentPill(overlay, options, context) {
${config.variants[config.variantNames[0]].blocks.join(',')}
</div>
<div class="hlx-info">How is it going?</div>`,
actions: config.manifest ? [{ label: 'Manifest', href: config.manifest }] : [],
actions: [
...config.manifest ? [{ label: 'Manifest', href: config.manifest }] : [],
{
label: '<span style="font-size:2em;line-height:1em">⚙</span>',
onclick: async () => {
// eslint-disable-next-line no-alert
const key = window.prompt(
'Please enter your domain key:',
window.localStorage.getItem(DOMAIN_KEY_NAME) || '',
);
if (key && key.match(/[a-f0-9-]+/)) {
window.localStorage.setItem(DOMAIN_KEY_NAME, key);
const performanceMetrics = await fetchRumData(experiment, {
...options,
domainKey: key,
});
if (performanceMetrics === null) {
return;
}
populatePerformanceMetrics(pill, config, performanceMetrics);
} else if (key === '') {
window.localStorage.removeItem(DOMAIN_KEY_NAME);
}
},
},
],
},
config.variantNames.map((vname) => createVariant(experiment, vname, config, options)),
);
Expand All @@ -305,7 +364,7 @@ async function decorateExperimentPill(overlay, options, context) {
}
overlay.append(pill);

const performanceMetrics = await fetchRumData(experiment, options);
const performanceMetrics = await fetchRumData(experiment, { ...options, domainKey });
if (performanceMetrics === null) {
return;
}
Expand Down

0 comments on commit 8faa1fb

Please sign in to comment.