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: EDS personalization using AEP Segments #54

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions data/segment-mappings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"id":"a44525f4-e115-41d3-a650-eaad3fa2a458","name":"Franklin Feedback Funnel State Lapsed Segment","description":"audience whose membership is lapsed"},{"id":"609b90e4-5306-4c37-b64b-e3026ad1f768","name":"Franklin Feedback photoshop Segment"},{"id":"e52f5a05-0f03-46e7-96a3-74446971af8e","name":"Franklin Feedback Interests segment"},{"id":"c2ba55f2-b327-4c5e-84b0-49df613cd71b","name":"Email Opened Segment"},{"id":"e0eab159-2464-4f93-b3c9-455ce5b5102a","name":"Test Alina2"},{"id":"78ef77be-bbf3-444d-bd15-f51043ce1f8f","name":"All test profiles"},{"id":"f8eeb1c4-42f0-4db5-a209-dc27c1224f23","name":"SitesNewsletterAudience"},{"id":"c891dba5-b7d0-4962-8152-f2463ebf408c","name":"ExpSuccess country segment"},{"id":"60c8669b-b9be-4242-b0ef-c3175808187b","name":"Visited Surf Product Detail Pages"},{"id":"de150daf-4bc5-4132-a550-dcea643cecee","name":"Female Surfers"},{"id":"f1972801-fd98-40b4-a313-18f1a3c84acb","name":"AlinaSegment"},{"id":"12af8939-12f2-4b41-a961-dba44b1e02d6","name":"SitesNewsletterv2"},{"id":"1ff20223-5239-4370-b887-6e69c28e5d28","name":"Cedric Huesler","description":"Person ID equals [email protected]"},{"id":"ef677d6f-44f9-4ac7-8827-c9ff745d1658","name":"Latha","description":"Latha Ramasamy"},{"id":"93f15377-5d55-4f1d-96f8-729ca4308d09","name":"Damian Test"},{"id":"50c89ea1-a3f8-4827-a61f-01ae6711c572","name":"AndreiP-segment"},{"id":"301fb79b-98ba-49ac-aafd-1b3b9e9066c2","name":"Demo segment"},{"id":"f417df3c-dbb0-40eb-8719-3d769d66917f","name":"Subscription - KFTest","description":"test (system generated segment for subscription - do NOT edit)"},{"id":"8ed561e9-bee2-45cf-988c-584f83d3ab96","name":"People who ordered in the last 30 days","description":"Last 30 days"},{"id":"6d7ac2f9-a806-4b90-a0f2-13cdee438acf","name":"Keara Only"},{"id":"f9aad193-fc81-4d1f-8bd0-7e4dcc6ff8ed","name":"Milestone Updates (All)"},{"id":"1946eedb-5372-42bc-85ad-673e0b6172cb","name":"WKND Adventure - Surfing","description":"A test segment of WKND adventurers that enjoy surfing"},{"id":"0839e05b-efff-485f-bbf3-13a533e15bd1","name":"demo"},{"id":"bbf00d86-ab3e-4e87-b77d-7cff7a4cfd36","name":"DX Summit Segment"},{"id":"be0d4fc6-e8f1-49cd-b247-57782f1c58f2","name":"Hemingway-Segment"},{"id":"f119b6e7-1814-4b86-99c6-1fefcca9c638","name":"Customer Journey"}]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this file is supposed to be cached by the sync action, I'd rather have the GH workflow as part of that PR as well, instead of a static version of the file

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ramboz I don't have access to settings on this repo, can you please help setting the secrets sent to you?

3 changes: 3 additions & 0 deletions plugins/experimentation/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export async function getResolvedAudiences(applicableAudiences, options, context
if (options.audiences[key] && typeof options.audiences[key] === 'function') {
return options.audiences[key]();
}
if (!options.audiences[key] && typeof options.audiences.default === 'function') {
return options.audiences.default(key);
}
return false;
}),
);
Expand Down
176 changes: 164 additions & 12 deletions scripts/analytics/lib-analytics.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,22 @@
* governing permissions and limitations under the License.
*/

const EVENT_TYPE_PROPOSITION_INTERACTION = 'propositionInteraction';
const EXPERIENCE_STEP_EXPERIMENTATION = 'experimentation';
/**
* Customer's XDM schema namespace
* @type {string}
*/
const CUSTOM_SCHEMA_NAMESPACE = '_sitesinternal';

/**
* Configure the cookie keys that should be mapped to the XDM schema and send with each event
* Ex: { 'funnelState': 'userState' }
* funnelState in the cookie will be sent as userState in the schema
*/
const COOKIE_MAPPING_TO_SCHEMA = {
};

