@probat/react 0.1.5 → 0.2.1
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/README.md +88 -0
- package/dist/index.d.mts +83 -2
- package/dist/index.d.ts +83 -2
- package/dist/index.js +596 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +595 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/context/ProbatContext.tsx +77 -2
- package/src/hoc/itrt-frontend.code-workspace +8 -36
- package/src/hoc/withExperiment.tsx +59 -8
- package/src/hooks/useExperiment.ts +28 -4
- package/src/index.ts +2 -0
- package/src/utils/api.ts +22 -0
- package/src/utils/heatmapTracker.ts +665 -0
package/dist/index.js
CHANGED
|
@@ -21,6 +21,475 @@ function detectEnvironment() {
|
|
|
21
21
|
return "prod";
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// src/utils/heatmapTracker.ts
|
|
25
|
+
function getStoredExperimentInfo() {
|
|
26
|
+
if (typeof window === "undefined") return {};
|
|
27
|
+
try {
|
|
28
|
+
const proposalId = window.localStorage.getItem("probat_active_proposal_id") || void 0;
|
|
29
|
+
const variantLabel = window.localStorage.getItem("probat_active_variant_label") || void 0;
|
|
30
|
+
return { proposalId, variantLabel };
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var HeatmapTracker = class {
|
|
36
|
+
constructor(config) {
|
|
37
|
+
this.clicks = [];
|
|
38
|
+
this.movements = [];
|
|
39
|
+
this.batchTimer = null;
|
|
40
|
+
this.cursorBatchTimer = null;
|
|
41
|
+
this.lastCursorTime = 0;
|
|
42
|
+
this.isInitialized = false;
|
|
43
|
+
this.handleMouseMove = (event) => {
|
|
44
|
+
if (!this.config.enabled || !this.config.trackCursor) return;
|
|
45
|
+
if (typeof window === "undefined") return;
|
|
46
|
+
const stored = getStoredExperimentInfo();
|
|
47
|
+
const activeProposalId = this.config.proposalId || stored.proposalId;
|
|
48
|
+
const activeVariantLabel = stored.proposalId === activeProposalId ? stored.variantLabel || this.config.variantLabel : this.config.variantLabel || stored.variantLabel;
|
|
49
|
+
const now2 = Date.now();
|
|
50
|
+
const cursorThrottle = this.config.cursorThrottle ?? 100;
|
|
51
|
+
if (now2 - this.lastCursorTime < cursorThrottle) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.lastCursorTime = now2;
|
|
55
|
+
console.log("[PROBAT Heatmap] Cursor movement captured:", {
|
|
56
|
+
x: event.pageX,
|
|
57
|
+
y: event.pageY,
|
|
58
|
+
movementsInBatch: this.movements.length + 1
|
|
59
|
+
});
|
|
60
|
+
const pageUrl = window.location.href;
|
|
61
|
+
const siteUrl = window.location.origin;
|
|
62
|
+
const x = event.pageX;
|
|
63
|
+
const y = event.pageY;
|
|
64
|
+
const viewportWidth = window.innerWidth;
|
|
65
|
+
const viewportHeight = window.innerHeight;
|
|
66
|
+
const movementEvent = {
|
|
67
|
+
page_url: pageUrl,
|
|
68
|
+
site_url: siteUrl,
|
|
69
|
+
x_coordinate: x,
|
|
70
|
+
y_coordinate: y,
|
|
71
|
+
viewport_width: viewportWidth,
|
|
72
|
+
viewport_height: viewportHeight,
|
|
73
|
+
session_id: this.sessionId,
|
|
74
|
+
proposal_id: activeProposalId,
|
|
75
|
+
variant_label: activeVariantLabel
|
|
76
|
+
};
|
|
77
|
+
this.movements.push(movementEvent);
|
|
78
|
+
const cursorBatchSize = this.config.cursorBatchSize ?? 50;
|
|
79
|
+
if (this.movements.length >= cursorBatchSize) {
|
|
80
|
+
this.sendCursorBatch();
|
|
81
|
+
} else {
|
|
82
|
+
this.scheduleCursorBatchSend();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
this.handleClick = (event) => {
|
|
86
|
+
if (!this.config.enabled) return;
|
|
87
|
+
if (typeof window === "undefined") return;
|
|
88
|
+
const stored = getStoredExperimentInfo();
|
|
89
|
+
const activeProposalId = this.config.proposalId || stored.proposalId;
|
|
90
|
+
const activeVariantLabel = stored.proposalId === activeProposalId ? stored.variantLabel || this.config.variantLabel : this.config.variantLabel || stored.variantLabel;
|
|
91
|
+
const target = event.target;
|
|
92
|
+
if (!target) return;
|
|
93
|
+
if (this.shouldExcludeElement(target)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const pageUrl = window.location.href;
|
|
97
|
+
const siteUrl = window.location.origin;
|
|
98
|
+
const x = event.pageX;
|
|
99
|
+
const y = event.pageY;
|
|
100
|
+
const viewportWidth = window.innerWidth;
|
|
101
|
+
const viewportHeight = window.innerHeight;
|
|
102
|
+
const elementInfo = this.extractElementInfo(target);
|
|
103
|
+
const clickEvent = {
|
|
104
|
+
page_url: pageUrl,
|
|
105
|
+
site_url: siteUrl,
|
|
106
|
+
x_coordinate: x,
|
|
107
|
+
y_coordinate: y,
|
|
108
|
+
viewport_width: viewportWidth,
|
|
109
|
+
viewport_height: viewportHeight,
|
|
110
|
+
element_tag: elementInfo.tag,
|
|
111
|
+
element_class: elementInfo.class,
|
|
112
|
+
element_id: elementInfo.id,
|
|
113
|
+
session_id: this.sessionId,
|
|
114
|
+
proposal_id: activeProposalId,
|
|
115
|
+
variant_label: activeVariantLabel
|
|
116
|
+
};
|
|
117
|
+
this.clicks.push(clickEvent);
|
|
118
|
+
const batchSize = this.config.batchSize ?? 10;
|
|
119
|
+
if (this.clicks.length >= batchSize) {
|
|
120
|
+
this.sendBatch();
|
|
121
|
+
} else {
|
|
122
|
+
this.scheduleBatchSend();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
this.sessionId = this.getOrCreateSessionId();
|
|
126
|
+
const stored = getStoredExperimentInfo();
|
|
127
|
+
this.config = {
|
|
128
|
+
apiBaseUrl: config.apiBaseUrl,
|
|
129
|
+
batchSize: config.batchSize || 10,
|
|
130
|
+
batchInterval: config.batchInterval || 5e3,
|
|
131
|
+
enabled: config.enabled !== false,
|
|
132
|
+
excludeSelectors: config.excludeSelectors || [
|
|
133
|
+
"[data-heatmap-exclude]",
|
|
134
|
+
'input[type="password"]',
|
|
135
|
+
'input[type="email"]',
|
|
136
|
+
"textarea"
|
|
137
|
+
],
|
|
138
|
+
trackCursor: config.trackCursor !== false,
|
|
139
|
+
// Enable cursor tracking by default
|
|
140
|
+
cursorThrottle: config.cursorThrottle || 100,
|
|
141
|
+
// Capture cursor position every 100ms
|
|
142
|
+
cursorBatchSize: config.cursorBatchSize || 50,
|
|
143
|
+
// Send every 50 movements
|
|
144
|
+
proposalId: config.proposalId || stored.proposalId,
|
|
145
|
+
variantLabel: config.variantLabel || stored.variantLabel
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
getOrCreateSessionId() {
|
|
149
|
+
if (typeof window === "undefined") return "";
|
|
150
|
+
const storageKey = "probat_heatmap_session_id";
|
|
151
|
+
let sessionId = localStorage.getItem(storageKey);
|
|
152
|
+
if (!sessionId) {
|
|
153
|
+
sessionId = `heatmap_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
|
154
|
+
localStorage.setItem(storageKey, sessionId);
|
|
155
|
+
}
|
|
156
|
+
return sessionId;
|
|
157
|
+
}
|
|
158
|
+
shouldExcludeElement(element) {
|
|
159
|
+
if (!this.config.excludeSelectors) return false;
|
|
160
|
+
for (const selector of this.config.excludeSelectors) {
|
|
161
|
+
if (element.matches(selector) || element.closest(selector)) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
extractElementInfo(element) {
|
|
168
|
+
return {
|
|
169
|
+
tag: element.tagName || null,
|
|
170
|
+
class: element.className && typeof element.className === "string" ? element.className : null,
|
|
171
|
+
id: element.id || null
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
scheduleCursorBatchSend() {
|
|
175
|
+
if (this.cursorBatchTimer) {
|
|
176
|
+
clearTimeout(this.cursorBatchTimer);
|
|
177
|
+
}
|
|
178
|
+
this.cursorBatchTimer = setTimeout(() => {
|
|
179
|
+
if (this.movements.length > 0) {
|
|
180
|
+
this.sendCursorBatch();
|
|
181
|
+
}
|
|
182
|
+
}, this.config.batchInterval ?? 5e3);
|
|
183
|
+
}
|
|
184
|
+
async sendCursorBatch() {
|
|
185
|
+
if (this.movements.length === 0) return;
|
|
186
|
+
const siteUrl = this.movements[0]?.site_url || window.location.origin;
|
|
187
|
+
const batch = {
|
|
188
|
+
movements: this.movements,
|
|
189
|
+
site_url: siteUrl,
|
|
190
|
+
proposal_id: this.config.proposalId,
|
|
191
|
+
variant_label: this.config.variantLabel
|
|
192
|
+
};
|
|
193
|
+
this.movements = [];
|
|
194
|
+
if (this.cursorBatchTimer) {
|
|
195
|
+
clearTimeout(this.cursorBatchTimer);
|
|
196
|
+
this.cursorBatchTimer = null;
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
console.log("[PROBAT Heatmap] Sending cursor movements batch:", {
|
|
200
|
+
count: batch.movements.length,
|
|
201
|
+
site_url: batch.site_url
|
|
202
|
+
});
|
|
203
|
+
const response = await fetch(`${this.config.apiBaseUrl}/api/heatmap/cursor-movements`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
"Content-Type": "application/json"
|
|
207
|
+
},
|
|
208
|
+
body: JSON.stringify(batch)
|
|
209
|
+
// Don't wait for response - fire and forget for performance
|
|
210
|
+
});
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
console.warn("[PROBAT Heatmap] Failed to send cursor movements:", response.status);
|
|
213
|
+
} else {
|
|
214
|
+
console.log("[PROBAT Heatmap] Successfully sent cursor movements batch");
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.warn("[PROBAT Heatmap] Error sending cursor movements:", error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
scheduleBatchSend() {
|
|
221
|
+
if (this.batchTimer) {
|
|
222
|
+
clearTimeout(this.batchTimer);
|
|
223
|
+
}
|
|
224
|
+
this.batchTimer = setTimeout(() => {
|
|
225
|
+
if (this.clicks.length > 0) {
|
|
226
|
+
this.sendBatch();
|
|
227
|
+
}
|
|
228
|
+
}, this.config.batchInterval ?? 5e3);
|
|
229
|
+
}
|
|
230
|
+
async sendBatch() {
|
|
231
|
+
if (this.clicks.length === 0) return;
|
|
232
|
+
if (this.batchTimer) {
|
|
233
|
+
clearTimeout(this.batchTimer);
|
|
234
|
+
this.batchTimer = null;
|
|
235
|
+
}
|
|
236
|
+
const siteUrl = this.clicks[0]?.site_url || window.location.origin;
|
|
237
|
+
const batch = {
|
|
238
|
+
clicks: [...this.clicks],
|
|
239
|
+
site_url: siteUrl
|
|
240
|
+
};
|
|
241
|
+
this.clicks = [];
|
|
242
|
+
try {
|
|
243
|
+
const response = await fetch(`${this.config.apiBaseUrl}/api/heatmap/clicks`, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: {
|
|
246
|
+
"Content-Type": "application/json"
|
|
247
|
+
},
|
|
248
|
+
body: JSON.stringify(batch)
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
console.warn("[PROBAT Heatmap] Failed to send clicks:", response.status);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.warn("[PROBAT Heatmap] Error sending clicks:", error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
init() {
|
|
258
|
+
if (this.isInitialized) {
|
|
259
|
+
console.warn("[PROBAT Heatmap] Tracker already initialized");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (typeof window === "undefined") {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
const params = new URLSearchParams(window.location.search);
|
|
266
|
+
if (params.get("heatmap") === "true") {
|
|
267
|
+
console.log("[PROBAT Heatmap] Heatmap visualization mode detected");
|
|
268
|
+
const pageUrl = params.get("page_url");
|
|
269
|
+
if (pageUrl) {
|
|
270
|
+
this.config.enabled = false;
|
|
271
|
+
this.isInitialized = true;
|
|
272
|
+
this.enableVisualization(pageUrl);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
document.addEventListener("click", this.handleClick, true);
|
|
277
|
+
if (this.config.trackCursor) {
|
|
278
|
+
document.addEventListener("mousemove", this.handleMouseMove, { passive: true });
|
|
279
|
+
console.log("[PROBAT Heatmap] Cursor tracking enabled");
|
|
280
|
+
} else {
|
|
281
|
+
console.log("[PROBAT Heatmap] Cursor tracking disabled");
|
|
282
|
+
}
|
|
283
|
+
this.isInitialized = true;
|
|
284
|
+
console.log("[PROBAT Heatmap] Tracker initialized");
|
|
285
|
+
window.addEventListener("beforeunload", () => {
|
|
286
|
+
if (this.clicks.length > 0) {
|
|
287
|
+
const siteUrl = this.clicks[0]?.site_url || window.location.origin;
|
|
288
|
+
const batch = {
|
|
289
|
+
clicks: this.clicks,
|
|
290
|
+
site_url: siteUrl
|
|
291
|
+
};
|
|
292
|
+
const blob = new Blob([JSON.stringify(batch)], {
|
|
293
|
+
type: "application/json"
|
|
294
|
+
});
|
|
295
|
+
navigator.sendBeacon(
|
|
296
|
+
`${this.config.apiBaseUrl}/api/heatmap/clicks`,
|
|
297
|
+
blob
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (this.movements.length > 0) {
|
|
301
|
+
const siteUrl = this.movements[0]?.site_url || window.location.origin;
|
|
302
|
+
const batch = {
|
|
303
|
+
movements: this.movements,
|
|
304
|
+
site_url: siteUrl
|
|
305
|
+
};
|
|
306
|
+
const blob = new Blob([JSON.stringify(batch)], {
|
|
307
|
+
type: "application/json"
|
|
308
|
+
});
|
|
309
|
+
navigator.sendBeacon(
|
|
310
|
+
`${this.config.apiBaseUrl}/api/heatmap/cursor-movements`,
|
|
311
|
+
blob
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
stop() {
|
|
317
|
+
if (!this.isInitialized) return;
|
|
318
|
+
if (this.clicks.length > 0) {
|
|
319
|
+
this.sendBatch();
|
|
320
|
+
}
|
|
321
|
+
if (this.movements.length > 0) {
|
|
322
|
+
this.sendCursorBatch();
|
|
323
|
+
}
|
|
324
|
+
document.removeEventListener("click", this.handleClick, true);
|
|
325
|
+
if (this.config.trackCursor) {
|
|
326
|
+
document.removeEventListener("mousemove", this.handleMouseMove);
|
|
327
|
+
}
|
|
328
|
+
if (this.batchTimer) {
|
|
329
|
+
clearTimeout(this.batchTimer);
|
|
330
|
+
this.batchTimer = null;
|
|
331
|
+
}
|
|
332
|
+
if (this.cursorBatchTimer) {
|
|
333
|
+
clearTimeout(this.cursorBatchTimer);
|
|
334
|
+
this.cursorBatchTimer = null;
|
|
335
|
+
}
|
|
336
|
+
this.isInitialized = false;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Enable visualization mode
|
|
340
|
+
*/
|
|
341
|
+
/**
|
|
342
|
+
* Enable visualization mode
|
|
343
|
+
*/
|
|
344
|
+
async enableVisualization(pageUrl) {
|
|
345
|
+
console.log("[PROBAT Heatmap] Enabling visualization mode for:", pageUrl);
|
|
346
|
+
this.stop();
|
|
347
|
+
this.config.enabled = false;
|
|
348
|
+
try {
|
|
349
|
+
const url = new URL(`${this.config.apiBaseUrl}/api/heatmap/aggregate`);
|
|
350
|
+
url.searchParams.append("site_url", window.location.origin);
|
|
351
|
+
url.searchParams.append("page_url", pageUrl);
|
|
352
|
+
url.searchParams.append("days", "30");
|
|
353
|
+
const params = new URLSearchParams(window.location.search);
|
|
354
|
+
const proposalId = params.get("proposal_id");
|
|
355
|
+
const variantLabel = params.get("variant_label");
|
|
356
|
+
if (proposalId) url.searchParams.append("proposal_id", proposalId);
|
|
357
|
+
if (variantLabel) url.searchParams.append("variant_label", variantLabel);
|
|
358
|
+
const response = await fetch(url.toString());
|
|
359
|
+
if (!response.ok) throw new Error("Failed to fetch heatmap data");
|
|
360
|
+
const data = await response.json();
|
|
361
|
+
if (data && data.points) {
|
|
362
|
+
console.log(`[PROBAT Heatmap] Found ${data.points.length} points. Rendering...`);
|
|
363
|
+
this.renderHeatmapOverlay(data);
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.error("[PROBAT Heatmap] Visualization error:", error);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Render heatmap overlay on valid points
|
|
371
|
+
*/
|
|
372
|
+
renderHeatmapOverlay(data) {
|
|
373
|
+
const points = data.points;
|
|
374
|
+
const trackedWidth = data.viewport_width || 0;
|
|
375
|
+
const existing = document.getElementById("probat-heatmap-overlay");
|
|
376
|
+
if (existing) existing.remove();
|
|
377
|
+
const canvas = document.createElement("canvas");
|
|
378
|
+
canvas.id = "probat-heatmap-overlay";
|
|
379
|
+
canvas.style.position = "absolute";
|
|
380
|
+
canvas.style.top = "0";
|
|
381
|
+
canvas.style.left = "0";
|
|
382
|
+
canvas.style.zIndex = "999999";
|
|
383
|
+
canvas.style.pointerEvents = "none";
|
|
384
|
+
canvas.style.display = "block";
|
|
385
|
+
canvas.style.margin = "0";
|
|
386
|
+
canvas.style.padding = "0";
|
|
387
|
+
document.documentElement.appendChild(canvas);
|
|
388
|
+
const resize = () => {
|
|
389
|
+
const dpr = window.devicePixelRatio || 1;
|
|
390
|
+
const width = document.documentElement.scrollWidth;
|
|
391
|
+
const height = document.documentElement.scrollHeight;
|
|
392
|
+
const currentWidth = window.innerWidth;
|
|
393
|
+
let offsetX = 0;
|
|
394
|
+
if (trackedWidth > 0 && trackedWidth !== currentWidth) {
|
|
395
|
+
offsetX = (currentWidth - trackedWidth) / 2;
|
|
396
|
+
console.log(`[PROBAT Heatmap] Horizontal adjustment: offset=${offsetX}px (Tracked: ${trackedWidth}px, Current: ${currentWidth}px)`);
|
|
397
|
+
}
|
|
398
|
+
canvas.style.width = width + "px";
|
|
399
|
+
canvas.style.height = height + "px";
|
|
400
|
+
canvas.width = width * dpr;
|
|
401
|
+
canvas.height = height * dpr;
|
|
402
|
+
const ctx = canvas.getContext("2d");
|
|
403
|
+
if (ctx) {
|
|
404
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
405
|
+
this.renderPoints(ctx, points, offsetX);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
window.addEventListener("resize", resize);
|
|
409
|
+
setTimeout(resize, 300);
|
|
410
|
+
setTimeout(resize, 1e3);
|
|
411
|
+
setTimeout(resize, 3e3);
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Draw points on canvas
|
|
415
|
+
*/
|
|
416
|
+
renderPoints(ctx, points, offsetX) {
|
|
417
|
+
points.forEach((point) => {
|
|
418
|
+
const x = point.x + offsetX;
|
|
419
|
+
const y = point.y;
|
|
420
|
+
const intensity = point.intensity;
|
|
421
|
+
const radius = 20 + intensity * 12;
|
|
422
|
+
const color = this.getHeatmapColor(intensity);
|
|
423
|
+
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
424
|
+
gradient.addColorStop(0, this.rgbToRgba(color, 0.8));
|
|
425
|
+
gradient.addColorStop(1, this.rgbToRgba(color, 0));
|
|
426
|
+
ctx.beginPath();
|
|
427
|
+
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
|
428
|
+
ctx.fillStyle = gradient;
|
|
429
|
+
ctx.fill();
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get heatmap color based on intensity
|
|
434
|
+
*/
|
|
435
|
+
getHeatmapColor(intensity) {
|
|
436
|
+
const clamped = Math.max(0, Math.min(1, intensity));
|
|
437
|
+
if (clamped < 0.25) {
|
|
438
|
+
const t = clamped / 0.25;
|
|
439
|
+
const r = Math.floor(0 + t * 0);
|
|
440
|
+
const g = Math.floor(0 + t * 255);
|
|
441
|
+
const b = Math.floor(255 + t * (255 - 255));
|
|
442
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
443
|
+
} else if (clamped < 0.5) {
|
|
444
|
+
const t = (clamped - 0.25) / 0.25;
|
|
445
|
+
const r = Math.floor(0 + t * 0);
|
|
446
|
+
const g = 255;
|
|
447
|
+
const b = Math.floor(255 + t * (0 - 255));
|
|
448
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
449
|
+
} else if (clamped < 0.75) {
|
|
450
|
+
const t = (clamped - 0.5) / 0.25;
|
|
451
|
+
const r = Math.floor(0 + t * 255);
|
|
452
|
+
const g = 255;
|
|
453
|
+
const b = 0;
|
|
454
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
455
|
+
} else {
|
|
456
|
+
const t = (clamped - 0.75) / 0.25;
|
|
457
|
+
const r = 255;
|
|
458
|
+
const g = Math.floor(255 + t * (0 - 255));
|
|
459
|
+
const b = 0;
|
|
460
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Convert RGB to RGBA
|
|
465
|
+
*/
|
|
466
|
+
rgbToRgba(rgb, opacity) {
|
|
467
|
+
const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
|
|
468
|
+
if (match) {
|
|
469
|
+
return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${opacity})`;
|
|
470
|
+
}
|
|
471
|
+
return rgb;
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
var trackerInstance = null;
|
|
475
|
+
function initHeatmapTracking(config) {
|
|
476
|
+
if (trackerInstance) {
|
|
477
|
+
trackerInstance.stop();
|
|
478
|
+
}
|
|
479
|
+
trackerInstance = new HeatmapTracker(config);
|
|
480
|
+
trackerInstance.init();
|
|
481
|
+
return trackerInstance;
|
|
482
|
+
}
|
|
483
|
+
function getHeatmapTracker() {
|
|
484
|
+
return trackerInstance;
|
|
485
|
+
}
|
|
486
|
+
function stopHeatmapTracking() {
|
|
487
|
+
if (trackerInstance) {
|
|
488
|
+
trackerInstance.stop();
|
|
489
|
+
trackerInstance = null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
24
493
|
// src/context/ProbatContext.tsx
|
|
25
494
|
var ProbatContext = React4.createContext(null);
|
|
26
495
|
function ProbatProvider({
|
|
@@ -28,19 +497,63 @@ function ProbatProvider({
|
|
|
28
497
|
clientKey,
|
|
29
498
|
environment: explicitEnvironment,
|
|
30
499
|
repoFullName: explicitRepoFullName,
|
|
500
|
+
proposalId,
|
|
501
|
+
variantLabel,
|
|
31
502
|
children
|
|
32
503
|
}) {
|
|
504
|
+
const storedProposalId = typeof window !== "undefined" ? window.localStorage.getItem("probat_active_proposal_id") || void 0 : void 0;
|
|
505
|
+
const storedVariantLabel = typeof window !== "undefined" ? window.localStorage.getItem("probat_active_variant_label") || void 0 : void 0;
|
|
33
506
|
const contextValue = React4.useMemo(() => {
|
|
34
507
|
const resolvedApiBaseUrl = apiBaseUrl || typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) !== "undefined" && undefined?.VITE_PROBAT_API || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_API || typeof window !== "undefined" && window.__PROBAT_API || "https://gushi.onrender.com";
|
|
35
508
|
const environment = explicitEnvironment || detectEnvironment();
|
|
36
509
|
const resolvedRepoFullName = explicitRepoFullName || typeof globalThis !== "undefined" && globalThis.process?.env?.NEXT_PUBLIC_PROBAT_REPO || typeof ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) !== "undefined" && undefined?.VITE_PROBAT_REPO || typeof window !== "undefined" && window.__PROBAT_REPO || void 0;
|
|
510
|
+
const params = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null;
|
|
511
|
+
const isHeatmapMode = params?.get("heatmap") === "true";
|
|
512
|
+
let urlProposalId;
|
|
513
|
+
let urlVariantLabel;
|
|
514
|
+
if (isHeatmapMode && params) {
|
|
515
|
+
urlProposalId = params.get("proposal_id") || void 0;
|
|
516
|
+
urlVariantLabel = params.get("variant_label") || void 0;
|
|
517
|
+
console.log("[PROBAT] Heatmap mode: Overriding variant from URL", { urlProposalId, urlVariantLabel });
|
|
518
|
+
}
|
|
519
|
+
const finalProposalId = urlProposalId || proposalId || (!isHeatmapMode ? storedProposalId : void 0);
|
|
520
|
+
const finalVariantLabel = urlVariantLabel || variantLabel || (!isHeatmapMode ? storedVariantLabel : void 0);
|
|
37
521
|
return {
|
|
38
522
|
apiBaseUrl: resolvedApiBaseUrl,
|
|
39
523
|
environment,
|
|
40
524
|
clientKey,
|
|
41
|
-
repoFullName: resolvedRepoFullName
|
|
525
|
+
repoFullName: resolvedRepoFullName,
|
|
526
|
+
proposalId: finalProposalId,
|
|
527
|
+
variantLabel: finalVariantLabel
|
|
528
|
+
};
|
|
529
|
+
}, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName, proposalId, variantLabel, storedProposalId, storedVariantLabel]);
|
|
530
|
+
React4.useEffect(() => {
|
|
531
|
+
if (typeof window !== "undefined") {
|
|
532
|
+
initHeatmapTracking({
|
|
533
|
+
apiBaseUrl: contextValue.apiBaseUrl,
|
|
534
|
+
batchSize: 10,
|
|
535
|
+
batchInterval: 5e3,
|
|
536
|
+
enabled: true,
|
|
537
|
+
excludeSelectors: [
|
|
538
|
+
"[data-heatmap-exclude]",
|
|
539
|
+
'input[type="password"]',
|
|
540
|
+
'input[type="email"]',
|
|
541
|
+
"textarea"
|
|
542
|
+
],
|
|
543
|
+
// Explicitly enable cursor tracking with sensible defaults
|
|
544
|
+
trackCursor: true,
|
|
545
|
+
cursorThrottle: 100,
|
|
546
|
+
// capture every 100ms
|
|
547
|
+
cursorBatchSize: 50,
|
|
548
|
+
// send every 50 movements (or after batchInterval)
|
|
549
|
+
proposalId: contextValue.proposalId,
|
|
550
|
+
variantLabel: contextValue.variantLabel
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return () => {
|
|
554
|
+
stopHeatmapTracking();
|
|
42
555
|
};
|
|
43
|
-
}, [apiBaseUrl,
|
|
556
|
+
}, [contextValue.apiBaseUrl, contextValue.proposalId, contextValue.variantLabel]);
|
|
44
557
|
return /* @__PURE__ */ React4__default.default.createElement(ProbatContext.Provider, { value: contextValue }, children);
|
|
45
558
|
}
|
|
46
559
|
function useProbatContext() {
|
|
@@ -74,6 +587,14 @@ async function fetchDecision(baseUrl, proposalId) {
|
|
|
74
587
|
const data = await res.json();
|
|
75
588
|
const experiment_id = (data.experiment_id || `exp_${proposalId}`).toString();
|
|
76
589
|
const label = data.label && data.label.trim() ? data.label : "control";
|
|
590
|
+
if (typeof window !== "undefined") {
|
|
591
|
+
try {
|
|
592
|
+
window.localStorage.setItem("probat_active_proposal_id", proposalId);
|
|
593
|
+
window.localStorage.setItem("probat_active_variant_label", label);
|
|
594
|
+
} catch (e) {
|
|
595
|
+
console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
77
598
|
return { experiment_id, label };
|
|
78
599
|
} finally {
|
|
79
600
|
pendingFetches.delete(proposalId);
|
|
@@ -152,6 +673,13 @@ async function fetchComponentExperimentConfig(baseUrl, repoFullName, componentPa
|
|
|
152
673
|
throw new Error(`HTTP ${res.status}`);
|
|
153
674
|
}
|
|
154
675
|
const data = await res.json();
|
|
676
|
+
if (typeof window !== "undefined" && data?.proposal_id) {
|
|
677
|
+
try {
|
|
678
|
+
window.localStorage.setItem("probat_active_proposal_id", data.proposal_id);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
console.warn("[PROBAT] Failed to set proposal_id in localStorage:", e);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
155
683
|
return data;
|
|
156
684
|
} catch (e) {
|
|
157
685
|
console.warn(`[PROBAT] Failed to fetch component config: ${e}`);
|
|
@@ -607,14 +1135,25 @@ function markTrackedVisit(proposalId, label) {
|
|
|
607
1135
|
|
|
608
1136
|
// src/hooks/useExperiment.ts
|
|
609
1137
|
function useExperiment(proposalId, options) {
|
|
610
|
-
const
|
|
1138
|
+
const context = useProbatContext();
|
|
1139
|
+
const { apiBaseUrl } = context;
|
|
611
1140
|
const [choice, setChoice] = React4.useState(null);
|
|
612
1141
|
const [isLoading, setIsLoading] = React4.useState(true);
|
|
613
1142
|
const [error, setError] = React4.useState(null);
|
|
614
1143
|
const autoTrackImpression = options?.autoTrackImpression !== false;
|
|
615
1144
|
React4.useEffect(() => {
|
|
616
1145
|
let alive = true;
|
|
617
|
-
const
|
|
1146
|
+
const isHeatmapMode = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("heatmap") === "true";
|
|
1147
|
+
if (context.proposalId === proposalId && context.variantLabel) {
|
|
1148
|
+
console.log(`[PROBAT] Forced variant from context: ${context.variantLabel}`);
|
|
1149
|
+
setChoice({
|
|
1150
|
+
experiment_id: `forced_${proposalId}`,
|
|
1151
|
+
label: context.variantLabel
|
|
1152
|
+
});
|
|
1153
|
+
setIsLoading(false);
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
const cached = isHeatmapMode ? null : readChoice(proposalId);
|
|
618
1157
|
if (cached) {
|
|
619
1158
|
setChoice({ experiment_id: cached.experiment_id, label: cached.label });
|
|
620
1159
|
setIsLoading(false);
|
|
@@ -627,7 +1166,9 @@ function useExperiment(proposalId, options) {
|
|
|
627
1166
|
proposalId
|
|
628
1167
|
);
|
|
629
1168
|
if (!alive) return;
|
|
630
|
-
|
|
1169
|
+
if (!isHeatmapMode) {
|
|
1170
|
+
writeChoice(proposalId, experiment_id, label);
|
|
1171
|
+
}
|
|
631
1172
|
setChoice({ experiment_id, label });
|
|
632
1173
|
setError(null);
|
|
633
1174
|
} catch (e) {
|
|
@@ -648,7 +1189,7 @@ function useExperiment(proposalId, options) {
|
|
|
648
1189
|
return () => {
|
|
649
1190
|
alive = false;
|
|
650
1191
|
};
|
|
651
|
-
}, [proposalId, apiBaseUrl]);
|
|
1192
|
+
}, [proposalId, apiBaseUrl, context.proposalId, context.variantLabel]);
|
|
652
1193
|
React4.useEffect(() => {
|
|
653
1194
|
if (!autoTrackImpression || !choice) return;
|
|
654
1195
|
const exp = choice.experiment_id;
|
|
@@ -784,32 +1325,70 @@ function withExperiment(Control, options) {
|
|
|
784
1325
|
if (!proposalId) return;
|
|
785
1326
|
if (useNewAPI && configLoading) return;
|
|
786
1327
|
let alive = true;
|
|
787
|
-
const
|
|
788
|
-
if (
|
|
1328
|
+
const isHeatmapMode = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("heatmap") === "true";
|
|
1329
|
+
if (context.proposalId === proposalId && context.variantLabel) {
|
|
1330
|
+
console.log(`[PROBAT HOC] Forced variant from context: ${context.variantLabel}`);
|
|
789
1331
|
setChoice({
|
|
1332
|
+
experiment_id: `forced_${proposalId}`,
|
|
1333
|
+
label: context.variantLabel
|
|
1334
|
+
});
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const cached = isHeatmapMode ? null : readChoice(proposalId);
|
|
1338
|
+
if (cached) {
|
|
1339
|
+
const choiceData = {
|
|
790
1340
|
experiment_id: cached.experiment_id,
|
|
791
1341
|
label: cached.label
|
|
792
|
-
}
|
|
1342
|
+
};
|
|
1343
|
+
setChoice(choiceData);
|
|
1344
|
+
if (typeof window !== "undefined" && !isHeatmapMode) {
|
|
1345
|
+
try {
|
|
1346
|
+
window.localStorage.setItem("probat_active_proposal_id", proposalId);
|
|
1347
|
+
window.localStorage.setItem("probat_active_variant_label", cached.label);
|
|
1348
|
+
} catch (e) {
|
|
1349
|
+
console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
793
1352
|
} else {
|
|
794
1353
|
(async () => {
|
|
795
1354
|
try {
|
|
796
1355
|
const { experiment_id, label: label2 } = await fetchDecision(apiBaseUrl, proposalId);
|
|
797
1356
|
if (!alive) return;
|
|
798
|
-
|
|
799
|
-
|
|
1357
|
+
if (!isHeatmapMode) {
|
|
1358
|
+
writeChoice(proposalId, experiment_id, label2);
|
|
1359
|
+
if (typeof window !== "undefined") {
|
|
1360
|
+
try {
|
|
1361
|
+
window.localStorage.setItem("probat_active_proposal_id", proposalId);
|
|
1362
|
+
window.localStorage.setItem("probat_active_variant_label", label2);
|
|
1363
|
+
} catch (e) {
|
|
1364
|
+
console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const choiceData = { experiment_id, label: label2 };
|
|
1369
|
+
setChoice(choiceData);
|
|
800
1370
|
} catch (e) {
|
|
801
1371
|
if (!alive) return;
|
|
802
|
-
|
|
1372
|
+
const choiceData = {
|
|
803
1373
|
experiment_id: `exp_${proposalId}`,
|
|
804
1374
|
label: "control"
|
|
805
|
-
}
|
|
1375
|
+
};
|
|
1376
|
+
setChoice(choiceData);
|
|
1377
|
+
if (typeof window !== "undefined" && !isHeatmapMode) {
|
|
1378
|
+
try {
|
|
1379
|
+
window.localStorage.setItem("probat_active_proposal_id", proposalId);
|
|
1380
|
+
window.localStorage.setItem("probat_active_variant_label", "control");
|
|
1381
|
+
} catch (err) {
|
|
1382
|
+
console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", err);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
806
1385
|
}
|
|
807
1386
|
})();
|
|
808
1387
|
}
|
|
809
1388
|
return () => {
|
|
810
1389
|
alive = false;
|
|
811
1390
|
};
|
|
812
|
-
}, [proposalId, apiBaseUrl, useNewAPI, configLoading]);
|
|
1391
|
+
}, [proposalId, apiBaseUrl, useNewAPI, configLoading, context.proposalId, context.variantLabel]);
|
|
813
1392
|
React4.useEffect(() => {
|
|
814
1393
|
if (!proposalId) return;
|
|
815
1394
|
const lbl = choice?.label ?? "control";
|
|
@@ -865,10 +1444,13 @@ exports.ProbatProviderClient = ProbatProviderClient;
|
|
|
865
1444
|
exports.detectEnvironment = detectEnvironment;
|
|
866
1445
|
exports.extractClickMeta = extractClickMeta;
|
|
867
1446
|
exports.fetchDecision = fetchDecision;
|
|
1447
|
+
exports.getHeatmapTracker = getHeatmapTracker;
|
|
868
1448
|
exports.hasTrackedVisit = hasTrackedVisit;
|
|
1449
|
+
exports.initHeatmapTracking = initHeatmapTracking;
|
|
869
1450
|
exports.markTrackedVisit = markTrackedVisit;
|
|
870
1451
|
exports.readChoice = readChoice;
|
|
871
1452
|
exports.sendMetric = sendMetric;
|
|
1453
|
+
exports.stopHeatmapTracking = stopHeatmapTracking;
|
|
872
1454
|
exports.useExperiment = useExperiment;
|
|
873
1455
|
exports.useProbatContext = useProbatContext;
|
|
874
1456
|
exports.useProbatMetrics = useProbatMetrics;
|