Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save yuhui/6657fda3c49db8fba5eb7934566f683a to your computer and use it in GitHub Desktop.
Save yuhui/6657fda3c49db8fba5eb7934566f683a to your computer and use it in GitHub Desktop.
Adobe Experience Platform Web SDK: improve the data that is collected through autoamatic click data collection.
// Use this custom code block to adjust or filter click data. You can use the following variables:
// content.clickedElement: The DOM element that was clicked
// content.pageName: The page name when the click happened
// content.linkName: The name of the clicked link
// content.linkRegion: The region of the clicked link
// content.linkType: The type of link (typically exit, download, or other)
// content.linkUrl: The destination URL of the clicked link
// Return false to omit link data.
/* globals content, document, event, _satellite */
/**
* This callback does the following:
* 1. Don't track clicks on link that don't use a known URL protocol.
* 2. Change "exit" link types that are for organisation-owned links to be a special "internal" link type.
* 3. Don't track "other" link types that are tracked by Web SDK automatically.
* 4. Replace content.linkRegion with a more useful value than BODY.
*/
const ALLOWED_LINK_PROTOCOLS = [
'ftp',
'http',
'https',
'javascript',
'mailto',
'sftp',
'tel',
];
const DUMMY_URL = 'none://none/none';
const {
clickedElement = null,
linkName = '',
linkRegion: defaultLinkRegion = '',
linkType = '',
linkUrl: interimLinkUrl = '',
} = content;
// use the <A> trick to retrieve the link's properties easily
const linkElement = document.createElement('a');
/**
* If linkUrl is not set, use a dummy URL to get properties properly later.
* If a dummy URL is not used, then "href" will get set with the URL of the __current page__ instead, which is WRONG!
*/
linkElement.href = interimLinkUrl || DUMMY_URL;
/**
* There is a weird situation where non-HTTP/s linkUrls have the format "[protocol][...text...][protocol]".
* Update linkUrl to replace "[protocol][...text...][protocol]" with "[protocol]".
*/
const { protocol } = linkElement;
const invalidLinkUrlPrefixRegExp = new RegExp(`^${protocol}.+${protocol}`);
content.linkUrl = interimLinkUrl.replace(invalidLinkUrlPrefixRegExp, protocol);
// get the linkUrl properties again
const {
linkUrl = '',
} = content;
linkElement.href = linkUrl || DUMMY_URL;
const {
hostname: linkHostname,
protocol: linkProtocol,
} = linkElement;
/**
* # Don't track clicks on link that don't use a known URL protocol
*
* In other words, track clicks on links from __known / allowed__ URL protocols.
*/
if (!ALLOWED_LINK_PROTOCOLS.includes(linkProtocol.replace(':', ''))) {
_satellite.logger.warn(`Links with "${linkProtocol}" are not being tracked >> aborting hit.`);
return false;
}
switch (linkType) {
case 'exit': {
/**
* # Change "exit" link types that are for organisation-owned links to be a special "internal" link type
*
* There are links to other domains that belong to this organisation.
* Refer to the regex in the "internal domains regex" data element for the list of these domains.
*
* If content.linkType is "exit" and content.linkUrl is from one of these organisation-owned domains,
* then mark them as "internal".
*
* These will be tracked as "other" link types later.
* See the "On before event send callback" section of this Web SDK extension.
*/
// take note of the "$" in the RegExp constructor!
const internalDomainsRegExp = new RegExp(`${_satellite.getVar('internal domains regex', event)}$`, 'i');
if (internalDomainsRegExp.test(linkHostname)) {
// IMPORTANT! "internal" is a non-Web SDK-standard link type. It is used only to identify that a link belongs to the organisation.
_satellite.logger.log(`Link is from an internal domain, "${linkHostname}" >> recognising as non-exit "internal" link.`);
content.linkType = 'internal';
}
break;
}
case 'other': {
/**
* # Don't track "other" link types that are tracked by Web SDK automatically
*
* If content.linkName is a default value for an "other" content.linkType,
* then that means this link's click was tracked automatically by Web SDK.
*
* But we really don't need to track such link clicks, so return false in such cases.
*/
const DEFAULT_LINK_NAMES = [
'Link Click',
'link clicked',
];
if (DEFAULT_LINK_NAMES.includes(linkName)) {
_satellite.logger.log(`Detected link of "other" type with name "${linkName}" >> aborting hit`);
return false;
}
break;
}
}
/**
* # Replace content.linkRegion with a more useful value than BODY
*
* Refer to https://experienceleague.adobe.com/en/docs/analytics/components/dimensions/activity-map-region#populate-this-dimension-with-data
* for how linkRegion is set by default.
*
* The update is to set linkRegion using class names when the default linkRegion is "BODY".
* But if no suitable class name is found, then use "BODY".
*/
if (clickedElement && defaultLinkRegion === 'BODY') {
let sectionElement = clickedElement;
let parentElement;
let foundDocumentBody = false;
let foundPageTopSection = false;
let foundPageTopElement = false;
let sectionElementHasId = !!sectionElement.id;
/**
* Actually, there probably isn't a need to check for an "id" in sectionElement,
* since linkRegion would be set to it by default already when found.
*
* But to ensure completeness of this algorithm __and just in case__,
* there will still be a check for "id" in sectionElement,
* and if found, then use that "id" as the linkRegion.
*/
while (!sectionElementHasId && !foundPageTopElement) {
parentElement = sectionElement.parentElement;
/**
* IMPORTANT! Update the `found*` lines below to account for the top-level content section of your page.
*/
foundDocumentBody = parentElement === document.body; // !! DO NOT REMOVE !!
foundPageTopSection = parentElement.nodeName.toLowerCase() === 'section';
foundPageTopElement = foundDocumentBody || foundPageTopSection;
if (!foundPageTopElement) {
sectionElement = parentElement;
sectionElementHasId = !!sectionElement.id;
}
}
content.linkRegion = defaultLinkRegion;
if (sectionElementHasId) {
content.linkRegion = sectionElement.id;
} else if (foundPageTopElement) {
if (sectionElement.classList.length > 0) {
content.linkRegion = sectionElement.classList[0];
} else {
content.linkRegion = `${parentElement.nodeName}>${sectionElement.nodeName}`;
}
}
_satellite.logger.log(`Renamed link region from "${defaultLinkRegion}" to "${content.linkRegion}"`);
}
_satellite.logger.log('XDM link click object', content);
// Modify content.xdm or content.data as necessary. There is no need to wrap the
// code in a function or return a value. For example:
// content.xdm.web.webPageDetails.name = "Checkout";
/* globals content, event, _satellite */
const { xdm } = content;
if (!xdm) {
_satellite.logger.warn('XDM object not found. __How weird!__ >> aborting hit');
return;
}
/**
* This callback does the following:
* 1. Update XDM objects with automatically tracked link clicks.
*/
/**
* # Update XDM objects with automatically tracked link clicks
*
* When link click events are tracked automatically by Web SDK, the events **do not** have required custom variables.
* So these Web SDK events need to be updated to include those required custom variables __before__ they are sent to Adobe.
*
* The update is done as follows:
* 1. Create a XDM schema-compatible object with the required custom variables.
* 2. Merge the following using the "Merged Objects" data element type from the Core extension:
* 2a. this newly created object,
* 2b. the XDM object that was created automatically from Web SDK's automatic link click feature, and
* 2c. the "XDM variable" data element.
*
* Now, this merged object can be sent in the Web SDK event to Adobe.
*
* But BEWARE!
* This update must be done with Web SDK's automatic link click events **only**!
* It __must__ never be done for other tracking that runs from Rules.
*/
const OTHER_LINK_TYPES = [
'internal', // Exit Links that have been identified as internal links
'other', // Custom Links
];
_satellite.setVar('XDM object already in Web SDK on before event callback', xdm);
const {
eventType = '',
} = xdm;
switch (eventType) {
case 'web.webinteraction.linkClicks': {
const {
web: {
webInteraction: {
name = '',
region = '',
type = '',
URL = '',
} = {},
} = {},
} = xdm;
const decodedUrl = decodeURI(URL);
switch (type) {
case 'download': {
/**
* Example:
* 1. Track the concatenation of the link name and filename extension to eVar6.
* 2. Track the download URL to eVar7.
* 3. Track "1" to event6.
*/
const URLBase = URL.replace(/(\?|#).*/, ''); // keep the link protocol, hostname and pathname only
const URLBaseParts = URLBase.split('/'); // expected result: [<protocol>, '', <hostname>, <pathname level 1>, <pathname level 2>, ..., <filename.extension>]
const fileName = URLBaseParts.pop(); // expected result: <filename.extension>
const fileNameParts = fileName.split('.'); // expected result: [<filename>, <extension>]
const fileExtension = fileNameParts.pop(); // expected result: <extension>
_satellite.setVar('setVar download link click details array', [
name,
fileExtension,
]);
const downloadLinkClickDetails = _satellite.getVar('download link click details', event);
const downloadLinkXdmObj = {
_experience: {
analytics: {
customDimensions: {
eVars: {
// Download Details (v6)
eVar6: downloadLinkClickDetails,
// Download URL (v7)
eVar7: decodedUrl,
},
},
event1to100: {
event6: {
// Download Click (e6)
value: 1,
},
},
},
},
web: {
webInteraction: {
// delete the name so that Adobe Analytics reports the URL in its "Download Link" dimension
name: '',
},
},
};
_satellite.setVar('setVar XDM download link object created in Web SDK on before event callback', downloadLinkXdmObj);
content.xdm = _satellite.getVar('XDM merged download link object for Web SDK automatic link tracking', event);
break;
}
case 'exit': {
/**
* Example:
* 1. Track the concatenation of the link type and link name to eVar8.
* 2. Track "1" to event8.
*/
_satellite.setVar('setVar exit link click details array', [
type,
name,
]);
const exitLinkClickDetails = _satellite.getVar('exit link click details', event);
const exitLinkXdmObj = {
_experience: {
analytics: {
customDimensions: {
eVars: {
// Exit Link (v8)
eVar8: exitLinkClickDetails,
},
},
event1to100: {
event8: {
// Exit Link Click (e8)
value: 1,
},
},
},
},
web: {
webInteraction: {
// delete the name so that Adobe Analytics reports the URL in its "Exit Link" dimension
name: '',
},
},
};
_satellite.setVar('setVar XDM exit link object created in Web SDK on before event callback', exitLinkXdmObj);
content.xdm = _satellite.getVar('XDM merged exit link object for Web SDK automatic link tracking', event);
break;
}
default: {
if (!OTHER_LINK_TYPES.includes(type)) {
_satellite.logger.warn(`Unrecognised link type "${type}" >> aborting hit`);
break;
}
let internalLinkXdmObj = {};
if (type === 'internal') {
/**
* This is NOT a standard Web SDK link type.
*
* It is a special link type that is used only to identify links to other domains that belong to this organisation.
* It is set in the "Filter click properties" section in this Web SDK extension.
*/
/**
* Example:
* 1. Track the link name to eVar9.
* 2. Track "1" to event9.
*/
internalLinkXdmObj = {
_experience: {
analytics: {
customDimensions: {
eVars: {
// Internal Link (v9)
eVar9: name,
},
},
event1to100: {
event9: {
// Internal Link Click (e9)
value: 1,
},
},
},
},
web: {
webInteraction: {
// Make this a Custom Link
type: 'other',
},
},
};
}
_satellite.setVar('setVar XDM internal link object created in Web SDK on before event callback', internalLinkXdmObj);
/**
* If an "other" link had been tracked automatically by Web SDK, then it will not have required custom variables.
* In that case, the required custom variables need to be added.
*/
/**
* Example:
* 1. Track the concatenation of the link region and link name to eVar5.
* 2. Track "1" to event5.
*/
_satellite.setVar('setVar other link click details array', [
region,
name,
]);
const otherLinkClickDetails = _satellite.getVar('other link click details', event);
const otherLinkXdmObj = {
_experience: {
analytics: {
customDimensions: {
eVars: {
// Custom Link Click Details (v5)
eVar5: otherLinkClickDetails,
},
},
event1to100: {
event5: {
// Custom Link Click (e5)
value: 1,
},
},
},
},
};
_satellite.setVar('setVar XDM other link object created in Web SDK on before event callback', otherLinkXdmObj);
content.xdm = _satellite.getVar('XDM merged other link object for Web SDK automatic link tracking', event);
break;
}
}
break;
}
case 'web.webpagedetails.pageViews': {
break;
}
}
_satellite.logger.log('XDM object content', content);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment