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