clarity-js 0.8.11 → 0.8.13-beta

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/build/clarity.extended.js +1 -1
  2. package/build/clarity.insight.js +1 -1
  3. package/build/clarity.js +2964 -3181
  4. package/build/clarity.min.js +1 -1
  5. package/build/clarity.module.js +2964 -3181
  6. package/build/clarity.performance.js +1 -1
  7. package/package.json +69 -76
  8. package/rollup.config.ts +88 -84
  9. package/src/clarity.ts +28 -34
  10. package/src/core/config.ts +2 -2
  11. package/src/core/event.ts +32 -36
  12. package/src/core/hash.ts +6 -5
  13. package/src/core/history.ts +11 -10
  14. package/src/core/index.ts +11 -21
  15. package/src/core/measure.ts +5 -9
  16. package/src/core/report.ts +3 -3
  17. package/src/core/scrub.ts +20 -29
  18. package/src/core/task.ts +45 -73
  19. package/src/core/time.ts +3 -3
  20. package/src/core/timeout.ts +2 -2
  21. package/src/core/version.ts +1 -1
  22. package/src/data/baseline.ts +55 -60
  23. package/src/data/consent.ts +2 -2
  24. package/src/data/custom.ts +13 -8
  25. package/src/data/dimension.ts +7 -11
  26. package/src/data/encode.ts +30 -36
  27. package/src/data/envelope.ts +38 -38
  28. package/src/data/extract.ts +77 -86
  29. package/src/data/index.ts +6 -10
  30. package/src/data/limit.ts +1 -1
  31. package/src/data/metadata.ts +266 -305
  32. package/src/data/metric.ts +8 -18
  33. package/src/data/ping.ts +4 -8
  34. package/src/data/signal.ts +18 -18
  35. package/src/data/summary.ts +4 -6
  36. package/src/data/token.ts +8 -10
  37. package/src/data/upgrade.ts +3 -7
  38. package/src/data/upload.ts +49 -100
  39. package/src/data/variable.ts +20 -27
  40. package/src/diagnostic/encode.ts +2 -2
  41. package/src/diagnostic/fraud.ts +4 -3
  42. package/src/diagnostic/internal.ts +5 -11
  43. package/src/diagnostic/script.ts +8 -12
  44. package/src/global.ts +1 -1
  45. package/src/insight/blank.ts +4 -4
  46. package/src/insight/encode.ts +17 -23
  47. package/src/insight/snapshot.ts +37 -57
  48. package/src/interaction/change.ts +6 -9
  49. package/src/interaction/click.ts +28 -34
  50. package/src/interaction/clipboard.ts +2 -2
  51. package/src/interaction/encode.ts +31 -35
  52. package/src/interaction/input.ts +9 -11
  53. package/src/interaction/pointer.ts +24 -35
  54. package/src/interaction/resize.ts +5 -5
  55. package/src/interaction/scroll.ts +11 -14
  56. package/src/interaction/selection.ts +8 -12
  57. package/src/interaction/submit.ts +2 -2
  58. package/src/interaction/timeline.ts +9 -13
  59. package/src/interaction/unload.ts +1 -1
  60. package/src/interaction/visibility.ts +2 -2
  61. package/src/layout/animation.ts +41 -47
  62. package/src/layout/discover.ts +5 -5
  63. package/src/layout/document.ts +19 -31
  64. package/src/layout/dom.ts +91 -141
  65. package/src/layout/encode.ts +37 -52
  66. package/src/layout/mutation.ts +318 -321
  67. package/src/layout/node.ts +81 -104
  68. package/src/layout/offset.ts +6 -7
  69. package/src/layout/region.ts +40 -60
  70. package/src/layout/schema.ts +8 -15
  71. package/src/layout/selector.ts +25 -47
  72. package/src/layout/style.ts +36 -44
  73. package/src/layout/target.ts +10 -14
  74. package/src/layout/traverse.ts +11 -17
  75. package/src/performance/blank.ts +1 -1
  76. package/src/performance/encode.ts +4 -4
  77. package/src/performance/interaction.ts +70 -58
  78. package/src/performance/navigation.ts +2 -2
  79. package/src/performance/observer.ts +26 -59
  80. package/src/queue.ts +9 -16
  81. package/tsconfig.json +1 -1
  82. package/tslint.json +32 -25
  83. package/types/core.d.ts +13 -13
  84. package/types/data.d.ts +29 -32
  85. package/types/diagnostic.d.ts +1 -1
  86. package/types/interaction.d.ts +4 -4
  87. package/types/layout.d.ts +21 -36
  88. package/types/performance.d.ts +5 -6
  89. package/.lintstagedrc.yml +0 -3
  90. package/biome.json +0 -43
