clarity-js 0.7.7 → 0.7.9
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 +145 -69
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +145 -69
- 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 +1 -1
- package/src/data/extract.ts +1 -1
- package/src/data/limit.ts +1 -0
- package/src/data/metadata.ts +24 -14
- package/src/data/variable.ts +43 -5
- package/src/interaction/click.ts +3 -3
- package/src/performance/blank.ts +7 -0
- package/src/performance/encode.ts +1 -1
- package/types/core.d.ts +2 -1
- package/types/data.d.ts +28 -7
- package/types/index.d.ts +1 -1
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
|
}
|
|
@@ -149,9 +153,13 @@ function track(u: User, consent: BooleanFlag = null): void {
|
|
|
149
153
|
// Convert time precision into days to reduce number of bytes we have to write in a cookie
|
|
150
154
|
// E.g. Math.ceil(1628735962643 / (24*60*60*1000)) => 18852 (days) => ejo in base36 (13 bytes => 3 bytes)
|
|
151
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
|
+
|
|
152
159
|
// To avoid cookie churn, write user id cookie only once every day
|
|
153
|
-
if (u.expiry === null || Math.abs(end - u.expiry) >= Setting.CookieInterval || u.consent !== consent) {
|
|
154
|
-
|
|
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);
|
|
155
163
|
}
|
|
156
164
|
}
|
|
157
165
|
|
|
@@ -185,7 +193,7 @@ function num(string: string, base: number = 10): number {
|
|
|
185
193
|
}
|
|
186
194
|
|
|
187
195
|
function user(): User {
|
|
188
|
-
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 };
|
|
189
197
|
let cookie = getCookie(Constant.CookieKey);
|
|
190
198
|
if(cookie && cookie.length > 0) {
|
|
191
199
|
// Splitting and looking up first part for forward compatibility, in case we wish to store additional information in a cookie
|
|
@@ -205,9 +213,11 @@ function user(): User {
|
|
|
205
213
|
}
|
|
206
214
|
// End code for backward compatibility
|
|
207
215
|
// Read version information and timestamp from cookie, if available
|
|
216
|
+
if (parts.length > 1) { output.version = num(parts[1]); }
|
|
208
217
|
if (parts.length > 2) { output.expiry = num(parts[2], 36); }
|
|
209
218
|
// Check if we have explicit consent to track this user
|
|
210
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]); }
|
|
211
221
|
// Set track configuration to true for this user if we have explicit consent, regardless of project setting
|
|
212
222
|
config.track = config.track || output.consent === BooleanFlag.True;
|
|
213
223
|
// Get user id from cookie only if we tracking is enabled, otherwise fallback to a random id
|
|
@@ -246,7 +256,7 @@ function setCookie(key: string, value: string, time: number): void {
|
|
|
246
256
|
rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`;
|
|
247
257
|
// We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net.
|
|
248
258
|
// So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL)
|
|
249
|
-
if (i < hostname.length - 1) {
|
|
259
|
+
if (i < hostname.length - 1) {
|
|
250
260
|
// Write the cookie on the current computed top level domain
|
|
251
261
|
document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`;
|
|
252
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/types/core.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export const enum ExtractSource {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export const enum Type {
|
|
42
|
-
Array = 1,
|
|
42
|
+
Array = 1,
|
|
43
43
|
Object = 2,
|
|
44
44
|
Simple = 3
|
|
45
45
|
}
|
|
@@ -136,6 +136,7 @@ export interface Config {
|
|
|
136
136
|
fallback?: string;
|
|
137
137
|
upgrade?: (key: string) => void;
|
|
138
138
|
action?: (key: string) => void;
|
|
139
|
+
dob?: number;
|
|
139
140
|
}
|
|
140
141
|
|
|
141
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",
|
|
@@ -290,7 +300,9 @@ export const enum Constant {
|
|
|
290
300
|
ConditionEnd = "}",
|
|
291
301
|
Seperator = "<SEP>",
|
|
292
302
|
Timeout = "Timeout",
|
|
293
|
-
Bang = "!"
|
|
303
|
+
Bang = "!",
|
|
304
|
+
SHA256 = "SHA-256",
|
|
305
|
+
Electron = "Electron"
|
|
294
306
|
}
|
|
295
307
|
|
|
296
308
|
export const enum XMLReadyState {
|
|
@@ -332,8 +344,10 @@ export interface Session {
|
|
|
332
344
|
|
|
333
345
|
export interface User {
|
|
334
346
|
id: string;
|
|
347
|
+
version: number;
|
|
335
348
|
expiry: number;
|
|
336
349
|
consent: BooleanFlag;
|
|
350
|
+
dob: number;
|
|
337
351
|
}
|
|
338
352
|
|
|
339
353
|
export interface Envelope extends Metadata {
|
|
@@ -372,6 +386,13 @@ export interface BaselineData {
|
|
|
372
386
|
activityTime: number;
|
|
373
387
|
}
|
|
374
388
|
|
|
389
|
+
export interface IdentityData {
|
|
390
|
+
userId: string;
|
|
391
|
+
userHint: string;
|
|
392
|
+
sessionId?: string;
|
|
393
|
+
pageId?: string;
|
|
394
|
+
}
|
|
395
|
+
|
|
375
396
|
export interface DimensionData {
|
|
376
397
|
[key: number]: string[];
|
|
377
398
|
}
|
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
|
|