@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.
- package/build/clarity.extended.js +1 -1
- package/build/clarity.insight.js +1 -1
- package/build/clarity.js +82 -57
- package/build/clarity.min.js +1 -1
- package/build/clarity.module.js +82 -57
- package/build/clarity.performance.js +1 -1
- package/package.json +1 -1
- package/src/clarity.ts +1 -1
- package/src/core/version.ts +1 -1
- package/src/data/gemx-upload.ts +1 -1
- package/src/data/gemx.ts +6 -0
- package/src/data/upload.ts +25 -23
- package/src/interaction/index.ts +2 -2
- package/src/interaction/unload.ts +1 -1
- package/src/layout/{gemx-discover.ts → gemx-snapshot.ts} +29 -10
- package/src/layout/index.ts +3 -3
- package/src/layout/mutation.ts +5 -1
- package/types/data.d.ts +3 -1
- package/types/global.d.ts +1 -0
package/src/data/upload.ts
CHANGED
|
@@ -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(
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
-
callback(payload, uploadType);
|
|
275
|
+
callback(payload, type);
|
|
274
276
|
done(sequence);
|
|
275
277
|
}
|
|
276
278
|
}
|
package/src/interaction/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
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));
|
|
@@ -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
|
|
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
|
|
46
|
-
const
|
|
47
|
-
|
|
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) {
|
|
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.
|
|
83
|
+
upload.flush('gemx-snapshot');
|
|
65
84
|
}
|
package/src/layout/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as discover from "@src/layout/discover";
|
|
2
|
-
import * as
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
gemxSnapshot.stop();
|
|
47
47
|
}
|
package/src/layout/mutation.ts
CHANGED
|
@@ -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
|
-
|
|
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; //
|
|
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