@gemx-dev/clarity-js 0.8.45 → 0.8.47

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gemx-dev/clarity-js",
3
- "version": "0.8.45",
3
+ "version": "0.8.47",
4
4
  "description": "Clarity js",
5
5
  "author": "Microsoft Corp.",
6
6
  "license": "MIT",
@@ -20,16 +20,11 @@
20
20
  "@rollup/plugin-node-resolve": "^15.0.0",
21
21
  "@rollup/plugin-terser": "^0.4.0",
22
22
  "@rollup/plugin-typescript": "^11.0.0",
23
- "@types/chai": "^4.3.0",
24
23
  "@types/resize-observer-browser": "^0.1.6",
25
- "chai": "^4.3.0",
26
24
  "del-cli": "^5.0.0",
27
25
  "husky": "^8.0.0",
28
26
  "lint-staged": "^13.1.0",
29
- "playwright": "^1.6.2",
30
27
  "rollup": "^3.0.0",
31
- "ts-mocha": "^11.1.0",
32
- "tslib": "^2.3.0",
33
28
  "tslint": "^6.1.3",
34
29
  "typescript": "^4.3.5"
35
30
  },
@@ -37,7 +32,7 @@
37
32
  "build": "yarn build:clean && yarn build:main",
38
33
  "build:main": "rollup -c rollup.config.ts --configPlugin @rollup/plugin-typescript",
39
34
  "build:clean": "del-cli build/*",
40
- "test": "ts-mocha -p test/tsconfig.test.json test/stub.test.ts",
35
+ "test": "playwright test --config ../../playwright.config.ts --project=clarity-js",
41
36
  "tslint": "tslint --project ./",
42
37
  "tslint:fix": "tslint --fix --project ./ --force"
43
38
  },
package/src/clarity.ts CHANGED
@@ -22,19 +22,17 @@ export { measure };
22
22
  const modules: Module[] = [diagnostic, layout, interaction, performance, dynamic];
23
23
 
24
24
  export function start(config: Config = null): void {
25
- // Check that browser supports required APIs and we do not attempt to start Clarity multiple times
26
- if (core.check()) {
27
- core.config(config);
28
- core.start();
29
- data.start();
30
- modules.forEach((x) => measure(x.start)());
25
+ // Check that browser supports required APIs and we do not attempt to start Clarity multiple times
26
+ if (core.check()) {
27
+ core.config(config);
28
+ core.start();
29
+ data.start();
30
+ modules.forEach(x => measure(x.start)());
31
31
 
32
- // If it's an internal call to start, without explicit configuration,
33
- // re-process any newly accumulated items in the queue
34
- if (config === null) {
35
- queue.process();
36
- }
37
- }
32
+ // If it's an internal call to start, without explicit configuration,
33
+ // re-process any newly accumulated items in the queue
34
+ if (config === null) { queue.process(); }
35
+ }
38
36
  }
39
37
 
40
38
  // By default Clarity is asynchronous and will yield by looking for requestIdleCallback.
@@ -43,29 +41,26 @@ export function start(config: Config = null): void {
43
41
  // we do allow external clients to manually pause Clarity for that short burst of time and minimize
44
42
  // performance impact even further. For reference, we are talking single digit milliseconds optimization here, not seconds.
45
43
  export function pause(): void {
46
- if (core.active()) {
47
- data.event(Constant.Clarity, Constant.Pause);
48
- task.pause();
49
- }
44
+ if (core.active()) {
45
+ data.event(Constant.Clarity, Constant.Pause);
46
+ task.pause();
47
+ }
50
48
  }
51
49
 
52
50
  // This is how external clients can get out of pause state, and resume Clarity to continue monitoring the page
53
51
  export function resume(): void {
54
- if (core.active()) {
55
- task.resume();
56
- data.event(Constant.Clarity, Constant.Resume);
57
- }
52
+ if (core.active()) {
53
+ task.resume();
54
+ data.event(Constant.Clarity, Constant.Resume);
55
+ }
58
56
  }
59
57
 
60
58
  export function stop(): void {
61
- if (core.active()) {
62
- // Stop modules in the reverse order of their initialization and start queuing up items again
63
- modules
64
- .slice()
65
- .reverse()
66
- .forEach((x) => measure(x.stop)());
67
- data.stop();
68
- core.stop();
69
- queue.setup();
70
- }
71
- }
59
+ if (core.active()) {
60
+ // Stop modules in the reverse order of their initialization and start queuing up items again
61
+ modules.slice().reverse().forEach(x => measure(x.stop)());
62
+ data.stop();
63
+ core.stop();
64
+ queue.setup();
65
+ }
66
+ }
@@ -9,23 +9,16 @@ export function reset(): void {
9
9
  }
10
10
 
