clarity-js 0.7.6 → 0.7.8
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.insight.js +1 -1
- package/build/clarity.js +222 -99
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +222 -99
- package/build/clarity.performance.js +1 -0
- package/package.json +2 -1
- package/rollup.config.ts +22 -0
- package/src/core/config.ts +2 -1
- package/src/core/scrub.ts +8 -5
- package/src/core/version.ts +1 -1
- package/src/data/encode.ts +10 -3
- package/src/data/extract.ts +69 -27
- package/src/data/limit.ts +1 -0
- package/src/data/metadata.ts +31 -16
- package/src/data/variable.ts +43 -5
- package/src/interaction/click.ts +3 -3
- package/src/performance/blank.ts +7 -0
- package/src/queue.ts +2 -0
- package/types/core.d.ts +4 -2
- package/types/data.d.ts +30 -8
- package/types/index.d.ts +1 -1
package/src/core/scrub.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function text(value: string, hint: string, privacy: Privacy, mangle: bool
|
|
|
64
64
|
switch (hint) {
|
|
65
65
|
case Layout.Constant.TextTag:
|
|
66
66
|
case Layout.Constant.DataAttribute:
|
|
67
|
-
return scrub(value);
|
|
67
|
+
return scrub(value, Data.Constant.Letter, Data.Constant.Digit);
|
|
68
68
|
case "value":
|
|
69
69
|
case "input":
|
|
70
70
|
case "click":
|
|
@@ -83,7 +83,10 @@ export function text(value: string, hint: string, privacy: Privacy, mangle: bool
|
|
|
83
83
|
return value;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export function url(input: string): string {
|
|
86
|
+
export function url(input: string, electron: boolean = false): string {
|
|
87
|
+
// Replace the URL for Electron apps so we don't send back file:/// URL
|
|
88
|
+
if (electron) { return `${Data.Constant.HTTPS}${Data.Constant.Electron}`; }
|
|
89
|
+
|
|
87
90
|
let drop = config.drop;
|
|
88
91
|
if (drop && drop.length > 0 && input && input.indexOf("?") > 0) {
|
|
89
92
|
let [path, query] = input.split("?");
|
|
@@ -109,9 +112,9 @@ function mask(value: string): string {
|
|
|
109
112
|
return value.replace(catchallRegex, Data.Constant.Mask);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
|
-
function scrub(value: string): string {
|
|
115
|
+
export function scrub(value: string, letter: string, digit: string): string {
|
|
113
116
|
regex(); // Initialize regular expressions
|
|
114
|
-
return value.replace(letterRegex,
|
|
117
|
+
return value ? value.replace(letterRegex, letter).replace(digitRegex, digit) : value;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
function mangleToken(value: string): string {
|
|
@@ -161,7 +164,7 @@ function redact(value: string): string {
|
|
|
161
164
|
// Check if unicode regex is supported, otherwise fallback to calling mask function on this token
|
|
162
165
|
if (unicodeRegex && currencyRegex !== null) {
|
|
163
166
|
// Do not redact information if the token contains a currency symbol
|
|
164
|
-
token = token.match(currencyRegex) ? token : token
|
|
167
|
+
token = token.match(currencyRegex) ? token : scrub(token, Data.Constant.Letter, Data.Constant.Digit);
|
|
165
168
|
} else {
|
|
166
169
|
token = mask(token);
|
|
167
170
|
}
|
package/src/core/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
let version = "0.7.
|
|
1
|
+
let version = "0.7.8";
|
|
2
2
|
export default version;
|
package/src/data/encode.ts
CHANGED
|
@@ -108,10 +108,17 @@ export default function(event: Event): void {
|
|
|
108
108
|
break;
|
|
109
109
|
case Event.Extract:
|
|
110
110
|
let extractKeys = extract.keys;
|
|
111
|
-
|
|
111
|
+
extractKeys.forEach((e => {
|
|
112
112
|
tokens.push(e);
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
let token = []
|
|
114
|
+
for (let d in extract.data[e]) {
|
|
115
|
+
let key = parseInt(d, 10);
|
|
116
|
+
token.push(key);
|
|
117
|
+
token.push(extract.data[e][d]);
|
|
118
|
+
}
|
|
119
|
+
tokens.push(token);
|
|
120
|
+
}));
|
|
121
|
+
|
|
115
122
|
extract.reset();
|
|
116
123
|
queue(tokens, false);
|
|
117
124
|
}
|
package/src/data/extract.ts
CHANGED
|
@@ -3,16 +3,23 @@ import { Event, Setting, ExtractData } from "@clarity-types/data";
|
|
|
3
3
|
import encode from "./encode";
|
|
4
4
|
import * as internal from "@src/diagnostic/internal";
|
|
5
5
|
import { Code, Constant, Severity } from "@clarity-types/data";
|
|
6
|
+
import { hashText } from "@src/clarity";
|
|
6
7
|
|
|
7
8
|
export let data: ExtractData = {};
|
|
8
|
-
export let keys: number
|
|
9
|
+
export let keys: Set<number> = new Set();
|
|
9
10
|
|
|
10
11
|
let variables : { [key: number]: { [key: number]: Syntax[] }} = {};
|
|
11
12
|
let selectors : { [key: number]: { [key: number]: string }} = {};
|
|
13
|
+
let hashes : { [key: number]: { [key: number]: string }} = {};
|
|
12
14
|
export function start(): void {
|
|
13
15
|
reset();
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
// Input string is of the following form:
|
|
19
|
+
// EXTRACT 101 { "1": ".class1", "2": "~window.a.b", "3": "!abc"}
|
|
20
|
+
// Which will set up event 101 to grab the contents of the class1 selector into component 1,
|
|
21
|
+
// the javascript evaluated contents of window.a.b into component 2,
|
|
22
|
+
// and the contents of Clarity's hash abc into component 3
|
|
16
23
|
export function trigger(input: string): void {
|
|
17
24
|
try {
|
|
18
25
|
var parts = input && input.length > 0 ? input.split(/ (.*)/) : [Constant.Empty];
|
|
@@ -20,10 +27,18 @@ export function trigger(input: string): void {
|
|
|
20
27
|
var values = parts.length > 1 ? JSON.parse(parts[1]) : {};
|
|
21
28
|
variables[key] = {};
|
|
22
29
|
selectors[key] = {};
|
|
30
|
+
hashes[key] = {};
|
|
23
31
|
for (var v in values) {
|
|
32
|
+
// values is a set of strings for proper JSON parsing, but it's more efficient
|
|
33
|
+
// to interact with them as numbers
|
|
24
34
|
let id = parseInt(v);
|
|
25
35
|
let value = values[v] as string;
|
|
26
|
-
let source =
|
|
36
|
+
let source = ExtractSource.Text;
|
|
37
|
+
if (value.startsWith(Constant.Tilde)) {
|
|
38
|
+
source = ExtractSource.Javascript
|
|
39
|
+
} else if (value.startsWith(Constant.Bang)) {
|
|
40
|
+
source = ExtractSource.Hash
|
|
41
|
+
}
|
|
27
42
|
switch (source) {
|
|
28
43
|
case ExtractSource.Javascript:
|
|
29
44
|
let variable = value.substring(1, value.length);
|
|
@@ -32,6 +47,10 @@ export function trigger(input: string): void {
|
|
|
32
47
|
case ExtractSource.Text:
|
|
33
48
|
selectors[key][id] = value;
|
|
34
49
|
break;
|
|
50
|
+
case ExtractSource.Hash:
|
|
51
|
+
let hash = value.substring(1, value.length);
|
|
52
|
+
hashes[key][id] = hash;
|
|
53
|
+
break;
|
|
35
54
|
}
|
|
36
55
|
}
|
|
37
56
|
}
|
|
@@ -48,44 +67,63 @@ export function compute(): void {
|
|
|
48
67
|
try {
|
|
49
68
|
for (let v in variables) {
|
|
50
69
|
let key = parseInt(v);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
70
|
+
let variableData = variables[key];
|
|
71
|
+
for (let v in variableData) {
|
|
72
|
+
let variableKey = parseInt(v);
|
|
73
|
+
let value = str(evaluate(clone(variableData[variableKey])));
|
|
74
|
+
if (value) {
|
|
75
|
+
update(key, variableKey, value);
|
|
57
76
|
}
|
|
77
|
+
}
|
|
58
78
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
79
|
+
let selectorData = selectors[key];
|
|
80
|
+
for (let s in selectorData) {
|
|
81
|
+
let selectorKey = parseInt(s);
|
|
82
|
+
let nodes = document.querySelectorAll(selectorData[selectorKey]) as NodeListOf<HTMLElement>;
|
|
83
|
+
if (nodes) {
|
|
84
|
+
let text = Array.from(nodes).map(e => e.textContent)
|
|
85
|
+
update(key, selectorKey, text.join(Constant.Seperator).substring(0, Setting.ExtractLimit));
|
|
67
86
|
}
|
|
68
87
|
}
|
|
88
|
+
|
|
89
|
+
let hashData = hashes[key];
|
|
90
|
+
for (let h in hashData) {
|
|
91
|
+
let hashKey = parseInt(h);
|
|
92
|
+
let content = hashText(hashData[hashKey]).trim().substring(0, Setting.ExtractLimit);
|
|
93
|
+
update(key, hashKey, content);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (keys.size > 0) {
|
|
98
|
+
encode(Event.Extract);
|
|
69
99
|
}
|
|
70
100
|
}
|
|
71
101
|
catch (e) { internal.log(Code.Selector, Severity.Warning, e ? e.name : null); }
|
|
72
|
-
|
|
73
|
-
encode(Event.Extract);
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
export function reset(): void {
|
|
77
|
-
|
|
78
|
-
keys = [];
|
|
79
|
-
variables = {};
|
|
80
|
-
selectors = {};
|
|
105
|
+
keys.clear();
|
|
81
106
|
}
|
|
82
107
|
|
|
83
|
-
export function update(key: number, subkey: number,
|
|
108
|
+
export function update(key: number, subkey: number, value: string): void {
|
|
109
|
+
var update = false;
|
|
84
110
|
if (!(key in data)) {
|
|
85
|
-
data[key] =
|
|
86
|
-
|
|
111
|
+
data[key] = {};
|
|
112
|
+
update = true;
|
|
87
113
|
}
|
|
88
|
-
|
|
114
|
+
|
|
115
|
+
if (!isEmpty(hashes[key])
|
|
116
|
+
&& (!(subkey in data[key]) || data[key][subkey] != value))
|
|
117
|
+
{
|
|
118
|
+
update = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
data[key][subkey] = value;
|
|
122
|
+
if (update) {
|
|
123
|
+
keys.add(key);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return;
|
|
89
127
|
}
|
|
90
128
|
|
|
91
129
|
export function stop(): void {
|
|
@@ -151,4 +189,8 @@ function match(base: Object, condition: string): boolean {
|
|
|
151
189
|
}
|
|
152
190
|
|
|
153
191
|
return true;
|
|
154
|
-
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isEmpty(obj: Object): boolean {
|
|
195
|
+
return Object.keys(obj).length == 0;
|
|
196
|
+
}
|
package/src/data/limit.ts
CHANGED
|
@@ -15,6 +15,7 @@ export function check(bytes: number): void {
|
|
|
15
15
|
if (data.check === Check.None) {
|
|
16
16
|
let reason = data.check;
|
|
17
17
|
reason = envelope.data.sequence >= Setting.PayloadLimit ? Check.Payload : reason;
|
|
18
|
+
reason = envelope.data.pageNum >= Setting.PageLimit ? Check.Page : reason;
|
|
18
19
|
reason = time() > Setting.ShutdownLimit ? Check.Shutdown : reason;
|
|
19
20
|
reason = bytes > Setting.PlaybackBytesLimit ? Check.Shutdown : reason;
|
|
20
21
|
if (reason !== data.check) {
|
package/src/data/metadata.ts
CHANGED
|
@@ -16,6 +16,7 @@ export function start(): void {
|
|
|
16
16
|
rootDomain = null;
|
|
17
17
|
const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty;
|
|
18
18
|
const title = document && document.title ? document.title : Constant.Empty;
|
|
19
|
+
const electron = ua.indexOf(Constant.Electron) > 0 ? BooleanFlag.True : BooleanFlag.False;
|
|
19
20
|
|
|
20
21
|
// Populate ids for this page
|
|
21
22
|
let s = session();
|
|
@@ -26,20 +27,23 @@ export function start(): void {
|
|
|
26
27
|
// Override configuration based on what's in the session storage, unless it is blank (e.g. using upload callback, like in devtools)
|
|
27
28
|
config.lean = config.track && s.upgrade !== null ? s.upgrade === BooleanFlag.False : config.lean;
|
|
28
29
|
config.upload = config.track && typeof config.upload === Constant.String && s.upload && s.upload.length > Constant.HTTPS.length ? s.upload : config.upload;
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
// Log page metadata as dimensions
|
|
31
32
|
dimension.log(Dimension.UserAgent, ua);
|
|
32
33
|
dimension.log(Dimension.PageTitle, title);
|
|
33
|
-
dimension.log(Dimension.Url, scrub.url(location.href));
|
|
34
|
+
dimension.log(Dimension.Url, scrub.url(location.href, !!electron));
|
|
34
35
|
dimension.log(Dimension.Referrer, document.referrer);
|
|
35
36
|
dimension.log(Dimension.TabId, tab());
|
|
36
37
|
dimension.log(Dimension.PageLanguage, document.documentElement.lang);
|
|
37
38
|
dimension.log(Dimension.DocumentDirection, document.dir);
|
|
38
39
|
dimension.log(Dimension.DevicePixelRatio, `${window.devicePixelRatio}`);
|
|
39
|
-
|
|
40
|
+
dimension.log(Dimension.Dob, u.dob.toString());
|
|
41
|
+
dimension.log(Dimension.CookieVersion, u.version.toString());
|
|
42
|
+
|
|
40
43
|
// Capture additional metadata as metrics
|
|
41
44
|
metric.max(Metric.ClientTimestamp, s.ts);
|
|
42
|
-
metric.max(Metric.Playback, BooleanFlag.False);
|
|
45
|
+
metric.max(Metric.Playback, BooleanFlag.False);
|
|
46
|
+
metric.max(Metric.Electron, electron);
|
|
43
47
|
|
|
44
48
|
// Capture navigator specific dimensions
|
|
45
49
|
if (navigator) {
|
|
@@ -48,7 +52,7 @@ export function start(): void {
|
|
|
48
52
|
metric.max(Metric.MaxTouchPoints, navigator.maxTouchPoints);
|
|
49
53
|
metric.max(Metric.DeviceMemory, Math.round((<any>navigator).deviceMemory));
|
|
50
54
|
userAgentData();
|
|
51
|
-
}
|
|
55
|
+
}
|
|
52
56
|
|
|
53
57
|
if (screen) {
|
|
54
58
|
metric.max(Metric.ScreenWidth, Math.round(screen.width));
|
|
@@ -69,12 +73,12 @@ export function start(): void {
|
|
|
69
73
|
function userAgentData(): void {
|
|
70
74
|
let uaData = navigator["userAgentData"];
|
|
71
75
|
if (uaData && uaData.getHighEntropyValues) {
|
|
72
|
-
uaData.getHighEntropyValues(["model","platform","platformVersion","uaFullVersion"]).then(ua => {
|
|
73
|
-
dimension.log(Dimension.Platform, ua.platform);
|
|
74
|
-
dimension.log(Dimension.PlatformVersion, ua.platformVersion);
|
|
76
|
+
uaData.getHighEntropyValues(["model","platform","platformVersion","uaFullVersion"]).then(ua => {
|
|
77
|
+
dimension.log(Dimension.Platform, ua.platform);
|
|
78
|
+
dimension.log(Dimension.PlatformVersion, ua.platformVersion);
|
|
75
79
|
ua.brands?.forEach(brand => { dimension.log(Dimension.Brand, brand.name + Constant.Tilde + brand.version); });
|
|
76
|
-
dimension.log(Dimension.Model, ua.model);
|
|
77
|
-
metric.max(Metric.Mobile, ua.mobile ? BooleanFlag.True : BooleanFlag.False);
|
|
80
|
+
dimension.log(Dimension.Model, ua.model);
|
|
81
|
+
metric.max(Metric.Mobile, ua.mobile ? BooleanFlag.True : BooleanFlag.False);
|
|
78
82
|
});
|
|
79
83
|
} else { dimension.log(Dimension.Platform, navigator.platform); }
|
|
80
84
|
}
|
|
@@ -85,11 +89,16 @@ export function stop(): void {
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
export function metadata(cb: MetadataCallback, wait: boolean = true): void {
|
|
88
|
-
|
|
92
|
+
let upgraded = config.lean ? BooleanFlag.False : BooleanFlag.True;
|
|
93
|
+
// if caller hasn't specified that they want to skip waiting for upgrade but we've already upgraded, we need to
|
|
94
|
+
// directly execute the callback rather than adding to our list as we only process callbacks at the moment
|
|
95
|
+
// we go through the upgrading flow.
|
|
96
|
+
if (data && (upgraded || wait === false)) {
|
|
89
97
|
// Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
|
|
90
98
|
cb(data, !config.lean);
|
|
99
|
+
} else {
|
|
100
|
+
callbacks.push({callback: cb, wait: wait });
|
|
91
101
|
}
|
|
92
|
-
callbacks.push({callback: cb, wait: wait });
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
export function id(): string {
|
|
@@ -144,9 +153,13 @@ function track(u: User, consent: BooleanFlag = null): void {
|
|
|
144
153
|
// Convert time precision into days to reduce number of bytes we have to write in a cookie
|
|
145
154
|
// E.g. Math.ceil(1628735962643 / (24*60*60*1000)) => 18852 (days) => ejo in base36 (13 bytes => 3 bytes)
|
|
146
155
|
let end = Math.ceil((Date.now() + (Setting.Expire * Time.Day))/Time.Day);
|
|
156
|
+
// If DOB is not set in the user object, use the date set in the config as a DOB
|
|
157
|
+
let dob = u.dob === 0 ? (config.dob === null ? 0 : config.dob) : u.dob;
|
|
158
|
+
|
|
147
159
|
// To avoid cookie churn, write user id cookie only once every day
|
|
148
|
-
if (u.expiry === null || Math.abs(end - u.expiry) >= Setting.CookieInterval || u.consent !== consent) {
|
|
149
|
-
|
|
160
|
+
if (u.expiry === null || Math.abs(end - u.expiry) >= Setting.CookieInterval || u.consent !== consent || u.dob !== dob) {
|
|
161
|
+
let cookieParts = [data.userId, Setting.CookieVersion, end.toString(36), consent, dob];
|
|
162
|
+
setCookie(Constant.CookieKey, cookieParts.join(Constant.Pipe), Setting.Expire);
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
165
|
|
|
@@ -180,7 +193,7 @@ function num(string: string, base: number = 10): number {
|
|
|
180
193
|
}
|
|
181
194
|
|
|
182
195
|
function user(): User {
|
|
183
|
-
let output: User = { id: shortid(), expiry: null, consent: BooleanFlag.False };
|
|
196
|
+
let output: User = { id: shortid(), version: 0, expiry: null, consent: BooleanFlag.False, dob: 0 };
|
|
184
197
|
let cookie = getCookie(Constant.CookieKey);
|
|
185
198
|
if(cookie && cookie.length > 0) {
|
|
186
199
|
// Splitting and looking up first part for forward compatibility, in case we wish to store additional information in a cookie
|
|
@@ -200,9 +213,11 @@ function user(): User {
|
|
|
200
213
|
}
|
|
201
214
|
// End code for backward compatibility
|
|
202
215
|
// Read version information and timestamp from cookie, if available
|
|
216
|
+
if (parts.length > 1) { output.version = num(parts[1]); }
|
|
203
217
|
if (parts.length > 2) { output.expiry = num(parts[2], 36); }
|
|
204
218
|
// Check if we have explicit consent to track this user
|
|
205
219
|
if (parts.length > 3 && num(parts[3]) === 1) { output.consent = BooleanFlag.True; }
|
|
220
|
+
if (parts.length > 4 && num(parts[1]) > 1) { output.dob = num(parts[4]); }
|
|
206
221
|
// Set track configuration to true for this user if we have explicit consent, regardless of project setting
|
|
207
222
|
config.track = config.track || output.consent === BooleanFlag.True;
|
|
208
223
|
// Get user id from cookie only if we tracking is enabled, otherwise fallback to a random id
|
|
@@ -241,7 +256,7 @@ function setCookie(key: string, value: string, time: number): void {
|
|
|
241
256
|
rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`;
|
|
242
257
|
// We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net.
|
|
243
258
|
// So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL)
|
|
244
|
-
if (i < hostname.length - 1) {
|
|
259
|
+
if (i < hostname.length - 1) {
|
|
245
260
|
// Write the cookie on the current computed top level domain
|
|
246
261
|
document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`;
|
|
247
262
|
// Once written, check if the cookie exists and its value matches exactly with what we intended to set
|
package/src/data/variable.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Constant, Event, VariableData } from "@clarity-types/data";
|
|
1
|
+
import { Constant, Event, IdentityData, Setting, VariableData } from "@clarity-types/data";
|
|
2
2
|
import * as core from "@src/core";
|
|
3
|
+
import { scrub } from "@src/core/scrub";
|
|
3
4
|
import encode from "./encode";
|
|
4
5
|
|
|
5
6
|
export let data: VariableData = null;
|
|
@@ -13,10 +14,28 @@ export function set(variable: string, value: string | string[]): void {
|
|
|
13
14
|
log(variable, values);
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function identify(userId: string, sessionId: string = null, pageId: string = null):
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
export async function identify(userId: string, sessionId: string = null, pageId: string = null, userHint: string = null): Promise<IdentityData> {
|
|
18
|
+
let output: IdentityData = { userId: await sha256(userId), userHint: userHint || redact(userId) };
|
|
19
|
+
|
|
20
|
+
// By default, hash custom userId using SHA256 algorithm on the client to preserve privacy
|
|
21
|
+
log(Constant.UserId, [output.userId]);
|
|
22
|
+
|
|
23
|
+
// Optional non-identifying name for the user
|
|
24
|
+
// If name is not explicitly provided, we automatically generate a redacted version of the userId
|
|
25
|
+
log(Constant.UserHint, [output.userHint]);
|
|
26
|
+
log(Constant.UserType, [detect(userId)]);
|
|
27
|
+
|
|
28
|
+
// Log sessionId and pageId if provided
|
|
29
|
+
if (sessionId) {
|
|
30
|
+
log(Constant.SessionId, [sessionId]);
|
|
31
|
+
output.sessionId = sessionId;
|
|
32
|
+
}
|
|
33
|
+
if (pageId) {
|
|
34
|
+
log(Constant.PageId, [pageId]);
|
|
35
|
+
output.pageId = pageId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return output;
|
|
20
39
|
}
|
|
21
40
|
|
|
22
41
|
function log(variable: string, value: string[]): void {
|
|
@@ -44,3 +63,22 @@ export function reset(): void {
|
|
|
44
63
|
export function stop(): void {
|
|
45
64
|
reset();
|
|
46
65
|
}
|
|
66
|
+
|
|
67
|
+
function redact(input: string): string {
|
|
68
|
+
return input && input.length >= Setting.WordLength ?
|
|
69
|
+
`${input.substring(0,2)}${scrub(input.substring(2), Constant.Asterix, Constant.Asterix)}` : scrub(input, Constant.Asterix, Constant.Asterix);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function sha256(input: string): Promise<string> {
|
|
73
|
+
try {
|
|
74
|
+
if (crypto && input) {
|
|
75
|
+
// Reference: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest#converting_a_digest_to_a_hex_string
|
|
76
|
+
const buffer = await crypto.subtle.digest(Constant.SHA256, new TextEncoder().encode(input));
|
|
77
|
+
return Array.prototype.map.call(new Uint8Array(buffer), (x: any) =>(('00'+x.toString(16)).slice(-2))).join('');
|
|
78
|
+
} else { return Constant.Empty; }
|
|
79
|
+
} catch { return Constant.Empty; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function detect(input: string): string {
|
|
83
|
+
return input && input.indexOf(Constant.At) > 0 ? Constant.Email : Constant.String;
|
|
84
|
+
}
|
package/src/interaction/click.ts
CHANGED
|
@@ -92,10 +92,10 @@ function text(element: Node): string {
|
|
|
92
92
|
// Grab text using "textContent" for most HTMLElements, however, use "value" for HTMLInputElements and "alt" for HTMLImageElement.
|
|
93
93
|
let t = element.textContent || (element as HTMLInputElement).value || (element as HTMLImageElement).alt;
|
|
94
94
|
if (t) {
|
|
95
|
-
//
|
|
96
|
-
// Also,
|
|
95
|
+
// Replace multiple occurrence of space characters with a single white space
|
|
96
|
+
// Also, trim any spaces at the beginning or at the end of string
|
|
97
97
|
// Finally, send only first few characters as specified by the Setting
|
|
98
|
-
output = t.
|
|
98
|
+
output = t.replace(/\s+/g, Constant.Space).trim().substr(0, Setting.ClickText);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
return output;
|
package/src/queue.ts
CHANGED
|
@@ -9,6 +9,8 @@ export function setup() {
|
|
|
9
9
|
if (typeof w !== "undefined") {
|
|
10
10
|
w[c] = function() {
|
|
11
11
|
(w[c].q = w[c].q || []).push(arguments);
|
|
12
|
+
// if the start function was called, don't queue it and instead process the queue
|
|
13
|
+
arguments[0] === "start" && w[c].q.unshift(w[c].q.pop()) && process();
|
|
12
14
|
};
|
|
13
15
|
}
|
|
14
16
|
}
|
package/types/core.d.ts
CHANGED
|
@@ -34,11 +34,12 @@ export const enum ExtractSource {
|
|
|
34
34
|
Javascript = 0,
|
|
35
35
|
Cookie = 1,
|
|
36
36
|
Text = 2,
|
|
37
|
-
Fragment = 3
|
|
37
|
+
Fragment = 3,
|
|
38
|
+
Hash = 4
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export const enum Type {
|
|
41
|
-
Array = 1,
|
|
42
|
+
Array = 1,
|
|
42
43
|
Object = 2,
|
|
43
44
|
Simple = 3
|
|
44
45
|
}
|
|
@@ -135,6 +136,7 @@ export interface Config {
|
|
|
135
136
|
fallback?: string;
|
|
136
137
|
upgrade?: (key: string) => void;
|
|
137
138
|
action?: (key: string) => void;
|
|
139
|
+
dob?: number;
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
export const enum Constant {
|
package/types/data.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type DecodedToken = (any | any[]);
|
|
|
5
5
|
|
|
6
6
|
export type MetadataCallback = (data: Metadata, playback: boolean) => void;
|
|
7
7
|
export interface MetadataCallbackOptions {
|
|
8
|
-
callback: MetadataCallback,
|
|
8
|
+
callback: MetadataCallback,
|
|
9
9
|
wait: boolean
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -105,7 +105,8 @@ export const enum Metric {
|
|
|
105
105
|
Iframed = 31,
|
|
106
106
|
MaxTouchPoints = 32,
|
|
107
107
|
HardwareConcurrency = 33,
|
|
108
|
-
DeviceMemory = 34
|
|
108
|
+
DeviceMemory = 34,
|
|
109
|
+
Electron = 35
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
export const enum Dimension {
|
|
@@ -136,7 +137,9 @@ export const enum Dimension {
|
|
|
136
137
|
Brand = 24,
|
|
137
138
|
Model = 25,
|
|
138
139
|
DevicePixelRatio = 26,
|
|
139
|
-
ConnectionType = 27
|
|
140
|
+
ConnectionType = 27,
|
|
141
|
+
Dob = 28,
|
|
142
|
+
CookieVersion = 29
|
|
140
143
|
}
|
|
141
144
|
|
|
142
145
|
export const enum Check {
|
|
@@ -146,7 +149,8 @@ export const enum Check {
|
|
|
146
149
|
Retry = 3,
|
|
147
150
|
Bytes = 4,
|
|
148
151
|
Collection = 5,
|
|
149
|
-
Server = 6
|
|
152
|
+
Server = 6,
|
|
153
|
+
Page = 7
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
export const enum Code {
|
|
@@ -190,7 +194,7 @@ export const enum IframeStatus {
|
|
|
190
194
|
export const enum Setting {
|
|
191
195
|
Expire = 365, // 1 Year
|
|
192
196
|
SessionExpire = 1, // 1 Day
|
|
193
|
-
CookieVersion =
|
|
197
|
+
CookieVersion = 2, // Increment this version every time there's a cookie schema change
|
|
194
198
|
SessionTimeout = 30 * Time.Minute, // 30 minutes
|
|
195
199
|
CookieInterval = 1, // 1 Day
|
|
196
200
|
PingInterval = 1 * Time.Minute, // 1 Minute
|
|
@@ -198,6 +202,7 @@ export const enum Setting {
|
|
|
198
202
|
SummaryInterval = 100, // Same events within 100ms will be collapsed into single summary
|
|
199
203
|
ClickText = 25, // Maximum number of characters to send as part of Click event's text field
|
|
200
204
|
PayloadLimit = 128, // Do not allow more than specified payloads per page
|
|
205
|
+
PageLimit = 128, // Do not allow more than 128 pages in a session
|
|
201
206
|
ShutdownLimit = 2 * Time.Hour, // Shutdown instrumentation after specified time
|
|
202
207
|
RetryLimit = 1, // Maximum number of attempts to upload a payload before giving up
|
|
203
208
|
PlaybackBytesLimit = 10 * 1024 * 1024, // 10MB
|
|
@@ -218,7 +223,7 @@ export const enum Setting {
|
|
|
218
223
|
MinUploadDelay = 100, // Minimum time before we are ready to flush events to the server
|
|
219
224
|
MaxUploadDelay = 30 * Time.Second, // Do flush out payload once every 30s,
|
|
220
225
|
ExtractLimit = 10000, // Do not extract more than 10000 characters
|
|
221
|
-
ChecksumPrecision = 24, // n-bit integer to represent token hash
|
|
226
|
+
ChecksumPrecision = 24, // n-bit integer to represent token hash
|
|
222
227
|
UploadTimeout = 15000 // Timeout in ms for XHR requests
|
|
223
228
|
}
|
|
224
229
|
|
|
@@ -249,6 +254,8 @@ export const enum Constant {
|
|
|
249
254
|
Dropped = "*na*",
|
|
250
255
|
Comma = ",",
|
|
251
256
|
Dot = ".",
|
|
257
|
+
At = "@",
|
|
258
|
+
Asterix = "*",
|
|
252
259
|
Semicolon = ";",
|
|
253
260
|
Equals = "=",
|
|
254
261
|
Path = ";path=/",
|
|
@@ -258,6 +265,7 @@ export const enum Constant {
|
|
|
258
265
|
Top = "_top",
|
|
259
266
|
String = "string",
|
|
260
267
|
Number = "number",
|
|
268
|
+
Email = "email",
|
|
261
269
|
CookieKey = "_clck", // Clarity Cookie Key
|
|
262
270
|
SessionKey = "_clsk", // Clarity Session Key
|
|
263
271
|
TabKey = "_cltk", // Clarity Tab Key
|
|
@@ -266,6 +274,8 @@ export const enum Constant {
|
|
|
266
274
|
Upgrade = "UPGRADE",
|
|
267
275
|
Action = "ACTION",
|
|
268
276
|
Extract = "EXTRACT",
|
|
277
|
+
UserHint = "userHint",
|
|
278
|
+
UserType = "userType",
|
|
269
279
|
UserId = "userId",
|
|
270
280
|
SessionId = "sessionId",
|
|
271
281
|
PageId = "pageId",
|
|
@@ -289,7 +299,10 @@ export const enum Constant {
|
|
|
289
299
|
ConditionStart = "{",
|
|
290
300
|
ConditionEnd = "}",
|
|
291
301
|
Seperator = "<SEP>",
|
|
292
|
-
Timeout = "Timeout"
|
|
302
|
+
Timeout = "Timeout",
|
|
303
|
+
Bang = "!",
|
|
304
|
+
SHA256 = "SHA-256",
|
|
305
|
+
Electron = "Electron"
|
|
293
306
|
}
|
|
294
307
|
|
|
295
308
|
export const enum XMLReadyState {
|
|
@@ -331,8 +344,10 @@ export interface Session {
|
|
|
331
344
|
|
|
332
345
|
export interface User {
|
|
333
346
|
id: string;
|
|
347
|
+
version: number;
|
|
334
348
|
expiry: number;
|
|
335
349
|
consent: BooleanFlag;
|
|
350
|
+
dob: number;
|
|
336
351
|
}
|
|
337
352
|
|
|
338
353
|
export interface Envelope extends Metadata {
|
|
@@ -371,6 +386,13 @@ export interface BaselineData {
|
|
|
371
386
|
activityTime: number;
|
|
372
387
|
}
|
|
373
388
|
|
|
389
|
+
export interface IdentityData {
|
|
390
|
+
userId: string;
|
|
391
|
+
userHint: string;
|
|
392
|
+
sessionId?: string;
|
|
393
|
+
pageId?: string;
|
|
394
|
+
}
|
|
395
|
+
|
|
374
396
|
export interface DimensionData {
|
|
375
397
|
[key: number]: string[];
|
|
376
398
|
}
|
|
@@ -409,7 +431,7 @@ export interface UpgradeData {
|
|
|
409
431
|
}
|
|
410
432
|
|
|
411
433
|
export interface ExtractData {
|
|
412
|
-
[key: number]: [number
|
|
434
|
+
[key: number]: { [subkey : number]: string }; // Array of { subkey: number } representing the extracted data
|
|
413
435
|
}
|
|
414
436
|
|
|
415
437
|
export interface UploadData {
|
package/types/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ interface Clarity {
|
|
|
14
14
|
consent: () => void;
|
|
15
15
|
event: (name: string, value: string) => void;
|
|
16
16
|
set: (variable: string, value: string | string[]) => void;
|
|
17
|
-
identify: (userId: string, sessionId?: string, pageId?: string) => void;
|
|
17
|
+
identify: (userId: string, sessionId?: string, pageId?: string, userHint?: string) => void;
|
|
18
18
|
metadata: (callback: Data.MetadataCallback, wait?: boolean) => void;
|
|
19
19
|
}
|
|
20
20
|
|