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.
- package/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +4609 -4762
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +4609 -4762
- package/build/clarity.performance.js +1 -1
- package/package.json +69 -76
- package/rollup.config.ts +88 -84
- package/src/clarity.ts +29 -35
- package/src/core/api.ts +1 -8
- package/src/core/config.ts +2 -2
- package/src/core/event.ts +32 -36
- package/src/core/hash.ts +6 -5
- package/src/core/history.ts +11 -10
- package/src/core/index.ts +11 -21
- package/src/core/measure.ts +5 -9
- package/src/core/report.ts +6 -10
- package/src/core/scrub.ts +27 -30
- package/src/core/task.ts +45 -73
- 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 +55 -60
- package/src/data/consent.ts +22 -4
- package/src/data/custom.ts +13 -8
- package/src/data/dimension.ts +7 -11
- package/src/data/encode.ts +38 -36
- package/src/data/envelope.ts +38 -38
- package/src/data/extract.ts +77 -86
- package/src/data/index.ts +9 -11
- package/src/data/limit.ts +1 -1
- package/src/data/metadata.ts +319 -305
- package/src/data/metric.ts +8 -18
- package/src/data/ping.ts +4 -8
- package/src/data/signal.ts +18 -18
- package/src/data/summary.ts +4 -6
- package/src/data/token.ts +8 -10
- package/src/data/upgrade.ts +3 -7
- package/src/data/upload.ts +49 -100
- package/src/data/variable.ts +20 -27
- package/src/diagnostic/encode.ts +2 -2
- package/src/diagnostic/fraud.ts +4 -3
- package/src/diagnostic/internal.ts +5 -11
- package/src/diagnostic/script.ts +8 -12
- package/src/global.ts +1 -1
- package/src/insight/blank.ts +4 -4
- package/src/insight/encode.ts +17 -23
- package/src/insight/snapshot.ts +37 -57
- package/src/interaction/change.ts +6 -9
- package/src/interaction/click.ts +28 -34
- package/src/interaction/clipboard.ts +2 -2
- package/src/interaction/encode.ts +31 -35
- package/src/interaction/input.ts +9 -11
- package/src/interaction/pointer.ts +30 -41
- package/src/interaction/resize.ts +5 -5
- package/src/interaction/scroll.ts +17 -20
- package/src/interaction/selection.ts +8 -12
- package/src/interaction/submit.ts +2 -2
- package/src/interaction/timeline.ts +9 -13
- package/src/interaction/unload.ts +1 -1
- package/src/interaction/visibility.ts +2 -2
- package/src/layout/animation.ts +41 -47
- package/src/layout/discover.ts +5 -5
- package/src/layout/document.ts +19 -31
- package/src/layout/dom.ts +91 -141
- package/src/layout/encode.ts +37 -52
- package/src/layout/mutation.ts +318 -321
- package/src/layout/node.ts +81 -104
- package/src/layout/offset.ts +6 -7
- package/src/layout/region.ts +43 -66
- package/src/layout/schema.ts +8 -15
- package/src/layout/selector.ts +25 -47
- package/src/layout/style.ts +37 -45
- package/src/layout/target.ts +10 -14
- package/src/layout/traverse.ts +11 -17
- package/src/performance/blank.ts +1 -1
- package/src/performance/encode.ts +4 -4
- package/src/performance/interaction.ts +70 -58
- package/src/performance/navigation.ts +2 -2
- package/src/performance/observer.ts +26 -59
- package/src/queue.ts +9 -16
- package/tsconfig.json +1 -1
- package/tslint.json +32 -25
- package/types/core.d.ts +13 -13
- package/types/data.d.ts +48 -32
- package/types/diagnostic.d.ts +1 -1
- package/types/index.d.ts +1 -0
- package/types/interaction.d.ts +4 -5
- package/types/layout.d.ts +21 -36
- package/types/performance.d.ts +5 -6
- package/.lintstagedrc.yml +0 -3
- package/biome.json +0 -43
package/src/data/metadata.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
281
|
+
}
|
|
282
|
+
return output;
|
|
261
283
|
}
|
|
262
284
|
|
|
263
|
-
function num(string: string, base = 10): number {
|
|
264
|
-
|
|
285
|
+
function num(string: string, base: number = 10): number {
|
|
286
|
+
return parseInt(string, base);
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
function user(): User {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
326
342
|
}
|
|
327
343
|
|
|
328
344
|
function decodeCookieValue(value: string): [boolean, string] {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
356
|
+
return encodeURIComponent(value);
|
|
339
357
|
}
|
|
340
358
|
|
|
341
359
|
function setCookie(key: string, value: string, time: number): void {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
}
|