@gem-sdk/clarity-visualize 0.8.94

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -0
  3. package/build/clarity.visualize.js +2753 -0
  4. package/build/clarity.visualize.min.js +1 -0
  5. package/build/clarity.visualize.module.js +2748 -0
  6. package/package.json +45 -0
  7. package/rollup.config.ts +79 -0
  8. package/src/clarity.ts +3 -0
  9. package/src/custom/README.md +87 -0
  10. package/src/custom/canvas-layer.ts +355 -0
  11. package/src/custom/dialog.ts +242 -0
  12. package/src/custom/index.ts +7 -0
  13. package/src/data.ts +103 -0
  14. package/src/enrich.ts +80 -0
  15. package/src/global.ts +9 -0
  16. package/src/heatmap.ts +512 -0
  17. package/src/index.ts +4 -0
  18. package/src/interaction.ts +524 -0
  19. package/src/layout.ts +805 -0
  20. package/src/styles/blobUnavailable/chineseSimplified.svg +5 -0
  21. package/src/styles/blobUnavailable/chineseTraditional.svg +5 -0
  22. package/src/styles/blobUnavailable/dutch.svg +5 -0
  23. package/src/styles/blobUnavailable/english.svg +5 -0
  24. package/src/styles/blobUnavailable/french.svg +5 -0
  25. package/src/styles/blobUnavailable/german.svg +5 -0
  26. package/src/styles/blobUnavailable/iconOnly.svg +4 -0
  27. package/src/styles/blobUnavailable/italian.svg +5 -0
  28. package/src/styles/blobUnavailable/japanese.svg +5 -0
  29. package/src/styles/blobUnavailable/korean.svg +5 -0
  30. package/src/styles/blobUnavailable/portuguese.svg +5 -0
  31. package/src/styles/blobUnavailable/russian.svg +5 -0
  32. package/src/styles/blobUnavailable/spanish.svg +5 -0
  33. package/src/styles/blobUnavailable/turkish.svg +5 -0
  34. package/src/styles/iframeUnavailable/chineseSimplified.svg +5 -0
  35. package/src/styles/iframeUnavailable/chineseTraditional.svg +5 -0
  36. package/src/styles/iframeUnavailable/dutch.svg +5 -0
  37. package/src/styles/iframeUnavailable/english.svg +5 -0
  38. package/src/styles/iframeUnavailable/french.svg +5 -0
  39. package/src/styles/iframeUnavailable/german.svg +5 -0
  40. package/src/styles/iframeUnavailable/iconOnly.svg +4 -0
  41. package/src/styles/iframeUnavailable/italian.svg +5 -0
  42. package/src/styles/iframeUnavailable/japanese.svg +5 -0
  43. package/src/styles/iframeUnavailable/korean.svg +5 -0
  44. package/src/styles/iframeUnavailable/portuguese.svg +5 -0
  45. package/src/styles/iframeUnavailable/russian.svg +5 -0
  46. package/src/styles/iframeUnavailable/spanish.svg +5 -0
  47. package/src/styles/iframeUnavailable/turkish.svg +5 -0
  48. package/src/styles/imageMasked/chineseSimplified.svg +5 -0
  49. package/src/styles/imageMasked/chineseTraditional.svg +5 -0
  50. package/src/styles/imageMasked/dutch.svg +5 -0
  51. package/src/styles/imageMasked/english.svg +5 -0
  52. package/src/styles/imageMasked/french.svg +5 -0
  53. package/src/styles/imageMasked/german.svg +5 -0
  54. package/src/styles/imageMasked/iconOnly.svg +4 -0
  55. package/src/styles/imageMasked/italian.svg +5 -0
  56. package/src/styles/imageMasked/japanese.svg +5 -0
  57. package/src/styles/imageMasked/korean.svg +5 -0
  58. package/src/styles/imageMasked/portuguese.svg +5 -0
  59. package/src/styles/imageMasked/russian.svg +5 -0
  60. package/src/styles/imageMasked/spanish.svg +5 -0
  61. package/src/styles/imageMasked/turkish.svg +5 -0
  62. package/src/styles/pointer/click.css +31 -0
  63. package/src/styles/pointer/pointerIcon.svg +18 -0
  64. package/src/styles/shared.css +6 -0
  65. package/src/visualizer.ts +308 -0
  66. package/tsconfig.json +13 -0
  67. package/tslint.json +33 -0
  68. package/types/heatmap.d.ts +18 -0
  69. package/types/index.d.ts +11 -0
  70. package/types/layout.d.ts +12 -0
  71. package/types/string-import.d.ts +9 -0
  72. package/types/visualize.d.ts +253 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Custom module for rendering HTML Dialog elements
