@gemx-dev/clarity-js 0.8.67 → 0.8.69
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 +181 -63
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +181 -63
- package/build/clarity.performance.js +1 -1
- package/package.json +1 -1
- package/src/core/config.ts +1 -0
- package/src/core/history.ts +28 -17
- package/src/core/scrub.ts +1 -4
- package/src/core/version.ts +1 -1
- package/src/data/consent.ts +9 -4
- package/src/data/dimension.ts +1 -1
- package/src/data/encode.ts +4 -4
- package/src/data/metadata.ts +21 -7
- package/src/insight/encode.ts +5 -2
- package/src/interaction/change.ts +3 -3
- package/src/interaction/click.ts +73 -6
- package/src/interaction/encode.ts +1 -0
- package/src/layout/constants.ts +13 -0
- package/src/layout/custom.ts +14 -9
- package/src/layout/dom.ts +10 -9
- package/src/layout/encode.ts +5 -2
- package/src/layout/index.ts +4 -2
- package/src/layout/mutation.ts +5 -5
- package/src/layout/selector.ts +2 -1
- package/test/consentv2.test.ts +160 -15
- package/types/core.d.ts +1 -0
- package/types/data.d.ts +1 -0
- package/types/interaction.d.ts +9 -0
- package/types/layout.d.ts +1 -8
- package/types/performance.d.ts +3 -0
package/src/layout/dom.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Privacy } from "@clarity-types/core";
|
|
2
2
|
import { Code, Setting, Severity } from "@clarity-types/data";
|
|
3
|
-
import { Constant,
|
|
3
|
+
import { Constant, NodeInfo, NodeMeta, NodeValue, Selector, SelectorInput, Source } from "@clarity-types/layout";
|
|
4
4
|
import config from "@src/core/config";
|
|
5
5
|
import { bind } from "@src/core/event";
|
|
6
6
|
import hash from "@src/core/hash";
|
|
@@ -9,6 +9,7 @@ import * as internal from "@src/diagnostic/internal";
|
|
|
9
9
|
import { removeObserver } from "@src/layout/node";
|
|
10
10
|
import * as region from "@src/layout/region";
|
|
11
11
|
import * as selector from "@src/layout/selector";
|
|
12
|
+
import { MaskTextList, MaskDisableList, MaskExcludeList, MaskTagsList } from "./constants";
|
|
12
13
|
let index: number = 1;
|
|
13
14
|
let nodesMap: Map<Number, Node> = null; // Maps id => node to retrieve further node details using id.
|
|
14
15
|
let values: NodeValue[] = [];
|
|
@@ -16,10 +17,10 @@ let updateMap: number[] = [];
|
|
|
16
17
|
let hashMap: { [hash: string]: number } = {};
|
|
17
18
|
let override = [];
|
|
18
19
|
let unmask = [];
|
|
19
|
-
let maskText = [];
|
|
20
|
-
let maskExclude = [];
|
|
21
|
-
let maskDisable = [];
|
|
22
|
-
let maskTags = [];
|
|
20
|
+
let maskText: string[] = [];
|
|
21
|
+
let maskExclude: string[] = [];
|
|
22
|
+
let maskDisable: string[] = [];
|
|
23
|
+
let maskTags: string[] = [];
|
|
23
24
|
|
|
24
25
|
// The WeakMap object is a collection of key/value pairs in which the keys are weakly referenced
|
|
25
26
|
let idMap: WeakMap<Node, number> = null; // Maps node => id.
|
|
@@ -44,10 +45,10 @@ function reset(): void {
|
|
|
44
45
|
hashMap = {};
|
|
45
46
|
override = [];
|
|
46
47
|
unmask = [];
|
|
47
|
-
maskText =
|
|
48
|
-
maskExclude =
|
|
49
|
-
maskDisable =
|
|
50
|
-
maskTags =
|
|
48
|
+
maskText = MaskTextList;
|
|
49
|
+
maskExclude = MaskExcludeList;
|
|
50
|
+
maskDisable = MaskDisableList;
|
|
51
|
+
maskTags = MaskTagsList;
|
|
51
52
|
nodesMap = new Map();
|
|
52
53
|
idMap = new WeakMap();
|
|
53
54
|
iframeMap = new WeakMap();
|
package/src/layout/encode.ts
CHANGED
|
@@ -105,7 +105,7 @@ export default async function (type: Event, timer: Timer = null, ts: number = nu
|
|
|
105
105
|
case "attributes":
|
|
106
106
|
for (let attr in data[key]) {
|
|
107
107
|
if (data[key][attr] !== undefined) {
|
|
108
|
-
tokens.push(attribute(attr, data[key][attr], privacy));
|
|
108
|
+
tokens.push(attribute(attr, data[key][attr], privacy, data.tag));
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
break;
|
|
@@ -149,6 +149,9 @@ function str(input: number): string {
|
|
|
149
149
|
return input.toString(36);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function attribute(key: string, value: string, privacy: Privacy): string {
|
|
152
|
+
function attribute(key: string, value: string, privacy: Privacy, tag: string): string {
|
|
153
|
+
if (key === Constant.Href && tag === Constant.LinkTag) {
|
|
154
|
+
return `${key}=${value}`;
|
|
155
|
+
}
|
|
153
156
|
return `${key}=${scrub.text(value, key.indexOf(Constant.DataAttribute) === 0 ? Constant.DataAttribute : key, privacy)}`;
|
|
154
157
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -20,17 +20,19 @@ export function start(): void {
|
|
|
20
20
|
region.start();
|
|
21
21
|
dom.start();
|
|
22
22
|
if (config.delayDom) {
|
|
23
|
-
// Lazy load layout module as part of page load time performance improvements experiment
|
|
23
|
+
// Lazy load layout module as part of page load time performance improvements experiment
|
|
24
24
|
bind(window, 'load', () => {
|
|
25
25
|
mutation.start();
|
|
26
26
|
});
|
|
27
27
|
} else {
|
|
28
28
|
mutation.start();
|
|
29
29
|
}
|
|
30
|
+
// IMPORTANT: Start custom element detection BEFORE discover
|
|
31
|
+
// This ensures pre-existing custom elements are registered before DOM traversal
|
|
32
|
+
custom.start();
|
|
30
33
|
discover.start();
|
|
31
34
|
style.start();
|
|
32
35
|
animation.start();
|
|
33
|
-
custom.start();
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export function stop(): void {
|
package/src/layout/mutation.ts
CHANGED
|
@@ -352,7 +352,7 @@ function proxyStyleRules(win: any): void {
|
|
|
352
352
|
// by injecting CSS using insertRule API vs. appending text node. A side effect of
|
|
353
353
|
// using javascript API is that it doesn't trigger DOM mutation and therefore we
|
|
354
354
|
// need to override the insertRule API and listen for changes manually.
|
|
355
|
-
if (win.clarityOverrides.InsertRule === undefined) {
|
|
355
|
+
if ("CSSStyleSheet" in win && win.CSSStyleSheet && win.CSSStyleSheet.prototype && win.clarityOverrides.InsertRule === undefined) {
|
|
356
356
|
win.clarityOverrides.InsertRule = win.CSSStyleSheet.prototype.insertRule;
|
|
357
357
|
win.CSSStyleSheet.prototype.insertRule = function (): number {
|
|
358
358
|
if (core.active()) {
|
|
@@ -362,7 +362,7 @@ function proxyStyleRules(win: any): void {
|
|
|
362
362
|
};
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
-
if ("CSSMediaRule" in win && win.clarityOverrides.MediaInsertRule === undefined) {
|
|
365
|
+
if ("CSSMediaRule" in win && win.CSSMediaRule && win.CSSMediaRule.prototype && win.clarityOverrides.MediaInsertRule === undefined) {
|
|
366
366
|
win.clarityOverrides.MediaInsertRule = win.CSSMediaRule.prototype.insertRule;
|
|
367
367
|
win.CSSMediaRule.prototype.insertRule = function (): number {
|
|
368
368
|
if (core.active()) {
|
|
@@ -372,7 +372,7 @@ function proxyStyleRules(win: any): void {
|
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
if (win.clarityOverrides.DeleteRule === undefined) {
|
|
375
|
+
if ("CSSStyleSheet" in win && win.CSSStyleSheet && win.CSSStyleSheet.prototype && win.clarityOverrides.DeleteRule === undefined) {
|
|
376
376
|
win.clarityOverrides.DeleteRule = win.CSSStyleSheet.prototype.deleteRule;
|
|
377
377
|
win.CSSStyleSheet.prototype.deleteRule = function (): void {
|
|
378
378
|
if (core.active()) {
|
|
@@ -382,7 +382,7 @@ function proxyStyleRules(win: any): void {
|
|
|
382
382
|
};
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
if ("CSSMediaRule" in win && win.clarityOverrides.MediaDeleteRule === undefined) {
|
|
385
|
+
if ("CSSMediaRule" in win && win.CSSMediaRule && win.CSSMediaRule.prototype && win.clarityOverrides.MediaDeleteRule === undefined) {
|
|
386
386
|
win.clarityOverrides.MediaDeleteRule = win.CSSMediaRule.prototype.deleteRule;
|
|
387
387
|
win.CSSMediaRule.prototype.deleteRule = function (): void {
|
|
388
388
|
if (core.active()) {
|
|
@@ -395,7 +395,7 @@ function proxyStyleRules(win: any): void {
|
|
|
395
395
|
// Add a hook to attachShadow API calls
|
|
396
396
|
// In case we are unable to add a hook and browser throws an exception,
|
|
397
397
|
// reset attachShadow variable and resume processing like before
|
|
398
|
-
if (win.clarityOverrides.AttachShadow === undefined) {
|
|
398
|
+
if ("Element" in win && win.Element && win.Element.prototype && win.clarityOverrides.AttachShadow === undefined) {
|
|
399
399
|
win.clarityOverrides.AttachShadow = win.Element.prototype.attachShadow;
|
|
400
400
|
try {
|
|
401
401
|
win.Element.prototype.attachShadow = function (): ShadowRoot {
|
package/src/layout/selector.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Character } from "../../types/data";
|
|
2
2
|
import { Constant, Selector, SelectorInput } from "../../types/layout";
|
|
3
|
+
import { ExcludeClassNamesList } from "./constants";
|
|
3
4
|
|
|
4
|
-
const excludeClassNames =
|
|
5
|
+
const excludeClassNames = ExcludeClassNamesList;
|
|
5
6
|
|
|
6
7
|
let extraExcludeClassNames: string[] = [];
|
|
7
8
|
let selectorMap: { [selector: string]: number[] } = {};
|
package/test/consentv2.test.ts
CHANGED
|
@@ -57,9 +57,10 @@ const clarityJsPath = join(__dirname, "../build/clarity.min.js");
|
|
|
57
57
|
/**
|
|
58
58
|
* Sets up a cookie mock for data: URLs which don't support cookies natively.
|
|
59
59
|
* Handles both cookie setting and deletion (via max-age or empty values).
|
|
60
|
+
* @param initialCookieValue - Optional initial cookie value (e.g., "marketing_id=abc123")
|
|
60
61
|
*/
|
|
61
|
-
function setupCookieMock() {
|
|
62
|
-
let cookieStore = "";
|
|
62
|
+
function setupCookieMock(initialCookieValue?: string): void {
|
|
63
|
+
let cookieStore = initialCookieValue || "";
|
|
63
64
|
Object.defineProperty(document, "cookie", {
|
|
64
65
|
get: () => cookieStore,
|
|
65
66
|
set: (value: string) => {
|
|
@@ -101,7 +102,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
101
102
|
// ========================
|
|
102
103
|
|
|
103
104
|
test("implicit denied: track=false results in denied consent", async ({ page }) => {
|
|
104
|
-
await page.evaluate(setupCookieMock);
|
|
105
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
105
106
|
|
|
106
107
|
// Verify initial state (before Clarity starts) - no cookies should exist
|
|
107
108
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -263,7 +264,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
263
264
|
});
|
|
264
265
|
|
|
265
266
|
test("consentv2 explicit denial: track=false → denied/denied remains without cookies", async ({ page }) => {
|
|
266
|
-
await page.evaluate(setupCookieMock);
|
|
267
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
267
268
|
|
|
268
269
|
// Verify initial state - no cookies before Clarity starts
|
|
269
270
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -333,7 +334,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
333
334
|
});
|
|
334
335
|
|
|
335
336
|
test("consentv2 mixed consent: track=false → denied analytics, granted ads no cookies", async ({ page }) => {
|
|
336
|
-
await page.evaluate(setupCookieMock);
|
|
337
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
337
338
|
|
|
338
339
|
// Verify initial state - no cookies before Clarity starts
|
|
339
340
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -400,7 +401,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
400
401
|
});
|
|
401
402
|
|
|
402
403
|
test("consentv2 mixed consent: track=false → granted analytics, denied ads sets cookies", async ({ page }) => {
|
|
403
|
-
await page.evaluate(setupCookieMock);
|
|
404
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
404
405
|
|
|
405
406
|
// Verify initial state - no cookies before Clarity starts
|
|
406
407
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -468,7 +469,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
468
469
|
});
|
|
469
470
|
|
|
470
471
|
test("consentv2 grants consent: track=false → granted/granted sets cookies", async ({ page }) => {
|
|
471
|
-
await page.evaluate(setupCookieMock);
|
|
472
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
472
473
|
|
|
473
474
|
// Verify initial state - no cookies before Clarity starts
|
|
474
475
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -573,7 +574,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
573
574
|
// ========================
|
|
574
575
|
|
|
575
576
|
test("implicit granted: track=true results in granted consent", async ({ page }) => {
|
|
576
|
-
await page.evaluate(setupCookieMock);
|
|
577
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
577
578
|
|
|
578
579
|
// Verify initial state - no cookies before Clarity starts
|
|
579
580
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -648,7 +649,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
648
649
|
});
|
|
649
650
|
|
|
650
651
|
test("consentv2 revokes consent: track=true → denied/denied deletes cookies", async ({ page }) => {
|
|
651
|
-
await page.evaluate(setupCookieMock);
|
|
652
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
652
653
|
|
|
653
654
|
// Verify initial state - no cookies before Clarity starts
|
|
654
655
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -750,7 +751,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
750
751
|
});
|
|
751
752
|
|
|
752
753
|
test("consentv2 mixed consent: track=true → granted analytics, denied ads keeps cookies", async ({ page }) => {
|
|
753
|
-
await page.evaluate(setupCookieMock);
|
|
754
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
754
755
|
|
|
755
756
|
// Verify initial state - no cookies before Clarity starts
|
|
756
757
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -817,7 +818,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
817
818
|
});
|
|
818
819
|
|
|
819
820
|
test("consentv2 mixed consent: track=true → denied analytics, granted ads deletes cookies", async ({ page }) => {
|
|
820
|
-
await page.evaluate(setupCookieMock);
|
|
821
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
821
822
|
|
|
822
823
|
// Verify initial state - no cookies before Clarity starts
|
|
823
824
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -887,7 +888,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
887
888
|
});
|
|
888
889
|
|
|
889
890
|
test("consentv2 maintains consent: track=true → granted/granted keeps cookies", async ({ page }) => {
|
|
890
|
-
await page.evaluate(setupCookieMock);
|
|
891
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
891
892
|
|
|
892
893
|
// Verify initial state - no cookies before Clarity starts
|
|
893
894
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -998,7 +999,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
998
999
|
// ========================
|
|
999
1000
|
|
|
1000
1001
|
test("consent v1: track=false → consent(true) grants consent and sets cookies", async ({ page }) => {
|
|
1001
|
-
await page.evaluate(setupCookieMock);
|
|
1002
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
1002
1003
|
|
|
1003
1004
|
// Verify initial state - no cookies before Clarity starts
|
|
1004
1005
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -1064,7 +1065,7 @@ test.describe("consentv2 - Production API", () => {
|
|
|
1064
1065
|
});
|
|
1065
1066
|
|
|
1066
1067
|
test("consent v1: track=true → consent(false) revokes consent and deletes cookies", async ({ page }) => {
|
|
1067
|
-
await page.evaluate(setupCookieMock);
|
|
1068
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
1068
1069
|
|
|
1069
1070
|
// Verify initial state - no cookies before Clarity starts
|
|
1070
1071
|
const initialState = await page.evaluate(({ sessionKey, cookieKey }) => {
|
|
@@ -1160,5 +1161,149 @@ test.describe("consentv2 - Production API", () => {
|
|
|
1160
1161
|
expect(consentResult.clskCookieValue).toBe("");
|
|
1161
1162
|
expect(consentResult.clckCookieValue).toBe("");
|
|
1162
1163
|
});
|
|
1163
|
-
});
|
|
1164
1164
|
|
|
1165
|
+
// ========================
|
|
1166
|
+
// Config cookies tests
|
|
1167
|
+
// ========================
|
|
1168
|
+
|
|
1169
|
+
interface ConfigCookieTestOptions {
|
|
1170
|
+
track: boolean;
|
|
1171
|
+
grantConsentAfterStart?: boolean;
|
|
1172
|
+
denyConsentAfterStart?: boolean;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
interface ConfigCookieTestResult {
|
|
1176
|
+
containsCookieValue: boolean;
|
|
1177
|
+
beforeChangeContainsCookie?: boolean;
|
|
1178
|
+
afterChangeContainsCookie?: boolean;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* Helper to run config cookie tests with consistent setup.
|
|
1183
|
+
* Sets up cookie mock with marketing_id=abc123 and returns whether the cookie value appears in payloads.
|
|
1184
|
+
*/
|
|
1185
|
+
async function runConfigCookieTest(page: any, options: ConfigCookieTestOptions): Promise<ConfigCookieTestResult> {
|
|
1186
|
+
// Set up cookie mock with initial marketing cookie before evaluating test logic
|
|
1187
|
+
await page.evaluate(setupCookieMock as (initialValue: string) => void, "marketing_id=abc123");
|
|
1188
|
+
|
|
1189
|
+
const result = await page.evaluate((opts: ConfigCookieTestOptions) => {
|
|
1190
|
+
|
|
1191
|
+
return new Promise((resolve) => {
|
|
1192
|
+
const payloadsBefore: string[] = [];
|
|
1193
|
+
const payloadsAfter: string[] = [];
|
|
1194
|
+
let phase = "before";
|
|
1195
|
+
|
|
1196
|
+
(window as any).clarity("start", {
|
|
1197
|
+
projectId: "test",
|
|
1198
|
+
track: opts.track,
|
|
1199
|
+
cookies: ["marketing_id"],
|
|
1200
|
+
upload: (payload: string) => {
|
|
1201
|
+
if (phase === "before") {
|
|
1202
|
+
payloadsBefore.push(payload);
|
|
1203
|
+
} else {
|
|
1204
|
+
payloadsAfter.push(payload);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
const hasConsentChange = opts.grantConsentAfterStart || opts.denyConsentAfterStart;
|
|
1210
|
+
// For denial tests, use longer delay to ensure initial upload completes before denial
|
|
1211
|
+
const consentChangeDelay = opts.denyConsentAfterStart
|
|
1212
|
+
? (window as any).CONSENT_CALLBACK_TIMEOUT / 2
|
|
1213
|
+
: (window as any).COOKIE_SETUP_DELAY;
|
|
1214
|
+
|
|
1215
|
+
if (hasConsentChange) {
|
|
1216
|
+
setTimeout(() => {
|
|
1217
|
+
phase = "after";
|
|
1218
|
+
const adStorage = opts.grantConsentAfterStart ? "granted" : "denied";
|
|
1219
|
+
const analyticsStorage = opts.grantConsentAfterStart ? "granted" : "denied";
|
|
1220
|
+
(window as any).clarity("consentv2", { ad_Storage: adStorage, analytics_Storage: analyticsStorage });
|
|
1221
|
+
}, consentChangeDelay);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// For denial tests, wait longer to allow for restart
|
|
1225
|
+
const totalTimeout = opts.denyConsentAfterStart
|
|
1226
|
+
? consentChangeDelay + (window as any).CONSENT_CALLBACK_TIMEOUT
|
|
1227
|
+
: (window as any).CONSENT_CALLBACK_TIMEOUT;
|
|
1228
|
+
|
|
1229
|
+
setTimeout(() => {
|
|
1230
|
+
const allPayloads = [...payloadsBefore, ...payloadsAfter].join("");
|
|
1231
|
+
resolve({
|
|
1232
|
+
containsCookieValue: allPayloads.includes("abc123"),
|
|
1233
|
+
beforeChangeContainsCookie: payloadsBefore.join("").includes("abc123"),
|
|
1234
|
+
afterChangeContainsCookie: payloadsAfter.join("").includes("abc123")
|
|
1235
|
+
});
|
|
1236
|
+
}, totalTimeout);
|
|
1237
|
+
});
|
|
1238
|
+
}, options);
|
|
1239
|
+
|
|
1240
|
+
return result as ConfigCookieTestResult;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
test("config cookies: track=true logs config cookies as variables", async ({ page }) => {
|
|
1244
|
+
const result = await runConfigCookieTest(page, { track: true });
|
|
1245
|
+
expect(result.containsCookieValue).toBe(true);
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
test("config cookies: track=false does not log config cookies", async ({ page }) => {
|
|
1249
|
+
const result = await runConfigCookieTest(page, { track: false });
|
|
1250
|
+
expect(result.containsCookieValue).toBe(false);
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
test("config cookies: track=false then consentv2 grants consent logs config cookies", async ({ page }) => {
|
|
1254
|
+
const result = await runConfigCookieTest(page, { track: false, grantConsentAfterStart: true });
|
|
1255
|
+
expect(result.containsCookieValue).toBe(true);
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
test("config cookies: track=true then consentv2 denies consent does not log cookies after restart", async ({ page }) => {
|
|
1259
|
+
const result = await runConfigCookieTest(page, { track: true, denyConsentAfterStart: true });
|
|
1260
|
+
// Before denial, cookies should be logged (track=true, implicit granted)
|
|
1261
|
+
expect(result.beforeChangeContainsCookie).toBe(true);
|
|
1262
|
+
// After restart with denied consent, cookies should NOT be logged
|
|
1263
|
+
expect(result.afterChangeContainsCookie).toBe(false);
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
test("GCM error handling: handles misconfigured GTM gracefully", async ({ page }) => {
|
|
1267
|
+
await page.evaluate(setupCookieMock as () => void);
|
|
1268
|
+
|
|
1269
|
+
const result = await page.evaluate(({ clarityJs }) => {
|
|
1270
|
+
return new Promise<{ errorWasThrown: boolean }>((resolve) => {
|
|
1271
|
+
let errorWasThrown = false;
|
|
1272
|
+
|
|
1273
|
+
// Mock GTM with misconfigured consent state
|
|
1274
|
+
(window as any).google_tag_data = {
|
|
1275
|
+
ics: {
|
|
1276
|
+
addListener: function(_events: string[], callback: () => void) {
|
|
1277
|
+
setTimeout(() => callback(), 100);
|
|
1278
|
+
},
|
|
1279
|
+
// getConsentState throws error due to uninitialized internal state
|
|
1280
|
+
getConsentState: function() {
|
|
1281
|
+
errorWasThrown = true;
|
|
1282
|
+
throw new TypeError("Cannot read properties of undefined (reading 'usedContainerScopedDefaults')");
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
|
|
1287
|
+
// Load and execute Clarity
|
|
1288
|
+
eval(clarityJs);
|
|
1289
|
+
|
|
1290
|
+
// Start Clarity with track=true
|
|
1291
|
+
(window as any).clarity("start", {
|
|
1292
|
+
projectId: "test",
|
|
1293
|
+
track: true,
|
|
1294
|
+
upload: false
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
// Wait for callback to be triggered and error to be thrown
|
|
1298
|
+
setTimeout(() => {
|
|
1299
|
+
resolve({
|
|
1300
|
+
errorWasThrown
|
|
1301
|
+
});
|
|
1302
|
+
}, 500);
|
|
1303
|
+
});
|
|
1304
|
+
}, { clarityJs: readFileSync(clarityJsPath, "utf-8") });
|
|
1305
|
+
|
|
1306
|
+
// Verify error was thrown and handled gracefully (test success proves it was caught)
|
|
1307
|
+
expect(result.errorWasThrown).toBe(true);
|
|
1308
|
+
});
|
|
1309
|
+
});
|
package/types/core.d.ts
CHANGED
package/types/data.d.ts
CHANGED
|
@@ -264,6 +264,7 @@ export const enum Setting {
|
|
|
264
264
|
PlaybackBytesLimit = 10 * 1024 * 1024, // 10MB
|
|
265
265
|
CollectionLimit = 128, // Number of unique entries for dimensions
|
|
266
266
|
ClickPrecision = 32767, // 2^15 - 1
|
|
267
|
+
ClickParentTraversal = 10, // Maximum number of parent elements to traverse when computing relative click coordinates
|
|
267
268
|
BoxPrecision = 100, // Up to 2 decimal points (e.g. 34.56)
|
|
268
269
|
ScriptErrorLimit = 5, // Do not send the same script error more than 5 times per page
|
|
269
270
|
DimensionLimit = 256, // Do not extract dimensions which are over 256 characters
|
package/types/interaction.d.ts
CHANGED
|
@@ -11,6 +11,14 @@ export const enum BrowsingContext {
|
|
|
11
11
|
Top = 3
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export const enum ClickSource {
|
|
15
|
+
Undefined = 0,
|
|
16
|
+
FirstParty = 1,
|
|
17
|
+
ThirdParty = 2,
|
|
18
|
+
Eval = 3,
|
|
19
|
+
Unknown = 4
|
|
20
|
+
}
|
|
21
|
+
|
|
14
22
|
export const enum Setting {
|
|
15
23
|
LookAhead = 500, // 500ms
|
|
16
24
|
InputLookAhead = 1000, // 1s
|
|
@@ -131,6 +139,7 @@ export interface ClickData {
|
|
|
131
139
|
tag: string;
|
|
132
140
|
class: string;
|
|
133
141
|
id: string;
|
|
142
|
+
source: ClickSource;
|
|
134
143
|
}
|
|
135
144
|
|
|
136
145
|
export interface TextInfo {
|
package/types/layout.d.ts
CHANGED
|
@@ -56,13 +56,6 @@ export const enum RegionVisibility {
|
|
|
56
56
|
ScrolledToEnd = 13
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export const enum Mask {
|
|
60
|
-
Text = "address,password,contact",
|
|
61
|
-
Disable = "radio,checkbox,range,button,reset,submit",
|
|
62
|
-
Exclude = "password,secret,pass,social,ssn,code,hidden",
|
|
63
|
-
Tags = "INPUT,SELECT,TEXTAREA"
|
|
64
|
-
}
|
|
65
|
-
|
|
66
59
|
export const enum Constant {
|
|
67
60
|
Empty = "",
|
|
68
61
|
SvgPrefix = "svg:",
|
|
@@ -95,6 +88,7 @@ export const enum Constant {
|
|
|
95
88
|
Object = "object",
|
|
96
89
|
Function = "function",
|
|
97
90
|
StyleTag = "STYLE",
|
|
91
|
+
LinkTag = "LINK",
|
|
98
92
|
InputTag = "INPUT",
|
|
99
93
|
IFrameTag = "IFRAME",
|
|
100
94
|
ImageTag = "IMG",
|
|
@@ -128,7 +122,6 @@ export const enum Constant {
|
|
|
128
122
|
ogType = "og:type",
|
|
129
123
|
ogTitle = "og:title",
|
|
130
124
|
SvgStyle = "svg:style",
|
|
131
|
-
ExcludeClassNames = "load,active,fixed,visible,focus,show,collaps,animat",
|
|
132
125
|
StyleSheet = "stylesheet"
|
|
133
126
|
}
|
|
134
127
|
|