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

feat: Support page level full redirection #43

Merged
merged 6 commits into from
Aug 14, 2024
Merged
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
10 changes: 10 additions & 0 deletions documentation/audiences.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ The audiences are set up directly in the page metadata block as follows:

The notation is pretty flexible and authors can also use `Audience (Mobile)` or `Audience Mobile` if this is a preferred notation.

#### Page redirect

If you aim to direct your audience to a target URL instead of just replacing the content, you can do so by adding the `Audience Resolution | redirect` property to the page metadata:

| Metadata | |
|---------------------|---------------------------------------------------------------|
| Audience: Mobile | [https://{ref}--{repo}--{org}.hlx.page/my-page-for-mobile]() |
| Audience: Desktop | [https://{ref}--{repo}--{org}.hlx.page/my-page-for-desktop]() |
| Audience Resolution | redirect |

### Section-level audiences

Each section in a page can also run any number of audiences. Section-level audiences are run after the page-level audiences have run, i.e. after the variants have been processed and their markup was pulled into the main page, so the section-level audiences that will run are dictated by the document from the current page-level experiment/audience/campaign, and not necessarily just the main page.
Expand Down
10 changes: 10 additions & 0 deletions documentation/campaigns.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ If you wanted to additionally restrict the campaign to specific audiences, so th
If any of the listed audiences is resolved, then the campaign will run and the matching content will be served.
If you needed both audiences to be resolved, you'd define a new `mobile-iphone` audience in your project and use that in the metadata instead.

#### Page Redirect

If you aim to fully direct a campaign page to a target URL instead of just replacing the content, you can do so by adding the `Campaign Resolution | redirect` property to the page metadata:

| Metadata | |
|----------------------|-----------------------------------------------------------------|
| Campaign: Xmas | [https://{ref}--{repo}--{org}.hlx.page/my-page-for-xmas]() |
| Campaign: Halloween | [https://{ref}--{repo}--{org}.hlx.page/my-page-for-halloween]() |
| Campaign Resolution | redirect |

### Section-level audiences

Each section in a page can also run any number of campaigns. Section-level campaigns are run after the page-level campaigns have run, i.e. after the variants have been processed and their markup was pulled into the main page, so the section-level campaigns that will run are dictated by the document from the current page-level experiment/audience/campaign, and not necessarily just the main page.
Expand Down
17 changes: 17 additions & 0 deletions documentation/experiments.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ Start and end dates are in the flexible JS [Date Time String Format](https://tc3

So you can both use generic dates, like `2024-01-31` or `2024/01/31`, and time-specific dates like `2024-01-31T13:37` or `2024/01/31 1:37 pm`. You can even enforce a specific timezone so your experiment activates when, say, it's 2am GMT+1 by using `2024/1/31 2:00 pm GMT+1` or similar notations.

#### Redirect page experiments
For the use case that fully redirect to the target URL instead of just replacing the content (our default behavior), you could add a new property `Experiment Resolution | redirect` in page metadata:
| 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 Resolution | redirect
FentPams marked this conversation as resolved.
Show resolved Hide resolved

In this example, the Hero Test experiment will redirect to one of the specified URLs when you simulate that variant.

Similarly, the redirects for audience personalization and campaign personalization could be enabled by adding:

`Audience Resolution | redirect`
`Campaign Resolution | redirect`

For more details, refer to: [audiences](./audiences.md#page-redirect), [campaigns](./campaigns.md#page-redirect)

### Section-level experiments

Each section in a page can also run 1 experiment, so you can have as many section-level experiments as you have sections.
Expand Down
58 changes: 46 additions & 12 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@
: '';
}

/**
* Fires a Real User Monitoring (RUM) event based on the provided type and configuration.
* @param {string} type - the type of event to be fired ("experiment", "campaign", or "audience")
* @param {Object} config - contains details about the experience
* @param {Object} pluginOptions - default plugin options with custom options
* @param {string} result - the URL of the served experience.
*/
function fireRUM(type, config, pluginOptions, result) {
const { selectedCampaign = 'default', selectedAudience = 'default' } = config;

const typeHandlers = {
experiment: () => ({
source: config.id,
target: result ? config.selectedVariant : config.variantNames[0],
}),
campaign: () => ({
source: result ? toClassName(selectedCampaign) : 'default',
target: Object.keys(pluginOptions.audiences).join(':'),
}),
audience: () => ({
source: result ? toClassName(selectedAudience) : 'default',
target: Object.keys(pluginOptions.audiences).join(':'),
}),
};

const { source, target } = typeHandlers[type]();
const rumType = type === 'experiment' ? 'experiment' : 'audience';
window.hlx?.rum?.sampleRUM(rumType, { source, target });
}

/**
* Sanitizes a name for use as a js property name.
* @param {String} name The unsanitized name
Expand Down Expand Up @@ -321,6 +351,7 @@
* Creates an instance of a modification handler that will be responsible for applying the desired
* personalized experience.
*
* @param {String} type The type of modifications to apply
* @param {Object} overrides The config overrides
* @param {Function} metadataToConfig a function that will handle the parsing of the metadata
* @param {Function} getExperienceUrl a function that returns the URL to the experience
Expand All @@ -329,6 +360,7 @@
* @returns the modification handler
*/
function createModificationsHandler(
type,
overrides,
metadataToConfig,
getExperienceUrl,
Expand All @@ -344,6 +376,13 @@
const url = await getExperienceUrl(ns.config);
let res;
if (url && new URL(url, window.location.origin).pathname !== window.location.pathname) {
if (toClassName(metadata?.resolution) === 'redirect') {
// Firing RUM event early since redirection will stop the rest of the JS execution
fireRUM(type, config, pluginOptions, url);
ramboz marked this conversation as resolved.
Show resolved Hide resolved
window.location.replace(url);

Check warning on line 382 in src/index.js

View check run for this annotation

Codecov / codecov/patch

src/index.js#L381-L382

Added lines #L381 - L382 were not covered by tests
// eslint-disable-next-line consistent-return
return;
}

Check warning on line 385 in src/index.js

View check run for this annotation

Codecov / codecov/patch

src/index.js#L384-L385

Added lines #L384 - L385 were not covered by tests
// eslint-disable-next-line no-await-in-loop
res = await replaceInner(new URL(url, window.location.origin).pathname, el);
} else {
Expand Down Expand Up @@ -479,6 +518,7 @@
cb,
) {
const modificationsHandler = createModificationsHandler(
type,
getAllQueryParameters(paramNS),
metadataToConfig,
getExperienceUrl,
Expand Down Expand Up @@ -699,16 +739,14 @@
parseExperimentManifest,
getUrlFromExperimentConfig,
(el, config, result) => {
fireRUM('experiment', config, pluginOptions, result);
// dispatch event
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', {
source: id,
target: variant,
});
document.dispatchEvent(new CustomEvent('aem:experimentation', {
detail: {
element: el,
Expand Down Expand Up @@ -802,15 +840,13 @@
parseCampaignManifest,
getUrlFromCampaignConfig,
(el, config, result) => {
fireRUM('campaign', config, pluginOptions, result);
// dispatch event
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('audience', {
source: campaign,
target: Object.keys(pluginOptions.audiences).join(':'),
});
document.dispatchEvent(new CustomEvent('aem:experimentation', {
detail: {
element: el,
Expand Down Expand Up @@ -885,14 +921,12 @@
parseAudienceManifest,
getUrlFromAudienceConfig,
(el, config, result) => {
fireRUM('audience', config, pluginOptions, result);
// dispatch event
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: audience,
target: Object.keys(pluginOptions.audiences).join(':'),
});
document.dispatchEvent(new CustomEvent('aem:experimentation', {
detail: {
element: el,
Expand Down
Loading