clarity-js 0.6.23
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/README.md +26 -0
- package/build/clarity.js +4479 -0
- package/build/clarity.min.js +1 -0
- package/build/clarity.module.js +4473 -0
- package/package.json +66 -0
- package/rollup.config.ts +38 -0
- package/src/clarity.ts +54 -0
- package/src/core/config.ts +21 -0
- package/src/core/copy.ts +3 -0
- package/src/core/event.ts +25 -0
- package/src/core/hash.ts +19 -0
- package/src/core/history.ts +69 -0
- package/src/core/index.ts +79 -0
- package/src/core/measure.ts +17 -0
- package/src/core/report.ts +27 -0
- package/src/core/scrub.ts +102 -0
- package/src/core/task.ts +180 -0
- package/src/core/time.ts +14 -0
- package/src/core/timeout.ts +10 -0
- package/src/core/version.ts +2 -0
- package/src/data/baseline.ts +89 -0
- package/src/data/compress.ts +31 -0
- package/src/data/custom.ts +18 -0
- package/src/data/dimension.ts +42 -0
- package/src/data/encode.ts +109 -0
- package/src/data/envelope.ts +46 -0
- package/src/data/index.ts +43 -0
- package/src/data/limit.ts +42 -0
- package/src/data/metadata.ts +232 -0
- package/src/data/metric.ts +51 -0
- package/src/data/ping.ts +36 -0
- package/src/data/summary.ts +34 -0
- package/src/data/token.ts +39 -0
- package/src/data/upgrade.ts +36 -0
- package/src/data/upload.ts +250 -0
- package/src/data/variable.ts +46 -0
- package/src/diagnostic/encode.ts +40 -0
- package/src/diagnostic/image.ts +23 -0
- package/src/diagnostic/index.ts +14 -0
- package/src/diagnostic/internal.ts +41 -0
- package/src/diagnostic/script.ts +45 -0
- package/src/global.ts +22 -0
- package/src/index.ts +8 -0
- package/src/interaction/click.ts +140 -0
- package/src/interaction/encode.ts +140 -0
- package/src/interaction/index.ts +45 -0
- package/src/interaction/input.ts +64 -0
- package/src/interaction/pointer.ts +108 -0
- package/src/interaction/resize.ts +30 -0
- package/src/interaction/scroll.ts +73 -0
- package/src/interaction/selection.ts +66 -0
- package/src/interaction/timeline.ts +65 -0
- package/src/interaction/unload.ts +25 -0
- package/src/interaction/visibility.ts +24 -0
- package/src/layout/box.ts +83 -0
- package/src/layout/discover.ts +27 -0
- package/src/layout/document.ts +46 -0
- package/src/layout/dom.ts +442 -0
- package/src/layout/encode.ts +111 -0
- package/src/layout/extract.ts +75 -0
- package/src/layout/index.ts +25 -0
- package/src/layout/mutation.ts +232 -0
- package/src/layout/node.ts +211 -0
- package/src/layout/offset.ts +19 -0
- package/src/layout/region.ts +143 -0
- package/src/layout/schema.ts +66 -0
- package/src/layout/selector.ts +24 -0
- package/src/layout/target.ts +44 -0
- package/src/layout/traverse.ts +28 -0
- package/src/performance/connection.ts +37 -0
- package/src/performance/encode.ts +40 -0
- package/src/performance/index.ts +15 -0
- package/src/performance/navigation.ts +31 -0
- package/src/performance/observer.ts +87 -0
- package/test/core.test.ts +82 -0
- package/test/helper.ts +104 -0
- package/test/html/core.html +17 -0
- package/test/tsconfig.test.json +6 -0
- package/tsconfig.json +21 -0
- package/tslint.json +33 -0
- package/types/core.d.ts +127 -0
- package/types/data.d.ts +344 -0
- package/types/diagnostic.d.ts +24 -0
- package/types/index.d.ts +30 -0
- package/types/interaction.d.ts +110 -0
- package/types/layout.d.ts +200 -0
- package/types/performance.d.ts +40 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Constant, Event, VariableData } from "@clarity-types/data";
|
|
2
|
+
import * as core from "@src/core";
|
|
3
|
+
import encode from "./encode";
|
|
4
|
+
|
|
5
|
+
export let data: VariableData = null;
|
|
6
|
+
|
|
7
|
+
export function start(): void {
|
|
8
|
+
reset();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function set(variable: string, value: string | string[]): void {
|
|
12
|
+
let values = typeof value === Constant.String ? [value as string] : value as string[];
|
|
13
|
+
log(variable, values);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function identify(userId: string, sessionId: string = null, pageId: string = null): void {
|
|
17
|
+
log(Constant.UserId, [userId]);
|
|
18
|
+
log(Constant.SessionId, [sessionId]);
|
|
19
|
+
log(Constant.PageId, [pageId]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function log(variable: string, value: string[]): void {
|
|
23
|
+
if (core.active() &&
|
|
24
|
+
variable &&
|
|
25
|
+
value &&
|
|
26
|
+
typeof variable === Constant.String &&
|
|
27
|
+
variable.length < 255) {
|
|
28
|
+
let validValues = variable in data ? data[variable] : [];
|
|
29
|
+
for (let i = 0; i < value.length; i++) {
|
|
30
|
+
if (typeof value[i] === Constant.String && value[i].length < 255) { validValues.push(value[i]); }
|
|
31
|
+
}
|
|
32
|
+
data[variable] = validValues;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compute(): void {
|
|
37
|
+
encode(Event.Variable);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function reset(): void {
|
|
41
|
+
data = {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function stop(): void {
|
|
45
|
+
reset();
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Event, Token } from "@clarity-types/data";
|
|
2
|
+
import { time } from "@src/core/time";
|
|
3
|
+
import { queue } from "@src/data/upload";
|
|
4
|
+
import * as image from "@src/diagnostic/image";
|
|
5
|
+
import * as internal from "@src/diagnostic/internal";
|
|
6
|
+
import * as script from "@src/diagnostic/script";
|
|
7
|
+
import { metadata } from "@src/layout/target";
|
|
8
|
+
|
|
9
|
+
export default async function (type: Event): Promise<void> {
|
|
10
|
+
let tokens: Token[] = [time(), type];
|
|
11
|
+
|
|
12
|
+
switch (type) {
|
|
13
|
+
case Event.ScriptError:
|
|
14
|
+
tokens.push(script.data.message);
|
|
15
|
+
tokens.push(script.data.line);
|
|
16
|
+
tokens.push(script.data.column);
|
|
17
|
+
tokens.push(script.data.stack);
|
|
18
|
+
tokens.push(script.data.source);
|
|
19
|
+
queue(tokens);
|
|
20
|
+
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
|
+
case Event.Log:
|
|
30
|
+
if (internal.data) {
|
|
31
|
+
tokens.push(internal.data.code);
|
|
32
|
+
tokens.push(internal.data.name);
|
|
33
|
+
tokens.push(internal.data.message);
|
|
34
|
+
tokens.push(internal.data.stack);
|
|
35
|
+
tokens.push(internal.data.severity);
|
|
36
|
+
queue(tokens, false);
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Event } from "@clarity-types/data";
|
|
2
|
+
import { ImageErrorData } from "@clarity-types/diagnostic";
|
|
3
|
+
import { bind } from "@src/core/event";
|
|
4
|
+
import { schedule } from "@src/core/task";
|
|
5
|
+
import encode from "./encode";
|
|
6
|
+
|
|
7
|
+
export let data: ImageErrorData;
|
|
8
|
+
|
|
9
|
+
export function start(): void {
|
|
10
|
+
bind(document, "error", handler, true);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function handler(error: ErrorEvent): void {
|
|
14
|
+
let element = error.target as HTMLElement;
|
|
15
|
+
if (element && element.tagName === "IMG") {
|
|
16
|
+
data = { source: (element as HTMLImageElement).src, target: element };
|
|
17
|
+
schedule(encode.bind(this, Event.ImageError));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function stop(): void {
|
|
22
|
+
data = null;
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as image from "./image";
|
|
2
|
+
import * as internal from "./internal";
|
|
3
|
+
import * as script from "./script";
|
|
4
|
+
|
|
5
|
+
export function start(): void {
|
|
6
|
+
script.start();
|
|
7
|
+
image.start();
|
|
8
|
+
internal.start();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function stop(): void {
|
|
12
|
+
image.stop();
|
|
13
|
+
internal.stop();
|
|
14
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Code, Constant, Event, Severity } from "@clarity-types/data";
|
|
2
|
+
import { LogData } from "@clarity-types/diagnostic";
|
|
3
|
+
import config from "@src/core/config";
|
|
4
|
+
import { bind } from "@src/core/event";
|
|
5
|
+
import encode from "./encode";
|
|
6
|
+
|
|
7
|
+
let history: { [key: number]: string[] } = {};
|
|
8
|
+
export let data: LogData;
|
|
9
|
+
|
|
10
|
+
export function start(): void {
|
|
11
|
+
history = {};
|
|
12
|
+
bind(document, "securitypolicyviolation", csp);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function log(code: Code, severity: Severity, name: string = null, message: string = null, stack: string = null): void {
|
|
16
|
+
let key = name ? `${name}|${message}`: "";
|
|
17
|
+
// While rare, it's possible for code to fail repeatedly during the lifetime of the same page
|
|
18
|
+
// In those cases, we only want to log the failure once and not spam logs with redundant information.
|
|
19
|
+
if (code in history && history[code].indexOf(key) >= 0) { return; }
|
|
20
|
+
|
|
21
|
+
data = { code, name, message, stack, severity };
|
|
22
|
+
|
|
23
|
+
// Maintain history of errors in memory to avoid sending redundant information
|
|
24
|
+
if (code in history) { history[code].push(key); } else { history[code] = [key]; }
|
|
25
|
+
|
|
26
|
+
encode(Event.Log);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function csp(e: SecurityPolicyViolationEvent): void {
|
|
30
|
+
let upload = config.upload as string;
|
|
31
|
+
let parts = upload ? upload.substr(0, upload.indexOf("/", Constant.HTTPS.length)).split(Constant.Dot) : []; // Look for first "/" starting after initial "https://" string
|
|
32
|
+
let domain = parts.length >= 2 ? parts.splice(-2).join(Constant.Dot) : null;
|
|
33
|
+
// Capture content security policy violation only if disposition value is not explicitly set to "report"
|
|
34
|
+
if (domain && e.blockedURI && e.blockedURI.indexOf(domain) >= 0 && e["disposition"] !== Constant.Report) {
|
|
35
|
+
log(Code.ContentSecurityPolicy, Severity.Warning, e.blockedURI);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function stop(): void {
|
|
40
|
+
history = {};
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Constant, Event, Setting } from "@clarity-types/data";
|
|
2
|
+
import { ScriptErrorData } from "@clarity-types/diagnostic";
|
|
3
|
+
import { bind } from "@src/core/event";
|
|
4
|
+
import * as box from "@src/layout/box";
|
|
5
|
+
import encode from "./encode";
|
|
6
|
+
|
|
7
|
+
let history: { [key: string]: number } = {};
|
|
8
|
+
export let data: ScriptErrorData;
|
|
9
|
+
|
|
10
|
+
export function start(): void {
|
|
11
|
+
bind(window, "error", handler);
|
|
12
|
+
history = {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function handler(error: ErrorEvent): boolean {
|
|
16
|
+
let e = error["error"] || error;
|
|
17
|
+
// While rare, it's possible for code to fail repeatedly during the lifetime of the same page
|
|
18
|
+
// In those cases, we only want to log the failure first few times and not spam logs with redundant information.
|
|
19
|
+
if (!(e.message in history)) { history[e.message] = 0; }
|
|
20
|
+
if (history[e.message]++ >= Setting.ScriptErrorLimit) { return true; }
|
|
21
|
+
|
|
22
|
+
// Send back information only if the handled error has valid information
|
|
23
|
+
if (e && e.message) {
|
|
24
|
+
data = {
|
|
25
|
+
message: e.message,
|
|
26
|
+
line: error["lineno"],
|
|
27
|
+
column: error["colno"],
|
|
28
|
+
stack: e.stack,
|
|
29
|
+
source: error["filename"]
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// In certain cases, ResizeObserver could lead to flood of benign errors - especially when video element is involved.
|
|
33
|
+
// Reference Chromium issue: https://bugs.chromium.org/p/chromium/issues/detail?id=809574
|
|
34
|
+
// Even though it doesn't impact user experience, or show up in console, it can still flood error reporting through on error
|
|
35
|
+
// To mitigate that, we turn off Clarity's ResizeObserver on getting the first instance of this error
|
|
36
|
+
if (e.message.indexOf(Constant.ResizeObserver) >= 0) {
|
|
37
|
+
box.stop();
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
encode(Event.ScriptError);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
package/src/global.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as clarity from "@src/clarity";
|
|
2
|
+
|
|
3
|
+
// Expose clarity in a browser environment
|
|
4
|
+
// To be efficient about queuing up operations while Clarity is wiring up, we expose clarity.*(args) => clarity(*, args);
|
|
5
|
+
// This allows us to reprocess any calls that we missed once Clarity is available on the page
|
|
6
|
+
// Once Clarity script bundle is loaded on the page, we also initialize a "v" property that holds current version
|
|
7
|
+
// We use the presence or absence of "v" to determine if we are attempting to run a duplicate instance
|
|
8
|
+
(function(): void {
|
|
9
|
+
if (typeof window !== "undefined") {
|
|
10
|
+
const w = window as any;
|
|
11
|
+
const c = 'clarity';
|
|
12
|
+
|
|
13
|
+
// Do not execute or reset global "clarity" variable if a version of Clarity is already running on the page
|
|
14
|
+
if (w[c] && w[c].v) { return console.warn("Error CL001: Multiple Clarity tags detected."); }
|
|
15
|
+
|
|
16
|
+
// Re-wire global "clarity" variable to map it to current instance of Clarity
|
|
17
|
+
const queue = w[c] ? (w[c].q || []) : [];
|
|
18
|
+
w[c] = function(method: string, ...args: any[]): void { return clarity[method](...args); }
|
|
19
|
+
w[c].v = clarity.version;
|
|
20
|
+
while (queue.length > 0) { w[c](...queue.shift()); }
|
|
21
|
+
}
|
|
22
|
+
})();
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { BooleanFlag, Constant, Event, Setting } from "@clarity-types/data";
|
|
2
|
+
import { BrowsingContext, ClickState } from "@clarity-types/interaction";
|
|
3
|
+
import { Box } from "@clarity-types/layout";
|
|
4
|
+
import { bind } from "@src/core/event";
|
|
5
|
+
import { schedule } from "@src/core/task";
|
|
6
|
+
import { time } from "@src/core/time";
|
|
7
|
+
import { iframe } from "@src/layout/dom";
|
|
8
|
+
import offset from "@src/layout/offset";
|
|
9
|
+
import { link, target } from "@src/layout/target";
|
|
10
|
+
import encode from "./encode";
|
|
11
|
+
|
|
12
|
+
const UserInputTags = ["input", "textarea", "radio", "button", "canvas"];
|
|
13
|
+
export let state: ClickState[] = [];
|
|
14
|
+
|
|
15
|
+
export function start(): void {
|
|
16
|
+
reset();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function observe(root: Node): void {
|
|
20
|
+
bind(root, "click", handler.bind(this, Event.Click, root), true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function handler(event: Event, root: Node, evt: MouseEvent): void {
|
|
24
|
+
let frame = iframe(root);
|
|
25
|
+
let d = frame ? frame.contentDocument.documentElement : document.documentElement;
|
|
26
|
+
let x = "pageX" in evt ? Math.round(evt.pageX) : ("clientX" in evt ? Math.round(evt["clientX"] + d.scrollLeft) : null);
|
|
27
|
+
let y = "pageY" in evt ? Math.round(evt.pageY) : ("clientY" in evt ? Math.round(evt["clientY"] + d.scrollTop) : null);
|
|
28
|
+
// In case of iframe, we adjust (x,y) to be relative to top parent's origin
|
|
29
|
+
if (frame) {
|
|
30
|
+
let distance = offset(frame);
|
|
31
|
+
x = x ? x + Math.round(distance.x) : x;
|
|
32
|
+
y = y ? y + Math.round(distance.y) : y;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let t = target(evt);
|
|
36
|
+
// Find nearest anchor tag (<a/>) parent if current target node is part of one
|
|
37
|
+
// If present, we use the returned link element to populate text and link properties below
|
|
38
|
+
let a = link(t);
|
|
39
|
+
|
|
40
|
+
// Get layout rectangle for the target element
|
|
41
|
+
let l = layout(t as Element);
|
|
42
|
+
|
|
43
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail
|
|
44
|
+
// This property helps differentiate between a keyboard navigation vs. pointer click
|
|
45
|
+
// In case of a keyboard navigation, we use center of target element as (x,y)
|
|
46
|
+
if (evt.detail === 0 && l) {
|
|
47
|
+
x = Math.round(l.x + (l.w / 2));
|
|
48
|
+
y = Math.round(l.y + (l.h / 2));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let eX = l ? Math.max(Math.floor(((x - l.x) / l.w) * Setting.ClickPrecision), 0) : 0;
|
|
52
|
+
let eY = l ? Math.max(Math.floor(((y - l.y) / l.h) * Setting.ClickPrecision), 0) : 0;
|
|
53
|
+
|
|
54
|
+
// Check for null values before processing this event
|
|
55
|
+
if (x !== null && y !== null) {
|
|
56
|
+
state.push({
|
|
57
|
+
time: time(), event, data: {
|
|
58
|
+
target: t,
|
|
59
|
+
x,
|
|
60
|
+
y,
|
|
61
|
+
eX,
|
|
62
|
+
eY,
|
|
63
|
+
button: evt.button,
|
|
64
|
+
reaction: reaction(t),
|
|
65
|
+
context: context(a),
|
|
66
|
+
text: text(t),
|
|
67
|
+
link: a ? a.href : null,
|
|
68
|
+
hash: null
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
schedule(encode.bind(this, event));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function text(element: Node): string {
|
|
76
|
+
let output = null;
|
|
77
|
+
if (element) {
|
|
78
|
+
// Grab text using "textContent" for most HTMLElements, however, use "value" for HTMLInputElements and "alt" for HTMLImageElement.
|
|
79
|
+
let t = element.textContent || (element as HTMLInputElement).value || (element as HTMLImageElement).alt;
|
|
80
|
+
if (t) {
|
|
81
|
+
// Trim any spaces at the beginning or at the end of string
|
|
82
|
+
// Also, replace multiple occurrence of space characters with a single white space
|
|
83
|
+
// Finally, send only first few characters as specified by the Setting
|
|
84
|
+
output = t.trim().replace(/\s+/g, Constant.Space).substr(0, Setting.ClickText);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return output;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function reaction(element: Node): BooleanFlag {
|
|
91
|
+
if (element.nodeType === Node.ELEMENT_NODE) {
|
|
92
|
+
let tag = (element as HTMLElement).tagName.toLowerCase();
|
|
93
|
+
if (UserInputTags.indexOf(tag) >= 0) {
|
|
94
|
+
return BooleanFlag.False;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return BooleanFlag.True;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function layout(element: Element): Box {
|
|
101
|
+
let box: Box = null;
|
|
102
|
+
let de = document.documentElement;
|
|
103
|
+
if (typeof element.getBoundingClientRect === "function") {
|
|
104
|
+
// getBoundingClientRect returns rectangle relative positioning to viewport
|
|
105
|
+
let rect = element.getBoundingClientRect();
|
|
106
|
+
|
|
107
|
+
if (rect && rect.width > 0 && rect.height > 0) {
|
|
108
|
+
// Add viewport's scroll position to rectangle to get position relative to document origin
|
|
109
|
+
// Also: using Math.floor() instead of Math.round() because in Edge,
|
|
110
|
+
// getBoundingClientRect returns partial pixel values (e.g. 162.5px) and Chrome already
|
|
111
|
+
// floors the value (e.g. 162px). This keeps consistent behavior across browsers.
|
|
112
|
+
box = {
|
|
113
|
+
x: Math.floor(rect.left + ("pageXOffset" in window ? window.pageXOffset : de.scrollLeft)),
|
|
114
|
+
y: Math.floor(rect.top + ("pageYOffset" in window ? window.pageYOffset : de.scrollTop)),
|
|
115
|
+
w: Math.floor(rect.width),
|
|
116
|
+
h: Math.floor(rect.height)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return box;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function context(a: HTMLAnchorElement): BrowsingContext {
|
|
124
|
+
if (a && a.hasAttribute(Constant.Target)) {
|
|
125
|
+
switch (a.getAttribute(Constant.Target)) {
|
|
126
|
+
case Constant.Blank: return BrowsingContext.Blank;
|
|
127
|
+
case Constant.Parent: return BrowsingContext.Parent;
|
|
128
|
+
case Constant.Top: return BrowsingContext.Top;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return BrowsingContext.Self;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function reset(): void {
|
|
135
|
+
state = [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function stop(): void {
|
|
139
|
+
reset();
|
|
140
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Event, Token } from "@clarity-types/data";
|
|
2
|
+
import scrub from "@src/core/scrub";
|
|
3
|
+
import { time } from "@src/core/time";
|
|
4
|
+
import * as baseline from "@src/data/baseline";
|
|
5
|
+
import { queue } from "@src/data/upload";
|
|
6
|
+
import { metadata } from "@src/layout/target";
|
|
7
|
+
import * as click from "./click";
|
|
8
|
+
import * as input from "./input";
|
|
9
|
+
import * as pointer from "./pointer";
|
|
10
|
+
import * as resize from "./resize";
|
|
11
|
+
import * as scroll from "./scroll";
|
|
12
|
+
import * as selection from "./selection";
|
|
13
|
+
import * as timeline from "./timeline";
|
|
14
|
+
import * as unload from "./unload";
|
|
15
|
+
import * as visibility from "./visibility";
|
|
16
|
+
|
|
17
|
+
export default async function (type: Event): Promise<void> {
|
|
18
|
+
let t = time();
|
|
19
|
+
let tokens: Token[] = [t, type];
|
|
20
|
+
switch (type) {
|
|
21
|
+
case Event.MouseDown:
|
|
22
|
+
case Event.MouseUp:
|
|
23
|
+
case Event.MouseMove:
|
|
24
|
+
case Event.MouseWheel:
|
|
25
|
+
case Event.DoubleClick:
|
|
26
|
+
case Event.TouchStart:
|
|
27
|
+
case Event.TouchEnd:
|
|
28
|
+
case Event.TouchMove:
|
|
29
|
+
case Event.TouchCancel:
|
|
30
|
+
for (let i = 0; i < pointer.state.length; i++) {
|
|
31
|
+
let entry = pointer.state[i];
|
|
32
|
+
let pTarget = metadata(entry.data.target as Node, entry.event);
|
|
33
|
+
if (pTarget.id > 0) {
|
|
34
|
+
tokens = [entry.time, entry.event];
|
|
35
|
+
tokens.push(pTarget.id);
|
|
36
|
+
tokens.push(entry.data.x);
|
|
37
|
+
tokens.push(entry.data.y);
|
|
38
|
+
queue(tokens);
|
|
39
|
+
baseline.track(entry.event, entry.data.x, entry.data.y);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
pointer.reset();
|
|
43
|
+
break;
|
|
44
|
+
case Event.Click:
|
|
45
|
+
for (let i = 0; i < click.state.length; i++) {
|
|
46
|
+
let entry = click.state[i];
|
|
47
|
+
let cTarget = metadata(entry.data.target as Node, entry.event);
|
|
48
|
+
tokens = [entry.time, entry.event];
|
|
49
|
+
tokens.push(cTarget.id);
|
|
50
|
+
tokens.push(entry.data.x);
|
|
51
|
+
tokens.push(entry.data.y);
|
|
52
|
+
tokens.push(entry.data.eX);
|
|
53
|
+
tokens.push(entry.data.eY);
|
|
54
|
+
tokens.push(entry.data.button);
|
|
55
|
+
tokens.push(entry.data.reaction);
|
|
56
|
+
tokens.push(entry.data.context);
|
|
57
|
+
tokens.push(scrub(entry.data.text, "click", cTarget.privacy));
|
|
58
|
+
tokens.push(entry.data.link);
|
|
59
|
+
tokens.push(cTarget.hash);
|
|
60
|
+
queue(tokens);
|
|
61
|
+
timeline.track(entry.time, entry.event, cTarget.hash, entry.data.x, entry.data.y, entry.data.reaction, entry.data.context);
|
|
62
|
+
}
|
|
63
|
+
click.reset();
|
|
64
|
+
break;
|
|
65
|
+
case Event.Resize:
|
|
66
|
+
let r = resize.data;
|
|
67
|
+
tokens.push(r.width);
|
|
68
|
+
tokens.push(r.height);
|
|
69
|
+
baseline.track(type, r.width, r.height);
|
|
70
|
+
resize.reset();
|
|
71
|
+
queue(tokens);
|
|
72
|
+
break;
|
|
73
|
+
case Event.Unload:
|
|
74
|
+
let u = unload.data;
|
|
75
|
+
tokens.push(u.name);
|
|
76
|
+
unload.reset();
|
|
77
|
+
queue(tokens);
|
|
78
|
+
break;
|
|
79
|
+
case Event.Input:
|
|
80
|
+
for (let i = 0; i < input.state.length; i++) {
|
|
81
|
+
let entry = input.state[i];
|
|
82
|
+
let iTarget = metadata(entry.data.target as Node, entry.event);
|
|
83
|
+
tokens = [entry.time, entry.event];
|
|
84
|
+
tokens.push(iTarget.id);
|
|
85
|
+
tokens.push(entry.data.value);
|
|
86
|
+
queue(tokens);
|
|
87
|
+
}
|
|
88
|
+
input.reset();
|
|
89
|
+
break;
|
|
90
|
+
case Event.Selection:
|
|
91
|
+
let s = selection.data;
|
|
92
|
+
if (s) {
|
|
93
|
+
let startTarget = metadata(s.start as Node, type);
|
|
94
|
+
let endTarget = metadata(s.end as Node, type);
|
|
95
|
+
tokens.push(startTarget.id);
|
|
96
|
+
tokens.push(s.startOffset);
|
|
97
|
+
tokens.push(endTarget.id);
|
|
98
|
+
tokens.push(s.endOffset);
|
|
99
|
+
selection.reset();
|
|
100
|
+
queue(tokens);
|
|
101
|
+
}
|
|
102
|
+
break;
|
|
103
|
+
case Event.Scroll:
|
|
104
|
+
for (let i = 0; i < scroll.state.length; i++) {
|
|
105
|
+
let entry = scroll.state[i];
|
|
106
|
+
let sTarget = metadata(entry.data.target as Node, entry.event);
|
|
107
|
+
if (sTarget.id > 0) {
|
|
108
|
+
tokens = [entry.time, entry.event];
|
|
109
|
+
tokens.push(sTarget.id);
|
|
110
|
+
tokens.push(entry.data.x);
|
|
111
|
+
tokens.push(entry.data.y);
|
|
112
|
+
queue(tokens);
|
|
113
|
+
baseline.track(entry.event, entry.data.x, entry.data.y);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
scroll.reset();
|
|
117
|
+
break;
|
|
118
|
+
case Event.Timeline:
|
|
119
|
+
for (let i = 0; i < timeline.updates.length; i++) {
|
|
120
|
+
let entry = timeline.updates[i];
|
|
121
|
+
tokens = [entry.time, entry.event];
|
|
122
|
+
tokens.push(entry.data.type);
|
|
123
|
+
tokens.push(entry.data.hash);
|
|
124
|
+
tokens.push(entry.data.x);
|
|
125
|
+
tokens.push(entry.data.y);
|
|
126
|
+
tokens.push(entry.data.reaction);
|
|
127
|
+
tokens.push(entry.data.context);
|
|
128
|
+
queue(tokens, false);
|
|
129
|
+
}
|
|
130
|
+
timeline.reset();
|
|
131
|
+
break;
|
|
132
|
+
case Event.Visibility:
|
|
133
|
+
let v = visibility.data;
|
|
134
|
+
tokens.push(v.visible);
|
|
135
|
+
queue(tokens);
|
|
136
|
+
baseline.visibility(t, v.visible);
|
|
137
|
+
visibility.reset();
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as click from "@src/interaction/click";
|
|
2
|
+
import * as input from "@src/interaction/input";
|
|
3
|
+
import * as pointer from "@src/interaction/pointer";
|
|
4
|
+
import * as resize from "@src/interaction/resize";
|
|
5
|
+
import * as scroll from "@src/interaction/scroll";
|
|
6
|
+
import * as selection from "@src/interaction/selection";
|
|
7
|
+
import * as timeline from "@src/interaction/timeline";
|
|
8
|
+
import * as unload from "@src/interaction/unload";
|
|
9
|
+
import * as visibility from "@src/interaction/visibility";
|
|
10
|
+
|
|
11
|
+
export function start(): void {
|
|
12
|
+
timeline.start();
|
|
13
|
+
click.start();
|
|
14
|
+
pointer.start();
|
|
15
|
+
input.start();
|
|
16
|
+
resize.start();
|
|
17
|
+
visibility.start();
|
|
18
|
+
scroll.start();
|
|
19
|
+
selection.start();
|
|
20
|
+
unload.start();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stop(): void {
|
|
24
|
+
timeline.stop();
|
|
25
|
+
click.stop();
|
|
26
|
+
pointer.stop();
|
|
27
|
+
input.stop();
|
|
28
|
+
resize.stop();
|
|
29
|
+
visibility.stop();
|
|
30
|
+
scroll.stop();
|
|
31
|
+
selection.stop();
|
|
32
|
+
unload.stop()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function observe(root: Node): void {
|
|
36
|
+
scroll.observe(root);
|
|
37
|
+
// Only monitor following interactions if the root node is a document
|
|
38
|
+
// In case of shadow DOM, following events automatically bubble up to the parent document.
|
|
39
|
+
if (root.nodeType === Node.DOCUMENT_NODE) {
|
|
40
|
+
click.observe(root);
|
|
41
|
+
pointer.observe(root);
|
|
42
|
+
input.observe(root);
|
|
43
|
+
selection.observe(root);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Event } from "@clarity-types/data";
|
|
2
|
+
import { InputData, InputState, Setting } from "@clarity-types/interaction";
|
|
3
|
+
import { bind } from "@src/core/event";
|
|
4
|
+
import scrub from "@src/core/scrub";
|
|
5
|
+
import { schedule } from "@src/core/task";
|
|
6
|
+
import { time } from "@src/core/time";
|
|
7
|
+
import { clearTimeout, setTimeout } from "@src/core/timeout";
|
|
8
|
+
import { get } from "@src/layout/dom";
|
|
9
|
+
import encode from "./encode";
|
|
10
|
+
import { target } from "@src/layout/target";
|
|
11
|
+
|
|
12
|
+
let timeout: number = null;
|
|
13
|
+
export let state: InputState[] = [];
|
|
14
|
+
|
|
15
|
+
export function start(): void {
|
|
16
|
+
reset();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function observe(root: Node): void {
|
|
20
|
+
bind(root, "input", recompute, true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function recompute(evt: UIEvent): void {
|
|
24
|
+
let input = target(evt) as HTMLInputElement;
|
|
25
|
+
let value = get(input);
|
|
26
|
+
if (input && input.type && value) {
|
|
27
|
+
let v;
|
|
28
|
+
switch (input.type) {
|
|
29
|
+
case "radio":
|
|
30
|
+
case "checkbox":
|
|
31
|
+
v = input.checked ? "true" : "false";
|
|
32
|
+
break;
|
|
33
|
+
case "range":
|
|
34
|
+
v = input.value;
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
v = scrub(input.value, "input", value.metadata.privacy);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let data: InputData = { target: input, value: v };
|
|
42
|
+
|
|
43
|
+
// If last entry in the queue is for the same target node as the current one, remove it so we can later swap it with current data.
|
|
44
|
+
if (state.length > 0 && (state[state.length - 1].data.target === data.target)) { state.pop(); }
|
|
45
|
+
|
|
46
|
+
state.push({ time: time(), event: Event.Input, data });
|
|
47
|
+
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
timeout = setTimeout(process, Setting.LookAhead, Event.Input);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function process(event: Event): void {
|
|
54
|
+
schedule(encode.bind(this, event));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function reset(): void {
|
|
58
|
+
state = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stop(): void {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
reset();
|
|
64
|
+
}
|