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/package.json
CHANGED
package/src/core/api.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Constant } from "@clarity-types/core";
|
|
2
|
+
|
|
3
|
+
export default function api(method: string): string {
|
|
4
|
+
// Zone.js, a popular package for Angular, overrides native browser APIs which can lead to inconsistent state for single page applications.
|
|
5
|
+
// Example issue: https://github.com/angular/angular/issues/31712
|
|
6
|
+
// As a work around, we ensuring Clarity access APIs outside of Zone (and use native implementation instead)
|
|
7
|
+
return window[Constant.Zone] && Constant.Symbol in window[Constant.Zone] ? window[Constant.Zone][Constant.Symbol](method) : method;
|
|
8
|
+
}
|
package/src/core/config.ts
CHANGED
package/src/core/event.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { BrowserEvent } from "@clarity-types/core";
|
|
1
|
+
import { BrowserEvent, Constant } from "@clarity-types/core";
|
|
2
|
+
import api from "./api";
|
|
2
3
|
import measure from "./measure";
|
|
3
4
|
|
|
4
5
|
let bindings: BrowserEvent[] = [];
|
|
@@ -8,7 +9,7 @@ export function bind(target: EventTarget, event: string, listener: EventListener
|
|
|
8
9
|
// Wrapping following lines inside try / catch to cover edge cases where we might try to access an inaccessible element.
|
|
9
10
|
// E.g. Iframe may start off as same-origin but later turn into cross-origin, and the following lines will throw an exception.
|
|
10
11
|
try {
|
|
11
|
-
target.
|
|
12
|
+
target[api(Constant.AddEventListener)](event, listener, capture);
|
|
12
13
|
bindings.push({ event, target, listener, capture });
|
|
13
14
|
} catch { /* do nothing */ }
|
|
14
15
|
}
|
|
@@ -18,7 +19,7 @@ export function reset(): void {
|
|
|
18
19
|
for (let binding of bindings) {
|
|
19
20
|
// Wrapping inside try / catch to avoid situations where the element may be destroyed before we get a chance to unbind
|
|
20
21
|
try {
|
|
21
|
-
binding.target.
|
|
22
|
+
binding.target[api(Constant.RemoveEventListener)](binding.event, binding.listener, binding.capture);
|
|
22
23
|
} catch { /* do nothing */ }
|
|
23
24
|
}
|
|
24
25
|
bindings = [];
|
package/src/core/history.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Code, Constant, Setting, Severity } from "@clarity-types/data";
|
|
|
2
2
|
import * as clarity from "@src/clarity";
|
|
3
3
|
import { bind } from "@src/core/event";
|
|
4
4
|
import * as internal from "@src/diagnostic/internal";
|
|
5
|
-
|
|
5
|
+
import * as core from "@src/core"
|
|
6
6
|
let pushState = null;
|
|
7
7
|
let replaceState = null;
|
|
8
8
|
let url = null;
|
|
@@ -12,24 +12,29 @@ export function start(): void {
|
|
|
12
12
|
url = getCurrentUrl();
|
|
13
13
|
count = 0;
|
|
14
14
|
bind(window, "popstate", compute);
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
// Add a proxy to history.pushState function
|
|
17
|
-
if (pushState === null) {
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
if (pushState === null) {
|
|
18
|
+
pushState = history.pushState;
|
|
19
|
+
history.pushState = function(): void {
|
|
20
20
|
pushState.apply(this, arguments);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
if (core.active() && check()) {
|
|
22
|
+
compute();
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
24
26
|
|
|
25
27
|
// Add a proxy to history.replaceState function
|
|
26
|
-
if (replaceState === null)
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
if (replaceState === null)
|
|
29
|
+
{
|
|
30
|
+
replaceState = history.replaceState;
|
|
31
|
+
history.replaceState = function(): void {
|
|
29
32
|
replaceState.apply(this, arguments);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
if (core.active() && check()) {
|
|
34
|
+
compute();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function check(): boolean {
|
|
@@ -54,18 +59,6 @@ function getCurrentUrl(): string {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
export function stop(): void {
|
|
57
|
-
// Restore original function definition of history.pushState
|
|
58
|
-
if (pushState !== null) {
|
|
59
|
-
history.pushState = pushState;
|
|
60
|
-
pushState = null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Restore original function definition of history.replaceState
|
|
64
|
-
if (replaceState !== null) {
|
|
65
|
-
history.replaceState = replaceState;
|
|
66
|
-
replaceState = null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
62
|
url = null;
|
|
70
63
|
count = 0;
|
|
71
64
|
}
|
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let version = "0.6.
|
|
1
|
+
let version = "0.6.34";
|
|
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/data/metadata.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Time } from "@clarity-types/core";
|
|
2
|
-
import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, Metric, Session, User, Setting } from "@clarity-types/data";
|
|
2
|
+
import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, User, Setting } from "@clarity-types/data";
|
|
3
3
|
import * as core from "@src/core";
|
|
4
4
|
import config from "@src/core/config";
|
|
5
5
|
import hash from "@src/core/hash";
|
|
@@ -8,11 +8,10 @@ import * as metric from "@src/data/metric";
|
|
|
8
8
|
import { set } from "@src/data/variable";
|
|
9
9
|
|
|
10
10
|
export let data: Metadata = null;
|
|
11
|
-
export let
|
|
11
|
+
export let callbacks: MetadataCallbackOptions[] = [];
|
|
12
12
|
let rootDomain = null;
|
|
13
13
|
|
|
14
14
|
export function start(): void {
|
|
15
|
-
callback = null;
|
|
16
15
|
rootDomain = null;
|
|
17
16
|
const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty;
|
|
18
17
|
const title = document && document.title ? document.title : Constant.Empty;
|
|
@@ -84,7 +83,6 @@ export function userAgentData(): void {
|
|
|
84
83
|
}
|
|
85
84
|
|
|
86
85
|
export function stop(): void {
|
|
87
|
-
callback = null;
|
|
88
86
|
rootDomain = null;
|
|
89
87
|
data = null;
|
|
90
88
|
}
|
|
@@ -93,10 +91,9 @@ export function metadata(cb: MetadataCallback, wait: boolean = true): void {
|
|
|
93
91
|
if (data && wait === false) {
|
|
94
92
|
// Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
|
|
95
93
|
cb(data, !config.lean);
|
|
96
|
-
} else {
|
|
97
|
-
// Save the callback for future reference; so we can inform the caller when page gets upgraded and we have a valid playback flag
|
|
98
|
-
callback = cb;
|
|
99
94
|
}
|
|
95
|
+
|
|
96
|
+
callbacks.push({callback: cb, wait: wait });
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
export function id(): string {
|
|
@@ -127,12 +124,20 @@ function tab(): string {
|
|
|
127
124
|
|
|
128
125
|
export function save(): void {
|
|
129
126
|
let ts = Math.round(Date.now());
|
|
130
|
-
let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
|
|
131
127
|
let upload = config.upload && typeof config.upload === Constant.String ? (config.upload as string).replace(Constant.HTTPS, Constant.Empty) : Constant.Empty;
|
|
132
|
-
|
|
128
|
+
let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
|
|
129
|
+
processCallback(upgrade);
|
|
133
130
|
setCookie(Constant.SessionKey, [data.sessionId, ts, data.pageNum, upgrade, upload].join(Constant.Pipe), Setting.SessionExpire);
|
|
134
131
|
}
|
|
135
132
|
|
|
133
|
+
function processCallback(upgrade: BooleanFlag) {
|
|
134
|
+
if (callbacks.length > 0) {
|
|
135
|
+
callbacks.forEach(x => {
|
|
136
|
+
if (x.callback && (!x.wait || upgrade)) { x.callback(data, !config.lean); }
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
136
141
|
function supported(target: Window | Document, api: string): boolean {
|
|
137
142
|
try { return !!target[api]; } catch { return false; }
|
|
138
143
|
}
|
package/src/data/upload.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function queue(tokens: Token[], transmit: boolean = true): void {
|
|
|
42
42
|
let now = time();
|
|
43
43
|
let type = tokens.length > 1 ? tokens[1] : null;
|
|
44
44
|
let event = JSON.stringify(tokens);
|
|
45
|
-
|
|
45
|
+
|
|
46
46
|
switch (type) {
|
|
47
47
|
case Event.Discover:
|
|
48
48
|
discoverBytes += event.length;
|
|
@@ -118,12 +118,12 @@ async function upload(final: boolean = false): Promise<void> {
|
|
|
118
118
|
|
|
119
119
|
let p = sendPlaybackBytes ? `[${playback.join()}]` : Constant.Empty;
|
|
120
120
|
let encoded: EncodedPayload = {e, a, p};
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
// Get the payload ready for sending over the wire
|
|
123
123
|
// We also attempt to compress the payload if it is not the last payload and the browser supports it
|
|
124
124
|
// In all other cases, we continue to send back string value
|
|
125
125
|
let payload = stringify(encoded);
|
|
126
|
-
let zipped = last ? null : await compress(payload)
|
|
126
|
+
let zipped = last ? null : await compress(payload)
|
|
127
127
|
metric.sum(Metric.TotalBytes, zipped ? zipped.length : payload.length);
|
|
128
128
|
send(payload, zipped, envelope.data.sequence, last);
|
|
129
129
|
|
|
@@ -151,8 +151,11 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
|
|
|
151
151
|
// However, we don't want to rely on it for every payload, since we have no ability to retry if the upload failed.
|
|
152
152
|
// Also, in case of sendBeacon, we do not have a way to alter HTTP headers and therefore can't send compressed payload
|
|
153
153
|
if (beacon && "sendBeacon" in navigator) {
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
try {
|
|
155
|
+
// Navigator needs to be bound to sendBeacon before it is used to avoid errors in some browsers
|
|
156
|
+
dispatched = navigator.sendBeacon.bind(navigator)(url, payload);
|
|
157
|
+
if (dispatched) { done(sequence); }
|
|
158
|
+
} catch { /* do nothing - and we will automatically fallback to XHR below */ }
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
// Before initiating XHR upload, we check if the data has already been uploaded using sendBeacon
|
|
@@ -189,7 +192,7 @@ function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
|
189
192
|
if (xhr && xhr.readyState === XMLReadyState.Done && transitData) {
|
|
190
193
|
// Attempt send payload again (as configured in settings) if we do not receive a success (2XX) response code back from the server
|
|
191
194
|
if ((xhr.status < 200 || xhr.status > 208) && transitData.attempts <= Setting.RetryLimit) {
|
|
192
|
-
// We re-attempt in all cases except when server explicitly rejects our request with 4XX error
|
|
195
|
+
// We re-attempt in all cases except when server explicitly rejects our request with 4XX error
|
|
193
196
|
if (xhr.status >= 400 && xhr.status < 500) {
|
|
194
197
|
// In case of a 4XX response from the server, we bail out instead of trying again
|
|
195
198
|
limit.trigger(Check.Server);
|
|
@@ -212,7 +215,7 @@ function check(xhr: XMLHttpRequest, sequence: number): void {
|
|
|
212
215
|
// Handle response if it was a 200 response with a valid body
|
|
213
216
|
if (xhr.status === 200 && xhr.responseText) { response(xhr.responseText); }
|
|
214
217
|
// If we exhausted our retries then trigger Clarity's shutdown for this page since the data will be incomplete
|
|
215
|
-
if (xhr.status === 0) {
|
|
218
|
+
if (xhr.status === 0) {
|
|
216
219
|
// And, right before we terminate the session, we will attempt one last time to see if we can use
|
|
217
220
|
// different transport option (sendBeacon vs. XHR) to get this data to the server for analysis purposes
|
|
218
221
|
send(transitData.data, null, sequence, true);
|
|
@@ -233,7 +236,7 @@ function done(sequence: number): void {
|
|
|
233
236
|
|
|
234
237
|
function delay(): number {
|
|
235
238
|
// Progressively increase delay as we continue to send more payloads from the client to the server
|
|
236
|
-
// If we are not uploading data to a server, and instead invoking UploadCallback, in that case keep returning configured value
|
|
239
|
+
// If we are not uploading data to a server, and instead invoking UploadCallback, in that case keep returning configured value
|
|
237
240
|
let gap = config.lean === false && discoverBytes > 0 ? Setting.MinUploadDelay : envelope.data.sequence * config.delay;
|
|
238
241
|
return typeof config.upload === Constant.String ? Math.max(Math.min(gap, Setting.MaxUploadDelay), Setting.MinUploadDelay) : config.delay;
|
|
239
242
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { Code,
|
|
1
|
+
import { Code, Event, Severity } from "@clarity-types/data";
|
|
2
2
|
import { LogData } from "@clarity-types/diagnostic";
|
|
3
|
-
import config from "@src/core/config";
|
|
4
|
-
import { bind } from "@src/core/event";
|
|
5
3
|
import encode from "./encode";
|
|
6
4
|
|
|
7
5
|
let history: { [key: number]: string[] } = {};
|
|
@@ -9,7 +7,6 @@ export let data: LogData;
|
|
|
9
7
|
|
|
10
8
|
export function start(): void {
|
|
11
9
|
history = {};
|
|
12
|
-
bind(document, "securitypolicyviolation", csp);
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
export function log(code: Code, severity: Severity, name: string = null, message: string = null, stack: string = null): void {
|
|
@@ -26,17 +23,6 @@ export function log(code: Code, severity: Severity, name: string = null, message
|
|
|
26
23
|
encode(Event.Log);
|
|
27
24
|
}
|
|
28
25
|
|
|
29
|
-
function csp(e: SecurityPolicyViolationEvent): void {
|
|
30
|
-
let upload = config.upload as string;
|
|
31
|
-
// Look for first "/" starting after initial "https://" string
|
|
32
|
-
let parts = upload && typeof upload === Constant.String ? upload.substr(0, upload.indexOf("/", Constant.HTTPS.length)).split(Constant.Dot) : [];
|
|
33
|
-
let domain = parts.length >= 2 ? parts.splice(-2).join(Constant.Dot) : null;
|
|
34
|
-
// Capture content security policy violation only if disposition value is not explicitly set to "report"
|
|
35
|
-
if (domain && e.blockedURI && e.blockedURI.indexOf(domain) >= 0 && e["disposition"] !== Constant.Report) {
|
|
36
|
-
log(Code.ContentSecurityPolicy, Severity.Warning, e.blockedURI);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
26
|
export function stop(): void {
|
|
41
27
|
history = {};
|
|
42
28
|
}
|
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.
|