/**
* Returns experiment id and variant running
* @returns {{experimentVariant: *, experimentId}}
Expand Down Expand Up @@ -49,23 +59,63 @@
function getDatastreamConfiguration() {
// Sites Internal
return {
edgeConfigId: 'caad777c-c410-4ceb-8b36-167f1cecc3de',
edgeConfigId: '2324184b-260b-4d66-a8ca-897ab9374fb3', // TODO: change this to earlier after testing
orgId: '908936ED5D35CC220A495CD4@AdobeOrg',
};
}

/**
* Enhance all events with additional details, like experiment running,
* If the configured key in COOKIE_MAPPING_TO_SCHEMA exists in the cookie
* it'll be added to the XDM schema
* @param {*} xdmData
*/
function updateAlloyEventWithCookieData(xdmData) {
const cookieData = document.cookie.split(';').reduce((res, item) => {
const [key, val] = item.split('=');
res[key.trim()] = val;
return res;
}, {});
Object.keys(cookieData).forEach((key) => {
const mappedKey = COOKIE_MAPPING_TO_SCHEMA[key];
if (mappedKey && xdmData.xdm[CUSTOM_SCHEMA_NAMESPACE]) {
xdmData.xdm[CUSTOM_SCHEMA_NAMESPACE][mappedKey] = cookieData[key];
}
});
}

/**
* Enhance all events with additional details, like experiment running, mapped cookie data, etc.
* before sending them to the edge
* @param options event in the XDM schema format
*/
function enhanceAnalyticsEvent(options) {
const experiment = getExperimentDetails();
const experienceDecisioningXDM = experiment ? {
decisioning: {
propositionEventType: EVENT_TYPE_PROPOSITION_INTERACTION,
propositions: [
{
scopeDetails: {
strategies: [
{
strategyID: experiment.experimentId,
step: EXPERIENCE_STEP_EXPERIMENTATION,
treatmentID: experiment.experimentVariant,
},
],
},
},
],
},
} : {};
options.xdm[CUSTOM_SCHEMA_NAMESPACE] = {
...options.xdm[CUSTOM_SCHEMA_NAMESPACE],
...(experiment && { experiment }), // add experiment details, if existing, to all events
};
// eslint-disable-next-line no-underscore-dangle
options.xdm._experience = experienceDecisioningXDM;
updateAlloyEventWithCookieData(options);
console.debug(`enhanceAnalyticsEvent complete: ${JSON.stringify(options)}`);

Check warning on line 118 in scripts/analytics/lib-analytics.js

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
}

