clarity-js 0.7.66 → 0.7.67

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.
Files changed (102) hide show
  1. package/README.md +26 -26
  2. package/build/clarity.extended.js +1 -1
  3. package/build/clarity.insight.js +1 -1
  4. package/build/clarity.js +5410 -5372
  5. package/build/clarity.min.js +1 -1
  6. package/build/clarity.module.js +5410 -5372
  7. package/build/clarity.performance.js +1 -1
  8. package/package.json +70 -70
  9. package/rollup.config.ts +98 -98
  10. package/src/clarity.ts +60 -60
  11. package/src/core/api.ts +8 -8
  12. package/src/core/config.ts +32 -32
  13. package/src/core/copy.ts +3 -3
  14. package/src/core/event.ts +26 -26
  15. package/src/core/hash.ts +19 -19
  16. package/src/core/history.ts +73 -73
  17. package/src/core/index.ts +83 -83
  18. package/src/core/measure.ts +19 -19
  19. package/src/core/report.ts +28 -28
  20. package/src/core/scrub.ts +196 -196
  21. package/src/core/task.ts +180 -180
  22. package/src/core/time.ts +19 -19
  23. package/src/core/timeout.ts +10 -10
  24. package/src/core/version.ts +2 -2
  25. package/src/data/baseline.ts +123 -92
  26. package/src/data/compress.ts +31 -31
  27. package/src/data/consent.ts +20 -20
  28. package/src/data/custom.ts +23 -23
  29. package/src/data/dimension.ts +53 -53
  30. package/src/data/encode.ts +134 -127
  31. package/src/data/envelope.ts +53 -53
  32. package/src/data/extract.ts +204 -204
  33. package/src/data/index.ts +46 -46
  34. package/src/data/limit.ts +44 -44
  35. package/src/data/metadata.ts +356 -356
  36. package/src/data/metric.ts +51 -51
  37. package/src/data/ping.ts +36 -36
  38. package/src/data/signal.ts +30 -30
  39. package/src/data/summary.ts +34 -34
  40. package/src/data/token.ts +39 -39
  41. package/src/data/upgrade.ts +37 -37
  42. package/src/data/upload.ts +283 -283
  43. package/src/data/variable.ts +83 -83
  44. package/src/diagnostic/encode.ts +40 -40
  45. package/src/diagnostic/fraud.ts +36 -36
  46. package/src/diagnostic/index.ts +15 -15
  47. package/src/diagnostic/internal.ts +28 -28
  48. package/src/diagnostic/script.ts +37 -37
  49. package/src/global.ts +6 -6
  50. package/src/index.ts +9 -9
  51. package/src/insight/blank.ts +14 -14
  52. package/src/insight/encode.ts +60 -60
  53. package/src/insight/snapshot.ts +114 -114
  54. package/src/interaction/change.ts +40 -40
  55. package/src/interaction/click.ts +156 -156
  56. package/src/interaction/clipboard.ts +34 -34
  57. package/src/interaction/encode.ts +191 -191
  58. package/src/interaction/index.ts +59 -59
  59. package/src/interaction/input.ts +59 -59
  60. package/src/interaction/pointer.ts +139 -139
  61. package/src/interaction/resize.ts +47 -47
  62. package/src/interaction/scroll.ts +124 -124
  63. package/src/interaction/selection.ts +68 -68
  64. package/src/interaction/submit.ts +32 -32
  65. package/src/interaction/timeline.ts +65 -65
  66. package/src/interaction/unload.ts +28 -28
  67. package/src/interaction/visibility.ts +26 -26
  68. package/src/layout/animation.ts +133 -133
  69. package/src/layout/discover.ts +31 -31
  70. package/src/layout/document.ts +48 -48
  71. package/src/layout/dom.ts +414 -414
  72. package/src/layout/encode.ts +145 -145
  73. package/src/layout/index.ts +41 -41
  74. package/src/layout/mutation.ts +340 -340
  75. package/src/layout/node.ts +264 -264
  76. package/src/layout/offset.ts +19 -19
  77. package/src/layout/region.ts +153 -153
  78. package/src/layout/schema.ts +63 -63
  79. package/src/layout/selector.ts +82 -82
  80. package/src/layout/style.ts +149 -149
  81. package/src/layout/target.ts +32 -32
  82. package/src/layout/traverse.ts +27 -27
  83. package/src/performance/blank.ts +7 -7
  84. package/src/performance/encode.ts +31 -31
  85. package/src/performance/index.ts +14 -14
  86. package/src/performance/interaction.ts +125 -125
  87. package/src/performance/navigation.ts +31 -31
  88. package/src/performance/observer.ts +108 -108
  89. package/src/queue.ts +33 -33
  90. package/test/core.test.ts +139 -139
  91. package/test/helper.ts +162 -162
  92. package/test/html/core.html +27 -27
  93. package/test/tsconfig.test.json +5 -5
  94. package/tsconfig.json +21 -21
  95. package/tslint.json +32 -32
  96. package/types/core.d.ts +151 -151
  97. package/types/data.d.ts +504 -497
  98. package/types/diagnostic.d.ts +24 -24
  99. package/types/index.d.ts +39 -39
  100. package/types/interaction.d.ts +157 -157
  101. package/types/layout.d.ts +273 -273
  102. package/types/performance.d.ts +64 -64
