Skip to content

Commit

Permalink
Merge pull request #1108 from adobe/activitymap-full-support
Browse files Browse the repository at this point in the history
Improved click collection with ActivityMap and event grouping support
  • Loading branch information
jonsnyder authored Jun 26, 2024
2 parents a7c4375 + d4b0b05 commit 7768221
Show file tree
Hide file tree
Showing 78 changed files with 3,093 additions and 717 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"test:scripts": "jasmine --config=scripts/specs/jasmine.json",
"sandbox:build": "rollup -c --environment SANDBOX && cd sandbox && npm run build",
"dev": "concurrently --names build,sandbox \"rollup -c -w --environment SANDBOX\" \"cd sandbox && export REACT_APP_NONCE=321 && npm start\"",
"dev:standalone": "npm run clean && rollup -c -w --environment STANDALONE",
"build": "npm run clean && rollup -c --environment BASE_CODE_MIN,STANDALONE,STANDALONE_MIN && echo \"Base Code:\" && cat distTest/baseCode.min.js",
"build:cli": "node scripts/build-alloy.js",
"prepare": "husky && cd sandbox && npm install",
Expand Down
1 change: 1 addition & 0 deletions scripts/getTestingTags.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/

// eslint-disable-next-line
import { Octokit } from "@octokit/rest";
import semver from "semver";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ import { noop } from "../../utils/index.js";

const createClickHandler = ({ eventManager, lifecycle, handleError }) => {
return (clickEvent) => {
// Ignore repropagated clicks from AppMeasurement
if (clickEvent.s_fe) {
return Promise.resolve();
}
// TODO: Consider safeguarding from the same object being clicked multiple times in rapid succession?
const clickedElement = clickEvent.target;
const event = eventManager.createEvent();
Expand All @@ -26,7 +30,6 @@ const createClickHandler = ({ eventManager, lifecycle, handleError }) => {
if (event.isEmpty()) {
return Promise.resolve();
}

return eventManager.sendEvent(event);
})
// eventManager.sendEvent() will return a promise resolved to an
Expand All @@ -45,6 +48,5 @@ export default ({ eventManager, lifecycle, handleError }) => {
lifecycle,
handleError,
});

document.addEventListener("click", clickHandler, true);
};
19 changes: 18 additions & 1 deletion src/components/ActivityCollector/configValidators.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,23 @@ export const downloadLinkQualifier = string()

export default objectOf({
clickCollectionEnabled: boolean().default(true),
onBeforeLinkClickSend: callback(),
clickCollection: objectOf({
internalLinkEnabled: boolean().default(true),
externalLinkEnabled: boolean().default(true),
downloadLinkEnabled: boolean().default(true),
// TODO: Consider moving downloadLinkQualifier here.
sessionStorageEnabled: boolean().default(false),
eventGroupingEnabled: boolean().default(false),
filterClickProperties: callback(),
}).default({
internalLinkEnabled: true,
externalLinkEnabled: true,
downloadLinkEnabled: true,
sessionStorageEnabled: false,
eventGroupingEnabled: false,
}),
downloadLinkQualifier,
onBeforeLinkClickSend: callback().deprecated(
'The field "onBeforeLinkClickSend" has been deprecated. Use "clickCollection.filterClickDetails" instead.',
),
});
33 changes: 33 additions & 0 deletions src/components/ActivityCollector/createClickActivityStorage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright 2024 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

import { CLICK_ACTIVITY_DATA } from "../../constants/sessionDataKeys.js";

export default ({ storage }) => {
return {
save: (data) => {
const jsonData = JSON.stringify(data);
storage.setItem(CLICK_ACTIVITY_DATA, jsonData);
},
load: () => {
let jsonData = null;
const data = storage.getItem(CLICK_ACTIVITY_DATA);
if (data) {
jsonData = JSON.parse(data);
}
return jsonData;
},
remove: () => {
storage.removeItem(CLICK_ACTIVITY_DATA);
},
};
};
224 changes: 224 additions & 0 deletions src/components/ActivityCollector/createClickedElementProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
Copyright 2022 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

const buildXdmFromClickedElementProperties = (props) => {
return {
eventType: "web.webinteraction.linkClicks",
web: {
webInteraction: {
name: props.linkName,
region: props.linkRegion,
type: props.linkType,
URL: props.linkUrl,
linkClicks: {
value: 1,
},
},
},
};
};

const buildDataFromClickedElementProperties = (props) => {
return {
__adobe: {
analytics: {
c: {
a: {
activitymap: {
page: props.pageName,
link: props.linkName,
region: props.linkRegion,
pageIDType: props.pageIDType,
},
},
},
},
},
};
};

const populateClickedElementPropertiesFromOptions = (options, props) => {
const { xdm, data, clickedElement } = options;
props.clickedElement = clickedElement;
if (xdm && xdm.web && xdm.web.webInteraction) {
const { name, region, type, URL } = xdm.web.webInteraction;
props.linkName = name;
props.linkRegion = region;
props.linkType = type;
props.linkUrl = URL;
}
// DATA has priority over XDM
/* eslint no-underscore-dangle: 0 */
if (data && data.__adobe && data.__adobe.analytics) {
const { c } = data.__adobe.analytics;
if (c && c.a && c.a.activitymap) {
// Set the properties if they exists
const { page, link, region, pageIDType } = c.a.activitymap;
props.pageName = page || props.pageName;
props.linkName = link || props.linkName;
props.linkRegion = region || props.linkRegion;
if (pageIDType !== undefined) {
props.pageIDType = pageIDType;
}
}
}
};

