clarity-js 0.8.1 → 0.8.3

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.
Files changed (102) hide show
  1. package/README.md +26 -26
  2. package/build/clarity.extended.js +1 -1
  3. package/build/clarity.insight.js +1 -1
  4. package/build/clarity.js +5537 -5537
  5. package/build/clarity.min.js +1 -1
  6. package/build/clarity.module.js +5537 -5537
  7. package/build/clarity.performance.js +1 -1
  8. package/package.json +70 -70
  9. package/rollup.config.ts +98 -98
  10. package/src/clarity.ts +60 -60
  11. package/src/core/api.ts +8 -8
  12. package/src/core/config.ts +32 -32
  13. package/src/core/copy.ts +3 -3
  14. package/src/core/event.ts +53 -53
  15. package/src/core/hash.ts +19 -19
  16. package/src/core/history.ts +73 -73
  17. package/src/core/index.ts +83 -83
  18. package/src/core/measure.ts +19 -19
  19. package/src/core/report.ts +28 -28
  20. package/src/core/scrub.ts +196 -196
  21. package/src/core/task.ts +180 -180
  22. package/src/core/time.ts +19 -19
  23. package/src/core/timeout.ts +10 -10
  24. package/src/core/version.ts +2 -2
  25. package/src/data/baseline.ts +155 -155
  26. package/src/data/compress.ts +31 -31
  27. package/src/data/consent.ts +20 -20
  28. package/src/data/custom.ts +23 -23
  29. package/src/data/dimension.ts +53 -53
  30. package/src/data/encode.ts +140 -140
  31. package/src/data/envelope.ts +53 -53
  32. package/src/data/extract.ts +211 -211
  33. package/src/data/index.ts +46 -46
  34. package/src/data/limit.ts +44 -44
  35. package/src/data/metadata.ts +356 -356
  36. package/src/data/metric.ts +51 -51
  37. package/src/data/ping.ts +36 -36
  38. package/src/data/signal.ts +30 -30
  39. package/src/data/summary.ts +34 -34
  40. package/src/data/token.ts +39 -39
  41. package/src/data/upgrade.ts +37 -37
  42. package/src/data/upload.ts +283 -283
  43. package/src/data/variable.ts +83 -83
  44. package/src/diagnostic/encode.ts +40 -40
  45. package/src/diagnostic/fraud.ts +36 -36
  46. package/src/diagnostic/index.ts +15 -15
  47. package/src/diagnostic/internal.ts +28 -28
  48. package/src/diagnostic/script.ts +37 -37
  49. package/src/global.ts +6 -6
  50. package/src/index.ts +9 -9
  51. package/src/insight/blank.ts +14 -14
  52. package/src/insight/encode.ts +60 -60
  53. package/src/insight/snapshot.ts +114 -114
  54. package/src/interaction/change.ts +40 -40
  55. package/src/interaction/click.ts +162 -162
  56. package/src/interaction/clipboard.ts +34 -34
  57. package/src/interaction/encode.ts +193 -193
  58. package/src/interaction/index.ts +59 -59
  59. package/src/interaction/input.ts +59 -59
  60. package/src/interaction/pointer.ts +139 -139
  61. package/src/interaction/resize.ts +47 -47
  62. package/src/interaction/scroll.ts +124 -124
  63. package/src/interaction/selection.ts +68 -68
  64. package/src/interaction/submit.ts +32 -32
  65. package/src/interaction/timeline.ts +65 -65
  66. package/src/interaction/unload.ts +28 -28
  67. package/src/interaction/visibility.ts +26 -26
  68. package/src/layout/animation.ts +133 -133
  69. package/src/layout/discover.ts +31 -31
  70. package/src/layout/document.ts +48 -48
  71. package/src/layout/dom.ts +437 -437
  72. package/src/layout/encode.ts +147 -147
  73. package/src/layout/index.ts +41 -41
  74. package/src/layout/mutation.ts +409 -409
  75. package/src/layout/node.ts +292 -292
  76. package/src/layout/offset.ts +19 -19
  77. package/src/layout/region.ts +153 -153
  78. package/src/layout/schema.ts +63 -63
  79. package/src/layout/selector.ts +82 -82
  80. package/src/layout/style.ts +150 -150
  81. package/src/layout/target.ts +32 -32
  82. package/src/layout/traverse.ts +27 -27
  83. package/src/performance/blank.ts +7 -7
  84. package/src/performance/encode.ts +31 -31
  85. package/src/performance/index.ts +14 -14
  86. package/src/performance/interaction.ts +125 -125
  87. package/src/performance/navigation.ts +31 -31
  88. package/src/performance/observer.ts +108 -108
  89. package/src/queue.ts +33 -33
  90. package/test/core.test.ts +139 -139
  91. package/test/helper.ts +162 -162
  92. package/test/html/core.html +27 -27
  93. package/test/tsconfig.test.json +5 -5
  94. package/tsconfig.json +21 -21
  95. package/tslint.json +32 -32
  96. package/types/core.d.ts +153 -153
  97. package/types/data.d.ts +510 -510
  98. package/types/diagnostic.d.ts +24 -24
  99. package/types/index.d.ts +39 -39
  100. package/types/interaction.d.ts +165 -165
  101. package/types/layout.d.ts +272 -272
  102. package/types/performance.d.ts +64 -64
