clarity-js 0.8.42 → 0.8.43

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