clarity-js 0.6.26 → 0.6.30
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 +1114 -1026
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +1114 -1026
- package/package.json +1 -1
- package/src/core/config.ts +1 -0
- package/src/core/measure.ts +2 -1
- package/src/core/report.ts +8 -7
- package/src/core/version.ts +1 -1
- package/src/data/limit.ts +0 -2
- package/src/data/metadata.ts +31 -3
- package/src/data/upload.ts +3 -0
- package/src/diagnostic/encode.ts +0 -10
- package/src/diagnostic/index.ts +0 -3
- package/src/interaction/clipboard.ts +32 -0
- package/src/interaction/encode.ts +30 -10
- package/src/interaction/index.ts +9 -1
- package/src/interaction/submit.ts +30 -0
- package/src/layout/dom.ts +2 -1
- package/src/layout/extract.ts +21 -2
- package/src/layout/mutation.ts +10 -6
- package/src/layout/selector.ts +18 -4
- package/src/performance/encode.ts +0 -9
- package/src/performance/index.ts +0 -3
- package/test/helper.ts +10 -2
- package/types/core.d.ts +4 -1
- package/types/data.d.ts +20 -4
- package/types/diagnostic.d.ts +0 -6
- package/types/index.d.ts +1 -1
- package/types/interaction.d.ts +27 -0
- package/src/diagnostic/image.ts +0 -23
- package/src/performance/connection.ts +0 -37
package/package.json
CHANGED
package/src/core/config.ts
CHANGED
package/src/core/measure.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { Setting } from "@clarity-types/core";
|
|
2
2
|
import { Metric } from "@clarity-types/data";
|
|
3
|
+
import { report } from "@src/core/report";
|
|
3
4
|
import * as metric from "@src/data/metric";
|
|
4
5
|
|
|
5
6
|
// tslint:disable-next-line: ban-types
|
|
6
7
|
export default function (method: Function): Function {
|
|
7
8
|
return function (): void {
|
|
8
9
|
let start = performance.now();
|
|
9
|
-
method.apply(this, arguments);
|
|
10
|
+
try { method.apply(this, arguments); } catch (ex) { throw report(ex); }
|
|
10
11
|
let duration = performance.now() - start;
|
|
11
12
|
metric.sum(Metric.TotalCost, duration);
|
|
12
13
|
if (duration > Setting.LongTask) {
|
package/src/core/report.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Report } from "@clarity-types/core";
|
|
2
|
-
import { Check } from "@clarity-types/data";
|
|
3
2
|
import config from "@src/core/config";
|
|
4
|
-
import { data } from "@src/data/
|
|
3
|
+
import { data } from "@src/data/envelope";
|
|
5
4
|
|
|
6
5
|
let history: string[];
|
|
7
6
|
|
|
@@ -9,19 +8,21 @@ export function reset(): void {
|
|
|
9
8
|
history = [];
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
export function report(
|
|
11
|
+
export function report(e: Error): Error {
|
|
13
12
|
// Do not report the same message twice for the same page
|
|
14
|
-
if (history && history.indexOf(message) === -1) {
|
|
13
|
+
if (history && history.indexOf(e.message) === -1) {
|
|
15
14
|
const url = config.report;
|
|
16
15
|
if (url && url.length > 0) {
|
|
17
|
-
let payload: Report = {
|
|
18
|
-
if (message) payload.m = message;
|
|
16
|
+
let payload: Report = {v: data.version, p: data.projectId, u: data.userId, s: data.sessionId, n: data.pageNum};
|
|
17
|
+
if (e.message) { payload.m = e.message; }
|
|
18
|
+
if (e.stack) { payload.e = e.stack; }
|
|
19
19
|
// Using POST request instead of a GET request (img-src) to not violate existing CSP rules
|
|
20
20
|
// Since, Clarity already uses XHR to upload data, we stick with similar POST mechanism for reporting too
|
|
21
21
|
let xhr = new XMLHttpRequest();
|
|
22
22
|
xhr.open("POST", url);
|
|
23
23
|
xhr.send(JSON.stringify(payload));
|
|
24
|
-
history.push(message);
|
|
24
|
+
history.push(e.message);
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
return e;
|
|
27
28
|
}
|
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let version = "0.6.
|
|
1
|
+
let version = "0.6.30";
|
|
2
2
|
export default version;
|
package/src/data/limit.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Check, Event, LimitData, Setting } from "@clarity-types/data";
|
|
2
2
|
import * as clarity from "@src/clarity";
|
|
3
|
-
import { report } from "@src/core/report";
|
|
4
3
|
import { time } from "@src/core/time";
|
|
5
4
|
import * as envelope from "@src/data/envelope";
|
|
6
5
|
import * as metadata from "@src/data/metadata";
|
|
@@ -25,7 +24,6 @@ export function check(bytes: number): void {
|
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
export function trigger(reason: Check): void {
|
|
28
|
-
report(reason);
|
|
29
27
|
data.check = reason;
|
|
30
28
|
metadata.clear();
|
|
31
29
|
clarity.stop();
|
package/src/data/metadata.ts
CHANGED
|
@@ -38,14 +38,16 @@ export function start(): void {
|
|
|
38
38
|
dimension.log(Dimension.TabId, tab());
|
|
39
39
|
dimension.log(Dimension.PageLanguage, document.documentElement.lang);
|
|
40
40
|
dimension.log(Dimension.DocumentDirection, document.dir);
|
|
41
|
-
|
|
42
41
|
if (navigator) {
|
|
43
42
|
dimension.log(Dimension.Language, (<any>navigator).userLanguage || navigator.language);
|
|
43
|
+
metric.max(Metric.Automation, navigator.webdriver ? BooleanFlag.True : BooleanFlag.False);
|
|
44
|
+
userAgentData();
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// Metrics
|
|
47
48
|
metric.max(Metric.ClientTimestamp, s.ts);
|
|
48
49
|
metric.max(Metric.Playback, BooleanFlag.False);
|
|
50
|
+
|
|
49
51
|
if (screen) {
|
|
50
52
|
metric.max(Metric.ScreenWidth, Math.round(screen.width));
|
|
51
53
|
metric.max(Metric.ScreenHeight, Math.round(screen.height));
|
|
@@ -62,13 +64,39 @@ export function start(): void {
|
|
|
62
64
|
track(u);
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
export function userAgentData(): void {
|
|
68
|
+
if (navigator["userAgentData"] && navigator["userAgentData"].getHighEntropyValues) {
|
|
69
|
+
navigator["userAgentData"].getHighEntropyValues(
|
|
70
|
+
["model",
|
|
71
|
+
"platform",
|
|
72
|
+
"platformVersion",
|
|
73
|
+
"uaFullVersion"])
|
|
74
|
+
.then(ua => {
|
|
75
|
+
dimension.log(Dimension.Platform, ua.platform);
|
|
76
|
+
dimension.log(Dimension.PlatformVersion, ua.platformVersion);
|
|
77
|
+
ua.brands?.forEach(brand => {
|
|
78
|
+
dimension.log(Dimension.Brand, brand.name + Constant.Tilde + brand.version);
|
|
79
|
+
});
|
|
80
|
+
dimension.log(Dimension.Model, ua.model);
|
|
81
|
+
metric.max(Metric.Mobile, ua.mobile ? BooleanFlag.True : BooleanFlag.False);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
export function stop(): void {
|
|
66
87
|
callback = null;
|
|
67
88
|
rootDomain = null;
|
|
89
|
+
data = null;
|
|
68
90
|
}
|
|
69
91
|
|
|
70
|
-
export function metadata(cb: MetadataCallback): void {
|
|
71
|
-
|
|
92
|
+
export function metadata(cb: MetadataCallback, wait: boolean = true): void {
|
|
93
|
+
if (data && wait === false) {
|
|
94
|
+
// Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
|
|
95
|
+
cb(data, !config.lean);
|
|
96
|
+
} else {
|
|
97
|
+
// Save the callback for future reference; so we can inform the caller when page gets upgraded and we have a valid playback flag
|
|
98
|
+
callback = cb;
|
|
99
|
+
}
|
|
72
100
|
}
|
|
73
101
|
|
|
74
102
|
export function id(): string {
|
package/src/data/upload.ts
CHANGED
|
@@ -56,6 +56,9 @@ export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
|
56
56
|
break;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
// Increment event count metric
|
|
60
|
+
metric.count(Metric.EventCount);
|
|
61
|
+
|
|
59
62
|
// Following two checks are precautionary and act as a fail safe mechanism to get out of unexpected situations.
|
|
60
63
|
// Check 1: If for any reason the upload hasn't happened after waiting for 2x the config.delay time,
|
|
61
64
|
// reset the timer. This allows Clarity to attempt an upload again.
|
package/src/diagnostic/encode.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
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
|
-
import * as image from "@src/diagnostic/image";
|
|
5
4
|
import * as internal from "@src/diagnostic/internal";
|
|
6
5
|
import * as script from "@src/diagnostic/script";
|
|
7
|
-
import { metadata } from "@src/layout/target";
|
|
8
6
|
|
|
9
7
|
export default async function (type: Event): Promise<void> {
|
|
10
8
|
let tokens: Token[] = [time(), type];
|
|
@@ -18,14 +16,6 @@ export default async function (type: Event): Promise<void> {
|
|
|
18
16
|
tokens.push(script.data.source);
|
|
19
17
|
queue(tokens);
|
|
20
18
|
break;
|
|
21
|
-
case Event.ImageError:
|
|
22
|
-
if (image.data) {
|
|
23
|
-
let imageTarget = metadata(image.data.target as Node, type);
|
|
24
|
-
tokens.push(image.data.source);
|
|
25
|
-
tokens.push(imageTarget.id);
|
|
26
|
-
queue(tokens);
|
|
27
|
-
}
|
|
28
|
-
break;
|
|
29
19
|
case Event.Log:
|
|
30
20
|
if (internal.data) {
|
|
31
21
|
tokens.push(internal.data.code);
|
package/src/diagnostic/index.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import * as image from "./image";
|
|
2
1
|
import * as internal from "./internal";
|
|
3
2
|
import * as script from "./script";
|
|
4
3
|
|
|
5
4
|
export function start(): void {
|
|
6
5
|
script.start();
|
|
7
|
-
image.start();
|
|
8
6
|
internal.start();
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
export function stop(): void {
|
|
12
|
-
image.stop();
|
|
13
10
|
internal.stop();
|
|
14
11
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Event } from "@clarity-types/data";
|
|
2
|
+
import { Clipboard, ClipboardState } from "@clarity-types/interaction";
|
|
3
|
+
import { bind } from "@src/core/event";
|
|
4
|
+
import { schedule } from "@src/core/task";
|
|
5
|
+
import { time } from "@src/core/time";
|
|
6
|
+
import encode from "./encode";
|
|
7
|
+
import { target } from "@src/layout/target";
|
|
8
|
+
|
|
9
|
+
export let state: ClipboardState[] = [];
|
|
10
|
+
|
|
11
|
+
export function start(): void {
|
|
12
|
+
reset();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function observe(root: Node): void {
|
|
16
|
+
bind(root, "cut", recompute.bind(this, Clipboard.Cut), true);
|
|
17
|
+
bind(root, "copy", recompute.bind(this, Clipboard.Copy), true);
|
|
18
|
+
bind(root, "paste", recompute.bind(this, Clipboard.Paste), true);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function recompute(action: Clipboard, evt: UIEvent): void {
|
|
22
|
+
state.push({ time: time(), event: Event.Clipboard, data: { target: target(evt), action } });
|
|
23
|
+
schedule(encode.bind(this, Event.Clipboard));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function reset(): void {
|
|
27
|
+
state = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function stop(): void {
|
|
31
|
+
reset();
|
|
32
|
+
}
|
|
@@ -5,11 +5,13 @@ import * as baseline from "@src/data/baseline";
|
|
|
5
5
|
import { queue } from "@src/data/upload";
|
|
6
6
|
import { metadata } from "@src/layout/target";
|
|
7
7
|
import * as click from "./click";
|
|
8
|
+
import * as clipboard from "./clipboard";
|
|
8
9
|
import * as input from "./input";
|
|
9
10
|
import * as pointer from "./pointer";
|
|
10
11
|
import * as resize from "./resize";
|
|
11
12
|
import * as scroll from "./scroll";
|
|
12
13
|
import * as selection from "./selection";
|
|
14
|
+
import * as submit from "./submit";
|
|
13
15
|
import * as timeline from "./timeline";
|
|
14
16
|
import * as unload from "./unload";
|
|
15
17
|
import * as visibility from "./visibility";
|
|
@@ -27,8 +29,7 @@ export default async function (type: Event): Promise<void> {
|
|
|
27
29
|
case Event.TouchEnd:
|
|
28
30
|
case Event.TouchMove:
|
|
29
31
|
case Event.TouchCancel:
|
|
30
|
-
for (let
|
|
31
|
-
let entry = pointer.state[i];
|
|
32
|
+
for (let entry of pointer.state) {
|
|
32
33
|
let pTarget = metadata(entry.data.target as Node, entry.event);
|
|
33
34
|
if (pTarget.id > 0) {
|
|
34
35
|
tokens = [entry.time, entry.event];
|
|
@@ -42,8 +43,7 @@ export default async function (type: Event): Promise<void> {
|
|
|
42
43
|
pointer.reset();
|
|
43
44
|
break;
|
|
44
45
|
case Event.Click:
|
|
45
|
-
for (let
|
|
46
|
-
let entry = click.state[i];
|
|
46
|
+
for (let entry of click.state) {
|
|
47
47
|
let cTarget = metadata(entry.data.target as Node, entry.event);
|
|
48
48
|
tokens = [entry.time, entry.event];
|
|
49
49
|
let cHash = cTarget.hash.join(Constant.Dot);
|
|
@@ -63,6 +63,18 @@ export default async function (type: Event): Promise<void> {
|
|
|
63
63
|
}
|
|
64
64
|
click.reset();
|
|
65
65
|
break;
|
|
66
|
+
case Event.Clipboard:
|
|
67
|
+
for (let entry of clipboard.state) {
|
|
68
|
+
tokens = [entry.time, entry.event];
|
|
69
|
+
let target = metadata(entry.data.target as Node, entry.event);
|
|
70
|
+
if (target.id > 0) {
|
|
71
|
+
tokens.push(target.id);
|
|
72
|
+
tokens.push(entry.data.action);
|
|
73
|
+
queue(tokens);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
clipboard.reset();
|
|
77
|
+
break;
|
|
66
78
|
case Event.Resize:
|
|
67
79
|
let r = resize.data;
|
|
68
80
|
tokens.push(r.width);
|
|
@@ -78,8 +90,7 @@ export default async function (type: Event): Promise<void> {
|
|
|
78
90
|
queue(tokens);
|
|
79
91
|
break;
|
|
80
92
|
case Event.Input:
|
|
81
|
-
for (let
|
|
82
|
-
let entry = input.state[i];
|
|
93
|
+
for (let entry of input.state) {
|
|
83
94
|
let iTarget = metadata(entry.data.target as Node, entry.event);
|
|
84
95
|
tokens = [entry.time, entry.event];
|
|
85
96
|
tokens.push(iTarget.id);
|
|
@@ -102,8 +113,7 @@ export default async function (type: Event): Promise<void> {
|
|
|
102
113
|
}
|
|
103
114
|
break;
|
|
104
115
|
case Event.Scroll:
|
|
105
|
-
for (let
|
|
106
|
-
let entry = scroll.state[i];
|
|
116
|
+
for (let entry of scroll.state) {
|
|
107
117
|
let sTarget = metadata(entry.data.target as Node, entry.event);
|
|
108
118
|
if (sTarget.id > 0) {
|
|
109
119
|
tokens = [entry.time, entry.event];
|
|
@@ -116,9 +126,19 @@ export default async function (type: Event): Promise<void> {
|
|
|
116
126
|
}
|
|
117
127
|
scroll.reset();
|
|
118
128
|
break;
|
|
129
|
+
case Event.Submit:
|
|
130
|
+
for (let entry of submit.state) {
|
|
131
|
+
tokens = [entry.time, entry.event];
|
|
132
|
+
let target = metadata(entry.data.target as Node, entry.event);
|
|
133
|
+
if (target.id > 0) {
|
|
134
|
+
tokens.push(target.id);
|
|
135
|
+
queue(tokens);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
submit.reset();
|
|
139
|
+
break;
|
|
119
140
|
case Event.Timeline:
|
|
120
|
-
for (let
|
|
121
|
-
let entry = timeline.updates[i];
|
|
141
|
+
for (let entry of timeline.updates) {
|
|
122
142
|
tokens = [entry.time, entry.event];
|
|
123
143
|
tokens.push(entry.data.type);
|
|
124
144
|
tokens.push(entry.data.hash);
|
package/src/interaction/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import * as click from "@src/interaction/click";
|
|
2
|
+
import * as clipboard from "@src/interaction/clipboard";
|
|
2
3
|
import * as input from "@src/interaction/input";
|
|
3
4
|
import * as pointer from "@src/interaction/pointer";
|
|
4
5
|
import * as resize from "@src/interaction/resize";
|
|
5
6
|
import * as scroll from "@src/interaction/scroll";
|
|
6
7
|
import * as selection from "@src/interaction/selection";
|
|
8
|
+
import * as submit from "@src/interaction/submit";
|
|
7
9
|
import * as timeline from "@src/interaction/timeline";
|
|
8
10
|
import * as unload from "@src/interaction/unload";
|
|
9
11
|
import * as visibility from "@src/interaction/visibility";
|
|
@@ -11,24 +13,28 @@ import * as visibility from "@src/interaction/visibility";
|
|
|
11
13
|
export function start(): void {
|
|
12
14
|
timeline.start();
|
|
13
15
|
click.start();
|
|
16
|
+
clipboard.start();
|
|
14
17
|
pointer.start();
|
|
15
18
|
input.start();
|
|
16
19
|
resize.start();
|
|
17
20
|
visibility.start();
|
|
18
21
|
scroll.start();
|
|
19
22
|
selection.start();
|
|
23
|
+
submit.start();
|
|
20
24
|
unload.start();
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export function stop(): void {
|
|
24
28
|
timeline.stop();
|
|
25
29
|
click.stop();
|
|
30
|
+
clipboard.stop();
|
|
26
31
|
pointer.stop();
|
|
27
32
|
input.stop();
|
|
28
33
|
resize.stop();
|
|
29
34
|
visibility.stop();
|
|
30
35
|
scroll.stop();
|
|
31
36
|
selection.stop();
|
|
37
|
+
submit.stop();
|
|
32
38
|
unload.stop()
|
|
33
39
|
}
|
|
34
40
|
|
|
@@ -38,8 +44,10 @@ export function observe(root: Node): void {
|
|
|
38
44
|
// In case of shadow DOM, following events automatically bubble up to the parent document.
|
|
39
45
|
if (root.nodeType === Node.DOCUMENT_NODE) {
|
|
40
46
|
click.observe(root);
|
|
47
|
+
clipboard.observe(root);
|
|
41
48
|
pointer.observe(root);
|
|
42
49
|
input.observe(root);
|
|
43
50
|
selection.observe(root);
|
|
51
|
+
submit.observe(root);
|
|
44
52
|
}
|
|
45
|
-
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Event } from "@clarity-types/data";
|
|
2
|
+
import { SubmitState } from "@clarity-types/interaction";
|
|
3
|
+
import { bind } from "@src/core/event";
|
|
4
|
+
import { schedule } from "@src/core/task";
|
|
5
|
+
import { time } from "@src/core/time";
|
|
6
|
+
import encode from "./encode";
|
|
7
|
+
import { target } from "@src/layout/target";
|
|
8
|
+
|
|
9
|
+
export let state: SubmitState[] = [];
|
|
10
|
+
|
|
11
|
+
export function start(): void {
|
|
12
|
+
reset();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function observe(root: Node): void {
|
|
16
|
+
bind(root, "submit", recompute, true);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function recompute(evt: UIEvent): void {
|
|
20
|
+
state.push({ time: time(), event: Event.Submit, data: { target: target(evt) } });
|
|
21
|
+
schedule(encode.bind(this, Event.Submit));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function reset(): void {
|
|
25
|
+
state = [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function stop(): void {
|
|
29
|
+
reset();
|
|
30
|
+
}
|
package/src/layout/dom.ts
CHANGED
|
@@ -12,7 +12,7 @@ let index: number = 1;
|
|
|
12
12
|
|
|
13
13
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#%3Cinput%3E_types
|
|
14
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"];
|
|
15
|
+
const DISALLOWED_NAMES = ["addr", "cell", "code", "dob", "email", "mob", "name", "phone", "secret", "social", "ssn", "tel", "zip", "pass", "card", "account", "cvv", "ccv"];
|
|
16
16
|
const DISALLOWED_MATCH = ["address", "password", "contact"];
|
|
17
17
|
|
|
18
18
|
let nodes: Node[] = [];
|
|
@@ -56,6 +56,7 @@ export function parse(root: ParentNode): void {
|
|
|
56
56
|
if ("querySelectorAll" in root) {
|
|
57
57
|
extract.regions(root, config.regions);
|
|
58
58
|
extract.metrics(root, config.metrics);
|
|
59
|
+
extract.dimensions(root, config.dimensions);
|
|
59
60
|
config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
|
|
60
61
|
config.unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
|
|
61
62
|
}
|
package/src/layout/extract.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Extract, Metric, Region, RegionFilter } from "@clarity-types/core";
|
|
2
|
-
import { Constant } from "@clarity-types/data";
|
|
1
|
+
import { Dimension, Extract, Metric, Region, RegionFilter } from "@clarity-types/core";
|
|
2
|
+
import { Constant, Setting } from "@clarity-types/data";
|
|
3
|
+
import * as dimension from "@src/data/dimension";
|
|
3
4
|
import * as metric from "@src/data/metric";
|
|
4
5
|
import * as region from "@src/layout/region";
|
|
5
6
|
|
|
@@ -33,6 +34,19 @@ export function metrics(root: ParentNode, value: Metric[]): void {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
export function dimensions(root: ParentNode, value: Dimension[]): void {
|
|
38
|
+
for (let v of value) {
|
|
39
|
+
const [dimensionId, source, match] = v;
|
|
40
|
+
if (match) {
|
|
41
|
+
switch (source) {
|
|
42
|
+
case Extract.Text: root.querySelectorAll(match).forEach(e => { dimension.log(dimensionId, str((e as HTMLElement).innerText)); }); break;
|
|
43
|
+
case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { dimension.log(dimensionId, str(e.getAttribute(match))); }); break;
|
|
44
|
+
case Extract.Javascript: dimension.log(dimensionId, str(evaluate(match, Constant.String))); break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
function regex(match: string): RegExp {
|
|
37
51
|
regexCache[match] = match in regexCache ? regexCache[match] : new RegExp(match);
|
|
38
52
|
return regexCache[match];
|
|
@@ -52,6 +66,11 @@ function evaluate(variable: string, type: string = null, base: Object = window):
|
|
|
52
66
|
return null;
|
|
53
67
|
}
|
|
54
68
|
|
|
69
|
+
function str(input: string): string {
|
|
70
|
+
// Automatically trim string to max of Setting.DimensionLimit to avoid fetching long strings
|
|
71
|
+
return input ? input.substr(0, Setting.DimensionLimit) : input;
|
|
72
|
+
}
|
|
73
|
+
|
|
55
74
|
function num(text: string, scale: number, localize: boolean = true): number {
|
|
56
75
|
try {
|
|
57
76
|
scale = scale || 1;
|
package/src/layout/mutation.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function start(): void {
|
|
|
36
36
|
|
|
37
37
|
if (insertRule === null) { insertRule = CSSStyleSheet.prototype.insertRule; }
|
|
38
38
|
if (deleteRule === null) { deleteRule = CSSStyleSheet.prototype.deleteRule; }
|
|
39
|
-
if (attachShadow === null) { attachShadow =
|
|
39
|
+
if (attachShadow === null) { attachShadow = Element.prototype.attachShadow; }
|
|
40
40
|
|
|
41
41
|
// Some popular open source libraries, like styled-components, optimize performance
|
|
42
42
|
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
@@ -52,10 +52,14 @@ export function start(): void {
|
|
|
52
52
|
return deleteRule.apply(this, arguments);
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// Add a hook to attachShadow API calls
|
|
56
|
+
// In case we are unable to add a hook and browser throws an exception,
|
|
57
|
+
// reset attachShadow variable and resume processing like before
|
|
58
|
+
try {
|
|
59
|
+
Element.prototype.attachShadow = function (): ShadowRoot {
|
|
60
|
+
return schedule(attachShadow.apply(this, arguments)) as ShadowRoot;
|
|
61
|
+
}
|
|
62
|
+
} catch { attachShadow = null; }
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
export function observe(node: Node): void {
|
|
@@ -103,7 +107,7 @@ export function stop(): void {
|
|
|
103
107
|
|
|
104
108
|
// Restoring original attachShadow
|
|
105
109
|
if (attachShadow != null) {
|
|
106
|
-
|
|
110
|
+
Element.prototype.attachShadow = attachShadow;
|
|
107
111
|
attachShadow = null;
|
|
108
112
|
}
|
|
109
113
|
|
package/src/layout/selector.ts
CHANGED
|
@@ -24,12 +24,13 @@ export default function(input: SelectorInput, beta: boolean = false): string {
|
|
|
24
24
|
let selector = `${prefix}${input.tag}${suffix}`;
|
|
25
25
|
let classes = Constant.Class in a && a[Constant.Class].length > 0 ? a[Constant.Class].trim().split(/\s+/) : null;
|
|
26
26
|
if (beta) {
|
|
27
|
-
// In beta mode, update selector to use "id" field when available
|
|
28
|
-
//
|
|
27
|
+
// In beta mode, update selector to use "id" field when available. There are two exceptions:
|
|
28
|
+
// (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
|
|
29
|
+
// (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
|
|
29
30
|
let id = Constant.Id in a && a[Constant.Id].length > 0 ? a[Constant.Id] : null;
|
|
30
31
|
classes = input.tag !== Constant.BodyTag && classes ? classes.filter(c => !hasDigits(c)) : [];
|
|
31
32
|
selector = classes.length > 0 ? `${prefix}${input.tag}.${classes.join(".")}${suffix}` : selector;
|
|
32
|
-
selector = id && hasDigits(id) === false ?
|
|
33
|
+
selector = id && hasDigits(id) === false ? `${getDomPrefix(prefix)}#${id}` : selector;
|
|
33
34
|
} else {
|
|
34
35
|
// Otherwise, fallback to stable mode, where we include class names as part of the selector
|
|
35
36
|
selector = classes ? `${prefix}${input.tag}.${classes.join(".")}${suffix}` : selector;
|
|
@@ -38,11 +39,24 @@ export default function(input: SelectorInput, beta: boolean = false): string {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
function getDomPrefix(prefix: string): string {
|
|
43
|
+
const shadowDomStart = prefix.lastIndexOf(Constant.ShadowDomTag);
|
|
44
|
+
const iframeDomStart = prefix.lastIndexOf(`${Constant.IFramePrefix}${Constant.HTML}`);
|
|
45
|
+
const domStart = Math.max(shadowDomStart, iframeDomStart);
|
|
46
|
+
|
|
47
|
+
if (domStart < 0) {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const domEnd = prefix.indexOf(">", domStart) + 1;
|
|
52
|
+
return prefix.substr(0, domEnd);
|
|
53
|
+
}
|
|
54
|
+
|
|
41
55
|
// Check if the given input string has digits or not
|
|
42
56
|
function hasDigits(value: string): boolean {
|
|
43
57
|
for (let i = 0; i < value.length; i++) {
|
|
44
58
|
let c = value.charCodeAt(i);
|
|
45
|
-
|
|
59
|
+
if (c >= Character.Zero && c <= Character.Nine) { return true };
|
|
46
60
|
}
|
|
47
61
|
return false;
|
|
48
62
|
}
|
|
@@ -1,21 +1,12 @@
|
|
|
1
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
|
-
import * as connection from "@src/performance/connection";
|
|
5
4
|
import * as navigation from "@src/performance/navigation";
|
|
6
5
|
|
|
7
6
|
export default async function(type: Event): Promise<void> {
|
|
8
7
|
let t = time();
|
|
9
8
|
let tokens: Token[] = [t, type];
|
|
10
9
|
switch (type) {
|
|
11
|
-
case Event.Connection:
|
|
12
|
-
tokens.push(connection.data.downlink);
|
|
13
|
-
tokens.push(connection.data.rtt);
|
|
14
|
-
tokens.push(connection.data.saveData);
|
|
15
|
-
tokens.push(connection.data.type);
|
|
16
|
-
connection.reset();
|
|
17
|
-
queue(tokens, false);
|
|
18
|
-
break;
|
|
19
10
|
case Event.Navigation:
|
|
20
11
|
tokens.push(navigation.data.fetchStart);
|
|
21
12
|
tokens.push(navigation.data.connectStart);
|
package/src/performance/index.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import * as connection from "@src/performance/connection";
|
|
2
1
|
import * as navigation from "@src/performance/navigation";
|
|
3
2
|
import * as observer from "@src/performance/observer";
|
|
4
3
|
|
|
5
4
|
export function start(): void {
|
|
6
5
|
navigation.reset();
|
|
7
|
-
connection.start();
|
|
8
6
|
observer.start();
|
|
9
7
|
}
|
|
10
8
|
|
|
11
9
|
export function stop(): void {
|
|
12
10
|
observer.stop();
|
|
13
|
-
connection.stop();
|
|
14
11
|
navigation.reset();
|
|
15
12
|
}
|
package/test/helper.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Core, Data, Layout } from "clarity-decode";
|
|
2
2
|
import * as fs from 'fs';
|
|
3
|
+
import * as url from 'url';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
import { Browser, Page, chromium } from 'playwright';
|
|
5
6
|
|
|
@@ -8,7 +9,13 @@ export async function launch(): Promise<Browser> {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export async function markup(page: Page, file: string, override: Core.Config = null): Promise<string[]> {
|
|
11
|
-
const
|
|
12
|
+
const htmlPath = path.resolve(__dirname, `./html/${file}`);
|
|
13
|
+
const htmlFileUrl = url.pathToFileURL(htmlPath).toString();
|
|
14
|
+
const html = fs.readFileSync(htmlPath, 'utf8');
|
|
15
|
+
await Promise.all([
|
|
16
|
+
page.goto(htmlFileUrl),
|
|
17
|
+
page.waitForNavigation()
|
|
18
|
+
]);
|
|
12
19
|
await page.setContent(html.replace("</body>", `
|
|
13
20
|
<script>
|
|
14
21
|
window.payloads = [];
|
|
@@ -17,6 +24,7 @@ export async function markup(page: Page, file: string, override: Core.Config = n
|
|
|
17
24
|
</script>
|
|
18
25
|
</body>
|
|
19
26
|
`));
|
|
27
|
+
await page.hover("#two");
|
|
20
28
|
await page.waitForFunction("payloads && payloads.length > 1");
|
|
21
29
|
return await page.evaluate('payloads');
|
|
22
30
|
}
|
|
@@ -34,7 +42,7 @@ export function node(decoded: Data.DecodedPayload[], key: string, value: string
|
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
// Walking over the decoded payload to find the right match
|
|
37
|
-
for (let i = decoded.length - 1; i
|
|
45
|
+
for (let i = decoded.length - 1; i >= 0; i--) {
|
|
38
46
|
if (decoded[i].dom) {
|
|
39
47
|
for (let j = 0; j < decoded[i].dom.length; j++) {
|
|
40
48
|
if (decoded[i].dom[j].data) {
|
package/types/core.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ type TaskResolve = () => void;
|
|
|
5
5
|
type UploadCallback = (data: string) => void;
|
|
6
6
|
type Region = [number /* RegionId */, string /* Query Selector */, RegionFilter? /* Region Filter */, string? /* Filter Text */];
|
|
7
7
|
type Metric = [Data.Metric /* MetricId */, Extract /* Extract Filter */, string /* Match Value */, number? /* Scale Factor */];
|
|
8
|
+
type Dimension = [Data.Dimension /* DimensionId */, Extract /* Extract Filter */, string /* Match Value */];
|
|
8
9
|
|
|
9
10
|
/* Enum */
|
|
10
11
|
|
|
@@ -100,12 +101,13 @@ export interface BrowserEvent {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
export interface Report {
|
|
103
|
-
|
|
104
|
+
v: string; // Version
|
|
104
105
|
p: string; // Project Id
|
|
105
106
|
u: string; // User Id
|
|
106
107
|
s: string; // Session Id
|
|
107
108
|
n: number; // Page Number
|
|
108
109
|
m?: string; // Message, optional
|
|
110
|
+
e?: string; // Error Stack, optional
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
export interface Config {
|
|
@@ -118,6 +120,7 @@ export interface Config {
|
|
|
118
120
|
unmask?: string[];
|
|
119
121
|
regions?: Region[];
|
|
120
122
|
metrics?: Metric[];
|
|
123
|
+
dimensions?: Dimension[];
|
|
121
124
|
cookies?: string[];
|
|
122
125
|
report?: string;
|
|
123
126
|
upload?: string | UploadCallback;
|