/**
Expand Down Expand Up @@ -111,7 +161,7 @@
async function sendAnalyticsEvent(xdmData) {
// eslint-disable-next-line no-undef
if (!alloy) {
console.warn('alloy not initialized, cannot send analytics event');

Check warning on line 164 in scripts/analytics/lib-analytics.js

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
return Promise.resolve();
}
// eslint-disable-next-line no-undef
Expand All @@ -130,7 +180,7 @@
export async function analyticsSetConsent(approved) {
// eslint-disable-next-line no-undef
if (!alloy) {
console.warn('alloy not initialized, cannot set consent');

Check warning on line 183 in scripts/analytics/lib-analytics.js

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
return Promise.resolve();
}
// eslint-disable-next-line no-undef
Expand Down Expand Up @@ -178,26 +228,38 @@
createInlineScript(document, document.body, getAlloyInitScript(), 'text/javascript');
}

/**
* Sets up analytics tracking with alloy (initializes and configures alloy)
* @param document
* @returns {Promise<void>}
*/
export async function setupAnalyticsTrackingWithAlloy(document) {
export async function setupAlloy(document) {
// eslint-disable-next-line no-undef
if (!alloy) {
console.warn('alloy not initialized, cannot configure');

Check warning on line 234 in scripts/analytics/lib-analytics.js

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
return;
}
// create a promise on window that resolves when alloy loading is complete
window.alloyLoader = new Promise((resolve) => {
window.alloyLoaderResolve = resolve;
});
// eslint-disable-next-line no-undef
const configurePromise = alloy('configure', getAlloyConfiguration(document));

await import('./alloy.min.js');
await configurePromise;
window.alloyLoaderResolve();
}

/**
* Sets up analytics tracking with alloy (initializes and configures alloy)
* @param document
* @returns {Promise<void>}
*/
export async function setupAnalyticsTrackingWithAlloy(document) {
if (window.alloyLoader) {
await window.alloyLoader;
} else {
await setupAlloy(document);
}
// Custom logic can be inserted here in order to support early tracking before alloy library
// loads, for e.g. for page views
const pageViewPromise = analyticsTrackPageViews(document); // track page view early

await import('./alloy.min.js');
await Promise.all([configurePromise, pageViewPromise]);
await analyticsTrackPageViews(document); // track page view early
}

/**
Expand Down Expand Up @@ -395,3 +457,93 @@

return sendAnalyticsEvent(baseXdm);
}

/**
* Sends custom data to analytics as additional fields in the custom name space
* @param element
* @param additionalXdmFields
* @returns {Promise<*>}
*/
export async function analyticsCustomData(additionalXdmFields = {}) {
const xdmData = {
eventType: 'web.webpagedetails.pageViews',
web: {
webPageDetails: {
pageViews: {
value: 0,
},
},
},
[CUSTOM_SCHEMA_NAMESPACE]: {
...additionalXdmFields,
},
};

return sendAnalyticsEvent(xdmData);
}

/**
* Checks if the analytics cookie consent is set to ALLOW
* @returns {boolean}
*/
function isCookieConsentAllowed() {
// check if cookie cookieconsent_status_ANALYTICS is set to ALLOW
const cookies = document.cookie.split(';');
// eslint-disable-next-line no-restricted-syntax
for (const cookie of cookies) {
if (cookie.trim().startsWith('cookieconsent_status_ANALYTICS=ALLOW')) {
return true;
}
}
return false;
}

function getSegmentsFromAlloyResponse(response) {
const segments = [];
if (response && response.destinations) {
Object.values(response.destinations).forEach((destination) => {
if (destination.segments) {
Object.values(destination.segments).forEach((segment) => {
segments.push(segment.id);
});
}
});
}
console.log('segments', segments);

Check warning on line 512 in scripts/analytics/lib-analytics.js

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
return segments;
}

export async function getSegmentsFromAlloy() {
if (!window.alloy) {
return [];
}
// make sure that the cookie consent is available before making the call to alloy,
// otherwise the call will be queued and never executed
if (!isCookieConsentAllowed()) {
return [];
}
if (window.rtcdpSegments) {
return window.rtcdpSegments;
}
if (window.alloyLoader) {
await window.alloyLoader;
} else {
await setupAlloy(document);
}
let result;
// avoid multiple calls to alloy for render decisions from different audiences
if (window.renderDecisionsResult) {
result = await window.renderDecisionsResult;
} else {
// eslint-disable-next-line no-undef
window.renderDecisionsResult = alloy('sendEvent', {
renderDecisions: true,
}).catch((error) => {
console.error('Error sending event to alloy:', error);

Check warning on line 542 in scripts/analytics/lib-analytics.js

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
return [];
});
result = await window.renderDecisionsResult;
}
window.rtcdpSegments = getSegmentsFromAlloyResponse(result);
return window.rtcdpSegments;
}
38 changes: 37 additions & 1 deletion scripts/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,18 @@ import {
analyticsTrackError,
initAnalyticsTrackingQueue,
setupAnalyticsTrackingWithAlloy,
getSegmentsFromAlloy,
analyticsCustomData,
} from './analytics/lib-analytics.js';

const LCP_BLOCKS = []; // add your LCP blocks to the list
/**
* Add your segment name id mappings path here. Configure Segment sync
* from https://github.com/adobe-rnd/aem-experimentation-gh-actions/tree/main/segments-sync
* to generate the segment-mappings automatically from your AEP segments.
*/
const SEGMENTNAME_ID_MAPPINGS = '/data/segment-mappings.json';
const RTCDP_AUDIENCE_PREFIX = 'rtcdp';
window.hlx.RUM_GENERATION = 'project-1'; // add your RUM generation information here

// Define the custom audiences mapping for experience decisioning
Expand All @@ -32,6 +41,23 @@ const AUDIENCES = {
desktop: () => window.innerWidth >= 600,
'new-visitor': () => !localStorage.getItem('franklin-visitor-returning'),
'returning-visitor': () => !!localStorage.getItem('franklin-visitor-returning'),
default: async (audience) => {
// check if the audience is rtcdp audience
if (audience.indexOf(RTCDP_AUDIENCE_PREFIX) !== -1) {
const rtcdpAudience = audience.replace(`${RTCDP_AUDIENCE_PREFIX}-`, '');
const segmentMappingsResponse = await fetch(SEGMENTNAME_ID_MAPPINGS);
const segmentMappingsJson = await segmentMappingsResponse.json();
const segmentMappings = [];
segmentMappingsJson.forEach((mapping) => {
const name = mapping.name.replace(/\s+/g, '-').toLowerCase();
segmentMappings[name] = mapping.id;
});
const rtcdpAudienceId = segmentMappings[rtcdpAudience];
const segments = await getSegmentsFromAlloy();
return segments.includes(rtcdpAudienceId);
}
return false;
},
};

window.hlx.plugins.add('rum-conversion', {
Expand Down Expand Up @@ -182,6 +208,7 @@ export function decorateMain(main) {
async function loadEager(doc) {
document.documentElement.lang = 'en';
decorateTemplateAndTheme();
await initAnalyticsTrackingQueue();

await window.hlx.plugins.run('loadEager');

Expand All @@ -190,7 +217,6 @@ async function loadEager(doc) {

const main = doc.querySelector('main');
if (main) {
await initAnalyticsTrackingQueue();
decorateMain(main);
await waitForLCP(LCP_BLOCKS);
}
Expand Down Expand Up @@ -271,6 +297,16 @@ async function loadPage() {
await window.hlx.plugins.load('lazy');
await loadLazy(document);
const setupAnalytics = setupAnalyticsTrackingWithAlloy(document);

// example of sending custom data to AEP through Alloy
// TODO: comment this after testing
if (document.location.href.includes('products')) {
analyticsCustomData({
Interests: 'sports',
Entitlements: 'photoshop',
});
}

loadDelayed();
await setupAnalytics;
}
Expand Down
Loading