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

Experimentation v2 integration #77

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3e246c3
remove v1 engine
Jun 10, 2024
5b251ff
Squashed 'plugins/experimentation/' content from commit 33c3263
Jun 10, 2024
bf1c872
Merge commit '5b251ff67e80d6a39dd1cb7e819ca25fb09052aa' as 'plugins/e…
Jun 10, 2024
2b6a7d2
Squashed 'plugins/experimentation/' changes from 33c3263..09f8a72
Jun 11, 2024
4e6769d
re-squash the latest v2 with fixed local overlay display issue
FentPams Jun 12, 2024
b04889d
fix the conditon of adding experiment plugin
FentPams Jun 17, 2024
e148709
Merge commit 'efa27f2bc90a0a346b9dadc1b4e7ec6e1e90eb19' into experime…
FentPams Jun 21, 2024
efa27f2
Squashed 'plugins/experimentation/' changes from 09f8a72..638e0db
FentPams Jun 21, 2024
30f172d
fix fragment experiment issue on auto-blocking block and regular blocks
FentPams Jun 21, 2024
cdecca4
fix: fetch campigns and audiences in metadata property for page-level…
FentPams Jun 23, 2024
d284d3d
fix: switch to default page in campaigns has issue on decoration, and…
FentPams Jun 24, 2024
5739b00
fix lint
FentPams Jun 24, 2024
2d84651
Revert package.json and package-lock.json to main branch state
FentPams Jun 26, 2024
c2843aa
Revert package.json and package-lock.json to match main branch
FentPams Jun 26, 2024
d9e9bc7
support naming feature for page variant
FentPams Jul 21, 2024
3624f6c
add naming support for fragment experiments
FentPams Jul 21, 2024
80535f3
Merge pull request #79 from hlxsites/naming-tst
FentPams Jul 22, 2024
4f24236
fix: remove leading hypens in metadata and support customized variant…
FentPams Jul 23, 2024
ebafebc
Merge pull request #80 from hlxsites/naming-tst
FentPams Jul 23, 2024
6cb5857
Squashed 'plugins/experimentation/' changes from 638e0db..9db8662
FentPams Jul 29, 2024
d16a812
Merge commit '6cb58577ef86d974b603c2e6b0834f026bd60cd2' into experime…
FentPams Jul 29, 2024
272bfc8
fix fetching labelNames
FentPams Jul 29, 2024
bb1bcc4
update decorateFunction in experimentation plugin
FentPams Aug 4, 2024
6d370f4
Squashed 'plugins/experimentation/' changes from 9db8662..942948d
FentPams Aug 7, 2024
0efa8a4
Merge commit '6d370f4e342db8abd5cd8dd49f0d119aa9d6e772' into experime…
FentPams Aug 7, 2024
fbbe751
align with updated v2 engine
FentPams Aug 7, 2024
050133e
update readme about experimentation
FentPams Aug 8, 2024
430ef17
introduce url redirection for page level experiment
FentPams Aug 13, 2024
5bc6727
add comment and sanitize resoluction property and return after replac…
FentPams Aug 13, 2024
97e67b7
Squashed 'plugins/experimentation/' changes from 942948d..d4a5c00
FentPams Aug 20, 2024
7553799
Merge commit '97e67b7207ab00e95c47651061c2cceac4d007d9' into experime…
FentPams Aug 20, 2024
822b48b
solve merge conflict
FentPams Aug 20, 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
3 changes: 2 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@
"@web/test-runner": "0.14.0",
FentPams marked this conversation as resolved.
Show resolved Hide resolved
"@web/test-runner-commands": "0.6.4",
"chai": "4.3.6",
"eslint": "8.22.0",
"eslint": "^8.22.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-plugin-import": "2.26.0",
"puppeteer": "^21.2.1",
"sinon": "14.0.0",
"stylelint": "14.10.0",
"stylelint-config-prettier": "9.0.3",
"stylelint-config-standard": "25.0.0",
"puppeteer": "^21.2.1"
"stylelint-config-standard": "25.0.0"
}
}
1 change: 0 additions & 1 deletion plugins/experimentation/.eslintignore

This file was deleted.

36 changes: 36 additions & 0 deletions plugins/experimentation/.github/workflows/release.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Release
on:
push:
branches:
- main

permissions:
contents: read

jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "lts/*"
- name: Install dependencies
run: npm ci
- name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
run: npm audit signatures
- name: Release
env:
# Token taken from: https://app.codecov.io/github/adobe/aem-experimentation/settings
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
28 changes: 28 additions & 0 deletions plugins/experimentation/.github/workflows/run-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Linting

on: [pull_request]

jobs:
build:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.44.1-jammy
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
- run: npm ci
- run: npm run lint
env:
CI: true
- run: npm run test
env:
CI: true
- name: Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: adobe/aem-experimentation
files: ./coverage/codecov.json
3 changes: 3 additions & 0 deletions plugins/experimentation/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
coverage/
playwright-report/
test-results/
13 changes: 13 additions & 0 deletions plugins/experimentation/.releaserc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}]
]
}
6 changes: 6 additions & 0 deletions plugins/experimentation/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## [1.0.1](https://github.com/adobe/aem-experimentation/compare/v1.0.0...v1.0.1) (2024-05-24)


### Bug Fixes

* semantic release ([5040fa8](https://github.com/adobe/aem-experimentation/commit/5040fa88c7a01b032431967e230abaaf6d69f9d6))
213 changes: 116 additions & 97 deletions plugins/experimentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,136 +26,128 @@ git subtree pull --squash --prefix plugins/experimentation git@github.com:adobe/

If you prefer using `https` links you'd replace `git@github.com: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 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
const AUDIENCES = {
mobile: () => window.innerWidth < 600,
desktop: () => window.innerWidth >= 600,
// define your custom audiences here as needed
};

window.hlx.plugins.add('experimentation', {
condition: () => getMetadata('experiment')
|| Object.keys(getAllMetadata('campaign')).length
|| Object.keys(getAllMetadata('audience')).length,
options: { audiences: AUDIENCES },
url: '/plugins/experimentation/src/index.js',
});
```

### On top of a regular boilerplate project

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:
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. at the start of the file:
```js
const AUDIENCES = {
mobile: () => window.innerWidth < 600,
desktop: () => window.innerWidth >= 600,
// define your custom audiences here as needed
const experimentationConfig = {
prodHost: 'www.my-site.com',
audiences: {
mobile: () => window.innerWidth < 600,
desktop: () => window.innerWidth >= 600,
// define your custom audiences here as needed
}
};

/**
* Gets all the metadata elements that are in the given scope.
* @param {String} scope The scope/prefix for the metadata
* @returns an array of HTMLElement nodes that match the given scope
*/
export function getAllMetadata(scope) {
return [...document.head.querySelectorAll(`meta[property^="${scope}:"],meta[name^="${scope}-"]`)]
.reduce((res, meta) => {
const id = toClassName(meta.name
? meta.name.substring(scope.length + 1)
: meta.getAttribute('property').split(':')[1]);
res[id] = meta.getAttribute('content');
return res;
}, {});
let runExperimentation;
let showExperimentationOverlay;
const isExperimentationEnabled = document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"]')
|| [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i));
if (isExperimentationEnabled) {
const {
loadEager: runExperimentation,
loadLazy: showExperimentationOverlay,
} = await import('../plugins/experimentation/src/index.js');
}
```
2. if this is the first plugin you add to your project, you'll also need to add:
```js
// Define an execution context
const pluginContext = {
getAllMetadata,
getMetadata,
loadCSS,
loadScript,
sampleRUM,
toCamelCase,
toClassName,
};
```
3. Early in the `loadEager` method you'll need to add:
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 (getMetadata('experiment')
|| Object.keys(getAllMetadata('campaign')).length
|| Object.keys(getAllMetadata('audience')).length) {
// eslint-disable-next-line import/no-relative-packages
const { loadEager: runEager } = await import('../plugins/experimentation/src/index.js');
await runEager(document, { audiences: AUDIENCES }, pluginContext);
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.
4. Finally at the end of the `loadLazy` method you'll have to add:
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 ((getMetadata('experiment')
|| Object.keys(getAllMetadata('campaign')).length
|| Object.keys(getAllMetadata('audience')).length)) {
// eslint-disable-next-line import/no-relative-packages
const { loadLazy: runLazy } = await import('../plugins/experimentation/src/index.js');
await runLazy(document, { audiences: AUDIENCES }, pluginContext);
if (showExperimentationOverlay) {
await showExperimentationOverlay(document, experimentationConfig);
}
}
```
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.

### 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 either `window.aem.plugins` or `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 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
const experimentationConfig = {
prodHost: 'www.my-site.com',
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',
});
```

### Increasing sampling rate for low traffic pages

When running experiments during short periods (i.e. a few days or 2 weeks) or on low-traffic pages (<100K page views a month), it is unlikely that you'll reach statistical significance on your tests with the default RUM sampling. For those use cases, we recommend adjusting the sampling rate for the pages in question to 1 out of 10 instead of the default 1 out of 100 visits.

Edit your html `<head>` and set configure the RUM sampling like:
```html
<meta name="experiment" content="...">
...
<!-- insert this script tag before loading aem.js or lib-franklin.js -->
<script>
window.RUM_SAMPLING_RATE = document.head.querySelector('[name^="experiment"],[name^="campaign-"],[name^="audience-"]')
|| [...document.querySelectorAll('.section-metadata div')].some((d) => d.textContent.match(/Experiment|Campaign|Audience/i))
? 10
: 100;
</script>
<script type="module" src="/scripts/aem.js"></script>
<script type="module" src="/scripts/scripts.js"></script>
```

Then double-check your `aem.js` file around line 20 and look for:
```js
const weight = new URLSearchParams(window.location.search).get('rum') === 'on' ? 1 : defaultSamplingRate;
```

If this is not present, please apply the following changes to the file: https://github.com/adobe/helix-rum-js/pull/159/files#diff-bfe9874d239014961b1ae4e89875a6155667db834a410aaaa2ebe3cf89820556

### 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`).
You have already seen the `audiences` option in the examples above, but here is the full list we support:

```js
runEager.call(document, {
// Overrides the base path if the plugin was installed in a sub-directory
basePath: '',

// 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'),

/* Generic properties */
// RUM sampling rate on regular AEM pages is 1 out of 100 page views
// but we increase this by default for audiences, campaigns and experiments
// to 1 out of 10 page views so we can collect metrics faster of the relative
// short durations of those campaigns/experiments
rumSamplingRate: 10,
isProd: () => !window.location.hostname.endsWith('hlx.page')
&& window.location.hostname !== ('localhost'),

// the storage type used to persist data between page views
// (for instance to remember what variant in an experiment the user was served)
Expand All @@ -174,14 +166,41 @@ runEager.call(document, {

/* Experimentation related properties */
// See more details on the dedicated Experiments page linked below
experimentsRoot: '/experiments',
experimentsConfigFile: 'manifest.json',
experimentsMetaTag: 'experiment',
experimentsMetaTagPrefix: 'experiment',
experimentsQueryParameter: 'experiment',
}, pluginContext);
});
```

For detailed implementation instructions on the different features, please read the dedicated pages we have on those topics:
- [Audiences](https://github.com/adobe/aem-experimentation/wiki/Audiences)
- [Campaigns](https://github.com/adobe/aem-experimentation/wiki/Campaigns)
- [Experiments](https://github.com/adobe/aem-experimentation/wiki/Experiments)
- [Audiences](/documentation/audiences.md)
- [Campaigns](/documentation/campaigns.md)
- [Experiments](/documentation/experiments.md)

## Extensibility & integrations

If you need to further integrate the experimentation plugin with custom analytics reporting or other 3rd-party libraries, you can listen for the `aem:experimentation` event:
```js
document.addEventListener('aem:experimentation', (ev) => console.log(ev.detail));
```

The event details will contain one of 3 possible sets of properties:
- For experiments:
- `type`: `experiment`
- `element`: the DOM element that was modified
- `experiment`: the experiment name
- `variant`: the variant name that was served
- For audiences:
- `type`: `audience`
- `element`: the DOM element that was modified
- `audience`: the audience that was resolved
- For campaigns:
- `type`: `campaign`
- `element`: the DOM element that was modified
- `campaign`: the campaign that was resolved

Additionally, you can leverage the following global JS objects `window.hlx.experiments`, `window.hlx.audiences` and `window.hlx.campaigns`.
Those will each be an array of objects containing:
- `type`: one of `page`, `section` or `fragment`
- `el`: the DOM element that was modified
- `servedExperience`: the URL for the content that was inlined for that experience
- `config`: an object containing the config details
Loading
Loading