clarity-js 0.8.12 → 0.8.14

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