11
11
  export function report(e: Error): Error {
12
- console.log(`🚀 🐥 ~ report ~ e:`, e);
13
12
  // Do not report the same message twice for the same page
14
13
  if (history && history.indexOf(e.message) === -1) {
15
14
  const url = config.report;
16
- console.log(`🚀 🐥 ~ report ~ url:`, url);
17
15
  if (url && url.length > 0 && data) {
18
- let payload: Report = { v: data.version, p: data.projectId, u: data.userId, s: data.sessionId, n: data.pageNum };
19
- if (e.message) {
20
- payload.m = e.message;
21
- }
22
- if (e.stack) {
23
- payload.e = e.stack;
24
- }
16
+ let payload: Report = {v: data.version, p: data.projectId, u: data.userId, s: data.sessionId, n: data.pageNum};
17
+ if (e.message) { payload.m = e.message; }
18
+ if (e.stack) { payload.e = e.stack; }
25
19
  // Using POST request instead of a GET request (img-src) to not violate existing CSP rules
26
20
  // Since, Clarity already uses XHR to upload data, we stick with similar POST mechanism for reporting too
27
21
  let xhr = new XMLHttpRequest();
28
- console.log(`🚀 🐥 ~ report ~ xhr:`, xhr);
29
22
  xhr.open("POST", url, true);
30
23
  xhr.send(JSON.stringify(payload));
31
24
  history.push(e.message);
@@ -1,2 +1,2 @@
1
- let version = "0.8.45";
1
+ let version = "0.8.47";
2
2
  export default version;
@@ -30,12 +30,13 @@ function processConsent(): void {
30
30
 
31
31
  const analytics_storage = ics.getConsentState(Constant.AnalyticsStorage);
32
32
  const ad_storage = ics.getConsentState(Constant.AdStorage);
33
- const consentState = getConsentState({ ad_Storage: ad_storage, analytics_Storage: analytics_storage });
34
- consentv2(consentState, ConsentSource.GCM);
33
+ const consentState = getGcmConsentState({ ad_Storage: ad_storage, analytics_Storage: analytics_storage });
34
+ consentv2(consentState);
35
35
  }
36
36
 
37
- function getConsentState(googleConsent: GCMConsentState): ConsentState {
37
+ function getGcmConsentState(googleConsent: GCMConsentState): ConsentState {
38
38
  const consentState: ConsentState = {
39
+ source: ConsentSource.GCM,
39
40
  ad_Storage: googleConsent.ad_Storage === GCMConsent.Granted ? Constant.Granted : Constant.Denied,
40
41
  analytics_Storage: googleConsent.analytics_Storage === GCMConsent.Granted ? Constant.Granted : Constant.Denied,
41
42
  };
@@ -0,0 +1,90 @@
1
+ import { Constant } from "@clarity-types/data";
2
+ import config from "@src/core/config";
3
+ import { decodeCookieValue, encodeCookieValue, supported } from "@src/data/util";
4
+
5
+ let rootDomain = null;
6
+ export const COOKIE_SEP = Constant.Caret;
7
+
8
+ export function start() {
9
+ rootDomain = null;
10
+ }
11
+
12
+ export function stop() {
13
+ rootDomain = null;
14
+ }
15
+
16
+ export function getCookie(key: string, limit = false): string {
17
+ if (supported(document, Constant.Cookie)) {
18
+ let cookies: string[] = document.cookie.split(Constant.Semicolon);
19
+ if (cookies) {
20
+ for (let i = 0; i < cookies.length; i++) {
21
+ let pair: string[] = cookies[i].split(Constant.Equals);
22
+ if (pair.length > 1 && pair[0] && pair[0].trim() === key) {
23
+ // Some browsers automatically url encode cookie values if they are not url encoded.
24
+ // We therefore encode and decode cookie values ourselves.
25
+ // For backwards compatability we need to consider 3 cases:
26
+ // * Cookie was previously not encoded by Clarity and browser did not encode it
27
+ // * Cookie was previously not encoded by Clarity and browser encoded it once or more
28
+ // * Cookie was previously encoded by Clarity and browser did not encode it
29
+ let [isEncoded, decodedValue] = decodeCookieValue(pair[1]);
30
+
31
+ while (isEncoded) {
32
+ [isEncoded, decodedValue] = decodeCookieValue(decodedValue);
33
+ }
34
+
35
+ // If we are limiting cookies, check if the cookie value is limited
36
+ if (limit) {
37
+ return decodedValue.endsWith(`${Constant.Tilde}1`) ? decodedValue.substring(0, decodedValue.length - 2) : null;
38
+ }
39
+
40
+ return decodedValue;
41
+ }
42
+ }
43
+ }
44
+ }
45
+ return null;
46
+ }
47
+
48
+ export function setCookie(key: string, value: string, time: number): void {
49
+ // only write cookies if we are currently in a cookie writing mode (and they are supported)
50
+ // OR if we are trying to write an empty cookie (i.e. clear the cookie value out)
51
+ if ((config.track || value == Constant.Empty) && ((navigator && navigator.cookieEnabled) || supported(document, Constant.Cookie))) {
52
+ // Some browsers automatically url encode cookie values if they are not url encoded.
53
+ // We therefore encode and decode cookie values ourselves.
54
+ let encodedValue = encodeCookieValue(value);
55
+
56
+ let expiry = new Date();
57
+ expiry.setDate(expiry.getDate() + time);
58
+ let expires = expiry ? Constant.Expires + expiry.toUTCString() : Constant.Empty;
59
+ let cookie = `${key}=${encodedValue}${Constant.Semicolon}${expires}${Constant.Path}`;
60
+ try {
61
+ // Attempt to get the root domain only once and fall back to writing cookie on the current domain.
62
+ if (rootDomain === null) {
63
+ let hostname = location.hostname ? location.hostname.split(Constant.Dot) : [];
64
+ // Walk backwards on a domain and attempt to set a cookie, until successful
65
+ for (let i = hostname.length - 1; i >= 0; i--) {
66
+ rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`;
67
+ // We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net.
68
+ // So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL)
69
+ if (i < hostname.length - 1) {
70
+ // Write the cookie on the current computed top level domain
71
+ document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`;
72
+ // Once written, check if the cookie exists and its value matches exactly with what we intended to set
73
+ // Checking for exact value match helps us eliminate a corner case where the cookie may already be present with a different value
74
+ // If the check is successful, no more action is required and we can return from the function since rootDomain cookie is already set
75
+ // If the check fails, continue with the for loop until we can successfully set and verify the cookie
76
+ if (getCookie(key) === value) {
77
+ return;
78
+ }
79
+ }
80
+ }
81
+ // Finally, if we were not successful and gone through all the options, play it safe and reset rootDomain to be empty
82
+ // This forces our code to fall back to always writing cookie to the current domain
83
+ rootDomain = Constant.Empty;
84
+ }
85
+ } catch {
86
+ rootDomain = Constant.Empty;
87
+ }
88
+ document.cookie = rootDomain ? `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}` : cookie;
89
+ }
90
+ }
package/src/data/index.ts CHANGED
@@ -13,6 +13,7 @@ import * as upgrade from "@src/data/upgrade";
13
13
  import * as upload from "@src/data/upload";
14
14
  import * as variable from "@src/data/variable";
15
15
  import * as extract from "@src/data/extract";
16
+ import * as cookie from "@src/data/cookie";
16
17
  export { event } from "@src/data/custom";
17
18
  export { consent, consentv2, metadata } from "@src/data/metadata";
18
19
  export { upgrade } from "@src/data/upgrade";
@@ -21,7 +22,7 @@ export { signal } from "@src/data/signal";
21
22
  export { max as maxMetric } from "@src/data/metric";
22
23
  export { log as dlog } from "@src/data/dimension";
23
24
 
24
- const modules: Module[] = [baseline, dimension, variable, limit, summary, consent, metadata, envelope, upload, ping, upgrade, extract];
25
+ const modules: Module[] = [baseline, dimension, variable, limit, summary, cookie, consent, metadata, envelope, upload, ping, upgrade, extract];
25
26
 
26
27
  export function start(): void {
27
28
  // Metric needs to be initialized before we can start measuring. so metric is not wrapped in measure
@@ -1,25 +1,24 @@
1
- import { Time } from "@clarity-types/core";
2
- import { BooleanFlag, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, User, Setting, ConsentState, ConsentSource, ConsentData } from "@clarity-types/data";
1
+ import { Constant as CoreConstant, Time } from "@clarity-types/core";
2
+ import { BooleanFlag, ConsentData, ConsentSource, ConsentState, Constant, Dimension, Metadata, MetadataCallback, MetadataCallbackOptions, Metric, Session, Setting, User } from "@clarity-types/data";
3
3
  import * as clarity from "@src/clarity";
4
4
  import * as core from "@src/core";
5
5
  import config from "@src/core/config";
6
6
  import hash from "@src/core/hash";
7
7
  import * as scrub from "@src/core/scrub";
8
+ import * as trackConsent from "@src/data/consent";
9
+ import { COOKIE_SEP, getCookie, setCookie } from "@src/data/cookie";
8
10
  import * as dimension from "@src/data/dimension";
9
11
  import * as metric from "@src/data/metric";
12
+ import { supported } from "@src/data/util";
10
13
  import { set } from "@src/data/variable";
11
- import * as trackConsent from "@src/data/consent";
12
- import { Constant as CoreConstant } from "@clarity-types/core";
13
14
 
14
15
  export let data: Metadata = null;
15
16
  export let callbacks: MetadataCallbackOptions[] = [];
16
17
  export let electron = BooleanFlag.False;
17
- let rootDomain = null;
18
18
  let consentStatus: ConsentState = null;
19
- let defaultStatus: ConsentState = { source: ConsentSource.API, ad_Storage: Constant.Denied, analytics_Storage: Constant.Denied };
19
+ let defaultStatus: ConsentState = { source: ConsentSource.Default, ad_Storage: Constant.Denied, analytics_Storage: Constant.Denied };
20
20
 
21
21
  export function start(): void {
22
- rootDomain = null;
23
22
  const ua = navigator && "userAgent" in navigator ? navigator.userAgent : Constant.Empty;
24
23
  const timezone = (typeof Intl !== 'undefined' && Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone) ?? '';
25
24
  const timezoneOffset = new Date().getTimezoneOffset().toString();
@@ -89,7 +88,7 @@ export function start(): void {
89
88
  // If consent status is not already set, initialize it based on project configuration. Otherwise, use the existing consent status.
90
89
  if (consentStatus === null) {
91
90
  consentStatus = {
92
- source: ConsentSource.Implicit,
91
+ source: u.consent ? ConsentSource.Cookie : ConsentSource.Implicit,
93
92
  ad_Storage: config.track ? Constant.Granted : Constant.Denied,
94
93
  analytics_Storage: config.track ? Constant.Granted : Constant.Denied,
95
94
  };
@@ -114,7 +113,6 @@ function userAgentData(): void {
114
113
  }
115
114
 
116
115
  export function stop(): void {
117
- rootDomain = null;
118
116
  data = null;
119
117
  callbacks.forEach(cb => { cb.called = false; });
120
118
  }
@@ -142,15 +140,15 @@ export function id(): string {
142
140
  //TODO: Remove this function once consentv2 is fully released
143
141
  export function consent(status = true): void {
144
142
  if (!status) {
145
- consentv2();
143
+ consentv2({ source: ConsentSource.APIv1, ad_Storage: Constant.Denied, analytics_Storage: Constant.Denied });
146
144
  return;
147
145
  }
148
146
 
149
- consentv2({ ad_Storage: Constant.Granted, analytics_Storage: Constant.Granted });
147
+ consentv2({ source: ConsentSource.APIv1, ad_Storage: Constant.Granted, analytics_Storage: Constant.Granted });
150
148
  trackConsent.consent();
151
149
  }
152
150
 
153
- export function consentv2(consentState: ConsentState = defaultStatus, source: number = ConsentSource.API): void {
151
+ export function consentv2(consentState: ConsentState = defaultStatus, source: number = ConsentSource.APIv2): void {
154
152
  const updatedStatus = {
155
153
  source: consentState.source ?? source,
156
154
  ad_Storage: normalizeConsent(consentState.ad_Storage, consentStatus?.ad_Storage),
@@ -162,6 +160,9 @@ export function consentv2(consentState: ConsentState = defaultStatus, source: nu
162
160
  updatedStatus.ad_Storage === consentStatus.ad_Storage &&
163
161
  updatedStatus.analytics_Storage === consentStatus.analytics_Storage
164
162
  ) {
163
+ consentStatus.source = updatedStatus.source;
164
+ trackConsent.trackConsentv2(getConsentData(consentStatus));
165
+ trackConsent.consent();
165
166
  return;
166
167
  }
167
168
 
@@ -171,8 +172,7 @@ export function consentv2(consentState: ConsentState = defaultStatus, source: nu
171
172
 
172
173
  if (!consentData.analytics_Storage && config.track) {
173
174
  config.track = false;
174
- setCookie(Constant.SessionKey, Constant.Empty, -Number.MAX_VALUE);
175
- setCookie(Constant.CookieKey, Constant.Empty, -Number.MAX_VALUE);
175
+ clear(true);
176
176
  clarity.stop();
177
177
  window.setTimeout(clarity.start, Setting.RestartDelay);
178
178
  return;
@@ -202,9 +202,14 @@ function normalizeConsent(value: unknown, fallback: string = Constant.Denied): s
202
202
  return typeof value === 'string' ? value.toLowerCase() : fallback;
203
203
  }
204
204
 
205
- export function clear(): void {
205
+ export function clear(all: boolean = false): void {
206
206
  // Clear any stored information in the cookie that tracks session information so we can restart fresh the next time
207
- setCookie(Constant.SessionKey, Constant.Empty, 0);
207
+ setCookie(Constant.SessionKey, Constant.Empty, -Number.MAX_VALUE);
208
+
209
+ // Clear user cookie as well if all flag is set
210
+ if (all) {
211
+ setCookie(Constant.CookieKey, Constant.Empty, -Number.MAX_VALUE);
212
+ }
208
213
  }
209
214
 
210
215
  function tab(): string {
@@ -227,7 +232,7 @@ export function save(): void {
227
232
  let ts = Math.round(Date.now());
228
233
  let upload = config.upload && typeof config.upload === Constant.String ? (config.upload as string).replace(Constant.HTTPS, Constant.Empty) : Constant.Empty;
229
234
  let upgrade = config.lean ? BooleanFlag.False : BooleanFlag.True;
230
- setCookie(Constant.SessionKey, [data.sessionId, ts, data.pageNum, upgrade, upload].join(Constant.Caret), Setting.SessionExpire);
235
+ setCookie(Constant.SessionKey, [data.sessionId, ts, data.pageNum, upgrade, upload].join(COOKIE_SEP), Setting.SessionExpire);
231
236
  }
232
237
 
233
238
  function processCallback(upgrade: BooleanFlag, consentUpdate: boolean = false): void {
@@ -250,10 +255,6 @@ function processCallback(upgrade: BooleanFlag, consentUpdate: boolean = false):
250
255
  }
251
256
  }
252
257
 
253
- function supported(target: Window | Document, api: string): boolean {
254
- try { return !!target[api]; } catch { return false; }
255
- }
256
-
257
258
  function track(u: User, consent: BooleanFlag = null): void {
258
259
  // If consent is not explicitly specified, infer it from the user object
259
260
  consent = consent === null ? u.consent : consent;
@@ -266,7 +267,7 @@ function track(u: User, consent: BooleanFlag = null): void {
266
267
  // To avoid cookie churn, write user id cookie only once every day
267
268
  if (u.expiry === null || Math.abs(end - u.expiry) >= Setting.CookieInterval || u.consent !== consent || u.dob !== dob) {
268
269
  let cookieParts = [data.userId, Setting.CookieVersion, end.toString(36), consent, dob];
269
- setCookie(Constant.CookieKey, cookieParts.join(Constant.Caret), Setting.Expire);
270
+ setCookie(Constant.CookieKey, cookieParts.join(COOKIE_SEP), Setting.Expire);
270
271
  }
271
272
  }
272
273
 
@@ -321,91 +322,3 @@ function user(): User {
321
322
  return output;
322
323
  }
323
324
 
324
- function getCookie(key: string, limit = false): string {
325
- if (supported(document, Constant.Cookie)) {
326
- let cookies: string[] = document.cookie.split(Constant.Semicolon);
327
- if (cookies) {
328
- for (let i = 0; i < cookies.length; i++) {
329
- let pair: string[] = cookies[i].split(Constant.Equals);
330
- if (pair.length > 1 && pair[0] && pair[0].trim() === key) {
331
- // Some browsers automatically url encode cookie values if they are not url encoded.
332
- // We therefore encode and decode cookie values ourselves.
333
- // For backwards compatability we need to consider 3 cases:
334
- // * Cookie was previously not encoded by Clarity and browser did not encode it
335
- // * Cookie was previously not encoded by Clarity and browser encoded it once or more
336
- // * Cookie was previously encoded by Clarity and browser did not encode it
337
- let [isEncoded, decodedValue] = decodeCookieValue(pair[1]);
338
-
339
- while (isEncoded) {
340
- [isEncoded, decodedValue] = decodeCookieValue(decodedValue);
341
- }
342
-
343
- // If we are limiting cookies, check if the cookie value is limited
344
- if (limit) {
345
- return decodedValue.endsWith(`${Constant.Tilde}1`)
346
- ? decodedValue.substring(0, decodedValue.length - 2)
347
- : null;
348
- }
349
-
350
- return decodedValue;
351
- }
352
- }
353
- }
354
- }
355
- return null;
356
- }
357
-
358
- function decodeCookieValue(value: string): [boolean, string] {
359
- try {
360
- let decodedValue = decodeURIComponent(value);
361
- return [decodedValue != value, decodedValue];
362
- }
363
- catch {
364
- }
365
-
366
- return [false, value];
367
- }
368
-
369
- function encodeCookieValue(value: string): string {
370
- return encodeURIComponent(value);
371
- }
372
-
373
- function setCookie(key: string, value: string, time: number): void {
374
- // only write cookies if we are currently in a cookie writing mode (and they are supported)
375
- // OR if we are trying to write an empty cookie (i.e. clear the cookie value out)
376
- if ((config.track || value == Constant.Empty) && ((navigator && navigator.cookieEnabled) || supported(document, Constant.Cookie))) {
377
- // Some browsers automatically url encode cookie values if they are not url encoded.
378
- // We therefore encode and decode cookie values ourselves.
379
- let encodedValue = encodeCookieValue(value);
380
-
381
- let expiry = new Date();
382
- expiry.setDate(expiry.getDate() + time);
383
- let expires = expiry ? Constant.Expires + expiry.toUTCString() : Constant.Empty;
384
- let cookie = `${key}=${encodedValue}${Constant.Semicolon}${expires}${Constant.Path}`;
385
- try {
386
- // Attempt to get the root domain only once and fall back to writing cookie on the current domain.
387
- if (rootDomain === null) {
388
- let hostname = location.hostname ? location.hostname.split(Constant.Dot) : [];
389
- // Walk backwards on a domain and attempt to set a cookie, until successful
390
- for (let i = hostname.length - 1; i >= 0; i--) {
391
- rootDomain = `.${hostname[i]}${rootDomain ? rootDomain : Constant.Empty}`;
392
- // We do not wish to attempt writing a cookie on the absolute last part of the domain, e.g. .com or .net.
393
- // So we start attempting after second-last part, e.g. .domain.com (PASS) or .co.uk (FAIL)
394
- if (i < hostname.length - 1) {
395
- // Write the cookie on the current computed top level domain
396
- document.cookie = `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}`;
397
- // Once written, check if the cookie exists and its value matches exactly with what we intended to set
398
- // Checking for exact value match helps us eliminate a corner case where the cookie may already be present with a different value
399
- // If the check is successful, no more action is required and we can return from the function since rootDomain cookie is already set
400
- // If the check fails, continue with the for loop until we can successfully set and verify the cookie
401
- if (getCookie(key) === value) { return; }
402
- }
403
- }
404
- // Finally, if we were not successful and gone through all the options, play it safe and reset rootDomain to be empty
405
- // This forces our code to fall back to always writing cookie to the current domain
406
- rootDomain = Constant.Empty;
407
- }
408
- } catch { rootDomain = Constant.Empty; }
409
- document.cookie = rootDomain ? `${cookie}${Constant.Semicolon}${Constant.Domain}${rootDomain}` : cookie;
410
- }
411
- }
@@ -0,0 +1,18 @@
1
+ export function supported(target: Window | Document, api: string): boolean {
2
+ try { return !!target[api]; } catch { return false; }
3
+ }
4
+
5
+ export function encodeCookieValue(value: string): string {
6
+ return encodeURIComponent(value);
7
+ }
8
+
9
+ export function decodeCookieValue(value: string): [boolean, string] {
10
+ try {
11
+ let decodedValue = decodeURIComponent(value);
12
+ return [decodedValue != value, decodedValue];
13
+ }
14
+ catch {
15
+ }
16
+
17
+ return [false, value];
18
+ }
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import hash from "./core/hash";
3
3
  import * as selector from "./layout/selector";
4
4
  import { get, getNode, lookup } from "./layout/dom";
5
5
 
6
- const helper = { hash, selector, get, getNode, lookup };
6
+ const helper = { hash, selector, get, getNode, lookup }
7
7
  const version = clarity.version;
8
8
 
9
9
  export { clarity, version, helper };
@@ -38,7 +38,7 @@ export function metadata(node: Node): TargetMetadata {
38
38
  return output;
39
39
  }
40
40
 
41
- export function snapshot(): void {
41
+ export function snapshot(): void {
42
42
  values = [];
43
43
  traverse(document);
44
44
  encode(Event.Snapshot);
@@ -54,13 +54,13 @@ function traverse(root: Node): void {
54
54
  let attributes = null, tag = null, value = null;
55
55
  let node = queue.shift();
56
56
  let next = node.firstChild;
57
- let parent = node.parentElement ? node.parentElement : (node.parentNode ? node.parentNode : null);
57
+ let parent = node.parentElement ? node.parentElement : (node.parentNode ? node.parentNode : null);
58
58
 
59
59
  while (next) {
60
60
  queue.push(next);
61
61
  next = next.nextSibling;
62
62
  }
63
-
63
+
64
64
  // Process the node
65
65
  let type = node.nodeType;
66
66
  switch (type) {
@@ -69,7 +69,7 @@ function traverse(root: Node): void {
69
69
  tag = Constant.DocumentTag;
70
70
  attributes = { name: doctype.name, publicId: doctype.publicId, systemId: doctype.systemId }
71
71
  break;
72
- case Node.TEXT_NODE:
72
+ case Node.TEXT_NODE:
73
73
  value = node.nodeValue;
74
74
  tag = idMap.get(parent) ? Constant.TextTag : tag;
75
75
  break;
@@ -79,7 +79,7 @@ function traverse(root: Node): void {
79
79
  tag = ["NOSCRIPT", "SCRIPT", "STYLE"].indexOf(element.tagName) < 0 ? element.tagName : tag;
80
80
  break;
81
81
  }
82
- add(node, parent, { tag, attributes, value });
82
+ add(node, parent, { tag, attributes, value });
83
83
  }
84
84
  }
85
85
 
@@ -88,7 +88,7 @@ function getAttributes(element: HTMLElement): { [key: string]: string } {
88
88
  let output = {};
89
89
  let attributes = element.attributes;
90
90
  if (attributes && attributes.length > 0) {
91
- for (let i = 0; i < attributes.length; i++) {
91
+ for (let i = 0; i < attributes.length; i++) {
92
92
  output[attributes[i].name] = attributes[i].value;
93
93
  }
94
94
  }
@@ -112,4 +112,4 @@ function add(node: Node, parent: Node, data: NodeInfo): void {
112
112
  }
113
113
  }
114
114
 
115
- export function get(_node: Node): NodeValue { return null; }
115
+ export function get(_node: Node): NodeValue {return null;}
@@ -0,0 +1,68 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import hash from "@src/core/hash";
3
+
4
+ test.describe("Core Utilities - Hash", () => {
5
+ test("hash function should generate consistent hash for same input", () => {
6
+ const input = "test-string";
7
+ const hash1 = hash(input);
8
+ const hash2 = hash(input);
9
+
10
+ expect(hash1).toBe(hash2);
11
+ expect(hash1).toBeTruthy();
12
+ expect(typeof hash1).toBe("string");
13
+ });
14
+
15
+ test("hash function should generate different hashes for different inputs", () => {
16
+ const hash1 = hash("input1");
17
+ const hash2 = hash("input2");
18
+ const hash3 = hash("completely-different-input");
19
+
20
+ expect(hash1).not.toBe(hash2);
21
+ expect(hash2).not.toBe(hash3);
22
+ expect(hash1).not.toBe(hash3);
23
+ });
24
+
25
+ test("hash function should respect precision parameter", () => {
26
+ const input = "test-precision";
27
+ const hashNoPrecision = hash(input);
28
+ const hash16 = hash(input, 16);
29
+ const hash8 = hash(input, 8);
30
+
31
+ expect(hashNoPrecision).toBeTruthy();
32
+ expect(hash16).toBeTruthy();
33
+ expect(hash8).toBeTruthy();
34
+
35
+ // Hash with precision should be different from no precision
36
+ expect(hashNoPrecision).not.toBe(hash16);
37
+
38
+ // Verify precision limits: 2^16 = 65536, 2^8 = 256
39
+ const hash16Num = parseInt(hash16, 36);
40
+ const hash8Num = parseInt(hash8, 36);
41
+ expect(hash16Num).toBeLessThan(65536);
42
+ expect(hash8Num).toBeLessThan(256);
43
+ });
44
+
45
+ test("hash function should handle empty strings", () => {
46
+ const result = hash("");
47
+ expect(result).toBeTruthy();
48
+ expect(typeof result).toBe("string");
49
+ });
50
+
51
+ test("hash function should handle special characters", () => {
52
+ const hash1 = hash("test@example.com");
53
+ const hash2 = hash("test#$%^&*()");
54
+ const hash3 = hash("🎉🎊✨");
55
+
56
+ expect(hash1).toBeTruthy();
57
+ expect(hash2).toBeTruthy();
58
+ expect(hash3).toBeTruthy();
59
+ expect(hash1).not.toBe(hash2);
60
+ expect(hash2).not.toBe(hash3);
61
+ });
62
+
63
+ test("hash function should produce base36 output", () => {
64
+ const result = hash("test-base36");
65
+ // Base36 should only contain 0-9 and a-z
66
+ expect(/^[0-9a-z]+$/.test(result)).toBe(true);
67
+ });
68
+ });
@@ -0,0 +1,42 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { start, stop, time } from "@src/core/time";
3
+
4
+ test.describe("Time Utilities", () => {
5
+ test("time module should start and stop", () => {
6
+ start();
7
+ const time1 = time();
8
+
9
+ expect(time1).toBeGreaterThanOrEqual(0);
10
+
11
+ stop();
12
+ const time2 = time();
13
+
14
+ // After stop, time should still work but use different baseline
15
+ expect(time2).toBeGreaterThanOrEqual(0);
16
+ });
17
+
18
+ test("time function should track elapsed time", async () => {
19
+ start();
20
+ const time1 = time();
21
+
22
+ // Wait a bit
23
+ await new Promise((resolve) => setTimeout(resolve, 50));
24
+
25
+ const time2 = time();
26
+
27
+ expect(time2).toBeGreaterThan(time1);
28
+ expect(time2 - time1).toBeGreaterThanOrEqual(50);
29
+
30
+ stop();
31
+ });
32
+
33
+ test("time function should handle null event parameter", () => {
34
+ start();
35
+ const result = time(null);
36
+
37
+ expect(result).toBeGreaterThanOrEqual(0);
38
+ expect(typeof result).toBe("number");
39
+
40
+ stop();
41
+ });
42
+ });
package/tsconfig.json CHANGED
@@ -1,21 +1,12 @@
1
1
  {
2
+ "extends": "../../tsconfig.json",
2
3
  "compilerOptions": {
3
- "module": "esnext",
4
- "target": "es5",
5
- "lib": ["es6", "dom", "es2016", "es2017"],
6
- "moduleResolution": "node",
7
- "forceConsistentCasingInFileNames": true,
8
- "noImplicitReturns": true,
9
- "noUnusedLocals": true,
10
- "noUnusedParameters": true,
11
- "resolveJsonModule": true,
12
- "esModuleInterop": true,
13
4
  "baseUrl": ".",
14
5
  "paths": {
15
6
  "@src/*": ["src/*"],
16
7
  "@clarity-types/*": ["types/*"]
17
8
  }
18
9
  },
19
- "include":["src/**/*.ts","types/**/*.d.ts", "rollup.config.ts"],
20
- "exclude": ["test", "node_modules", "build"]
10
+ "include":["src/**/*.ts", "types/**/*.d.ts", "test/**/*.ts", "rollup.config.ts"],
11
+ "exclude": ["node_modules", "build"]
21
12
  }
package/types/data.d.ts CHANGED
@@ -1,17 +1,17 @@
1
1
  import { Time } from "@clarity-types/core";
2
- export type Target = number | Node;
3
- export type Token = string | number | number[] | string[] | (string | number)[];
4
- export type DecodedToken = any | any[];
2
+ export type Target = (number | Node);
3
+ export type Token = (string | number | number[] | string[] | (string | number)[]);
4
+ export type DecodedToken = (any | any[]);
5
5
 
6
6
  export type MetadataCallback = (data: Metadata, playback: boolean, consentStatus?: ConsentState) => void;
7
7
  export interface MetadataCallbackOptions {
8
- callback: MetadataCallback;
9
- wait: boolean;
10
- recall: boolean;
11
- called: boolean;
12
- consentInfo: boolean;
8
+ callback: MetadataCallback,
9
+ wait: boolean,
10
+ recall: boolean,
11
+ called: boolean,
12
+ consentInfo: boolean
13
13
  }
14
- export type SignalCallback = (data: ClaritySignal) => void;
14
+ export type SignalCallback = (data: ClaritySignal) => void
15
15
 
16
16
  /* Enum */
17
17
  export const enum Event {
@@ -87,7 +87,7 @@ export const enum Event {
87
87
  Keystrokes = 104,
88
88
  BackGesture = 105,
89
89
  WebViewStatus = 106,
90
- AppInstallReferrer = 107,
90
+ AppInstallReferrer = 107
91
91
  // 200-300 reserved for internal use
92
92
  }
93
93
 
@@ -182,7 +182,7 @@ export const enum Dimension {
182
182
  Timezone = 34,
183
183
  TimezoneOffset = 35,
184
184
  Consent = 36,
185
- InteractionNextPaint = 37,
185
+ InteractionNextPaint = 37
186
186
  // 200-300 reserved for internal use
187
187
  }
188
188
 
@@ -194,7 +194,7 @@ export const enum Check {
194
194
  Bytes = 4,
195
195
  Collection = 5,
196
196
  Server = 6,
197
- Page = 7,
197
+ Page = 7
198
198
  }
199
199
 
200
200
  export const enum Code {
@@ -218,29 +218,29 @@ export const enum Severity {
218
218
  Info = 0,
219
219
  Warning = 1,
220
220
  Error = 2,
221
- Fatal = 3,
221
+ Fatal = 3
222
222
  }
223
223
 
224
224
  export const enum Upload {
225
225
  Async = 0,
226
- Beacon = 1,
226
+ Beacon = 1
227
227
  }
228
228
 
229
229
  export const enum BooleanFlag {
230
230
  False = 0,
231
- True = 1,
231
+ True = 1
232
232
  }
233
233
 
234
234
  export const enum GCMConsent {
235
235
  Unknown = 0,
236
236
  Granted = 1,
237
- Denied = 2,
237
+ Denied = 2
238
238
  }
239
239
 
240
240
  export const enum IframeStatus {
241
241
  Unknown = 0,
242
242
  TopFrame = 1,
243
- Iframe = 2,
243
+ Iframe = 2
244
244
  }
245
245
 
246
246
  export const enum Setting {
@@ -291,11 +291,11 @@ export const enum Character {
291
291
  Blank = 32,
292
292
  Tab = 9,
293
293
  NewLine = 10,
294
- Return = 13,
294
+ Return = 13
295
295
  }
296
296
 
297
297
  export const enum ApplicationPlatform {
298
- WebApp = 0,
298
+ WebApp = 0
299
299
  }
300
300
 
301
301
  export const enum Constant {
@@ -380,27 +380,36 @@ export const enum XMLReadyState {
380
380
  Opened = 1,
381
381
  Headers_Recieved = 2,
382
382
  Loading = 3,
383
- Done = 4,
383
+ Done = 4
384
384
  }
385
385
 
386
386
  export const enum ConsentSource {
387
387
  Implicit = 0,
388
388
  API = 1,
389
389
  GCM = 2,
390
+ TCF = 3,
391
+ APIv1 = 4,
392
+ APIv2 = 5,
393
+ Cookie = 6,
394
+ Default = 7,
395
+ // 100-255 Reserved for CMP integration, both internal and external
396
+ ClarityShopifyPixel = 100,
397
+ ClarityShopifyApp = 101,
398
+ UET = 102
390
399
  }
391
400
 
392
401
  /* Helper Interfaces */
393
402
 
394
403
  export interface Payload {
395
- e: Token[] /* Envelope */;
396
- a: Token[][] /* Events that are used for data analysis */;
397
- p: Token[][] /* Events that are primarily used for session playback */;
404
+ e: Token[]; /* Envelope */
405
+ a: Token[][]; /* Events that are used for data analysis */
406
+ p: Token[][]; /* Events that are primarily used for session playback */
398
407
  }
399
408
 
400
409
  export interface EncodedPayload {
401
- e: string /* Envelope */;
402
- a: string /* Analytics Payload */;
403
- p: string /* Playback Payload */;
410
+ e: string; /* Envelope */
411
+ a: string; /* Analytics Payload */
412
+ p: string; /* Playback Payload */
404
413
  }
405
414
 
406
415
  export interface Metadata {
@@ -534,8 +543,8 @@ export interface UploadData {
534
543
  }
535
544
 
536
545
  export interface ClaritySignal {
537
- type: string;
538
- value?: number;
546
+ type: string
547
+ value?: number
539
548
  }
540
549
 
541
550
  export interface PerformanceEventTiming extends PerformanceEntry {
@@ -557,7 +566,7 @@ export interface ConsentState {
557
566
  export const enum ConsentType {
558
567
  None = 0,
559
568
  Implicit = 1,
560
- General = 2,
569
+ General = 2
561
570
  }
562
571
 
563
572
  export interface ConsentData {
package/types/index.d.ts CHANGED
@@ -11,12 +11,12 @@ interface Clarity {
11
11
  pause: () => void;
12
12
  resume: () => void;
13
13
  upgrade: (key: string) => void;
14
- consent: () => void;
15
- consentv2: () => void;
14
+ consent: (status?: boolean) => void;
15
+ consentv2: (consentState?: Data.ConsentState, source?: number) => void;
16
16
  event: (name: string, value: string) => void;
17
17
  set: (variable: string, value: string | string[]) => void;
18
18
  identify: (userId: string, sessionId?: string, pageId?: string, userHint?: string) => void;
19
- metadata: (callback: Data.MetadataCallback, wait?: boolean) => void;
19
+ metadata: (callback: Data.MetadataCallback, wait?: boolean, recall?: boolean, consentInfo?: boolean) => void;
20
20
  signal: (callback: Data.SignalCallback) => void;
21
21
  }
22
22
 
package/test/core.test.ts DELETED
@@ -1,139 +0,0 @@
1
- import { assert } from "chai";
2
- import { Browser, Page } from "playwright";
3
- import { changes, clicks, inputs, launch, markup, node, text } from "./helper";
4
- import { decode } from "@gemx-dev/clarity-decode";
5
-
6
- let browser: Browser;
7
- let page: Page;
8
-
9
- describe("Core Tests", () => {
10
- before(async () => {
11
- browser = await launch();
12
- });
13
-
14
- beforeEach(async () => {
15
- page = await browser.newPage();
16
- await page.goto("about:blank");
17
- });
18
-
19
- afterEach(async () => {
20
- await page.close();
21
- });
22
-
23
- after(async () => {
24
- await browser.close();
25
- browser = null;
26
- });
27
-
28
- it("should mask sensitive content by default", async () => {
29
- let encoded: string[] = await markup(page, "core.html");
30
- let decoded = encoded.map((x) => decode(x));
31
- let heading = text(decoded, "one");
32
- let address = text(decoded, "two");
33
- let email = node(decoded, "attributes.id", "eml");
34
- let password = node(decoded, "attributes.id", "pwd");
35
- let search = node(decoded, "attributes.id", "search");
36
- let card = node(decoded, "attributes.id", "cardnum");
37
- let option = text(decoded, "option1");
38
- let textarea = text(decoded, "textarea");
39
- let click = clicks(decoded)[0];
40
- let input = inputs(decoded)[0];
41
- let group = changes(decoded);
42
-
43
- // Non-sensitive fields continue to pass through with sensitive bits masked off
44
- assert.equal(heading, "Thanks for your order #▫▪▪▫▫▫▪▪");
45
-
46
- // Sensitive fields, including input fields, are randomized and masked
47
- assert.equal(address, "•••••• ••••• ••••• ••••• ••••• •••••");
48
- assert.equal(email.attributes.value, "••••• •••• •••• ••••");
49
- assert.equal(password.attributes.value, "••••");
50
- assert.equal(search.attributes.value, "••••• •••• ••••");
51
- assert.equal(card.attributes.value, "•••••");
52
- assert.equal(textarea, "••••• •••••");
53
- assert.equal(option, "• •••••");
54
-
55
- // Clicked text and input value should be consistent with uber masking configuration
56
- assert.equal(click.data.text, "Hello ▪▪▪▫▪");
57
- assert.equal(input.data.value, "••••• •••• •••• ••••");
58
- assert.equal(group.length, 2);
59
- // Search change - we should captured mangled input and hash
60
- assert.equal(group[0].data.type, "search");
61
- assert.equal(group[0].data.value, "••••• •••• •••• ••••");
62
- assert.equal(group[0].data.checksum, "4y7m6");
63
- // Password change - we should capture placholder value and empty hash
64
- assert.equal(group[1].data.type, "password");
65
- assert.equal(group[1].data.value, "••••");
66
- assert.equal(group[1].data.checksum, "");
67
- });
68
-
69
- it("should mask all text in strict mode", async () => {
70
- let encoded: string[] = await markup(page, "core.html", { content: false });
71
- let decoded = encoded.map((x) => decode(x));
72
- let heading = text(decoded, "one");
73
- let address = text(decoded, "two");
74
- let email = node(decoded, "attributes.id", "eml");
75
- let password = node(decoded, "attributes.id", "pwd");
76
- let search = node(decoded, "attributes.id", "search");
77
- let card = node(decoded, "attributes.id", "cardnum");
78
- let click = clicks(decoded)[0];
79
- let input = inputs(decoded)[0];
80
- let option = text(decoded, "option1");
81
-
82
- // All fields are randomized and masked
83
- assert.equal(heading, "• ••••• ••••• ••••• ••••• •••••");
84
- assert.equal(address, "•••••• ••••• ••••• ••••• ••••• •••••");
85
- assert.equal(email.attributes.value, "••••• •••• •••• ••••");
86
- assert.equal(password.attributes.value, "••••");
87
- assert.equal(search.attributes.value, "••••• •••• ••••");
88
- assert.equal(card.attributes.value, "•••••");
89
- assert.equal(option, "• •••••");
90
-
91
- // Clicked text and input value should also be masked in strict mode
92
- assert.equal(click.data.text, "••••• •••• ••••");
93
- assert.equal(input.data.value, "••••• •••• •••• ••••");
94
- });
95
-
96
- it("should unmask non-sensitive text in relaxed mode", async () => {
97
- let encoded: string[] = await markup(page, "core.html", { unmask: ["body"] });
98
- let decoded = encoded.map((x) => decode(x));
99
- let heading = text(decoded, "one");
100
- let address = text(decoded, "two");
101
- let email = node(decoded, "attributes.id", "eml");
102
- let password = node(decoded, "attributes.id", "pwd");
103
- let search = node(decoded, "attributes.id", "search");
104
- let card = node(decoded, "attributes.id", "cardnum");
105
- let click = clicks(decoded)[0];
106
- let input = inputs(decoded)[0];
107
- let option = text(decoded, "option1");
108
-
109
- // Text flows through unmasked for non-sensitive fields, with exception of input fields
110
- assert.equal(heading, "Thanks for your order #2AB700GH");
111
- assert.equal(address, "1 Microsoft Way, Redmond, WA - 98052");
112
- assert.equal(search.attributes.value, "••••• •••• ••••");
113
- assert.equal(option, "• •••••");
114
-
115
- // Sensitive fields are still masked
116
- assert.equal(email.attributes.value, "••••• •••• •••• ••••");
117
- assert.equal(password.attributes.value, "••••");
118
- assert.equal(card.attributes.value, "•••••");
119
-
120
- // Clicked text comes through unmasked in relaxed mode but input is still masked
121
- assert.equal(click.data.text, "Hello Wor1d");
122
- assert.equal(input.data.value, "••••• •••• •••• ••••");
123
- });
124
-
125
- it("should respect mask config even in relaxed mode", async () => {
126
- let encoded: string[] = await markup(page, "core.html", { mask: ["#mask"], unmask: ["body"] });
127
- let decoded = encoded.map((x) => decode(x));
128
- let subtree = text(decoded, "child");
129
- let click = clicks(decoded)[0];
130
- let input = inputs(decoded)[0];
131
-
132
- // Masked sub-trees continue to stay masked
133
- assert.equal(subtree, "••••• •••••");
134
-
135
- // Clicked text is masked due to masked configuration and input value is also masked
136
- assert.equal(click.data.text, "••••• •••• ••••");
137
- assert.equal(input.data.value, "••••• •••• •••• ••••");
138
- });
139
- });
package/test/helper.ts DELETED
@@ -1,167 +0,0 @@
1
- import { Core, Data, decode, Interaction, Layout } from "@gemx-dev/clarity-decode";
2
- import * as fs from "fs";
3
- import * as url from "url";
4
- import * as path from "path";
5
- import { Browser, Page, chromium } from "playwright";
6
-
7
- export async function launch(): Promise<Browser> {
8
- return chromium.launch({ headless: false, args: ["--no-sandbox"] });
9
- }
10
-
11
- export async function markup(page: Page, file: string, override: Core.Config = null): Promise<string[]> {
12
- const htmlPath = path.resolve(__dirname, `./html/${file}`);
13
- const htmlFileUrl = url.pathToFileURL(htmlPath).toString();
14
- const html = fs.readFileSync(htmlPath, "utf8");
15
- await Promise.all([page.goto(htmlFileUrl), page.waitForNavigation()]);
16
- await page.setContent(
17
- html.replace(
18
- "</body>",
19
- `
20
- <script>
21
- window.payloads = [];
22
- ${fs.readFileSync(path.resolve(__dirname, `../build/clarity.min.js`), "utf8")};
23
- clarity("start", ${config(override)});
24
- </script>
25
- </body>
26
- `
27
- )
28
- );
29
- await page.hover("#two");
30
- await page.click("#child");
31
- await page.locator("#search").fill("");
32
- await page.locator("#search").type("query with numb3rs");
33
- await page.locator("#pwd").type("p1ssw0rd");
34
- await page.locator("#eml").fill("");
35
- await page.locator("#eml").type("hello@world.com");
36
- await page.waitForFunction("payloads && payloads.length > 2");
37
- return await page.evaluate("payloads");
38
- }
39
-
40
- export function clicks(decoded: Data.DecodedPayload[]): Interaction.ClickEvent[] {
41
- let output: Interaction.ClickEvent[] = [];
42
- for (let i = decoded.length - 1; i >= 0; i--) {
43
- if (decoded[i].click) {
44
- for (let j = 0; j < decoded[i].click.length; j++) {
45
- output.push(decoded[i].click[j]);
46
- }
47
- }
48
- }
49
- return output;
50
- }
51
-
52
- export function inputs(decoded: Data.DecodedPayload[]): Interaction.InputEvent[] {
53
- let output: Interaction.InputEvent[] = [];
54
- for (let i = decoded.length - 1; i >= 0; i--) {
55
- if (decoded[i].input) {
56
- for (let j = 0; j < decoded[i].input.length; j++) {
57
- output.push(decoded[i].input[j]);
58
- }
59
- }
60
- }
61
- return output;
62
- }
63
-
64
- export function changes(decoded: Data.DecodedPayload[]): Interaction.ChangeEvent[] {
65
- let output: Interaction.ChangeEvent[] = [];
66
- for (let i = decoded.length - 1; i >= 0; i--) {
67
- if (decoded[i].change) {
68
- for (let j = 0; j < decoded[i].change.length; j++) {
69
- output.push(decoded[i].change[j]);
70
- }
71
- }
72
- }
73
- return output;
74
- }
75
-
76
- export function node(
77
- decoded: Data.DecodedPayload[],
78
- key: string,
79
- value: string | number,
80
- tag: string = null
81
- ): Layout.DomData {
82
- let sub = null;
83
-
84
- // Exploding nested keys into key and sub key
85
- if (key.indexOf(".") > 0) {
86
- const parts = key.split(".");
87
- if (parts.length === 2) {
88
- key = parts[0];
89
- sub = parts[1];
90
- }
91
- }
92
-
93
- // Walking over the decoded payload to find the right match
94
- for (let i = decoded.length - 1; i >= 0; i--) {
95
- if (decoded[i].dom) {
96
- for (let j = 0; j < decoded[i].dom.length; j++) {
97
- if (decoded[i].dom[j].data) {
98
- for (let k = 0; k < decoded[i].dom[j].data.length; k++) {
99
- let d = decoded[i].dom[j].data[k];
100
- if ((sub && d[key] && d[key][sub] === value) || (d[key] && d[key] === value)) {
101
- if ((tag && d.tag === tag) || tag === null) {
102
- return d;
103
- }
104
- }
105
- }
106
- }
107
- }
108
- }
109
- }
110
- return null;
111
- }
112
-
113
- export function text(decoded: Data.DecodedPayload[], id: string): string {
114
- let parent = node(decoded, "attributes.id", id);
115
- if (parent) {
116
- let child = node(decoded, "parent", parent.id, "*T");
117
- if (child && child.value) {
118
- return child.value;
119
- }
120
- }
121
- return null;
122
- }
123
-
124
- function config(override: Core.Config): string {
125
- const settings = {
126
- delay: 100,
127
- content: true,
128
- fraud: [],
129
- regions: [],
130
- mask: [],
131
- unmask: [],
132
- upload: (payload) => {
133
- window["payloads"].push(payload);
134
- window["clarity"]("upgrade", "test");
135
- },
136
- };
137
-
138
- // Process overrides
139
- if (override) {
140
- for (let key of Object.keys(override)) {
141
- settings[key] = override[key];
142
- }
143
- }
144
-
145
- // Serialize configuration
146
- let output = "";
147
- for (let key of Object.keys(settings)) {
148
- switch (key) {
149
- case "upload":
150
- output += `${JSON.stringify(key)}: ${settings[key].toString()},`;
151
- break;
152
- case "projectId":
153
- case "mask":
154
- case "unmask":
155
- case "regions":
156
- case "cookies":
157
- case "fraud":
158
- output += `${JSON.stringify(key)}: ${JSON.stringify(settings[key])},`;
159
- break;
160
- default:
161
- output += `${JSON.stringify(key)}: ${settings[key]},`;
162
- break;
163
- }
164
- }
165
- output += `"projectId": "test"`;
166
- return "{" + output + "}";
167
- }
@@ -1,34 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Core Tests</title>
5
- <style>
6
- input,
7
- textarea,
8
- select {
9
- margin: 10px;
10
- display: block;
11
- }
12
- </style>
13
- </head>
14
- <body>
15
- <div>
16
- <h1 id="one">Thanks for your order #2AB700GH</h1>
17
- <p id="two" class="address-details">1 Microsoft Way, Redmond, WA - 98052</p>
18
- </div>
19
- <div id="mask">
20
- <span id="child">Hello Wor1d</span>
21
- </div>
22
- <form name="login">
23
- <input type="email" id="eml" title="Email" value="random@email.test" />
24
- <input type="password" id="pwd" title="Password" maxlength="16" value="passw0rd" />
25
- <input type="search" id="search" title="Search" value="hello w0rld" />
26
- <input type="text" id="cardnum" title="CC" value="1234" />
27
- <textarea id="textarea" autocapitalize="off" role="combobox" rows="5" placeholder="" spellcheck="false">Hell0 World</textarea>
28
- <select id="select" ng-options="origin.code" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
29
- <option id="option1" label="Halifax" value="string:YHZ">Halifax</option>
30
- <option id="option2" label="Montréal" value="string:YUL" selected="selected">Montréal</option>
31
- </select>
32
- </form>
33
- </body>
34
- </html>
package/test/stub.test.ts DELETED
@@ -1,7 +0,0 @@
1
- import { assert } from 'chai';
2
-
3
- describe('Stub Tests! Replace with working real tests!', () => {
4
- it('should be a successful test', () => {
5
- assert.isTrue(true);
6
- });
7
- });
@@ -1,6 +0,0 @@
1
- {
2
- "extends": "../tsconfig.json",
3
- "compilerOptions": {
4
- "module": "commonjs"
5
- }
6
- }