UNPKG

6.25 kBJavaScriptView Raw
1const isLeftClickEvent = event => event.button === 0;
2
3const isModifiedEvent = event =>
4 !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
5
6const trackMap = {
7 select: ['focus', 'blur'],
8 textarea: ['focus', 'blur'],
9 input: ['focus', 'blur'],
10 default: ['click'],
11};
12
13const isValidEventTypeOnTarget = event =>
14 (trackMap[event.target.nodeName.toLowerCase()] || trackMap.default).indexOf(
15 event.type
16 ) > -1;
17
18const isPluginEnabled = plugin =>
19 typeof plugin.isEnabled === 'function'
20 ? plugin.isEnabled()
21 : plugin.isEnabled;
22
23const camelCase = str =>
24 str.replace(/-([a-z\d])/gi, (match, char) => char.toUpperCase());
25
26/**
27 * Polyfill for [`Event.composedPath()`][1].
28 * https://gist.github.com/kleinfreund/e9787d73776c0e3750dcfcdc89f100ec
29 */
30const getComposedPath = node => {
31 let parent;
32 if (node.parentNode) {
33 parent = node.parentNode;
34 } else if (node.host) {
35 parent = node.host;
36 } else if (node.defaultView) {
37 parent = node.defaultView;
38 }
39
40 if (parent !== undefined) {
41 return [node].concat(getComposedPath(parent));
42 }
43
44 return [node];
45};
46
47export default class AvAnalytics {
48 constructor(
49 plugins,
50 promise = Promise,
51 pageTracking,
52 autoTrack = true,
53 options = {}
54 ) {
55 // if plugins or promise are undefined,
56 // or if either is skipped and pageTracking boolean is used in their place
57 if (!plugins || !promise) {
58 throw new Error('[plugins], and [promise] must be defined');
59 }
60
61 this.plugins = Array.isArray(plugins) ? plugins : [plugins];
62 this.pageTracking = !!pageTracking;
63
64 this.Promise = promise;
65 this.recursive = !!options.recursive;
66 this.attributePrefix = options.attributePrefix || 'data-analytics';
67
68 this.isPageTracking = false;
69 this.hasInit = false;
70
71 if (autoTrack) {
72 this.startAutoTrack();
73 }
74 }
75
76 startAutoTrack = () => {
77 document.body.addEventListener('click', this.handleEvent, true);
78 document.body.addEventListener('focus', this.handleEvent, true);
79 document.body.addEventListener('blur', this.handleEvent, true);
80 };
81
82 stopAutoTrack = () => {
83 document.body.removeEventListener('click', this.handleEvent, true);
84 document.body.removeEventListener('focus', this.handleEvent, true);
85 document.body.removeEventListener('blur', this.handleEvent, true);
86 };
87
88 handleEvent = event => {
89 if (this.invalidEvent(event)) {
90 return;
91 }
92 const target = event.target || event.srcElement;
93 const path = getComposedPath(event.target);
94
95 let analyticAttrs = {};
96
97 if (this.recursive) {
98 // Reverse the array so we pull attributes from top down
99 path.reverse().forEach(pth => {
100 const attrs = this.getAnalyticAttrs(pth);
101
102 analyticAttrs = { ...analyticAttrs, ...attrs };
103
104 // To consider using the element it has to have analytics attrs
105 if (Object.keys(attrs).length > 0) {
106 analyticAttrs.elemId =
107 pth.getAttribute('id') || pth.getAttribute('name');
108 }
109 });
110 } else {
111 analyticAttrs = this.getAnalyticAttrs(target);
112 }
113
114 if (
115 !Object.keys(analyticAttrs).length > 0 ||
116 (this.recursive && !analyticAttrs.action) ||
117 analyticAttrs.action !== event.type
118 ) {
119 return;
120 }
121
122 analyticAttrs.action = analyticAttrs.action || event.type;
123 analyticAttrs.event = event.type;
124 analyticAttrs.elemId =
125 analyticAttrs.elemId ||
126 target.getAttribute('id') ||
127 target.getAttribute('name');
128
129 this.trackEvent(analyticAttrs);
130 };
131
132 invalidEvent = event =>
133 isModifiedEvent(event) ||
134 (event.type === 'click' && !isLeftClickEvent(event)) ||
135 !isValidEventTypeOnTarget(event);
136
137 getAnalyticAttrs = elem => {
138 if (!elem.attributes) {
139 return {};
140 }
141
142 const attrs = elem.attributes;
143 const analyticAttrs = {};
144
145 if (elem.nodeType === 1) {
146 for (let i = attrs.length - 1; i >= 0; i--) {
147 const { name } = attrs[i];
148 if (name.indexOf(`${this.attributePrefix}-`) === 0) {
149 const camelName = camelCase(
150 name.slice(this.attributePrefix.length + 1)
151 );
152 analyticAttrs[camelName] = elem.getAttribute(name);
153 }
154 }
155 }
156 return analyticAttrs;
157 };
158
159 startPageTracking = () => {
160 if (!this.pageListener) {
161 this.pageListener = this.trackPageView;
162 window.addEventListener('hashchange', this.pageListener, false);
163 }
164 };
165
166 stopPageTracking = () => {
167 if (this.pageListener) {
168 window.removeEventListener('hashchange', this.pageListener, false);
169 delete this.pageListener;
170 }
171 };
172
173 init = () => {
174 this.setPageTracking();
175
176 this.plugins.forEach(plugin => {
177 if (isPluginEnabled(plugin) && typeof plugin.init === 'function') {
178 plugin.init();
179 }
180 });
181 };
182
183 setPageTracking = value => {
184 // eslint-disable-next-line eqeqeq
185 if (value != undefined) {
186 this.pageTracking = !!value;
187 }
188
189 const canPageTrack =
190 typeof this.startPageTracking === 'function' &&
191 typeof this.stopPageTracking === 'function';
192
193 if (canPageTrack && this.pageTracking !== this.isPageTracking) {
194 if (this.pageTracking) {
195 this.startPageTracking();
196 } else {
197 this.stopPageTracking();
198 }
199 this.isPageTracking = this.pageTracking;
200 }
201 };
202
203 trackEvent = properties => {
204 const promises = [];
205 properties.url = properties.url || window.location.href || 'N/A';
206
207 this.plugins.forEach(plugin => {
208 const props = {
209 ...properties,
210 };
211
212 if (isPluginEnabled(plugin) && typeof plugin.trackEvent === 'function') {
213 promises.push(plugin.trackEvent(props));
214 }
215 });
216 return this.Promise.all(promises);
217 };
218
219 trackPageView = url => {
220 // hashchanges are an object so we want to grab the new url from it
221 if (typeof url === 'object') {
222 url = url.newURL;
223 }
224
225 url = url || window.location.href;
226 const promises = [];
227 this.plugins.forEach(plugin => {
228 if (
229 isPluginEnabled(plugin) &&
230 typeof plugin.trackPageView === 'function'
231 ) {
232 promises.push(plugin.trackPageView(url));
233 }
234 });
235 return this.Promise.all(promises);
236 };
237}