clarity-js 0.6.32 → 0.6.35
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.js +2080 -2100
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +2080 -2100
- package/package.json +1 -1
- package/src/core/config.ts +2 -2
- package/src/core/history.ts +27 -27
- package/src/core/version.ts +1 -1
- package/src/data/encode.ts +9 -0
- package/src/data/extract.ts +143 -0
- package/src/data/index.ts +3 -1
- package/src/data/metadata.ts +15 -11
- package/src/data/metric.ts +3 -3
- package/src/diagnostic/encode.ts +9 -0
- package/src/diagnostic/fraud.ts +29 -0
- package/src/diagnostic/index.ts +2 -0
- package/src/diagnostic/script.ts +1 -11
- package/src/interaction/click.ts +2 -1
- package/src/interaction/encode.ts +4 -3
- package/src/interaction/input.ts +1 -8
- package/src/layout/dom.ts +105 -90
- package/src/layout/encode.ts +15 -15
- package/src/layout/index.ts +0 -3
- package/src/layout/mutation.ts +32 -44
- package/src/layout/target.ts +7 -4
- package/src/performance/observer.ts +10 -2
- package/types/core.d.ts +20 -13
- package/types/data.d.ts +27 -7
- package/types/diagnostic.d.ts +6 -0
- package/types/interaction.d.ts +1 -0
- package/types/layout.d.ts +8 -6
- package/types/performance.d.ts +0 -7
- package/src/layout/box.ts +0 -83
- package/src/layout/extract.ts +0 -94
package/src/layout/dom.ts
CHANGED
|
@@ -1,31 +1,29 @@
|
|
|
1
1
|
import { Privacy } from "@clarity-types/core";
|
|
2
|
-
import { Code,
|
|
3
|
-
import { Constant, NodeInfo, NodeValue, Selector, SelectorInput, Source } from "@clarity-types/layout";
|
|
2
|
+
import { Code, Severity } from "@clarity-types/data";
|
|
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 hash from "@src/core/hash";
|
|
6
6
|
import * as internal from "@src/diagnostic/internal";
|
|
7
|
-
import * as extract from "@src/layout/extract";
|
|
8
7
|
import * as region from "@src/layout/region";
|
|
9
8
|
import selector from "@src/layout/selector";
|
|
10
|
-
|
|
9
|
+
import * as mutation from "@src/layout/mutation";
|
|
10
|
+
import * as extract from "@src/data/extract";
|
|
11
11
|
let index: number = 1;
|
|
12
|
-
|
|
13
|
-
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#%3Cinput%3E_types
|
|
14
|
-
const DISALLOWED_TYPES = ["password", "hidden", "email", "tel"];
|
|
15
|
-
const DISALLOWED_NAMES = ["addr", "cell", "code", "dob", "email", "mob", "name", "phone", "secret", "social", "ssn", "tel", "zip", "pass", "card", "account", "cvv", "ccv"];
|
|
16
|
-
const DISALLOWED_MATCH = ["address", "password", "contact"];
|
|
17
|
-
|
|
18
12
|
let nodes: Node[] = [];
|
|
19
13
|
let values: NodeValue[] = [];
|
|
20
14
|
let updateMap: number[] = [];
|
|
21
15
|
let hashMap: { [hash: string]: number } = {};
|
|
22
16
|
let override = [];
|
|
23
17
|
let unmask = [];
|
|
18
|
+
let updatedFragments: { [fragment: number]: string } = {};
|
|
19
|
+
let maskText = [];
|
|
20
|
+
let maskDisable = [];
|
|
24
21
|
|
|
25
22
|
// The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
|
|
26
23
|
let idMap: WeakMap<Node, number> = null; // Maps node => id.
|
|
27
24
|
let iframeMap: WeakMap<Document, HTMLIFrameElement> = null; // Maps iframe's contentDocument => parent iframe element
|
|
28
25
|
let privacyMap: WeakMap<Node, Privacy> = null; // Maps node => Privacy (enum)
|
|
26
|
+
let fraudMap: WeakMap<Node, number> = null; // Maps node => FraudId (number)
|
|
29
27
|
|
|
30
28
|
export function start(): void {
|
|
31
29
|
reset();
|
|
@@ -44,9 +42,12 @@ function reset(): void {
|
|
|
44
42
|
hashMap = {};
|
|
45
43
|
override = [];
|
|
46
44
|
unmask = [];
|
|
45
|
+
maskText = Mask.Text.split(Constant.Comma);
|
|
46
|
+
maskDisable = Mask.Disable.split(Constant.Comma);
|
|
47
47
|
idMap = new WeakMap();
|
|
48
48
|
iframeMap = new WeakMap();
|
|
49
49
|
privacyMap = new WeakMap();
|
|
50
|
+
fraudMap = new WeakMap();
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
// We parse new root nodes for any regions or masked nodes in the beginning (document) and
|
|
@@ -61,10 +62,9 @@ export function parse(root: ParentNode, init: boolean = false): void {
|
|
|
61
62
|
// Since mutations may happen on leaf nodes too, e.g. text nodes, which may not support all selector APIs.
|
|
62
63
|
// We ensure that the root note supports querySelectorAll API before executing the code below to identify new regions.
|
|
63
64
|
if ("querySelectorAll" in root) {
|
|
64
|
-
|
|
65
|
-
extract.metrics(root, config.metrics);
|
|
66
|
-
extract.dimensions(root, config.dimensions);
|
|
65
|
+
config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions
|
|
67
66
|
config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
|
|
67
|
+
config.fraud.forEach(x => root.querySelectorAll(x[1]).forEach(e => fraudMap.set(e, x[0]))); // Fraud Check
|
|
68
68
|
unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
|
|
69
69
|
}
|
|
70
70
|
} catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
|
|
@@ -85,20 +85,20 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
85
85
|
let id = getId(node, true);
|
|
86
86
|
let parentId = parent ? getId(parent) : null;
|
|
87
87
|
let previousId = getPreviousId(node);
|
|
88
|
-
let
|
|
89
|
-
let parentValue = null;
|
|
88
|
+
let parentValue: NodeValue = null;
|
|
90
89
|
let regionId = region.exists(node) ? id : null;
|
|
90
|
+
let fragmentId = null;
|
|
91
|
+
let fraudId = fraudMap.has(node) ? fraudMap.get(node) : null;
|
|
92
|
+
let privacyId = config.content ? Privacy.Sensitive : Privacy.Text
|
|
91
93
|
|
|
92
94
|
if (parentId >= 0 && values[parentId]) {
|
|
93
95
|
parentValue = values[parentId];
|
|
94
96
|
parentValue.children.push(id);
|
|
95
97
|
regionId = regionId === null ? parentValue.region : regionId;
|
|
96
|
-
|
|
98
|
+
fragmentId = parentValue.fragment;
|
|
99
|
+
fraudId = fraudId === null ? parentValue.metadata.fraud : fraudId;
|
|
97
100
|
}
|
|
98
101
|
|
|
99
|
-
// Check to see if this particular node should be masked or not
|
|
100
|
-
privacy = getPrivacy(node, data, parentValue, privacy);
|
|
101
|
-
|
|
102
102
|
// If there's an explicit region attribute set on the element, use it to mark a region on the page
|
|
103
103
|
if (data.attributes && Constant.RegionData in data.attributes) {
|
|
104
104
|
region.observe(node, data.attributes[Constant.RegionData]);
|
|
@@ -115,12 +115,14 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
115
115
|
selector: null,
|
|
116
116
|
hash: null,
|
|
117
117
|
region: regionId,
|
|
118
|
-
metadata: { active: true, suspend: false, privacy, position: null, size: null }
|
|
118
|
+
metadata: { active: true, suspend: false, privacy: privacyId, position: null, fraud: fraudId, size: null },
|
|
119
|
+
fragment: fragmentId,
|
|
119
120
|
};
|
|
120
121
|
|
|
122
|
+
privacy(node, values[id], parentValue);
|
|
121
123
|
updateSelector(values[id]);
|
|
122
|
-
size(values[id]
|
|
123
|
-
track(id, source);
|
|
124
|
+
size(values[id]);
|
|
125
|
+
track(id, source, values[id].fragment);
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
export function update(node: Node, parent: Node, data: NodeInfo, source: Source): void {
|
|
@@ -174,9 +176,14 @@ export function update(node: Node, parent: Node, data: NodeInfo, source: Source)
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
// track node if it is a part of scheduled fragment mutation
|
|
180
|
+
if(value.fragment && updatedFragments[value.fragment]) {
|
|
181
|
+
changed = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
177
184
|
// Update selector
|
|
178
185
|
updateSelector(value);
|
|
179
|
-
track(id, source, changed, parentChanged);
|
|
186
|
+
track(id, source, values[id].fragment, changed, parentChanged);
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
|
|
@@ -203,66 +210,63 @@ export function iframe(node: Node): HTMLIFrameElement {
|
|
|
203
210
|
return doc && iframeMap.has(doc) ? iframeMap.get(doc) : null;
|
|
204
211
|
}
|
|
205
212
|
|
|
206
|
-
function
|
|
207
|
-
let
|
|
213
|
+
function privacy(node: Node, value: NodeValue, parent: NodeValue): void {
|
|
214
|
+
let data = value.data;
|
|
215
|
+
let metadata = value.metadata;
|
|
216
|
+
let current = metadata.privacy;
|
|
217
|
+
let attributes = data.attributes || {};
|
|
208
218
|
let tag = data.tag.toUpperCase();
|
|
209
219
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Check for disallowed list of fields (e.g. address, phone, etc.) only if the input node is not already masked
|
|
241
|
-
if (tag === Constant.InputTag) {
|
|
242
|
-
if (privacy === Privacy.None) {
|
|
220
|
+
switch (true) {
|
|
221
|
+
case Constant.MaskData in attributes:
|
|
222
|
+
metadata.privacy = Privacy.TextImage;
|
|
223
|
+
break;
|
|
224
|
+
case Constant.UnmaskData in attributes:
|
|
225
|
+
metadata.privacy = Privacy.None;
|
|
226
|
+
break;
|
|
227
|
+
case privacyMap.has(node):
|
|
228
|
+
// If this node was explicitly configured to contain sensitive content, honor that privacy setting
|
|
229
|
+
metadata.privacy = privacyMap.get(node);
|
|
230
|
+
break;
|
|
231
|
+
case fraudMap.has(node):
|
|
232
|
+
// If this node was explicitly configured to be evaluated for fraud, then also mask content
|
|
233
|
+
metadata.privacy = Privacy.Text;
|
|
234
|
+
break;
|
|
235
|
+
case tag === Constant.TextTag:
|
|
236
|
+
// If it's a text node belonging to a STYLE or TITLE tag or one of SCRUB_EXCEPTIONS, then capture content
|
|
237
|
+
let pTag = parent && parent.data ? parent.data.tag : Constant.Empty;
|
|
238
|
+
let pSelector = parent && parent.selector ? parent.selector[Selector.Stable] : Constant.Empty;
|
|
239
|
+
metadata.privacy = pTag === Constant.StyleTag || pTag === Constant.TitleTag || override.some(x => pSelector.indexOf(x) >= 0) ? Privacy.None : current;
|
|
240
|
+
break;
|
|
241
|
+
case Constant.Type in attributes:
|
|
242
|
+
// If this node has an explicit type assigned to it, go through masking rules to determine right privacy setting
|
|
243
|
+
metadata.privacy = inspect(attributes[Constant.Type], metadata);
|
|
244
|
+
break;
|
|
245
|
+
case tag === Constant.InputTag && current === Privacy.None:
|
|
246
|
+
// If even default privacy setting is to not mask, we still scan through input fields for any sensitive information
|
|
243
247
|
let field: string = Constant.Empty;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
248
|
+
Object.keys(attributes).forEach(x => field += attributes[x].toLowerCase());
|
|
249
|
+
metadata.privacy = inspect(field, metadata);
|
|
250
|
+
break;
|
|
251
|
+
case current === Privacy.Sensitive && tag === Constant.InputTag:
|
|
252
|
+
// If it's a button or an input option, make an exception to disable masking
|
|
253
|
+
metadata.privacy = maskDisable.indexOf(attributes[Constant.Type]) >= 0 ? Privacy.None : current;
|
|
254
|
+
break;
|
|
255
|
+
case current === Privacy.Sensitive:
|
|
256
|
+
// In a mode where we mask sensitive information by default, look through class names to aggressively mask content
|
|
257
|
+
metadata.privacy = inspect(attributes[Constant.Class], metadata);
|
|
258
|
+
break;
|
|
259
|
+
default:
|
|
260
|
+
metadata.privacy = parent ? parent.metadata.privacy : metadata.privacy;
|
|
261
|
+
break;
|
|
256
262
|
}
|
|
263
|
+
}
|
|
257
264
|
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (Constant.UnmaskData in attributes) { privacy = Privacy.None; }
|
|
264
|
-
|
|
265
|
-
return privacy;
|
|
265
|
+
function inspect(input: string, metadata: NodeMeta): Privacy {
|
|
266
|
+
if (input && maskText.some(x => input.indexOf(x) >= 0)) {
|
|
267
|
+
return Privacy.Text;
|
|
268
|
+
}
|
|
269
|
+
return metadata.privacy;
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
function diff(a: NodeInfo, b: NodeInfo, field: string): boolean {
|
|
@@ -296,6 +300,9 @@ function updateSelector(value: NodeValue): void {
|
|
|
296
300
|
value.selector = [selector(s), selector(s, true)];
|
|
297
301
|
value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
|
|
298
302
|
value.hash.forEach(h => hashMap[h] = value.id);
|
|
303
|
+
if (value.hash.some(h => extract.fragments.indexOf(h) !== -1)) {
|
|
304
|
+
value.fragment = value.id;
|
|
305
|
+
}
|
|
299
306
|
}
|
|
300
307
|
|
|
301
308
|
export function getNode(id: number): Node {
|
|
@@ -331,6 +338,11 @@ export function updates(): NodeValue[] {
|
|
|
331
338
|
if (id in values) { output.push(values[id]); }
|
|
332
339
|
}
|
|
333
340
|
updateMap = [];
|
|
341
|
+
for (let id in updatedFragments) {
|
|
342
|
+
extract.update(updatedFragments[id], id, true)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
updatedFragments = {}
|
|
334
346
|
return output;
|
|
335
347
|
}
|
|
336
348
|
|
|
@@ -343,19 +355,10 @@ function remove(id: number, source: Source): void {
|
|
|
343
355
|
}
|
|
344
356
|
}
|
|
345
357
|
|
|
346
|
-
function size(value: NodeValue
|
|
347
|
-
let data = value.data;
|
|
348
|
-
let tag = data.tag;
|
|
349
|
-
|
|
350
|
-
// If this element is a text node, is masked, and longer than configured length, then track box model for the parent element
|
|
351
|
-
let isLongText = tag === Constant.TextTag && data.value && data.value.length > Setting.ResizeObserverThreshold;
|
|
352
|
-
let isMasked = value.metadata.privacy === Privacy.Text || value.metadata.privacy === Privacy.TextImage;
|
|
353
|
-
if (isLongText && isMasked && parent && parent.metadata.size === null) { parent.metadata.size = []; }
|
|
354
|
-
|
|
358
|
+
function size(value: NodeValue): void {
|
|
355
359
|
// If this element is a image node, and is masked, then track box model for the current element
|
|
356
|
-
if (data.tag === Constant.ImageTag && value.metadata.privacy === Privacy.TextImage) { value.metadata.size = []; }
|
|
360
|
+
if (value.data.tag === Constant.ImageTag && value.metadata.privacy === Privacy.TextImage) { value.metadata.size = []; }
|
|
357
361
|
}
|
|
358
|
-
|
|
359
362
|
function getPreviousId(node: Node): number {
|
|
360
363
|
let id = null;
|
|
361
364
|
|
|
@@ -368,7 +371,19 @@ function getPreviousId(node: Node): number {
|
|
|
368
371
|
return id;
|
|
369
372
|
}
|
|
370
373
|
|
|
371
|
-
function track(id: number, source: Source, changed: boolean = true, parentChanged: boolean = false): void {
|
|
374
|
+
function track(id: number, source: Source, fragment: number = null, changed: boolean = true, parentChanged: boolean = false): void {
|
|
375
|
+
// if updated node is a part of fragment and the fragment is not being tracked currently, schedule a mutation on the fragment node
|
|
376
|
+
if (fragment && !updatedFragments[fragment]) {
|
|
377
|
+
let node = getNode(fragment)
|
|
378
|
+
let value = getValue(fragment);
|
|
379
|
+
if (node && value) {
|
|
380
|
+
mutation.schedule(node, true);
|
|
381
|
+
value.hash.forEach(h => {
|
|
382
|
+
if(extract.fragments.indexOf(h) !== -1) { updatedFragments[fragment] = h;}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
372
387
|
// Keep track of the order in which mutations happened, they may not be sequential
|
|
373
388
|
// Edge case: If an element is added later on, and pre-discovered element is moved as a child.
|
|
374
389
|
// In that case, we need to reorder the pre-discovered element in the update list to keep visualization consistent.
|
package/src/layout/encode.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Privacy, Task, Timer } from "@clarity-types/core";
|
|
2
|
-
import { Event, Token } from "@clarity-types/data";
|
|
2
|
+
import { Event, Setting, Token } from "@clarity-types/data";
|
|
3
3
|
import { Constant, NodeInfo, NodeValue } from "@clarity-types/layout";
|
|
4
4
|
import config from "@src/core/config";
|
|
5
5
|
import scrub from "@src/core/scrub";
|
|
@@ -8,7 +8,7 @@ import { time } from "@src/core/time";
|
|
|
8
8
|
import tokenize from "@src/data/token";
|
|
9
9
|
import * as baseline from "@src/data/baseline";
|
|
10
10
|
import { queue } from "@src/data/upload";
|
|
11
|
-
import * as
|
|
11
|
+
import * as fraud from "@src/diagnostic/fraud";
|
|
12
12
|
import * as doc from "./document";
|
|
13
13
|
import * as dom from "./dom";
|
|
14
14
|
import * as region from "./region";
|
|
@@ -35,16 +35,6 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
|
|
|
35
35
|
}
|
|
36
36
|
region.reset();
|
|
37
37
|
break;
|
|
38
|
-
case Event.Box:
|
|
39
|
-
let b = box.data;
|
|
40
|
-
for (let entry of b) {
|
|
41
|
-
tokens.push(entry.id);
|
|
42
|
-
tokens.push(entry.width);
|
|
43
|
-
tokens.push(entry.height);
|
|
44
|
-
}
|
|
45
|
-
box.reset();
|
|
46
|
-
queue(tokens);
|
|
47
|
-
break;
|
|
48
38
|
case Event.Discover:
|
|
49
39
|
case Event.Mutation:
|
|
50
40
|
// Check if we are operating within the context of the current page
|
|
@@ -62,18 +52,17 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
|
|
|
62
52
|
let privacy = value.metadata.privacy;
|
|
63
53
|
let mangle = shouldMangle(value);
|
|
64
54
|
let keys = active ? ["tag", "attributes", "value"] : ["tag"];
|
|
65
|
-
box.compute(value.id);
|
|
66
55
|
for (let key of keys) {
|
|
67
56
|
if (data[key]) {
|
|
68
57
|
switch (key) {
|
|
69
58
|
case "tag":
|
|
70
|
-
let
|
|
59
|
+
let box = size(value);
|
|
71
60
|
let factor = mangle ? -1 : 1;
|
|
72
61
|
tokens.push(value.id * factor);
|
|
73
62
|
if (value.parent && active) { tokens.push(value.parent); }
|
|
74
63
|
if (value.previous && active) { tokens.push(value.previous); }
|
|
75
64
|
tokens.push(suspend ? Constant.SuspendMutationTag : data[key]);
|
|
76
|
-
if (
|
|
65
|
+
if (box && box.length === 2) { tokens.push(`${Constant.Box}${str(box[0])}.${str(box[1])}`); }
|
|
77
66
|
break;
|
|
78
67
|
case "attributes":
|
|
79
68
|
for (let attr in data[key]) {
|
|
@@ -83,6 +72,7 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
|
|
|
83
72
|
}
|
|
84
73
|
break;
|
|
85
74
|
case "value":
|
|
75
|
+
fraud.check(value.metadata.fraud, value.id, data[key]);
|
|
86
76
|
tokens.push(scrub(data[key], data.tag, privacy, mangle));
|
|
87
77
|
break;
|
|
88
78
|
}
|
|
@@ -101,6 +91,16 @@ function shouldMangle(value: NodeValue): boolean {
|
|
|
101
91
|
return value.data.tag === Constant.TextTag && !(privacy === Privacy.None || privacy === Privacy.Sensitive);
|
|
102
92
|
}
|
|
103
93
|
|
|
94
|
+
function size(value: NodeValue): number[] {
|
|
95
|
+
if (value.metadata.size !== null && value.metadata.size.length === 0) {
|
|
96
|
+
let img = dom.getNode(value.id) as HTMLImageElement;
|
|
97
|
+
if (img) {
|
|
98
|
+
return [Math.floor(img.offsetWidth * Setting.BoxPrecision), Math.floor(img.offsetHeight * Setting.BoxPrecision)];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return value.metadata.size;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
104
|
function str(input: number): string {
|
|
105
105
|
return input.toString(36);
|
|
106
106
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as box from "@src/layout/box";
|
|
2
1
|
import * as discover from "@src/layout/discover";
|
|
3
2
|
import * as doc from "@src/layout/document";
|
|
4
3
|
import * as dom from "@src/layout/dom";
|
|
@@ -13,13 +12,11 @@ export function start(): void {
|
|
|
13
12
|
dom.start();
|
|
14
13
|
mutation.start();
|
|
15
14
|
discover.start();
|
|
16
|
-
box.start();
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
export function stop(): void {
|
|
20
18
|
region.stop();
|
|
21
19
|
dom.stop();
|
|
22
20
|
mutation.stop();
|
|
23
|
-
box.stop();
|
|
24
21
|
doc.end();
|
|
25
22
|
}
|
package/src/layout/mutation.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Priority, Task, Timer } from "@clarity-types/core";
|
|
|
2
2
|
import { Code, Event, Metric, Severity } from "@clarity-types/data";
|
|
3
3
|
import { Constant, MutationHistory, MutationQueue, Setting, Source } from "@clarity-types/layout";
|
|
4
4
|
import api from "@src/core/api";
|
|
5
|
+
import * as core from "@src/core";
|
|
5
6
|
import { bind } from "@src/core/event";
|
|
6
7
|
import measure from "@src/core/measure";
|
|
7
8
|
import * as task from "@src/core/task";
|
|
@@ -35,32 +36,38 @@ export function start(): void {
|
|
|
35
36
|
activePeriod = 0;
|
|
36
37
|
history = {};
|
|
37
38
|
|
|
38
|
-
if (insertRule === null) { insertRule = CSSStyleSheet.prototype.insertRule; }
|
|
39
|
-
if (deleteRule === null) { deleteRule = CSSStyleSheet.prototype.deleteRule; }
|
|
40
|
-
if (attachShadow === null) { attachShadow = Element.prototype.attachShadow; }
|
|
41
|
-
|
|
42
39
|
// Some popular open source libraries, like styled-components, optimize performance
|
|
43
40
|
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
44
41
|
// using javascript API is that it doesn't trigger DOM mutation and therefore we
|
|
45
42
|
// need to override the insertRule API and listen for changes manually.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return deleteRule.apply(this, arguments);
|
|
54
|
-
};
|
|
43
|
+
if (insertRule === null) {
|
|
44
|
+
insertRule = CSSStyleSheet.prototype.insertRule;
|
|
45
|
+
CSSStyleSheet.prototype.insertRule = function(): number {
|
|
46
|
+
if (core.active()) { schedule(this.ownerNode); }
|
|
47
|
+
return insertRule.apply(this, arguments);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
51
|
+
if (deleteRule === null) {
|
|
52
|
+
deleteRule = CSSStyleSheet.prototype.deleteRule;
|
|
53
|
+
CSSStyleSheet.prototype.deleteRule = function(): void {
|
|
54
|
+
if (core.active()) { schedule(this.ownerNode); }
|
|
55
|
+
return deleteRule.apply(this, arguments);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add a hook to attachShadow API calls
|
|
60
|
+
// In case we are unable to add a hook and browser throws an exception,
|
|
61
|
+
// reset attachShadow variable and resume processing like before
|
|
62
|
+
if (attachShadow === null) {
|
|
63
|
+
attachShadow = Element.prototype.attachShadow;
|
|
64
|
+
try {
|
|
65
|
+
Element.prototype.attachShadow = function (): ShadowRoot {
|
|
66
|
+
if (core.active()) { return schedule(attachShadow.apply(this, arguments)) as ShadowRoot; }
|
|
67
|
+
else { return attachShadow.apply(this, arguments)}
|
|
68
|
+
}
|
|
69
|
+
} catch { attachShadow = null; }
|
|
70
|
+
}
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
export function observe(node: Node): void {
|
|
@@ -90,25 +97,6 @@ export function monitor(frame: HTMLIFrameElement): void {
|
|
|
90
97
|
export function stop(): void {
|
|
91
98
|
for (let observer of observers) { if (observer) { observer.disconnect(); } }
|
|
92
99
|
observers = [];
|
|
93
|
-
|
|
94
|
-
// Restoring original insertRule
|
|
95
|
-
if (insertRule !== null) {
|
|
96
|
-
CSSStyleSheet.prototype.insertRule = insertRule;
|
|
97
|
-
insertRule = null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Restoring original deleteRule
|
|
101
|
-
if (deleteRule !== null) {
|
|
102
|
-
CSSStyleSheet.prototype.deleteRule = deleteRule;
|
|
103
|
-
deleteRule = null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Restoring original attachShadow
|
|
107
|
-
if (attachShadow != null) {
|
|
108
|
-
Element.prototype.attachShadow = attachShadow;
|
|
109
|
-
attachShadow = null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
100
|
history = {};
|
|
113
101
|
mutations = [];
|
|
114
102
|
queue = [];
|
|
@@ -218,7 +206,7 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer): Pr
|
|
|
218
206
|
}
|
|
219
207
|
}
|
|
220
208
|
|
|
221
|
-
function schedule(node: Node): Node {
|
|
209
|
+
export function schedule(node: Node, fragment: boolean = false): Node {
|
|
222
210
|
// Only schedule manual trigger for this node if it's not already in the queue
|
|
223
211
|
if (queue.indexOf(node) < 0) { queue.push(node); }
|
|
224
212
|
|
|
@@ -226,19 +214,19 @@ function schedule(node: Node): Node {
|
|
|
226
214
|
// It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
|
|
227
215
|
// And in those cases we do not wish to monitor changes multiple times for the same node.
|
|
228
216
|
if (timeout) { clearTimeout(timeout); }
|
|
229
|
-
timeout = setTimeout(trigger, Setting.LookAhead);
|
|
217
|
+
timeout = setTimeout(() => { trigger(fragment) }, Setting.LookAhead);
|
|
230
218
|
|
|
231
219
|
return node;
|
|
232
220
|
}
|
|
233
221
|
|
|
234
|
-
function trigger(): void {
|
|
222
|
+
function trigger(fragment: boolean): void {
|
|
235
223
|
for (let node of queue) {
|
|
236
224
|
// Generate a mutation for this node only if it still exists
|
|
237
225
|
if (node) {
|
|
238
226
|
let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
239
227
|
// Skip re-processing shadowRoot if it was already discovered
|
|
240
228
|
if (shadowRoot && dom.has(node)) { continue; }
|
|
241
|
-
generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
|
|
229
|
+
generate(node, shadowRoot || fragment ? Constant.ChildList : Constant.CharacterData);
|
|
242
230
|
}
|
|
243
231
|
}
|
|
244
232
|
queue = [];
|
package/src/layout/target.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Privacy } from "@clarity-types/core";
|
|
2
2
|
import { Event } from "@clarity-types/data";
|
|
3
3
|
import { TargetMetadata } from "@clarity-types/layout";
|
|
4
|
-
import
|
|
4
|
+
import * as fraud from "@src/diagnostic/fraud";
|
|
5
|
+
import * as region from "@src/layout/region";
|
|
5
6
|
import * as dom from "@src/layout/dom";
|
|
6
7
|
import * as mutation from "@src/layout/mutation";
|
|
7
8
|
|
|
@@ -25,16 +26,18 @@ export function link(node: Node): HTMLAnchorElement {
|
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function metadata(node: Node, event: Event): TargetMetadata {
|
|
29
|
+
export function metadata(node: Node, event: Event, text: string = null): TargetMetadata {
|
|
29
30
|
// If the node is null, we return a reserved value for id: 0. Valid assignment of id begins from 1+.
|
|
30
31
|
let output: TargetMetadata = { id: 0, hash: null, privacy: Privacy.Text, node };
|
|
31
32
|
if (node) {
|
|
32
33
|
let value = dom.get(node);
|
|
33
34
|
if (value !== null) {
|
|
35
|
+
let metadata = value.metadata;
|
|
34
36
|
output.id = value.id;
|
|
35
37
|
output.hash = value.hash;
|
|
36
|
-
output.privacy =
|
|
37
|
-
if (value.region) { track(value.region, event); }
|
|
38
|
+
output.privacy = metadata.privacy;
|
|
39
|
+
if (value.region) { region.track(value.region, event); }
|
|
40
|
+
if (metadata.fraud) { fraud.check(metadata.fraud, value.id, text || value.data.value); }
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Code, Constant, Dimension, Metric, Severity } from "@clarity-types/data";
|
|
1
|
+
import { Code, Constant, Dimension, Metric, Setting, Severity } from "@clarity-types/data";
|
|
2
|
+
import config from "@src/core/config";
|
|
2
3
|
import { bind } from "@src/core/event";
|
|
3
4
|
import measure from "@src/core/measure";
|
|
4
5
|
import { setTimeout } from "@src/core/timeout";
|
|
@@ -56,7 +57,9 @@ function process(entries: PerformanceEntryList): void {
|
|
|
56
57
|
navigation.compute(entry as PerformanceNavigationTiming);
|
|
57
58
|
break;
|
|
58
59
|
case Constant.Resource:
|
|
59
|
-
|
|
60
|
+
let name = entry.name;
|
|
61
|
+
dimension.log(Dimension.NetworkHosts, host(name));
|
|
62
|
+
if (name === config.upload || name === config.fallback) { metric.max(Metric.UploadTime, entry.duration); }
|
|
60
63
|
break;
|
|
61
64
|
case Constant.LongTask:
|
|
62
65
|
metric.count(Metric.LongTaskCount);
|
|
@@ -73,6 +76,11 @@ function process(entries: PerformanceEntryList): void {
|
|
|
73
76
|
break;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
79
|
+
if (performance && Constant.Memory in performance && performance[Constant.Memory].usedJSHeapSize) {
|
|
80
|
+
// Track consumed memory (MBs) where "memory" API is available
|
|
81
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/Performance/memory
|
|
82
|
+
metric.max(Metric.UsedMemory, Math.abs(performance[Constant.Memory].usedJSHeapSize / Setting.MegaByte));
|
|
83
|
+
}
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
export function stop(): void {
|
package/types/core.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import * as Data from "./data";
|
|
|
3
3
|
type TaskFunction = () => Promise<void>;
|
|
4
4
|
type TaskResolve = () => void;
|
|
5
5
|
type UploadCallback = (data: string) => void;
|
|
6
|
-
type Region = [number /* RegionId */, string /* Query Selector
|
|
7
|
-
type
|
|
8
|
-
type
|
|
6
|
+
type Region = [number /* RegionId */, string /* Query Selector */];
|
|
7
|
+
type Fraud = [number /* FraudId */, string /* Query Selector */];
|
|
8
|
+
export type Extract = ExtractSource /* Extraction Source */ | number /* Extract Id */ | string | string[] /* Hash or Query Selector or String Token */;
|
|
9
9
|
|
|
10
10
|
/* Enum */
|
|
11
11
|
|
|
@@ -21,7 +21,6 @@ export const enum Time {
|
|
|
21
21
|
Day = 24 * 60 * 60 * 1000
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
|
|
25
24
|
export const enum Task {
|
|
26
25
|
Wait = 0,
|
|
27
26
|
Run = 1,
|
|
@@ -32,15 +31,23 @@ export const enum Setting {
|
|
|
32
31
|
LongTask = 30, // 30ms
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
export const enum
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
export const enum ExtractSource {
|
|
35
|
+
Javascript = 0,
|
|
36
|
+
Cookie = 1,
|
|
37
|
+
Text = 2,
|
|
38
|
+
Fragment = 3
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const enum Type {
|
|
42
|
+
Array = 1,
|
|
43
|
+
Object = 2,
|
|
44
|
+
Simple = 3
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
export type Syntax = {
|
|
48
|
+
name: string,
|
|
49
|
+
type: Type,
|
|
50
|
+
condition: string
|
|
44
51
|
}
|
|
45
52
|
|
|
46
53
|
export const enum Privacy {
|
|
@@ -119,9 +126,9 @@ export interface Config {
|
|
|
119
126
|
mask?: string[];
|
|
120
127
|
unmask?: string[];
|
|
121
128
|
regions?: Region[];
|
|
122
|
-
|
|
123
|
-
dimensions?: Dimension[];
|
|
129
|
+
extract?: Extract[];
|
|
124
130
|
cookies?: string[];
|
|
131
|
+
fraud?: Fraud[];
|
|
125
132
|
report?: string;
|
|
126
133
|
upload?: string | UploadCallback;
|
|
127
134
|
fallback?: string;
|