mixpanel-browser 2.59.0 → 2.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -70,7 +70,7 @@ function getPreviousElementSibling(el) {
70
70
  }
71
71
  }
72
72
 
73
- function getPropertiesFromElement(el) {
73
+ function getPropertiesFromElement(el, ev, blockAttrsSet, extraAttrs, allowElementCallback, allowSelectors) {
74
74
  var props = {
75
75
  '$classes': getClassName(el).split(' '),
76
76
  '$tag_name': el.tagName.toLowerCase()
@@ -80,9 +80,9 @@ function getPropertiesFromElement(el) {
80
80
  props['$id'] = elId;
81
81
  }
82
82
 
83
- if (shouldTrackElement(el)) {
84
- _.each(TRACKED_ATTRS, function(attr) {
85
- if (el.hasAttribute(attr)) {
83
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors)) {
84
+ _.each(TRACKED_ATTRS.concat(extraAttrs), function(attr) {
85
+ if (el.hasAttribute(attr) && !blockAttrsSet[attr]) {
86
86
  var attrVal = el.getAttribute(attr);
87
87
  if (shouldTrackValue(attrVal)) {
88
88
  props['$attr-' + attr] = attrVal;
@@ -106,8 +106,21 @@ function getPropertiesFromElement(el) {
106
106
  return props;
107
107
  }
108
108
 
109
- function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
110
- blockSelectors = blockSelectors || [];
109
+ function getPropsForDOMEvent(ev, config) {
110
+ var allowElementCallback = config.allowElementCallback;
111
+ var allowSelectors = config.allowSelectors || [];
112
+ var blockAttrs = config.blockAttrs || [];
113
+ var blockElementCallback = config.blockElementCallback;
114
+ var blockSelectors = config.blockSelectors || [];
115
+ var captureTextContent = config.captureTextContent || false;
116
+ var captureExtraAttrs = config.captureExtraAttrs || [];
117
+
118
+ // convert array to set every time, as the config may have changed
119
+ var blockAttrsSet = {};
120
+ _.each(blockAttrs, function(attr) {
121
+ blockAttrsSet[attr] = true;
122
+ });
123
+
111
124
  var props = null;
112
125
 
113
126
  var target = typeof ev.target === 'undefined' ? ev.srcElement : ev.target;
@@ -115,7 +128,11 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
115
128
  target = target.parentNode;
116
129
  }
117
130
 
118
- if (shouldTrackDomEvent(target, ev)) {
131
+ if (
132
+ shouldTrackDomEvent(target, ev) &&
133
+ isElementAllowed(target, ev, allowElementCallback, allowSelectors) &&
134
+ !isElementBlocked(target, ev, blockElementCallback, blockSelectors)
135
+ ) {
119
136
  var targetElementList = [target];
120
137
  var curEl = target;
121
138
  while (curEl.parentNode && !isTag(curEl, 'body')) {
@@ -126,37 +143,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
126
143
  var elementsJson = [];
127
144
  var href, explicitNoTrack = false;
128
145
  _.each(targetElementList, function(el) {
129
- var shouldTrackEl = shouldTrackElement(el);
146
+ var shouldTrackDetails = shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors);
130
147
 
131
148
  // if the element or a parent element is an anchor tag
132
149
  // include the href as a property
133
- if (el.tagName.toLowerCase() === 'a') {
150
+ if (!blockAttrsSet['href'] && el.tagName.toLowerCase() === 'a') {
134
151
  href = el.getAttribute('href');
135
- href = shouldTrackEl && shouldTrackValue(href) && href;
152
+ href = shouldTrackDetails && shouldTrackValue(href) && href;
136
153
  }
137
154
 
138
- // allow users to programmatically prevent tracking of elements by adding classes such as 'mp-no-track'
139
- var classes = getClasses(el);
140
- _.each(OPT_OUT_CLASSES, function(cls) {
141
- if (classes[cls]) {
142
- explicitNoTrack = true;
143
- }
144
- });
145
-
146
- if (!explicitNoTrack) {
147
- // programmatically prevent tracking of elements that match CSS selectors
148
- _.each(blockSelectors, function(sel) {
149
- try {
150
- if (el['matches'](sel)) {
151
- explicitNoTrack = true;
152
- }
153
- } catch (err) {
154
- logger.critical('Error while checking selector: ' + sel, err);
155
- }
156
- });
155
+ if (isElementBlocked(el, ev, blockElementCallback, blockSelectors)) {
156
+ explicitNoTrack = true;
157
157
  }
158
158
 
159
- elementsJson.push(getPropertiesFromElement(el));
159
+ elementsJson.push(getPropertiesFromElement(el, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors));
160
160
  }, this);
161
161
 
162
162
  if (!explicitNoTrack) {
@@ -170,9 +170,17 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
170
170
  '$viewportHeight': Math.max(docElement['clientHeight'], window['innerHeight'] || 0),
171
171
  '$viewportWidth': Math.max(docElement['clientWidth'], window['innerWidth'] || 0)
172
172
  };
173
+ _.each(captureExtraAttrs, function(attr) {
174
+ if (!blockAttrsSet[attr] && target.hasAttribute(attr)) {
175
+ var attrVal = target.getAttribute(attr);
176
+ if (shouldTrackValue(attrVal)) {
177
+ props['$el_attr__' + attr] = attrVal;
178
+ }
179
+ }
180
+ });
173
181
 
174
182
  if (captureTextContent) {
175
- elementText = getSafeText(target);
183
+ elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
176
184
  if (elementText && elementText.length) {
177
185
  props['$el_text'] = elementText;
178
186
  }
@@ -188,14 +196,22 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
188
196
  }
189
197
  // prioritize text content from "real" click target if different from original target
190
198
  if (captureTextContent) {
191
- var elementText = getSafeText(target);
199
+ var elementText = getSafeText(target, ev, allowElementCallback, allowSelectors);
192
200
  if (elementText && elementText.length) {
193
201
  props['$el_text'] = elementText;
194
202
  }
195
203
  }
196
204
 
197
205
  if (target) {
198
- var targetProps = getPropertiesFromElement(target);
206
+ // target may have been recalculated; check allowlists and blocklists again
207
+ if (
208
+ !isElementAllowed(target, ev, allowElementCallback, allowSelectors) ||
209
+ isElementBlocked(target, ev, blockElementCallback, blockSelectors)
210
+ ) {
211
+ return null;
212
+ }
213
+
214
+ var targetProps = getPropertiesFromElement(target, ev, blockAttrsSet, captureExtraAttrs, allowElementCallback, allowSelectors);
199
215
  props['$target'] = targetProps;
200
216
  // pull up more props onto main event props
201
217
  props['$el_classes'] = targetProps['$classes'];
@@ -211,19 +227,20 @@ function getPropsForDOMEvent(ev, blockSelectors, captureTextContent) {
211
227
  }
212
228
 
213
229
 
214
- /*
230
+ /**
215
231
  * Get the direct text content of an element, protecting against sensitive data collection.
216
232
  * Concats textContent of each of the element's text node children; this avoids potential
217
233
  * collection of sensitive data that could happen if we used element.textContent and the
218
234
  * element had sensitive child elements, since element.textContent includes child content.
219
235
  * Scrubs values that look like they could be sensitive (i.e. cc or ssn number).
220
236
  * @param {Element} el - element to get the text of
237
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
221
238
  * @returns {string} the element's direct text content
222
239
  */
223
- function getSafeText(el) {
240
+ function getSafeText(el, ev, allowElementCallback, allowSelectors) {
224
241
  var elText = '';
225
242
 
226
- if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) {
243
+ if (shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) && el.childNodes && el.childNodes.length) {
227
244
  _.each(el.childNodes, function(child) {
228
245
  if (isTextNode(child) && child.textContent) {
229
246
  elText += _.trim(child.textContent)
@@ -262,6 +279,75 @@ function guessRealClickTarget(ev) {
262
279
  return target;
263
280
  }
264
281
 
282
+ function isElementAllowed(el, ev, allowElementCallback, allowSelectors) {
283
+ if (allowElementCallback) {
284
+ try {
285
+ if (!allowElementCallback(el, ev)) {
286
+ return false;
287
+ }
288
+ } catch (err) {
289
+ logger.critical('Error while checking element in allowElementCallback', err);
290
+ return false;
291
+ }
292
+ }
293
+
294
+ if (!allowSelectors.length) {
295
+ // no allowlist; all elements are fair game
296
+ return true;
297
+ }
298
+
299
+ for (var i = 0; i < allowSelectors.length; i++) {
300
+ var sel = allowSelectors[i];
301
+ try {
302
+ if (el['matches'](sel)) {
303
+ return true;
304
+ }
305
+ } catch (err) {
306
+ logger.critical('Error while checking selector: ' + sel, err);
307
+ }
308
+ }
309
+ return false;
310
+ }
311
+
312
+ function isElementBlocked(el, ev, blockElementCallback, blockSelectors) {
313
+ var i;
314
+
315
+ if (blockElementCallback) {
316
+ try {
317
+ if (blockElementCallback(el, ev)) {
318
+ return true;
319
+ }
320
+ } catch (err) {
321
+ logger.critical('Error while checking element in blockElementCallback', err);
322
+ return true;
323
+ }
324
+ }
325
+
326
+ if (blockSelectors && blockSelectors.length) {
327
+ // programmatically prevent tracking of elements that match CSS selectors
328
+ for (i = 0; i < blockSelectors.length; i++) {
329
+ var sel = blockSelectors[i];
330
+ try {
331
+ if (el['matches'](sel)) {
332
+ return true;
333
+ }
334
+ } catch (err) {
335
+ logger.critical('Error while checking selector: ' + sel, err);
336
+ }
337
+ }
338
+ }
339
+
340
+ // allow users to programmatically prevent tracking of elements by adding default classes such as 'mp-no-track'
341
+ var classes = getClasses(el);
342
+ for (i = 0; i < OPT_OUT_CLASSES.length; i++) {
343
+ if (classes[OPT_OUT_CLASSES[i]]) {
344
+ return true;
345
+ }
346
+ }
347
+
348
+ return false;
349
+ }
350
+
265
351
  /*
266
352
  * Check whether a DOM node has nodeType Node.ELEMENT_NODE
267
353
  * @param {Node} node - node to check
@@ -336,11 +422,16 @@ function shouldTrackDomEvent(el, ev) {
336
422
  * Check whether a DOM element should be "tracked" or if it may contain sensitive data
337
423
  * using a variety of heuristics.
338
424
  * @param {Element} el - element to check
425
+ * @param {Array<string>} allowSelectors - CSS selectors for elements that should be included
339
426
  * @returns {boolean} whether the element should be tracked
340
427
  */
341
- function shouldTrackElement(el) {
428
+ function shouldTrackElementDetails(el, ev, allowElementCallback, allowSelectors) {
342
429
  var i;
343
430
 
431
+ if (!isElementAllowed(el, ev, allowElementCallback, allowSelectors)) {
432
+ return false;
433
+ }
434
+
344
435
  for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) {
345
436
  var classes = getClasses(curEl);
346
437
  for (i = 0; i < SENSITIVE_DATA_CLASSES.length; i++) {
@@ -428,7 +519,7 @@ export {
428
519
  getSafeText,
429
520
  logger,
430
521
  minDOMApisSupported,
431
- shouldTrackDomEvent, shouldTrackElement, shouldTrackValue,
522
+ shouldTrackDomEvent, shouldTrackElementDetails, shouldTrackValue,
432
523
  EV_CHANGE, EV_CLICK, EV_HASHCHANGE, EV_MP_LOCATION_CHANGE, EV_POPSTATE,
433
524
  EV_SCROLLEND, EV_SUBMIT
434
525
  };
package/src/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  var Config = {
2
2
  DEBUG: false,
3
- LIB_VERSION: '2.59.0'
3
+ LIB_VERSION: '2.60.0'
4
4
  };
5
5
 
6
6
  export default Config;
@@ -345,8 +345,12 @@ MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) {
345
345
  if (!(k in union_q)) {
346
346
  union_q[k] = [];
347
347
  }
348
- // We may send duplicates, the server will dedup them.
349
- union_q[k] = union_q[k].concat(v);
348
+ // Prevent duplicate values
349
+ _.each(v, function(item) {
350
+ if (!_.include(union_q[k], item)) {
351
+ union_q[k].push(item);
352
+ }
353
+ });
350
354
  }
351
355
  });
352
356
  this._pop_from_people_queue(UNSET_ACTION, q_data);