@availity/analytics-core
Version:
Analytics base configuration for sdk-js
214 lines (176 loc) • 6.5 kB
JavaScript
import {
camelCase,
getComposedPath,
isLeftClickEvent,
isModifiedEvent,
isPluginEnabled,
isValidEventTypeOnTarget,
} from './util';
export default class AvAnalytics {
constructor(plugins, promise = Promise, pageTracking, autoTrack = true, options = {}) {
// if plugins or promise are undefined,
// or if either is skipped and pageTracking boolean is used in their place
if (!plugins || !promise) {
throw new Error('[plugins] and [promise] must be defined');
}
this.plugins = Array.isArray(plugins) ? plugins : [plugins];
this.pageTracking = !!pageTracking;
if (options.eventModifiers) {
this.eventModifiers = Array.isArray(options.eventModifiers) ? options.eventModifiers : [options.eventModifiers];
} else {
this.eventModifiers = ['action'];
}
this.Promise = promise;
this.recursive = !!options.recursive;
this.attributePrefix = options.attributePrefix || 'data-analytics';
this.isPageTracking = false;
this.hasInit = false;
this.hasLogPlugin = this.plugins.some((plugin) => {
const apiName = plugin.AvLogMessages?.defaultConfig?.name;
return apiName === 'appl/analytics/log' || apiName === 'spc/analytics/log';
});
if (autoTrack) {
this.startAutoTrack();
}
}
startAutoTrack = () => {
document.body.addEventListener('click', this.handleEvent, true);
document.body.addEventListener('focus', this.handleEvent, true);
document.body.addEventListener('blur', this.handleEvent, true);
};
stopAutoTrack = () => {
document.body.removeEventListener('click', this.handleEvent, true);
document.body.removeEventListener('focus', this.handleEvent, true);
document.body.removeEventListener('blur', this.handleEvent, true);
};
handleEvent = (event) => {
if (this.invalidEvent(event)) {
return;
}
const target = event.target || event.srcElement;
const path = getComposedPath(event.target);
let analyticAttrs = {};
if (this.recursive) {
// Reverse the array so we pull attributes from top down
for (const pth of path.reverse()) {
const attrs = this.getAnalyticAttrs(pth);
analyticAttrs = { ...analyticAttrs, ...attrs };
// To consider using the element it has to have analytics attrs
if (Object.keys(attrs).length > 0) {
analyticAttrs.elemId = pth.getAttribute('id') || pth.getAttribute('name') || undefined;
}
}
} else {
analyticAttrs = this.getAnalyticAttrs(target);
}
const actions = analyticAttrs ? this.eventModifiers.filter((mod) => analyticAttrs[mod] === event.type) : [];
if (Object.keys(analyticAttrs).length === 0 || (this.recursive && actions.length === 0) || actions.length === 0) {
return;
}
analyticAttrs.action = analyticAttrs.action || event.type;
analyticAttrs.event = event.type;
analyticAttrs.elemId =
analyticAttrs.elemId || target.getAttribute('id') || target.getAttribute('name') || undefined;
if (analyticAttrs.elemId === undefined) {
delete analyticAttrs.elemId;
}
// remove keys for the click listeners
for (const key of actions) {
if (key !== 'action' && key !== 'event') {
delete analyticAttrs[key];
}
}
this.trackEvent(analyticAttrs);
};
invalidEvent = (event) =>
isModifiedEvent(event) || (event.type === 'click' && !isLeftClickEvent(event)) || !isValidEventTypeOnTarget(event);
getAnalyticAttrs = (elem) => {
if (!elem.attributes) return {};
const attrs = elem.attributes;
const analyticAttrs = {};
if (elem.nodeType === 1) {
for (let i = attrs.length - 1; i >= 0; i--) {
const { name } = attrs[i];
if (name.indexOf(`${this.attributePrefix}-`) === 0) {
const camelName = camelCase(name.slice(this.attributePrefix.length + 1));
analyticAttrs[camelName] = elem.getAttribute(name);
}
}
if (this.hasLogPlugin) {
const overridesKeys = Object.keys(analyticAttrs).filter(key => key.startsWith('overrides'));
if (overridesKeys.length > 0) {
analyticAttrs.overrides = {};
for (const key of overridesKeys) {
const nestedKey = key.slice(9); // Remove 'overrides'
const finalKey = nestedKey.charAt(0).toLowerCase() + nestedKey.slice(1);
analyticAttrs.overrides[finalKey] = analyticAttrs[key];
delete analyticAttrs[key];
}
}
}
}
return analyticAttrs;
};
startPageTracking = () => {
if (!this.pageListener) {
this.pageListener = this.trackPageView;
window.addEventListener('hashchange', this.pageListener, false);
}
};
stopPageTracking = () => {
if (this.pageListener) {
window.removeEventListener('hashchange', this.pageListener, false);
delete this.pageListener;
}
};
init = () => {
this.setPageTracking();
for (const plugin of this.plugins) {
if (isPluginEnabled(plugin) && typeof plugin.init === 'function') {
plugin.init();
}
}
};
setPageTracking = (value) => {
// eslint-disable-next-line eqeqeq
if (value != undefined) {
this.pageTracking = !!value;
}
const canPageTrack = typeof this.startPageTracking === 'function' && typeof this.stopPageTracking === 'function';
if (canPageTrack && this.pageTracking !== this.isPageTracking) {
if (this.pageTracking) {
this.startPageTracking();
} else {
this.stopPageTracking();
}
this.isPageTracking = this.pageTracking;
}
};
trackEvent = (properties) => {
const promises = [];
properties.url = properties.url || window.location.href || 'N/A';
for (const plugin of this.plugins) {
const props = {
...properties,
};
if (isPluginEnabled(plugin) && typeof plugin.trackEvent === 'function') {
promises.push(plugin.trackEvent(props));
}
}
return this.Promise.all(promises);
};
trackPageView = (url) => {
// hashchanges are an object so we want to grab the new url from it
if (typeof url === 'object') {
url = url.newURL;
}
url = url || window.location.href;
const promises = [];
for (const plugin of this.plugins) {
if (isPluginEnabled(plugin) && typeof plugin.trackPageView === 'function') {
promises.push(plugin.trackPageView(url));
}
}
return this.Promise.all(promises);
};
}