@@ -1,383 +1,344 @@
1
1
  import { Time } from "@clarity-types/core";
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";
2
+ import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, User, Setting } from "@clarity-types/data";
14
3
  import * as clarity from "@src/clarity";
15
4
  import * as core from "@src/core";
16
5
  import config from "@src/core/config";
17
6
  import hash from "@src/core/hash";
18
7
  import * as scrub from "@src/core/scrub";
19
- import * as trackConsent from "@src/data/consent";
20
8
  import * as dimension from "@src/data/dimension";
21
9
  import * as metric from "@src/data/metric";
22
10
  import { set } from "@src/data/variable";
11
+ import * as trackConsent from "@src/data/consent";
23
12
 
24
13
  export let data: Metadata = null;
25
- export const callbacks: MetadataCallbackOptions[] = [];
14
+ export let callbacks: MetadataCallbackOptions[] = [];
26
15
  export let electron = BooleanFlag.False;
27
16
  let rootDomain = null;
28
17
 
29
18
  export function start(): void {
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);
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);
98
83
  }
99
84
 
100
85
  function userAgentData(): void {
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
- }
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); }
118
96
  }
119
97
 
120
98
  export function stop(): void {
121
- rootDomain = null;
122
- data = null;
123
- for (const cb of callbacks) {
124
- cb.called = false;
125
- }
99
+ rootDomain = null;
100
+ data = null;
101
+ callbacks.forEach(cb => { cb.called = false; });
126
102
  }
127
103
 
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
- }
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
+ }
142
118
  }
143
119
 
144
120
  export function id(): string {
145
- return data ? [data.userId, data.sessionId, data.pageNum].join(Constant.Dot) : Constant.Empty;
121
+ return data ? [data.userId, data.sessionId, data.pageNum].join(Constant.Dot) : Constant.Empty;
146
122
  }
147
123
 
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
- }
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
+ }
164
140
  }
165
141
 
166
142
  export function clear(): void {
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);
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);
169
145
  }
170
146
 
171
147
  function tab(): string {
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;
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;
179
155
  }
180
156
 
181
157
  export function callback(): void {
182
- const upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
183
- processCallback(upgrade);
158
+ let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
159
+ processCallback(upgrade);
184
160
  }
185
161
 
186
162
  export function save(): void {
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);
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);
195
168
  }
196
169
 
197
170
  function processCallback(upgrade: BooleanFlag) {
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
- }
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--;
209
180
  }
181
+ }
210
182
  }
183
+ }
211
184
  }
212
185
 
213
186
  function supported(target: Window | Document, api: string): boolean {
214
- try {
215
- return !!target[api];
216
- } catch {
217
- return false;
218
- }
187
+ try { return !!target[api]; } catch { return false; }
219
188
  }
220
189
 
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
- }
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
+ }
235
204
  }
236
205
 
237
206
  export function shortid(): string {
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);
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);
243
212
  }
244
213
 
245
214
  function session(): Session {
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
- }
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]}`;
259
227
  }
260
- return output;
228
+ }
229
+ return output;
261
230
  }
262
231
 
263
- function num(string: string, base = 10): number {
264
- return Number.parseInt(string, base);
232
+ function num(string: string, base: number = 10): number {
233
+ return parseInt(string, base);
265
234
  }
266
235
 
267
236
  function user(): User {
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;
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;
294
255
  }
295
256
 
296
257
  function getCookie(key: string, limit = false): string {
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
- }
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;
323
284
  }
285
+ }
324
286
  }
325
- return null;
287
+ }
288
+ return null;
326
289
  }
327
290
 
328
291
  function decodeCookieValue(value: string): [boolean, string] {
329
- try {
330
- const decodedValue = decodeURIComponent(value);
331
- return [decodedValue !== value, decodedValue];
332
- } catch {}
333
-
334
- return [false, value];
292
+ try {
293
+ let decodedValue = decodeURIComponent(value);
294
+ return [decodedValue != value, decodedValue];
295
+ }
296
+ catch {
297
+ }
298
+
299
+ return [false, value];
335
300
  }
336
301
 
337
302
  function encodeCookieValue(value: string): string {
338
- return encodeURIComponent(value);
303
+ return encodeURIComponent(value);
339
304
  }
340
305
 
341
306
  function setCookie(key: string, value: string, time: number): void {
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;
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
+ }
380
336
  }
381
- document.cookie = rootDomain ? `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}` : cookie;
382
- }
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
+ }
383
344
  }