clarity-js 0.6.32 → 0.6.33
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 +2134 -2069
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +2134 -2069
- package/package.json +1 -1
- package/src/core/config.ts +1 -2
- 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/layout/dom.ts +36 -9
- package/src/layout/mutation.ts +4 -4
- package/types/core.d.ts +18 -13
- package/types/data.d.ts +13 -3
- package/types/layout.d.ts +1 -0
- package/src/layout/extract.ts +0 -94
package/package.json
CHANGED
package/src/core/config.ts
CHANGED
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let version = "0.6.
|
|
1
|
+
let version = "0.6.33";
|
|
2
2
|
export default version;
|
package/src/data/encode.ts
CHANGED
|
@@ -9,6 +9,7 @@ import * as ping from "@src/data/ping";
|
|
|
9
9
|
import * as summary from "@src/data/summary";
|
|
10
10
|
import * as upgrade from "@src/data/upgrade";
|
|
11
11
|
import * as variable from "@src/data/variable";
|
|
12
|
+
import * as extract from "@src/data/extract";
|
|
12
13
|
import { queue, track } from "./upload";
|
|
13
14
|
|
|
14
15
|
export default function(event: Event): void {
|
|
@@ -105,5 +106,13 @@ export default function(event: Event): void {
|
|
|
105
106
|
queue(tokens, false);
|
|
106
107
|
}
|
|
107
108
|
break;
|
|
109
|
+
case Event.Extract:
|
|
110
|
+
let extractKeys = extract.keys;
|
|
111
|
+
for (let e of extractKeys) {
|
|
112
|
+
tokens.push(e);
|
|
113
|
+
tokens.push(extract.data[e]);
|
|
114
|
+
}
|
|
115
|
+
extract.reset();
|
|
116
|
+
queue(tokens, false);
|
|
108
117
|
}
|
|
109
118
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ExtractSource, Syntax, Type } from "@clarity-types/core";
|
|
2
|
+
import { Event, Setting, ExtractData } from "@clarity-types/data";
|
|
3
|
+
import config from "@src/core/config";
|
|
4
|
+
import encode from "./encode";
|
|
5
|
+
import * as internal from "@src/diagnostic/internal";
|
|
6
|
+
import { Code, Constant, Severity } from "@clarity-types/data";
|
|
7
|
+
|
|
8
|
+
export let data: ExtractData = {};
|
|
9
|
+
export let keys: (number | string)[] = [];
|
|
10
|
+
|
|
11
|
+
let variables : { [key: number]: Syntax[] } = {};
|
|
12
|
+
let selectors : { [key: number]: string } = {};
|
|
13
|
+
export let fragments: string[] = [];
|
|
14
|
+
|
|
15
|
+
export function start(): void {
|
|
16
|
+
try {
|
|
17
|
+
let e = config.extract;
|
|
18
|
+
if (!e) { return; }
|
|
19
|
+
for (let i = 0; i < e.length; i+=3) {
|
|
20
|
+
let source = e[i] as ExtractSource;
|
|
21
|
+
let key = e[i+1] as number;
|
|
22
|
+
switch (source) {
|
|
23
|
+
case ExtractSource.Javascript:
|
|
24
|
+
let variable = e[i+2] as string;
|
|
25
|
+
variables[key] = parse(variable);
|
|
26
|
+
break;
|
|
27
|
+
case ExtractSource.Cookie:
|
|
28
|
+
/*Todo: Add cookie extract logic*/
|
|
29
|
+
break;
|
|
30
|
+
case ExtractSource.Text:
|
|
31
|
+
let match = e[i+2] as string;
|
|
32
|
+
selectors[key] = match;
|
|
33
|
+
break;
|
|
34
|
+
case ExtractSource.Fragment:
|
|
35
|
+
fragments = e[i+2] as string[];
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch(e) {
|
|
41
|
+
internal.log(Code.Config, Severity.Warning, e ? e.name : null);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clone(v: Syntax[]): Syntax[] {
|
|
46
|
+
return JSON.parse(JSON.stringify(v));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function compute(): void {
|
|
50
|
+
try {
|
|
51
|
+
for (let v in variables) {
|
|
52
|
+
let value = str(evaluate(clone(variables[v])));
|
|
53
|
+
if (value) { update(v, value); }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (let s in selectors) {
|
|
57
|
+
let node = document.querySelector(selectors[s] as string) as HTMLElement;
|
|
58
|
+
if (node) { update(s, node.innerText); }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
|
|
62
|
+
|
|
63
|
+
encode(Event.Extract);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function reset(): void {
|
|
67
|
+
keys = [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function update(key: string, value: string | number, force: boolean = false): void {
|
|
71
|
+
if (!(key in data) || (key in data && data[key] !== value) || force ) {
|
|
72
|
+
data[key] = value;
|
|
73
|
+
keys.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function stop(): void {
|
|
78
|
+
data = {};
|
|
79
|
+
keys = [];
|
|
80
|
+
variables = {};
|
|
81
|
+
selectors = {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parse(variable: string): Syntax[] {
|
|
85
|
+
let syntax: Syntax[] = [];
|
|
86
|
+
let parts = variable.split(Constant.Dot);
|
|
87
|
+
while (parts.length > 0) {
|
|
88
|
+
let part = parts.shift();
|
|
89
|
+
let arrayStart = part.indexOf(Constant.ArrayStart);
|
|
90
|
+
let conditionStart = part.indexOf(Constant.ConditionStart);
|
|
91
|
+
let conditionEnd = part.indexOf(Constant.ConditionEnd);
|
|
92
|
+
syntax.push({
|
|
93
|
+
name : arrayStart > 0 ? part.substring(0, arrayStart) : (conditionStart > 0 ? part.substring(0, conditionStart) : part),
|
|
94
|
+
type : arrayStart > 0 ? Type.Array : (conditionStart > 0 ? Type.Object : Type.Simple),
|
|
95
|
+
condition : conditionStart > 0 ? part.substring(conditionStart + 1, conditionEnd) : null
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return syntax;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The function below takes in a variable name in following format: "a.b.c" and safely evaluates its value in javascript context
|
|
103
|
+
// 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,
|
|
104
|
+
// return the value for window["a"]["b"]["c"].
|
|
105
|
+
function evaluate(variable: Syntax[], base: Object = window): any {
|
|
106
|
+
if (variable.length == 0) { return base; }
|
|
107
|
+
let part = variable.shift();
|
|
108
|
+
let output;
|
|
109
|
+
if (base && base[part.name]) {
|
|
110
|
+
let obj = base[part.name];
|
|
111
|
+
if (part.type !== Type.Array && match(obj, part.condition)) {
|
|
112
|
+
output = evaluate(variable, obj);
|
|
113
|
+
}
|
|
114
|
+
else if (Array.isArray(obj)) {
|
|
115
|
+
let filtered = [];
|
|
116
|
+
for (var value of obj) {
|
|
117
|
+
if (match(value, part.condition)) {
|
|
118
|
+
let op = evaluate(variable, value)
|
|
119
|
+
if (op) { filtered.push(op); }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
output = filtered;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return output;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function str(input: string): string {
|
|
132
|
+
// Automatically trim string to max of Setting.ExtractLimit to avoid fetching long strings
|
|
133
|
+
return input ? JSON.stringify(input).substring(0, Setting.ExtractLimit) : input;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function match(base: Object, condition: string): boolean {
|
|
137
|
+
if (condition) {
|
|
138
|
+
let prop = condition.split(":");
|
|
139
|
+
return prop.length > 1 ? base[prop[0]] == prop[1] : base[prop[0]]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return true;
|
|
143
|
+
}
|
package/src/data/index.ts
CHANGED
|
@@ -11,12 +11,13 @@ import * as summary from "@src/data/summary";
|
|
|
11
11
|
import * as upgrade from "@src/data/upgrade";
|
|
12
12
|
import * as upload from "@src/data/upload";
|
|
13
13
|
import * as variable from "@src/data/variable";
|
|
14
|
+
import * as extract from "@src/data/extract";
|
|
14
15
|
export { event } from "@src/data/custom";
|
|
15
16
|
export { consent, metadata } from "@src/data/metadata";
|
|
16
17
|
export { upgrade } from "@src/data/upgrade";
|
|
17
18
|
export { set, identify } from "@src/data/variable";
|
|
18
19
|
|
|
19
|
-
const modules: Module[] = [baseline, dimension, variable, limit, summary, metadata, envelope, upload, ping, upgrade];
|
|
20
|
+
const modules: Module[] = [baseline, dimension, variable, limit, summary, metadata, envelope, upload, ping, upgrade, extract];
|
|
20
21
|
|
|
21
22
|
export function start(): void {
|
|
22
23
|
// Metric needs to be initialized before we can start measuring. so metric is not wrapped in measure
|
|
@@ -40,4 +41,5 @@ export function compute(): void {
|
|
|
40
41
|
metric.compute();
|
|
41
42
|
summary.compute();
|
|
42
43
|
limit.compute();
|
|
44
|
+
extract.compute();
|
|
43
45
|
}
|
package/src/layout/dom.ts
CHANGED
|
@@ -4,10 +4,10 @@ import { Constant, NodeInfo, NodeValue, Selector, SelectorInput, Source } from "
|
|
|
4
4
|
import config from "@src/core/config";
|
|
5
5
|
import hash from "@src/core/hash";
|
|
6
6
|
import * as internal from "@src/diagnostic/internal";
|
|
7
|
-
import * as extract from "@src/layout/extract";
|
|
8
7
|
import * as region from "@src/layout/region";
|
|
9
8
|
import selector from "@src/layout/selector";
|
|
10
|
-
|
|
9
|
+
import * as mutation from "@src/layout/mutation";
|
|
10
|
+
import * as extract from "@src/data/extract";
|
|
11
11
|
let index: number = 1;
|
|
12
12
|
|
|
13
13
|
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input#%3Cinput%3E_types
|
|
@@ -21,6 +21,7 @@ let updateMap: number[] = [];
|
|
|
21
21
|
let hashMap: { [hash: string]: number } = {};
|
|
22
22
|
let override = [];
|
|
23
23
|
let unmask = [];
|
|
24
|
+
let updatedFragments: { [fragment: number]: string } = {};
|
|
24
25
|
|
|
25
26
|
// The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
|
|
26
27
|
let idMap: WeakMap<Node, number> = null; // Maps node => id.
|
|
@@ -61,9 +62,7 @@ export function parse(root: ParentNode, init: boolean = false): void {
|
|
|
61
62
|
// Since mutations may happen on leaf nodes too, e.g. text nodes, which may not support all selector APIs.
|
|
62
63
|
// We ensure that the root note supports querySelectorAll API before executing the code below to identify new regions.
|
|
63
64
|
if ("querySelectorAll" in root) {
|
|
64
|
-
|
|
65
|
-
extract.metrics(root, config.metrics);
|
|
66
|
-
extract.dimensions(root, config.dimensions);
|
|
65
|
+
config.regions.forEach(x => root.querySelectorAll(x[1]).forEach(e => region.observe(e, `${x[0]}`))); // Regions
|
|
67
66
|
config.mask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.TextImage))); // Masked Elements
|
|
68
67
|
unmask.forEach(x => root.querySelectorAll(x).forEach(e => privacyMap.set(e, Privacy.None))); // Unmasked Elements
|
|
69
68
|
}
|
|
@@ -88,11 +87,13 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
88
87
|
let privacy = config.content ? Privacy.Sensitive : Privacy.Text;
|
|
89
88
|
let parentValue = null;
|
|
90
89
|
let regionId = region.exists(node) ? id : null;
|
|
90
|
+
let fragmentId = null;
|
|
91
91
|
|
|
92
92
|
if (parentId >= 0 && values[parentId]) {
|
|
93
93
|
parentValue = values[parentId];
|
|
94
94
|
parentValue.children.push(id);
|
|
95
95
|
regionId = regionId === null ? parentValue.region : regionId;
|
|
96
|
+
fragmentId = parentValue.fragment;
|
|
96
97
|
privacy = parentValue.metadata.privacy;
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -115,12 +116,13 @@ export function add(node: Node, parent: Node, data: NodeInfo, source: Source): v
|
|
|
115
116
|
selector: null,
|
|
116
117
|
hash: null,
|
|
117
118
|
region: regionId,
|
|
118
|
-
metadata: { active: true, suspend: false, privacy, position: null, size: null }
|
|
119
|
+
metadata: { active: true, suspend: false, privacy, position: null, size: null },
|
|
120
|
+
fragment: fragmentId,
|
|
119
121
|
};
|
|
120
122
|
|
|
121
123
|
updateSelector(values[id]);
|
|
122
124
|
size(values[id], parentValue);
|
|
123
|
-
track(id, source);
|
|
125
|
+
track(id, source, values[id].fragment);
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
export function update(node: Node, parent: Node, data: NodeInfo, source: Source): void {
|
|
@@ -174,9 +176,14 @@ export function update(node: Node, parent: Node, data: NodeInfo, source: Source)
|
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
// track node if it is a part of scheduled fragment mutation
|
|
180
|
+
if(value.fragment && updatedFragments[value.fragment]) {
|
|
181
|
+
changed = true;
|
|
182
|
+
}
|
|
183
|
+
|
|
177
184
|
// Update selector
|
|
178
185
|
updateSelector(value);
|
|
179
|
-
track(id, source, changed, parentChanged);
|
|
186
|
+
track(id, source, values[id].fragment, changed, parentChanged);
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
|
|
@@ -296,6 +303,9 @@ function updateSelector(value: NodeValue): void {
|
|
|
296
303
|
value.selector = [selector(s), selector(s, true)];
|
|
297
304
|
value.hash = value.selector.map(x => x ? hash(x) : null) as [string, string];
|
|
298
305
|
value.hash.forEach(h => hashMap[h] = value.id);
|
|
306
|
+
if (value.hash.some(h => extract.fragments.indexOf(h) !== -1)) {
|
|
307
|
+
value.fragment = value.id;
|
|
308
|
+
}
|
|
299
309
|
}
|
|
300
310
|
|
|
301
311
|
export function getNode(id: number): Node {
|
|
@@ -331,6 +341,11 @@ export function updates(): NodeValue[] {
|
|
|
331
341
|
if (id in values) { output.push(values[id]); }
|
|
332
342
|
}
|
|
333
343
|
updateMap = [];
|
|
344
|
+
for (let id in updatedFragments) {
|
|
345
|
+
extract.update(updatedFragments[id], id, true)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
updatedFragments = {}
|
|
334
349
|
return output;
|
|
335
350
|
}
|
|
336
351
|
|
|
@@ -368,7 +383,19 @@ function getPreviousId(node: Node): number {
|
|
|
368
383
|
return id;
|
|
369
384
|
}
|
|
370
385
|
|
|
371
|
-
function track(id: number, source: Source, changed: boolean = true, parentChanged: boolean = false): void {
|
|
386
|
+
function track(id: number, source: Source, fragment: number = null, changed: boolean = true, parentChanged: boolean = false): void {
|
|
387
|
+
// if updated node is a part of fragment and the fragment is not being tracked currently, schedule a mutation on the fragment node
|
|
388
|
+
if (fragment && !updatedFragments[fragment]) {
|
|
389
|
+
let node = getNode(fragment)
|
|
390
|
+
let value = getValue(fragment);
|
|
391
|
+
if (node && value) {
|
|
392
|
+
mutation.schedule(node, true);
|
|
393
|
+
value.hash.forEach(h => {
|
|
394
|
+
if(extract.fragments.indexOf(h) !== -1) { updatedFragments[fragment] = h;}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
372
399
|
// Keep track of the order in which mutations happened, they may not be sequential
|
|
373
400
|
// Edge case: If an element is added later on, and pre-discovered element is moved as a child.
|
|
374
401
|
// In that case, we need to reorder the pre-discovered element in the update list to keep visualization consistent.
|
package/src/layout/mutation.ts
CHANGED
|
@@ -218,7 +218,7 @@ async function processNodeList(list: NodeList, source: Source, timer: Timer): Pr
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
function schedule(node: Node): Node {
|
|
221
|
+
export function schedule(node: Node, fragment: boolean = false): Node {
|
|
222
222
|
// Only schedule manual trigger for this node if it's not already in the queue
|
|
223
223
|
if (queue.indexOf(node) < 0) { queue.push(node); }
|
|
224
224
|
|
|
@@ -226,19 +226,19 @@ function schedule(node: Node): Node {
|
|
|
226
226
|
// It's common for a webpage to call multiple synchronous "insertRule" / "deleteRule" calls.
|
|
227
227
|
// And in those cases we do not wish to monitor changes multiple times for the same node.
|
|
228
228
|
if (timeout) { clearTimeout(timeout); }
|
|
229
|
-
timeout = setTimeout(trigger, Setting.LookAhead);
|
|
229
|
+
timeout = setTimeout(() => { trigger(fragment) }, Setting.LookAhead);
|
|
230
230
|
|
|
231
231
|
return node;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
function trigger(): void {
|
|
234
|
+
function trigger(fragment: boolean): void {
|
|
235
235
|
for (let node of queue) {
|
|
236
236
|
// Generate a mutation for this node only if it still exists
|
|
237
237
|
if (node) {
|
|
238
238
|
let shadowRoot = node.nodeType === Node.DOCUMENT_FRAGMENT_NODE;
|
|
239
239
|
// Skip re-processing shadowRoot if it was already discovered
|
|
240
240
|
if (shadowRoot && dom.has(node)) { continue; }
|
|
241
|
-
generate(node, shadowRoot ? Constant.ChildList : Constant.CharacterData);
|
|
241
|
+
generate(node, shadowRoot || fragment ? Constant.ChildList : Constant.CharacterData);
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
244
|
queue = [];
|
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,8 +125,7 @@ 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;
|
package/types/data.d.ts
CHANGED
|
@@ -54,7 +54,8 @@ export const enum Event {
|
|
|
54
54
|
Summary = 36,
|
|
55
55
|
Box = 37,
|
|
56
56
|
Clipboard = 38,
|
|
57
|
-
Submit = 39
|
|
57
|
+
Submit = 39,
|
|
58
|
+
Extract = 40
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
export const enum Metric {
|
|
@@ -138,7 +139,8 @@ export const enum Code {
|
|
|
138
139
|
/**
|
|
139
140
|
* @deprecated No longer support ContentSecurityPolicy
|
|
140
141
|
*/
|
|
141
|
-
ContentSecurityPolicy = 7
|
|
142
|
+
ContentSecurityPolicy = 7,
|
|
143
|
+
Config = 8
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
export const enum Severity {
|
|
@@ -187,7 +189,8 @@ export const enum Setting {
|
|
|
187
189
|
MaxFirstPayloadBytes = 1 * 1024 * 1024, // 1MB: Cap the very first payload to a maximum of 1MB
|
|
188
190
|
UploadFactor = 3, // Slow down sequence by specified factor
|
|
189
191
|
MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
|
|
190
|
-
MaxUploadDelay = 30 * Time.Second // Do flush out payload once every 30s
|
|
192
|
+
MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
|
|
193
|
+
ExtractLimit = 10000 // Do not extract more than 10000 characters
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
export const enum Character {
|
|
@@ -248,6 +251,9 @@ export const enum Constant {
|
|
|
248
251
|
Accept = "Accept",
|
|
249
252
|
ClarityGzip = "application/x-clarity-gzip",
|
|
250
253
|
Tilde = "~",
|
|
254
|
+
ArrayStart = "[",
|
|
255
|
+
ConditionStart = "{",
|
|
256
|
+
ConditionEnd = "}"
|
|
251
257
|
}
|
|
252
258
|
|
|
253
259
|
export const enum XMLReadyState {
|
|
@@ -366,6 +372,10 @@ export interface UpgradeData {
|
|
|
366
372
|
key: string;
|
|
367
373
|
}
|
|
368
374
|
|
|
375
|
+
export interface ExtractData {
|
|
376
|
+
[key: string]: string | number;
|
|
377
|
+
}
|
|
378
|
+
|
|
369
379
|
export interface UploadData {
|
|
370
380
|
sequence: number;
|
|
371
381
|
attempts: number;
|
package/types/layout.d.ts
CHANGED
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
|
-
}
|