clarity-js 0.8.11-beta → 0.8.12

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