clarity-js 0.6.31 → 0.6.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/clarity.js +2150 -2099
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +2150 -2099
- package/package.json +1 -1
- package/src/core/api.ts +8 -0
- package/src/core/config.ts +1 -2
- package/src/core/event.ts +4 -3
- package/src/core/history.ts +19 -26
- package/src/core/version.ts +1 -1
- package/src/data/encode.ts +9 -0
- package/src/data/extract.ts +143 -0
- package/src/data/index.ts +3 -1
- package/src/data/metadata.ts +14 -9
- package/src/data/upload.ts +11 -8
- package/src/diagnostic/internal.ts +1 -15
- package/src/layout/dom.ts +36 -9
- package/src/layout/mutation.ts +41 -52
- package/types/core.d.ts +25 -13
- package/types/data.d.ts +21 -4
- package/types/layout.d.ts +1 -2
- package/src/layout/extract.ts +0 -94
package/src/layout/mutation.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Priority, Task, Timer } from "@clarity-types/core";
|
|
2
2
|
import { Code, Event, Metric, Severity } from "@clarity-types/data";
|
|
3
3
|
import { Constant, MutationHistory, MutationQueue, Setting, Source } from "@clarity-types/layout";
|
|
4
|
+
import api from "@src/core/api";
|
|
5
|
+
import * as core from "@src/core";
|
|
4
6
|
import { bind } from "@src/core/event";
|
|
5
7
|
import measure from "@src/core/measure";
|
|
6
8
|
import * as task from "@src/core/task";
|
|
@@ -34,32 +36,38 @@ export function start(): void {
|
|
|
34
36
|
activePeriod = 0;
|
|
35
37
|
history = {};
|
|
36
38
|
|
|
37
|
-
if (insertRule === null) { insertRule = CSSStyleSheet.prototype.insertRule; }
|
|
38
|
-
if (deleteRule === null) { deleteRule = CSSStyleSheet.prototype.deleteRule; }
|
|
39
|
-
if (attachShadow === null) { attachShadow = Element.prototype.attachShadow; }
|
|
40
|
-
|
|
41
39
|
// Some popular open source libraries, like styled-components, optimize performance
|
|
42
40
|
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
43
41
|
// using javascript API is that it doesn't trigger DOM mutation and therefore we
|
|
44
42
|
// need to override the insertRule API and listen for changes manually.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return deleteRule.apply(this, arguments);
|
|
53
|
-
};
|
|
43
|
+
if (insertRule === null) {
|
|
44
|
+
insertRule = CSSStyleSheet.prototype.insertRule;
|
|
45
|
+
CSSStyleSheet.prototype.insertRule = function(): number {
|
|
46
|
+
if (core.active()) { schedule(this.ownerNode); }
|
|
47
|
+
return insertRule.apply(this, arguments);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
51
|
+
if (deleteRule === null) {
|
|
52
|
+
deleteRule = CSSStyleSheet.prototype.deleteRule;
|
|
53
|
+
CSSStyleSheet.prototype.deleteRule = function(): void {
|
|
54
|
+
if (core.active()) { schedule(this.ownerNode); }
|
|
55
|
+
return deleteRule.apply(this, arguments);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Add a hook to attachShadow API calls
|
|
60
|
+
// In case we are unable to add a hook and browser throws an exception,
|
|
61
|
+
// reset attachShadow variable and resume processing like before
|
|
62
|
+
if (attachShadow === null) {
|
|
63
|
+
attachShadow = Element.prototype.attachShadow;
|
|
64
|
+
try {
|
|
65
|
+
Element.prototype.attachShadow = function (): ShadowRoot {
|
|
66
|
+
if (core.active()) { return schedule(attachShadow.apply(this, arguments)) as ShadowRoot; }
|
|
67
|
+
else { return attachShadow.apply(this, arguments)}
|
|
68
|
+
}
|
|
69
|
+
} catch { attachShadow = null; }
|
|
70
|
+
}
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
export function observe(node: Node): void {
|
|
@@ -68,11 +76,8 @@ export function observe(node: Node): void {
|
|
|
68
76
|
// For this reason, we need to wire up mutations every time we see a new shadow dom.
|
|
69
77
|
// Also, wrap it inside a try / catch. In certain browsers (e.g. legacy Edge), observer on shadow dom can throw errors
|
|
70
78
|
try {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
// As a temporary work around, ensuring Clarity can invoke MutationObserver outside of Zone (and use native implementation instead)
|
|
74
|
-
let api: string = window[Constant.Zone] && Constant.Symbol in window[Constant.Zone] ? window[Constant.Zone][Constant.Symbol](Constant.MutationObserver) : Constant.MutationObserver;
|
|
75
|
-
let observer = api in window ? new window[api](measure(handle) as MutationCallback) : null;
|
|
79
|
+
let m = api(Constant.MutationObserver);
|
|
80
|
+
let observer = m in window ? new window[m](measure(handle) as MutationCallback) : null;
|
|
76
81
|
if (observer) {
|
|
77
82
|
observer.observe(node, { attributes: true, childList: true, characterData: true, subtree: true });
|
|
78
83
|
observers.push(observer);
|
|
@@ -92,25 +97,6 @@ export function monitor(frame: HTMLIFrameElement): void {
|
|
|
92
97
|
export function stop(): void {
|
|
93
98
|
for (let observer of observers) { if (observer) { observer.disconnect(); } }
|
|
94
99
|
observers = [];
|
|
95
|
-
|
|
96
|
-
// Restoring original insertRule
|
|
97
|
-
if (insertRule !== null) {
|
|
98
|
-
CSSStyleSheet.prototype.insertRule = insertRule;
|
|
99
|
-
insertRule = null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Restoring original deleteRule
|
|
103
|
-
if (deleteRule !== null) {
|
|
104
|
-
CSSStyleSheet.prototype.deleteRule = deleteRule;
|
|
105
|
-
deleteRule = null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Restoring original attachShadow
|
|
109
|
-
if (attachShadow != null) {
|
|
110
|
-
Element.prototype.attachShadow = attachShadow;
|
|
111
|
-
attachShadow = null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
100
|
history = {};
|
|
115
101
|
mutations = [];
|
|
116
102
|
queue = [];
|
|
@@ -220,7 +206,7 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer): Pr
|
|
|
220
206
|
}
|
|
221
207
|
}
|
|
222
208
|
|
|
223
|
-
function schedule(node: Node): Node {
|
|
209
|
+
export function schedule(node: Node, fragment: boolean = false): Node {
|
|
224
210
|
// Only schedule manual trigger for this node if it's not already in the queue
|
|
225
211
|
if (queue.indexOf(node) < 0) { queue.push(node); }
|
|
226
212
|
|
|
@@ -228,17 +214,20 @@ function schedule(node: Node): Node {
|
|
|
228
214
|
// It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
|
|
229
215
|
// And in those cases we do not wish to monitor changes multiple times for the same node.
|
|
230
216
|
if (timeout) { clearTimeout(timeout); }
|
|
231
|
-
timeout = setTimeout(trigger, Setting.LookAhead);
|
|
217
|
+
timeout = setTimeout(() => { trigger(fragment) }, Setting.LookAhead);
|
|
232
218
|
|
|
233
219
|
return node;
|
|
234
220
|
}
|
|
235
221
|
|
|
236
|
-
function trigger(): void {
|
|
222
|
+
function trigger(fragment: boolean): void {
|
|
237
223
|
for (let node of queue) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
224
|
+
// Generate a mutation for this node only if it still exists
|
|
225
|
+
if (node) {
|
|
226
|
+
let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
227
|
+
// Skip re-processing shadowRoot if it was already discovered
|
|
228
|
+
if (shadowRoot && dom.has(node)) { continue; }
|
|
229
|
+
generate(node, shadowRoot || fragment ? Constant.ChildList : Constant.CharacterData);
|
|
230
|
+
}
|
|
242
231
|
}
|
|
243
232
|
queue = [];
|
|
244
233
|
}
|
package/types/core.d.ts
CHANGED
|
@@ -3,9 +3,8 @@ import * as Data from "./data";
|
|
|
3
3
|
type TaskFunction = () => Promise<void>;
|
|
4
4
|
type TaskResolve = () => void;
|
|
5
5
|
type UploadCallback = (data: string) => void;
|
|
6
|
-
type Region = [number /* RegionId */, string /* Query Selector
|
|
7
|
-
type
|
|
8
|
-
type Dimension = [Data.Dimension /* DimensionId */, Extract /* Extract Filter */, string /* Match Value */];
|
|
6
|
+
type Region = [number /* RegionId */, string /* Query Selector */];
|
|
7
|
+
export type Extract = ExtractSource /* Extraction Source */ | number /* Extract Id */ | string | string[] /* Hash or Query Selector or String Token */;
|
|
9
8
|
|
|
10
9
|
/* Enum */
|
|
11
10
|
|
|
@@ -21,7 +20,6 @@ export const enum Time {
|
|
|
21
20
|
Day = 24 * 60 * 60 * 1000
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
23
|
export const enum Task {
|
|
26
24
|
Wait = 0,
|
|
27
25
|
Run = 1,
|
|
@@ -32,15 +30,23 @@ export const enum Setting {
|
|
|
32
30
|
LongTask = 30, // 30ms
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
export const enum
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
export const enum ExtractSource {
|
|
34
|
+
Javascript = 0,
|
|
35
|
+
Cookie = 1,
|
|
36
|
+
Text = 2,
|
|
37
|
+
Fragment = 3
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const enum Type {
|
|
41
|
+
Array = 1,
|
|
42
|
+
Object = 2,
|
|
43
|
+
Simple = 3
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
export
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
export type Syntax = {
|
|
47
|
+
name: string,
|
|
48
|
+
type: Type,
|
|
49
|
+
condition: string
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
export const enum Privacy {
|
|
@@ -119,11 +125,17 @@ export interface Config {
|
|
|
119
125
|
mask?: string[];
|
|
120
126
|
unmask?: string[];
|
|
121
127
|
regions?: Region[];
|
|
122
|
-
|
|
123
|
-
dimensions?: Dimension[];
|
|
128
|
+
extract?: Extract[];
|
|
124
129
|
cookies?: string[];
|
|
125
130
|
report?: string;
|
|
126
131
|
upload?: string | UploadCallback;
|
|
127
132
|
fallback?: string;
|
|
128
133
|
upgrade?: (key: string) => void;
|
|
129
134
|
}
|
|
135
|
+
|
|
136
|
+
export const enum Constant {
|
|
137
|
+
Zone = "Zone",
|
|
138
|
+
Symbol = "__symbol__",
|
|
139
|
+
AddEventListener = "addEventListener",
|
|
140
|
+
RemoveEventListener = "removeEventListener"
|
|
141
|
+
}
|
package/types/data.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Time } from "@clarity-types/core";
|
|
2
|
+
import { callback } from "@src/data/metadata";
|
|
2
3
|
export type Target = (number | Node);
|
|
3
4
|
export type Token = (string | number | number[] | string[]);
|
|
4
5
|
export type DecodedToken = (any | any[]);
|
|
5
6
|
|
|
6
7
|
export type MetadataCallback = (data: Metadata, playback: boolean) => void;
|
|
8
|
+
export interface MetadataCallbackOptions {
|
|
9
|
+
callback: MetadataCallback,
|
|
10
|
+
wait: boolean
|
|
11
|
+
}
|
|
7
12
|
|
|
8
13
|
/* Enum */
|
|
9
|
-
|
|
10
14
|
export const enum Event {
|
|
11
15
|
/* Data */
|
|
12
16
|
Metric = 0,
|
|
@@ -54,7 +58,8 @@ export const enum Event {
|
|
|
54
58
|
Summary = 36,
|
|
55
59
|
Box = 37,
|
|
56
60
|
Clipboard = 38,
|
|
57
|
-
Submit = 39
|
|
61
|
+
Submit = 39,
|
|
62
|
+
Extract = 40
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
export const enum Metric {
|
|
@@ -135,7 +140,11 @@ export const enum Code {
|
|
|
135
140
|
CallStackDepth = 4,
|
|
136
141
|
Selector = 5,
|
|
137
142
|
Metric = 6,
|
|
138
|
-
|
|
143
|
+
/**
|
|
144
|
+
* @deprecated No longer support ContentSecurityPolicy
|
|
145
|
+
*/
|
|
146
|
+
ContentSecurityPolicy = 7,
|
|
147
|
+
Config = 8
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
export const enum Severity {
|
|
@@ -184,7 +193,8 @@ export const enum Setting {
|
|
|
184
193
|
MaxFirstPayloadBytes = 1 * 1024 * 1024, // 1MB: Cap the very first payload to a maximum of 1MB
|
|
185
194
|
UploadFactor = 3, // Slow down sequence by specified factor
|
|
186
195
|
MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
|
|
187
|
-
MaxUploadDelay = 30 * Time.Second // Do flush out payload once every 30s
|
|
196
|
+
MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
|
|
197
|
+
ExtractLimit = 10000 // Do not extract more than 10000 characters
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
export const enum Character {
|
|
@@ -245,6 +255,9 @@ export const enum Constant {
|
|
|
245
255
|
Accept = "Accept",
|
|
246
256
|
ClarityGzip = "application/x-clarity-gzip",
|
|
247
257
|
Tilde = "~",
|
|
258
|
+
ArrayStart = "[",
|
|
259
|
+
ConditionStart = "{",
|
|
260
|
+
ConditionEnd = "}"
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
export const enum XMLReadyState {
|
|
@@ -363,6 +376,10 @@ export interface UpgradeData {
|
|
|
363
376
|
key: string;
|
|
364
377
|
}
|
|
365
378
|
|
|
379
|
+
export interface ExtractData {
|
|
380
|
+
[key: string]: string | number;
|
|
381
|
+
}
|
|
382
|
+
|
|
366
383
|
export interface UploadData {
|
|
367
384
|
sequence: number;
|
|
368
385
|
attempts: number;
|
package/types/layout.d.ts
CHANGED
|
@@ -75,8 +75,6 @@ export const enum Constant {
|
|
|
75
75
|
BorderBox = "border-box",
|
|
76
76
|
Value = "value",
|
|
77
77
|
MutationObserver = "MutationObserver",
|
|
78
|
-
Zone = "Zone",
|
|
79
|
-
Symbol = "__symbol__",
|
|
80
78
|
JsonLD = "application/ld+json",
|
|
81
79
|
String = "string",
|
|
82
80
|
Number = "number",
|
|
@@ -156,6 +154,7 @@ export interface NodeValue {
|
|
|
156
154
|
hash: [string, string];
|
|
157
155
|
region: number;
|
|
158
156
|
metadata: NodeMeta;
|
|
157
|
+
fragment: number;
|
|
159
158
|
}
|
|
160
159
|
|
|
161
160
|
export interface NodeMeta {
|
package/src/layout/extract.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { Dimension, Extract, Metric, Region, RegionFilter } from "@clarity-types/core";
|
|
2
|
-
import { Constant, Setting } from "@clarity-types/data";
|
|
3
|
-
import * as dimension from "@src/data/dimension";
|
|
4
|
-
import * as metric from "@src/data/metric";
|
|
5
|
-
import * as region from "@src/layout/region";
|
|
6
|
-
|
|
7
|
-
const formatRegex = /1/g;
|
|
8
|
-
const digitsRegex = /[^0-9\.]/g;
|
|
9
|
-
const digitsWithCommaRegex = /[^0-9\.,]/g;
|
|
10
|
-
const regexCache: {[key: string]: RegExp} = {};
|
|
11
|
-
|
|
12
|
-
export function regions(root: ParentNode, value: Region[]): void {
|
|
13
|
-
for (let v of value) {
|
|
14
|
-
const [regionId, selector, filter, match] = v;
|
|
15
|
-
let valid = true;
|
|
16
|
-
switch (filter) {
|
|
17
|
-
case RegionFilter.Url: valid = match && !!top.location.href.match(regex(match)); break;
|
|
18
|
-
case RegionFilter.Javascript: valid = match && !!evaluate(match); break;
|
|
19
|
-
}
|
|
20
|
-
if (valid) { root.querySelectorAll(selector).forEach(e => region.observe(e, regionId.toString())); }
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function metrics(root: ParentNode, value: Metric[]): void {
|
|
25
|
-
for (let v of value) {
|
|
26
|
-
const [metricId, source, match, scale] = v;
|
|
27
|
-
if (match) {
|
|
28
|
-
switch (source) {
|
|
29
|
-
case Extract.Text: root.querySelectorAll(match).forEach(e => { metric.max(metricId, num((e as HTMLElement).innerText, scale)); }); break;
|
|
30
|
-
case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { metric.max(metricId, num(e.getAttribute(match), scale, false)); }); break;
|
|
31
|
-
case Extract.Javascript: metric.max(metricId, evaluate(match, Constant.Number) as number); break;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function dimensions(root: ParentNode, value: Dimension[]): void {
|
|
38
|
-
for (let v of value) {
|
|
39
|
-
const [dimensionId, source, match] = v;
|
|
40
|
-
if (match) {
|
|
41
|
-
switch (source) {
|
|
42
|
-
case Extract.Text: root.querySelectorAll(match).forEach(e => { dimension.log(dimensionId, str((e as HTMLElement).innerText)); }); break;
|
|
43
|
-
case Extract.Attribute: root.querySelectorAll(`[${match}]`).forEach(e => { dimension.log(dimensionId, str(e.getAttribute(match))); }); break;
|
|
44
|
-
case Extract.Javascript: dimension.log(dimensionId, str(evaluate(match, Constant.String))); break;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function regex(match: string): RegExp {
|
|
51
|
-
regexCache[match] = match in regexCache ? regexCache[match] : new RegExp(match);
|
|
52
|
-
return regexCache[match];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// The function below takes in a variable name in following format: "a.b.c" and safely evaluates its value in javascript context
|
|
56
|
-
// For instance, for a.b.c, it will first check window["a"]. If it exists, it will recursively look at: window["a"]["b"] and finally,
|
|
57
|
-
// return the value for window["a"]["b"]["c"].
|
|
58
|
-
function evaluate(variable: string, type: string = null, base: Object = window): any {
|
|
59
|
-
let parts = variable.split(Constant.Dot);
|
|
60
|
-
let first = parts.shift();
|
|
61
|
-
if (base && base[first]) {
|
|
62
|
-
if (parts.length > 0) { return evaluate(parts.join(Constant.Dot), type, base[first]); }
|
|
63
|
-
let output = type === null || type === typeof base[first] ? base[first] : null;
|
|
64
|
-
return output;
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function str(input: string): string {
|
|
70
|
-
// Automatically trim string to max of Setting.DimensionLimit to avoid fetching long strings
|
|
71
|
-
return input ? input.substr(0, Setting.DimensionLimit) : input;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function num(text: string, scale: number, localize: boolean = true): number {
|
|
75
|
-
try {
|
|
76
|
-
scale = scale || 1;
|
|
77
|
-
// Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat
|
|
78
|
-
let lang = document.documentElement.lang;
|
|
79
|
-
if (Intl && Intl.NumberFormat && lang && localize) {
|
|
80
|
-
text = text.replace(digitsWithCommaRegex, Constant.Empty);
|
|
81
|
-
// Infer current group and decimal separator from current locale
|
|
82
|
-
let group = Intl.NumberFormat(lang).format(11111).replace(formatRegex, Constant.Empty);
|
|
83
|
-
let decimal = Intl.NumberFormat(lang).format(1.1).replace(formatRegex, Constant.Empty);
|
|
84
|
-
|
|
85
|
-
// Parse number using inferred group and decimal separators
|
|
86
|
-
return Math.round(parseFloat(text
|
|
87
|
-
.replace(new RegExp('\\' + group, 'g'), Constant.Empty)
|
|
88
|
-
.replace(new RegExp('\\' + decimal), Constant.Dot)
|
|
89
|
-
) * scale);
|
|
90
|
-
}
|
|
91
|
-
// Fallback to en locale
|
|
92
|
-
return Math.round(parseFloat(text.replace(digitsRegex, Constant.Empty)) * scale);
|
|
93
|
-
} catch { return null; }
|
|
94
|
-
}
|