1 | const isLeftClickEvent = event => event.button === 0;
|
2 |
|
3 | const isModifiedEvent = event =>
|
4 | !!(event.metaKey || event.altKey || event.ctrlKey || event.shiftKey);
|
5 |
|
6 | const trackMap = {
|
7 | select: ['focus', 'blur'],
|
8 | textarea: ['focus', 'blur'],
|
9 | input: ['focus', 'blur'],
|
10 | default: ['click'],
|
11 | };
|
12 |
|
13 | const isValidEventTypeOnTarget = event =>
|
14 | (trackMap[event.target.nodeName.toLowerCase()] || trackMap.default).indexOf(
|
15 | event.type
|
16 | ) > -1;
|
17 |
|
18 | const isPluginEnabled = plugin =>
|
19 | typeof plugin.isEnabled === 'function'
|
20 | ? plugin.isEnabled()
|
21 | : plugin.isEnabled;
|
22 |
|
23 | const camelCase = str =>
|
24 | str.replace(/-([a-z\d])/gi, (match, char) => char.toUpperCase());
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 | const 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 |
|
47 | export default class AvAnalytics {
|
48 | constructor(
|
49 | plugins,
|
50 | promise = Promise,
|
51 | pageTracking,
|
52 | autoTrack = true,
|
53 | options = {}
|
54 | ) {
|
55 |
|
56 |
|
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 |
|
99 | path.reverse().forEach(pth => {
|
100 | const attrs = this.getAnalyticAttrs(pth);
|
101 |
|
102 | analyticAttrs = { ...analyticAttrs, ...attrs };
|
103 |
|
104 |
|
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 |
|
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 |
|
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 | }
|