3
+ * Handles proper visualization of modal dialogs in the top-layer
4
+ */
5
+
6
+ import { Constant as LayoutConstants } from "@gem-sdk/clarity-js/types/layout";
7
+
8
+ export interface DialogRenderOptions {
9
+ isModal: boolean;
10
+ shouldBeOpen: boolean;
11
+ isExistingDialog: boolean;
12
+ }
13
+
14
+ /**
15
+ * Pending dialogs tracking for synchronization
16
+ * Used to track when all dialogs have finished rendering
17
+ * Uses Set to avoid counting the same dialog multiple times
18
+ */
19
+ let pendingDialogs: Set<HTMLDialogElement> = new Set();
20
+ let resolveDialogsRendered: (() => void) | null = null;
21
+ let dialogsRenderedPromise: Promise<void> | null = null;
22
+
23
+ /**
24
+ * Returns a promise that resolves when all pending dialogs are rendered
25
+ */
26
+ export function waitForDialogsRendered(): Promise<void> {
27
+ if (pendingDialogs.size === 0) {
28
+ // No pending dialogs, resolve immediately
29
+ return Promise.resolve();
30
+ }
31
+
32
+ // Return existing promise if already waiting
33
+ if (dialogsRenderedPromise) {
34
+ return dialogsRenderedPromise;
35
+ }
36
+
37
+ // Create new promise
38
+ dialogsRenderedPromise = new Promise<void>((resolve) => {
39
+ resolveDialogsRendered = resolve;
40
+ });
41
+
42
+ return dialogsRenderedPromise;
43
+ }
44
+
45
+ /**
46
+ * Resets the dialog rendering state
47
+ * Should be called before starting a new render cycle
48
+ */
49
+ export function resetDialogRenderState(): void {
50
+ pendingDialogs.clear();
51
+ resolveDialogsRendered = null;
52
+ dialogsRenderedPromise = null;
53
+ }
54
+
55
+ /**
56
+ * Extracts dialog rendering options from node attributes
57
+ *
58
+ * @param attributes - Node attributes containing dialog state
59
+ * @param dialogElement - The dialog element being rendered
60
+ * @returns Dialog render options
61
+ */
62
+ export function getDialogRenderOptions(
63
+ attributes: { [key: string]: string },
64
+ dialogElement: HTMLDialogElement | null
65
+ ): DialogRenderOptions {
66
+ return {
67
+ isModal: attributes[LayoutConstants.GXDialogModal] === "true",
68
+ shouldBeOpen: attributes["open"] !== undefined,
69
+ isExistingDialog: !!dialogElement
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Shows a dialog element with proper modal/non-modal handling
75
+ * Modal dialogs are shown via showModal() to render in top-layer with backdrop
76
+ * Non-modal dialogs are shown via show() method
77
+ *
78
+ * @param dialogElement - The dialog element to show
79
+ * @param isModal - Whether this is a modal dialog
80
+ * @param onError - Optional error callback
81
+ */
82
+ export function showDialog(
83
+ dialogElement: HTMLDialogElement,
84
+ isModal: boolean,
85
+ onError?: (error: Error) => void
86
+ ): void {
87
+ try {
88
+ if (!dialogElement.isConnected) {
89
+ notifyDialogComplete(dialogElement);
90
+ return;
91
+ }
92
+ if (!dialogElement.open) {
93
+ notifyDialogComplete(dialogElement);
94
+ return;
95
+ }
96
+
97
+ dialogElement.close();
98
+ if (isModal) {
99
+ dialogElement.showModal();
100
+ } else {
101
+ dialogElement.show();
102
+ }
103
+
104
+ // Wait for animations/transitions to complete
105
+ waitForDialogAnimation(dialogElement).then(() => {
106
+ notifyDialogComplete(dialogElement);
107
+ });
108
+ } catch (e) {
109
+ console.error('❌ Error showing dialog:', e);
110
+ if (onError) {
111
+ onError(e as Error);
112
+ }
113
+ // Still mark as complete even on error
114
+ notifyDialogComplete(dialogElement);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Waits for dialog animations/transitions to complete
120
+ * Listens for animationend and transitionend events with timeout fallback
121
+ */
122
+ function waitForDialogAnimation(dialogElement: HTMLDialogElement): Promise<void> {
123
+ return new Promise((resolve) => {
124
+ let resolved = false;
125
+ const timeoutMs = 1000; // Maximum wait time for animations
126
+
127
+ const complete = () => {
128
+ if (resolved) return;
129
+ resolved = true;
130
+ cleanup();
131
+ resolve();
132
+ };
133
+
134
+ // Fallback timeout in case no animation events fire
135
+ const timeout = setTimeout(complete, timeoutMs);
136
+
137
+ // Listen for animation end
138
+ const onAnimationEnd = (e: AnimationEvent) => {
139
+ if (e.target === dialogElement) {
140
+ complete();
141
+ }
142
+ };
143
+
144
+ // Listen for transition end
145
+ const onTransitionEnd = (TransitionEvent: TransitionEvent) => {
146
+ if (TransitionEvent.target === dialogElement) {
147
+ complete();
148
+ }
149
+ };
150
+
151
+ const cleanup = () => {
152
+ clearTimeout(timeout);
153
+ dialogElement.removeEventListener('animationend', onAnimationEnd as EventListener);
154
+ dialogElement.removeEventListener('transitionend', onTransitionEnd as EventListener);
155
+ };
156
+
157
+ dialogElement.addEventListener('animationend', onAnimationEnd as EventListener, { once: true });
158
+ dialogElement.addEventListener('transitionend', onTransitionEnd as EventListener, { once: true });
159
+
160
+ // If no animations/transitions, resolve after one frame
161
+ requestAnimationFrame(() => {
162
+ const computedStyle = window.getComputedStyle(dialogElement);
163
+ const hasAnimation = computedStyle.animationName !== 'none';
164
+ const hasTransition = computedStyle.transitionDuration !== '0s';
165
+
166
+ if (!hasAnimation && !hasTransition) {
167
+ complete();
168
+ }
169
+ });
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Internal: Notify that a dialog has completed rendering
175
+ */
176
+ function notifyDialogComplete(dialogElement: HTMLDialogElement): void {
177
+ pendingDialogs.delete(dialogElement);
178
+
179
+ // If all dialogs are done, resolve the promise
180
+ if (pendingDialogs.size === 0 && resolveDialogsRendered) {
181
+ resolveDialogsRendered();
182
+ resolveDialogsRendered = null;
183
+ dialogsRenderedPromise = null;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Closes a dialog element safely
189
+ *
190
+ * @param dialogElement - The dialog element to close
191
+ */
192
+ export function closeDialog(dialogElement: HTMLDialogElement): void {
193
+ try {
194
+ if (dialogElement.open) {
195
+ dialogElement.close();
196
+ }
197
+ } catch (e) {
198
+ console.warn('⚠️ Error closing dialog:', e);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Handles dialog rendering based on its state
204
+ * This is the main entry point for dialog rendering logic
205
+ *
206
+ * @param dialogElement - The dialog element to render
207
+ * @param options - Dialog render options
208
+ * @param onError - Optional error callback
209
+ */
210
+ export function renderDialog(
211
+ dialogElement: HTMLDialogElement,
212
+ options: DialogRenderOptions,
213
+ onError?: (error: Error) => void
214
+ ): void {
215
+ const { isModal, shouldBeOpen, isExistingDialog } = options;
216
+ if (!shouldBeOpen) return;
217
+
218
+ // Add dialog to pending set (Set automatically handles duplicates)
219
+ pendingDialogs.add(dialogElement);
220
+
221
+ const doShow = () => showDialog(dialogElement, isModal, onError);
222
+ if (isExistingDialog) {
223
+ doShow();
224
+ } else {
225
+ setTimeout(doShow, 0);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Removes custom tracking attributes before rendering
231
+ * These attributes are only for internal use
232
+ *
233
+ * @param attributes - Attributes object to clean
234
+ * @returns Cleaned attributes
235
+ */
236
+ export function cleanDialogAttributes(
237
+ attributes: { [key: string]: string }
238
+ ): { [key: string]: string } {
239
+ const cleaned = { ...attributes };
240
+ delete cleaned[LayoutConstants.GXDialogModal];
241
+ return cleaned;
242
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Custom modules for GemX Clarity Visualize extensions
3
+ * Contains custom rendering logic that extends the base Clarity visualization
4
+ */
5
+
6
+ export * from "./dialog";
7
+ export * from "./canvas-layer";
package/src/data.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { Data, Layout } from "@gem-sdk/clarity-js";
2
+ import type { Data as DecodedData, Layout as DecodedLayout } from "@gem-sdk/clarity-decode";
3
+ import { PlaybackState, RegionState } from "@clarity-types/visualize";
4
+
5
+ export class DataHelper {
6
+ regionMap = {};
7
+ regions: { [key: string]: RegionState} = {};
8
+ metrics: {[key: number]: number} = {};
9
+ lean = false;
10
+ state: PlaybackState;
11
+
12
+ constructor(state: PlaybackState) {
13
+ this.state = state;
14
+ }
15
+
16
+ static METRIC_MAP = {
17
+ [Data.Metric.TotalBytes]: { name: "Total Bytes", unit: "KB" },
18
+ [Data.Metric.TotalCost]: { name: "Total Cost", unit: "ms" },
19
+ [Data.Metric.LayoutCost]: { name: "Layout Cost", unit: "ms" },
20
+ [Data.Metric.LargestPaint]: { name: "LCP", unit: "s" },
21
+ [Data.Metric.CumulativeLayoutShift]: { name: "CLS", unit: "cls" },
22
+ [Data.Metric.LongTaskCount]: { name: "Long Tasks" },
23
+ [Data.Metric.CartTotal]: { name: "Cart Total", unit: "html-price" },
24
+ [Data.Metric.ProductPrice]: { name: "Product Price", unit: "ld-price" },
25
+ [Data.Metric.ThreadBlockedTime]: { name: "Thread Blocked", unit: "ms" },
26
+ [Data.Dimension.InteractionNextPaint]: { name: "INP", unit: "ms" }
27
+ };
28
+
29
+ public reset = (): void => {
30
+ this.metrics = {};
31
+ this.lean = false;
32
+ this.regions = {};
33
+ this.regionMap = {};
34
+ }
35
+
36
+ public metric = (event: DecodedData.MetricEvent): void => {
37
+ if (this.state.options.metadata) {
38
+ let metricMarkup = [];
39
+ let regionMarkup = [];
40
+ // Copy over metrics for future reference
41
+ for (let m in event.data) {
42
+ const eventType = typeof event.data[m];
43
+ if (eventType === "number" || (event.event === Data.Event.Dimension && m === Data.Dimension.InteractionNextPaint.toString())) {
44
+ if (!(m in this.metrics)) { this.metrics[m] = 0; }
45
+ let key = parseInt(m, 10);
46
+ let value = eventType === "object" ? Number(event.data[m]?.[0]) : event.data[m];
47
+ if (m in DataHelper.METRIC_MAP && (DataHelper.METRIC_MAP[m].unit === "html-price" ||DataHelper.METRIC_MAP[m].unit === "ld-price")) {
48
+ this.metrics[m] = value;
49
+ } else { this.metrics[m] += value; }
50
+ this.lean = key === Data.Metric.Playback && value === 0 ? true : this.lean;
51
+ }
52
+ }
53
+
54
+ for (let entry in this.metrics) {
55
+ if (entry in DataHelper.METRIC_MAP) {
56
+ let m = this.metrics[entry];
57
+ let map = DataHelper.METRIC_MAP[entry];
58
+ let unit = "unit" in map ? map.unit : Data.Constant.Empty;
59
+ metricMarkup.push(`<li><h2>${this.value(m, unit)}<span>${this.key(unit)}</span></h2>${map.name}</li>`);
60
+ }
61
+ }
62
+
63
+ // Append region information to metadata
64
+ for (let name in this.regions) {
65
+ let r = this.regions[name];
66
+ let className = (r.visibility === Layout.RegionVisibility.Visible ? "visible" : (r.interaction === Layout.InteractionState.Clicked ? "clicked" : Data.Constant.Empty));
67
+ regionMarkup.push(`<span class="${className}">${name}</span>`);
68
+ }
69
+
70
+ this.state.options.metadata.innerHTML = `<ul>${metricMarkup.join(Data.Constant.Empty)}</ul><div>${regionMarkup.join(Data.Constant.Empty)}</div>`;
71
+ }
72
+ }
73
+
74
+ public region(event: DecodedLayout.RegionEvent): void {
75
+ let data = event.data;
76
+ for (let r of data) {
77
+ if (!(r.name in this.regions)) {
78
+ this.regions[r.name] = { interaction: r.interaction , visibility: r.visibility }
79
+ }
80
+ this.regionMap[r.id] = r.name;
81
+ }
82
+ }
83
+
84
+ private key = (unit: string): string => {
85
+ switch (unit) {
86
+ case "html-price":
87
+ case "ld-price":
88
+ case "cls":
89
+ return Data.Constant.Empty;
90
+ default: return unit;
91
+ }
92
+ }
93
+
94
+ private value = (num: number, unit: string): number => {
95
+ switch (unit) {
96
+ case "KB": return Math.round(num / 1024);
97
+ case "s": return Math.round(num / 10) / 100;
98
+ case "cls": return num / 1000;
99
+ case "html-price": return num / 100;
100
+ default: return num;
101
+ }
102
+ }
103
+ }
package/src/enrich.ts ADDED
@@ -0,0 +1,80 @@
1
+ import { helper, Layout } from "@gem-sdk/clarity-js";
2
+ import { Layout as DecodedLayout } from "@gem-sdk/clarity-decode";
3
+ import { NodeData } from "@clarity-types/visualize";
4
+
5
+ export class EnrichHelper {
6
+
7
+ children: { [key: number]: number[] };
8
+ nodes: { [key: number]: NodeData };
9
+
10
+ constructor() {
11
+ this.reset();
12
+ }
13
+
14
+ public reset = (): void => {
15
+ this.children = {};
16
+ this.nodes = {};
17
+ helper.selector.reset();
18
+ }
19
+
20
+ public selectors = (event: DecodedLayout.DomEvent): DecodedLayout.DomEvent => {
21
+ event.data.forEach && event.data.forEach(d => {
22
+ let parent = this.nodes[d.parent];
23
+ let children = this.children[d.parent] || [];
24
+ let node = this.nodes[d.id] || { tag: d.tag, parent: d.parent, previous: d.previous };
25
+ let attributes = d.attributes || {};
26
+
27
+ /* Track parent-child relationship for this element */
28
+ if (node.parent !== d.parent) {
29
+ let childIndex = d.previous === null ? 0 : children.indexOf(d.previous) + 1;
30
+ children.splice(childIndex, 0, d.id);
31
+
32
+ // Stop tracking this node from children of previous parent
33
+ if (node.parent !== d.parent) {
34
+ let exParent = this.children[node.parent];
35
+ let nodeIndex = exParent ? exParent.indexOf(d.id) : -1;
36
+ if (nodeIndex >= 0) {
37
+ this.children[node.parent].splice(nodeIndex, 1);
38
+ }
39
+ }
40
+ node.parent = d.parent;
41
+ } else if (children.indexOf(d.id) < 0) { children.push(d.id); }
42
+
43
+ /* Get current position */
44
+ node.position = this.position(d.id, d.tag, node, children, children.map(c => this.nodes[c]));
45
+
46
+ /* For backward compatibility, continue populating current selector and hash like before in addition to beta selector and hash */
47
+ let input: Layout.SelectorInput = { id: d.id, tag: d.tag, prefix: parent ? [parent.alpha, parent.beta] : null, position: node.position, attributes };
48
+
49
+ // Get stable selector
50
+ // We intentionally use "null" value for empty selectors to keep parity with v0.6.25 and before.
51
+ let selectorAlpha = helper.selector.get(input, Layout.Selector.Alpha);
52
+ d.selectorAlpha = selectorAlpha.length > 0 ? selectorAlpha : null;
53
+ d.hashAlpha = selectorAlpha.length > 0 ? helper.hash(d.selectorAlpha) : null;
54
+
55
+ // Get beta selector
56
+ let selectorBeta = helper.selector.get(input, Layout.Selector.Beta);
57
+ d.selectorBeta = selectorBeta.length > 0 ? selectorBeta : null;
58
+ d.hashBeta = selectorBeta.length > 0 ? helper.hash(d.selectorBeta) : null;
59
+
60
+ /* Track state for future reference */
61
+ node.alpha = selectorAlpha;
62
+ node.beta = selectorBeta;
63
+ this.nodes[d.id] = node;
64
+ if (d.parent) { this.children[d.parent] = children; }
65
+ });
66
+ return event;
67
+ }
68
+
69
+ private position = (id: number, tag: string, child: NodeData, children: number[], siblings: NodeData[]): number => {
70
+ child.position = 1;
71
+ let idx = children ? children.indexOf(id) : -1;
72
+ while (idx-- > 0) {
73
+ if (tag === siblings[idx].tag) {
74
+ child.position = siblings[idx].position + 1;
75
+ break;
76
+ }
77
+ }
78
+ return child.position;
79
+ }
80
+ }
package/src/global.ts ADDED
@@ -0,0 +1,9 @@
1
+ import * as visualize from "@src/clarity";
2
+
3
+ // Expose clarity variable globally to allow access to public interface in a browser
4
+ if (typeof window !== "undefined") {
5
+ if ((window as any).clarity === undefined || (window as any).clarity === null) {
6
+ (window as any).clarity = {};
7
+ }
8
+ (window as any).clarity.visualize = visualize;
9
+ }