clarity-js 0.8.10-beta → 0.8.10
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/.lintstagedrc.yml +3 -0
- package/biome.json +43 -0
- package/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +3581 -3019
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +3581 -3019
- package/build/clarity.performance.js +1 -1
- package/package.json +17 -10
- package/rollup.config.ts +84 -88
- package/src/clarity.ts +34 -28
- package/src/core/config.ts +2 -2
- package/src/core/event.ts +36 -32
- package/src/core/hash.ts +5 -6
- package/src/core/history.ts +10 -11
- package/src/core/index.ts +21 -11
- package/src/core/measure.ts +9 -5
- package/src/core/report.ts +9 -5
- package/src/core/scrub.ts +29 -20
- package/src/core/task.ts +73 -45
- 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 +60 -55
- package/src/data/consent.ts +2 -2
- package/src/data/custom.ts +8 -13
- package/src/data/dimension.ts +11 -7
- package/src/data/encode.ts +36 -30
- package/src/data/envelope.ts +38 -38
- package/src/data/extract.ts +86 -77
- package/src/data/index.ts +10 -6
- package/src/data/limit.ts +1 -1
- package/src/data/metadata.ts +305 -266
- package/src/data/metric.ts +18 -8
- package/src/data/ping.ts +8 -4
- package/src/data/signal.ts +18 -18
- package/src/data/summary.ts +6 -4
- package/src/data/token.ts +10 -8
- package/src/data/upgrade.ts +7 -3
- package/src/data/upload.ts +100 -49
- package/src/data/variable.ts +27 -20
- package/src/diagnostic/encode.ts +2 -2
- package/src/diagnostic/fraud.ts +3 -4
- package/src/diagnostic/internal.ts +11 -5
- package/src/diagnostic/script.ts +12 -8
- package/src/global.ts +1 -1
- package/src/insight/blank.ts +4 -4
- package/src/insight/encode.ts +23 -17
- package/src/insight/snapshot.ts +57 -37
- package/src/interaction/change.ts +9 -6
- package/src/interaction/click.ts +34 -28
- package/src/interaction/clipboard.ts +2 -2
- package/src/interaction/encode.ts +35 -31
- package/src/interaction/input.ts +11 -9
- package/src/interaction/pointer.ts +41 -30
- package/src/interaction/resize.ts +5 -5
- package/src/interaction/scroll.ts +20 -17
- package/src/interaction/selection.ts +12 -8
- package/src/interaction/submit.ts +2 -2
- package/src/interaction/timeline.ts +13 -9
- package/src/interaction/unload.ts +1 -1
- package/src/interaction/visibility.ts +2 -2
- package/src/layout/animation.ts +47 -41
- package/src/layout/discover.ts +5 -5
- package/src/layout/document.ts +31 -19
- package/src/layout/dom.ts +141 -91
- package/src/layout/encode.ts +52 -37
- package/src/layout/mutation.ts +321 -318
- package/src/layout/node.ts +104 -81
- package/src/layout/offset.ts +7 -6
- package/src/layout/region.ts +66 -43
- package/src/layout/schema.ts +15 -8
- package/src/layout/selector.ts +47 -25
- package/src/layout/style.ts +44 -36
- package/src/layout/target.ts +14 -10
- package/src/layout/traverse.ts +17 -11
- package/src/performance/blank.ts +1 -1
- package/src/performance/encode.ts +4 -4
- package/src/performance/interaction.ts +58 -70
- package/src/performance/navigation.ts +2 -2
- package/src/performance/observer.ts +59 -26
- package/src/queue.ts +16 -9
- package/tsconfig.json +2 -2
- package/tslint.json +25 -32
- package/types/core.d.ts +13 -13
- package/types/data.d.ts +32 -29
- package/types/diagnostic.d.ts +1 -1
- package/types/interaction.d.ts +5 -4
- package/types/layout.d.ts +36 -21
- package/types/performance.d.ts +6 -5
package/src/data/metric.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Event, Metric, MetricData } from "@clarity-types/data";
|
|
1
|
+
import { Event, Metric, type MetricData } from "@clarity-types/data";
|
|
2
2
|
import encode from "./encode";
|
|
3
3
|
|
|
4
4
|
export let data: MetricData = null;
|
|
@@ -16,16 +16,24 @@ export function stop(): void {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function count(metric: Metric): void {
|
|
19
|
-
if (!(metric in data)) {
|
|
20
|
-
|
|
19
|
+
if (!(metric in data)) {
|
|
20
|
+
data[metric] = 0;
|
|
21
|
+
}
|
|
22
|
+
if (!(metric in updates)) {
|
|
23
|
+
updates[metric] = 0;
|
|
24
|
+
}
|
|
21
25
|
data[metric]++;
|
|
22
26
|
updates[metric]++;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
export function sum(metric: Metric, value: number): void {
|
|
26
|
-
if (value !== null) {
|
|
27
|
-
if (!(metric in data)) {
|
|
28
|
-
|
|
30
|
+
if (value !== null) {
|
|
31
|
+
if (!(metric in data)) {
|
|
32
|
+
data[metric] = 0;
|
|
33
|
+
}
|
|
34
|
+
if (!(metric in updates)) {
|
|
35
|
+
updates[metric] = 0;
|
|
36
|
+
}
|
|
29
37
|
data[metric] += value;
|
|
30
38
|
updates[metric] += value;
|
|
31
39
|
}
|
|
@@ -33,8 +41,10 @@ export function sum(metric: Metric, value: number): void {
|
|
|
33
41
|
|
|
34
42
|
export function max(metric: Metric, value: number): void {
|
|
35
43
|
// Ensure that we do not process null or NaN values
|
|
36
|
-
if (value !== null && isNaN(value) === false) {
|
|
37
|
-
if (!(metric in data)) {
|
|
44
|
+
if (value !== null && Number.isNaN(value) === false) {
|
|
45
|
+
if (!(metric in data)) {
|
|
46
|
+
data[metric] = 0;
|
|
47
|
+
}
|
|
38
48
|
if (value > data[metric] || data[metric] === 0) {
|
|
39
49
|
updates[metric] = value;
|
|
40
50
|
data[metric] = value;
|
package/src/data/ping.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Event, PingData, Setting } from "@clarity-types/data";
|
|
1
|
+
import { Event, type PingData, Setting } from "@clarity-types/data";
|
|
2
2
|
import { suspend } from "@src/core";
|
|
3
3
|
import { time } from "@src/core/time";
|
|
4
4
|
import { clearTimeout, setTimeout } from "@src/core/timeout";
|
|
@@ -15,18 +15,22 @@ export function start(): void {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export function reset(): void {
|
|
18
|
-
if (timeout) {
|
|
18
|
+
if (timeout) {
|
|
19
|
+
clearTimeout(timeout);
|
|
20
|
+
}
|
|
19
21
|
timeout = setTimeout(ping, interval);
|
|
20
22
|
last = time();
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
function ping(): void {
|
|
24
|
-
|
|
26
|
+
const now = time();
|
|
25
27
|
data = { gap: now - last };
|
|
26
28
|
encode(Event.Ping);
|
|
27
29
|
if (data.gap < Setting.PingTimeout) {
|
|
28
30
|
timeout = setTimeout(ping, interval);
|
|
29
|
-
} else {
|
|
31
|
+
} else {
|
|
32
|
+
suspend();
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export function stop(): void {
|
package/src/data/signal.ts
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { ClaritySignal, SignalCallback } from
|
|
1
|
+
import type { ClaritySignal, SignalCallback } from "@clarity-types/data";
|
|
2
2
|
|
|
3
3
|
export let signalCallback: SignalCallback = null;
|
|
4
4
|
|
|
5
5
|
export function signal(cb: SignalCallback): void {
|
|
6
|
-
|
|
6
|
+
signalCallback = cb;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function parseSignals(signalsPayload: string): ClaritySignal[] {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
try {
|
|
11
|
+
const parsedSignals: ClaritySignal[] = JSON.parse(signalsPayload);
|
|
12
|
+
return parsedSignals;
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export function signalsEvent(signalsPayload: string) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
try {
|
|
20
|
+
if (!signalCallback) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const signals = parseSignals(signalsPayload);
|
|
24
|
+
for (const signal of signals) {
|
|
25
|
+
signalCallback(signal);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
//do nothing
|
|
22
29
|
}
|
|
23
|
-
const signals = parseSignals(signalsPayload);
|
|
24
|
-
signals.forEach((signal) => {
|
|
25
|
-
signalCallback(signal);
|
|
26
|
-
});
|
|
27
|
-
} catch {
|
|
28
|
-
//do nothing
|
|
29
|
-
}
|
|
30
30
|
}
|
package/src/data/summary.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Event,
|
|
1
|
+
import { Event, Setting, type SummaryData } from "@clarity-types/data";
|
|
2
2
|
import encode from "./encode";
|
|
3
3
|
|
|
4
4
|
export let data: SummaryData = null;
|
|
@@ -15,13 +15,15 @@ export function track(event: Event, time: number): void {
|
|
|
15
15
|
if (!(event in data)) {
|
|
16
16
|
data[event] = [[time, 0]];
|
|
17
17
|
} else {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const e = data[event];
|
|
19
|
+
const last = e[e.length - 1];
|
|
20
20
|
// Add a new entry only if the new event occurs after configured interval
|
|
21
21
|
// Otherwise, extend the duration of the previous entry
|
|
22
22
|
if (time - last[0] > Setting.SummaryInterval) {
|
|
23
23
|
data[event].push([time, 0]);
|
|
24
|
-
} else {
|
|
24
|
+
} else {
|
|
25
|
+
last[1] = time - last[0];
|
|
26
|
+
}
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
package/src/data/token.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Token } from "@clarity-types/data";
|
|
2
2
|
|
|
3
3
|
// Following code takes an array of tokens and transforms it to optimize for repeating tokens and make it efficient to send over the wire
|
|
4
4
|
// The way it works is that it iterate over all tokens and checks if the current token was already seen in the tokens array so far
|
|
@@ -6,18 +6,20 @@ import {Constant, Token} from "@clarity-types/data";
|
|
|
6
6
|
// E.g. If tokens array is: ["hello", "world", "coding", "language", "world", "language", "example"]
|
|
7
7
|
// Then the resulting tokens array after following code execution would be: ["hello", "world", "coding", "language", [1, 3], "example"]
|
|
8
8
|
// Where [1,3] points to tokens[1] => "world" and tokens[3] => "language"
|
|
9
|
-
export default function(tokens: Token[]): Token[] {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
export default function (tokens: Token[]): Token[] {
|
|
10
|
+
const output: Token[] = [];
|
|
11
|
+
const lookup: { [key: string]: number } = {};
|
|
12
12
|
let pointer = 0;
|
|
13
13
|
let reference = null;
|
|
14
14
|
for (let i = 0; i < tokens.length; i++) {
|
|
15
15
|
// Only optimize for string values
|
|
16
|
-
if (typeof tokens[i] ===
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
if (typeof tokens[i] === "string") {
|
|
17
|
+
const token = tokens[i] as string;
|
|
18
|
+
const index = lookup[token] || -1;
|
|
19
19
|
if (index >= 0) {
|
|
20
|
-
if (reference) {
|
|
20
|
+
if (reference) {
|
|
21
|
+
reference.push(index);
|
|
22
|
+
} else {
|
|
21
23
|
reference = [index];
|
|
22
24
|
output.push(reference);
|
|
23
25
|
pointer++;
|
package/src/data/upgrade.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Constant, Event, UpgradeData } from "@clarity-types/data";
|
|
1
|
+
import { Constant, Event, type UpgradeData } from "@clarity-types/data";
|
|
2
2
|
import * as core from "@src/core";
|
|
3
3
|
import config from "@src/core/config";
|
|
4
4
|
import encode from "@src/data/encode";
|
|
@@ -9,7 +9,9 @@ import * as style from "@src/layout/style";
|
|
|
9
9
|
export let data: UpgradeData = null;
|
|
10
10
|
|
|
11
11
|
export function start(): void {
|
|
12
|
-
if (!config.lean && config.upgrade) {
|
|
12
|
+
if (!config.lean && config.upgrade) {
|
|
13
|
+
config.upgrade(Constant.Config);
|
|
14
|
+
}
|
|
13
15
|
data = null;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -28,7 +30,9 @@ export function upgrade(key: string): void {
|
|
|
28
30
|
metadata.save();
|
|
29
31
|
|
|
30
32
|
// Callback upgrade handler, if configured
|
|
31
|
-
if (config.upgrade) {
|
|
33
|
+
if (config.upgrade) {
|
|
34
|
+
config.upgrade(key);
|
|
35
|
+
}
|
|
32
36
|
|
|
33
37
|
encode(Event.Upgrade);
|
|
34
38
|
|
package/src/data/upload.ts
CHANGED
|
@@ -1,34 +1,48 @@
|
|
|
1
|
-
import { UploadCallback } from "@clarity-types/core";
|
|
2
|
-
import {
|
|
1
|
+
import type { UploadCallback } from "@clarity-types/core";
|
|
2
|
+
import {
|
|
3
|
+
BooleanFlag,
|
|
4
|
+
Check,
|
|
5
|
+
Code,
|
|
6
|
+
Constant,
|
|
7
|
+
type EncodedPayload,
|
|
8
|
+
Event,
|
|
9
|
+
Metric,
|
|
10
|
+
Setting,
|
|
11
|
+
Severity,
|
|
12
|
+
type Token,
|
|
13
|
+
type Transit,
|
|
14
|
+
type UploadData,
|
|
15
|
+
XMLReadyState,
|
|
16
|
+
} from "@clarity-types/data";
|
|
3
17
|
import * as clarity from "@src/clarity";
|
|
4
18
|
import config from "@src/core/config";
|
|
5
19
|
import measure from "@src/core/measure";
|
|
20
|
+
import { report } from "@src/core/report";
|
|
6
21
|
import { time } from "@src/core/time";
|
|
7
22
|
import { clearTimeout, setTimeout } from "@src/core/timeout";
|
|
8
23
|
import compress from "@src/data/compress";
|
|
9
24
|
import encode from "@src/data/encode";
|
|
10
25
|
import * as envelope from "@src/data/envelope";
|
|
26
|
+
import * as extract from "@src/data/extract";
|
|
11
27
|
import * as data from "@src/data/index";
|
|
12
28
|
import * as limit from "@src/data/limit";
|
|
13
29
|
import * as metadata from "@src/data/metadata";
|
|
14
30
|
import * as metric from "@src/data/metric";
|
|
15
31
|
import * as ping from "@src/data/ping";
|
|
32
|
+
import { signalsEvent } from "@src/data/signal";
|
|
16
33
|
import * as internal from "@src/diagnostic/internal";
|
|
17
34
|
import * as timeline from "@src/interaction/timeline";
|
|
18
35
|
import * as region from "@src/layout/region";
|
|
19
|
-
import * as extract from "@src/data/extract";
|
|
20
36
|
import * as style from "@src/layout/style";
|
|
21
|
-
import { report } from "@src/core/report";
|
|
22
|
-
import { signalsEvent } from "@src/data/signal";
|
|
23
37
|
|
|
24
|
-
let discoverBytes
|
|
25
|
-
let playbackBytes
|
|
38
|
+
let discoverBytes = 0;
|
|
39
|
+
let playbackBytes = 0;
|
|
26
40
|
let playback: string[];
|
|
27
41
|
let analysis: string[];
|
|
28
42
|
let timeout: number = null;
|
|
29
43
|
let transit: Transit;
|
|
30
44
|
let active: boolean;
|
|
31
|
-
let queuedTime
|
|
45
|
+
let queuedTime = 0;
|
|
32
46
|
let leanLimit = false;
|
|
33
47
|
export let track: UploadData;
|
|
34
48
|
|
|
@@ -44,11 +58,11 @@ export function start(): void {
|
|
|
44
58
|
track = null;
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
export function queue(tokens: Token[], transmit
|
|
61
|
+
export function queue(tokens: Token[], transmit = true): void {
|
|
48
62
|
if (active) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
const now = time();
|
|
64
|
+
const type = tokens.length > 1 ? tokens[1] : null;
|
|
65
|
+
const event = JSON.stringify(tokens);
|
|
52
66
|
|
|
53
67
|
if (!config.lean) {
|
|
54
68
|
leanLimit = false;
|
|
@@ -58,15 +72,20 @@ export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
switch (type) {
|
|
75
|
+
// biome-ignore lint/suspicious/noFallthroughSwitchClause: we want discover bytes to also count as playback bytes
|
|
61
76
|
case Event.Discover:
|
|
62
|
-
if (leanLimit) {
|
|
77
|
+
if (leanLimit) {
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
63
80
|
discoverBytes += event.length;
|
|
64
81
|
case Event.Box:
|
|
65
82
|
case Event.Mutation:
|
|
66
83
|
case Event.Snapshot:
|
|
67
84
|
case Event.StyleSheetAdoption:
|
|
68
85
|
case Event.StyleSheetUpdate:
|
|
69
|
-
if (leanLimit) {
|
|
86
|
+
if (leanLimit) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
70
89
|
playbackBytes += event.length;
|
|
71
90
|
playback.push(event);
|
|
72
91
|
break;
|
|
@@ -81,8 +100,8 @@ export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
|
81
100
|
// Following two checks are precautionary and act as a fail safe mechanism to get out of unexpected situations.
|
|
82
101
|
// Check 1: If for any reason the upload hasn't happened after waiting for 2x the config.delay time,
|
|
83
102
|
// reset the timer. This allows Clarity to attempt an upload again.
|
|
84
|
-
|
|
85
|
-
if (now - queuedTime >
|
|
103
|
+
const gap = delay();
|
|
104
|
+
if (now - queuedTime > gap * 2) {
|
|
86
105
|
clearTimeout(timeout);
|
|
87
106
|
timeout = null;
|
|
88
107
|
}
|
|
@@ -91,7 +110,9 @@ export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
|
91
110
|
// However, in certain scenarios - like metric calculation - which are triggered as part of an existing upload
|
|
92
111
|
// We enrich the data going out with the existing upload. In these cases, call to upload comes with 'transmit' set to false.
|
|
93
112
|
if (transmit && timeout === null) {
|
|
94
|
-
if (type !== Event.Ping) {
|
|
113
|
+
if (type !== Event.Ping) {
|
|
114
|
+
ping.reset();
|
|
115
|
+
}
|
|
95
116
|
timeout = setTimeout(upload, gap);
|
|
96
117
|
queuedTime = now;
|
|
97
118
|
limit.check(playbackBytes);
|
|
@@ -113,14 +134,17 @@ export function stop(): void {
|
|
|
113
134
|
active = false;
|
|
114
135
|
}
|
|
115
136
|
|
|
116
|
-
async function upload(final
|
|
137
|
+
async function upload(final = false): Promise<void> {
|
|
117
138
|
timeout = null;
|
|
118
139
|
|
|
119
140
|
// Check if we can send playback bytes over the wire or not
|
|
120
141
|
// For better instrumentation coverage, we send playback bytes from second sequence onwards
|
|
121
142
|
// And, we only send playback metric when we are able to send the playback bytes back to server
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
const sendPlaybackBytes =
|
|
144
|
+
config.lean === false && playbackBytes > 0 && (playbackBytes < Setting.MaxFirstPayloadBytes || envelope.data.sequence > 0);
|
|
145
|
+
if (sendPlaybackBytes) {
|
|
146
|
+
metric.max(Metric.Playback, BooleanFlag.True);
|
|
147
|
+
}
|
|
124
148
|
|
|
125
149
|
// CAUTION: Ensure "transmit" is set to false in the queue function for following events
|
|
126
150
|
// Otherwise you run a risk of infinite loop.
|
|
@@ -133,18 +157,18 @@ async function upload(final: boolean = false): Promise<void> {
|
|
|
133
157
|
// In real world tests, we noticed that certain third party scripts (e.g. https://www.npmjs.com/package/raven-js)
|
|
134
158
|
// could inject function arguments for internal tracking (likely stack traces for script errors).
|
|
135
159
|
// For these edge cases, we want to ensure that an injected object (e.g. {"key": "value"}) isn't mistaken to be true.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
160
|
+
const last = final === true;
|
|
161
|
+
const e = JSON.stringify(envelope.envelope(last));
|
|
162
|
+
const a = `[${analysis.join()}]`;
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
|
|
164
|
+
const p = sendPlaybackBytes ? `[${playback.join()}]` : Constant.Empty;
|
|
165
|
+
const encoded: EncodedPayload = { e, a, p };
|
|
142
166
|
|
|
143
167
|
// Get the payload ready for sending over the wire
|
|
144
168
|
// We also attempt to compress the payload if it is not the last payload and the browser supports it
|
|
145
169
|
// In all other cases, we continue to send back string value
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
const payload = stringify(encoded);
|
|
171
|
+
const zipped = last ? null : await compress(payload);
|
|
148
172
|
metric.sum(Metric.TotalBytes, zipped ? zipped.length : payload.length);
|
|
149
173
|
send(payload, zipped, envelope.data.sequence, last);
|
|
150
174
|
|
|
@@ -162,9 +186,9 @@ function stringify(encoded: EncodedPayload): string {
|
|
|
162
186
|
return encoded.p.length > 0 ? `{"e":${encoded.e},"a":${encoded.a},"p":${encoded.p}}` : `{"e":${encoded.e},"a":${encoded.a}}`;
|
|
163
187
|
}
|
|
164
188
|
|
|
165
|
-
function send(payload: string, zipped: Uint8Array, sequence: number, beacon
|
|
189
|
+
function send(payload: string, zipped: Uint8Array, sequence: number, beacon = false): void {
|
|
166
190
|
// Upload data if a valid URL is defined in the config
|
|
167
|
-
if (typeof config.upload ===
|
|
191
|
+
if (typeof config.upload === "string") {
|
|
168
192
|
const url = config.upload as string;
|
|
169
193
|
let dispatched = false;
|
|
170
194
|
|
|
@@ -176,8 +200,12 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
|
|
|
176
200
|
try {
|
|
177
201
|
// Navigator needs to be bound to sendBeacon before it is used to avoid errors in some browsers
|
|
178
202
|
dispatched = navigator.sendBeacon.bind(navigator)(url, payload);
|
|
179
|
-
if (dispatched) {
|
|
180
|
-
|
|
203
|
+
if (dispatched) {
|
|
204
|
+
done(sequence);
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
/* do nothing - and we will automatically fallback to XHR below */
|
|
208
|
+
}
|
|
181
209
|
}
|
|
182
210
|
|
|
183
211
|
// Before initiating XHR upload, we check if the data has already been uploaded using sendBeacon
|
|
@@ -188,12 +216,22 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
|
|
|
188
216
|
if (dispatched === false) {
|
|
189
217
|
// While tracking payload for retry, we only track string value of the payload to err on the safe side
|
|
190
218
|
// Not all browsers support compression API and the support for it in supported browsers is still experimental
|
|
191
|
-
if (sequence in transit) {
|
|
192
|
-
|
|
219
|
+
if (sequence in transit) {
|
|
220
|
+
transit[sequence].attempts++;
|
|
221
|
+
} else {
|
|
222
|
+
transit[sequence] = { data: payload, attempts: 1 };
|
|
223
|
+
}
|
|
224
|
+
const xhr = new XMLHttpRequest();
|
|
193
225
|
xhr.open("POST", url, true);
|
|
194
226
|
xhr.timeout = Setting.UploadTimeout;
|
|
195
|
-
xhr.ontimeout = () => {
|
|
196
|
-
|
|
227
|
+
xhr.ontimeout = () => {
|
|
228
|
+
report(new Error(`${Constant.Timeout} : ${url}`));
|
|
229
|
+
};
|
|
230
|
+
if (sequence !== null) {
|
|
231
|
+
xhr.onreadystatechange = (): void => {
|
|
232
|
+
measure(check)(xhr, sequence);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
197
235
|
xhr.withCredentials = true;
|
|
198
236
|
if (zipped) {
|
|
199
237
|
// If we do have valid compressed array, send it with appropriate HTTP headers so server can decode it appropriately
|
|
@@ -212,7 +250,7 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
|
|
|
212
250
|
}
|
|
213
251
|
|
|
214
252
|
function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
215
|
-
|
|
253
|
+
const transitData = transit[sequence];
|
|
216
254
|
if (xhr && xhr.readyState === XMLReadyState.Done && transitData) {
|
|
217
255
|
// Attempt send payload again (as configured in settings) if we do not receive a success (2XX) response code back from the server
|
|
218
256
|
if ((xhr.status < 200 || xhr.status > 208) && transitData.attempts <= Setting.RetryLimit) {
|
|
@@ -226,7 +264,9 @@ function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
|
226
264
|
// 1: Browsers block upload because of content security policy violation
|
|
227
265
|
// 2: Safari will terminate pending XHR requests with status code 0 if the user navigates away from the page
|
|
228
266
|
// In any case, we switch the upload URL to fallback configuration (if available) before re-trying one more time
|
|
229
|
-
if (xhr.status === 0) {
|
|
267
|
+
if (xhr.status === 0) {
|
|
268
|
+
config.upload = config.fallback ? config.fallback : config.upload;
|
|
269
|
+
}
|
|
230
270
|
// In all other cases, re-attempt sending the same data
|
|
231
271
|
// For retry we always fallback to string payload, even though we may have attempted
|
|
232
272
|
// sending zipped payload earlier
|
|
@@ -235,9 +275,13 @@ function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
|
235
275
|
} else {
|
|
236
276
|
track = { sequence, attempts: transitData.attempts, status: xhr.status };
|
|
237
277
|
// Send back an event only if we were not successful in our first attempt
|
|
238
|
-
if (transitData.attempts > 1) {
|
|
278
|
+
if (transitData.attempts > 1) {
|
|
279
|
+
encode(Event.Upload);
|
|
280
|
+
}
|
|
239
281
|
// Handle response if it was a 200 response with a valid body
|
|
240
|
-
if (xhr.status === 200 && xhr.responseText) {
|
|
282
|
+
if (xhr.status === 200 && xhr.responseText) {
|
|
283
|
+
response(xhr.responseText);
|
|
284
|
+
}
|
|
241
285
|
// If we exhausted our retries then trigger Clarity's shutdown for this page since the data will be incomplete
|
|
242
286
|
if (xhr.status === 0) {
|
|
243
287
|
// And, right before we terminate the session, we will attempt one last time to see if we can use
|
|
@@ -246,7 +290,9 @@ function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
|
246
290
|
limit.trigger(Check.Retry);
|
|
247
291
|
}
|
|
248
292
|
// Signal that this request completed successfully
|
|
249
|
-
if (xhr.status >= 200 && xhr.status <= 208) {
|
|
293
|
+
if (xhr.status >= 200 && xhr.status <= 208) {
|
|
294
|
+
done(sequence);
|
|
295
|
+
}
|
|
250
296
|
// Stop tracking this payload now that it's all done
|
|
251
297
|
delete transit[sequence];
|
|
252
298
|
}
|
|
@@ -264,15 +310,14 @@ function done(sequence: number): void {
|
|
|
264
310
|
function delay(): number {
|
|
265
311
|
// Progressively increase delay as we continue to send more payloads from the client to the server
|
|
266
312
|
// If we are not uploading data to a server, and instead invoking UploadCallback, in that case keep returning configured value
|
|
267
|
-
|
|
268
|
-
return typeof config.upload ===
|
|
313
|
+
const gap = config.lean === false && discoverBytes > 0 ? Setting.MinUploadDelay : envelope.data.sequence * config.delay;
|
|
314
|
+
return typeof config.upload === "string" ? Math.max(Math.min(gap, Setting.MaxUploadDelay), Setting.MinUploadDelay) : config.delay;
|
|
269
315
|
}
|
|
270
316
|
|
|
271
317
|
function response(payload: string): void {
|
|
272
|
-
|
|
273
|
-
for (
|
|
274
|
-
|
|
275
|
-
let parts = line && line.length > 0 ? line.split(/ (.*)/) : [Constant.Empty];
|
|
318
|
+
const lines = payload && payload.length > 0 ? payload.split("\n") : [];
|
|
319
|
+
for (const line of lines) {
|
|
320
|
+
const parts = line && line.length > 0 ? line.split(/ (.*)/) : [Constant.Empty];
|
|
276
321
|
switch (parts[0]) {
|
|
277
322
|
case Constant.End:
|
|
278
323
|
// Clear out session storage and end the session so we can start fresh the next time
|
|
@@ -284,13 +329,19 @@ function response(payload: string): void {
|
|
|
284
329
|
break;
|
|
285
330
|
case Constant.Action:
|
|
286
331
|
// Invoke action callback, if configured and has a valid value
|
|
287
|
-
if (config.action && parts.length > 1) {
|
|
332
|
+
if (config.action && parts.length > 1) {
|
|
333
|
+
config.action(parts[1]);
|
|
334
|
+
}
|
|
288
335
|
break;
|
|
289
336
|
case Constant.Extract:
|
|
290
|
-
if (parts.length > 1) {
|
|
337
|
+
if (parts.length > 1) {
|
|
338
|
+
extract.trigger(parts[1]);
|
|
339
|
+
}
|
|
291
340
|
break;
|
|
292
341
|
case Constant.Signal:
|
|
293
|
-
if (parts.length > 1) {
|
|
342
|
+
if (parts.length > 1) {
|
|
343
|
+
signalsEvent(parts[1]);
|
|
344
|
+
}
|
|
294
345
|
break;
|
|
295
346
|
}
|
|
296
347
|
}
|
package/src/data/variable.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Constant, Event, IdentityData, Setting, VariableData } from "@clarity-types/data";
|
|
1
|
+
import { Constant, Event, type IdentityData, Setting, type VariableData } from "@clarity-types/data";
|
|
2
2
|
import * as core from "@src/core";
|
|
3
3
|
import { scrub } from "@src/core/scrub";
|
|
4
4
|
import encode from "./encode";
|
|
@@ -10,23 +10,28 @@ export function start(): void {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function set(variable: string, value: string | string[]): void {
|
|
13
|
-
|
|
13
|
+
const values = typeof value === "string" ? [value as string] : (value as string[]);
|
|
14
14
|
log(variable, values);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export async function identify(
|
|
18
|
-
|
|
17
|
+
export async function identify(
|
|
18
|
+
userId: string,
|
|
19
|
+
sessionId: string = null,
|
|
20
|
+
pageId: string = null,
|
|
21
|
+
userHint: string = null,
|
|
22
|
+
): Promise<IdentityData> {
|
|
23
|
+
const output: IdentityData = { userId: await sha256(userId), userHint: userHint || redact(userId) };
|
|
19
24
|
|
|
20
25
|
// By default, hash custom userId using SHA256 algorithm on the client to preserve privacy
|
|
21
26
|
log(Constant.UserId, [output.userId]);
|
|
22
|
-
|
|
27
|
+
|
|
23
28
|
// Optional non-identifying name for the user
|
|
24
29
|
// If name is not explicitly provided, we automatically generate a redacted version of the userId
|
|
25
30
|
log(Constant.UserHint, [output.userHint]);
|
|
26
|
-
log(Constant.UserType, [detect(userId)]);
|
|
31
|
+
log(Constant.UserType, [detect(userId)]);
|
|
27
32
|
|
|
28
33
|
// Log sessionId and pageId if provided
|
|
29
|
-
if (sessionId) {
|
|
34
|
+
if (sessionId) {
|
|
30
35
|
log(Constant.SessionId, [sessionId]);
|
|
31
36
|
output.sessionId = sessionId;
|
|
32
37
|
}
|
|
@@ -39,14 +44,12 @@ export async function identify(userId: string, sessionId: string = null, pageId:
|
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
function log(variable: string, value: string[]): void {
|
|
42
|
-
if (core.active() &&
|
|
43
|
-
variable
|
|
44
|
-
value &&
|
|
45
|
-
typeof variable === Constant.String &&
|
|
46
|
-
variable.length < 255) {
|
|
47
|
-
let validValues = variable in data ? data[variable] : [];
|
|
47
|
+
if (core.active() && variable && value && typeof variable === "string" && variable.length < 255) {
|
|
48
|
+
const validValues = variable in data ? data[variable] : [];
|
|
48
49
|
for (let i = 0; i < value.length; i++) {
|
|
49
|
-
if (typeof value[i] ===
|
|
50
|
+
if (typeof value[i] === "string" && value[i].length < 255) {
|
|
51
|
+
validValues.push(value[i]);
|
|
52
|
+
}
|
|
50
53
|
}
|
|
51
54
|
data[variable] = validValues;
|
|
52
55
|
}
|
|
@@ -65,8 +68,9 @@ export function stop(): void {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function redact(input: string): string {
|
|
68
|
-
return input && input.length >= Setting.WordLength
|
|
69
|
-
`${input.substring(0,2)}${scrub(input.substring(2), Constant.Asterix, Constant.Asterix)}`
|
|
71
|
+
return input && input.length >= Setting.WordLength
|
|
72
|
+
? `${input.substring(0, 2)}${scrub(input.substring(2), Constant.Asterix, Constant.Asterix)}`
|
|
73
|
+
: scrub(input, Constant.Asterix, Constant.Asterix);
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
async function sha256(input: string): Promise<string> {
|
|
@@ -74,11 +78,14 @@ async function sha256(input: string): Promise<string> {
|
|
|
74
78
|
if (crypto && input) {
|
|
75
79
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
|
76
80
|
const buffer = await crypto.subtle.digest(Constant.SHA256, new TextEncoder().encode(input));
|
|
77
|
-
return Array.prototype.map.call(new Uint8Array(buffer), (x
|
|
78
|
-
}
|
|
79
|
-
|
|
81
|
+
return Array.prototype.map.call(new Uint8Array(buffer), (x) => `00${x.toString(16)}`.slice(-2)).join("");
|
|
82
|
+
}
|
|
83
|
+
return Constant.Empty;
|
|
84
|
+
} catch {
|
|
85
|
+
return Constant.Empty;
|
|
86
|
+
}
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
function detect(input: string): string {
|
|
83
90
|
return input && input.indexOf(Constant.At) > 0 ? Constant.Email : Constant.String;
|
|
84
|
-
}
|
|
91
|
+
}
|
package/src/diagnostic/encode.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Event, Token } from "@clarity-types/data";
|
|
1
|
+
import { Event, type Token } from "@clarity-types/data";
|
|
2
2
|
import * as scrub from "@src/core/scrub";
|
|
3
3
|
import { time } from "@src/core/time";
|
|
4
4
|
import { queue } from "@src/data/upload";
|
|
@@ -7,7 +7,7 @@ import * as internal from "@src/diagnostic/internal";
|
|
|
7
7
|
import * as script from "@src/diagnostic/script";
|
|
8
8
|
|
|
9
9
|
export default async function (type: Event): Promise<void> {
|
|
10
|
-
|
|
10
|
+
const tokens: Token[] = [time(), type];
|
|
11
11
|
|
|
12
12
|
switch (type) {
|
|
13
13
|
case Event.ScriptError:
|
package/src/diagnostic/fraud.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BooleanFlag, Event, IframeStatus, Metric, Setting } from "@clarity-types/data";
|
|
2
|
-
import { FraudData } from "@clarity-types/diagnostic";
|
|
2
|
+
import type { FraudData } from "@clarity-types/diagnostic";
|
|
3
3
|
import config from "@src/core/config";
|
|
4
4
|
import hash from "@src/core/hash";
|
|
5
5
|
import * as metric from "@src/data/metric";
|
|
@@ -13,11 +13,10 @@ export function start(): void {
|
|
|
13
13
|
metric.max(Metric.Automation, navigator.webdriver ? BooleanFlag.True : BooleanFlag.False);
|
|
14
14
|
try {
|
|
15
15
|
// some sites (unintentionally) overwrite the window.self property, so we also check for the main window object
|
|
16
|
-
metric.max(Metric.Iframed, window.top
|
|
16
|
+
metric.max(Metric.Iframed, window.top === window.self || window.top === window ? IframeStatus.TopFrame : IframeStatus.Iframe);
|
|
17
17
|
} catch (ex) {
|
|
18
18
|
metric.max(Metric.Iframed, IframeStatus.Unknown);
|
|
19
19
|
}
|
|
20
|
-
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export function check(id: number, target: number, input: string): void {
|
|
@@ -34,4 +33,4 @@ export function check(id: number, target: number, input: string): void {
|
|
|
34
33
|
|
|
35
34
|
export function stop(): void {
|
|
36
35
|
history = [];
|
|
37
|
-
}
|
|
36
|
+
}
|