clarity-js 0.8.9 → 0.8.10

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