From 59aea1eb07adc9f9052e8ddf68831ca2e0d17adb Mon Sep 17 00:00:00 2001 From: Julien Ramboz Date: Fri, 27 Oct 2023 16:08:50 +0200 Subject: [PATCH] Squashed 'plugins/experience-decisioning/' changes from f602282..be2e07d be2e07d doc: update README.md 287e35f feat: adopt the plugin api (#2) 9028520 fix: possible css leaking into pill overlay 162aab2 fix: improve anonymization for better gdpr/hippa compliance (#3) 0071dbd fix: audience pill activation a486023 fix: audience parsing 4789afd feat: limit the sampling rate 7fbef36 feat: limit the sampling rate (#1) 92e2abb fix: campaigns parsing 4467d3e fix: block-level experiments resolution 4438f52 doc: update readme 44a3aa4 fix: support installation in sub-directories a77433f style: properly inherit text color in the overlay git-subtree-dir: plugins/experience-decisioning git-subtree-split: be2e07ddce1d9c8d1622e6221f7a16593d87b811 --- README.md | 41 +++++++++++-- src/index.js | 150 +++++++++++++++++++++++++++--------------------- src/preview.css | 3 + src/preview.js | 55 +++++++++--------- src/ued.js | 12 +++- 5 files changed, 163 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 3d5941da..b2094234 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,33 @@ If you prefer using `https` links you'd replace `git@github.com:adobe/aem-experi ## Project instrumentation +### 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`. + +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('experience-decisioning', { + condition: () => getMetadata('experiment') + || Object.keys(getAllMetadata('campaign')).length + || Object.keys(getAllMetadata('audience')).length, + options: { audiences: AUDIENCES }, + load: 'eager', + url: '/plugins/experience-decisioning/src/index.js', +}); +``` + +### Without the plugin system + 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: @@ -77,7 +104,7 @@ To properly connect and configure the plugin for your project, you'll need to ed || Object.keys(getAllMetadata('audience')).length) { // eslint-disable-next-line import/no-relative-packages const { loadEager: runEager } = await import('../plugins/experience-decisioning/src/index.js'); - await runEager.call(pluginContext, { audiences: AUDIENCES }); + await runEager(document, { audiences: AUDIENCES }, pluginContext); } … } @@ -90,11 +117,10 @@ To properly connect and configure the plugin for your project, you'll need to ed // Add below snippet at the end of the lazy phase if ((getMetadata('experiment') || Object.keys(getAllMetadata('campaign')).length - || Object.keys(getAllMetadata('audience')).length) - && (window.location.hostname.endsWith('hlx.page') || window.location.hostname === ('localhost'))) { + || Object.keys(getAllMetadata('audience')).length)) { // eslint-disable-next-line import/no-relative-packages const { loadLazy: runLazy } = await import('../plugins/experience-decisioning/src/index.js'); - await runLazy.call(pluginContext, { audiences: AUDIENCES }); + await runLazy(document, { audiences: AUDIENCES }, pluginContext); } } ``` @@ -107,6 +133,13 @@ You have already seen the `audiences` option in the examples above, but here is ```js runEager.call(pluginContext, { + // 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 + // (prod environments do not get the pill overlay) + 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 diff --git a/src/index.js b/src/index.js index 3230c481..22142b79 100644 --- a/src/index.js +++ b/src/index.js @@ -9,9 +9,11 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ +const MAX_SAMPLING_RATE = 10; // At a maximum we sample 1 in 10 requests + export const DEFAULT_OPTIONS = { // Generic properties - rumSamplingRate: 10, // 1 in 10 requests + rumSamplingRate: MAX_SAMPLING_RATE, // 1 in 10 requests // Audiences related properties audiences: {}, @@ -43,7 +45,7 @@ function isBot() { * @param {object} options the plugin options * @returns Returns the names of the resolved audiences, or `null` if no audience is configured */ -export async function getResolvedAudiences(applicableAudiences, options) { +export async function getResolvedAudiences(applicableAudiences, options, context) { if (!applicableAudiences.length || !Object.keys(options.audiences).length) { return null; } @@ -51,7 +53,7 @@ export async function getResolvedAudiences(applicableAudiences, options) { // we check if it is applicable const usp = new URLSearchParams(window.location.search); const forcedAudience = usp.has(options.audiencesQueryParameter) - ? this.toClassName(usp.get(options.audiencesQueryParameter)) + ? context.toClassName(usp.get(options.audiencesQueryParameter)) : null; if (forcedAudience) { return applicableAudiences.includes(forcedAudience) ? [forcedAudience] : []; @@ -84,7 +86,7 @@ async function replaceInner(path, element) { const resp = await fetch(plainPath); if (!resp.ok) { // eslint-disable-next-line no-console - console.log('error loading experiment content:', resp); + console.log('error loading content:', resp); return false; } const html = await resp.text(); @@ -93,7 +95,7 @@ async function replaceInner(path, element) { return true; } catch (e) { // eslint-disable-next-line no-console - console.log(`error loading experiment content: ${plainPath}`, e); + console.log(`error loading content: ${plainPath}`, e); } return false; } @@ -119,11 +121,11 @@ async function replaceInner(path, element) { * } * }; */ -function parseExperimentConfig(json) { +function parseExperimentConfig(json, context) { const config = {}; try { json.settings.data.forEach((line) => { - const key = this.toCamelCase(line.Name); + const key = context.toCamelCase(line.Name); if (key === 'audience' || key === 'audiences') { config.audiences = line.Value ? line.Value.split(',').map((str) => str.trim()) : []; } else if (key === 'experimentName') { @@ -135,19 +137,19 @@ function parseExperimentConfig(json) { const variants = {}; let variantNames = Object.keys(json.experiences.data[0]); variantNames.shift(); - variantNames = variantNames.map((vn) => this.toCamelCase(vn)); + variantNames = variantNames.map((vn) => context.toCamelCase(vn)); variantNames.forEach((variantName) => { variants[variantName] = {}; }); let lastKey = 'default'; json.experiences.data.forEach((line) => { - let key = this.toCamelCase(line.Name); + let key = context.toCamelCase(line.Name); if (!key) key = lastKey; lastKey = key; const vns = Object.keys(line); vns.shift(); vns.forEach((vn) => { - const camelVN = this.toCamelCase(vn); + const camelVN = context.toCamelCase(vn); if (key === 'pages' || key === 'blocks') { variants[camelVN][key] = variants[camelVN][key] || []; if (key === 'pages') variants[camelVN][key].push(new URL(line[vn]).pathname); @@ -221,11 +223,16 @@ function inferEmptyPercentageSplits(variants) { * @param {string} instantExperiment The list of varaints * @returns {object} the experiment manifest */ -export function getConfigForInstantExperiment(experimentId, instantExperiment, pluginOptions) { - const audience = this.getMetadata(`${pluginOptions.experimentsMetaTag}-audience`); +export function getConfigForInstantExperiment( + experimentId, + instantExperiment, + pluginOptions, + context, +) { + const audience = context.getMetadata(`${pluginOptions.experimentsMetaTag}-audience`); const config = { label: `Instant Experiment: ${experimentId}`, - audiences: audience ? audience.split(',').map(this.toClassName) : [], + audiences: audience ? audience.split(',').map(context.toClassName) : [], status: 'Active', id: experimentId, variants: {}, @@ -234,7 +241,7 @@ export function getConfigForInstantExperiment(experimentId, instantExperiment, p const pages = instantExperiment.split(',').map((p) => new URL(p.trim()).pathname); - const splitString = this.getMetadata(`${pluginOptions.experimentsMetaTag}-split`); + const splitString = context.getMetadata(`${pluginOptions.experimentsMetaTag}-split`); const splits = splitString // custom split ? splitString.split(',').map((i) => parseInt(i, 10) / 100) @@ -280,7 +287,7 @@ export function getConfigForInstantExperiment(experimentId, instantExperiment, p * @param {object} pluginOptions The plugin options * @returns {object} containing the experiment manifest */ -export async function getConfigForFullExperiment(experimentId, pluginOptions) { +export async function getConfigForFullExperiment(experimentId, pluginOptions, context) { const path = `${pluginOptions.experimentsRoot}/${experimentId}/${pluginOptions.experimentsConfigFile}`; try { const resp = await fetch(path); @@ -291,8 +298,8 @@ export async function getConfigForFullExperiment(experimentId, pluginOptions) { } const json = await resp.json(); const config = pluginOptions.parser - ? pluginOptions.parser.call(this, json) - : parseExperimentConfig.call(this, json); + ? pluginOptions.parser(json, context) + : parseExperimentConfig(json, context); if (!config) { return null; } @@ -329,15 +336,15 @@ function getDecisionPolicy(config) { return decisionPolicy; } -export async function getConfig(experiment, instantExperiment, pluginOptions) { +export 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('/') : []; const experimentConfig = instantExperiment - ? await getConfigForInstantExperiment.call(this, experiment, instantExperiment, pluginOptions) - : await getConfigForFullExperiment.call(this, experiment, pluginOptions); + ? await getConfigForInstantExperiment(experiment, instantExperiment, pluginOptions, context) + : await getConfigForFullExperiment(experiment, pluginOptions, context); // eslint-disable-next-line no-console console.debug(experimentConfig); @@ -346,16 +353,17 @@ export async function getConfig(experiment, instantExperiment, pluginOptions) { } const forcedAudience = usp.has(pluginOptions.audiencesQueryParameter) - ? this.toClassName(usp.get(pluginOptions.audiencesQueryParameter)) + ? context.toClassName(usp.get(pluginOptions.audiencesQueryParameter)) : null; experimentConfig.resolvedAudiences = await getResolvedAudiences( experimentConfig.audiences, pluginOptions, + context, ); experimentConfig.run = ( // experiment is active or forced - (this.toCamelCase(experimentConfig.status) === 'active' || forcedExperiment) + (context.toCamelCase(experimentConfig.status) === 'active' || forcedExperiment) // experiment has resolved audiences if configured && (!experimentConfig.resolvedAudiences || experimentConfig.resolvedAudiences.length) // forced audience resolves if defined @@ -381,21 +389,21 @@ export async function getConfig(experiment, instantExperiment, pluginOptions) { return experimentConfig; } -export async function runExperiment(customOptions = {}) { +export async function runExperiment(document, options, context) { if (isBot()) { return false; } - const pluginOptions = { ...DEFAULT_OPTIONS, ...customOptions }; - const experiment = this.getMetadata(pluginOptions.experimentsMetaTag); + const pluginOptions = { ...DEFAULT_OPTIONS, ...(options || {}) }; + const experiment = context.getMetadata(pluginOptions.experimentsMetaTag); if (!experiment) { return false; } - const variants = this.getMetadata('instant-experiment') - || this.getMetadata(`${pluginOptions.experimentsMetaTag}-variants`); + const variants = context.getMetadata('instant-experiment') + || context.getMetadata(`${pluginOptions.experimentsMetaTag}-variants`); let experimentConfig; try { - experimentConfig = await getConfig.call(this, experiment, variants, pluginOptions); + experimentConfig = await getConfig(experiment, variants, pluginOptions, context); } catch (err) { // eslint-disable-next-line no-console console.error('Invalid experiment config.', err); @@ -432,36 +440,38 @@ export async function runExperiment(customOptions = {}) { 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]}`); - this.sampleRUM('experiment', { + context.sampleRUM('experiment', { source: experimentConfig.id, target: result ? experimentConfig.selectedVariant : experimentConfig.variantNames[0], }); return result; } -export async function runCampaign(customOptions) { +export async function runCampaign(document, options, context) { if (isBot()) { return false; } - const options = { ...DEFAULT_OPTIONS, ...customOptions }; + const pluginOptions = { ...DEFAULT_OPTIONS, ...options }; const usp = new URLSearchParams(window.location.search); - const campaign = (usp.has(options.campaignsQueryParameter) - ? this.toClassName(usp.get(options.campaignsQueryParameter)) + const campaign = (usp.has(pluginOptions.campaignsQueryParameter) + ? context.toClassName(usp.get(pluginOptions.campaignsQueryParameter)) : null) - || (usp.has('utm_campaign') ? this.toClassName(usp.get('utm_campaign')) : null); + || (usp.has('utm_campaign') ? context.toClassName(usp.get('utm_campaign')) : null); if (!campaign) { return false; } - const audiences = this.getMetadata(`${options.audiencesMetaTagPrefix}-audience`) - .split(',').map(this.toClassName); - const resolvedAudiences = await getResolvedAudiences(audiences, options); - if (!!resolvedAudiences && !resolvedAudiences.length) { - return false; + let audiences = context.getMetadata(`${pluginOptions.campaignsMetaTagPrefix}-audience`); + if (audiences) { + audiences = audiences.split(',').map(context.toClassName); + const resolvedAudiences = await getResolvedAudiences(audiences, pluginOptions, context); + if (!!resolvedAudiences && !resolvedAudiences.length) { + return false; + } } - const allowedCampaigns = this.getAllMetadata(options.campaignsMetaTagPrefix); + const allowedCampaigns = context.getAllMetadata(pluginOptions.campaignsMetaTagPrefix); if (!Object.keys(allowedCampaigns).includes(campaign)) { return false; } @@ -479,7 +489,7 @@ export async function runCampaign(customOptions) { console.debug(`failed to serve campaign ${campaign}. Falling back to default content.`); } document.body.classList.add(`campaign-${campaign}`); - this.sampleRUM('campaign', { + context.sampleRUM('campaign', { source: window.location.href, target: result ? campaign : 'default', }); @@ -491,13 +501,13 @@ export async function runCampaign(customOptions) { } } -export async function serveAudience(customOptions) { +export async function serveAudience(document, options, context) { if (isBot()) { return false; } - const pluginOptions = { ...DEFAULT_OPTIONS, ...customOptions }; - const configuredAudiences = this.getAllMetadata(pluginOptions.audiencesMetaTagPrefix); + const pluginOptions = { ...DEFAULT_OPTIONS, ...(options || {}) }; + const configuredAudiences = context.getAllMetadata(pluginOptions.audiencesMetaTagPrefix); if (!Object.keys(configuredAudiences).length) { return false; } @@ -505,6 +515,7 @@ export async function serveAudience(customOptions) { const audiences = await getResolvedAudiences( Object.keys(configuredAudiences), pluginOptions, + context, ); if (!audiences || !audiences.length) { return false; @@ -512,7 +523,7 @@ export async function serveAudience(customOptions) { const usp = new URLSearchParams(window.location.search); const forcedAudience = usp.has(pluginOptions.audiencesQueryParameter) - ? this.toClassName(usp.get(pluginOptions.audiencesQueryParameter)) + ? context.toClassName(usp.get(pluginOptions.audiencesQueryParameter)) : null; const urlString = configuredAudiences[forcedAudience || audiences[0]]; @@ -528,7 +539,7 @@ export async function serveAudience(customOptions) { console.debug(`failed to serve audience ${forcedAudience || audiences[0]}. Falling back to default content.`); } document.body.classList.add(audiences.map((audience) => `audience-${audience}`)); - this.sampleRUM('audiences', { + context.sampleRUM('audiences', { source: window.location.href, target: result ? forcedAudience || audiences.join(',') : 'default', }); @@ -550,7 +561,8 @@ window.hlx.patchBlockConfig.push((config) => { // The current experiment does not modify the block if (experiment.selectedVariant === experiment.variantNames[0] - || !experiment.blocks || !experiment.blocks.includes(config.blockName)) { + || !experiment.variants[experiment.variantNames[0]].blocks + || !experiment.variants[experiment.variantNames[0]].blocks.includes(config.blockName)) { return config; } @@ -586,7 +598,7 @@ window.hlx.patchBlockConfig.push((config) => { path = `/blocks/${config.blockName}`; } } else { // Experimenting from a different branch on the same branch - path = variant.blocks[index]; + path = `/blocks/${variant.blocks[index]}`; } if (!origin && !path) { return config; @@ -601,40 +613,50 @@ window.hlx.patchBlockConfig.push((config) => { }); let isAdjusted = false; -function adjustedRumSamplingRate(checkpoint, customOptions) { - const pluginOptions = { ...DEFAULT_OPTIONS, ...customOptions }; +function adjustedRumSamplingRate(checkpoint, options, context) { + const pluginOptions = { ...DEFAULT_OPTIONS, ...(options || {}) }; return (data) => { if (!window.hlx.rum.isSelected && !isAdjusted) { isAdjusted = true; - window.hlx.rum.weight = Math.min(window.hlx.rum.weight, pluginOptions.rumSamplingRate); + // adjust sampling rate based on project config … + window.hlx.rum.weight = Math.min( + window.hlx.rum.weight, + // … but limit it to the 10% sampling at max to avoid losing anonymization + // and reduce burden on the backend + Math.max(pluginOptions.rumSamplingRate, MAX_SAMPLING_RATE), + ); window.hlx.rum.isSelected = (window.hlx.rum.random * window.hlx.rum.weight < 1); if (window.hlx.rum.isSelected) { - this.sampleRUM(checkpoint, data); + context.sampleRUM(checkpoint, data); } } return true; }; } -export async function loadEager(customOptions = {}) { - this.sampleRUM.always.on('audiences', adjustedRumSamplingRate('audiences', customOptions)); - this.sampleRUM.always.on('campaign', adjustedRumSamplingRate('campaign', customOptions)); - this.sampleRUM.always.on('experiment', adjustedRumSamplingRate('experiment', customOptions)); - let res = await runCampaign.call(this, customOptions); +export async function loadEager(document, options, context) { + context.sampleRUM.always.on('audiences', adjustedRumSamplingRate('audiences', options, context)); + context.sampleRUM.always.on('campaign', adjustedRumSamplingRate('campaign', options, context)); + context.sampleRUM.always.on('experiment', adjustedRumSamplingRate('experiment', options, context)); + let res = await runCampaign(document, options, context); if (!res) { - res = await runExperiment.call(this, customOptions); + res = await runExperiment(document, options, context); } if (!res) { - res = await serveAudience.call(this, customOptions); + res = await serveAudience(document, options, context); } } -export async function loadLazy(customOptions = {}) { +export async function loadLazy(document, options, context) { const pluginOptions = { ...DEFAULT_OPTIONS, - ...customOptions, + ...(options || {}), }; - // eslint-disable-next-line import/no-cycle - const preview = await import('./preview.js'); - preview.default.call({ ...this, getResolvedAudiences }, pluginOptions); + 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 }); + } } diff --git a/src/preview.css b/src/preview.css index 7be3a55e..8269cfa0 100644 --- a/src/preview.css +++ b/src/preview.css @@ -146,6 +146,9 @@ .hlx-popup code { margin: 0; padding: 0; + background: inherit; + border: inherit; + color: inherit; font-size: inherit; line-height: 1.5; } diff --git a/src/preview.js b/src/preview.js index 22aed53a..5401bb85 100644 --- a/src/preview.js +++ b/src/preview.js @@ -273,14 +273,14 @@ function populatePerformanceMetrics(div, config, { * Create Badge if a Page is enlisted in a AEM Experiment * @return {Object} returns a badge or empty string */ -async function decorateExperimentPill(overlay, options) { +async function decorateExperimentPill(overlay, options, context) { const config = window?.hlx?.experiment; - const experiment = this.toClassName(this.getMetadata(options.experimentsMetaTag)); - // eslint-disable-next-line no-console - console.log('preview experiment', experiment); + const experiment = context.toClassName(context.getMetadata(options.experimentsMetaTag)); if (!experiment || !config) { return; } + // eslint-disable-next-line no-console + console.log('preview experiment', experiment); const pill = createPopupButton( `Experiment: ${config.id}`, @@ -300,7 +300,7 @@ async function decorateExperimentPill(overlay, options) { }, config.variantNames.map((vname) => createVariant(experiment, vname, config, options)), ); - pill.classList.add(`is-${this.toClassName(config.status)}`); + pill.classList.add(`is-${context.toClassName(config.status)}`); overlay.append(pill); const performanceMetrics = await fetchRumData(experiment, options); @@ -329,25 +329,25 @@ function createCampaign(campaign, isSelected, options) { * Create Badge if a Page is enlisted in a AEM Campaign * @return {Object} returns a badge or empty string */ -async function decorateCampaignPill(overlay, options) { - const campaigns = this.getAllMetadata(options.campaignsMetaTagPrefix); +async function decorateCampaignPill(overlay, options, context) { + const campaigns = context.getAllMetadata(options.campaignsMetaTagPrefix); if (!Object.keys(campaigns).length) { return; } const usp = new URLSearchParams(window.location.search); const forcedAudience = usp.has(options.audiencesQueryParameter) - ? this.toClassName(usp.get(options.audiencesQueryParameter)) + ? context.toClassName(usp.get(options.audiencesQueryParameter)) : null; - const audiences = campaigns.audience.split(',').map(this.toClassName); - const resolvedAudiences = await this.getResolvedAudiences(audiences, options); + const audiences = campaigns.audience?.split(',').map(context.toClassName) || []; + const resolvedAudiences = await context.getResolvedAudiences(audiences, options); const isActive = forcedAudience ? audiences.includes(forcedAudience) : (!resolvedAudiences || !!resolvedAudiences.length); const campaign = (usp.has(options.campaignsQueryParameter) - ? this.toClassName(usp.get(options.campaignsQueryParameter)) + ? context.toClassName(usp.get(options.campaignsQueryParameter)) : null) - || (usp.has('utm_campaign') ? this.toClassName(usp.get('utm_campaign')) : null); + || (usp.has('utm_campaign') ? context.toClassName(usp.get('utm_campaign')) : null); const pill = createPopupButton( `Campaign: ${campaign || 'default'}`, { @@ -363,7 +363,7 @@ async function decorateCampaignPill(overlay, options) { createCampaign('default', !campaign || !isActive, options), ...Object.keys(campaigns) .filter((c) => c !== 'audience') - .map((c) => createCampaign(c, isActive && this.toClassName(campaign) === c, options)), + .map((c) => createCampaign(c, isActive && context.toClassName(campaign) === c, options)), ], ); @@ -388,30 +388,31 @@ function createAudience(audience, isSelected, options) { * Create Badge if a Page is enlisted in a AEM Audiences * @return {Object} returns a badge or empty string */ -async function decorateAudiencesPill(overlay, options) { - const audiences = this.getAllMetadata(options.audiencesMetaTagPrefix); +async function decorateAudiencesPill(overlay, options, context) { + const audiences = context.getAllMetadata(options.audiencesMetaTagPrefix); if (!Object.keys(audiences).length || !Object.keys(options.audiences).length) { return; } - const usp = new URLSearchParams(window.location.search); - const forcedAudience = usp.has(options.audiencesQueryParameter) - ? this.toClassName(usp.get(options.audiencesQueryParameter)) - : null; + const resolvedAudiences = await context.getResolvedAudiences( + Object.keys(audiences), + options, + context, + ); const pill = createPopupButton( 'Audiences', { label: 'Audiences for this page:', }, [ - createAudience('default', !forcedAudience || forcedAudience === 'default', options), + createAudience('default', !resolvedAudiences.length || resolvedAudiences[0] === 'default', options), ...Object.keys(audiences) .filter((a) => a !== 'audience') - .map((a) => createAudience(a, forcedAudience === a, options)), + .map((a) => createAudience(a, resolvedAudiences && resolvedAudiences[0] === a, options)), ], ); - if (forcedAudience) { + if (resolvedAudiences.length) { pill.classList.add('is-active'); } overlay.append(pill); @@ -421,13 +422,13 @@ async function decorateAudiencesPill(overlay, options) { * Decorates Preview mode badges and overlays * @return {Object} returns a badge or empty string */ -export default async function decoratePreviewMode(options) { +export default async function decoratePreviewMode(document, options, context) { try { - this.loadCSS(`${window.hlx.codeBasePath}/plugins/experience-decisioning/src/preview.css`); + context.loadCSS(`${options.basePath || window.hlx.codeBasePath}/plugins/experience-decisioning/src/preview.css`); const overlay = getOverlay(options); - await decorateAudiencesPill.call(this, overlay, options); - await decorateCampaignPill.call(this, overlay, options); - await decorateExperimentPill.call(this, overlay, options); + await decorateAudiencesPill(overlay, options, context); + await decorateCampaignPill(overlay, options, context); + await decorateExperimentPill(overlay, options, context); } catch (e) { // eslint-disable-next-line no-console console.log(e); diff --git a/src/ued.js b/src/ued.js index e4147239..d28e91c3 100644 --- a/src/ued.js +++ b/src/ued.js @@ -9,6 +9,9 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ + +var storage = window.sessionStorage; + function murmurhash3_32_gc(key, seed) { var remainder = key.length & 3; var bytes = key.length - remainder; @@ -92,7 +95,7 @@ function assignTreatment(allocationPercentages, treatments) { return treatments[i]; } function getLastExperimentTreatment(experimentId) { - var experimentsStr = localStorage.getItem(LOCAL_STORAGE_KEY); + var experimentsStr = storage.getItem(LOCAL_STORAGE_KEY); if (experimentsStr) { var experiments = JSON.parse(experimentsStr); if (experiments[experimentId]) { @@ -102,7 +105,7 @@ function getLastExperimentTreatment(experimentId) { return null; } function setLastExperimentTreatment(experimentId, treatment) { - var experimentsStr = localStorage.getItem(LOCAL_STORAGE_KEY); + var experimentsStr = storage.getItem(LOCAL_STORAGE_KEY); var experiments = experimentsStr ? JSON.parse(experimentsStr) : {}; var now = new Date(); var expKeys = Object.keys(experiments); @@ -114,7 +117,7 @@ function setLastExperimentTreatment(experimentId, treatment) { }); var date = now.toISOString().split('T')[0]; experiments[experimentId] = { treatment: treatment, date: date }; - localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(experiments)); + storage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(experiments)); } function assignTreatmentByDevice(experimentId, allocationPercentages, treatments) { var cachedTreatmentId = getLastExperimentTreatment(experimentId); @@ -173,6 +176,9 @@ function traverseDecisionTree(decisionNodesMap, context, currentNodeId) { } } function evaluateDecisionPolicy(decisionPolicy, context) { + if (context.storage && context.storage instanceof Storage) { + storage = context.storage; + } var decisionNodesMap = {}; decisionPolicy.decisionNodes.forEach(function (item) { decisionNodesMap[item['id']] = item;