clarity-decode 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.decode.js +677 -0
- package/build/clarity.decode.min.js +1 -0
- package/build/clarity.decode.module.js +673 -0
- package/package.json +61 -0
- package/rollup.config.ts +30 -0
- package/src/clarity.ts +195 -0
- package/src/data.ts +87 -0
- package/src/diagnostic.ts +34 -0
- package/src/global.ts +9 -0
- package/src/index.ts +1 -0
- package/src/interaction.ts +80 -0
- package/src/layout.ts +163 -0
- package/src/performance.ts +38 -0
- package/tsconfig.json +20 -0
- package/tslint.json +33 -0
- package/types/core.d.ts +8 -0
- package/types/data.d.ts +84 -0
- package/types/diagnostic.d.ts +9 -0
- package/types/index.d.ts +10 -0
- package/types/interaction.d.ts +23 -0
- package/types/layout.d.ts +29 -0
- package/types/performance.d.ts +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clarity-decode",
|
|
3
|
+
"version": "0.6.23",
|
|
4
|
+
"description": "An analytics library that uses web page interactions to generate aggregated insights",
|
|
5
|
+
"author": "Microsoft Corp.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "build/clarity.decode.js",
|
|
8
|
+
"module": "build/clarity.decode.module.js",
|
|
9
|
+
"unpkg": "build/clarity.decode.min.js",
|
|
10
|
+
"types": "types/index.d.ts",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"clarity",
|
|
13
|
+
"Microsoft",
|
|
14
|
+
"interactions",
|
|
15
|
+
"cursor",
|
|
16
|
+
"pointer",
|
|
17
|
+
"instrumentation",
|
|
18
|
+
"analytics"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/microsoft/clarity.git",
|
|
23
|
+
"directory": "packages/clarity-decode"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Microsoft/clarity/issues"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"clarity-js": "^0.6.23"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@rollup/plugin-commonjs": "^19.0.1",
|
|
33
|
+
"@rollup/plugin-node-resolve": "^13.0.2",
|
|
34
|
+
"del-cli": "^4.0.1",
|
|
35
|
+
"husky": "^7.0.1",
|
|
36
|
+
"lint-staged": "^11.0.1",
|
|
37
|
+
"rollup": "^2.53.2",
|
|
38
|
+
"rollup-plugin-terser": "^7.0.2",
|
|
39
|
+
"rollup-plugin-typescript2": "^0.30.0",
|
|
40
|
+
"ts-node": "^10.1.0",
|
|
41
|
+
"tslint": "^6.1.3",
|
|
42
|
+
"typescript": "^4.3.5"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "yarn build:clean && yarn build:main",
|
|
46
|
+
"build:main": "rollup -c rollup.config.ts",
|
|
47
|
+
"build:clean": "del-cli build/*",
|
|
48
|
+
"tslint": "tslint --project ./",
|
|
49
|
+
"tslint:fix": "tslint --fix --project ./ --force"
|
|
50
|
+
},
|
|
51
|
+
"husky": {
|
|
52
|
+
"hooks": {
|
|
53
|
+
"pre-commit": "lint-staged"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"lint-staged": {
|
|
57
|
+
"*.ts": [
|
|
58
|
+
"tslint --format codeFrame"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
}
|
package/rollup.config.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import commonjs from "@rollup/plugin-commonjs";
|
|
2
|
+
import resolve from "@rollup/plugin-node-resolve";
|
|
3
|
+
import typescript from "rollup-plugin-typescript2";
|
|
4
|
+
import { terser } from "rollup-plugin-terser";
|
|
5
|
+
import pkg from "./package.json";
|
|
6
|
+
|
|
7
|
+
export default [
|
|
8
|
+
{
|
|
9
|
+
input: "src/index.ts",
|
|
10
|
+
output: [
|
|
11
|
+
{ file: pkg.main, format: "cjs", exports: "named" },
|
|
12
|
+
{ file: pkg.module, format: "es", exports: "named" }
|
|
13
|
+
],
|
|
14
|
+
plugins: [
|
|
15
|
+
resolve(),
|
|
16
|
+
typescript({ rollupCommonJSResolveHack: true, clean: true }),
|
|
17
|
+
commonjs({ include: ["node_modules/**"] })
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
input: "src/global.ts",
|
|
22
|
+
output: [ { file: pkg.unpkg, format: "iife", exports: "named" } ],
|
|
23
|
+
plugins: [
|
|
24
|
+
resolve(),
|
|
25
|
+
typescript({ rollupCommonJSResolveHack: true, clean: true }),
|
|
26
|
+
terser({output: {comments: false}}),
|
|
27
|
+
commonjs({ include: ["node_modules/**"] })
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
];
|
package/src/clarity.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Data, version } from "clarity-js";
|
|
2
|
+
import { BaselineEvent, CustomEvent, DecodedPayload, DecodedVersion, DimensionEvent } from "../types/data";
|
|
3
|
+
import { LimitEvent, MetricEvent, PingEvent, SummaryEvent, UpgradeEvent, UploadEvent, VariableEvent } from "../types/data";
|
|
4
|
+
import { ImageErrorEvent, LogEvent, ScriptErrorEvent } from "../types/diagnostic";
|
|
5
|
+
import { ClickEvent, InputEvent, PointerEvent, ResizeEvent, ScrollEvent, TimelineEvent } from "../types/interaction";
|
|
6
|
+
import { SelectionEvent, UnloadEvent, VisibilityEvent } from "../types/interaction";
|
|
7
|
+
import { BoxEvent, DocumentEvent, DomEvent, RegionEvent } from "../types/layout";
|
|
8
|
+
import { ConnectionEvent, NavigationEvent } from "../types/performance";
|
|
9
|
+
|
|
10
|
+
import * as data from "./data";
|
|
11
|
+
import * as diagnostic from "./diagnostic";
|
|
12
|
+
import * as interaction from "./interaction";
|
|
13
|
+
import * as layout from "./layout";
|
|
14
|
+
import * as performance from "./performance";
|
|
15
|
+
|
|
16
|
+
export function decode(input: string): DecodedPayload {
|
|
17
|
+
let json: Data.Payload = JSON.parse(input);
|
|
18
|
+
let envelope = data.envelope(json.e);
|
|
19
|
+
let timestamp = Date.now();
|
|
20
|
+
let payload: DecodedPayload = { timestamp, envelope };
|
|
21
|
+
|
|
22
|
+
// Sort encoded events by time to simplify summary computation
|
|
23
|
+
// It's possible for individual events to be out of order, dependent on how they are buffered on the client
|
|
24
|
+
// E.g. scroll events are queued internally before they are sent over the wire.
|
|
25
|
+
// By comparison, events like resize & click are sent out immediately.
|
|
26
|
+
let encoded: Data.Token[][] = json.p ? json.a.concat(json.p) : json.a;
|
|
27
|
+
encoded = encoded.sort((a: Data.Token[], b: Data.Token[]): number => (a[0] as number) - (b[0] as number));
|
|
28
|
+
|
|
29
|
+
// Check if the incoming version is compatible with the current running code
|
|
30
|
+
// We do an exact match for the major version and minor version.
|
|
31
|
+
// For patch, we are backward and forward compatible with up to version change.
|
|
32
|
+
let jsonVersion = parseVersion(payload.envelope.version);
|
|
33
|
+
let codeVersion = parseVersion(version);
|
|
34
|
+
|
|
35
|
+
if (jsonVersion.major !== codeVersion.major ||
|
|
36
|
+
Math.abs(jsonVersion.minor - codeVersion.minor) > 1) {
|
|
37
|
+
throw new Error(`Invalid version. Actual: ${payload.envelope.version} | Expected: ${version} (+/- 1) | ${input.substr(0, 250)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Reset components before decoding to keep them stateless */
|
|
41
|
+
layout.reset();
|
|
42
|
+
|
|
43
|
+
for (let entry of encoded) {
|
|
44
|
+
switch (entry[1]) {
|
|
45
|
+
case Data.Event.Baseline:
|
|
46
|
+
if (payload.baseline === undefined) { payload.baseline = []; }
|
|
47
|
+
payload.baseline.push(data.decode(entry) as BaselineEvent);
|
|
48
|
+
break;
|
|
49
|
+
case Data.Event.Ping:
|
|
50
|
+
if (payload.ping === undefined) { payload.ping = []; }
|
|
51
|
+
payload.ping.push(data.decode(entry) as PingEvent);
|
|
52
|
+
break;
|
|
53
|
+
case Data.Event.Limit:
|
|
54
|
+
if (payload.limit === undefined) { payload.limit = []; }
|
|
55
|
+
payload.limit.push(data.decode(entry) as LimitEvent);
|
|
56
|
+
break;
|
|
57
|
+
case Data.Event.Upgrade:
|
|
58
|
+
if (payload.upgrade === undefined) { payload.upgrade = []; }
|
|
59
|
+
payload.upgrade.push(data.decode(entry) as UpgradeEvent);
|
|
60
|
+
break;
|
|
61
|
+
case Data.Event.Metric:
|
|
62
|
+
if (payload.metric === undefined) { payload.metric = []; }
|
|
63
|
+
let metric = data.decode(entry) as MetricEvent;
|
|
64
|
+
// It's not possible to accurately include the byte count of the payload within the same payload
|
|
65
|
+
// The value we get from clarity-js lags behind by a payload. To work around that,
|
|
66
|
+
// we increment the bytes from the incoming payload at decode time.
|
|
67
|
+
metric.data[Data.Metric.TotalBytes] = input.length;
|
|
68
|
+
payload.metric.push(metric);
|
|
69
|
+
break;
|
|
70
|
+
case Data.Event.Dimension:
|
|
71
|
+
if (payload.dimension === undefined) { payload.dimension = []; }
|
|
72
|
+
payload.dimension.push(data.decode(entry) as DimensionEvent);
|
|
73
|
+
break;
|
|
74
|
+
case Data.Event.Summary:
|
|
75
|
+
if (payload.summary === undefined) { payload.summary = []; }
|
|
76
|
+
payload.summary.push(data.decode(entry) as SummaryEvent);
|
|
77
|
+
break;
|
|
78
|
+
case Data.Event.Custom:
|
|
79
|
+
if (payload.custom === undefined) { payload.custom = []; }
|
|
80
|
+
payload.custom.push(data.decode(entry) as CustomEvent);
|
|
81
|
+
break;
|
|
82
|
+
case Data.Event.Variable:
|
|
83
|
+
if (payload.variable === undefined) { payload.variable = []; }
|
|
84
|
+
payload.variable.push(data.decode(entry) as VariableEvent);
|
|
85
|
+
break;
|
|
86
|
+
case Data.Event.Upload:
|
|
87
|
+
if (payload.upload === undefined) { payload.upload = []; }
|
|
88
|
+
payload.upload.push(data.decode(entry) as UploadEvent);
|
|
89
|
+
break;
|
|
90
|
+
case Data.Event.MouseDown:
|
|
91
|
+
case Data.Event.MouseUp:
|
|
92
|
+
case Data.Event.MouseMove:
|
|
93
|
+
case Data.Event.MouseWheel:
|
|
94
|
+
case Data.Event.DoubleClick:
|
|
95
|
+
case Data.Event.TouchStart:
|
|
96
|
+
case Data.Event.TouchCancel:
|
|
97
|
+
case Data.Event.TouchEnd:
|
|
98
|
+
case Data.Event.TouchMove:
|
|
99
|
+
if (payload.pointer === undefined) { payload.pointer = []; }
|
|
100
|
+
payload.pointer.push(interaction.decode(entry) as PointerEvent);
|
|
101
|
+
break;
|
|
102
|
+
case Data.Event.Click:
|
|
103
|
+
if (payload.click === undefined) { payload.click = []; }
|
|
104
|
+
let clickEntry = interaction.decode(entry) as ClickEvent;
|
|
105
|
+
payload.click.push(clickEntry);
|
|
106
|
+
break;
|
|
107
|
+
case Data.Event.Scroll:
|
|
108
|
+
if (payload.scroll === undefined) { payload.scroll = []; }
|
|
109
|
+
payload.scroll.push(interaction.decode(entry) as ScrollEvent);
|
|
110
|
+
break;
|
|
111
|
+
case Data.Event.Resize:
|
|
112
|
+
if (payload.resize === undefined) { payload.resize = []; }
|
|
113
|
+
payload.resize.push(interaction.decode(entry) as ResizeEvent);
|
|
114
|
+
break;
|
|
115
|
+
case Data.Event.Selection:
|
|
116
|
+
if (payload.selection === undefined) { payload.selection = []; }
|
|
117
|
+
payload.selection.push(interaction.decode(entry) as SelectionEvent);
|
|
118
|
+
break;
|
|
119
|
+
case Data.Event.Timeline:
|
|
120
|
+
if (payload.timeline === undefined) { payload.timeline = []; }
|
|
121
|
+
payload.timeline.push(interaction.decode(entry) as TimelineEvent);
|
|
122
|
+
break;
|
|
123
|
+
case Data.Event.Input:
|
|
124
|
+
if (payload.input === undefined) { payload.input = []; }
|
|
125
|
+
payload.input.push(interaction.decode(entry) as InputEvent);
|
|
126
|
+
break;
|
|
127
|
+
case Data.Event.Unload:
|
|
128
|
+
if (payload.unload === undefined) { payload.unload = []; }
|
|
129
|
+
payload.unload.push(interaction.decode(entry) as UnloadEvent);
|
|
130
|
+
break;
|
|
131
|
+
case Data.Event.Visibility:
|
|
132
|
+
if (payload.visibility === undefined) { payload.visibility = []; }
|
|
133
|
+
payload.visibility.push(interaction.decode(entry) as VisibilityEvent);
|
|
134
|
+
break;
|
|
135
|
+
case Data.Event.Box:
|
|
136
|
+
if (payload.box === undefined) { payload.box = []; }
|
|
137
|
+
payload.box.push(layout.decode(entry) as BoxEvent);
|
|
138
|
+
break;
|
|
139
|
+
case Data.Event.Region:
|
|
140
|
+
if (payload.region === undefined) { payload.region = []; }
|
|
141
|
+
payload.region.push(layout.decode(entry) as RegionEvent);
|
|
142
|
+
break;
|
|
143
|
+
case Data.Event.Discover:
|
|
144
|
+
case Data.Event.Mutation:
|
|
145
|
+
if (payload.dom === undefined) { payload.dom = []; }
|
|
146
|
+
payload.dom.push(layout.decode(entry) as DomEvent);
|
|
147
|
+
break;
|
|
148
|
+
case Data.Event.Document:
|
|
149
|
+
if (payload.doc === undefined) { payload.doc = []; }
|
|
150
|
+
payload.doc.push(layout.decode(entry) as DocumentEvent);
|
|
151
|
+
break;
|
|
152
|
+
case Data.Event.ScriptError:
|
|
153
|
+
if (payload.script === undefined) { payload.script = []; }
|
|
154
|
+
payload.script.push(diagnostic.decode(entry) as ScriptErrorEvent);
|
|
155
|
+
break;
|
|
156
|
+
case Data.Event.ImageError:
|
|
157
|
+
if (payload.image === undefined) { payload.image = []; }
|
|
158
|
+
payload.image.push(diagnostic.decode(entry) as ImageErrorEvent);
|
|
159
|
+
break;
|
|
160
|
+
case Data.Event.Log:
|
|
161
|
+
if (payload.log === undefined) { payload.log = []; }
|
|
162
|
+
payload.log.push(diagnostic.decode(entry) as LogEvent);
|
|
163
|
+
break;
|
|
164
|
+
case Data.Event.Connection:
|
|
165
|
+
if (payload.connection === undefined) { payload.connection = []; }
|
|
166
|
+
payload.connection.push(performance.decode(entry) as ConnectionEvent);
|
|
167
|
+
break;
|
|
168
|
+
case Data.Event.Navigation:
|
|
169
|
+
if (payload.navigation === undefined) { payload.navigation = []; }
|
|
170
|
+
payload.navigation.push(performance.decode(entry) as NavigationEvent);
|
|
171
|
+
break;
|
|
172
|
+
default:
|
|
173
|
+
console.error(`No handler for Event: ${JSON.stringify(entry)}`);
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return payload;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
function parseVersion(ver: string): DecodedVersion {
|
|
183
|
+
let output: DecodedVersion = { major: 0, minor: 0, patch: 0, beta: 0 };
|
|
184
|
+
let parts = ver.split(".");
|
|
185
|
+
if (parts.length === 3) {
|
|
186
|
+
let subparts = parts[2].split("-b");
|
|
187
|
+
output.major = parseInt(parts[0], 10);
|
|
188
|
+
output.minor = parseInt(parts[1], 10);
|
|
189
|
+
if (subparts.length === 2) {
|
|
190
|
+
output.patch = parseInt(subparts[0], 10);
|
|
191
|
+
output.beta = parseInt(subparts[1], 10);
|
|
192
|
+
} else { output.patch = parseInt(parts[2], 10); }
|
|
193
|
+
}
|
|
194
|
+
return output;
|
|
195
|
+
}
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Data } from "clarity-js";
|
|
2
|
+
import { Constant, DataEvent } from "../types/data";
|
|
3
|
+
|
|
4
|
+
export function decode(tokens: Data.Token[]): DataEvent {
|
|
5
|
+
let time = tokens[0] as number;
|
|
6
|
+
let event = tokens[1] as Data.Event;
|
|
7
|
+
switch (event) {
|
|
8
|
+
case Data.Event.Ping:
|
|
9
|
+
let ping: Data.PingData = { gap: tokens[2] as number };
|
|
10
|
+
return { time, event, data: ping };
|
|
11
|
+
case Data.Event.Limit:
|
|
12
|
+
let limit: Data.LimitData = { check: tokens[2] as number };
|
|
13
|
+
return { time, event, data: limit };
|
|
14
|
+
case Data.Event.Custom:
|
|
15
|
+
let custom: Data.CustomData = { key: tokens[2] as string, value: tokens[3] as string };
|
|
16
|
+
return { time, event, data: custom };
|
|
17
|
+
case Data.Event.Upgrade:
|
|
18
|
+
let upgrade: Data.UpgradeData = { key: tokens[2] as string };
|
|
19
|
+
return { time, event, data: upgrade };
|
|
20
|
+
case Data.Event.Upload:
|
|
21
|
+
let upload: Data.UploadData = { sequence: tokens[2] as number, attempts: tokens[3] as number, status: tokens[4] as number };
|
|
22
|
+
return { time, event, data: upload };
|
|
23
|
+
case Data.Event.Metric:
|
|
24
|
+
let m = 2; // Start from 3rd index since first two are used for time & event
|
|
25
|
+
let metrics: Data.MetricData = {};
|
|
26
|
+
while (m < tokens.length) {
|
|
27
|
+
metrics[tokens[m++] as number] = tokens[m++] as number;
|
|
28
|
+
}
|
|
29
|
+
return { time, event, data: metrics };
|
|
30
|
+
case Data.Event.Dimension:
|
|
31
|
+
let d = 2; // Start from 3rd index since first two are used for time & event
|
|
32
|
+
let dimensions: Data.DimensionData = {};
|
|
33
|
+
while (d < tokens.length) {
|
|
34
|
+
dimensions[tokens[d++] as number] = tokens[d++] as string[];
|
|
35
|
+
}
|
|
36
|
+
return { time, event, data: dimensions };
|
|
37
|
+
case Data.Event.Summary:
|
|
38
|
+
let s = 2; // Start from 3rd index since first two are used for time & event
|
|
39
|
+
let summary: Data.SummaryData = {};
|
|
40
|
+
while (s < tokens.length) {
|
|
41
|
+
let key = tokens[s++] as number;
|
|
42
|
+
let values = tokens[s++] as number[];
|
|
43
|
+
summary[key] = [];
|
|
44
|
+
for (let i = 0; i < values.length - 1; i += 2) {
|
|
45
|
+
summary[key].push([values[i], values[i + 1]]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { time, event, data: summary };
|
|
49
|
+
case Data.Event.Baseline:
|
|
50
|
+
let baselineData: Data.BaselineData = {
|
|
51
|
+
visible: tokens[2] as number,
|
|
52
|
+
docWidth: tokens[3] as number,
|
|
53
|
+
docHeight: tokens[4] as number,
|
|
54
|
+
screenWidth: tokens[5] as number,
|
|
55
|
+
screenHeight: tokens[6] as number,
|
|
56
|
+
scrollX: tokens[7] as number,
|
|
57
|
+
scrollY: tokens[8] as number,
|
|
58
|
+
pointerX: tokens[9] as number,
|
|
59
|
+
pointerY: tokens[10] as number,
|
|
60
|
+
activityTime: tokens[11] as number
|
|
61
|
+
}
|
|
62
|
+
return { time, event, data: baselineData };
|
|
63
|
+
case Data.Event.Variable:
|
|
64
|
+
let v = 2; // Start from 3rd index since first two are used for time & event
|
|
65
|
+
let variables: Data.VariableData = {};
|
|
66
|
+
while (v < tokens.length) {
|
|
67
|
+
variables[tokens[v++] as string] = typeof tokens[v + 1] == Constant.String ? [tokens[v++] as string] : tokens[v++] as string[];
|
|
68
|
+
}
|
|
69
|
+
return { time, event, data: variables };
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function envelope(tokens: Data.Token[]): Data.Envelope {
|
|
75
|
+
return {
|
|
76
|
+
version: tokens[0] as string,
|
|
77
|
+
sequence: tokens[1] as number,
|
|
78
|
+
start: tokens[2] as number,
|
|
79
|
+
duration: tokens[3] as number,
|
|
80
|
+
projectId: tokens[4] as string,
|
|
81
|
+
userId: tokens[5] as string,
|
|
82
|
+
sessionId: tokens[6] as string,
|
|
83
|
+
pageNum: tokens[7] as number,
|
|
84
|
+
upload: tokens[8] as Data.Upload,
|
|
85
|
+
end: tokens[9] as Data.BooleanFlag
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Data, Diagnostic } from "clarity-js";
|
|
2
|
+
import { DiagnosticEvent } from "../types/diagnostic";
|
|
3
|
+
|
|
4
|
+
export function decode(tokens: Data.Token[]): DiagnosticEvent {
|
|
5
|
+
let time = tokens[0] as number;
|
|
6
|
+
let event = tokens[1] as Data.Event;
|
|
7
|
+
switch (event) {
|
|
8
|
+
case Data.Event.ImageError:
|
|
9
|
+
let imageError: Diagnostic.ImageErrorData = {
|
|
10
|
+
source: tokens[2] as string,
|
|
11
|
+
target: tokens[3] as number,
|
|
12
|
+
};
|
|
13
|
+
return { time, event, data: imageError };
|
|
14
|
+
case Data.Event.ScriptError:
|
|
15
|
+
let scriptError: Diagnostic.ScriptErrorData = {
|
|
16
|
+
message: tokens[2] as string,
|
|
17
|
+
line: tokens[3] as number,
|
|
18
|
+
column: tokens[4] as number,
|
|
19
|
+
stack: tokens[5] as string,
|
|
20
|
+
source: tokens[6] as string
|
|
21
|
+
};
|
|
22
|
+
return { time, event, data: scriptError };
|
|
23
|
+
case Data.Event.Log:
|
|
24
|
+
let log: Diagnostic.LogData = {
|
|
25
|
+
code: tokens[2] as number,
|
|
26
|
+
name: tokens[3] as string,
|
|
27
|
+
message: tokens[4] as string,
|
|
28
|
+
stack: tokens[5] as string,
|
|
29
|
+
severity: tokens[6] as number
|
|
30
|
+
};
|
|
31
|
+
return { time, event, data: log };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
package/src/global.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as decode from "@src/clarity";
|
|
2
|
+
|
|
3
|
+
// Expose clarity variable globally to allow access to public interface in a browser
|
|
4
|
+
if (typeof window !== "undefined") {
|
|
5
|
+
if ((window as any).clarity === undefined || (window as any).clarity === null) {
|
|
6
|
+
(window as any).clarity = {};
|
|
7
|
+
}
|
|
8
|
+
(window as any).clarity.decode = decode;
|
|
9
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { decode } from "./clarity";
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Data, Interaction } from "clarity-js";
|
|
2
|
+
import { InteractionEvent } from "../types/interaction";
|
|
3
|
+
|
|
4
|
+
export function decode(tokens: Data.Token[]): InteractionEvent {
|
|
5
|
+
let time = tokens[0] as number;
|
|
6
|
+
let event = tokens[1] as Data.Event;
|
|
7
|
+
switch (event) {
|
|
8
|
+
case Data.Event.MouseDown:
|
|
9
|
+
case Data.Event.MouseUp:
|
|
10
|
+
case Data.Event.MouseMove:
|
|
11
|
+
case Data.Event.MouseWheel:
|
|
12
|
+
case Data.Event.DoubleClick:
|
|
13
|
+
case Data.Event.TouchStart:
|
|
14
|
+
case Data.Event.TouchCancel:
|
|
15
|
+
case Data.Event.TouchEnd:
|
|
16
|
+
case Data.Event.TouchMove:
|
|
17
|
+
let pointerData: Interaction.PointerData = {
|
|
18
|
+
target: tokens[2] as number,
|
|
19
|
+
x: tokens[3] as number,
|
|
20
|
+
y: tokens[4] as number
|
|
21
|
+
};
|
|
22
|
+
return { time, event, data: pointerData };
|
|
23
|
+
case Data.Event.Click:
|
|
24
|
+
let clickData: Interaction.ClickData = {
|
|
25
|
+
target: tokens[2] as number,
|
|
26
|
+
x: tokens[3] as number,
|
|
27
|
+
y: tokens[4] as number,
|
|
28
|
+
eX: tokens[5] as number,
|
|
29
|
+
eY: tokens[6] as number,
|
|
30
|
+
button: tokens[7] as number,
|
|
31
|
+
reaction: tokens[8] as number,
|
|
32
|
+
context: tokens[9] as number,
|
|
33
|
+
text: tokens[10] as string,
|
|
34
|
+
link: tokens[11] as string,
|
|
35
|
+
hash: tokens[12] as string
|
|
36
|
+
};
|
|
37
|
+
return { time, event, data: clickData };
|
|
38
|
+
case Data.Event.Resize:
|
|
39
|
+
let resizeData: Interaction.ResizeData = { width: tokens[2] as number, height: tokens[3] as number };
|
|
40
|
+
return { time, event, data: resizeData };
|
|
41
|
+
case Data.Event.Input:
|
|
42
|
+
let inputData: Interaction.InputData = {
|
|
43
|
+
target: tokens[2] as number,
|
|
44
|
+
value: tokens[3] as string
|
|
45
|
+
};
|
|
46
|
+
return { time, event, data: inputData };
|
|
47
|
+
case Data.Event.Selection:
|
|
48
|
+
let selectionData: Interaction.SelectionData = {
|
|
49
|
+
start: tokens[2] as number,
|
|
50
|
+
startOffset: tokens[3] as number,
|
|
51
|
+
end: tokens[4] as number,
|
|
52
|
+
endOffset: tokens[5] as number
|
|
53
|
+
};
|
|
54
|
+
return { time, event, data: selectionData };
|
|
55
|
+
case Data.Event.Scroll:
|
|
56
|
+
let scrollData: Interaction.ScrollData = {
|
|
57
|
+
target: tokens[2] as number,
|
|
58
|
+
x: tokens[3] as number,
|
|
59
|
+
y: tokens[4] as number
|
|
60
|
+
};
|
|
61
|
+
return { time, event, data: scrollData };
|
|
62
|
+
case Data.Event.Timeline:
|
|
63
|
+
let timelineData: Interaction.TimelineData = {
|
|
64
|
+
type: tokens[2] as number,
|
|
65
|
+
hash: tokens[3] as string,
|
|
66
|
+
x: tokens[4] as number,
|
|
67
|
+
y: tokens[5] as number,
|
|
68
|
+
reaction: tokens[6] as number,
|
|
69
|
+
context: tokens[7] as number
|
|
70
|
+
};
|
|
71
|
+
return { time, event, data: timelineData };
|
|
72
|
+
case Data.Event.Visibility:
|
|
73
|
+
let visibleData: Interaction.VisibilityData = { visible: tokens[2] as string };
|
|
74
|
+
return { time, event, data: visibleData };
|
|
75
|
+
case Data.Event.Unload:
|
|
76
|
+
let unloadData: Interaction.UnloadData = { name: tokens[2] as string };
|
|
77
|
+
return { time, event, data: unloadData };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
package/src/layout.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { helper, Data, Layout } from "clarity-js";
|
|
2
|
+
import { DomData, LayoutEvent } from "../types/layout";
|
|
3
|
+
|
|
4
|
+
const AverageWordLength = 6;
|
|
5
|
+
const Space = " ";
|
|
6
|
+
let hashes: { [key: number]: string } = {};
|
|
7
|
+
|
|
8
|
+
export function reset(): void {
|
|
9
|
+
hashes = {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function decode(tokens: Data.Token[]): LayoutEvent {
|
|
13
|
+
let time = tokens[0] as number;
|
|
14
|
+
let event = tokens[1] as Data.Event;
|
|
15
|
+
|
|
16
|
+
switch (event) {
|
|
17
|
+
case Data.Event.Document:
|
|
18
|
+
let documentData: Layout.DocumentData = { width: tokens[2] as number, height: tokens[3] as number };
|
|
19
|
+
return { time, event, data: documentData };
|
|
20
|
+
case Data.Event.Region:
|
|
21
|
+
let regionData: Layout.RegionData[] = [];
|
|
22
|
+
// From 0.6.15 we send each reach update in an individual event. This allows us to include time with it.
|
|
23
|
+
// To keep it backward compatible (<= 0.6.14), we look for multiple regions in the same event. This works both with newer and older payloads.
|
|
24
|
+
// In future, we can update the logic to look deterministically for only 3 fields and remove the for loop.
|
|
25
|
+
for (let i = 2; i < tokens.length; i += 3) {
|
|
26
|
+
let region: Layout.RegionData = {
|
|
27
|
+
id: tokens[i] as number,
|
|
28
|
+
state: tokens[i + 1] as number,
|
|
29
|
+
name: tokens[i + 2] as string
|
|
30
|
+
};
|
|
31
|
+
regionData.push(region);
|
|
32
|
+
}
|
|
33
|
+
return { time, event, data: regionData };
|
|
34
|
+
case Data.Event.Box:
|
|
35
|
+
let boxData: Layout.BoxData[] = [];
|
|
36
|
+
for (let i = 2; i < tokens.length; i += 3) {
|
|
37
|
+
let box: Layout.BoxData = {
|
|
38
|
+
id: tokens[i] as number,
|
|
39
|
+
width: tokens[i + 1] as number / Data.Setting.BoxPrecision,
|
|
40
|
+
height: tokens[i + 2] as number / Data.Setting.BoxPrecision
|
|
41
|
+
};
|
|
42
|
+
boxData.push(box);
|
|
43
|
+
}
|
|
44
|
+
return { time, event, data: boxData };
|
|
45
|
+
case Data.Event.Discover:
|
|
46
|
+
case Data.Event.Mutation:
|
|
47
|
+
let lastType = null;
|
|
48
|
+
let node = [];
|
|
49
|
+
let tagIndex = 0;
|
|
50
|
+
let domData: DomData[] = [];
|
|
51
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
52
|
+
let token = tokens[i];
|
|
53
|
+
let type = typeof(token);
|
|
54
|
+
switch (type) {
|
|
55
|
+
case "number":
|
|
56
|
+
if (type !== lastType && lastType !== null) {
|
|
57
|
+
domData.push(process(node, tagIndex));
|
|
58
|
+
node = [];
|
|
59
|
+
tagIndex = 0;
|
|
60
|
+
}
|
|
61
|
+
node.push(token);
|
|
62
|
+
tagIndex++;
|
|
63
|
+
break;
|
|
64
|
+
case "string":
|
|
65
|
+
node.push(token);
|
|
66
|
+
break;
|
|
67
|
+
case "object":
|
|
68
|
+
let subtoken = token[0];
|
|
69
|
+
let subtype = typeof(subtoken);
|
|
70
|
+
switch (subtype) {
|
|
71
|
+
case "number":
|
|
72
|
+
for (let t of (token as number[])) {
|
|
73
|
+
node.push(tokens.length > t ? tokens[t] : null);
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
lastType = type;
|
|
79
|
+
}
|
|
80
|
+
// Process last node
|
|
81
|
+
domData.push(process(node, tagIndex));
|
|
82
|
+
|
|
83
|
+
return { time, event, data: domData };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function process(node: any[] | number[], tagIndex: number): DomData {
|
|
89
|
+
let [tag, position]: string[] = node[tagIndex] ? node[tagIndex].split("~") : [node[tagIndex]];
|
|
90
|
+
let output: DomData = {
|
|
91
|
+
id: Math.abs(node[0]),
|
|
92
|
+
parent: tagIndex > 1 ? node[1] : null,
|
|
93
|
+
previous: tagIndex > 2 ? node[2] : null,
|
|
94
|
+
tag,
|
|
95
|
+
position: position ? parseInt(position, 10) : null,
|
|
96
|
+
selector: null,
|
|
97
|
+
hash: null
|
|
98
|
+
};
|
|
99
|
+
let masked = node[0] < 0;
|
|
100
|
+
let hasAttribute = false;
|
|
101
|
+
let attributes: Layout.Attributes = {};
|
|
102
|
+
let value = null;
|
|
103
|
+
let prefix = output.parent in hashes ? `${hashes[output.parent]}>` : (output.parent ? Layout.Constant.Empty : null);
|
|
104
|
+
|
|
105
|
+
for (let i = tagIndex + 1; i < node.length; i++) {
|
|
106
|
+
// Explicitly convert the token into a string value
|
|
107
|
+
let token = node[i].toString();
|
|
108
|
+
let keyIndex = token.indexOf("=");
|
|
109
|
+
let firstChar = token[0];
|
|
110
|
+
let lastChar = token[token.length - 1];
|
|
111
|
+
if (i === (node.length - 1) && output.tag === "STYLE") {
|
|
112
|
+
value = token;
|
|
113
|
+
} else if (output.tag !== Layout.Constant.TextTag && lastChar === ">" && keyIndex === -1) {
|
|
114
|
+
prefix = token;
|
|
115
|
+
} else if (output.tag !== Layout.Constant.TextTag && firstChar === Layout.Constant.Box && keyIndex === -1) {
|
|
116
|
+
let parts = token.substr(1).split(Layout.Constant.Period);
|
|
117
|
+
if (parts.length === 2) {
|
|
118
|
+
output.width = num(parts[0]) / Data.Setting.BoxPrecision;
|
|
119
|
+
output.height = num(parts[1]) / Data.Setting.BoxPrecision;
|
|
120
|
+
}
|
|
121
|
+
} else if (output.tag !== Layout.Constant.TextTag && keyIndex > 0) {
|
|
122
|
+
hasAttribute = true;
|
|
123
|
+
let k = token.substr(0, keyIndex);
|
|
124
|
+
let v = token.substr(keyIndex + 1);
|
|
125
|
+
attributes[k] = v;
|
|
126
|
+
} else if (output.tag === Layout.Constant.TextTag) {
|
|
127
|
+
value = masked ? unmask(token) : token;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let selector = helper.selector(output.tag, prefix, attributes, output.position);
|
|
132
|
+
if (selector.length > 0) {
|
|
133
|
+
output.selector = selector;
|
|
134
|
+
output.hash = helper.hash(selector);
|
|
135
|
+
hashes[output.id] = selector;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (hasAttribute) { output.attributes = attributes; }
|
|
139
|
+
if (value) { output.value = value; }
|
|
140
|
+
|
|
141
|
+
return output;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function num(input: string): number {
|
|
145
|
+
return input ? parseInt(input, 36) : null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function unmask(value: string): string {
|
|
149
|
+
let trimmed = value.trim();
|
|
150
|
+
if (trimmed.length > 0 && trimmed.indexOf(Space) === -1) {
|
|
151
|
+
let length = num(trimmed);
|
|
152
|
+
if (length > 0) {
|
|
153
|
+
let quotient = Math.floor(length / AverageWordLength);
|
|
154
|
+
let remainder = length % AverageWordLength;
|
|
155
|
+
let output = Array(remainder + 1).join(Data.Constant.Mask);
|
|
156
|
+
for (let i = 0; i < quotient; i++) {
|
|
157
|
+
output += (i === 0 && remainder === 0 ? Data.Constant.Mask : Space) + Array(AverageWordLength).join(Data.Constant.Mask);
|
|
158
|
+
}
|
|
159
|
+
return output;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return value;
|
|
163
|
+
}
|