-
Notifications
You must be signed in to change notification settings - Fork 43
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
Improved click collection with ActivityMap and event grouping support #1108
Changes from 15 commits
a6e7f6a
cadeb61
08395c9
17887d1
c00ab65
cac8372
e016398
4ec8285
3e650f1
9428ec5
a534bde
e8aeaa4
c276b1f
fc460a5
b423597
818c88a
2e61d4d
d4b0b05
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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"; | ||
|
||
export default ({ storage }) => { | ||
return { | ||
liljenback marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
} | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
/* | ||
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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We haven't yet dropped support for IE11, although I think it is planned for this year. I don't think IE11 supports property accessors. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like we're using it in a few other locations such as the createTaskQueue.js. |
||
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, | ||
clickedElement: props.clickedElement | ||
}; | ||
}, | ||
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"; | ||
|
||
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; | ||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if you wanted to run AppMeasurement and Web SDK side by side to ensure they were collecting all the data?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AppMeasurement ignores the event that has been "tagged" with
s_fe
because it is the previously captured and cancelled event that was then re-fired. What happened was that Alloy ended up picking up the original event and this event as well causing two link click event getting sent. It might be that this design in AppMeasurement no longer is required (with the beacon API being available etc.). This addition makes it so that alloy is not affected by the "additional " event.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, depending on the browser prioritization or execution logic for the capturing click handlers this could potentially cause race conditions where we end up losing the event. Perhaps a better solution would be to introduce some sort of rate limiting instead.