export default ({ properties, logger } = {}) => {
let props = properties || {};
const clickedElementProperties = {
get pageName() {
return props.pageName;
},
set pageName(value) {
props.pageName = value;
},
get linkName() {
return props.linkName;
},
set linkName(value) {
props.linkName = value;
},
get linkRegion() {
return props.linkRegion;
},
set linkRegion(value) {
props.linkRegion = value;
},
get linkType() {
return props.linkType;
},
set linkType(value) {
props.linkType = value;
},
get linkUrl() {
return props.linkUrl;
},
set linkUrl(value) {
props.linkUrl = value;
},
get pageIDType() {
return props.pageIDType;
},
set pageIDType(value) {
props.pageIDType = value;
},
get clickedElement() {
return props.clickedElement;
},
set clickedElement(value) {
props.clickedElement = value;
},
get properties() {
return {
pageName: props.pageName,
linkName: props.linkName,
linkRegion: props.linkRegion,
linkType: props.linkType,
linkUrl: props.linkUrl,
pageIDType: props.pageIDType,
};
},
isValidLink() {
return (
!!props.linkUrl &&
!!props.linkType &&
!!props.linkName &&
!!props.linkRegion
);
},
isInternalLink() {
return this.isValidLink() && props.linkType === "other";
},
isValidActivityMapData() {
return (
!!props.pageName &&
!!props.linkName &&
!!props.linkRegion &&
props.pageIDType !== undefined
);
},
get xdm() {
if (props.filteredXdm) {
return props.filteredXdm;
}
return buildXdmFromClickedElementProperties(this);
},
get data() {
if (props.filteredData) {
return props.filteredData;
}
return buildDataFromClickedElementProperties(this);
},
applyPropertyFilter(filter) {
if (filter && filter(props) === false) {
if (logger) {
logger.info(
`Clicked element properties were rejected by filter function: ${JSON.stringify(
this.properties,
null,
2,
)}`,
);
}
props = {};
}
},
applyOptionsFilter(filter) {
const opts = this.options;
if (opts && opts.clickedElement && (opts.xdm || opts.data)) {
// Properties are rejected if filter is explicitly false.
if (filter && filter(opts) === false) {
if (logger) {
logger.info(
`Clicked element properties were rejected by filter function: ${JSON.stringify(
this.properties,
null,
2,
)}`,
);
}
this.options = undefined;
return;
}
this.options = opts;
// This is just to ensure that any fields outside clicked element properties
// set by the user filter persists.
props.filteredXdm = opts.xdm;
props.filteredData = opts.data;
}
},
get options() {
const opts = {};
if (this.isValidLink()) {
opts.xdm = this.xdm;
}
if (this.isValidActivityMapData()) {
opts.data = this.data;
}
if (this.clickedElement) {
opts.clickedElement = this.clickedElement;
}
if (!opts.xdm && !opts.data) {
return undefined;
}
return opts;
},
set options(value) {
props = {};
if (value) {
populateClickedElementPropertiesFromOptions(value, props);
}
},
};
return clickedElementProperties;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright 2022 Adobe. All rights reserved.
This file is licensed to you under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. You may obtain a copy
of the License at http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
OF ANY KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
*/

import createClickedElementProperties from "./createClickedElementProperties.js";

export default ({
window,
getLinkName,
getLinkRegion,
getAbsoluteUrlFromAnchorElement,
findClickableElement,
determineLinkType,
}) => {
return ({ clickedElement, config, logger, clickActivityStorage }) => {
const {
onBeforeLinkClickSend: optionsFilter, // Deprecated
clickCollection,
} = config;
const { filterClickDetails: propertyFilter } = clickCollection;
const elementProperties = createClickedElementProperties({ logger });
if (clickedElement) {
const clickableElement = findClickableElement(clickedElement);
if (clickableElement) {
elementProperties.clickedElement = clickedElement;
elementProperties.linkUrl = getAbsoluteUrlFromAnchorElement(
window,
clickableElement,
);
elementProperties.linkType = determineLinkType(
window,
config,
elementProperties.linkUrl,
clickableElement,
);
elementProperties.linkRegion = getLinkRegion(clickableElement);
elementProperties.linkName = getLinkName(clickableElement);
elementProperties.pageIDType = 0;
elementProperties.pageName = window.location.href;
// Check if we have a page-name stored from an earlier page view event
const storedLinkData = clickActivityStorage.load();
if (storedLinkData && storedLinkData.pageName) {
elementProperties.pageName = storedLinkData.pageName;
// Perhaps pageIDType should be established after customer filter is applied
// Like if pageName starts with "http" then pageIDType = 0
elementProperties.pageIDType = 1;
}
// If defined, run user provided filter function
if (propertyFilter) {
// clickCollection.filterClickDetails
elementProperties.applyPropertyFilter(propertyFilter);
} else if (optionsFilter) {
// onBeforeLinkClickSend
elementProperties.applyOptionsFilter(optionsFilter);
}
}
}
return elementProperties;
};
};
Loading

0 comments on commit 7768221

Please sign in to comment.