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.
- package/.lintstagedrc.yml +3 -0
- package/biome.json +43 -0
- package/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +3095 -2876
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +3095 -2876
- package/build/clarity.performance.js +1 -1
- package/package.json +17 -10
- package/rollup.config.ts +84 -88
- package/src/clarity.ts +34 -28
- package/src/core/config.ts +2 -2
- package/src/core/event.ts +36 -32
- package/src/core/hash.ts +5 -6
- package/src/core/history.ts +10 -11
- package/src/core/index.ts +21 -11
- package/src/core/measure.ts +9 -5
- package/src/core/report.ts +9 -5
- package/src/core/scrub.ts +29 -20
- package/src/core/task.ts +73 -45
- package/src/core/time.ts +3 -3
- package/src/core/timeout.ts +2 -2
- package/src/core/version.ts +1 -1
- package/src/data/baseline.ts +60 -55
- package/src/data/consent.ts +2 -2
- package/src/data/custom.ts +8 -13
- package/src/data/dimension.ts +11 -7
- package/src/data/encode.ts +36 -30
- package/src/data/envelope.ts +38 -38
- package/src/data/extract.ts +86 -77
- package/src/data/index.ts +10 -6
- package/src/data/limit.ts +1 -1
- package/src/data/metadata.ts +305 -266
- package/src/data/metric.ts +18 -8
- package/src/data/ping.ts +8 -4
- package/src/data/signal.ts +18 -18
- package/src/data/summary.ts +6 -4
- package/src/data/token.ts +10 -8
- package/src/data/upgrade.ts +7 -3
- package/src/data/upload.ts +100 -49
- package/src/data/variable.ts +27 -20
- package/src/diagnostic/encode.ts +2 -2
- package/src/diagnostic/fraud.ts +3 -4
- package/src/diagnostic/internal.ts +11 -5
- package/src/diagnostic/script.ts +12 -8
- package/src/global.ts +1 -1
- package/src/insight/blank.ts +4 -4
- package/src/insight/encode.ts +23 -17
- package/src/insight/snapshot.ts +57 -37
- package/src/interaction/change.ts +9 -6
- package/src/interaction/click.ts +34 -28
- package/src/interaction/clipboard.ts +2 -2
- package/src/interaction/encode.ts +35 -31
- package/src/interaction/input.ts +11 -9
- package/src/interaction/pointer.ts +41 -30
- package/src/interaction/resize.ts +5 -5
- package/src/interaction/scroll.ts +20 -17
- package/src/interaction/selection.ts +12 -8
- package/src/interaction/submit.ts +2 -2
- package/src/interaction/timeline.ts +13 -9
- package/src/interaction/unload.ts +1 -1
- package/src/interaction/visibility.ts +2 -2
- package/src/layout/animation.ts +47 -41
- package/src/layout/discover.ts +5 -5
- package/src/layout/document.ts +31 -19
- package/src/layout/dom.ts +141 -91
- package/src/layout/encode.ts +52 -37
- package/src/layout/mutation.ts +321 -318
- package/src/layout/node.ts +104 -81
- package/src/layout/offset.ts +7 -6
- package/src/layout/region.ts +66 -43
- package/src/layout/schema.ts +15 -8
- package/src/layout/selector.ts +47 -25
- package/src/layout/style.ts +45 -37
- package/src/layout/target.ts +14 -10
- package/src/layout/traverse.ts +17 -11
- package/src/performance/blank.ts +1 -1
- package/src/performance/encode.ts +4 -4
- package/src/performance/interaction.ts +58 -70
- package/src/performance/navigation.ts +2 -2
- package/src/performance/observer.ts +59 -26
- package/src/queue.ts +16 -9
- package/tsconfig.json +1 -1
- package/tslint.json +25 -32
- package/types/core.d.ts +13 -13
- package/types/data.d.ts +32 -29
- package/types/diagnostic.d.ts +1 -1
- package/types/interaction.d.ts +5 -4
- package/types/layout.d.ts +36 -21
- package/types/performance.d.ts +6 -5
package/src/data/metadata.ts
CHANGED
|
@@ -1,344 +1,383 @@
|
|
|
1
1
|
import { Time } from "@clarity-types/core";
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
uaData
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
145
|
+
return data ? [data.userId, data.sessionId, data.pageNum].join(Constant.Dot) : Constant.Empty;
|
|
122
146
|
}
|
|
123
147
|
|
|
124
|
-
export function consent(status
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
159
|
-
|
|
182
|
+
const upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
|
|
183
|
+
processCallback(upgrade);
|
|
160
184
|
}
|
|
161
185
|
|
|
162
186
|
export function save(): void {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
214
|
+
try {
|
|
215
|
+
return !!target[api];
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
188
219
|
}
|
|
189
220
|
|
|
190
|
-
function track(u: User,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
233
|
-
|
|
263
|
+
function num(string: string, base = 10): number {
|
|
264
|
+
return Number.parseInt(string, base);
|
|
234
265
|
}
|
|
235
266
|
|
|
236
267
|
function user(): User {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
338
|
+
return encodeURIComponent(value);
|
|
304
339
|
}
|
|
305
340
|
|
|
306
341
|
function setCookie(key: string, value: string, time: number): void {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
338
|
-
|
|
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
|
}
|