clarity-js 0.8.11 → 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 +2964 -3181
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +2964 -3181
- 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/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 -32
- 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/core/event.ts
CHANGED
|
@@ -1,57 +1,53 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BrowserEvent, Constant } from "@clarity-types/core";
|
|
2
2
|
import api from "./api";
|
|
3
3
|
import measure from "./measure";
|
|
4
4
|
|
|
5
5
|
let bindings: Map<EventTarget, BrowserEvent[]> = new Map();
|
|
6
6
|
|
|
7
|
-
export function bind(target: EventTarget, event: string, listener: EventListener, capture = false, passive = true): void {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
bindings.set(target, []);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
bindings.get(target).push({ event, listener, options: { capture, passive } });
|
|
19
|
-
} catch {
|
|
20
|
-
/* do nothing */
|
|
7
|
+
export function bind(target: EventTarget, event: string, listener: EventListener, capture: boolean = false, passive: boolean = true): void {
|
|
8
|
+
listener = measure(listener) as EventListener;
|
|
9
|
+
// Wrapping following lines inside try / catch to cover edge cases where we might try to access an inaccessible element.
|
|
10
|
+
// E.g. Iframe may start off as same-origin but later turn into cross-origin, and the following lines will throw an exception.
|
|
11
|
+
try {
|
|
12
|
+
target[api(Constant.AddEventListener)](event, listener, { capture, passive });
|
|
13
|
+
if (!has(target)) {
|
|
14
|
+
bindings.set(target, []);
|
|
21
15
|
}
|
|
16
|
+
|
|
17
|
+
bindings.get(target).push({ event, listener, options: { capture, passive } });
|
|
18
|
+
} catch {
|
|
19
|
+
/* do nothing */
|
|
20
|
+
}
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export function reset(): void {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
// Walk through existing list of bindings and remove them all
|
|
25
|
+
bindings.forEach((bindingsPerTarget: BrowserEvent[], target: EventTarget) => {
|
|
26
|
+
resetByTarget(bindingsPerTarget, target);
|
|
27
|
+
});
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
bindings = new Map();
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
export function unbind(target: EventTarget) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
if (!has(target)) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
resetByTarget(bindings.get(target), target);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
export function has(target: EventTarget): boolean {
|
|
41
|
-
|
|
40
|
+
return bindings.has(target);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
function resetByTarget(bindingsPerTarget: BrowserEvent[], target: EventTarget): void {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
} catch {
|
|
53
|
-
/* do nothing */
|
|
54
|
-
}
|
|
44
|
+
bindingsPerTarget.forEach((binding) => {
|
|
45
|
+
// Wrapping inside try / catch to avoid situations where the element may be destroyed before we get a chance to unbind
|
|
46
|
+
try {
|
|
47
|
+
target[api(Constant.RemoveEventListener)](binding.event, binding.listener, { capture: binding.options.capture, passive: binding.options.passive });
|
|
48
|
+
} catch {
|
|
49
|
+
/* do nothing */
|
|
55
50
|
}
|
|
56
|
-
|
|
51
|
+
});
|
|
52
|
+
bindings.delete(target);
|
|
57
53
|
}
|
package/src/core/hash.ts
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
// tslint:disable: no-bitwise
|
|
2
|
+
export default function(input: string, precision: number = null): string {
|
|
2
3
|
// Code inspired from C# GetHashCode: https://github.com/Microsoft/referencesource/blob/master/mscorlib/system/string.cs
|
|
3
4
|
let hash = 0;
|
|
4
5
|
let hashOne = 5381;
|
|
5
6
|
let hashTwo = hashOne;
|
|
6
7
|
for (let i = 0; i < input.length; i += 2) {
|
|
7
|
-
|
|
8
|
+
let charOne = input.charCodeAt(i);
|
|
8
9
|
hashOne = ((hashOne << 5) + hashOne) ^ charOne;
|
|
9
10
|
if (i + 1 < input.length) {
|
|
10
|
-
|
|
11
|
+
let charTwo = input.charCodeAt(i + 1);
|
|
11
12
|
hashTwo = ((hashTwo << 5) + hashTwo) ^ charTwo;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
// Replace the magic number from C# implementation (1566083941) with a smaller prime number (11579)
|
|
15
16
|
// This ensures we don't hit integer overflow and prevent collisions
|
|
16
|
-
hash = Math.abs(hashOne + hashTwo * 11579);
|
|
17
|
-
return (precision ? hash % 2
|
|
17
|
+
hash = Math.abs(hashOne + (hashTwo * 11579));
|
|
18
|
+
return (precision ? hash % Math.pow(2, precision) : hash).toString(36);
|
|
18
19
|
}
|
package/src/core/history.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BooleanFlag, Code, Constant, Metric, Setting, Severity } from "@clarity-types/data";
|
|
2
2
|
import { FunctionNames } from "@clarity-types/performance";
|
|
3
3
|
import * as clarity from "@src/clarity";
|
|
4
|
-
import * as core from "@src/core"
|
|
4
|
+
import * as core from "@src/core"
|
|
5
5
|
import { bind } from "@src/core/event";
|
|
6
|
-
import * as metric from "@src/data/metric";
|
|
7
6
|
import * as internal from "@src/diagnostic/internal";
|
|
7
|
+
import * as metric from "@src/data/metric";
|
|
8
8
|
|
|
9
9
|
let pushState = null;
|
|
10
10
|
let replaceState = null;
|
|
@@ -17,10 +17,10 @@ export function start(): void {
|
|
|
17
17
|
bind(window, "popstate", compute);
|
|
18
18
|
|
|
19
19
|
// Add a proxy to history.pushState function
|
|
20
|
-
if (pushState === null) {
|
|
21
|
-
pushState = history.pushState;
|
|
22
|
-
history.pushState = function
|
|
23
|
-
pushState.apply(this,
|
|
20
|
+
if (pushState === null) {
|
|
21
|
+
pushState = history.pushState;
|
|
22
|
+
history.pushState = function(): void {
|
|
23
|
+
pushState.apply(this, arguments);
|
|
24
24
|
if (core.active() && check()) {
|
|
25
25
|
compute();
|
|
26
26
|
}
|
|
@@ -28,10 +28,11 @@ export function start(): void {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
// Add a proxy to history.replaceState function
|
|
31
|
-
if (replaceState === null)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
if (replaceState === null)
|
|
32
|
+
{
|
|
33
|
+
replaceState = history.replaceState;
|
|
34
|
+
history.replaceState = function(): void {
|
|
35
|
+
replaceState.apply(this, arguments);
|
|
35
36
|
if (core.active() && check()) {
|
|
36
37
|
compute();
|
|
37
38
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Config } from "@clarity-types/core";
|
|
2
2
|
import { Constant } from "@clarity-types/data";
|
|
3
3
|
import { FunctionNames } from "@clarity-types/performance";
|
|
4
|
-
import * as clarity from "@src/clarity";
|
|
5
4
|
import configuration from "@src/core/config";
|
|
6
5
|
import * as event from "@src/core/event";
|
|
7
6
|
import * as history from "@src/core/history";
|
|
8
7
|
import * as report from "@src/core/report";
|
|
9
8
|
import * as task from "@src/core/task";
|
|
10
9
|
import * as time from "@src/core/time";
|
|
10
|
+
import * as clarity from "@src/clarity";
|
|
11
11
|
import * as custom from "@src/data/custom";
|
|
12
12
|
|
|
13
13
|
let status = false;
|
|
@@ -36,17 +36,15 @@ export function active(): boolean {
|
|
|
36
36
|
|
|
37
37
|
export function check(): boolean {
|
|
38
38
|
try {
|
|
39
|
-
|
|
40
|
-
return
|
|
41
|
-
status === false &&
|
|
39
|
+
let globalPrivacyControlSet = navigator && "globalPrivacyControl" in navigator && navigator['globalPrivacyControl'] == true;
|
|
40
|
+
return status === false &&
|
|
42
41
|
typeof Promise !== "undefined" &&
|
|
43
|
-
window
|
|
44
|
-
document
|
|
42
|
+
window["MutationObserver"] &&
|
|
43
|
+
document["createTreeWalker"] &&
|
|
45
44
|
"now" in Date &&
|
|
46
45
|
"now" in performance &&
|
|
47
46
|
typeof WeakMap !== "undefined" &&
|
|
48
47
|
!globalPrivacyControlSet
|
|
49
|
-
);
|
|
50
48
|
} catch (ex) {
|
|
51
49
|
return false;
|
|
52
50
|
}
|
|
@@ -54,13 +52,9 @@ export function check(): boolean {
|
|
|
54
52
|
|
|
55
53
|
export function config(override: Config): boolean {
|
|
56
54
|
// Process custom configuration overrides, if available
|
|
57
|
-
if (override === null || status) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
for (const key in override) {
|
|
61
|
-
if (key in configuration) {
|
|
62
|
-
configuration[key] = override[key];
|
|
63
|
-
}
|
|
55
|
+
if (override === null || status) { return false; }
|
|
56
|
+
for (let key in override) {
|
|
57
|
+
if (key in configuration) { configuration[key] = override[key]; }
|
|
64
58
|
}
|
|
65
59
|
return true;
|
|
66
60
|
}
|
|
@@ -77,12 +71,8 @@ export function suspend(): void {
|
|
|
77
71
|
if (status) {
|
|
78
72
|
custom.event(Constant.Clarity, Constant.Suspend);
|
|
79
73
|
clarity.stop();
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
for (const x of ["resize", "scroll", "pageshow"]) {
|
|
84
|
-
event.bind(window, x, restart);
|
|
85
|
-
}
|
|
74
|
+
["mousemove", "touchstart"].forEach(x => event.bind(document, x, restart));
|
|
75
|
+
["resize", "scroll", "pageshow"].forEach(x => event.bind(window, x, restart));
|
|
86
76
|
}
|
|
87
77
|
}
|
|
88
78
|
|
package/src/core/measure.ts
CHANGED
|
@@ -3,16 +3,12 @@ import { report } from "@src/core/report";
|
|
|
3
3
|
import * as metric from "@src/data/metric";
|
|
4
4
|
import * as internal from "@src/diagnostic/internal";
|
|
5
5
|
|
|
6
|
-
//
|
|
6
|
+
// tslint:disable-next-line: ban-types
|
|
7
7
|
export default function (method: Function): Function {
|
|
8
|
-
return function (
|
|
9
|
-
|
|
10
|
-
try {
|
|
11
|
-
|
|
12
|
-
} catch (ex) {
|
|
13
|
-
throw report(ex);
|
|
14
|
-
}
|
|
15
|
-
const duration = performance.now() - start;
|
|
8
|
+
return function (): void {
|
|
9
|
+
let start = performance.now();
|
|
10
|
+
try { method.apply(this, arguments); } catch (ex) { throw report(ex); }
|
|
11
|
+
let duration = performance.now() - start;
|
|
16
12
|
metric.sum(Metric.TotalCost, duration);
|
|
17
13
|
if (duration > Setting.LongTask) {
|
|
18
14
|
metric.count(Metric.LongTaskCount);
|
package/src/core/report.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Report } from "@clarity-types/core";
|
|
2
2
|
import config from "@src/core/config";
|
|
3
3
|
import { data } from "@src/data/envelope";
|
|
4
4
|
|
|
@@ -12,7 +12,7 @@ export function report(e: Error): Error {
|
|
|
12
12
|
// Do not report the same message twice for the same page
|
|
13
13
|
if (history && history.indexOf(e.message) === -1) {
|
|
14
14
|
const url = config.report;
|
|
15
|
-
if (url && url.length > 0) {
|
|
15
|
+
if (url && url.length > 0 && data) {
|
|
16
16
|
const payload: Report = { v: data.version, p: data.projectId, u: data.userId, s: data.sessionId, n: data.pageNum };
|
|
17
17
|
if (e.message) {
|
|
18
18
|
payload.m = e.message;
|
|
@@ -22,7 +22,7 @@ export function report(e: Error): Error {
|
|
|
22
22
|
}
|
|
23
23
|
// Using POST request instead of a GET request (img-src) to not violate existing CSP rules
|
|
24
24
|
// Since, Clarity already uses XHR to upload data, we stick with similar POST mechanism for reporting too
|
|
25
|
-
|
|
25
|
+
let xhr = new XMLHttpRequest();
|
|
26
26
|
xhr.open("POST", url, true);
|
|
27
27
|
xhr.send(JSON.stringify(payload));
|
|
28
28
|
history.push(e.message);
|
package/src/core/scrub.ts
CHANGED
|
@@ -10,9 +10,9 @@ let digitRegex = null;
|
|
|
10
10
|
let letterRegex = null;
|
|
11
11
|
let currencyRegex = null;
|
|
12
12
|
|
|
13
|
-
export function text(value: string, hint: string, privacy: Privacy, mangle = false, type?: string): string {
|
|
13
|
+
export function text(value: string, hint: string, privacy: Privacy, mangle: boolean = false, type?: string): string {
|
|
14
14
|
if (value) {
|
|
15
|
-
if (hint
|
|
15
|
+
if (hint == "input" && (type === "checkbox" || type === "radio")) {
|
|
16
16
|
return value;
|
|
17
17
|
}
|
|
18
18
|
|
|
@@ -88,21 +88,17 @@ export function text(value: string, hint: string, privacy: Privacy, mangle = fal
|
|
|
88
88
|
return value;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
export function url(input: string, electron = false, truncate = false): string {
|
|
91
|
+
export function url(input: string, electron: boolean = false, truncate: boolean = false): string {
|
|
92
92
|
let result = input;
|
|
93
93
|
// Replace the URL for Electron apps so we don't send back file:/// URL
|
|
94
94
|
if (electron) {
|
|
95
95
|
result = `${Data.Constant.HTTPS}${Data.Constant.Electron}`;
|
|
96
96
|
} else {
|
|
97
|
-
|
|
97
|
+
let drop = config.drop;
|
|
98
98
|
if (drop && drop.length > 0 && input && input.indexOf("?") > 0) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
result =
|
|
102
|
-
`${path}?${query
|
|
103
|
-
.split("&")
|
|
104
|
-
.map((p) => (drop.some((x) => p.indexOf(`${x}=`) === 0) ? `${p.split("=")[0]}=${swap}` : p))
|
|
105
|
-
.join("&")}`;
|
|
99
|
+
let [path, query] = input.split("?");
|
|
100
|
+
let swap = Data.Constant.Dropped;
|
|
101
|
+
result = path + "?" + query.split("&").map(p => drop.some(x => p.indexOf(`${x}=`) === 0) ? `${p.split("=")[0]}=${swap}` : p).join("&");
|
|
106
102
|
}
|
|
107
103
|
}
|
|
108
104
|
|
|
@@ -113,12 +109,12 @@ export function url(input: string, electron = false, truncate = false): string {
|
|
|
113
109
|
}
|
|
114
110
|
|
|
115
111
|
function mangleText(value: string): string {
|
|
116
|
-
|
|
112
|
+
let trimmed = value.trim();
|
|
117
113
|
if (trimmed.length > 0) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
114
|
+
let first = trimmed[0];
|
|
115
|
+
let index = value.indexOf(first);
|
|
116
|
+
let prefix = value.substr(0, index);
|
|
117
|
+
let suffix = value.substr(index + trimmed.length);
|
|
122
118
|
return `${prefix}${trimmed.length.toString(36)}${suffix}`;
|
|
123
119
|
}
|
|
124
120
|
return value;
|
|
@@ -134,7 +130,7 @@ export function scrub(value: string, letter: string, digit: string): string {
|
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
function mangleToken(value: string): string {
|
|
137
|
-
|
|
133
|
+
let length = ((Math.floor(value.length / Data.Setting.WordLength) + 1) * Data.Setting.WordLength);
|
|
138
134
|
let output: string = Layout.Constant.Empty;
|
|
139
135
|
for (let i = 0; i < length; i++) {
|
|
140
136
|
output += i > 0 && i % Data.Setting.WordLength === 0 ? Data.Constant.Space : Data.Constant.Mask;
|
|
@@ -147,12 +143,10 @@ function regex(): void {
|
|
|
147
143
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Unicode_Property_Escapes
|
|
148
144
|
if (unicodeRegex && digitRegex === null) {
|
|
149
145
|
try {
|
|
150
|
-
digitRegex =
|
|
151
|
-
letterRegex =
|
|
152
|
-
currencyRegex =
|
|
153
|
-
} catch {
|
|
154
|
-
unicodeRegex = false;
|
|
155
|
-
}
|
|
146
|
+
digitRegex = new RegExp("\\p{N}", "gu");
|
|
147
|
+
letterRegex = new RegExp("\\p{L}", "gu");
|
|
148
|
+
currencyRegex = new RegExp("\\p{Sc}", "gu");
|
|
149
|
+
} catch { unicodeRegex = false; }
|
|
156
150
|
}
|
|
157
151
|
}
|
|
158
152
|
|
|
@@ -167,19 +161,16 @@ function redact(value: string): string {
|
|
|
167
161
|
regex(); // Initialize regular expressions
|
|
168
162
|
|
|
169
163
|
for (let i = 0; i < value.length; i++) {
|
|
170
|
-
|
|
164
|
+
let c = value.charCodeAt(i);
|
|
171
165
|
hasDigit = hasDigit || (c >= Data.Character.Zero && c <= Data.Character.Nine); // Check for digits in the current word
|
|
172
166
|
hasEmail = hasEmail || c === Data.Character.At; // Check for @ sign anywhere within the current word
|
|
173
|
-
hasWhitespace =
|
|
174
|
-
c === Data.Character.Tab || c === Data.Character.NewLine || c === Data.Character.Return || c === Data.Character.Blank;
|
|
167
|
+
hasWhitespace = c === Data.Character.Tab || c === Data.Character.NewLine || c === Data.Character.Return || c === Data.Character.Blank;
|
|
175
168
|
|
|
176
169
|
// Process each word as an individual token to redact any sensitive information
|
|
177
170
|
if (i === 0 || i === value.length - 1 || hasWhitespace) {
|
|
178
171
|
// Performance optimization: Lazy load string -> array conversion only when required
|
|
179
172
|
if (hasDigit || hasEmail) {
|
|
180
|
-
if (array === null) {
|
|
181
|
-
array = value.split(Data.Constant.Empty);
|
|
182
|
-
}
|
|
173
|
+
if (array === null) { array = value.split(Data.Constant.Empty); }
|
|
183
174
|
// Work on a token at a time so we don't have to apply regex to a larger string
|
|
184
175
|
let token = value.substring(spaceIndex + 1, hasWhitespace ? i : i + 1);
|
|
185
176
|
// Check if unicode regex is supported, otherwise fallback to calling mask function on this token
|
package/src/core/task.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
Priority,
|
|
4
|
-
type RequestIdleCallbackDeadline,
|
|
5
|
-
type RequestIdleCallbackOptions,
|
|
6
|
-
Task,
|
|
7
|
-
type Timer,
|
|
8
|
-
} from "@clarity-types/core";
|
|
9
|
-
import type { TaskFunction, TaskResolve, Tasks } from "@clarity-types/core";
|
|
1
|
+
import { AsyncTask, Priority, RequestIdleCallbackDeadline, RequestIdleCallbackOptions, Task, Timer } from "@clarity-types/core";
|
|
2
|
+
import { TaskFunction, TaskResolve, Tasks } from "@clarity-types/core";
|
|
10
3
|
import { Code, Metric, Setting, Severity } from "@clarity-types/data";
|
|
11
4
|
import * as metadata from "@src/data/metadata";
|
|
12
5
|
import * as metric from "@src/data/metric";
|
|
@@ -32,9 +25,7 @@ export function resume(): void {
|
|
|
32
25
|
if (pauseTask) {
|
|
33
26
|
resumeResolve();
|
|
34
27
|
pauseTask = null;
|
|
35
|
-
if (activeTask === null) {
|
|
36
|
-
run();
|
|
37
|
-
}
|
|
28
|
+
if (activeTask === null) { run(); }
|
|
38
29
|
}
|
|
39
30
|
}
|
|
40
31
|
|
|
@@ -47,14 +38,14 @@ export function reset(): void {
|
|
|
47
38
|
|
|
48
39
|
export async function schedule(task: TaskFunction, priority: Priority = Priority.Normal): Promise<void> {
|
|
49
40
|
// If this task is already scheduled, skip it
|
|
50
|
-
for (
|
|
41
|
+
for (let q of queuedTasks) {
|
|
51
42
|
if (q.task === task) {
|
|
52
43
|
return;
|
|
53
44
|
}
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
let promise = new Promise<void>((resolve: TaskResolve): void => {
|
|
48
|
+
let insert = priority === Priority.High ? "unshift" : "push";
|
|
58
49
|
// Queue this task for asynchronous execution later
|
|
59
50
|
// We also store a unique page identifier (id) along with the task to ensure
|
|
60
51
|
// ensure that we do not accidentally execute this task in context of a different page
|
|
@@ -63,49 +54,38 @@ export async function schedule(task: TaskFunction, priority: Priority = Priority
|
|
|
63
54
|
|
|
64
55
|
// If there is no active task running, and Clarity is not in pause state,
|
|
65
56
|
// invoke the first task in the queue synchronously. This ensures that we don't yield the thread during unload event
|
|
66
|
-
if (activeTask === null && pauseTask === null) {
|
|
67
|
-
run();
|
|
68
|
-
}
|
|
57
|
+
if (activeTask === null && pauseTask === null) { run(); }
|
|
69
58
|
|
|
70
59
|
return promise;
|
|
71
60
|
}
|
|
72
61
|
|
|
73
62
|
function run(): void {
|
|
74
|
-
|
|
63
|
+
let entry = queuedTasks.shift();
|
|
75
64
|
if (entry) {
|
|
76
65
|
activeTask = entry;
|
|
77
|
-
entry
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (entry.id !== metadata.id()) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (error) {
|
|
96
|
-
internal.log(Code.RunTask, Severity.Warning, error.name, error.message, error.stack);
|
|
97
|
-
}
|
|
98
|
-
activeTask = null;
|
|
99
|
-
run();
|
|
100
|
-
});
|
|
66
|
+
entry.task().then((): void => {
|
|
67
|
+
// Bail out if the context in which this task was operating is different from the current page
|
|
68
|
+
// An example scenario where task could span across pages is Single Page Applications (SPA)
|
|
69
|
+
// A task that started on page #1, but completes on page #2
|
|
70
|
+
if (entry.id !== metadata.id()) { return; }
|
|
71
|
+
entry.resolve();
|
|
72
|
+
activeTask = null; // Reset active task back to null now that the promise is resolved
|
|
73
|
+
run();
|
|
74
|
+
}).catch((error: Error): void => {
|
|
75
|
+
// If one of the scheduled tasks failed, log, recover and continue processing rest of the tasks
|
|
76
|
+
if (entry.id !== metadata.id()) { return; }
|
|
77
|
+
if (error) { internal.log(Code.RunTask, Severity.Warning, error.name, error.message, error.stack); }
|
|
78
|
+
activeTask = null;
|
|
79
|
+
run();
|
|
80
|
+
});
|
|
101
81
|
}
|
|
102
82
|
}
|
|
103
83
|
|
|
104
84
|
export function state(timer: Timer): Task {
|
|
105
|
-
|
|
85
|
+
let id = key(timer);
|
|
106
86
|
if (id in tracker) {
|
|
107
|
-
|
|
108
|
-
return elapsed > tracker[id].yield ? Task.Wait : Task.Run;
|
|
87
|
+
let elapsed = performance.now() - tracker[id].start;
|
|
88
|
+
return (elapsed > tracker[id].yield) ? Task.Wait : Task.Run;
|
|
109
89
|
}
|
|
110
90
|
// If this task is no longer being tracked, send stop message to the caller
|
|
111
91
|
return Task.Stop;
|
|
@@ -116,10 +96,10 @@ export function start(timer: Timer): void {
|
|
|
116
96
|
}
|
|
117
97
|
|
|
118
98
|
function restart(timer: Timer): void {
|
|
119
|
-
|
|
120
|
-
if (tracker
|
|
121
|
-
|
|
122
|
-
|
|
99
|
+
let id = key(timer);
|
|
100
|
+
if (tracker && tracker[id]) {
|
|
101
|
+
let c = tracker[id].calls;
|
|
102
|
+
let y = tracker[id].yield;
|
|
123
103
|
start(timer);
|
|
124
104
|
tracker[id].calls = c + 1;
|
|
125
105
|
tracker[id].yield = y;
|
|
@@ -127,17 +107,15 @@ function restart(timer: Timer): void {
|
|
|
127
107
|
}
|
|
128
108
|
|
|
129
109
|
export function stop(timer: Timer): void {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
let end = performance.now();
|
|
111
|
+
let id = key(timer);
|
|
112
|
+
let duration = end - tracker[id].start;
|
|
133
113
|
metric.sum(timer.cost, duration);
|
|
134
114
|
metric.count(Metric.InvokeCount);
|
|
135
115
|
|
|
136
116
|
// For the first execution, which is synchronous, time is automatically counted towards TotalDuration.
|
|
137
117
|
// However, for subsequent asynchronous runs, we need to manually update TotalDuration metric.
|
|
138
|
-
if (tracker[id].calls > 0) {
|
|
139
|
-
metric.sum(Metric.TotalCost, duration);
|
|
140
|
-
}
|
|
118
|
+
if (tracker[id].calls > 0) { metric.sum(Metric.TotalCost, duration); }
|
|
141
119
|
}
|
|
142
120
|
|
|
143
121
|
export async function suspend(timer: Timer): Promise<Task> {
|
|
@@ -145,10 +123,10 @@ export async function suspend(timer: Timer): Promise<Task> {
|
|
|
145
123
|
// It's possible that Clarity is wrapping up instrumentation on a page and we are still in the middle of an async task.
|
|
146
124
|
// In that case, we do not wish to continue yielding thread.
|
|
147
125
|
// Instead, we will turn async task into a sync task and maximize our chances of getting some data back.
|
|
148
|
-
|
|
126
|
+
let id = key(timer);
|
|
149
127
|
if (id in tracker) {
|
|
150
128
|
stop(timer);
|
|
151
|
-
// some customer polyfills for requestIdleCallback return null
|
|
129
|
+
// some customer polyfills for requestIdleCallback return null
|
|
152
130
|
tracker[id].yield = (await wait())?.timeRemaining() || Setting.LongTask;
|
|
153
131
|
restart(timer);
|
|
154
132
|
}
|
|
@@ -162,9 +140,7 @@ function key(timer: Timer): string {
|
|
|
162
140
|
}
|
|
163
141
|
|
|
164
142
|
async function wait(): Promise<RequestIdleCallbackDeadline> {
|
|
165
|
-
if (pauseTask) {
|
|
166
|
-
await pauseTask;
|
|
167
|
-
}
|
|
143
|
+
if (pauseTask) { await pauseTask; }
|
|
168
144
|
return new Promise<RequestIdleCallbackDeadline>((resolve: (deadline: RequestIdleCallbackDeadline) => void): void => {
|
|
169
145
|
requestIdleCallback(resolve, { timeout: idleTimeout });
|
|
170
146
|
});
|
|
@@ -186,24 +162,20 @@ function requestIdleCallbackPolyfill(callback: (deadline: RequestIdleCallbackDea
|
|
|
186
162
|
const incoming = channel.port1;
|
|
187
163
|
const outgoing = channel.port2;
|
|
188
164
|
incoming.onmessage = (event: MessageEvent): void => {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
165
|
+
let currentTime = performance.now();
|
|
166
|
+
let elapsed = currentTime - startTime;
|
|
167
|
+
let duration = currentTime - event.data;
|
|
192
168
|
if (duration > Setting.LongTask && elapsed < options.timeout) {
|
|
193
|
-
requestAnimationFrame((): void => {
|
|
194
|
-
outgoing.postMessage(currentTime);
|
|
195
|
-
});
|
|
169
|
+
requestAnimationFrame((): void => { outgoing.postMessage(currentTime); });
|
|
196
170
|
} else {
|
|
197
|
-
|
|
171
|
+
let didTimeout = elapsed > options.timeout;
|
|
198
172
|
callback({
|
|
199
173
|
didTimeout,
|
|
200
|
-
timeRemaining: (): number =>
|
|
174
|
+
timeRemaining: (): number => didTimeout ? Setting.LongTask : Math.max(0, Setting.LongTask - duration)
|
|
201
175
|
});
|
|
202
176
|
}
|
|
203
177
|
};
|
|
204
|
-
requestAnimationFrame((): void => {
|
|
205
|
-
outgoing.postMessage(performance.now());
|
|
206
|
-
});
|
|
178
|
+
requestAnimationFrame((): void => { outgoing.postMessage(performance.now()); });
|
|
207
179
|
}
|
|
208
180
|
|
|
209
|
-
|
|
181
|
+
let requestIdleCallback = window["requestIdleCallback"] || requestIdleCallbackPolyfill;
|
package/src/core/time.ts
CHANGED
|
@@ -4,13 +4,13 @@ export function start(): void {
|
|
|
4
4
|
startTime = performance.now() + performance.timeOrigin;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
// event.timestamp is number of milliseconds elapsed since the document was loaded
|
|
7
|
+
// event.timestamp is number of milliseconds elapsed since the document was loaded
|
|
8
8
|
// since iframes can be loaded later the event timestamp is not the same as performance.now()
|
|
9
9
|
// converting everything to absolute time by adding timeorigin of the event view
|
|
10
10
|
// to synchronize times before calculating the difference with start time
|
|
11
11
|
export function time(event: UIEvent | PageTransitionEvent = null): number {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
let ts = event && event.timeStamp > 0 ? event.timeStamp : performance.now();
|
|
13
|
+
let origin = event && (event as UIEvent).view ? (event as UIEvent).view.performance.timeOrigin : performance.timeOrigin;
|
|
14
14
|
return Math.max(Math.round(ts + origin - startTime), 0);
|
|
15
15
|
}
|
|
16
16
|
|
package/src/core/timeout.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { Event } from "@clarity-types/data";
|
|
2
2
|
import measure from "./measure";
|
|
3
3
|
|
|
4
4
|
export function setTimeout(handler: (event?: Event | boolean) => void, timeout?: number, event?: Event): number {
|
|
@@ -6,5 +6,5 @@ export function setTimeout(handler: (event?: Event | boolean) => void, timeout?:
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function clearTimeout(handle: number): void {
|
|
9
|
-
window.clearTimeout(handle);
|
|
9
|
+
return window.clearTimeout(handle);
|
|
10
10
|
}
|
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
let version = "0.8.13-beta";
|
|
2
2
|
export default version;
|