Skip to content

Commit

Permalink
Squashed 'plugins/experimentation/' changes from 638e0db..9db8662
Browse files Browse the repository at this point in the history
9db8662 feat: support customized variant name for fragment variants (#36)
c9ff2c0 fix: only fetch pathname to fix url matching issue (#38)
f7c45aa fix: Support Flexible Parsing (#37)
f29dd80 fix: failing tests following refactoring
9db77eb fix: audience checkpoint name
7dc74e1 feat: prepare data for RUMv2 collection
f6ccbcd fix: update getAllMetadata function (#33)
ae769a2 feat: Supporting Naming Variants in Page Expereimentation (#31)

git-subtree-dir: plugins/experimentation
git-subtree-split: 9db86622117e4327cec042bfc3b16e73a71a4bec
  • Loading branch information
FentPams committed Jul 29, 2024
1 parent efa27f2 commit 6cb5857
Show file tree
Hide file tree
Showing 12 changed files with 83 additions and 25 deletions.
13 changes: 13 additions & 0 deletions documentation/experiments.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ which would essentially disable the control page.

If you need to be really granular, decimal numbers are also supported, like `33.34, 33.33, 33.33`.

#### Custom Variant Labels
This feature allows authors to specify custom labels for both page and section experiment variants via metadata. While the internal variant IDs remain in a predefined format (e.g., challenger-1, challenger-2), the labels in the overlay pill can be customized to provide more meaningful names.

To customize the labels, add a new entry in the page metadata or section metadata(below takes page metadata as an example):

| Metadata | |
|---------------------|--------------------------------------------------------------|
| Experiment | Hero Test |
| Experiment Variants | [https://{ref}--{repo}--{org}.hlx.page/my-page-variant-1](), [https://{ref}--{repo}--{org}.hlx.page/my-page-variant-2](), [https://{ref}--{repo}--{org}.hlx.page/my-page-variant-3]() |
| Experiment Names | foo1, foo2, foo3 |

The names defined will match with the corresponding variants in sequence. If the number of names provided is less than the number of variants, the default naming will be applied for the remaining variants.

#### Code-level experiments

Note that the above assumes you have different content variants to serve, but if you want to run a pure code-based A/B Test, this is also achievable via:
Expand Down
65 changes: 47 additions & 18 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ export function toCamelCase(name) {
return toClassName(name).replace(/-([a-z])/g, (g) => g[1].toUpperCase());
}

/**
* Removes all leading hyphens from a string.
* @param {String} after the string to remove the leading hyphens from, usually is colon
* @returns {String} The string without leading hyphens
*/
export function removeLeadingHyphens(inputString) {
// Remove all leading hyphens which are converted from the space in metadata
return inputString.replace(/^(-+)/, '');
}

/**
* Retrieves the content of metadata tags.
* @param {String} name The metadata name (or property)
Expand All @@ -94,12 +104,18 @@ export function getMetadata(name) {
*/
export function getAllMetadata(scope) {
const value = getMetadata(scope);
return [...document.head.querySelectorAll(`meta[name^="${scope}-"]`)]
.reduce((res, meta) => {
const key = toCamelCase(meta.name.substring(scope.length + 1));
res[key] = meta.getAttribute('content');
return res;
}, value ? { value } : {});
const metaTags = document.head.querySelectorAll(`meta[name^="${scope}"], meta[property^="${scope}:"]`);
return [...metaTags].reduce((res, meta) => {
const key = removeLeadingHyphens(
meta.getAttribute('name')
? meta.getAttribute('name').substring(scope.length)
: meta.getAttribute('property').substring(scope.length + 1),
);

const camelCaseKey = toCamelCase(key);
res[camelCaseKey] = meta.getAttribute('content');
return res;
}, value ? { value } : {});
}

/**
Expand Down Expand Up @@ -325,7 +341,8 @@ function createModificationsHandler(
const url = await getExperienceUrl(ns.config);
let res;
if (url && new URL(url, window.location.origin).pathname !== window.location.pathname) {
res = await replaceInner(url, el);
// eslint-disable-next-line no-await-in-loop
res = await replaceInner(new URL(url, window.location.origin).pathname, el);
} else {
res = url;
}
Expand Down Expand Up @@ -361,7 +378,7 @@ function depluralizeProps(obj, props = []) {
async function getManifestEntriesForCurrentPage(urlString) {
try {
const url = new URL(urlString, window.location.origin);
const response = await fetch(url);
const response = await fetch(url.pathname);
const json = await response.json();
return json.data
.map((entry) => Object.keys(entry).reduce((res, k) => {
Expand Down Expand Up @@ -418,7 +435,7 @@ function watchMutationsAndApplyFragments(
let res;
if (url && new URL(url, window.location.origin).pathname !== window.location.pathname) {
// eslint-disable-next-line no-await-in-loop
res = await replaceInner(url, el, entry.selector);
res = await replaceInner(new URL(url, window.location.origin).pathname, el, entry.selector);
} else {
res = url;
}
Expand Down Expand Up @@ -578,14 +595,19 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) {
label: 'Control',
};

// get the custom labels for the variants names
const labelNames = stringToArray(metadata.name);
pages.forEach((page, i) => {
const vname = `challenger-${i + 1}`;
// label with custom name or default
const customLabel = labelNames.length > i ? labelNames[i] : `Challenger ${i + 1}`;

variantNames.push(vname);
variants[vname] = {
percentageSplit: `${splits[i].toFixed(4)}`,
pages: [page],
blocks: [],
label: `Challenger ${i + 1}`,
label: customLabel,
};
});
inferEmptyPercentageSplits(Object.values(variants));
Expand All @@ -600,7 +622,7 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) {

const config = {
id,
label: metadata.name || `Experiment ${metadata.value || metadata.experiment}`,
label: `Experiment ${metadata.value || metadata.experiment}`,
status: metadata.status || 'active',
audiences,
endDate,
Expand Down Expand Up @@ -643,14 +665,15 @@ async function getExperimentConfig(pluginOptions, metadata, overrides) {

return config;
}

/**
* Parses the campaign manifest.
*/
function parseExperimentManifest(entries) {
return Object.values(Object.groupBy(
entries.map((e) => depluralizeProps(e, ['experiment', 'variant', 'split'])),
entries.map((e) => depluralizeProps(e, ['experiment', 'variant', 'split', 'name'])),
({ experiment }) => experiment,
)).map(aggregateEntries('experiment', ['split', 'url', 'variant']));
)).map(aggregateEntries('experiment', ['split', 'url', 'variant', 'name']));
}

function getUrlFromExperimentConfig(config) {
Expand All @@ -670,6 +693,8 @@ async function runExperiment(document, pluginOptions) {
(el, config, result) => {
const { id, selectedVariant, variantNames } = config;
const variant = result ? selectedVariant : variantNames[0];
el.dataset.experiment = id;
el.dataset.variant = variant;
el.classList.add(`experiment-${toClassName(id)}`);
el.classList.add(`variant-${toClassName(variant)}`);
window.hlx?.rum?.sampleRUM('experiment', {
Expand Down Expand Up @@ -771,10 +796,12 @@ async function runCampaign(document, pluginOptions) {
(el, config, result) => {
const { selectedCampaign = 'default' } = config;
const campaign = result ? toClassName(selectedCampaign) : 'default';
el.dataset.audience = selectedCampaign;
el.dataset.audiences = Object.keys(pluginOptions.audiences).join(',');
el.classList.add(`campaign-${campaign}`);
window.hlx?.rum?.sampleRUM('campaign', {
source: el.className,
target: campaign,
window.hlx?.rum?.sampleRUM('audience', {
source: campaign,
target: Object.keys(pluginOptions.audiences).join(':'),
});
document.dispatchEvent(new CustomEvent('aem:experimentation', {
detail: {
Expand Down Expand Up @@ -841,6 +868,7 @@ function getUrlFromAudienceConfig(config) {
}

async function serveAudience(document, pluginOptions) {
document.body.dataset.audiences = Object.keys(pluginOptions.audiences).join(',');
return applyAllModifications(
pluginOptions.audiencesMetaTagPrefix,
pluginOptions.audiencesQueryParameter,
Expand All @@ -851,10 +879,11 @@ async function serveAudience(document, pluginOptions) {
(el, config, result) => {
const { selectedAudience = 'default' } = config;
const audience = result ? toClassName(selectedAudience) : 'default';
el.dataset.audience = audience;
el.classList.add(`audience-${audience}`);
window.hlx?.rum?.sampleRUM('audience', {
source: el.className,
target: audience,
source: audience,
target: Object.keys(pluginOptions.audiences).join(':'),
});
document.dispatchEvent(new CustomEvent('aem:experimentation', {
detail: {
Expand Down
4 changes: 2 additions & 2 deletions src/preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,9 @@ function createVariant(experiment, variantName, config, options) {
experimentURL.searchParams.set(options.experimentsQueryParameter, `${experiment}/${variantName}`);

return {
label: `<code>${variantName}</code>`,
label: `<code>${variant.label}</code>`,
description: `
<p>${variant.label}</p>
<p>${variantName}</p>
<p class="percentage">(${percentage} split)</p>
<p class="performance"></p>`,
actions: [{ label: 'Simulate', href: experimentURL.href }],
Expand Down
4 changes: 2 additions & 2 deletions tests/audiences.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ test.describe('Page-level audiences', () => {
expect(await page.evaluate(() => window.rumCalls)).toContainEqual([
'audience',
expect.objectContaining({
source: 'audience-foo',
target: 'foo',
source: 'foo',
target: 'foo:bar',
}),
]);
});
Expand Down
6 changes: 3 additions & 3 deletions tests/campaigns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ test.describe('Page-level campaigns', () => {
});
await goToAndRunCampaign(page, '/tests/fixtures/campaigns/page-level?campaign=foo');
expect(await page.evaluate(() => window.rumCalls)).toContainEqual([
'campaign',
'audience',
expect.objectContaining({
source: 'campaign-foo',
target: 'foo',
source: 'foo',
target: 'foo:bar',
}),
]);
});
Expand Down
1 change: 1 addition & 0 deletions tests/experiments.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ test.describe('Fragment-level experiments', () => {
'challenger-1': expect.objectContaining({ percentageSplit: '0.3333' }),
'challenger-2': expect.objectContaining({ percentageSplit: '0.3333' }),
},
label: expect.stringMatching(/Experiment Baz/),
}),
servedExperience: expect.stringMatching(/\/tests\/fixtures\/experiments\/(fragment|section)-level/),
}),
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/audiences/page-level.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<head>
<meta name="audience-foo" content="/tests/fixtures/audiences/variant-1"/>
<meta name="audience-bar" content="/tests/fixtures/audiences/variant-2"/>
<meta property="audience:-foo" content="/tests/fixtures/audiences/variant-1"/>
<script>
window.AUDIENCES = {
foo: () => true,
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/campaigns/page-level.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
<meta name="campaign-foo" content="/tests/fixtures/campaigns/variant-1"/>
<meta name="campaign-bar" content="/tests/fixtures/campaigns/variant-2"/>
<script type="module" src="/tests/fixtures/scripts.js"></script>
<script>
window.AUDIENCES = {
foo: () => true,
bar: () => true,
}
</script>
</head>
<body>
<main>
Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/experiments/fragments--alt.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
"Pages": "/tests/fixtures/experiments/fragment-level--alt",
"Experiments": "Baz",
"Variants": "challenger-1",
"Names": "C1",
"Selectors": ".fragment",
"Urls": "/tests/fixtures/experiments/section-level-v1"
},
{
"Pages": "/tests/fixtures/experiments/fragment-level--alt",
"Experiments": "Baz",
"Variants": "challenger-2",
"Names": "C2",
"Selectors": ".fragment",
"Urls": "/tests/fixtures/experiments/section-level-v2"
}
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/experiments/fragments.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,31 @@
"Page": "/tests/fixtures/experiments/fragment-level",
"Experiment": "Baz",
"Variant": "challenger-1",
"Names": "C1",
"Selector": ".fragment",
"Url": "/tests/fixtures/experiments/section-level-v1"
},
{
"Page": "/tests/fixtures/experiments/fragment-level",
"Experiment": "Baz",
"Variant": "challenger-2",
"Names": "C2",
"Selector": ".fragment",
"Url": "/tests/fixtures/experiments/section-level-v2"
},
{
"Page": "/tests/fixtures/experiments/fragment-level--async",
"Experiment": "Baz",
"Variant": "challenger-1",
"Names": "C1",
"Selector": ".fragment",
"Url": "/tests/fixtures/experiments/section-level-v1"
},
{
"Page": "/tests/fixtures/experiments/fragment-level--async",
"Experiment": "Baz",
"Variant": "challenger-2",
"Names": "C2",
"Selector": ".fragment",
"Url": "/tests/fixtures/experiments/section-level-v2"
}
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/experiments/page-level.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<head>
<meta name="experiment" content="foo"/>
<meta name="experiment-variants" content="/tests/fixtures/experiments/page-level-v1,/tests/fixtures/experiments/page-level-v2"/>
<meta name="experiment-name" content="V1,V2"/>
<script type="module" src="/tests/fixtures/scripts.js"></script>
</head>
<body>
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/experiments/section-level.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<a href="/tests/fixtures/experiments/section-level-v2">/tests/fixtures/experiments/section-level-v2</a>
</div>
</div>
<div><div>Experiment Names</div><div>V1,V2</div></div>
</div>
</div>
</main>
Expand Down

0 comments on commit 6cb5857

Please sign in to comment.