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
package/src/core/task.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { AsyncTask, Priority, RequestIdleCallbackDeadline, RequestIdleCallbackOptions, Task, Timer } from "@clarity-types/core";
|
|
2
|
+
import { Setting, TaskFunction, TaskResolve, Tasks } from "@clarity-types/core";
|
|
3
|
+
import { Code, Metric, Severity } from "@clarity-types/data";
|
|
4
|
+
import * as metadata from "@src/data/metadata";
|
|
5
|
+
import * as metric from "@src/data/metric";
|
|
6
|
+
import * as internal from "@src/diagnostic/internal";
|
|
7
|
+
|
|
8
|
+
// Track the start time to be able to compute duration at the end of the task
|
|
9
|
+
const idleTimeout = 5000;
|
|
10
|
+
let tracker: Tasks = {};
|
|
11
|
+
let queuedTasks: AsyncTask[] = [];
|
|
12
|
+
let activeTask: AsyncTask = null;
|
|
13
|
+
let pauseTask: Promise<void> = null;
|
|
14
|
+
let resumeResolve: TaskResolve = null;
|
|
15
|
+
|
|
16
|
+
export function pause(): void {
|
|
17
|
+
if (pauseTask === null) {
|
|
18
|
+
pauseTask = new Promise<void>((resolve: TaskResolve): void => {
|
|
19
|
+
resumeResolve = resolve;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resume(): void {
|
|
25
|
+
if (pauseTask) {
|
|
26
|
+
resumeResolve();
|
|
27
|
+
pauseTask = null;
|
|
28
|
+
if (activeTask === null) { run(); }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function reset(): void {
|
|
33
|
+
tracker = {};
|
|
34
|
+
queuedTasks = [];
|
|
35
|
+
activeTask = null;
|
|
36
|
+
pauseTask = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function schedule(task: TaskFunction, priority: Priority = Priority.Normal): Promise<void> {
|
|
40
|
+
// If this task is already scheduled, skip it
|
|
41
|
+
for (let q of queuedTasks) {
|
|
42
|
+
if (q.task === task) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let promise = new Promise<void>((resolve: TaskResolve): void => {
|
|
48
|
+
let insert = priority === Priority.High ? "unshift" : "push";
|
|
49
|
+
// Queue this task for asynchronous execution later
|
|
50
|
+
// We also store a unique page identifier (id) along with the task to ensure
|
|
51
|
+
// ensure that we do not accidentally execute this task in context of a different page
|
|
52
|
+
queuedTasks[insert]({ task, resolve, id: metadata.id() });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// If there is no active task running, and Clarity is not in pause state,
|
|
56
|
+
// invoke the first task in the queue synchronously. This ensures that we don't yield the thread during unload event
|
|
57
|
+
if (activeTask === null && pauseTask === null) { run(); }
|
|
58
|
+
|
|
59
|
+
return promise;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function run(): void {
|
|
63
|
+
let entry = queuedTasks.shift();
|
|
64
|
+
if (entry) {
|
|
65
|
+
activeTask = entry;
|
|
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
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function state(timer: Timer): Task {
|
|
85
|
+
let id = key(timer);
|
|
86
|
+
if (id in tracker) {
|
|
87
|
+
let elapsed = performance.now() - tracker[id].start;
|
|
88
|
+
return (elapsed > tracker[id].yield) ? Task.Wait : Task.Run;
|
|
89
|
+
}
|
|
90
|
+
// If this task is no longer being tracked, send stop message to the caller
|
|
91
|
+
return Task.Stop;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function start(timer: Timer): void {
|
|
95
|
+
tracker[key(timer)] = { start: performance.now(), calls: 0, yield: Setting.LongTask };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function restart(timer: Timer): void {
|
|
99
|
+
let id = key(timer);
|
|
100
|
+
if (tracker && tracker[id]) {
|
|
101
|
+
let c = tracker[id].calls;
|
|
102
|
+
let y = tracker[id].yield;
|
|
103
|
+
start(timer);
|
|
104
|
+
tracker[id].calls = c + 1;
|
|
105
|
+
tracker[id].yield = y;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function stop(timer: Timer): void {
|
|
110
|
+
let end = performance.now();
|
|
111
|
+
let id = key(timer);
|
|
112
|
+
let duration = end - tracker[id].start;
|
|
113
|
+
metric.sum(timer.cost, duration);
|
|
114
|
+
metric.count(Metric.InvokeCount);
|
|
115
|
+
|
|
116
|
+
// For the first execution, which is synchronous, time is automatically counted towards TotalDuration.
|
|
117
|
+
// However, for subsequent asynchronous runs, we need to manually update TotalDuration metric.
|
|
118
|
+
if (tracker[id].calls > 0) { metric.sum(Metric.TotalCost, duration); }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function suspend(timer: Timer): Promise<Task> {
|
|
122
|
+
// Suspend and yield the thread only if the task is still being tracked
|
|
123
|
+
// It's possible that Clarity is wrapping up instrumentation on a page and we are still in the middle of an async task.
|
|
124
|
+
// In that case, we do not wish to continue yielding thread.
|
|
125
|
+
// Instead, we will turn async task into a sync task and maximize our chances of getting some data back.
|
|
126
|
+
let id = key(timer);
|
|
127
|
+
if (id in tracker) {
|
|
128
|
+
stop(timer);
|
|
129
|
+
tracker[id].yield = (await wait()).timeRemaining();
|
|
130
|
+
restart(timer);
|
|
131
|
+
}
|
|
132
|
+
// After we are done with suspending task, ensure that we are still operating in the right context
|
|
133
|
+
// If the task is still being tracked, continue running the task, otherwise ask caller to stop execution
|
|
134
|
+
return id in tracker ? Task.Run : Task.Stop;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function key(timer: Timer): string {
|
|
138
|
+
return `${timer.id}.${timer.cost}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function wait(): Promise<RequestIdleCallbackDeadline> {
|
|
142
|
+
if (pauseTask) { await pauseTask; }
|
|
143
|
+
return new Promise<RequestIdleCallbackDeadline>((resolve: (deadline: RequestIdleCallbackDeadline) => void): void => {
|
|
144
|
+
requestIdleCallback(resolve, { timeout: idleTimeout });
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Use native implementation of requestIdleCallback if it exists.
|
|
149
|
+
// Otherwise, fall back to a custom implementation using requestAnimationFrame & MessageChannel.
|
|
150
|
+
// While it's not possible to build a perfect polyfill given the nature of this API, the following code attempts to get close.
|
|
151
|
+
// Background context: requestAnimationFrame invokes the js code right before: style, layout and paint computation within the frame.
|
|
152
|
+
// This means, that any code that runs as part of requestAnimationFrame will by default be blocking in nature. Not what we want.
|
|
153
|
+
// For non-blocking behavior, We need to know when browser has finished painiting. This can be accomplished in two different ways (hacks):
|
|
154
|
+
// (1) Use MessageChannel to pass the message, and browser will receive the message right after pain event has occured.
|
|
155
|
+
// (2) Use setTimeout call within requestAnimationFrame. This also works, but there's a risk that browser may throttle setTimeout calls.
|
|
156
|
+
// Given this information, we are currently using (1) from above. More information on (2) as well as some additional context is below:
|
|
157
|
+
// https://developer.mozilla.org/en-US/docs/Mozilla/Firefox/Performance_best_practices_for_Firefox_fe_engineers
|
|
158
|
+
function requestIdleCallbackPolyfill(callback: (deadline: RequestIdleCallbackDeadline) => void, options: RequestIdleCallbackOptions): void {
|
|
159
|
+
const startTime = performance.now();
|
|
160
|
+
const channel = new MessageChannel();
|
|
161
|
+
const incoming = channel.port1;
|
|
162
|
+
const outgoing = channel.port2;
|
|
163
|
+
incoming.onmessage = (event: MessageEvent): void => {
|
|
164
|
+
let currentTime = performance.now();
|
|
165
|
+
let elapsed = currentTime - startTime;
|
|
166
|
+
let duration = currentTime - event.data;
|
|
167
|
+
if (duration > Setting.LongTask && elapsed < options.timeout) {
|
|
168
|
+
requestAnimationFrame((): void => { outgoing.postMessage(currentTime); });
|
|
169
|
+
} else {
|
|
170
|
+
let didTimeout = elapsed > options.timeout;
|
|
171
|
+
callback({
|
|
172
|
+
didTimeout,
|
|
173
|
+
timeRemaining: (): number => didTimeout ? Setting.LongTask : Math.max(0, Setting.LongTask - duration)
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
requestAnimationFrame((): void => { outgoing.postMessage(performance.now()); });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let requestIdleCallback = window["requestIdleCallback"] || requestIdleCallbackPolyfill;
|
package/src/core/time.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
let startTime = 0;
|
|
2
|
+
|
|
3
|
+
export function start(): void {
|
|
4
|
+
startTime = performance.now();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function time(ts: number = null): number {
|
|
8
|
+
ts = ts ? ts : performance.now();
|
|
9
|
+
return Math.max(Math.round(ts - startTime), 0);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function stop(): void {
|
|
13
|
+
startTime = 0;
|
|
14
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Event } from "@clarity-types/data";
|
|
2
|
+
import measure from "./measure";
|
|
3
|
+
|
|
4
|
+
export function setTimeout(handler: (event?: Event | boolean) => void, timeout: number, event?: Event): number {
|
|
5
|
+
return window.setTimeout(measure(handler), timeout, event);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function clearTimeout(handle: number): void {
|
|
9
|
+
return window.clearTimeout(handle);
|
|
10
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Event, BooleanFlag } from "@clarity-types/data";
|
|
2
|
+
import { BaselineData, BaselineState } from "@clarity-types/data";
|
|
3
|
+
import { time } from "@src/core/time";
|
|
4
|
+
import encode from "@src/data/encode";
|
|
5
|
+
|
|
6
|
+
export let state: BaselineState = null;
|
|
7
|
+
let buffer: BaselineData = null;
|
|
8
|
+
let update: boolean = false;
|
|
9
|
+
|
|
10
|
+
export function start(): void {
|
|
11
|
+
update = false;
|
|
12
|
+
reset();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function reset(): void {
|
|
16
|
+
// Baseline state holds the previous values - if it is updated in the current payload,
|
|
17
|
+
// reset the state to current value after sending the previous state
|
|
18
|
+
if (update) {
|
|
19
|
+
state = { time: time(), event: Event.Baseline, data: {
|
|
20
|
+
visible: buffer.visible,
|
|
21
|
+
docWidth: buffer.docWidth,
|
|
22
|
+
docHeight: buffer.docHeight,
|
|
23
|
+
screenWidth: buffer.screenWidth,
|
|
24
|
+
screenHeight: buffer.screenHeight,
|
|
25
|
+
scrollX: buffer.scrollX,
|
|
26
|
+
scrollY: buffer.scrollY,
|
|
27
|
+
pointerX: buffer.pointerX,
|
|
28
|
+
pointerY: buffer.pointerY,
|
|
29
|
+
activityTime: buffer.activityTime
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
buffer = buffer ? buffer : {
|
|
34
|
+
visible: BooleanFlag.True,
|
|
35
|
+
docWidth: 0,
|
|
36
|
+
docHeight: 0,
|
|
37
|
+
screenWidth: 0,
|
|
38
|
+
screenHeight: 0,
|
|
39
|
+
scrollX: 0,
|
|
40
|
+
scrollY: 0,
|
|
41
|
+
pointerX: 0,
|
|
42
|
+
pointerY: 0,
|
|
43
|
+
activityTime: 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function track(event: Event, x: number, y: number): void {
|
|
48
|
+
switch (event) {
|
|
49
|
+
case Event.Document:
|
|
50
|
+
buffer.docWidth = x;
|
|
51
|
+
buffer.docHeight = y;
|
|
52
|
+
break;
|
|
53
|
+
case Event.Resize:
|
|
54
|
+
buffer.screenWidth = x;
|
|
55
|
+
buffer.screenHeight = y;
|
|
56
|
+
break;
|
|
57
|
+
case Event.Scroll:
|
|
58
|
+
buffer.scrollX = x;
|
|
59
|
+
buffer.scrollY = y;
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
buffer.pointerX = x;
|
|
63
|
+
buffer.pointerY = y;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
update = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function activity(t: number): void {
|
|
70
|
+
buffer.activityTime = t;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function visibility(t: number, visible: string): void {
|
|
74
|
+
buffer.visible = visible === "visible" ? BooleanFlag.True : BooleanFlag.False;
|
|
75
|
+
if (!buffer.visible) {
|
|
76
|
+
activity(t);
|
|
77
|
+
}
|
|
78
|
+
update = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function compute(): void {
|
|
82
|
+
if (update) {
|
|
83
|
+
encode(Event.Baseline);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function stop(): void {
|
|
88
|
+
reset();
|
|
89
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Constant } from "@clarity-types/data";
|
|
2
|
+
|
|
3
|
+
const supported = Constant.CompressionStream in window;
|
|
4
|
+
|
|
5
|
+
export default async function(input: string): Promise<Uint8Array> {
|
|
6
|
+
try {
|
|
7
|
+
if (supported) {
|
|
8
|
+
// Create a readable stream from given input string and
|
|
9
|
+
// pipe it through text encoder and compression stream to gzip it
|
|
10
|
+
const stream = new ReadableStream({async start(controller) {
|
|
11
|
+
controller.enqueue(input);
|
|
12
|
+
controller.close();
|
|
13
|
+
}}).pipeThrough(new TextEncoderStream()).pipeThrough(new window[Constant.CompressionStream]("gzip"));
|
|
14
|
+
return new Uint8Array(await read(stream));
|
|
15
|
+
}
|
|
16
|
+
} catch { /* do nothing */ }
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function read(stream: ReadableStream): Promise<number[]> {
|
|
21
|
+
const reader = stream.getReader();
|
|
22
|
+
const chunks:number[] = [];
|
|
23
|
+
let done = false;
|
|
24
|
+
let value: number[] = [];
|
|
25
|
+
while (!done) {
|
|
26
|
+
({ done, value } = await reader.read());
|
|
27
|
+
if (done) { return chunks; }
|
|
28
|
+
chunks.push(...value);
|
|
29
|
+
}
|
|
30
|
+
return chunks;
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Constant, CustomData, Event } from "@clarity-types/data";
|
|
2
|
+
import * as core from "@src/core";
|
|
3
|
+
import encode from "./encode";
|
|
4
|
+
|
|
5
|
+
export let data: CustomData = null;
|
|
6
|
+
|
|
7
|
+
export function event(key: string, value: string): void {
|
|
8
|
+
if (core.active() &&
|
|
9
|
+
key &&
|
|
10
|
+
value &&
|
|
11
|
+
typeof key === Constant.String &&
|
|
12
|
+
typeof value === Constant.String &&
|
|
13
|
+
key.length < 255 &&
|
|
14
|
+
value.length < 255) {
|
|
15
|
+
data = { key, value};
|
|
16
|
+
encode(Event.Custom);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Check, Event, Dimension, DimensionData, Setting } from "@clarity-types/data";
|
|
2
|
+
import * as limit from "@src/data/limit";
|
|
3
|
+
import encode from "./encode";
|
|
4
|
+
|
|
5
|
+
export let data: DimensionData = null;
|
|
6
|
+
export let updates: DimensionData = null;
|
|
7
|
+
|
|
8
|
+
export function start(): void {
|
|
9
|
+
data = {};
|
|
10
|
+
updates = {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function stop(): void {
|
|
14
|
+
data = {};
|
|
15
|
+
updates = {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function log(dimension: Dimension, value: string): void {
|
|
19
|
+
// Check valid value before moving ahead
|
|
20
|
+
if (value) {
|
|
21
|
+
// Ensure received value is casted into a string if it wasn't a string to begin with
|
|
22
|
+
value = `${value}`;
|
|
23
|
+
if (!(dimension in data)) { data[dimension] = []; }
|
|
24
|
+
if (data[dimension].indexOf(value) < 0) {
|
|
25
|
+
data[dimension].push(value);
|
|
26
|
+
// If this is a new value, track it as part of updates object
|
|
27
|
+
// This allows us to only send back new values in subsequent payloads
|
|
28
|
+
if (!(dimension in updates)) { updates[dimension] = []; }
|
|
29
|
+
updates[dimension].push(value);
|
|
30
|
+
// Limit check to ensure we have a cap on number of dimensions we can collect
|
|
31
|
+
if (data[dimension].length > Setting.CollectionLimit) { limit.trigger(Check.Collection); }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compute(): void {
|
|
37
|
+
encode(Event.Dimension);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function reset(): void {
|
|
41
|
+
updates = {};
|
|
42
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Event, Token } from "@clarity-types/data";
|
|
2
|
+
import { time } from "@src/core/time";
|
|
3
|
+
import * as baseline from "@src/data/baseline";
|
|
4
|
+
import * as custom from "@src/data/custom";
|
|
5
|
+
import * as dimension from "@src/data/dimension";
|
|
6
|
+
import * as limit from "@src/data/limit";
|
|
7
|
+
import * as metric from "@src/data/metric";
|
|
8
|
+
import * as ping from "@src/data/ping";
|
|
9
|
+
import * as summary from "@src/data/summary";
|
|
10
|
+
import * as upgrade from "@src/data/upgrade";
|
|
11
|
+
import * as variable from "@src/data/variable";
|
|
12
|
+
import { queue, track } from "./upload";
|
|
13
|
+
|
|
14
|
+
export default function(event: Event): void {
|
|
15
|
+
let t = time();
|
|
16
|
+
let tokens: Token[] = [t, event];
|
|
17
|
+
switch (event) {
|
|
18
|
+
case Event.Baseline:
|
|
19
|
+
let b = baseline.state;
|
|
20
|
+
if (b) {
|
|
21
|
+
tokens = [b.time, b.event];
|
|
22
|
+
tokens.push(b.data.visible);
|
|
23
|
+
tokens.push(b.data.docWidth);
|
|
24
|
+
tokens.push(b.data.docHeight);
|
|
25
|
+
tokens.push(b.data.screenWidth);
|
|
26
|
+
tokens.push(b.data.screenHeight);
|
|
27
|
+
tokens.push(b.data.scrollX);
|
|
28
|
+
tokens.push(b.data.scrollY);
|
|
29
|
+
tokens.push(b.data.pointerX);
|
|
30
|
+
tokens.push(b.data.pointerY);
|
|
31
|
+
tokens.push(b.data.activityTime);
|
|
32
|
+
queue(tokens, false);
|
|
33
|
+
}
|
|
34
|
+
baseline.reset();
|
|
35
|
+
break;
|
|
36
|
+
case Event.Ping:
|
|
37
|
+
tokens.push(ping.data.gap);
|
|
38
|
+
queue(tokens);
|
|
39
|
+
break;
|
|
40
|
+
case Event.Limit:
|
|
41
|
+
tokens.push(limit.data.check);
|
|
42
|
+
queue(tokens, false);
|
|
43
|
+
break;
|
|
44
|
+
case Event.Upgrade:
|
|
45
|
+
tokens.push(upgrade.data.key);
|
|
46
|
+
queue(tokens);
|
|
47
|
+
break;
|
|
48
|
+
case Event.Upload:
|
|
49
|
+
tokens.push(track.sequence);
|
|
50
|
+
tokens.push(track.attempts);
|
|
51
|
+
tokens.push(track.status);
|
|
52
|
+
queue(tokens, false);
|
|
53
|
+
break;
|
|
54
|
+
case Event.Custom:
|
|
55
|
+
tokens.push(custom.data.key);
|
|
56
|
+
tokens.push(custom.data.value);
|
|
57
|
+
queue(tokens);
|
|
58
|
+
break;
|
|
59
|
+
case Event.Variable:
|
|
60
|
+
let variableKeys = Object.keys(variable.data);
|
|
61
|
+
if (variableKeys.length > 0) {
|
|
62
|
+
for (let v of variableKeys) {
|
|
63
|
+
tokens.push(v);
|
|
64
|
+
tokens.push(variable.data[v]);
|
|
65
|
+
}
|
|
66
|
+
variable.reset();
|
|
67
|
+
queue(tokens, false);
|
|
68
|
+
}
|
|
69
|
+
break;
|
|
70
|
+
case Event.Metric:
|
|
71
|
+
let metricKeys = Object.keys(metric.updates);
|
|
72
|
+
if (metricKeys.length > 0) {
|
|
73
|
+
for (let m of metricKeys) {
|
|
74
|
+
let key = parseInt(m, 10);
|
|
75
|
+
tokens.push(key);
|
|
76
|
+
// For computation, we need microseconds precision that performance.now() API offers
|
|
77
|
+
// However, for data over the wire, we round it off to milliseconds precision.
|
|
78
|
+
tokens.push(Math.round(metric.updates[m]));
|
|
79
|
+
}
|
|
80
|
+
metric.reset();
|
|
81
|
+
queue(tokens, false);
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case Event.Dimension:
|
|
85
|
+
let dimensionKeys = Object.keys(dimension.updates);
|
|
86
|
+
if (dimensionKeys.length > 0) {
|
|
87
|
+
for (let d of dimensionKeys) {
|
|
88
|
+
let key = parseInt(d, 10);
|
|
89
|
+
tokens.push(key);
|
|
90
|
+
tokens.push(dimension.updates[d]);
|
|
91
|
+
}
|
|
92
|
+
dimension.reset();
|
|
93
|
+
queue(tokens, false);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case Event.Summary:
|
|
97
|
+
let eventKeys = Object.keys(summary.data);
|
|
98
|
+
if (eventKeys.length > 0) {
|
|
99
|
+
for (let e of eventKeys) {
|
|
100
|
+
let key = parseInt(e, 10);
|
|
101
|
+
tokens.push(key);
|
|
102
|
+
tokens.push([].concat(...summary.data[e]));
|
|
103
|
+
}
|
|
104
|
+
summary.reset();
|
|
105
|
+
queue(tokens, false);
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BooleanFlag, Token, Upload, Envelope } from "@clarity-types/data";
|
|
2
|
+
import { time } from "@src/core/time";
|
|
3
|
+
import version from "@src/core/version";
|
|
4
|
+
import * as metadata from "@src/data/metadata";
|
|
5
|
+
|
|
6
|
+
export let data: Envelope = null;
|
|
7
|
+
|
|
8
|
+
export function start(): void {
|
|
9
|
+
const m = metadata.data;
|
|
10
|
+
data = {
|
|
11
|
+
version,
|
|
12
|
+
sequence: 0,
|
|
13
|
+
start: 0,
|
|
14
|
+
duration: 0,
|
|
15
|
+
projectId: m.projectId,
|
|
16
|
+
userId: m.userId,
|
|
17
|
+
sessionId: m.sessionId,
|
|
18
|
+
pageNum: m.pageNum,
|
|
19
|
+
upload: Upload.Async,
|
|
20
|
+
end: BooleanFlag.False
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function stop(): void {
|
|
25
|
+
data = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function envelope(last: boolean): Token[] {
|
|
29
|
+
data.start = data.start + data.duration;
|
|
30
|
+
data.duration = time() - data.start;
|
|
31
|
+
data.sequence++;
|
|
32
|
+
data.upload = last && "sendBeacon" in navigator ? Upload.Beacon : Upload.Async;
|
|
33
|
+
data.end = last ? BooleanFlag.True : BooleanFlag.False;
|
|
34
|
+
return [
|
|
35
|
+
data.version,
|
|
36
|
+
data.sequence,
|
|
37
|
+
data.start,
|
|
38
|
+
data.duration,
|
|
39
|
+
data.projectId,
|
|
40
|
+
data.userId,
|
|
41
|
+
data.sessionId,
|
|
42
|
+
data.pageNum,
|
|
43
|
+
data.upload,
|
|
44
|
+
data.end
|
|
45
|
+
];
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import measure from "@src/core/measure";
|
|
2
|
+
import * as baseline from "@src/data/baseline";
|
|
3
|
+
import * as envelope from "@src/data/envelope";
|
|
4
|
+
import * as dimension from "@src/data/dimension";
|
|
5
|
+
import * as metadata from "@src/data/metadata";
|
|
6
|
+
import { Module } from "@clarity-types/core";
|
|
7
|
+
import * as metric from "@src/data/metric";
|
|
8
|
+
import * as ping from "@src/data/ping";
|
|
9
|
+
import * as limit from "@src/data/limit";
|
|
10
|
+
import * as summary from "@src/data/summary";
|
|
11
|
+
import * as upgrade from "@src/data/upgrade";
|
|
12
|
+
import * as upload from "@src/data/upload";
|
|
13
|
+
import * as variable from "@src/data/variable";
|
|
14
|
+
export { event } from "@src/data/custom";
|
|
15
|
+
export { consent, metadata } from "@src/data/metadata";
|
|
16
|
+
export { upgrade } from "@src/data/upgrade";
|
|
17
|
+
export { set, identify } from "@src/data/variable";
|
|
18
|
+
|
|
19
|
+
const modules: Module[] = [baseline, dimension, variable, limit, summary, metadata, envelope, upload, ping, upgrade];
|
|
20
|
+
|
|
21
|
+
export function start(): void {
|
|
22
|
+
// Metric needs to be initialized before we can start measuring. so metric is not wrapped in measure
|
|
23
|
+
metric.start();
|
|
24
|
+
modules.forEach(x => measure(x.start)());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function stop(): void {
|
|
28
|
+
// Stop modules in the reverse order of their initialization
|
|
29
|
+
// The ordering below should respect inter-module dependency.
|
|
30
|
+
// E.g. if upgrade depends on upload, then upgrade needs to end before upload.
|
|
31
|
+
// Similarly, if upload depends on metadata, upload needs to end before metadata.
|
|
32
|
+
modules.slice().reverse().forEach(x => measure(x.stop)());
|
|
33
|
+
metric.stop();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function compute(): void {
|
|
37
|
+
variable.compute();
|
|
38
|
+
baseline.compute();
|
|
39
|
+
dimension.compute();
|
|
40
|
+
metric.compute();
|
|
41
|
+
summary.compute();
|
|
42
|
+
limit.compute();
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Check, Event, LimitData, Setting } from "@clarity-types/data";
|
|
2
|
+
import * as clarity from "@src/clarity";
|
|
3
|
+
import { report } from "@src/core/report";
|
|
4
|
+
import { time } from "@src/core/time";
|
|
5
|
+
import * as envelope from "@src/data/envelope";
|
|
6
|
+
import * as metadata from "@src/data/metadata";
|
|
7
|
+
import encode from "./encode";
|
|
8
|
+
|
|
9
|
+
export let data: LimitData;
|
|
10
|
+
|
|
11
|
+
export function start(): void {
|
|
12
|
+
data = { check: Check.None };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function check(bytes: number): void {
|
|
16
|
+
if (data.check === Check.None) {
|
|
17
|
+
let reason = data.check;
|
|
18
|
+
reason = envelope.data.sequence >= Setting.PayloadLimit ? Check.Payload : reason;
|
|
19
|
+
reason = time() > Setting.ShutdownLimit ? Check.Shutdown : reason;
|
|
20
|
+
reason = bytes > Setting.PlaybackBytesLimit ? Check.Shutdown : reason;
|
|
21
|
+
if (reason !== data.check) {
|
|
22
|
+
trigger(reason);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function trigger(reason: Check): void {
|
|
28
|
+
report(reason);
|
|
29
|
+
data.check = reason;
|
|
30
|
+
metadata.clear();
|
|
31
|
+
clarity.stop();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function compute(): void {
|
|
35
|
+
if (data.check !== Check.None) {
|
|
36
|
+
encode(Event.Limit);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function stop(): void {
|
|
41
|
+
data = null;
|
|
42
|
+
}
|