clarity-js 0.6.39 → 0.6.41

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.
@@ -88,7 +88,8 @@ var config$1 = {
88
88
  report: null,
89
89
  upload: null,
90
90
  fallback: null,
91
- upgrade: null
91
+ upgrade: null,
92
+ action: null
92
93
  };
93
94
 
94
95
  function api(method) {
@@ -111,7 +112,7 @@ function stop$B() {
111
112
  startTime = 0;
112
113
  }
113
114
 
114
- var version$1 = "0.6.39";
115
+ var version$1 = "0.6.41";
115
116
 
116
117
  // tslint:disable: no-bitwise
117
118
  function hash (input) {
@@ -138,9 +139,9 @@ var buffer = null;
138
139
  var update$2 = false;
139
140
  function start$E() {
140
141
  update$2 = false;
141
- reset$o();
142
+ reset$p();
142
143
  }
143
- function reset$o() {
144
+ function reset$p() {
144
145
  // Baseline state holds the previous values - if it is updated in the current payload,
145
146
  // reset the state to current value after sending the previous state
146
147
  if (update$2) {
@@ -208,14 +209,14 @@ function compute$c() {
208
209
  }
209
210
  }
210
211
  function stop$A() {
211
- reset$o();
212
+ reset$p();
212
213
  }
213
214
 
214
215
  var baseline = /*#__PURE__*/Object.freeze({
215
216
  __proto__: null,
216
217
  get state () { return state$9; },
217
218
  start: start$E,
218
- reset: reset$o,
219
+ reset: reset$p,
219
220
  track: track$7,
220
221
  activity: activity,
221
222
  visibility: visibility,
@@ -285,7 +286,7 @@ function max(metric, value) {
285
286
  function compute$b() {
286
287
  encode$1(0 /* Event.Metric */);
287
288
  }
288
- function reset$n() {
289
+ function reset$o() {
289
290
  updates$3 = {};
290
291
  }
291
292
 
@@ -304,7 +305,7 @@ function start$C() {
304
305
  interval = 60000 /* Setting.PingInterval */;
305
306
  last = 0;
306
307
  }
307
- function reset$m() {
308
+ function reset$n() {
308
309
  if (timeout$6) {
309
310
  clearTimeout(timeout$6);
310
311
  }
@@ -332,7 +333,7 @@ var ping$1 = /*#__PURE__*/Object.freeze({
332
333
  __proto__: null,
333
334
  get data () { return data$h; },
334
335
  start: start$C,
335
- reset: reset$m,
336
+ reset: reset$n,
336
337
  stop: stop$y
337
338
  });
338
339
 
@@ -363,7 +364,7 @@ function track$6(event, time) {
363
364
  function compute$a() {
364
365
  encode$1(36 /* Event.Summary */);
365
366
  }
366
- function reset$l() {
367
+ function reset$m() {
367
368
  data$g = {};
368
369
  }
369
370
 
@@ -374,7 +375,7 @@ var summary = /*#__PURE__*/Object.freeze({
374
375
  stop: stop$x,
375
376
  track: track$6,
376
377
  compute: compute$a,
377
- reset: reset$l
378
+ reset: reset$m
378
379
  });
379
380
 
380
381
  var data$f = null;
@@ -416,7 +417,7 @@ var upgrade$1 = /*#__PURE__*/Object.freeze({
416
417
 
417
418
  var data$e = null;
418
419
  function start$z() {
419
- reset$k();
420
+ reset$l();
420
421
  }
421
422
  function set(variable, value) {
422
423
  var values = typeof value === "string" /* Constant.String */ ? [value] : value;
@@ -447,11 +448,11 @@ function log$2(variable, value) {
447
448
  function compute$9() {
448
449
  encode$1(34 /* Event.Variable */);
449
450
  }
450
- function reset$k() {
451
+ function reset$l() {
451
452
  data$e = {};
452
453
  }
453
454
  function stop$v() {
454
- reset$k();
455
+ reset$l();
455
456
  }
456
457
 
457
458
  var variable = /*#__PURE__*/Object.freeze({
@@ -461,7 +462,7 @@ var variable = /*#__PURE__*/Object.freeze({
461
462
  set: set,
462
463
  identify: identify,
463
464
  compute: compute$9,
464
- reset: reset$k,
465
+ reset: reset$l,
465
466
  stop: stop$v
466
467
  });
467
468
 
@@ -701,7 +702,7 @@ function redact(value) {
701
702
  // Check if unicode regex is supported, otherwise fallback to calling mask function on this token
702
703
  if (unicodeRegex && currencyRegex !== null) {
703
704
  // Do not redact information if the token contains a currency symbol
704
- token = token.match(currencyRegex) ? token : token.replace(letterRegex, "\u2022" /* Data.Constant.Letter */).replace(digitRegex, "\u2022" /* Data.Constant.Digit */);
705
+ token = token.match(currencyRegex) ? token : token.replace(letterRegex, "\u25AA" /* Data.Constant.Letter */).replace(digitRegex, "\u25AB" /* Data.Constant.Digit */);
705
706
  }
706
707
  else {
707
708
  token = mask(token);
@@ -739,12 +740,15 @@ function check$4(id, target, input) {
739
740
  }
740
741
  }
741
742
 
742
- var TAGS = ["DIV", "TR", "P", "LI", "UL", "A", "BUTTON"];
743
- function selector (input, beta) {
744
- if (beta === void 0) { beta = false; }
743
+ var excludeClassNames = "load,active,fixed,visible,focus,show,collaps,animat" /* Constant.ExcludeClassNames */.split("," /* Constant.Comma */);
744
+ var selectorMap = {};
745
+ function reset$k() {
746
+ selectorMap = {};
747
+ }
748
+ function get$1(input, type) {
745
749
  var a = input.attributes;
746
- var prefix = input.prefix ? input.prefix[beta ? 1 /* Selector.Beta */ : 0 /* Selector.Stable */] : null;
747
- var suffix = beta || ((a && !("class" /* Constant.Class */ in a)) || TAGS.indexOf(input.tag) >= 0) ? ":nth-of-type(".concat(input.position, ")") : "" /* Constant.Empty */;
750
+ var prefix = input.prefix ? input.prefix[type] : null;
751
+ var suffix = type === 0 /* Selector.Alpha */ ? "".concat("~" /* Constant.Tilde */).concat(input.position - 1) : ":nth-of-type(".concat(input.position, ")");
748
752
  switch (input.tag) {
749
753
  case "STYLE":
750
754
  case "TITLE":
@@ -759,23 +763,33 @@ function selector (input, beta) {
759
763
  if (prefix === null) {
760
764
  return "" /* Constant.Empty */;
761
765
  }
762
- prefix = "".concat(prefix, ">");
766
+ prefix = "".concat(prefix).concat(">" /* Constant.Separator */);
763
767
  input.tag = input.tag.indexOf("svg:" /* Constant.SvgPrefix */) === 0 ? input.tag.substr("svg:" /* Constant.SvgPrefix */.length) : input.tag;
764
768
  var selector = "".concat(prefix).concat(input.tag).concat(suffix);
765
- var classes = "class" /* Constant.Class */ in a && a["class" /* Constant.Class */].length > 0 ? a["class" /* Constant.Class */].trim().split(/\s+/) : null;
766
- if (beta) {
767
- // In beta mode, update selector to use "id" field when available. There are two exceptions:
768
- // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
769
- // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
770
- var id = "id" /* Constant.Id */ in a && a["id" /* Constant.Id */].length > 0 ? a["id" /* Constant.Id */] : null;
771
- classes = input.tag !== "BODY" /* Constant.BodyTag */ && classes ? classes.filter(function (c) { return !hasDigits(c); }) : [];
772
- selector = classes.length > 0 ? "".concat(prefix).concat(input.tag, ".").concat(classes.join(".")).concat(suffix) : selector;
773
- selector = id && hasDigits(id) === false ? "".concat(getDomPrefix(prefix), "#").concat(id) : selector;
774
- }
775
- else {
776
- // Otherwise, fallback to stable mode, where we include class names as part of the selector
777
- selector = classes ? "".concat(prefix).concat(input.tag, ".").concat(classes.join(".")).concat(suffix) : selector;
769
+ var id = "id" /* Constant.Id */ in a && a["id" /* Constant.Id */].length > 0 ? a["id" /* Constant.Id */] : null;
770
+ var classes = input.tag !== "BODY" /* Constant.BodyTag */ && "class" /* Constant.Class */ in a && a["class" /* Constant.Class */].length > 0 ? a["class" /* Constant.Class */].trim().split(/\s+/).filter(function (c) { return filter(c); }).join("." /* Constant.Period */) : null;
771
+ if (classes && classes.length > 0) {
772
+ if (type === 0 /* Selector.Alpha */) {
773
+ // In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
774
+ // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
775
+ var key = "".concat(getDomPath(prefix)).concat(input.tag).concat("." /* Constant.Dot */).concat(classes);
776
+ if (!(key in selectorMap)) {
777
+ selectorMap[key] = [];
778
+ }
779
+ if (selectorMap[key].indexOf(input.id) < 0) {
780
+ selectorMap[key].push(input.id);
781
+ }
782
+ selector = "".concat(key).concat("~" /* Constant.Tilde */).concat(selectorMap[key].indexOf(input.id));
783
+ }
784
+ else {
785
+ // In Beta mode, we continue to look at query selectors in context of the full page
786
+ selector = "".concat(prefix).concat(input.tag, ".").concat(classes).concat(suffix);
787
+ }
778
788
  }
789
+ // Update selector to use "id" field when available. There are two exceptions:
790
+ // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
791
+ // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
792
+ selector = id && filter(id) ? "".concat(getDomPrefix(prefix)).concat("#" /* Constant.Hash */).concat(id) : selector;
779
793
  return selector;
780
794
  }
781
795
  }
@@ -784,22 +798,42 @@ function getDomPrefix(prefix) {
784
798
  var iframeDomStart = prefix.lastIndexOf("".concat("iframe:" /* Constant.IFramePrefix */).concat("HTML" /* Constant.HTML */));
785
799
  var domStart = Math.max(shadowDomStart, iframeDomStart);
786
800
  if (domStart < 0) {
787
- return "";
801
+ return "" /* Constant.Empty */;
802
+ }
803
+ return prefix.substring(0, prefix.indexOf(">" /* Constant.Separator */, domStart) + 1);
804
+ }
805
+ function getDomPath(input) {
806
+ var parts = input.split(">" /* Constant.Separator */);
807
+ for (var i = 0; i < parts.length; i++) {
808
+ var tIndex = parts[i].indexOf("~" /* Constant.Tilde */);
809
+ var dIndex = parts[i].indexOf("." /* Constant.Dot */);
810
+ parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
788
811
  }
789
- var domEnd = prefix.indexOf(">", domStart) + 1;
790
- return prefix.substr(0, domEnd);
812
+ return parts.join(">" /* Constant.Separator */);
791
813
  }
792
- // Check if the given input string has digits or not
793
- function hasDigits(value) {
814
+ // Check if the given input string has digits or excluded class names
815
+ function filter(value) {
816
+ if (!value) {
817
+ return false;
818
+ } // Do not process empty strings
819
+ if (excludeClassNames.some(function (x) { return value.toLowerCase().indexOf(x) >= 0; })) {
820
+ return false;
821
+ }
794
822
  for (var i = 0; i < value.length; i++) {
795
823
  var c = value.charCodeAt(i);
796
824
  if (c >= 48 /* Character.Zero */ && c <= 57 /* Character.Nine */) {
797
- return true;
825
+ return false;
798
826
  }
799
827
  }
800
- return false;
828
+ return true;
801
829
  }
802
830
 
831
+ var selector = /*#__PURE__*/Object.freeze({
832
+ __proto__: null,
833
+ reset: reset$k,
834
+ get: get$1
835
+ });
836
+
803
837
  // Track the start time to be able to compute duration at the end of the task
804
838
  var idleTimeout = 5000;
805
839
  var tracker = {};
@@ -1119,7 +1153,7 @@ function encode$4 (type, timer, ts) {
1119
1153
  }
1120
1154
  tokens.push(suspend ? "*M" /* Constant.SuspendMutationTag */ : data[key]);
1121
1155
  if (box && box.length === 2) {
1122
- tokens.push("".concat("#" /* Constant.Box */).concat(str$1(box[0]), ".").concat(str$1(box[1])));
1156
+ tokens.push("".concat("#" /* Constant.Hash */).concat(str$1(box[0]), ".").concat(str$1(box[1])));
1123
1157
  }
1124
1158
  break;
1125
1159
  case "attributes":
@@ -2373,6 +2407,7 @@ function reset$7() {
2373
2407
  iframeMap = new WeakMap();
2374
2408
  privacyMap = new WeakMap();
2375
2409
  fraudMap = new WeakMap();
2410
+ reset$k();
2376
2411
  }
2377
2412
  // We parse new root nodes for any regions or masked nodes in the beginning (document) and
2378
2413
  // later whenever there are new additions or modifications to DOM (mutations)
@@ -2549,14 +2584,11 @@ function privacy(node, value, parent) {
2549
2584
  metadata.privacy = 2 /* Privacy.Text */;
2550
2585
  break;
2551
2586
  case tag === "*T" /* Constant.TextTag */:
2552
- // If it's a text node belonging to a STYLE or TITLE tag or one of SCRUB_EXCEPTIONS, then capture content
2587
+ // If it's a text node belonging to a STYLE or TITLE tag or one of scrub exceptions, then capture content
2553
2588
  var pTag = parent && parent.data ? parent.data.tag : "" /* Constant.Empty */;
2554
- var pSelector_1 = parent && parent.selector ? parent.selector[0 /* Selector.Stable */] : "" /* Constant.Empty */;
2555
- metadata.privacy = pTag === "STYLE" /* Constant.StyleTag */ || pTag === "TITLE" /* Constant.TitleTag */ || override.some(function (x) { return pSelector_1.indexOf(x) >= 0; }) ? 0 /* Privacy.None */ : current;
2556
- break;
2557
- case "type" /* Constant.Type */ in attributes:
2558
- // If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
2559
- metadata.privacy = inspect(attributes["type" /* Constant.Type */], maskInput, metadata);
2589
+ var pSelector_1 = parent && parent.selector ? parent.selector[1 /* Selector.Default */] : "" /* Constant.Empty */;
2590
+ var tags = ["STYLE" /* Constant.StyleTag */, "TITLE" /* Constant.TitleTag */, "svg:style" /* Constant.SvgStyle */];
2591
+ metadata.privacy = tags.includes(pTag) || override.some(function (x) { return pSelector_1.indexOf(x) >= 0; }) ? 0 /* Privacy.None */ : current;
2560
2592
  break;
2561
2593
  case tag === "INPUT" /* Constant.InputTag */ && current === 0 /* Privacy.None */:
2562
2594
  // If even default privacy setting is to not mask, we still scan through input fields for any sensitive information
@@ -2564,11 +2596,13 @@ function privacy(node, value, parent) {
2564
2596
  Object.keys(attributes).forEach(function (x) { return field_1 += attributes[x].toLowerCase(); });
2565
2597
  metadata.privacy = inspect(field_1, maskInput, metadata);
2566
2598
  break;
2567
- case current === 1 /* Privacy.Sensitive */ && tag === "INPUT" /* Constant.InputTag */:
2599
+ case tag === "INPUT" /* Constant.InputTag */ && current === 1 /* Privacy.Sensitive */:
2568
2600
  // Look through class names to aggressively mask content
2569
2601
  metadata.privacy = inspect(attributes["class" /* Constant.Class */], maskText, metadata);
2570
- // If it's a button or an input option, make an exception to disable masking
2571
- metadata.privacy = maskDisable.indexOf(attributes["type" /* Constant.Type */]) >= 0 ? 0 /* Privacy.None */ : current;
2602
+ // If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
2603
+ metadata.privacy = inspect(attributes["type" /* Constant.Type */], maskInput, metadata);
2604
+ // If it's a button or an input option, make an exception to disable masking in sensitive mode
2605
+ metadata.privacy = maskDisable.indexOf(attributes["type" /* Constant.Type */]) >= 0 ? 0 /* Privacy.None */ : metadata.privacy;
2572
2606
  break;
2573
2607
  case current === 1 /* Privacy.Sensitive */:
2574
2608
  // In a mode where we mask sensitive information by default, look through class names to aggressively mask content
@@ -2615,10 +2649,11 @@ function updateSelector(value) {
2615
2649
  var prefix = parent ? parent.selector : null;
2616
2650
  var d = value.data;
2617
2651
  var p = position(parent, value);
2618
- var s = { tag: d.tag, prefix: prefix, position: p, attributes: d.attributes };
2619
- value.selector = [selector(s), selector(s, true)];
2652
+ var s = { id: value.id, tag: d.tag, prefix: prefix, position: p, attributes: d.attributes };
2653
+ value.selector = [get$1(s, 0 /* Selector.Alpha */), get$1(s, 1 /* Selector.Beta */)];
2620
2654
  value.hash = value.selector.map(function (x) { return x ? hash(x) : null; });
2621
2655
  value.hash.forEach(function (h) { return hashMap[h] = value.id; });
2656
+ // Match fragment configuration against both alpha and beta hash
2622
2657
  if (value.hash.some(function (h) { return fragments.indexOf(h) !== -1; })) {
2623
2658
  value.fragment = value.id;
2624
2659
  }
@@ -3162,7 +3197,7 @@ function queue(tokens, transmit) {
3162
3197
  // We enrich the data going out with the existing upload. In these cases, call to upload comes with 'transmit' set to false.
3163
3198
  if (transmit && timeout === null) {
3164
3199
  if (type !== 25 /* Event.Ping */) {
3165
- reset$m();
3200
+ reset$n();
3166
3201
  }
3167
3202
  timeout = setTimeout(upload, gap);
3168
3203
  queuedTime = now;
@@ -3352,8 +3387,8 @@ function delay() {
3352
3387
  return typeof config$1.upload === "string" /* Constant.String */ ? Math.max(Math.min(gap, 30000 /* Setting.MaxUploadDelay */), 100 /* Setting.MinUploadDelay */) : config$1.delay;
3353
3388
  }
3354
3389
  function response(payload) {
3355
- var key = payload && payload.length > 0 ? payload.split(" ")[0] : "" /* Constant.Empty */;
3356
- switch (key) {
3390
+ var parts = payload && payload.length > 0 ? payload.split(" ") : ["" /* Constant.Empty */];
3391
+ switch (parts[0]) {
3357
3392
  case "END" /* Constant.End */:
3358
3393
  // Clear out session storage and end the session so we can start fresh the next time
3359
3394
  trigger(6 /* Check.Server */);
@@ -3362,6 +3397,12 @@ function response(payload) {
3362
3397
  // Upgrade current session to send back playback information
3363
3398
  upgrade("Auto" /* Constant.Auto */);
3364
3399
  break;
3400
+ case "ACTION" /* Constant.Action */:
3401
+ // Invoke action callback, if configured and has a valid value
3402
+ if (config$1.action && parts.length > 1) {
3403
+ config$1.action(parts[1]);
3404
+ }
3405
+ break;
3365
3406
  }
3366
3407
  }
3367
3408
 
@@ -3617,7 +3658,7 @@ function encode$1 (event) {
3617
3658
  tokens.push(b.data.activityTime);
3618
3659
  queue(tokens, false);
3619
3660
  }
3620
- reset$o();
3661
+ reset$p();
3621
3662
  break;
3622
3663
  case 25 /* Event.Ping */:
3623
3664
  tokens.push(data$h.gap);
@@ -3650,7 +3691,7 @@ function encode$1 (event) {
3650
3691
  tokens.push(v);
3651
3692
  tokens.push(data$e[v]);
3652
3693
  }
3653
- reset$k();
3694
+ reset$l();
3654
3695
  queue(tokens, false);
3655
3696
  }
3656
3697
  break;
@@ -3665,7 +3706,7 @@ function encode$1 (event) {
3665
3706
  // However, for data over the wire, we round it off to milliseconds precision.
3666
3707
  tokens.push(Math.round(updates$3[m]));
3667
3708
  }
3668
- reset$n();
3709
+ reset$o();
3669
3710
  queue(tokens, false);
3670
3711
  }
3671
3712
  break;
@@ -3691,7 +3732,7 @@ function encode$1 (event) {
3691
3732
  tokens.push(key);
3692
3733
  tokens.push([].concat.apply([], data$g[e]));
3693
3734
  }
3694
- reset$l();
3735
+ reset$m();
3695
3736
  queue(tokens, false);
3696
3737
  }
3697
3738
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-js",
3
- "version": "0.6.39",
3
+ "version": "0.6.41",
4
4
  "description": "An analytics library that uses web page interactions to generate aggregated insights",
5
5
  "author": "Microsoft Corp.",
6
6
  "license": "MIT",
@@ -15,7 +15,8 @@ let config: Config = {
15
15
  report: null,
16
16
  upload: null,
17
17
  fallback: null,
18
- upgrade: null
18
+ upgrade: null,
19
+ action: null
19
20
  };
20
21
 
21
22
  export default config;
@@ -1,2 +1,2 @@
1
- let version = "0.6.39";
1
+ let version = "0.6.41";
2
2
  export default version;
@@ -242,8 +242,8 @@ function delay(): number {
242
242
  }
243
243
 
244
244
  function response(payload: string): void {
245
- let key = payload && payload.length > 0 ? payload.split(" ")[0] : Constant.Empty;
246
- switch (key) {
245
+ let parts = payload && payload.length > 0 ? payload.split(" ") : [Constant.Empty];
246
+ switch (parts[0]) {
247
247
  case Constant.End:
248
248
  // Clear out session storage and end the session so we can start fresh the next time
249
249
  limit.trigger(Check.Server);
@@ -252,5 +252,9 @@ function response(payload: string): void {
252
252
  // Upgrade current session to send back playback information
253
253
  clarity.upgrade(Constant.Auto);
254
254
  break;
255
+ case Constant.Action:
256
+ // Invoke action callback, if configured and has a valid value
257
+ if (config.action && parts.length > 1) { config.action(parts[1]); }
258
+ break;
255
259
  }
256
260
  }
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as clarity from "./clarity";
2
2
  import hash from "./core/hash";
3
- import selector from "./layout/selector";
3
+ import * as selector from "./layout/selector";
4
4
  import { get, getNode, lookup } from "./layout/dom";
5
5
 
6
6
  const helper = { hash, selector, get, getNode, lookup }
package/src/layout/dom.ts CHANGED
@@ -5,7 +5,7 @@ import config from "@src/core/config";
5
5
  import hash from "@src/core/hash";
6
6
  import * as internal from "@src/diagnostic/internal";
7
7
  import * as region from "@src/layout/region";
8
- import selector from "@src/layout/selector";
8
+ import * as selector from "@src/layout/selector";
9
9
  import * as mutation from "@src/layout/mutation";
10
10
  import * as extract from "@src/data/extract";
11
11
  let index: number = 1;
@@ -50,6 +50,7 @@ function reset(): void {
50
50
  iframeMap = new WeakMap();
51
51
  privacyMap = new WeakMap();
52
52
  fraudMap = new WeakMap();
53
+ selector.reset();
53
54
  }
54
55
 
55
56
  // We parse new root nodes for any regions or masked nodes in the beginning (document) and
@@ -236,14 +237,11 @@ function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
236
237
  metadata.privacy = Privacy.Text;
237
238
  break;
238
239
  case tag === Constant.TextTag:
239
- // If it's a text node belonging to a STYLE or TITLE tag or one of SCRUB_EXCEPTIONS, then capture content
240
+ // If it's a text node belonging to a STYLE or TITLE tag or one of scrub exceptions, then capture content
240
241
  let pTag = parent && parent.data ? parent.data.tag : Constant.Empty;
241
- let pSelector = parent && parent.selector ? parent.selector[Selector.Stable] : Constant.Empty;
242
- metadata.privacy = pTag === Constant.StyleTag || pTag === Constant.TitleTag || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
243
- break;
244
- case Constant.Type in attributes:
245
- // If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
246
- metadata.privacy = inspect(attributes[Constant.Type], maskInput, metadata);
242
+ let pSelector = parent && parent.selector ? parent.selector[Selector.Default] : Constant.Empty;
243
+ let tags : string[] = [Constant.StyleTag, Constant.TitleTag, Constant.SvgStyle];
244
+ metadata.privacy = tags.includes(pTag) || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
247
245
  break;
248
246
  case tag === Constant.InputTag && current === Privacy.None:
249
247
  // If even default privacy setting is to not mask, we still scan through input fields for any sensitive information
@@ -251,11 +249,13 @@ function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
251
249
  Object.keys(attributes).forEach(x => field += attributes[x].toLowerCase());
252
250
  metadata.privacy = inspect(field, maskInput, metadata);
253
251
  break;
254
- case current === Privacy.Sensitive && tag === Constant.InputTag:
252
+ case tag === Constant.InputTag && current === Privacy.Sensitive:
255
253
  // Look through class names to aggressively mask content
256
254
  metadata.privacy = inspect(attributes[Constant.Class], maskText, metadata);
257
- // If it's a button or an input option, make an exception to disable masking
258
- metadata.privacy = maskDisable.indexOf(attributes[Constant.Type]) >= 0 ? Privacy.None : current;
255
+ // If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
256
+ metadata.privacy = inspect(attributes[Constant.Type], maskInput, metadata);
257
+ // If it's a button or an input option, make an exception to disable masking in sensitive mode
258
+ metadata.privacy = maskDisable.indexOf(attributes[Constant.Type]) >= 0 ? Privacy.None : metadata.privacy;
259
259
  break;
260
260
  case current === Privacy.Sensitive:
261
261
  // In a mode where we mask sensitive information by default, look through class names to aggressively mask content
@@ -298,10 +298,11 @@ function updateSelector(value: NodeValue): void {
298
298
  let prefix = parent ? parent.selector : null;
299
299
  let d = value.data;
300
300
  let p = position(parent, value);
301
- let s: SelectorInput = { tag: d.tag, prefix, position: p, attributes: d.attributes };
302
- value.selector = [selector(s), selector(s, true)];
301
+ let s: SelectorInput = { id: value.id, tag: d.tag, prefix, position: p, attributes: d.attributes };
302
+ value.selector = [selector.get(s, Selector.Alpha), selector.get(s, Selector.Beta)];
303
303
  value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
304
304
  value.hash.forEach(h => hashMap[h] = value.id);
305
+ // Match fragment configuration against both alpha and beta hash
305
306
  if (value.hash.some(h => extract.fragments.indexOf(h) !== -1)) {
306
307
  value.fragment = value.id;
307
308
  }
@@ -62,7 +62,7 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
62
62
  if (value.parent && active) { tokens.push(value.parent); }
63
63
  if (value.previous && active) { tokens.push(value.previous); }
64
64
  tokens.push(suspend ? Constant.SuspendMutationTag : data[key]);
65
- if (box && box.length === 2) { tokens.push(`${Constant.Box}${str(box[0])}.${str(box[1])}`); }
65
+ if (box && box.length === 2) { tokens.push(`${Constant.Hash}${str(box[0])}.${str(box[1])}`); }
66
66
  break;
67
67
  case "attributes":
68
68
  for (let attr in data[key]) {
@@ -1,12 +1,17 @@
1
1
  import { Character } from "../../types/data";
2
2
  import { Constant, Selector, SelectorInput } from "../../types/layout";
3
3
 
4
- const TAGS = ["DIV", "TR", "P", "LI", "UL", "A", "BUTTON"];
4
+ const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
5
+ let selectorMap: { [selector: string]: number[] } = {};
5
6
 
6
- export default function(input: SelectorInput, beta: boolean = false): string {
7
+ export function reset(): void {
8
+ selectorMap = {};
9
+ }
10
+
11
+ export function get(input: SelectorInput, type: Selector): string {
7
12
  let a = input.attributes;
8
- let prefix = input.prefix ? input.prefix[beta ? Selector.Beta : Selector.Stable] : null;
9
- let suffix = beta || ((a && !(Constant.Class in a)) || TAGS.indexOf(input.tag) >= 0) ? `:nth-of-type(${input.position})` : Constant.Empty;
13
+ let prefix = input.prefix ? input.prefix[type] : null;
14
+ let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`;
10
15
  switch (input.tag) {
11
16
  case "STYLE":
12
17
  case "TITLE":
@@ -19,22 +24,28 @@ export default function(input: SelectorInput, beta: boolean = false): string {
19
24
  return Constant.HTML;
20
25
  default:
21
26
  if (prefix === null) { return Constant.Empty; }
22
- prefix = `${prefix}>`;
27
+ prefix = `${prefix}${Constant.Separator}`;
23
28
  input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
24
29
  let selector = `${prefix}${input.tag}${suffix}`;
25
- let classes = Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/) : null;
26
- if (beta) {
27
- // In beta mode, update selector to use "id" field when available. There are two exceptions:
28
- // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
29
- // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
30
- let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
31
- classes = input.tag !== Constant.BodyTag && classes ? classes.filter(c => !hasDigits(c)) : [];
32
- selector = classes.length > 0 ? `${prefix}${input.tag}.${classes.join(".")}${suffix}` : selector;
33
- selector = id && hasDigits(id) === false ? `${getDomPrefix(prefix)}#${id}` : selector;
34
- } else {
35
- // Otherwise, fallback to stable mode, where we include class names as part of the selector
36
- selector = classes ? `${prefix}${input.tag}.${classes.join(".")}${suffix}` : selector;
37
- }
30
+ let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
31
+ let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null;
32
+ if (classes && classes.length > 0) {
33
+ if (type === Selector.Alpha) {
34
+ // In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
35
+ // If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
36
+ let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
37
+ if (!(key in selectorMap)) { selectorMap[key] = []; }
38
+ if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); }
39
+ selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
40
+ } else {
41
+ // In Beta mode, we continue to look at query selectors in context of the full page
42
+ selector = `${prefix}${input.tag}.${classes}${suffix}`
43
+ }
44
+ }
45
+ // Update selector to use "id" field when available. There are two exceptions:
46
+ // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
47
+ // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
48
+ selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
38
49
  return selector;
39
50
  }
40
51
  }
@@ -44,19 +55,28 @@ function getDomPrefix(prefix: string): string {
44
55
  const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
45
56
  const domStart = Math.max(shadowDomStart, iframeDomStart);
46
57
 
47
- if (domStart < 0) {
48
- return "";
49
- }
58
+ if (domStart < 0) { return Constant.Empty; }
50
59
 
51
- const domEnd = prefix.indexOf(">", domStart) + 1;
52
- return prefix.substr(0, domEnd);
60
+ return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
61
+ }
62
+
63
+ function getDomPath(input: string): string {
64
+ let parts = input.split(Constant.Separator);
65
+ for (let i = 0; i < parts.length; i++) {
66
+ let tIndex = parts[i].indexOf(Constant.Tilde);
67
+ let dIndex = parts[i].indexOf(Constant.Dot);
68
+ parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
69
+ }
70
+ return parts.join(Constant.Separator);
53
71
  }
54
72
 
55
- // Check if the given input string has digits or not
56
- function hasDigits(value: string): boolean {
73
+ // Check if the given input string has digits or excluded class names
74
+ function filter(value: string): boolean {
75
+ if (!value) { return false; } // Do not process empty strings
76
+ if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; }
57
77
  for (let i = 0; i < value.length; i++) {
58
78
  let c = value.charCodeAt(i);
59
- if (c >= Character.Zero && c <= Character.Nine) { return true };
79
+ if (c >= Character.Zero && c <= Character.Nine) { return false };
60
80
  }
61
- return false;
81
+ return true;
62
82
  }