@gemx-dev/clarity-js 0.8.89 → 0.8.91

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.
@@ -36,27 +36,46 @@ function fire(): void {
36
36
  flush();
37
37
  }
38
38
 
39
- function schedule(trigger: { scrollDepthPercent?: number; clickCount?: number; activeTimeMs?: number; maxPlaybackBytes?: number }): void {
39
+ function schedule(trigger: { scrollDepthPercent?: number; clickCount?: number; activeTimeMs?: number; delayMs?: number; maxPlaybackBytes?: number }): void {
40
40
  triggered = false;
41
- let clickCount = 0;
41
+ let windowExpired = false;
42
+ let delayScheduled = false;
43
+
44
+ const scheduleDelayedFire = (type: 'scroll' | 'click'): void => {
45
+ if (delayScheduled || windowExpired) return;
46
+ delayScheduled = true;
47
+ console.log(`[GemX Snapshot] ${type} triggered:`);
48
+ setTimeout(fire, trigger.delayMs ?? 0);
49
+ };
50
+
51
+ if (trigger.activeTimeMs) {
52
+ setTimeout((): void => {
53
+ windowExpired = true;
54
+ scroll.onTrustedScroll(null);
55
+ click.onTrustedClick(null);
56
+ }, trigger.activeTimeMs);
57
+ }
42
58
 
43
59
  if (trigger.scrollDepthPercent) {
44
60
  scroll.onTrustedScroll((): void => {
45
- const scrolled = window.scrollY + window.innerHeight;
46
- const total = document.documentElement.scrollHeight;
47
- if ((scrolled / total) * 100 >= trigger.scrollDepthPercent) { fire(); }
61
+ const docH = document.documentElement.scrollHeight;
62
+ const vpH = window.innerHeight;
63
+ const maxScrollY = docH - vpH;
64
+ if (maxScrollY <= 0) return;
65
+
66
+ const scrollY = "pageYOffset" in window ? Math.round(window.pageYOffset) : Math.round((document.documentElement as HTMLElement).scrollTop);
67
+ const pct = Math.min(100, Math.round(scrollY / maxScrollY * 100));
68
+ if (pct >= trigger.scrollDepthPercent) { scheduleDelayedFire('scroll'); }
48
69
  });
49
70
  }
50
71
 
51
72
  if (trigger.clickCount) {
73
+ let clickCount = 0;
52
74
  click.onTrustedClick((): void => {
53
75
  clickCount++;
54
- if (clickCount >= trigger.clickCount) { fire(); }
76
+ if (clickCount >= trigger.clickCount) { scheduleDelayedFire('click'); }
55
77
  });
56
78
  }
57
-
58
- if (trigger.activeTimeMs) { setTimeout(fire, trigger.activeTimeMs); }
59
- // maxPlaybackBytes trigger is handled via gemxUpload.start() callback above
60
79
  }
61
80
 
62
81
  function flush(): void {
@@ -20,6 +20,8 @@ import traverse from "@src/layout/traverse";
20
20
  import processNode from "./node";
21
21
  import config from "@src/core/config";
22
22
  import * as gemx from "@src/data/gemx";
23
+ import * as gemxUpload from "@src/data/gemx-upload";
24
+
23
25
 
24
26
  let observers: Set<MutationObserver> = new Set();
25
27
  let mutations: MutationQueue[] = [];
@@ -129,7 +131,9 @@ async function processMutation(timer: Timer, mutation: MutationRecord, instance:
129
131
  let target = mutation.target;
130
132
 
131
133
  const ignoredClasses = gemx.get().ignoreMutationClassNames;
132
- if (ignoredClasses.length > 0 && target instanceof Element) {
134
+ // Before processed snapshot, we need to check if the mutation is ignored
135
+ const isSnapshotProcessed = gemxUpload.isActive();
136
+ if (!isSnapshotProcessed && ignoredClasses.length > 0 && target instanceof Element) {
133
137
  const el = target as Element;
134
138
  if (ignoredClasses.some(cls => el.classList.contains(cls))) { return; }
135
139
  }
@@ -6,9 +6,13 @@ import * as gemx from "@src/data/gemx";
6
6
  const excludeClassNames = ExcludeClassNamesList;
7
7
 
8
8
  let selectorMap: { [selector: string]: number[] } = {};
9
+ // Tracks how many times each HTML id attribute has been seen during a traversal.
10
+ // If count > 1, the id is duplicated in the DOM and must not be used as a CSS selector.
11
+ let htmlIdCountMap: Map<string, number> = new Map();
9
12
 
10
13
  export function reset(): void {
11
14
  selectorMap = {};
15
+ htmlIdCountMap = new Map();
12
16
  }
13
17
 
14
18
  export function get(input: SelectorInput, type: Selector): string {
@@ -54,7 +58,16 @@ export function get(input: SelectorInput, type: Selector): string {
54
58
  // Update selector to use "id" field when available. There are two exceptions:
55
59
  // (1) if "id" appears to be an auto generated string token, e.g. guid or a random id containing digits
56
60
  // (2) if "id" appears inside a shadow DOM, in which case we continue to prefix up to shadow DOM to prevent conflicts
57
- selector = id && filter(id) ? `${getDomPrefix(prefix)}${Constant.Hash}${id}` : selector;
61
+ // (3) if "id" is duplicated in the DOM — using it would resolve to the wrong element during replay.
62
+ // Note: the very first occurrence of a duplicated id may still use #id until a duplicate is encountered.
63
+ if (id && filter(id)) {
64
+ const count = (htmlIdCountMap.get(id) ?? 0) + 1;
65
+ htmlIdCountMap.set(id, count);
66
+ if (count === 1) {
67
+ selector = `${getDomPrefix(prefix)}${Constant.Hash}${id}`;
68
+ }
69
+ // count > 1 means duplicate — fall through to keep the class/position-based selector
70
+ }
58
71
  return selector;
59
72
  }
60
73
  }
package/types/data.d.ts CHANGED
@@ -598,11 +598,13 @@ export interface GCMConsentState {
598
598
  export interface DiscoverTrigger {
599
599
  scrollDepthPercent?: number; // trigger khi scroll đến X% chiều cao trang
600
600
  clickCount?: number; // trigger sau X lần click
601
- activeTimeMs?: number; // trigger sau X ms kể từ window.load
601
+ activeTimeMs?: number; // window tối đa để nhận trigger (ms)
602
+ delayMs?: number; // delay thêm sau khi điều kiện thỏa trước khi fire
602
603
  maxPlaybackBytes?: number; // trigger khi buffer đạt X bytes
603
604
  }
604
605
 
605
606
  export interface GemXConfig {
607
+ debug: boolean;
606
608
  excludeClassNames: string[];
607
609
  ignoreMutationClassNames: string[];
608
610
  discoverTrigger: DiscoverTrigger | null;
package/types/global.d.ts CHANGED
@@ -2,6 +2,7 @@ declare global {
2
2
  interface Window {
3
3
  clarityOverrides?: { [key: string]: ((...args: any[]) => any) | undefined };
4
4
  GemXHeatmap?: {
5
+ debug: boolean;
5
6
  excludeClassNames: string[];
6
7
  ignoreMutationClassNames?: string[];
7
8
  discoverTrigger?: {