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