@probat/react 0.2.0 → 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/dist/index.mjs CHANGED
@@ -37,25 +37,23 @@ var HeatmapTracker = class {
37
37
  if (!this.config.enabled || !this.config.trackCursor) return;
38
38
  if (typeof window === "undefined") return;
39
39
  const stored = getStoredExperimentInfo();
40
- const proposalId = this.config.proposalId || stored.proposalId;
41
- const variantLabel = this.config.variantLabel || stored.variantLabel;
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
42
  const now2 = Date.now();
43
43
  const cursorThrottle = this.config.cursorThrottle ?? 100;
44
44
  if (now2 - this.lastCursorTime < cursorThrottle) {
45
45
  return;
46
46
  }
47
47
  this.lastCursorTime = now2;
48
- if (this.movements.length < 3) {
49
- console.log("[PROBAT Heatmap] Cursor movement captured:", {
50
- x: event.clientX,
51
- y: event.clientY,
52
- movementsInBatch: this.movements.length + 1
53
- });
54
- }
48
+ console.log("[PROBAT Heatmap] Cursor movement captured:", {
49
+ x: event.pageX,
50
+ y: event.pageY,
51
+ movementsInBatch: this.movements.length + 1
52
+ });
55
53
  const pageUrl = window.location.href;
56
54
  const siteUrl = window.location.origin;
57
- const x = event.clientX;
58
- const y = event.clientY;
55
+ const x = event.pageX;
56
+ const y = event.pageY;
59
57
  const viewportWidth = window.innerWidth;
60
58
  const viewportHeight = window.innerHeight;
61
59
  const movementEvent = {
@@ -66,8 +64,8 @@ var HeatmapTracker = class {
66
64
  viewport_width: viewportWidth,
67
65
  viewport_height: viewportHeight,
68
66
  session_id: this.sessionId,
69
- proposal_id: proposalId,
70
- variant_label: variantLabel
67
+ proposal_id: activeProposalId,
68
+ variant_label: activeVariantLabel
71
69
  };
72
70
  this.movements.push(movementEvent);
73
71
  const cursorBatchSize = this.config.cursorBatchSize ?? 50;
@@ -81,8 +79,8 @@ var HeatmapTracker = class {
81
79
  if (!this.config.enabled) return;
82
80
  if (typeof window === "undefined") return;
83
81
  const stored = getStoredExperimentInfo();
84
- const proposalId = this.config.proposalId || stored.proposalId;
85
- const variantLabel = this.config.variantLabel || stored.variantLabel;
82
+ const activeProposalId = this.config.proposalId || stored.proposalId;
83
+ const activeVariantLabel = stored.proposalId === activeProposalId ? stored.variantLabel || this.config.variantLabel : this.config.variantLabel || stored.variantLabel;
86
84
  const target = event.target;
87
85
  if (!target) return;
88
86
  if (this.shouldExcludeElement(target)) {
@@ -90,8 +88,8 @@ var HeatmapTracker = class {
90
88
  }
91
89
  const pageUrl = window.location.href;
92
90
  const siteUrl = window.location.origin;
93
- const x = event.clientX;
94
- const y = event.clientY;
91
+ const x = event.pageX;
92
+ const y = event.pageY;
95
93
  const viewportWidth = window.innerWidth;
96
94
  const viewportHeight = window.innerHeight;
97
95
  const elementInfo = this.extractElementInfo(target);
@@ -106,8 +104,8 @@ var HeatmapTracker = class {
106
104
  element_class: elementInfo.class,
107
105
  element_id: elementInfo.id,
108
106
  session_id: this.sessionId,
109
- proposal_id: proposalId,
110
- variant_label: variantLabel
107
+ proposal_id: activeProposalId,
108
+ variant_label: activeVariantLabel
111
109
  };
112
110
  this.clicks.push(clickEvent);
113
111
  const batchSize = this.config.batchSize ?? 10;
@@ -257,6 +255,17 @@ var HeatmapTracker = class {
257
255
  if (typeof window === "undefined") {
258
256
  return;
259
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
+ }
260
269
  document.addEventListener("click", this.handleClick, true);
261
270
  if (this.config.trackCursor) {
262
271
  document.addEventListener("mousemove", this.handleMouseMove, { passive: true });
@@ -265,12 +274,7 @@ var HeatmapTracker = class {
265
274
  console.log("[PROBAT Heatmap] Cursor tracking disabled");
266
275
  }
267
276
  this.isInitialized = true;
268
- console.log("[PROBAT Heatmap] Tracker initialized", {
269
- enabled: this.config.enabled,
270
- trackCursor: this.config.trackCursor,
271
- cursorThrottle: this.config.cursorThrottle,
272
- cursorBatchSize: this.config.cursorBatchSize
273
- });
277
+ console.log("[PROBAT Heatmap] Tracker initialized");
274
278
  window.addEventListener("beforeunload", () => {
275
279
  if (this.clicks.length > 0) {
276
280
  const siteUrl = this.clicks[0]?.site_url || window.location.origin;
@@ -324,6 +328,141 @@ var HeatmapTracker = class {
324
328
  }
325
329
  this.isInitialized = false;
326
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
+ }
327
466
  };
328
467
  var trackerInstance = null;
329
468
  function initHeatmapTracking(config) {
@@ -361,13 +500,24 @@ function ProbatProvider({
361
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";
362
501
  const environment = explicitEnvironment || detectEnvironment();
363
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);
364
514
  return {
365
515
  apiBaseUrl: resolvedApiBaseUrl,
366
516
  environment,
367
517
  clientKey,
368
518
  repoFullName: resolvedRepoFullName,
369
- proposalId: proposalId || storedProposalId,
370
- variantLabel: variantLabel || storedVariantLabel
519
+ proposalId: finalProposalId,
520
+ variantLabel: finalVariantLabel
371
521
  };
372
522
  }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName, proposalId, variantLabel, storedProposalId, storedVariantLabel]);
373
523
  useEffect(() => {
@@ -396,7 +546,7 @@ function ProbatProvider({
396
546
  return () => {
397
547
  stopHeatmapTracking();
398
548
  };
399
- }, [contextValue.apiBaseUrl]);
549
+ }, [contextValue.apiBaseUrl, contextValue.proposalId, contextValue.variantLabel]);
400
550
  return /* @__PURE__ */ React4.createElement(ProbatContext.Provider, { value: contextValue }, children);
401
551
  }
402
552
  function useProbatContext() {
@@ -978,14 +1128,25 @@ function markTrackedVisit(proposalId, label) {
978
1128
 
979
1129
  // src/hooks/useExperiment.ts
980
1130
  function useExperiment(proposalId, options) {
981
- const { apiBaseUrl } = useProbatContext();
1131
+ const context = useProbatContext();
1132
+ const { apiBaseUrl } = context;
982
1133
  const [choice, setChoice] = useState(null);
983
1134
  const [isLoading, setIsLoading] = useState(true);
984
1135
  const [error, setError] = useState(null);
985
1136
  const autoTrackImpression = options?.autoTrackImpression !== false;
986
1137
  useEffect(() => {
987
1138
  let alive = true;
988
- const cached = readChoice(proposalId);
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);
989
1150
  if (cached) {
990
1151
  setChoice({ experiment_id: cached.experiment_id, label: cached.label });
991
1152
  setIsLoading(false);
@@ -998,7 +1159,9 @@ function useExperiment(proposalId, options) {
998
1159
  proposalId
999
1160
  );
1000
1161
  if (!alive) return;
1001
- writeChoice(proposalId, experiment_id, label);
1162
+ if (!isHeatmapMode) {
1163
+ writeChoice(proposalId, experiment_id, label);
1164
+ }
1002
1165
  setChoice({ experiment_id, label });
1003
1166
  setError(null);
1004
1167
  } catch (e) {
@@ -1019,7 +1182,7 @@ function useExperiment(proposalId, options) {
1019
1182
  return () => {
1020
1183
  alive = false;
1021
1184
  };
1022
- }, [proposalId, apiBaseUrl]);
1185
+ }, [proposalId, apiBaseUrl, context.proposalId, context.variantLabel]);
1023
1186
  useEffect(() => {
1024
1187
  if (!autoTrackImpression || !choice) return;
1025
1188
  const exp = choice.experiment_id;
@@ -1155,14 +1318,23 @@ function withExperiment(Control, options) {
1155
1318
  if (!proposalId) return;
1156
1319
  if (useNewAPI && configLoading) return;
1157
1320
  let alive = true;
1158
- const cached = readChoice(proposalId);
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}`);
1324
+ setChoice({
1325
+ experiment_id: `forced_${proposalId}`,
1326
+ label: context.variantLabel
1327
+ });
1328
+ return;
1329
+ }
1330
+ const cached = isHeatmapMode ? null : readChoice(proposalId);
1159
1331
  if (cached) {
1160
1332
  const choiceData = {
1161
1333
  experiment_id: cached.experiment_id,
1162
1334
  label: cached.label
1163
1335
  };
1164
1336
  setChoice(choiceData);
1165
- if (typeof window !== "undefined") {
1337
+ if (typeof window !== "undefined" && !isHeatmapMode) {
1166
1338
  try {
1167
1339
  window.localStorage.setItem("probat_active_proposal_id", proposalId);
1168
1340
  window.localStorage.setItem("probat_active_variant_label", cached.label);
@@ -1175,17 +1347,19 @@ function withExperiment(Control, options) {
1175
1347
  try {
1176
1348
  const { experiment_id, label: label2 } = await fetchDecision(apiBaseUrl, proposalId);
1177
1349
  if (!alive) return;
1178
- writeChoice(proposalId, experiment_id, label2);
1179
- const choiceData = { experiment_id, label: label2 };
1180
- setChoice(choiceData);
1181
- if (typeof window !== "undefined") {
1182
- try {
1183
- window.localStorage.setItem("probat_active_proposal_id", proposalId);
1184
- window.localStorage.setItem("probat_active_variant_label", label2);
1185
- } catch (e) {
1186
- console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
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
+ }
1187
1359
  }
1188
1360
  }
1361
+ const choiceData = { experiment_id, label: label2 };
1362
+ setChoice(choiceData);
1189
1363
  } catch (e) {
1190
1364
  if (!alive) return;
1191
1365
  const choiceData = {
@@ -1193,7 +1367,7 @@ function withExperiment(Control, options) {
1193
1367
  label: "control"
1194
1368
  };
1195
1369
  setChoice(choiceData);
1196
- if (typeof window !== "undefined") {
1370
+ if (typeof window !== "undefined" && !isHeatmapMode) {
1197
1371
  try {
1198
1372
  window.localStorage.setItem("probat_active_proposal_id", proposalId);
1199
1373
  window.localStorage.setItem("probat_active_variant_label", "control");
@@ -1207,7 +1381,7 @@ function withExperiment(Control, options) {
1207
1381
  return () => {
1208
1382
  alive = false;
1209
1383
  };
1210
- }, [proposalId, apiBaseUrl, useNewAPI, configLoading]);
1384
+ }, [proposalId, apiBaseUrl, useNewAPI, configLoading, context.proposalId, context.variantLabel]);
1211
1385
  useEffect(() => {
1212
1386
  if (!proposalId) return;
1213
1387
  const lbl = choice?.label ?? "control";