package/src/layout/dom.ts CHANGED
@@ -1,414 +1,414 @@
1
- import { Privacy } from "@clarity-types/core";
2
- import { Code, Setting, Severity } from "@clarity-types/data";
3
- import { Constant, Mask, NodeInfo, NodeMeta, NodeValue, Selector, SelectorInput, Source } from "@clarity-types/layout";
4
- import config from "@src/core/config";
5
- import { bind } from "@src/core/event";
6
- import hash from "@src/core/hash";
7
- import { shortid } from "@src/data/metadata";
8
- import * as internal from "@src/diagnostic/internal";
9
- import * as region from "@src/layout/region";
10
- import * as selector from "@src/layout/selector";
11
- let index: number = 1;
12
- let nodesMap: Map<Number, Node> = null; // Maps id => node to retrieve further node details using id.
13
- let values: NodeValue[] = [];
14
- let updateMap: number[] = [];
15
- let hashMap: { [hash: string]: number } = {};
16
- let override = [];
17
- let unmask = [];
18
- let maskText = [];
19
- let maskExclude = [];
20
- let maskDisable = [];
21
- let maskTags = [];
22
-
23
- // The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
24
- let idMap: WeakMap<Node, number> = null; // Maps node => id.
25
- let iframeMap: WeakMap<Document, HTMLIFrameElement> = null; // Maps iframe's contentDocument => parent iframe element
26
- let privacyMap: WeakMap<Node, Privacy> = null; // Maps node => Privacy (enum)
27
- let fraudMap: WeakMap<Node, number> = null; // Maps node => FraudId (number)
28
-
29
- export function start(): void {
30
- reset();
31
- parse(document, true);
32
- }
33
-
34
- export function stop(): void {
35
- reset();
36
- }
37
-
38
- function reset(): void {
39
- index = 1;
40
- values = [];
41
- updateMap = [];
42
- hashMap = {};
43
- override = [];
44
- unmask = [];
45
- maskText = Mask.Text.split(Constant.Comma);
46
- maskExclude = Mask.Exclude.split(Constant.Comma);
47
- maskDisable = Mask.Disable.split(Constant.Comma);
48
- maskTags = Mask.Tags.split(Constant.Comma);
49
- nodesMap = new Map();
50
- idMap = new WeakMap();
51
- iframeMap = new WeakMap();
52
- privacyMap = new WeakMap();
53
- fraudMap = new WeakMap();
54
- selector.reset();
55
- }
56
-
57
- // We parse new root nodes for any regions or masked nodes in the beginning (document) and
58
- // later whenever there are new additions or modifications to DOM (mutations)
59
- export function parse(root: ParentNode, init: boolean = false): void {
60
- // Wrap selectors in a try / catch block.
61
- // It's possible for script to receive invalid selectors, e.g. "'#id'" with extra quotes, and cause the code below to fail
62
- try {
63
- // Parse unmask configuration into separate query selectors and override tokens as part of initialization
64
- if (init) { config.unmask.forEach(x => x.indexOf(Constant.Bang) < 0 ? unmask.push(x) : override.push(x.substr(1))); }
65
-
66
- // Since mutations may happen on leaf nodes too, e.g. text nodes, which may not support all selector APIs.
67
- // We ensure that the root note supports querySelectorAll API before executing the code below to identify new regions.
68
- if ("querySelectorAll" in root) {
69
- config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions
70
- config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
71
- config.checksum.forEach(x => root.querySelectorAll(x[1]).forEach(e => fraudMap.set(e, x[0]))); // Fraud Checksum Check
72
- unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
73
- }
74
- } catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
75
- }
76
-
77
- export function getId(node: Node, autogen: boolean = false): number {
78
- if (node === null) { return null; }
79
- let id = idMap.get(node);
80
- if (!id && autogen) {
81
- id = index++;
82
- idMap.set(node, id);
83
- }
84
-
85
- return id ? id : null;
86
- }
87
-
88
- export function add(node: Node, parent: Node, data: NodeInfo, source: Source): void {
89
- let parentId = parent ? getId(parent) : null;
90
-
91
- // Do not add detached nodes
92
- if ((!parent || !parentId) && (node as ShadowRoot).host == null && node.nodeType !== Node.DOCUMENT_TYPE_NODE) {
93
- return;
94
- }
95
-
96
- let id = getId(node, true);
97
- let previousId = getPreviousId(node);
98
- let parentValue: NodeValue = null;
99
- let regionId = region.exists(node) ? id : null;
100
- let fraudId = fraudMap.has(node) ? fraudMap.get(node) : null;
101
- let privacyId = config.content ? Privacy.Sensitive : Privacy.TextImage
102
- if (parentId >= 0 && values[parentId]) {
103
- parentValue = values[parentId];
104
- parentValue.children.push(id);
105
- regionId = regionId === null ? parentValue.region : regionId;
106
- fraudId = fraudId === null ? parentValue.metadata.fraud : fraudId;
107
- privacyId = parentValue.metadata.privacy;
108
- }
109
-
110
- // If there's an explicit region attribute set on the element, use it to mark a region on the page
111
- if (data.attributes && Constant.RegionData in data.attributes) {
112
- region.observe(node, data.attributes[Constant.RegionData]);
113
- regionId = id;
114
- }
115
-
116
- nodesMap.set(id, node);
117
- values[id] = {
118
- id,
119
- parent: parentId,
120
- previous: previousId,
121
- children: [],
122
- data,
123
- selector: null,
124
- hash: null,
125
- region: regionId,
126
- metadata: { active: true, suspend: false, privacy: privacyId, position: null, fraud: fraudId, size: null },
127
- };
128
-
129
- privacy(node, values[id], parentValue);
130
- updateSelector(values[id]);
131
- updateImageSize(values[id]);
132
- track(id, source);
133
- }
134
-
135
- export function update(node: Node, parent: Node, data: NodeInfo, source: Source): void {
136
- let id = getId(node);
137
- let parentId = parent ? getId(parent) : null;
138
- let previousId = getPreviousId(node);
139
- let changed = false;
140
- let parentChanged = false;
141
-
142
- if (id in values) {
143
- let value = values[id];
144
- value.metadata.active = true;
145
-
146
- // Handle case where internal ordering may have changed
147
- if (value.previous !== previousId) {
148
- changed = true;
149
- value.previous = previousId;
150
- }
151
-
152
- // Handle case where parent might have been updated
153
- if (value.parent !== parentId) {
154
- changed = true;
155
- let oldParentId = value.parent;
156
- value.parent = parentId;
157
- // Move this node to the right location under new parent
158
- if (parentId !== null && parentId >= 0) {
159
- let childIndex = previousId === null ? 0 : values[parentId].children.indexOf(previousId) + 1;
160
- values[parentId].children.splice(childIndex, 0, id);
161
- // Update region after the move
162
- value.region = region.exists(node) ? id : values[parentId].region;
163
- } else {
164
- // Mark this element as deleted if the parent has been updated to null
165
- remove(id, source);
166
- }
167
-
168
- // Remove reference to this node from the old parent
169
- if (oldParentId !== null && oldParentId >= 0) {
170
- let nodeIndex = values[oldParentId].children.indexOf(id);
171
- if (nodeIndex >= 0) {
172
- values[oldParentId].children.splice(nodeIndex, 1);
173
- }
174
- }
175
- parentChanged = true;
176
- }
177
-
178
- // Update data
179
- for (let key in data) {
180
- if (diff(value["data"], data, key)) {
181
- changed = true;
182
- value["data"][key] = data[key];
183
- }
184
- }
185
-
186
- // Update selector
187
- updateSelector(value);
188
- track(id, source, changed, parentChanged);
189
- }
190
- }
191
-
192
- export function sameorigin(node: Node): boolean {
193
- let output = false;
194
- if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === Constant.IFrameTag) {
195
- let frame = node as HTMLIFrameElement;
196
- // To determine if the iframe is same-origin or not, we try accessing it's contentDocument.
197
- // If the browser throws an exception, we assume it's cross-origin and move on.
198
- // However, if we do a get a valid document object back, we assume the contents are accessible and iframe is same-origin.
199
- try {
200
- let doc = frame.contentDocument;
201
- if (doc) {
202
- iframeMap.set(frame.contentDocument, frame);
203
- output = true;
204
- }
205
- } catch { /* do nothing */ }
206
- }
207
- return output;
208
- }
209
-
210
- export function iframe(node: Node): HTMLIFrameElement {
211
- let doc = node.nodeType === Node.DOCUMENT_NODE ? node as Document : null;
212
- return doc && iframeMap.has(doc) ? iframeMap.get(doc) : null;
213
- }
214
-
215
- function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
216
- let data = value.data;
217
- let metadata = value.metadata;
218
- let current = metadata.privacy;
219
- let attributes = data.attributes || {};
220
- let tag = data.tag.toUpperCase();
221
-
222
- switch (true) {
223
- case maskTags.indexOf(tag) >= 0:
224
- let type = attributes[Constant.Type];
225
- let meta: string = Constant.Empty;
226
- const excludedPrivacyAttributes = [Constant.Class, Constant.Style]
227
- Object.keys(attributes)
228
- .filter((x) => !excludedPrivacyAttributes.includes(x as Constant))
229
- .forEach((x) => (meta += attributes[x].toLowerCase()));
230
- let exclude = maskExclude.some((x) => meta.indexOf(x) >= 0);
231
- // Regardless of privacy mode, always mask off user input from input boxes or drop downs with two exceptions:
232
- // (1) The node is detected to be one of the excluded fields, in which case we drop everything
233
- // (2) The node's type is one of the allowed types (like checkboxes)
234
- metadata.privacy = tag === Constant.InputTag && maskDisable.indexOf(type) >= 0 ? current : (exclude ? Privacy.Exclude : Privacy.Text);
235
- break;
236
- case Constant.MaskData in attributes:
237
- metadata.privacy = Privacy.TextImage;
238
- break;
239
- case Constant.UnmaskData in attributes:
240
- metadata.privacy = Privacy.None;
241
- break;
242
- case privacyMap.has(node):
243
- // If this node was explicitly configured to contain sensitive content, honor that privacy setting
244
- metadata.privacy = privacyMap.get(node);
245
- break;
246
- case fraudMap.has(node):
247
- // If this node was explicitly configured to be evaluated for fraud, then also mask content
248
- metadata.privacy = Privacy.Text;
249
- break;
250
- case tag === Constant.TextTag:
251
- // If it's a text node belonging to a STYLE or TITLE tag or one of scrub exceptions, then capture content
252
- let pTag = parent && parent.data ? parent.data.tag : Constant.Empty;
253
- let pSelector = parent && parent.selector ? parent.selector[Selector.Default] : Constant.Empty;
254
- let tags: string[] = [Constant.StyleTag, Constant.TitleTag, Constant.SvgStyle];
255
- metadata.privacy = tags.includes(pTag) || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
256
- break;
257
- case current === Privacy.Sensitive:
258
- // In a mode where we mask sensitive information by default, look through class names to aggressively mask content
259
- metadata.privacy = inspect(attributes[Constant.Class], maskText, metadata);
260
- break;
261
- case tag === Constant.ImageTag:
262
- // Mask images with blob src as it is not publicly available anyway.
263
- if(attributes.src?.startsWith('blob:')){
264
- metadata.privacy = Privacy.TextImage;
265
- }
266
- break;
267
- }
268
- }
269
-
270
- function inspect(input: string, lookup: string[], metadata: NodeMeta): Privacy {
271
- if (input && lookup.some(x => input.indexOf(x) >= 0)) {
272
- return Privacy.Text;
273
- }
274
- return metadata.privacy;
275
- }
276
-
277
- function diff(a: NodeInfo, b: NodeInfo, field: string): boolean {
278
- if (typeof a[field] === "object" && typeof b[field] === "object") {
279
- for (let key in a[field]) { if (a[field][key] !== b[field][key]) { return true; } }
280
- for (let key in b[field]) { if (b[field][key] !== a[field][key]) { return true; } }
281
- return false;
282
- }
283
- return a[field] !== b[field];
284
- }
285
-
286
- function position(parent: NodeValue, child: NodeValue): number {
287
- child.metadata.position = 1;
288
- let idx = parent ? parent.children.indexOf(child.id) : -1;
289
- while (idx-- > 0) {
290
- let sibling = values[parent.children[idx]];
291
- if (child.data.tag === sibling.data.tag) {
292
- child.metadata.position = sibling.metadata.position + 1;
293
- break;
294
- }
295
- }
296
- return child.metadata.position;
297
- }
298
-
299
- function updateSelector(value: NodeValue): void {
300
- let parent = value.parent && value.parent in values ? values[value.parent] : null;
301
- let prefix = parent ? parent.selector : null;
302
- let d = value.data;
303
- let p = position(parent, value);
304
- let s: SelectorInput = { id: value.id, tag: d.tag, prefix, position: p, attributes: d.attributes };
305
- value.selector = [selector.get(s, Selector.Alpha), selector.get(s, Selector.Beta)];
306
- value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
307
- value.hash.forEach(h => hashMap[h] = value.id);
308
- }
309
-
310
- export function hashText(hash: string): string {
311
- let id = lookup(hash);
312
- let node = getNode(id);
313
- return node !== null && node.textContent !== null ? node.textContent.substr(0, Setting.ClickText) : '';
314
- }
315
-
316
- export function getNode(id: number): Node {
317
- return nodesMap.has(id) ? nodesMap.get(id) : null;
318
- }
319
-
320
- export function getValue(id: number): NodeValue {
321
- if (id in values) {
322
- return values[id];
323
- }
324
- return null;
325
- }
326
-
327
- export function get(node: Node): NodeValue {
328
- let id = getId(node);
329
- return id in values ? values[id] : null;
330
- }
331
-
332
- export function lookup(hash: string): number {
333
- return hash in hashMap ? hashMap[hash] : null;
334
- }
335
-
336
- export function has(node: Node): boolean {
337
- return nodesMap.has(getId(node));
338
- }
339
-
340
- export function updates(): NodeValue[] {
341
- let output = [];
342
- for (let id of updateMap) {
343
- if (id in values) { output.push(values[id]); }
344
- }
345
- updateMap = [];
346
-
347
- return output;
348
- }
349
-
350
- function remove(id: number, source: Source): void {
351
- if (id in values) {
352
- let value = values[id];
353
- value.metadata.active = false;
354
- value.parent = null;
355
- track(id, source);
356
-
357
- // Clean up node references for removed nodes
358
- removeNodeFromNodesMap(id);
359
- }
360
- }
361
-
362
- function removeNodeFromNodesMap(id: number) {
363
- // Shadow dom roots shouldn't be deleted,
364
- // we should keep listening to the mutations there even they're not rendered in the DOM.
365
- if(nodesMap.get(id).nodeType === Node.DOCUMENT_FRAGMENT_NODE){
366
- return;
367
- }
368
- nodesMap.delete(id);
369
-
370
- let value = id in values ? values[id] : null;
371
- if (value && value.children) {
372
- for (let childId of value.children) {
373
- removeNodeFromNodesMap(childId);
374
- }
375
- }
376
- }
377
-
378
- function updateImageSize(value: NodeValue): void {
379
- // If this element is a image node, and is masked, then track box model for the current element
380
- if (value.data.tag === Constant.ImageTag && value.metadata.privacy === Privacy.TextImage) {
381
- let img = getNode(value.id) as HTMLImageElement;
382
- // We will not capture the natural image dimensions until it loads.
383
- if(img && (!img.complete || img.naturalWidth === 0)){
384
- // This will trigger mutation to update the original width and height after image loads.
385
- bind(img, 'load', () => {
386
- img.setAttribute('data-clarity-loaded', `${shortid()}`);
387
- })
388
- }
389
- value.metadata.size = [];
390
- }
391
- }
392
-
393
- function getPreviousId(node: Node): number {
394
- let id = null;
395
-
396
- // Some nodes may not have an ID by design since Clarity skips over tags like SCRIPT, NOSCRIPT, META, COMMENTS, etc..
397
- // In that case, we keep going back and check for their sibling until we find a sibling with ID or no more sibling nodes are left.
398
- while (id === null && node.previousSibling) {
399
- id = getId(node.previousSibling);
400
- node = node.previousSibling;
401
- }
402
- return id;
403
- }
404
-
405
- function track(id: number, source: Source, changed: boolean = true, parentChanged: boolean = false): void {
406
- // Keep track of the order in which mutations happened, they may not be sequential
407
- // Edge case: If an element is added later on, and pre-discovered element is moved as a child.
408
- // In that case, we need to reorder the pre-discovered element in the update list to keep visualization consistent.
409
- let uIndex = updateMap.indexOf(id);
410
- if (uIndex >= 0 && source === Source.ChildListAdd && parentChanged) {
411
- updateMap.splice(uIndex, 1);
412
- updateMap.push(id);
413
- } else if (uIndex === -1 && changed) { updateMap.push(id); }
414
- }
1
+ import { Privacy } from "@clarity-types/core";
2
+ import { Code, Setting, Severity } from "@clarity-types/data";
3
+ import { Constant, Mask, NodeInfo, NodeMeta, NodeValue, Selector, SelectorInput, Source } from "@clarity-types/layout";
4
+ import config from "@src/core/config";
5
+ import { bind } from "@src/core/event";
6
+ import hash from "@src/core/hash";
7
+ import { shortid } from "@src/data/metadata";
8
+ import * as internal from "@src/diagnostic/internal";
9
+ import * as region from "@src/layout/region";
10
+ import * as selector from "@src/layout/selector";
11
+ let index: number = 1;
12
+ let nodesMap: Map<Number, Node> = null; // Maps id => node to retrieve further node details using id.
13
+ let values: NodeValue[] = [];
14
+ let updateMap: number[] = [];
15
+ let hashMap: { [hash: string]: number } = {};
16
+ let override = [];
17
+ let unmask = [];
18
+ let maskText = [];
19
+ let maskExclude = [];
20
+ let maskDisable = [];
21
+ let maskTags = [];
22
+
23
+ // The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
24
+ let idMap: WeakMap<Node, number> = null; // Maps node => id.
25
+ let iframeMap: WeakMap<Document, HTMLIFrameElement> = null; // Maps iframe's contentDocument => parent iframe element
26
+ let privacyMap: WeakMap<Node, Privacy> = null; // Maps node => Privacy (enum)
27
+ let fraudMap: WeakMap<Node, number> = null; // Maps node => FraudId (number)
28
+
29
+ export function start(): void {
30
+ reset();
31
+ parse(document, true);
32
+ }
33
+
34
+ export function stop(): void {
35
+ reset();
36
+ }
37
+
38
+ function reset(): void {
39
+ index = 1;
40
+ values = [];
41
+ updateMap = [];
42
+ hashMap = {};
43
+ override = [];
44
+ unmask = [];
45
+ maskText = Mask.Text.split(Constant.Comma);
46
+ maskExclude = Mask.Exclude.split(Constant.Comma);
47
+ maskDisable = Mask.Disable.split(Constant.Comma);
48
+ maskTags = Mask.Tags.split(Constant.Comma);
49
+ nodesMap = new Map();
50
+ idMap = new WeakMap();
51
+ iframeMap = new WeakMap();
52
+ privacyMap = new WeakMap();
53
+ fraudMap = new WeakMap();
54
+ selector.reset();
55
+ }
56
+
57
+ // We parse new root nodes for any regions or masked nodes in the beginning (document) and
58
+ // later whenever there are new additions or modifications to DOM (mutations)
59
+ export function parse(root: ParentNode, init: boolean = false): void {
60
+ // Wrap selectors in a try / catch block.
61
+ // It's possible for script to receive invalid selectors, e.g. "'#id'" with extra quotes, and cause the code below to fail
62
+ try {
63
+ // Parse unmask configuration into separate query selectors and override tokens as part of initialization
64
+ if (init) { config.unmask.forEach(x => x.indexOf(Constant.Bang) < 0 ? unmask.push(x) : override.push(x.substr(1))); }
65
+
66
+ // Since mutations may happen on leaf nodes too, e.g. text nodes, which may not support all selector APIs.
67
+ // We ensure that the root note supports querySelectorAll API before executing the code below to identify new regions.
68
+ if ("querySelectorAll" in root) {
69
+ config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions
70
+ config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
71
+ config.checksum.forEach(x => root.querySelectorAll(x[1]).forEach(e => fraudMap.set(e, x[0]))); // Fraud Checksum Check
72
+ unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
73
+ }
74
+ } catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
75
+ }
76
+
77
+ export function getId(node: Node, autogen: boolean = false): number {
78
+ if (node === null) { return null; }
79
+ let id = idMap.get(node);
80
+ if (!id && autogen) {
81
+ id = index++;
82
+ idMap.set(node, id);
83
+ }
84
+
85
+ return id ? id : null;
86
+ }
87
+
88
+ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): void {
89
+ let parentId = parent ? getId(parent) : null;
90
+
91
+ // Do not add detached nodes
92
+ if ((!parent || !parentId) && (node as ShadowRoot).host == null && node.nodeType !== Node.DOCUMENT_TYPE_NODE) {
93
+ return;
94
+ }
95
+
96
+ let id = getId(node, true);
97
+ let previousId = getPreviousId(node);
98
+ let parentValue: NodeValue = null;
99
+ let regionId = region.exists(node) ? id : null;
100
+ let fraudId = fraudMap.has(node) ? fraudMap.get(node) : null;
101
+ let privacyId = config.content ? Privacy.Sensitive : Privacy.TextImage
102
+ if (parentId >= 0 && values[parentId]) {
103
+ parentValue = values[parentId];
104
+ parentValue.children.push(id);
105
+ regionId = regionId === null ? parentValue.region : regionId;
106
+ fraudId = fraudId === null ? parentValue.metadata.fraud : fraudId;
107
+ privacyId = parentValue.metadata.privacy;
108
+ }
109
+
110
+ // If there's an explicit region attribute set on the element, use it to mark a region on the page
111
+ if (data.attributes && Constant.RegionData in data.attributes) {
112
+ region.observe(node, data.attributes[Constant.RegionData]);
113
+ regionId = id;
114
+ }
115
+
116
+ nodesMap.set(id, node);
117
+ values[id] = {
118
+ id,
119
+ parent: parentId,
120
+ previous: previousId,
121
+ children: [],
122
+ data,
123
+ selector: null,
124
+ hash: null,
125
+ region: regionId,
126
+ metadata: { active: true, suspend: false, privacy: privacyId, position: null, fraud: fraudId, size: null },
127
+ };
128
+
129
+ privacy(node, values[id], parentValue);
130
+ updateSelector(values[id]);
131
+ updateImageSize(values[id]);
132
+ track(id, source);
133
+ }
134
+
135
+ export function update(node: Node, parent: Node, data: NodeInfo, source: Source): void {
136
+ let id = getId(node);
137
+ let parentId = parent ? getId(parent) : null;
138
+ let previousId = getPreviousId(node);
139
+ let changed = false;
140
+ let parentChanged = false;
141
+
142
+ if (id in values) {
143
+ let value = values[id];
144
+ value.metadata.active = true;
145
+
146
+ // Handle case where internal ordering may have changed
147
+ if (value.previous !== previousId) {
148
+ changed = true;
149
+ value.previous = previousId;
150
+ }
151
+
152
+ // Handle case where parent might have been updated
153
+ if (value.parent !== parentId) {
154
+ changed = true;
155
+ let oldParentId = value.parent;
156
+ value.parent = parentId;
157
+ // Move this node to the right location under new parent
158
+ if (parentId !== null && parentId >= 0) {
159
+ let childIndex = previousId === null ? 0 : values[parentId].children.indexOf(previousId) + 1;
160
+ values[parentId].children.splice(childIndex, 0, id);
161
+ // Update region after the move
162
+ value.region = region.exists(node) ? id : values[parentId].region;
163
+ } else {
164
+ // Mark this element as deleted if the parent has been updated to null
165
+ remove(id, source);
166
+ }
167
+
168
+ // Remove reference to this node from the old parent
169
+ if (oldParentId !== null && oldParentId >= 0) {
170
+ let nodeIndex = values[oldParentId].children.indexOf(id);
171
+ if (nodeIndex >= 0) {
172
+ values[oldParentId].children.splice(nodeIndex, 1);
173
+ }
174
+ }
175
+ parentChanged = true;
176
+ }
177
+
178
+ // Update data
179
+ for (let key in data) {
180
+ if (diff(value["data"], data, key)) {
181
+ changed = true;
182
+ value["data"][key] = data[key];
183
+ }
184
+ }
185
+
186
+ // Update selector
187
+ updateSelector(value);
188
+ track(id, source, changed, parentChanged);
189
+ }
190
+ }
191
+
192
+ export function sameorigin(node: Node): boolean {
193
+ let output = false;
194
+ if (node.nodeType === Node.ELEMENT_NODE && (node as HTMLElement).tagName === Constant.IFrameTag) {
195
+ let frame = node as HTMLIFrameElement;
196
+ // To determine if the iframe is same-origin or not, we try accessing it's contentDocument.
197
+ // If the browser throws an exception, we assume it's cross-origin and move on.
198
+ // However, if we do a get a valid document object back, we assume the contents are accessible and iframe is same-origin.
199
+ try {
200
+ let doc = frame.contentDocument;
201
+ if (doc) {
202
+ iframeMap.set(frame.contentDocument, frame);
203
+ output = true;
204
+ }
205
+ } catch { /* do nothing */ }
206
+ }
207
+ return output;
208
+ }
209
+
210
+ export function iframe(node: Node): HTMLIFrameElement {
211
+ let doc = node.nodeType === Node.DOCUMENT_NODE ? node as Document : null;
212
+ return doc && iframeMap.has(doc) ? iframeMap.get(doc) : null;
213
+ }
214
+
215
+ function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
216
+ let data = value.data;
217
+ let metadata = value.metadata;
218
+ let current = metadata.privacy;
219
+ let attributes = data.attributes || {};
220
+ let tag = data.tag.toUpperCase();
221
+
222
+ switch (true) {
223
+ case maskTags.indexOf(tag) >= 0:
224
+ let type = attributes[Constant.Type];
225
+ let meta: string = Constant.Empty;
226
+ const excludedPrivacyAttributes = [Constant.Class, Constant.Style]
227
+ Object.keys(attributes)
228
+ .filter((x) => !excludedPrivacyAttributes.includes(x as Constant))
229
+ .forEach((x) => (meta += attributes[x].toLowerCase()));
230
+ let exclude = maskExclude.some((x) => meta.indexOf(x) >= 0);
231
+ // Regardless of privacy mode, always mask off user input from input boxes or drop downs with two exceptions:
232
+ // (1) The node is detected to be one of the excluded fields, in which case we drop everything
233
+ // (2) The node's type is one of the allowed types (like checkboxes)
234
+ metadata.privacy = tag === Constant.InputTag && maskDisable.indexOf(type) >= 0 ? current : (exclude ? Privacy.Exclude : Privacy.Text);
235
+ break;
236
+ case Constant.MaskData in attributes:
237
+ metadata.privacy = Privacy.TextImage;
238
+ break;
239
+ case Constant.UnmaskData in attributes:
240
+ metadata.privacy = Privacy.None;
241
+ break;
242
+ case privacyMap.has(node):
243
+ // If this node was explicitly configured to contain sensitive content, honor that privacy setting
244
+ metadata.privacy = privacyMap.get(node);
245
+ break;
246
+ case fraudMap.has(node):
247
+ // If this node was explicitly configured to be evaluated for fraud, then also mask content
248
+ metadata.privacy = Privacy.Text;
249
+ break;
250
+ case tag === Constant.TextTag:
251
+ // If it's a text node belonging to a STYLE or TITLE tag or one of scrub exceptions, then capture content
252
+ let pTag = parent && parent.data ? parent.data.tag : Constant.Empty;
253
+ let pSelector = parent && parent.selector ? parent.selector[Selector.Default] : Constant.Empty;
254
+ let tags: string[] = [Constant.StyleTag, Constant.TitleTag, Constant.SvgStyle];
255
+ metadata.privacy = tags.includes(pTag) || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
256
+ break;
257
+ case current === Privacy.Sensitive:
258
+ // In a mode where we mask sensitive information by default, look through class names to aggressively mask content
259
+ metadata.privacy = inspect(attributes[Constant.Class], maskText, metadata);
260
+ break;
261
+ case tag === Constant.ImageTag:
262
+ // Mask images with blob src as it is not publicly available anyway.
263
+ if(attributes.src?.startsWith('blob:')){
264
+ metadata.privacy = Privacy.TextImage;
265
+ }
266
+ break;
267
+ }
268
+ }
269
+
270
+ function inspect(input: string, lookup: string[], metadata: NodeMeta): Privacy {
271
+ if (input && lookup.some(x => input.indexOf(x) >= 0)) {
272
+ return Privacy.Text;
273
+ }
274
+ return metadata.privacy;
275
+ }
276
+
277
+ function diff(a: NodeInfo, b: NodeInfo, field: string): boolean {
278
+ if (typeof a[field] === "object" && typeof b[field] === "object") {
279
+ for (let key in a[field]) { if (a[field][key] !== b[field][key]) { return true; } }
280
+ for (let key in b[field]) { if (b[field][key] !== a[field][key]) { return true; } }
281
+ return false;
282
+ }
283
+ return a[field] !== b[field];
284
+ }
285
+
286
+ function position(parent: NodeValue, child: NodeValue): number {
287
+ child.metadata.position = 1;
288
+ let idx = parent ? parent.children.indexOf(child.id) : -1;
289
+ while (idx-- > 0) {
290
+ let sibling = values[parent.children[idx]];
291
+ if (child.data.tag === sibling.data.tag) {
292
+ child.metadata.position = sibling.metadata.position + 1;
293
+ break;
294
+ }
295
+ }
296
+ return child.metadata.position;
297
+ }
298
+
299
+ function updateSelector(value: NodeValue): void {
300
+ let parent = value.parent && value.parent in values ? values[value.parent] : null;
301
+ let prefix = parent ? parent.selector : null;
302
+ let d = value.data;
303
+ let p = position(parent, value);
304
+ let s: SelectorInput = { id: value.id, tag: d.tag, prefix, position: p, attributes: d.attributes };
305
+ value.selector = [selector.get(s, Selector.Alpha), selector.get(s, Selector.Beta)];
306
+ value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
307
+ value.hash.forEach(h => hashMap[h] = value.id);
308
+ }
309
+
310
+ export function hashText(hash: string): string {
311
+ let id = lookup(hash);
312
+ let node = getNode(id);
313
+ return node !== null && node.textContent !== null ? node.textContent.substr(0, Setting.ClickText) : '';
314
+ }
315
+
316
+ export function getNode(id: number): Node {
317
+ return nodesMap.has(id) ? nodesMap.get(id) : null;
318
+ }
319
+
320
+ export function getValue(id: number): NodeValue {
321
+ if (id in values) {
322
+ return values[id];
323
+ }
324
+ return null;
325
+ }
326
+
327
+ export function get(node: Node): NodeValue {
328
+ let id = getId(node);
329
+ return id in values ? values[id] : null;
330
+ }
331
+
332
+ export function lookup(hash: string): number {
333
+ return hash in hashMap ? hashMap[hash] : null;
334
+ }
335
+
336
+ export function has(node: Node): boolean {
337
+ return nodesMap.has(getId(node));
338
+ }
339
+
340
+ export function updates(): NodeValue[] {
341
+ let output = [];
342
+ for (let id of updateMap) {
343
+ if (id in values) { output.push(values[id]); }
344
+ }
345
+ updateMap = [];
346
+
347
+ return output;
348
+ }
349
+
350
+ function remove(id: number, source: Source): void {
351
+ if (id in values) {
352
+ let value = values[id];
353
+ value.metadata.active = false;
354
+ value.parent = null;
355
+ track(id, source);
356
+
357
+ // Clean up node references for removed nodes
358
+ removeNodeFromNodesMap(id);
359
+ }
360
+ }
361
+
362
+ function removeNodeFromNodesMap(id: number) {
363
+ // Shadow dom roots shouldn't be deleted,
364
+ // we should keep listening to the mutations there even they're not rendered in the DOM.
365
+ if(nodesMap.get(id).nodeType === Node.DOCUMENT_FRAGMENT_NODE){
366
+ return;
367
+ }
368
+ nodesMap.delete(id);
369
+
370
+ let value = id in values ? values[id] : null;
371
+ if (value && value.children) {
372
+ for (let childId of value.children) {
373
+ removeNodeFromNodesMap(childId);
374
+ }
375
+ }
376
+ }
377
+
378
+ function updateImageSize(value: NodeValue): void {
379
+ // If this element is a image node, and is masked, then track box model for the current element
380
+ if (value.data.tag === Constant.ImageTag && value.metadata.privacy === Privacy.TextImage) {
381
+ let img = getNode(value.id) as HTMLImageElement;
382
+ // We will not capture the natural image dimensions until it loads.
383
+ if(img && (!img.complete || img.naturalWidth === 0)){
384
+ // This will trigger mutation to update the original width and height after image loads.
385
+ bind(img, 'load', () => {
386
+ img.setAttribute('data-clarity-loaded', `${shortid()}`);
387
+ })
388
+ }
389
+ value.metadata.size = [];
390
+ }
391
+ }
392
+
393
+ function getPreviousId(node: Node): number {
394
+ let id = null;
395
+
396
+ // Some nodes may not have an ID by design since Clarity skips over tags like SCRIPT, NOSCRIPT, META, COMMENTS, etc..
397
+ // In that case, we keep going back and check for their sibling until we find a sibling with ID or no more sibling nodes are left.
398
+ while (id === null && node.previousSibling) {
399
+ id = getId(node.previousSibling);
400
+ node = node.previousSibling;
401
+ }
402
+ return id;
403
+ }
404
+
405
+ function track(id: number, source: Source, changed: boolean = true, parentChanged: boolean = false): void {
406
+ // Keep track of the order in which mutations happened, they may not be sequential
407
+ // Edge case: If an element is added later on, and pre-discovered element is moved as a child.
408
+ // In that case, we need to reorder the pre-discovered element in the update list to keep visualization consistent.
409
+ let uIndex = updateMap.indexOf(id);
410
+ if (uIndex >= 0 && source === Source.ChildListAdd && parentChanged) {
411
+ updateMap.splice(uIndex, 1);
412
+ updateMap.push(id);
413
+ } else if (uIndex === -1 && changed) { updateMap.push(id); }
414
+ }