@gemx-dev/clarity-js 0.8.88 → 0.8.90

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.
@@ -23,6 +23,7 @@ import { signalsEvent } from "@src/data/signal";
23
23
  import { snapshot } from "@src/insight/snapshot";
24
24
  import * as dynamic from "@src/core/dynamic";
25
25
  import * as gemxUpload from "@src/data/gemx-upload";
26
+ import * as gemx from "@src/data/gemx";
26
27
 
27
28
  let discoverBytes: number = 0;
28
29
  let playbackBytes: number = 0;
@@ -33,7 +34,6 @@ let transit: Transit;
33
34
  let active: boolean;
34
35
  let queuedTime: number = 0;
35
36
  let leanLimit = false;
36
- let hasGemxPayload = false;
37
37
  export let track: UploadData;
38
38
 
39
39
  export function start(): void {
@@ -41,7 +41,6 @@ export function start(): void {
41
41
  discoverBytes = 0;
42
42
  playbackBytes = 0;
43
43
  leanLimit = false;
44
- hasGemxPayload = false;
45
44
  queuedTime = 0;
46
45
  playback = [];
47
46
  analysis = [];
@@ -114,27 +113,17 @@ export function queue(tokens: Token[], transmit: boolean = true): void {
114
113
  }
115
114
  }
116
115
 
117
- export function flush(): void {
116
+ export function flush(type: 'gemx-snapshot' | 'gemx-unload'): void {
118
117
  if (!active) { return; }
119
118
  clearTimeout(timeout);
120
119
  timeout = null;
121
- upload(true);
122
- }
123
-
124
- // Trigger an immediate non-beacon upload (used by gemx-discover after inject).
125
- // Unlike flush(), this does NOT set final=true, so playback data is never stripped.
126
- export function uploadNow(): void {
127
- if (!active) { return; }
128
- clearTimeout(timeout);
129
- timeout = null;
130
- upload(false);
120
+ upload(false, type);
131
121
  }
132
122
 
133
123
  // Inject pre-buffered tokens directly into playback as discover events.
134
- // Used by gemx-discover when trigger fires: all buffered Discover tokens are
124
+ // Used by gemx-snapshot when trigger fires: all buffered Discover tokens are
135
125
  // counted towards discoverBytes so upload uses MinUploadDelay (fast path).
136
126
  export function inject(buffered: Token[][]): void {
137
- hasGemxPayload = true;
138
127
  for (const tokens of buffered) {
139
128
  const e = JSON.stringify(tokens);
140
129
  discoverBytes += e.length;
@@ -157,7 +146,7 @@ export function stop(): void {
157
146
  active = false;
158
147
  }
159
148
 
160
- async function upload(final: boolean = false): Promise<void> {
149
+ async function upload(final: boolean = false, type?: string): Promise<void> {
161
150
  if (!active) {
162
151
  return;
163
152
  }
@@ -167,7 +156,8 @@ async function upload(final: boolean = false): Promise<void> {
167
156
  // Check if we can send playback bytes over the wire or not
168
157
  // For better instrumentation coverage, we send playback bytes from second sequence onwards
169
158
  // And, we only send playback metric when we are able to send the playback bytes back to server
170
- let sendPlaybackBytes = (hasGemxPayload || config.lean === false) && playbackBytes > 0 && (playbackBytes < Setting.MaxFirstPayloadBytes || envelope.data.sequence > 0);
159
+ const isGemxSnapshot = type === "gemx-snapshot";
160
+ let sendPlaybackBytes = (isGemxSnapshot || config.lean === false) && playbackBytes > 0 && (playbackBytes < Setting.MaxFirstPayloadBytes || envelope.data.sequence > 0);
171
161
  if (sendPlaybackBytes) { metric.max(Metric.Playback, BooleanFlag.True); }
172
162
 
173
163
  // CAUTION: Ensure "transmit" is set to false in the queue function for following events
@@ -184,7 +174,21 @@ async function upload(final: boolean = false): Promise<void> {
184
174
  let last = final === true;
185
175
 
186
176
  // In some cases envelope has null data because it's part of the shutdown process while there's one upload call queued which might introduce runtime error
187
- if(!envelope.data) return;
177
+ if (!envelope.data) return;
178
+
179
+ const isDebug = gemx.isDebug();
180
+ if (!type && isDebug) {
181
+ const trackedEvents: [number, string][] = [
182
+ [Event.Click, "click"],
183
+ [Event.Scroll, "scroll"],
184
+ ];
185
+ const eventSet = new Set(analysis.map(item => (JSON.parse(item) as Token[])[1]));
186
+ const labels: string[] = [];
187
+ for (const [eventType, label] of trackedEvents) {
188
+ if (eventSet.has(eventType)) labels.push(label);
189
+ }
190
+ type = labels.length > 0 ? labels.join("-") : undefined;
191
+ }
188
192
 
189
193
  let e = JSON.stringify(envelope.envelope(last));
190
194
  let a = `[${analysis.join()}]`;
@@ -204,7 +208,7 @@ async function upload(final: boolean = false): Promise<void> {
204
208
  let payload = stringify(encoded);
205
209
  let zipped = last ? null : await compress(payload);
206
210
  metric.sum(Metric.TotalBytes, zipped ? zipped.length : payload.length);
207
- send(payload, zipped, envelope.data.sequence, last);
211
+ send(payload, zipped, envelope.data.sequence, last, type);
208
212
 
209
213
  // Clear out events now that payload has been dispatched
210
214
  analysis = [];
@@ -213,7 +217,6 @@ async function upload(final: boolean = false): Promise<void> {
213
217
  playbackBytes = 0;
214
218
  discoverBytes = 0;
215
219
  leanLimit = false;
216
- hasGemxPayload = false;
217
220
  }
218
221
  }
219
222
 
@@ -221,7 +224,7 @@ function stringify(encoded: EncodedPayload): string {
221
224
  return encoded.p.length > 0 ? `{"e":${encoded.e},"a":${encoded.a},"p":${encoded.p}}` : `{"e":${encoded.e},"a":${encoded.a}}`;
222
225
  }
223
226
 
224
- function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boolean = false): void {
227
+ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boolean = false, type?: string): void {
225
228
  // Upload data if a valid URL is defined in the config
226
229
  if (typeof config.upload === Constant.String) {
227
230
  const url = config.upload as string;
@@ -269,8 +272,7 @@ function send(payload: string, zipped: Uint8Array, sequence: number, beacon: boo
269
272
  }
270
273
  } else if (config.upload) {
271
274
  const callback = config.upload as UploadCallback;
272
- const uploadType = hasGemxPayload ? "gemx-discover" : undefined;
273
- callback(payload, uploadType);
275
+ callback(payload, type);
274
276
  done(sequence);
275
277
  }
276
278
  }
@@ -49,7 +49,7 @@ const ALL_OBSERVE = { allRoots: [scroll], documentOnly: [click, clipboard, point
49
49
  let modules: Module[] = ALL;
50
50
 
51
51
  export function start(): void {
52
- modules = config.mode === "gemx" ? GEMX : ALL;
52
+ modules = config?.mode === "gemx" ? GEMX : ALL;
53
53
  modules.forEach(m => m.start());
54
54
  }
55
55
 
@@ -58,7 +58,7 @@ export function stop(): void {
58
58
  }
59
59
 
60
60
  export function observe(root: Node): void {
61
- const observeModules = config.mode === "gemx" ? GEMX_OBSERVE : ALL_OBSERVE;
61
+ const observeModules = config?.mode === "gemx" ? GEMX_OBSERVE : ALL_OBSERVE;
62
62
  observeModules.allRoots.forEach(m => m.observe(root));
63
63
  if (root.nodeType === Node.DOCUMENT_NODE) {
64
64
  observeModules.documentOnly.forEach(m => m.observe(root));
@@ -14,7 +14,7 @@ export function start(): void {
14
14
  }
15
15
 
16
16
  function beforeUnload(): void {
17
- upload.flush();
17
+ upload.flush('gemx-unload');
18
18
  }
19
19
 
20
20
  function recompute(evt: PageTransitionEvent): void {
@@ -36,30 +36,49 @@ 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 {
63
82
  upload.inject(gemxUpload.drain());
64
- upload.uploadNow();
83
+ upload.flush('gemx-snapshot');
65
84
  }
@@ -1,5 +1,5 @@
1
1
  import * as discover from "@src/layout/discover";
2
- import * as gemxDiscover from "@src/layout/gemx-discover";
2
+ import * as gemxSnapshot from "@src/layout/gemx-snapshot";
3
3
  import * as doc from "@src/layout/document";
4
4
  import * as dom from "@src/layout/dom";
5
5
  import * as mutation from "@src/layout/mutation";
@@ -29,7 +29,7 @@ export function start(): void {
29
29
  // IMPORTANT: Start custom element detection BEFORE discover
30
30
  // This ensures pre-existing custom elements are registered before DOM traversal
31
31
  custom.start();
32
- gemxDiscover.start(); // activates upload buffer before discover traverses
32
+ gemxSnapshot.start(); // activates upload buffer before discover traverses
33
33
  discover.start();
34
34
  style.start();
35
35
  animation.start();
@@ -43,5 +43,5 @@ export function stop(): void {
43
43
  style.stop();
44
44
  animation.stop();
45
45
  custom.stop();
46
- gemxDiscover.stop();
46
+ gemxSnapshot.stop();
47
47
  }
@@ -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
  }
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?: {