clarity-js 0.8.10-beta → 0.8.10
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/.lintstagedrc.yml +3 -0
- package/biome.json +43 -0
- package/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +3581 -3019
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +3581 -3019
- package/build/clarity.performance.js +1 -1
- package/package.json +17 -10
- package/rollup.config.ts +84 -88
- package/src/clarity.ts +34 -28
- package/src/core/config.ts +2 -2
- package/src/core/event.ts +36 -32
- package/src/core/hash.ts +5 -6
- package/src/core/history.ts +10 -11
- package/src/core/index.ts +21 -11
- package/src/core/measure.ts +9 -5
- package/src/core/report.ts +9 -5
- package/src/core/scrub.ts +29 -20
- package/src/core/task.ts +73 -45
- 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 +60 -55
- package/src/data/consent.ts +2 -2
- package/src/data/custom.ts +8 -13
- package/src/data/dimension.ts +11 -7
- package/src/data/encode.ts +36 -30
- package/src/data/envelope.ts +38 -38
- package/src/data/extract.ts +86 -77
- package/src/data/index.ts +10 -6
- package/src/data/limit.ts +1 -1
- package/src/data/metadata.ts +305 -266
- package/src/data/metric.ts +18 -8
- package/src/data/ping.ts +8 -4
- package/src/data/signal.ts +18 -18
- package/src/data/summary.ts +6 -4
- package/src/data/token.ts +10 -8
- package/src/data/upgrade.ts +7 -3
- package/src/data/upload.ts +100 -49
- package/src/data/variable.ts +27 -20
- package/src/diagnostic/encode.ts +2 -2
- package/src/diagnostic/fraud.ts +3 -4
- package/src/diagnostic/internal.ts +11 -5
- package/src/diagnostic/script.ts +12 -8
- package/src/global.ts +1 -1
- package/src/insight/blank.ts +4 -4
- package/src/insight/encode.ts +23 -17
- package/src/insight/snapshot.ts +57 -37
- package/src/interaction/change.ts +9 -6
- package/src/interaction/click.ts +34 -28
- package/src/interaction/clipboard.ts +2 -2
- package/src/interaction/encode.ts +35 -31
- package/src/interaction/input.ts +11 -9
- package/src/interaction/pointer.ts +41 -30
- package/src/interaction/resize.ts +5 -5
- package/src/interaction/scroll.ts +20 -17
- package/src/interaction/selection.ts +12 -8
- package/src/interaction/submit.ts +2 -2
- package/src/interaction/timeline.ts +13 -9
- package/src/interaction/unload.ts +1 -1
- package/src/interaction/visibility.ts +2 -2
- package/src/layout/animation.ts +47 -41
- package/src/layout/discover.ts +5 -5
- package/src/layout/document.ts +31 -19
- package/src/layout/dom.ts +141 -91
- package/src/layout/encode.ts +52 -37
- package/src/layout/mutation.ts +321 -318
- package/src/layout/node.ts +104 -81
- package/src/layout/offset.ts +7 -6
- package/src/layout/region.ts +66 -43
- package/src/layout/schema.ts +15 -8
- package/src/layout/selector.ts +47 -25
- package/src/layout/style.ts +44 -36
- package/src/layout/target.ts +14 -10
- package/src/layout/traverse.ts +17 -11
- package/src/performance/blank.ts +1 -1
- package/src/performance/encode.ts +4 -4
- package/src/performance/interaction.ts +58 -70
- package/src/performance/navigation.ts +2 -2
- package/src/performance/observer.ts +59 -26
- package/src/queue.ts +16 -9
- package/tsconfig.json +2 -2
- package/tslint.json +25 -32
- package/types/core.d.ts +13 -13
- package/types/data.d.ts +32 -29
- package/types/diagnostic.d.ts +1 -1
- package/types/interaction.d.ts +5 -4
- package/types/layout.d.ts +36 -21
- 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
|
|
13
|
-
let nodesMap: Map<
|
|
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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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) {
|
|
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
|
|
81
|
-
if (node === 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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
183
|
-
if (diff(value
|
|
210
|
+
for (const key in data) {
|
|
211
|
+
if (diff(value.data, data, key)) {
|
|
184
212
|
changed = true;
|
|
185
|
-
value
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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 =
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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(
|
|
312
|
+
if (attributes.src?.startsWith("blob:")) {
|
|
280
313
|
metadata.privacy = Privacy.TextImage;
|
|
281
314
|
}
|
|
282
|
-
|
|
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 (
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
358
|
-
for (
|
|
359
|
-
if (id in values) {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
394
|
-
if (value
|
|
395
|
-
for (
|
|
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
|
-
|
|
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,
|
|
409
|
-
img.setAttribute(
|
|
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(
|
|
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
|
|
429
|
-
|
|
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
|
-
|
|
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) {
|
|
486
|
+
} else if (uIndex === -1 && changed) {
|
|
487
|
+
updateMap.push(id);
|
|
488
|
+
}
|
|
439
489
|
}
|
package/src/layout/encode.ts
CHANGED
|
@@ -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
|
-
|
|
19
|
+
const eventTime = ts || time();
|
|
20
20
|
let tokens: Token[] = [eventTime, type];
|
|
21
21
|
switch (type) {
|
|
22
|
-
case Event.Document:
|
|
23
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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) {
|
|
75
|
-
|
|
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 (
|
|
81
|
+
for (const value of values) {
|
|
79
82
|
let state = task.state(timer);
|
|
80
|
-
if (state === Task.Wait) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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) {
|
|
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) {
|
|
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 (
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|