clarity-js 0.8.12 → 0.8.13-beta
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 +3952 -4179
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +3952 -4179
- package/build/clarity.performance.js +1 -1
- package/package.json +69 -76
- package/rollup.config.ts +88 -84
- package/src/clarity.ts +28 -34
- 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 +3 -3
- package/src/core/scrub.ts +20 -29
- 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 +2 -2
- package/src/data/custom.ts +13 -8
- package/src/data/dimension.ts +7 -11
- package/src/data/encode.ts +30 -36
- package/src/data/envelope.ts +38 -38
- package/src/data/extract.ts +77 -86
- package/src/data/index.ts +6 -10
- package/src/data/limit.ts +1 -1
- package/src/data/metadata.ts +266 -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 +24 -35
- package/src/interaction/resize.ts +5 -5
- package/src/interaction/scroll.ts +11 -14
- 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 +40 -60
- package/src/layout/schema.ts +8 -15
- package/src/layout/selector.ts +25 -47
- package/src/layout/style.ts +36 -44
- 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 +29 -33
- package/types/diagnostic.d.ts +1 -1
- package/types/interaction.d.ts +4 -4
- 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/selector.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Character } from "../../types/data";
|
|
2
|
-
import { Constant, Selector,
|
|
2
|
+
import { Constant, Selector, SelectorInput } from "../../types/layout";
|
|
3
3
|
|
|
4
4
|
const excludeClassNames = Constant.ExcludeClassNames.split(Constant.Comma);
|
|
5
5
|
let selectorMap: { [selector: string]: number[] } = {};
|
|
@@ -9,9 +9,9 @@ export function reset(): void {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function get(input: SelectorInput, type: Selector): string {
|
|
12
|
-
|
|
12
|
+
let a = input.attributes;
|
|
13
13
|
let prefix = input.prefix ? input.prefix[type] : null;
|
|
14
|
-
|
|
14
|
+
let suffix = type === Selector.Alpha ? `${Constant.Tilde}${input.position-1}` : `:nth-of-type(${input.position})`;
|
|
15
15
|
switch (input.tag) {
|
|
16
16
|
case "STYLE":
|
|
17
17
|
case "TITLE":
|
|
@@ -22,37 +22,24 @@ export function get(input: SelectorInput, type: Selector): string {
|
|
|
22
22
|
return Constant.Empty;
|
|
23
23
|
case "HTML":
|
|
24
24
|
return Constant.HTML;
|
|
25
|
-
default:
|
|
26
|
-
if (prefix === null) {
|
|
27
|
-
return Constant.Empty;
|
|
28
|
-
}
|
|
25
|
+
default:
|
|
26
|
+
if (prefix === null) { return Constant.Empty; }
|
|
29
27
|
prefix = `${prefix}${Constant.Separator}`;
|
|
30
28
|
input.tag = input.tag.indexOf(Constant.SvgPrefix) === 0 ? input.tag.substr(Constant.SvgPrefix.length) : input.tag;
|
|
31
29
|
let selector = `${prefix}${input.tag}${suffix}`;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0
|
|
35
|
-
? a[Constant.Class]
|
|
36
|
-
.trim()
|
|
37
|
-
.split(/\s+/)
|
|
38
|
-
.filter((c) => filter(c))
|
|
39
|
-
.join(Constant.Period)
|
|
40
|
-
: null;
|
|
30
|
+
let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
|
|
31
|
+
let classes = input.tag !== Constant.BodyTag && Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/).filter(c => filter(c)).join(Constant.Period) : null;
|
|
41
32
|
if (classes && classes.length > 0) {
|
|
42
33
|
if (type === Selector.Alpha) {
|
|
43
34
|
// In Alpha mode, update selector to use class names, with relative positioning within the parent id container.
|
|
44
35
|
// If the node has valid class name(s) then drop relative positioning within the parent path to keep things simple.
|
|
45
|
-
|
|
46
|
-
if (!(key in selectorMap)) {
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
if (selectorMap[key].indexOf(input.id) < 0) {
|
|
50
|
-
selectorMap[key].push(input.id);
|
|
51
|
-
}
|
|
36
|
+
let key = `${getDomPath(prefix)}${input.tag}${Constant.Dot}${classes}`;
|
|
37
|
+
if (!(key in selectorMap)) { selectorMap[key] = []; }
|
|
38
|
+
if (selectorMap[key].indexOf(input.id) < 0) { selectorMap[key].push(input.id); }
|
|
52
39
|
selector = `${key}${Constant.Tilde}${selectorMap[key].indexOf(input.id)}`;
|
|
53
40
|
} else {
|
|
54
41
|
// In Beta mode, we continue to look at query selectors in context of the full page
|
|
55
|
-
selector = `${prefix}${input.tag}.${classes}${suffix}
|
|
42
|
+
selector = `${prefix}${input.tag}.${classes}${suffix}`
|
|
56
43
|
}
|
|
57
44
|
}
|
|
58
45
|
// Update selector to use "id" field when available. There are two exceptions:
|
|
@@ -60,45 +47,36 @@ export function get(input: SelectorInput, type: Selector): string {
|
|
|
60
47
|
// (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
|
|
61
48
|
selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
|
|
62
49
|
return selector;
|
|
63
|
-
}
|
|
64
50
|
}
|
|
65
51
|
}
|
|
66
52
|
|
|
67
53
|
function getDomPrefix(prefix: string): string {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return Constant.Empty;
|
|
74
|
-
}
|
|
54
|
+
const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
|
|
55
|
+
const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
|
|
56
|
+
const domStart = Math.max(shadowDomStart, iframeDomStart);
|
|
57
|
+
|
|
58
|
+
if (domStart < 0) { return Constant.Empty; }
|
|
75
59
|
|
|
76
|
-
|
|
60
|
+
return prefix.substring(0, prefix.indexOf(Constant.Separator, domStart) + 1);
|
|
77
61
|
}
|
|
78
62
|
|
|
79
63
|
function getDomPath(input: string): string {
|
|
80
|
-
|
|
64
|
+
let parts = input.split(Constant.Separator);
|
|
81
65
|
for (let i = 0; i < parts.length; i++) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : tIndex > 0 ? tIndex : parts[i].length);
|
|
66
|
+
let tIndex = parts[i].indexOf(Constant.Tilde);
|
|
67
|
+
let dIndex = parts[i].indexOf(Constant.Dot);
|
|
68
|
+
parts[i] = parts[i].substring(0, dIndex > 0 ? dIndex : (tIndex > 0 ? tIndex : parts[i].length));
|
|
85
69
|
}
|
|
86
70
|
return parts.join(Constant.Separator);
|
|
87
71
|
}
|
|
88
72
|
|
|
89
73
|
// Check if the given input string has digits or excluded class names
|
|
90
74
|
function filter(value: string): boolean {
|
|
91
|
-
if (!value) {
|
|
92
|
-
|
|
93
|
-
} // Do not process empty strings
|
|
94
|
-
if (excludeClassNames.some((x) => value.toLowerCase().indexOf(x) >= 0)) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
75
|
+
if (!value) { return false; } // Do not process empty strings
|
|
76
|
+
if (excludeClassNames.some(x => value.toLowerCase().indexOf(x) >= 0)) { return false; }
|
|
97
77
|
for (let i = 0; i < value.length; i++) {
|
|
98
|
-
|
|
99
|
-
if (c >= Character.Zero && c <= Character.Nine) {
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
78
|
+
let c = value.charCodeAt(i);
|
|
79
|
+
if (c >= Character.Zero && c <= Character.Nine) { return false };
|
|
102
80
|
}
|
|
103
81
|
return true;
|
|
104
82
|
}
|
package/src/layout/style.ts
CHANGED
|
@@ -1,70 +1,67 @@
|
|
|
1
1
|
import { Event } from "@clarity-types/data";
|
|
2
|
-
import {
|
|
3
|
-
import * as core from "@src/core";
|
|
4
|
-
import config from "@src/core/config";
|
|
2
|
+
import { StyleSheetOperation, StyleSheetState } from "@clarity-types/layout";
|
|
5
3
|
import { time } from "@src/core/time";
|
|
6
4
|
import { shortid } from "@src/data/metadata";
|
|
7
|
-
import { getId } from "@src/layout/dom";
|
|
8
5
|
import encode from "@src/layout/encode";
|
|
6
|
+
import { getId } from "@src/layout/dom";
|
|
7
|
+
import * as core from "@src/core";
|
|
8
|
+
import config from "@src/core/config";
|
|
9
9
|
import { getCssRules } from "./node";
|
|
10
10
|
|
|
11
11
|
export let sheetUpdateState: StyleSheetState[] = [];
|
|
12
12
|
export let sheetAdoptionState: StyleSheetState[] = [];
|
|
13
|
-
const styleSheetId =
|
|
13
|
+
const styleSheetId = 'claritySheetId';
|
|
14
14
|
let styleSheetMap = {};
|
|
15
|
-
let styleTimeMap: {
|
|
15
|
+
let styleTimeMap: {[key: string]: number} = {};
|
|
16
16
|
let documentNodes = [];
|
|
17
17
|
let createdSheetIds = [];
|
|
18
18
|
|
|
19
|
-
function proxyStyleRules(win:
|
|
19
|
+
function proxyStyleRules(win: any) {
|
|
20
20
|
if ((config.lean && config.lite) || win === null || win === undefined) {
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
win.clarityOverrides = win.clarityOverrides || {};
|
|
25
25
|
|
|
26
|
-
if (win.CSSStyleSheet
|
|
27
|
-
if (win.clarityOverrides.replace === undefined) {
|
|
28
|
-
win.clarityOverrides.replace = win.CSSStyleSheet.prototype.replace;
|
|
29
|
-
win.CSSStyleSheet.prototype.replace = function
|
|
26
|
+
if (win['CSSStyleSheet'] && win.CSSStyleSheet.prototype) {
|
|
27
|
+
if (win.clarityOverrides.replace === undefined) {
|
|
28
|
+
win.clarityOverrides.replace = win.CSSStyleSheet.prototype.replace;
|
|
29
|
+
win.CSSStyleSheet.prototype.replace = function(): Promise<CSSStyleSheet> {
|
|
30
30
|
if (core.active()) {
|
|
31
31
|
// if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
|
|
32
32
|
// and attached the sheet to a document. This way the timestamp of the style sheet creation will align
|
|
33
33
|
// to when it is used in the document rather than potentially being misaligned during the traverse process.
|
|
34
34
|
if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
|
|
35
|
-
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace,
|
|
35
|
+
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.Replace, arguments[0]);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
return win.clarityOverrides.replace.apply(this,
|
|
38
|
+
return win.clarityOverrides.replace.apply(this, arguments);
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
if (win.clarityOverrides.replaceSync === undefined) {
|
|
43
|
-
win.clarityOverrides.replaceSync = win.CSSStyleSheet.prototype.replaceSync;
|
|
44
|
-
win.CSSStyleSheet.prototype.replaceSync = function
|
|
42
|
+
if (win.clarityOverrides.replaceSync === undefined) {
|
|
43
|
+
win.clarityOverrides.replaceSync = win.CSSStyleSheet.prototype.replaceSync;
|
|
44
|
+
win.CSSStyleSheet.prototype.replaceSync = function(): void {
|
|
45
45
|
if (core.active()) {
|
|
46
46
|
// if we haven't seen this stylesheet on this page yet, wait until the checkDocumentStyles has found it
|
|
47
47
|
// and attached the sheet to a document. This way the timestamp of the style sheet creation will align
|
|
48
48
|
// to when it is used in the document rather than potentially being misaligned during the traverse process.
|
|
49
49
|
if (createdSheetIds.indexOf(this[styleSheetId]) > -1) {
|
|
50
|
-
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync,
|
|
51
|
-
}
|
|
50
|
+
trackStyleChange(time(), this[styleSheetId], StyleSheetOperation.ReplaceSync, arguments[0]);
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
|
-
win.clarityOverrides.replaceSync.apply(this,
|
|
54
|
-
return;
|
|
53
|
+
return win.clarityOverrides.replaceSync.apply(this, arguments);
|
|
55
54
|
};
|
|
56
55
|
}
|
|
57
|
-
}
|
|
56
|
+
}
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
export function start(): void {
|
|
61
60
|
proxyStyleRules(window);
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
export function checkDocumentStyles(documentNode: Document,
|
|
65
|
-
if (config.lean && config.lite) {
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
63
|
+
export function checkDocumentStyles(documentNode: Document, timestamp: number): void {
|
|
64
|
+
if (config.lean && config.lite) { return; }
|
|
68
65
|
|
|
69
66
|
if (documentNodes.indexOf(documentNode) === -1) {
|
|
70
67
|
documentNodes.push(documentNode);
|
|
@@ -72,13 +69,13 @@ export function checkDocumentStyles(documentNode: Document, cachedTimestamp: num
|
|
|
72
69
|
proxyStyleRules(documentNode.defaultView);
|
|
73
70
|
}
|
|
74
71
|
}
|
|
75
|
-
|
|
72
|
+
timestamp = timestamp || time();
|
|
76
73
|
if (!documentNode?.adoptedStyleSheets) {
|
|
77
74
|
// if we don't have adoptedStyledSheets on the Node passed to us, we can short circuit.
|
|
78
75
|
return;
|
|
79
76
|
}
|
|
80
|
-
|
|
81
|
-
for (
|
|
77
|
+
let currentStyleSheets: string[] = [];
|
|
78
|
+
for (var styleSheet of documentNode.adoptedStyleSheets) {
|
|
82
79
|
// If we haven't seen this style sheet on this page yet, we create a reference to it for the visualizer.
|
|
83
80
|
// For SPA or times in which Clarity restarts on a given page, our visualizer would lose context
|
|
84
81
|
// on the previously created style sheet for page N-1.
|
|
@@ -93,27 +90,22 @@ export function checkDocumentStyles(documentNode: Document, cachedTimestamp: num
|
|
|
93
90
|
currentStyleSheets.push(styleSheet[styleSheetId]);
|
|
94
91
|
}
|
|
95
92
|
|
|
96
|
-
|
|
93
|
+
let documentId = getId(documentNode, true);
|
|
97
94
|
if (!styleSheetMap[documentId]) {
|
|
98
95
|
styleSheetMap[documentId] = [];
|
|
99
96
|
}
|
|
100
97
|
if (!arraysEqual(currentStyleSheets, styleSheetMap[documentId])) {
|
|
101
98
|
// Using -1 to signify the root document node as we don't track that as part of our nodeMap
|
|
102
|
-
trackStyleAdoption(
|
|
103
|
-
timestamp,
|
|
104
|
-
documentNode === document ? -1 : getId(documentNode),
|
|
105
|
-
StyleSheetOperation.SetAdoptedStyles,
|
|
106
|
-
currentStyleSheets,
|
|
107
|
-
);
|
|
99
|
+
trackStyleAdoption(timestamp, documentNode == document ? -1 : getId(documentNode), StyleSheetOperation.SetAdoptedStyles, currentStyleSheets);
|
|
108
100
|
styleSheetMap[documentId] = currentStyleSheets;
|
|
109
101
|
styleTimeMap[documentId] = timestamp;
|
|
110
102
|
}
|
|
111
103
|
}
|
|
112
104
|
|
|
113
105
|
export function compute(): void {
|
|
114
|
-
for (
|
|
115
|
-
|
|
116
|
-
|
|
106
|
+
for (var documentNode of documentNodes) {
|
|
107
|
+
var docId = documentNode == document ? -1 : getId(documentNode);
|
|
108
|
+
let ts = docId in styleTimeMap ? styleTimeMap[docId] : null;
|
|
117
109
|
checkDocumentStyles(documentNode, ts);
|
|
118
110
|
}
|
|
119
111
|
}
|
|
@@ -138,8 +130,8 @@ function trackStyleChange(time: number, id: string, operation: StyleSheetOperati
|
|
|
138
130
|
data: {
|
|
139
131
|
id,
|
|
140
132
|
operation,
|
|
141
|
-
cssRules
|
|
142
|
-
}
|
|
133
|
+
cssRules
|
|
134
|
+
}
|
|
143
135
|
});
|
|
144
136
|
|
|
145
137
|
encode(Event.StyleSheetUpdate);
|
|
@@ -152,8 +144,8 @@ function trackStyleAdoption(time: number, id: number, operation: StyleSheetOpera
|
|
|
152
144
|
data: {
|
|
153
145
|
id,
|
|
154
146
|
operation,
|
|
155
|
-
newIds
|
|
156
|
-
}
|
|
147
|
+
newIds
|
|
148
|
+
}
|
|
157
149
|
});
|
|
158
150
|
|
|
159
151
|
encode(Event.StyleSheetAdoption);
|
|
@@ -165,4 +157,4 @@ function arraysEqual(a: string[], b: string[]): boolean {
|
|
|
165
157
|
}
|
|
166
158
|
|
|
167
159
|
return a.every((value, index) => value === b[index]);
|
|
168
|
-
}
|
|
160
|
+
}
|
package/src/layout/target.ts
CHANGED
|
@@ -1,34 +1,30 @@
|
|
|
1
1
|
import { Privacy } from "@clarity-types/core";
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import { Event } from "@clarity-types/data";
|
|
3
|
+
import { TargetMetadata } from "@clarity-types/layout";
|
|
4
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
|
-
import * as region from "@src/layout/region";
|
|
8
8
|
|
|
9
9
|
export function target(evt: UIEvent): Node {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
let path = evt.composed && evt.composedPath ? evt.composedPath() : null;
|
|
11
|
+
let node = (path && path.length > 0 ? path[0] : evt.target) as Node;
|
|
12
12
|
mutation.active(); // Mark active periods of time so mutations can continue uninterrupted
|
|
13
13
|
return node && node.nodeType === Node.DOCUMENT_NODE ? (node as Document).documentElement : node;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function metadata(node: Node, event: Event, text: string = null): TargetMetadata {
|
|
17
17
|
// If the node is null, we return a reserved value for id: 0. Valid assignment of id begins from 1+.
|
|
18
|
-
|
|
18
|
+
let output: TargetMetadata = { id: 0, hash: null, privacy: Privacy.Text };
|
|
19
19
|
if (node) {
|
|
20
|
-
|
|
20
|
+
let value = dom.get(node);
|
|
21
21
|
if (value !== null) {
|
|
22
|
-
|
|
22
|
+
let metadata = value.metadata;
|
|
23
23
|
output.id = value.id;
|
|
24
24
|
output.hash = value.hash;
|
|
25
25
|
output.privacy = metadata.privacy;
|
|
26
|
-
if (value.region) {
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
if (metadata.fraud) {
|
|
30
|
-
fraud.check(metadata.fraud, value.id, text || value.data.value);
|
|
31
|
-
}
|
|
26
|
+
if (value.region) { region.track(value.region, event); }
|
|
27
|
+
if (metadata.fraud) { fraud.check(metadata.fraud, value.id, text || value.data.value); }
|
|
32
28
|
}
|
|
33
29
|
}
|
|
34
30
|
|
package/src/layout/traverse.ts
CHANGED
|
@@ -1,34 +1,28 @@
|
|
|
1
|
-
import { Task,
|
|
2
|
-
import
|
|
1
|
+
import { Task, Timer } from "@clarity-types/core";
|
|
2
|
+
import { Source } from "@clarity-types/layout";
|
|
3
3
|
import * as task from "@src/core/task";
|
|
4
4
|
import node from "@src/layout/node";
|
|
5
5
|
|
|
6
|
-
export default async function
|
|
7
|
-
|
|
6
|
+
export default async function(root: Node, timer: Timer, source: Source, timestamp: number): Promise<void> {
|
|
7
|
+
let queue = [root];
|
|
8
8
|
while (queue.length > 0) {
|
|
9
|
-
|
|
9
|
+
let entry = queue.shift();
|
|
10
10
|
let next = entry.firstChild;
|
|
11
11
|
|
|
12
12
|
while (next) {
|
|
13
13
|
queue.push(next);
|
|
14
14
|
next = next.nextSibling;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
// Check the status of current task to see if we should yield before continuing
|
|
18
18
|
let state = task.state(timer);
|
|
19
|
-
if (state === Task.Wait) {
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
if (state === Task.Stop) {
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
19
|
+
if (state === Task.Wait) { state = await task.suspend(timer); }
|
|
20
|
+
if (state === Task.Stop) { break; }
|
|
25
21
|
|
|
26
22
|
// Check if processing a node gives us a pointer to one of its sub nodes for traversal
|
|
27
23
|
// E.g. an element node may give us a pointer to traverse shadowDom if shadowRoot property is set
|
|
28
24
|
// Or, an iframe from the same origin could give a pointer to it's document for traversing contents of iframe.
|
|
29
|
-
|
|
30
|
-
if (subnode) {
|
|
31
|
-
queue.push(subnode);
|
|
32
|
-
}
|
|
25
|
+
let subnode = node(entry, source, timestamp);
|
|
26
|
+
if (subnode) { queue.push(subnode); }
|
|
33
27
|
}
|
|
34
|
-
}
|
|
28
|
+
}
|
package/src/performance/blank.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {Event, Token} from "@clarity-types/data";
|
|
2
2
|
import { time } from "@src/core/time";
|
|
3
3
|
import { queue } from "@src/data/upload";
|
|
4
4
|
import * as navigation from "@src/performance/navigation";
|
|
5
5
|
|
|
6
|
-
export default async function
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
export default async function(type: Event): Promise<void> {
|
|
7
|
+
let t = time();
|
|
8
|
+
let tokens: Token[] = [t, type];
|
|
9
9
|
switch (type) {
|
|
10
10
|
case Event.Navigation:
|
|
11
11
|
tokens.push(navigation.data.fetchStart);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { PerformanceEventTiming, Interaction } from '@clarity-types/data';
|
|
2
2
|
|
|
3
3
|
// Estimate variables to keep track of interactions
|
|
4
4
|
let interactionCountEstimate = 0;
|
|
5
|
-
let minKnownInteractionId =
|
|
5
|
+
let minKnownInteractionId = Infinity;
|
|
6
6
|
let maxKnownInteractionId = 0;
|
|
7
7
|
|
|
8
8
|
let prevInteractionCount = 0; // Used to track interaction count between pages
|
|
@@ -21,25 +21,33 @@ const longestInteractionMap: Map<number, Interaction> = new Map();
|
|
|
21
21
|
* Dividing by 7 helps approximate the interaction count more accurately, since interaction IDs are spread out across a large range.
|
|
22
22
|
*/
|
|
23
23
|
const countInteractions = (entry: PerformanceEventTiming) => {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
if ('interactionCount' in performance) {
|
|
25
|
+
interactionCountEstimate = performance.interactionCount as number;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (entry.interactionId) {
|
|
30
|
+
minKnownInteractionId = Math.min(
|
|
31
|
+
minKnownInteractionId,
|
|
32
|
+
entry.interactionId
|
|
33
|
+
);
|
|
34
|
+
maxKnownInteractionId = Math.max(
|
|
35
|
+
maxKnownInteractionId,
|
|
36
|
+
entry.interactionId
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
interactionCountEstimate = maxKnownInteractionId
|
|
40
|
+
? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1
|
|
41
|
+
: 0;
|
|
42
|
+
}
|
|
35
43
|
};
|
|
36
44
|
|
|
37
45
|
const getInteractionCount = () => {
|
|
38
|
-
|
|
46
|
+
return interactionCountEstimate || 0;
|
|
39
47
|
};
|
|
40
48
|
|
|
41
49
|
const getInteractionCountForNavigation = () => {
|
|
42
|
-
|
|
50
|
+
return getInteractionCount() - prevInteractionCount;
|
|
43
51
|
};
|
|
44
52
|
|
|
45
53
|
/**
|
|
@@ -47,67 +55,71 @@ const getInteractionCountForNavigation = () => {
|
|
|
47
55
|
* the candidate interaction based on the current interaction count.
|
|
48
56
|
* Dividing by 50 is a heuristic to estimate the 98th percentile (P98) interaction.
|
|
49
57
|
* This assumes one out of every 50 interactions represents the P98 interaction.
|
|
50
|
-
* By dividing the total interaction count by 50, we get an index to approximate
|
|
58
|
+
* By dividing the total interaction count by 50, we get an index to approximate
|
|
51
59
|
* the slowest 2% of interactions, helping identify a likely P98 candidate.
|
|
52
60
|
*/
|
|
53
61
|
export const estimateP98LongestInteraction = () => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
if(!longestInteractionList.length){
|
|
63
|
+
return -1;
|
|
64
|
+
}
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
const candidateInteractionIndex = Math.min(
|
|
67
|
+
longestInteractionList.length - 1,
|
|
68
|
+
Math.floor(getInteractionCountForNavigation() / 50)
|
|
69
|
+
);
|
|
59
70
|
|
|
60
|
-
|
|
71
|
+
return longestInteractionList[candidateInteractionIndex].latency;
|
|
61
72
|
};
|
|
62
73
|
|
|
63
74
|
/**
|
|
64
75
|
* Resets the interaction tracking, usually called after navigation to a new page.
|
|
65
76
|
*/
|
|
66
77
|
export const resetInteractions = () => {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
prevInteractionCount = getInteractionCount();
|
|
79
|
+
longestInteractionList.length = 0;
|
|
80
|
+
longestInteractionMap.clear();
|
|
70
81
|
};
|
|
71
82
|
|
|
72
83
|
/**
|
|
73
84
|
* Processes a PerformanceEventTiming entry by updating the longest interaction list.
|
|
74
85
|
*/
|
|
75
86
|
export const processInteractionEntry = (entry: PerformanceEventTiming) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
// Ignore entries with 0 interactionId or very short durations
|
|
88
|
+
if (!entry.interactionId || entry.duration < DEFAULT_DURATION_THRESHOLD) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
countInteractions(entry);
|
|
93
|
+
|
|
94
|
+
const minLongestInteraction =
|
|
95
|
+
longestInteractionList[longestInteractionList.length - 1];
|
|
96
|
+
|
|
97
|
+
const existingInteraction = longestInteractionMap.get(entry.interactionId!);
|
|
98
|
+
|
|
99
|
+
// Either update existing, add new, or replace shortest interaction if necessary
|
|
100
|
+
if (
|
|
101
|
+
existingInteraction ||
|
|
102
|
+
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
|
|
103
|
+
entry.duration > minLongestInteraction?.latency
|
|
104
|
+
) {
|
|
105
|
+
if (!existingInteraction) {
|
|
106
|
+
const interaction = {
|
|
107
|
+
id: entry.interactionId,
|
|
108
|
+
latency: entry.duration,
|
|
109
|
+
};
|
|
110
|
+
longestInteractionMap.set(interaction.id, interaction);
|
|
111
|
+
longestInteractionList.push(interaction);
|
|
112
|
+
} else if (entry.duration > existingInteraction.latency) {
|
|
113
|
+
existingInteraction.latency = entry.duration;
|
|
79
114
|
}
|
|
80
115
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
existingInteraction ||
|
|
90
|
-
longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
|
|
91
|
-
entry.duration > minLongestInteraction?.latency
|
|
92
|
-
) {
|
|
93
|
-
if (!existingInteraction) {
|
|
94
|
-
const interaction = {
|
|
95
|
-
id: entry.interactionId,
|
|
96
|
-
latency: entry.duration,
|
|
97
|
-
};
|
|
98
|
-
longestInteractionMap.set(interaction.id, interaction);
|
|
99
|
-
longestInteractionList.push(interaction);
|
|
100
|
-
} else if (entry.duration > existingInteraction.latency) {
|
|
101
|
-
existingInteraction.latency = entry.duration;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
longestInteractionList.sort((a, b) => b.latency - a.latency);
|
|
105
|
-
|
|
106
|
-
// Trim the list to the maximum number of interactions to consider
|
|
107
|
-
if (longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) {
|
|
108
|
-
for (const i of longestInteractionList.splice(MAX_INTERACTIONS_TO_CONSIDER)) {
|
|
109
|
-
longestInteractionMap.delete(i.id);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
116
|
+
longestInteractionList.sort((a, b) => b.latency - a.latency);
|
|
117
|
+
|
|
118
|
+
// Trim the list to the maximum number of interactions to consider
|
|
119
|
+
if (longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) {
|
|
120
|
+
longestInteractionList
|
|
121
|
+
.splice(MAX_INTERACTIONS_TO_CONSIDER)
|
|
122
|
+
.forEach((i) => longestInteractionMap.delete(i.id));
|
|
112
123
|
}
|
|
124
|
+
}
|
|
113
125
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Event } from "@clarity-types/data";
|
|
2
|
-
import
|
|
2
|
+
import { NavigationData } from "@clarity-types/performance";
|
|
3
3
|
import encode from "./encode";
|
|
4
4
|
|
|
5
5
|
export let data: NavigationData = null;
|
|
@@ -25,7 +25,7 @@ export function compute(entry: PerformanceNavigationTiming): void {
|
|
|
25
25
|
type: entry.type,
|
|
26
26
|
protocol: entry.nextHopProtocol,
|
|
27
27
|
encodedSize: entry.encodedBodySize ? entry.encodedBodySize : 0,
|
|
28
|
-
decodedSize: entry.decodedBodySize ? entry.decodedBodySize : 0
|
|
28
|
+
decodedSize: entry.decodedBodySize ? entry.decodedBodySize : 0
|
|
29
29
|
};
|
|
30
30
|
encode(Event.Navigation);
|
|
31
31
|
}
|