@@ -1,356 +1,356 @@
1
- import { Time } from "@clarity-types/core";
2
- import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, User, Setting } from "@clarity-types/data";
3
- import * as clarity from "@src/clarity";
4
- import * as core from "@src/core";
5
- import config from "@src/core/config";
6
- import hash from "@src/core/hash";
7
- import * as scrub from "@src/core/scrub";
8
- import * as dimension from "@src/data/dimension";
9
- import * as metric from "@src/data/metric";
10
- import { set } from "@src/data/variable";
11
- import * as trackConsent from "@src/data/consent";
12
-
13
- export let data: Metadata = null;
14
- export let callbacks: MetadataCallbackOptions[] = [];
15
- export let electron = BooleanFlag.False;
16
- let rootDomain = null;
17
-
18
- export function start(): void {
19
- rootDomain = null;
20
- const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty;
21
- const timezone = Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone ?? '';
22
- const timezoneOffset = new Date().getTimezoneOffset().toString();
23
- const ancestorOrigins = window.location.ancestorOrigins ? Array.from(window.location.ancestorOrigins).toString() : '';
24
- const title = document && document.title ? document.title : Constant.Empty;
25
- electron = ua.indexOf(Constant.Electron) > 0 ? BooleanFlag.True : BooleanFlag.False;
26
-
27
- // Populate ids for this page
28
- let s = session();
29
- let u = user();
30
- let projectId = config.projectId || hash(location.host);
31
- data = { projectId, userId: u.id, sessionId: s.session, pageNum: s.count };
32
-
33
- // Override configuration based on what's in the session storage, unless it is blank (e.g. using upload callback, like in devtools)
34
- config.lean = config.track && s.upgrade !== null ? s.upgrade === BooleanFlag.False : config.lean;
35
- config.upload = config.track && typeof config.upload === Constant.String && s.upload && s.upload.length > Constant.HTTPS.length ? s.upload : config.upload;
36
-
37
- // Log page metadata as dimensions
38
- dimension.log(Dimension.UserAgent, ua);
39
- dimension.log(Dimension.PageTitle, title);
40
- dimension.log(Dimension.Url, scrub.url(location.href, !!electron));
41
- dimension.log(Dimension.Referrer, document.referrer);
42
- dimension.log(Dimension.TabId, tab());
43
- dimension.log(Dimension.PageLanguage, document.documentElement.lang);
44
- dimension.log(Dimension.DocumentDirection, document.dir);
45
- dimension.log(Dimension.DevicePixelRatio, `${window.devicePixelRatio}`);
46
- dimension.log(Dimension.Dob, u.dob.toString());
47
- dimension.log(Dimension.CookieVersion, u.version.toString());
48
- dimension.log(Dimension.AncestorOrigins, ancestorOrigins);
49
- dimension.log(Dimension.Timezone, timezone);
50
- dimension.log(Dimension.TimezoneOffset, timezoneOffset);
51
-
52
- // Capture additional metadata as metrics
53
- metric.max(Metric.ClientTimestamp, s.ts);
54
- metric.max(Metric.Playback, BooleanFlag.False);
55
- metric.max(Metric.Electron, electron);
56
-
57
- // Capture navigator specific dimensions
58
- if (navigator) {
59
- dimension.log(Dimension.Language, navigator.language);
60
- metric.max(Metric.HardwareConcurrency, navigator.hardwareConcurrency);
61
- metric.max(Metric.MaxTouchPoints, navigator.maxTouchPoints);
62
- metric.max(Metric.DeviceMemory, Math.round((<any>navigator).deviceMemory));
63
- userAgentData();
64
- }
65
-
66
- if (screen) {
67
- metric.max(Metric.ScreenWidth, Math.round(screen.width));
68
- metric.max(Metric.ScreenHeight, Math.round(screen.height));
69
- metric.max(Metric.ColorDepth, Math.round(screen.colorDepth));
70
- }
71
-
72
- // Read cookies specified in configuration
73
- for (let key of config.cookies) {
74
- let value = getCookie(key);
75
- if (value) { set(key, value); }
76
- }
77
-
78
- // Track consent config
79
- trackConsent.config(config.track);
80
-
81
- // Track ids using a cookie if configuration allows it
82
- track(u);
83
- }
84
-
85
- function userAgentData(): void {
86
- let uaData = navigator["userAgentData"];
87
- if (uaData && uaData.getHighEntropyValues) {
88
- uaData.getHighEntropyValues(["model", "platform", "platformVersion", "uaFullVersion"]).then(ua => {
89
- dimension.log(Dimension.Platform, ua.platform);
90
- dimension.log(Dimension.PlatformVersion, ua.platformVersion);
91
- ua.brands?.forEach(brand => { dimension.log(Dimension.Brand, brand.name + Constant.Tilde + brand.version); });
92
- dimension.log(Dimension.Model, ua.model);
93
- metric.max(Metric.Mobile, ua.mobile ? BooleanFlag.True : BooleanFlag.False);
94
- });
95
- } else { dimension.log(Dimension.Platform, navigator.platform); }
96
- }
97
-
98
- export function stop(): void {
99
- rootDomain = null;
100
- data = null;
101
- callbacks.forEach(cb => { cb.called = false; });
102
- }
103
-
104
- export function metadata(cb: MetadataCallback, wait: boolean = true, recall: boolean = false): void {
105
- let upgraded = config.lean ? BooleanFlag.False : BooleanFlag.True;
106
- let called = false;
107
- // if caller hasn't specified that they want to skip waiting for upgrade but we've already upgraded, we need to
108
- // directly execute the callback in addition to adding to our list as we only process callbacks at the moment
109
- // we go through the upgrading flow.
110
- if (data && (upgraded || wait === false)) {
111
- // Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
112
- cb(data, !config.lean);
113
- called = true;
114
- }
115
- if (recall || !called) {
116
- callbacks.push({ callback: cb, wait, recall, called });
117
- }
118
- }
119
-
120
- export function id(): string {
121
- return data ? [data.userId, data.sessionId, data.pageNum].join(Constant.Dot) : Constant.Empty;
122
- }
123
-
124
- export function consent(status: boolean = true): void {
125
- if (!status) {
126
- config.track = false;
127
- setCookie(Constant.SessionKey, Constant.Empty, -Number.MAX_VALUE);
128
- setCookie(Constant.CookieKey, Constant.Empty, -Number.MAX_VALUE);
129
- clarity.stop();
130
- window.setTimeout(clarity.start, Setting.RestartDelay);
131
- return;
132
- }
133
-
134
- if (core.active()) {
135
- config.track = true;
136
- track(user(), BooleanFlag.True);
137
- save();
138
- trackConsent.consent();
139
- }
140
- }
141
-
142
- export function clear(): void {
143
- // Clear any stored information in the cookie that tracks session information so we can restart fresh the next time
144
- setCookie(Constant.SessionKey, Constant.Empty, 0);
145
- }
146
-
147
- function tab(): string {
148
- let id = shortid();
149
- if (config.track && supported(window, Constant.SessionStorage)) {
150
- let value = sessionStorage.getItem(Constant.TabKey);
151
- id = value ? value : id;
152
- sessionStorage.setItem(Constant.TabKey, id);
153
- }
154
- return id;
155
- }
156
-
157
- export function callback(): void {
158
- let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
159
- processCallback(upgrade);
160
- }
161
-
162
- export function save(): void {
163
- if (!data || !config.track) return;
164
- let ts = Math.round(Date.now());
165
- let upload = config.upload && typeof config.upload === Constant.String ? (config.upload as string).replace(Constant.HTTPS, Constant.Empty) : Constant.Empty;
166
- let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
167
- setCookie(Constant.SessionKey, [data.sessionId, ts, data.pageNum, upgrade, upload].join(Constant.Pipe), Setting.SessionExpire);
168
- }
169
-
170
- function processCallback(upgrade: BooleanFlag) {
171
- if (callbacks.length > 0) {
172
- for (let i = 0; i < callbacks.length; i++) {
173
- const cb = callbacks[i];
174
- if (cb.callback && !cb.called && (!cb.wait || upgrade)) {
175
- cb.callback(data, !config.lean);
176
- cb.called = true;
177
- if (!cb.recall) {
178
- callbacks.splice(i, 1);
179
- i--;
180
- }
181
- }
182
- }
183
- }
184
- }
185
-
186
- function supported(target: Window | Document, api: string): boolean {
187
- try { return !!target[api]; } catch { return false; }
188
- }
189
-
190
- function track(u: User, consent: BooleanFlag = null): void {
191
- // If consent is not explicitly specified, infer it from the user object
192
- consent = consent === null ? u.consent : consent;
193
- // Convert time precision into days to reduce number of bytes we have to write in a cookie
194
- // E.g. Math.ceil(1628735962643 / (24*60*60*1000)) => 18852 (days) => ejo in base36 (13 bytes => 3 bytes)
195
- let end = Math.ceil((Date.now() + (Setting.Expire * Time.Day)) / Time.Day);
196
- // If DOB is not set in the user object, use the date set in the config as a DOB
197
- let dob = u.dob === 0 ? (config.dob === null ? 0 : config.dob) : u.dob;
198
-
199
- // To avoid cookie churn, write user id cookie only once every day
200
- if (u.expiry === null || Math.abs(end - u.expiry) >= Setting.CookieInterval || u.consent !== consent || u.dob !== dob) {
201
- let cookieParts = [data.userId, Setting.CookieVersion, end.toString(36), consent, dob];
202
- setCookie(Constant.CookieKey, cookieParts.join(Constant.Pipe), Setting.Expire);
203
- }
204
- }
205
-
206
- export function shortid(): string {
207
- let id = Math.floor(Math.random() * Math.pow(2, 32));
208
- if (window && window.crypto && window.crypto.getRandomValues && Uint32Array) {
209
- id = window.crypto.getRandomValues(new Uint32Array(1))[0];
210
- }
211
- return id.toString(36);
212
- }
213
-
214
- function session(): Session {
215
- let output: Session = { session: shortid(), ts: Math.round(Date.now()), count: 1, upgrade: null, upload: Constant.Empty };
216
- let value = getCookie(Constant.SessionKey, !config.includeSubdomains);
217
- if (value) {
218
- let parts = value.split(Constant.Pipe);
219
- // Making it backward & forward compatible by using greater than comparison (v0.6.21)
220
- // In future version, we can reduce the parts length to be 5 where the last part contains the full upload URL
221
- if (parts.length >= 5 && output.ts - num(parts[1]) < Setting.SessionTimeout) {
222
- output.session = parts[0];
223
- output.count = num(parts[2]) + 1;
224
- output.upgrade = num(parts[3]);
225
- output.upload = parts.length >= 6 ? `${Constant.HTTPS}${parts[5]}/${parts[4]}` : `${Constant.HTTPS}${parts[4]}`;
226
- }
227
- }
228
- return output;
229
- }
230
-
231
- function num(string: string, base: number = 10): number {
232
- return parseInt(string, base);
233
- }
234
-
235
- function user(): User {
236
- let output: User = { id: shortid(), version: 0, expiry: null, consent: BooleanFlag.False, dob: 0 };
237
- let cookie = getCookie(Constant.CookieKey, !config.includeSubdomains);
238
- if (cookie && cookie.length > 0) {
239
- // Splitting and looking up first part for forward compatibility, in case we wish to store additional information in a cookie
240
- let parts = cookie.split(Constant.Pipe);
241
- // For backward compatibility introduced in v0.6.18; following code can be removed with future iterations
242
- // Count number of times Clarity's user cookie crumb appears in document.cookie (could be on different sub-domains e.g. www.domain.com and .domain.com)
243
- let count = 0;
244
- for (let c of document.cookie.split(Constant.Semicolon)) { count += c.split(Constant.Equals)[0].trim() === Constant.CookieKey ? 1 : 0; }
245
- // Check if we either got version-less cookie value or saw multiple copies of the user cookie crumbs
246
- // In both these cases, we go ahead and delete the existing cookie set on current domain
247
- if (parts.length === 1 || count > 1) {
248
- let deleted = `${Constant.Semicolon}${Constant.Expires}${(new Date(0)).toUTCString()}${Constant.Path}`;
249
- // First, delete current user cookie which might be set on current sub-domain vs. root domain
250
- document.cookie = `${Constant.CookieKey}=${deleted}`;
251
- // Second, same thing for current session cookie so it can be re-written later with the root domain
252
- document.cookie = `${Constant.SessionKey}=${deleted}`;
253
- }
254
- // End code for backward compatibility
255
- // Read version information and timestamp from cookie, if available
256
- if (parts.length > 1) { output.version = num(parts[1]); }
257
- if (parts.length > 2) { output.expiry = num(parts[2], 36); }
258
- // Check if we have explicit consent to track this user
259
- if (parts.length > 3 && num(parts[3]) === 1) { output.consent = BooleanFlag.True; }
260
- if (parts.length > 4 && num(parts[1]) > 1) { output.dob = num(parts[4]); }
261
- // Set track configuration to true for this user if we have explicit consent, regardless of project setting
262
- config.track = config.track || output.consent === BooleanFlag.True;
263
- // Get user id from cookie only if we tracking is enabled, otherwise fallback to a random id
264
- output.id = config.track ? parts[0] : output.id;
265
- }
266
- return output;
267
- }
268
-
269
- function getCookie(key: string, limit = false): string {
270
- if (supported(document, Constant.Cookie)) {
271
- let cookies: string[] = document.cookie.split(Constant.Semicolon);
272
- if (cookies) {
273
- for (let i = 0; i < cookies.length; i++) {
274
- let pair: string[] = cookies[i].split(Constant.Equals);
275
- if (pair.length > 1 && pair[0] && pair[0].trim() === key) {
276
- // Some browsers automatically url encode cookie values if they are not url encoded.
277
- // We therefore encode and decode cookie values ourselves.
278
- // For backwards compatability we need to consider 3 cases:
279
- // * Cookie was previously not encoded by Clarity and browser did not encode it
280
- // * Cookie was previously not encoded by Clarity and browser encoded it once or more
281
- // * Cookie was previously encoded by Clarity and browser did not encode it
282
- let [isEncoded, decodedValue] = decodeCookieValue(pair[1]);
283
-
284
- while (isEncoded) {
285
- [isEncoded, decodedValue] = decodeCookieValue(decodedValue);
286
- }
287
-
288
- // If we are limiting cookies, check if the cookie value is limited
289
- if (limit) {
290
- return decodedValue.endsWith(`${Constant.Tilde}1`)
291
- ? decodedValue.substring(0, decodedValue.length - 2)
292
- : null;
293
- }
294
-
295
- return decodedValue;
296
- }
297
- }
298
- }
299
- }
300
- return null;
301
- }
302
-
303
- function decodeCookieValue(value: string): [boolean, string] {
304
- try {
305
- let decodedValue = decodeURIComponent(value);
306
- return [decodedValue != value, decodedValue];
307
- }
308
- catch {
309
- }
310
-
311
- return [false, value];
312
- }
313
-
314
- function encodeCookieValue(value: string): string {
315
- return encodeURIComponent(value);
316
- }
317
-
318
- function setCookie(key: string, value: string, time: number): void {
319
- // only write cookies if we are currently in a cookie writing mode (and they are supported)
320
- // OR if we are trying to write an empty cookie (i.e. clear the cookie value out)
321
- if ((config.track || value == Constant.Empty) && ((navigator && navigator.cookieEnabled) || supported(document, Constant.Cookie))) {
322
- // Some browsers automatically url encode cookie values if they are not url encoded.
323
- // We therefore encode and decode cookie values ourselves.
324
- let encodedValue = encodeCookieValue(value);
325
-
326
- let expiry = new Date();
327
- expiry.setDate(expiry.getDate() + time);
328
- let expires = expiry ? Constant.Expires + expiry.toUTCString() : Constant.Empty;
329
- let cookie = `${key}=${encodedValue}${Constant.Semicolon}${expires}${Constant.Path}`;
330
- try {
331
- // Attempt to get the root domain only once and fall back to writing cookie on the current domain.
332
- if (rootDomain === null) {
333
- let hostname = location.hostname ? location.hostname.split(Constant.Dot) : [];
334
- // Walk backwards on a domain and attempt to set a cookie, until successful
335
- for (let i = hostname.length - 1; i >= 0; i--) {
336
- rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`;
337
- // We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net.
338
- // So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL)
339
- if (i < hostname.length - 1) {
340
- // Write the cookie on the current computed top level domain
341
- document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`;
342
- // Once written, check if the cookie exists and its value matches exactly with what we intended to set
343
- // Checking for exact value match helps us eliminate a corner case where the cookie may already be present with a different value
344
- // If the check is successful, no more action is required and we can return from the function since rootDomain cookie is already set
345
- // If the check fails, continue with the for loop until we can successfully set and verify the cookie
346
- if (getCookie(key) === value) { return; }
347
- }
348
- }
349
- // Finally, if we were not successful and gone through all the options, play it safe and reset rootDomain to be empty
350
- // This forces our code to fall back to always writing cookie to the current domain
351
- rootDomain = Constant.Empty;
352
- }
353
- } catch { rootDomain = Constant.Empty; }
354
- document.cookie = rootDomain ? `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}` : cookie;
355
- }
356
- }
1
+ import { Time } from "@clarity-types/core";
2
+ import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, User, Setting } from "@clarity-types/data";
3
+ import * as clarity from "@src/clarity";
4
+ import * as core from "@src/core";
5
+ import config from "@src/core/config";
6
+ import hash from "@src/core/hash";
7
+ import * as scrub from "@src/core/scrub";
8
+ import * as dimension from "@src/data/dimension";
9
+ import * as metric from "@src/data/metric";
10
+ import { set } from "@src/data/variable";
11
+ import * as trackConsent from "@src/data/consent";
12
+
13
+ export let data: Metadata = null;
14
+ export let callbacks: MetadataCallbackOptions[] = [];
15
+ export let electron = BooleanFlag.False;
16
+ let rootDomain = null;
17
+
18
+ export function start(): void {
19
+ rootDomain = null;
20
+ const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty;
21
+ const timezone = Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone ?? '';
22
+ const timezoneOffset = new Date().getTimezoneOffset().toString();
23
+ const ancestorOrigins = window.location.ancestorOrigins ? Array.from(window.location.ancestorOrigins).toString() : '';
24
+ const title = document && document.title ? document.title : Constant.Empty;
25
+ electron = ua.indexOf(Constant.Electron) > 0 ? BooleanFlag.True : BooleanFlag.False;
26
+
27
+ // Populate ids for this page
28
+ let s = session();
29
+ let u = user();
30
+ let projectId = config.projectId || hash(location.host);
31
+ data = { projectId, userId: u.id, sessionId: s.session, pageNum: s.count };
32
+
33
+ // Override configuration based on what's in the session storage, unless it is blank (e.g. using upload callback, like in devtools)
34
+ config.lean = config.track && s.upgrade !== null ? s.upgrade === BooleanFlag.False : config.lean;
35
+ config.upload = config.track && typeof config.upload === Constant.String && s.upload && s.upload.length > Constant.HTTPS.length ? s.upload : config.upload;
36
+
37
+ // Log page metadata as dimensions
38
+ dimension.log(Dimension.UserAgent, ua);
39
+ dimension.log(Dimension.PageTitle, title);
40
+ dimension.log(Dimension.Url, scrub.url(location.href, !!electron));
41
+ dimension.log(Dimension.Referrer, document.referrer);
42
+ dimension.log(Dimension.TabId, tab());
43
+ dimension.log(Dimension.PageLanguage, document.documentElement.lang);
44
+ dimension.log(Dimension.DocumentDirection, document.dir);
45
+ dimension.log(Dimension.DevicePixelRatio, `${window.devicePixelRatio}`);
46
+ dimension.log(Dimension.Dob, u.dob.toString());
47
+ dimension.log(Dimension.CookieVersion, u.version.toString());
48
+ dimension.log(Dimension.AncestorOrigins, ancestorOrigins);
49
+ dimension.log(Dimension.Timezone, timezone);
50
+ dimension.log(Dimension.TimezoneOffset, timezoneOffset);
51
+
52
+ // Capture additional metadata as metrics
53
+ metric.max(Metric.ClientTimestamp, s.ts);
54
+ metric.max(Metric.Playback, BooleanFlag.False);
55
+ metric.max(Metric.Electron, electron);
56
+
57
+ // Capture navigator specific dimensions
58
+ if (navigator) {
59
+ dimension.log(Dimension.Language, navigator.language);
60
+ metric.max(Metric.HardwareConcurrency, navigator.hardwareConcurrency);
61
+ metric.max(Metric.MaxTouchPoints, navigator.maxTouchPoints);
62
+ metric.max(Metric.DeviceMemory, Math.round((<any>navigator).deviceMemory));
63
+ userAgentData();
64
+ }
65
+
66
+ if (screen) {
67
+ metric.max(Metric.ScreenWidth, Math.round(screen.width));
68
+ metric.max(Metric.ScreenHeight, Math.round(screen.height));
69
+ metric.max(Metric.ColorDepth, Math.round(screen.colorDepth));
70
+ }
71
+
72
+ // Read cookies specified in configuration
73
+ for (let key of config.cookies) {
74
+ let value = getCookie(key);
75
+ if (value) { set(key, value); }
76
+ }
77
+
78
+ // Track consent config
79
+ trackConsent.config(config.track);
80
+
81
+ // Track ids using a cookie if configuration allows it
82
+ track(u);
83
+ }
84
+
85
+ function userAgentData(): void {
86
+ let uaData = navigator["userAgentData"];
87
+ if (uaData && uaData.getHighEntropyValues) {
88
+ uaData.getHighEntropyValues(["model", "platform", "platformVersion", "uaFullVersion"]).then(ua => {
89
+ dimension.log(Dimension.Platform, ua.platform);
90
+ dimension.log(Dimension.PlatformVersion, ua.platformVersion);
91
+ ua.brands?.forEach(brand => { dimension.log(Dimension.Brand, brand.name + Constant.Tilde + brand.version); });
92
+ dimension.log(Dimension.Model, ua.model);
93
+ metric.max(Metric.Mobile, ua.mobile ? BooleanFlag.True : BooleanFlag.False);
94
+ });
95
+ } else { dimension.log(Dimension.Platform, navigator.platform); }
96
+ }
97
+
98
+ export function stop(): void {
99
+ rootDomain = null;
100
+ data = null;
101
+ callbacks.forEach(cb => { cb.called = false; });
102
+ }
103
+
104
+ export function metadata(cb: MetadataCallback, wait: boolean = true, recall: boolean = false): void {
105
+ let upgraded = config.lean ? BooleanFlag.False : BooleanFlag.True;
106
+ let called = false;
107
+ // if caller hasn't specified that they want to skip waiting for upgrade but we've already upgraded, we need to
108
+ // directly execute the callback in addition to adding to our list as we only process callbacks at the moment
109
+ // we go through the upgrading flow.
110
+ if (data && (upgraded || wait === false)) {
111
+ // Immediately invoke the callback if the caller explicitly doesn't want to wait for the upgrade confirmation
112
+ cb(data, !config.lean);
113
+ called = true;
114
+ }
115
+ if (recall || !called) {
116
+ callbacks.push({ callback: cb, wait, recall, called });
117
+ }
118
+ }
119
+
120
+ export function id(): string {
121
+ return data ? [data.userId, data.sessionId, data.pageNum].join(Constant.Dot) : Constant.Empty;
122
+ }
123
+
124
+ export function consent(status: boolean = true): void {
125
+ if (!status) {
126
+ config.track = false;
127
+ setCookie(Constant.SessionKey, Constant.Empty, -Number.MAX_VALUE);
128
+ setCookie(Constant.CookieKey, Constant.Empty, -Number.MAX_VALUE);
129
+ clarity.stop();
130
+ window.setTimeout(clarity.start, Setting.RestartDelay);
131
+ return;
132
+ }
133
+
134
+ if (core.active()) {
135
+ config.track = true;
136
+ track(user(), BooleanFlag.True);
137
+ save();
138
+ trackConsent.consent();
139
+ }
140
+ }
141
+
142
+ export function clear(): void {
143
+ // Clear any stored information in the cookie that tracks session information so we can restart fresh the next time
144
+ setCookie(Constant.SessionKey, Constant.Empty, 0);
145
+ }
146
+
147
+ function tab(): string {
148
+ let id = shortid();
149
+ if (config.track && supported(window, Constant.SessionStorage)) {
150
+ let value = sessionStorage.getItem(Constant.TabKey);
151
+ id = value ? value : id;
152
+ sessionStorage.setItem(Constant.TabKey, id);
153
+ }
154
+ return id;
155
+ }
156
+
157
+ export function callback(): void {
158
+ let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
159
+ processCallback(upgrade);
160
+ }
161
+
162
+ export function save(): void {
163
+ if (!data || !config.track) return;
164
+ let ts = Math.round(Date.now());
165
+ let upload = config.upload && typeof config.upload === Constant.String ? (config.upload as string).replace(Constant.HTTPS, Constant.Empty) : Constant.Empty;
166
+ let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
167
+ setCookie(Constant.SessionKey, [data.sessionId, ts, data.pageNum, upgrade, upload].join(Constant.Pipe), Setting.SessionExpire);
168
+ }
169
+
170
+ function processCallback(upgrade: BooleanFlag) {
171
+ if (callbacks.length > 0) {
172
+ for (let i = 0; i < callbacks.length; i++) {
173
+ const cb = callbacks[i];
174
+ if (cb.callback && !cb.called && (!cb.wait || upgrade)) {
175
+ cb.callback(data, !config.lean);
176
+ cb.called = true;
177
+ if (!cb.recall) {
178
+ callbacks.splice(i, 1);
179
+ i--;
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ function supported(target: Window | Document, api: string): boolean {
187
+ try { return !!target[api]; } catch { return false; }
188
+ }
189
+
190
+ function track(u: User, consent: BooleanFlag = null): void {
191
+ // If consent is not explicitly specified, infer it from the user object
192
+ consent = consent === null ? u.consent : consent;
193
+ // Convert time precision into days to reduce number of bytes we have to write in a cookie
194
+ // E.g. Math.ceil(1628735962643 / (24*60*60*1000)) => 18852 (days) => ejo in base36 (13 bytes => 3 bytes)
195
+ let end = Math.ceil((Date.now() + (Setting.Expire * Time.Day)) / Time.Day);
196
+ // If DOB is not set in the user object, use the date set in the config as a DOB
197
+ let dob = u.dob === 0 ? (config.dob === null ? 0 : config.dob) : u.dob;
198
+
199
+ // To avoid cookie churn, write user id cookie only once every day
200
+ if (u.expiry === null || Math.abs(end - u.expiry) >= Setting.CookieInterval || u.consent !== consent || u.dob !== dob) {
201
+ let cookieParts = [data.userId, Setting.CookieVersion, end.toString(36), consent, dob];
202
+ setCookie(Constant.CookieKey, cookieParts.join(Constant.Pipe), Setting.Expire);
203
+ }
204
+ }
205
+
206
+ export function shortid(): string {
207
+ let id = Math.floor(Math.random() * Math.pow(2, 32));
208
+ if (window && window.crypto && window.crypto.getRandomValues && Uint32Array) {
209
+ id = window.crypto.getRandomValues(new Uint32Array(1))[0];
210
+ }
211
+ return id.toString(36);
212
+ }
213
+
214
+ function session(): Session {
215
+ let output: Session = { session: shortid(), ts: Math.round(Date.now()), count: 1, upgrade: null, upload: Constant.Empty };
216
+ let value = getCookie(Constant.SessionKey, !config.includeSubdomains);
217
+ if (value) {
218
+ let parts = value.split(Constant.Pipe);
219
+ // Making it backward & forward compatible by using greater than comparison (v0.6.21)
220
+ // In future version, we can reduce the parts length to be 5 where the last part contains the full upload URL
221
+ if (parts.length >= 5 && output.ts - num(parts[1]) < Setting.SessionTimeout) {
222
+ output.session = parts[0];
223
+ output.count = num(parts[2]) + 1;
224
+ output.upgrade = num(parts[3]);
225
+ output.upload = parts.length >= 6 ? `${Constant.HTTPS}${parts[5]}/${parts[4]}` : `${Constant.HTTPS}${parts[4]}`;
226
+ }
227
+ }
228
+ return output;
229
+ }
230
+
231
+ function num(string: string, base: number = 10): number {
232
+ return parseInt(string, base);
233
+ }
234
+
235
+ function user(): User {
236
+ let output: User = { id: shortid(), version: 0, expiry: null, consent: BooleanFlag.False, dob: 0 };
237
+ let cookie = getCookie(Constant.CookieKey, !config.includeSubdomains);
238
+ if (cookie && cookie.length > 0) {
239
+ // Splitting and looking up first part for forward compatibility, in case we wish to store additional information in a cookie
240
+ let parts = cookie.split(Constant.Pipe);
241
+ // For backward compatibility introduced in v0.6.18; following code can be removed with future iterations
242
+ // Count number of times Clarity's user cookie crumb appears in document.cookie (could be on different sub-domains e.g. www.domain.com and .domain.com)
243
+ let count = 0;
244
+ for (let c of document.cookie.split(Constant.Semicolon)) { count += c.split(Constant.Equals)[0].trim() === Constant.CookieKey ? 1 : 0; }
245
+ // Check if we either got version-less cookie value or saw multiple copies of the user cookie crumbs
246
+ // In both these cases, we go ahead and delete the existing cookie set on current domain
247
+ if (parts.length === 1 || count > 1) {
248
+ let deleted = `${Constant.Semicolon}${Constant.Expires}${(new Date(0)).toUTCString()}${Constant.Path}`;
249
+ // First, delete current user cookie which might be set on current sub-domain vs. root domain
250
+ document.cookie = `${Constant.CookieKey}=${deleted}`;
251
+ // Second, same thing for current session cookie so it can be re-written later with the root domain
252
+ document.cookie = `${Constant.SessionKey}=${deleted}`;
253
+ }
254
+ // End code for backward compatibility
255
+ // Read version information and timestamp from cookie, if available
256
+ if (parts.length > 1) { output.version = num(parts[1]); }
257
+ if (parts.length > 2) { output.expiry = num(parts[2], 36); }
258
+ // Check if we have explicit consent to track this user
259
+ if (parts.length > 3 && num(parts[3]) === 1) { output.consent = BooleanFlag.True; }
260
+ if (parts.length > 4 && num(parts[1]) > 1) { output.dob = num(parts[4]); }
261
+ // Set track configuration to true for this user if we have explicit consent, regardless of project setting
262
+ config.track = config.track || output.consent === BooleanFlag.True;
263
+ // Get user id from cookie only if we tracking is enabled, otherwise fallback to a random id
264
+ output.id = config.track ? parts[0] : output.id;
265
+ }
266
+ return output;
267
+ }
268
+
269
+ function getCookie(key: string, limit = false): string {
270
+ if (supported(document, Constant.Cookie)) {
271
+ let cookies: string[] = document.cookie.split(Constant.Semicolon);
272
+ if (cookies) {
273
+ for (let i = 0; i < cookies.length; i++) {
274
+ let pair: string[] = cookies[i].split(Constant.Equals);
275
+ if (pair.length > 1 && pair[0] && pair[0].trim() === key) {
276
+ // Some browsers automatically url encode cookie values if they are not url encoded.
277
+ // We therefore encode and decode cookie values ourselves.
278
+ // For backwards compatability we need to consider 3 cases:
279
+ // * Cookie was previously not encoded by Clarity and browser did not encode it
280
+ // * Cookie was previously not encoded by Clarity and browser encoded it once or more
281
+ // * Cookie was previously encoded by Clarity and browser did not encode it
282
+ let [isEncoded, decodedValue] = decodeCookieValue(pair[1]);
283
+
284
+ while (isEncoded) {
285
+ [isEncoded, decodedValue] = decodeCookieValue(decodedValue);
286
+ }
287
+
288
+ // If we are limiting cookies, check if the cookie value is limited
289
+ if (limit) {
290
+ return decodedValue.endsWith(`${Constant.Tilde}1`)
291
+ ? decodedValue.substring(0, decodedValue.length - 2)
292
+ : null;
293
+ }
294
+
295
+ return decodedValue;
296
+ }
297
+ }
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+
303
+ function decodeCookieValue(value: string): [boolean, string] {
304
+ try {
305
+ let decodedValue = decodeURIComponent(value);
306
+ return [decodedValue != value, decodedValue];
307
+ }
308
+ catch {
309
+ }
310
+
311
+ return [false, value];
312
+ }
313
+
314
+ function encodeCookieValue(value: string): string {
315
+ return encodeURIComponent(value);
316
+ }
317
+
318
+ function setCookie(key: string, value: string, time: number): void {
319
+ // only write cookies if we are currently in a cookie writing mode (and they are supported)
320
+ // OR if we are trying to write an empty cookie (i.e. clear the cookie value out)
321
+ if ((config.track || value == Constant.Empty) && ((navigator && navigator.cookieEnabled) || supported(document, Constant.Cookie))) {
322
+ // Some browsers automatically url encode cookie values if they are not url encoded.
323
+ // We therefore encode and decode cookie values ourselves.
324
+ let encodedValue = encodeCookieValue(value);
325
+
326
+ let expiry = new Date();
327
+ expiry.setDate(expiry.getDate() + time);
328
+ let expires = expiry ? Constant.Expires + expiry.toUTCString() : Constant.Empty;
329
+ let cookie = `${key}=${encodedValue}${Constant.Semicolon}${expires}${Constant.Path}`;
330
+ try {
331
+ // Attempt to get the root domain only once and fall back to writing cookie on the current domain.
332
+ if (rootDomain === null) {
333
+ let hostname = location.hostname ? location.hostname.split(Constant.Dot) : [];
334
+ // Walk backwards on a domain and attempt to set a cookie, until successful
335
+ for (let i = hostname.length - 1; i >= 0; i--) {
336
+ rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`;
337
+ // We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net.
338
+ // So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL)
339
+ if (i < hostname.length - 1) {
340
+ // Write the cookie on the current computed top level domain
341
+ document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`;
342
+ // Once written, check if the cookie exists and its value matches exactly with what we intended to set
343
+ // Checking for exact value match helps us eliminate a corner case where the cookie may already be present with a different value
344
+ // If the check is successful, no more action is required and we can return from the function since rootDomain cookie is already set
345
+ // If the check fails, continue with the for loop until we can successfully set and verify the cookie
346
+ if (getCookie(key) === value) { return; }
347
+ }
348
+ }
349
+ // Finally, if we were not successful and gone through all the options, play it safe and reset rootDomain to be empty
350
+ // This forces our code to fall back to always writing cookie to the current domain
351
+ rootDomain = Constant.Empty;
352
+ }
353
+ } catch { rootDomain = Constant.Empty; }
354
+ document.cookie = rootDomain ? `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}` : cookie;
355
+ }
356
+ }