@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.d.mts CHANGED
@@ -229,6 +229,29 @@ declare class HeatmapTracker {
229
229
  private sendBatch;
230
230
  init(): void;
231
231
  stop(): void;
232
+ /**
233
+ * Enable visualization mode
234
+ */
235
+ /**
236
+ * Enable visualization mode
237
+ */
238
+ private enableVisualization;
239
+ /**
240
+ * Render heatmap overlay on valid points
241
+ */
242
+ private renderHeatmapOverlay;
243
+ /**
244
+ * Draw points on canvas
245
+ */
246
+ private renderPoints;
247
+ /**
248
+ * Get heatmap color based on intensity
249
+ */
250
+ private getHeatmapColor;
251
+ /**
252
+ * Convert RGB to RGBA
253
+ */
254
+ private rgbToRgba;
232
255
  }
233
256
  /**
234
257
  * Initialize heatmap tracking for user websites
package/dist/index.d.ts CHANGED
@@ -229,6 +229,29 @@ declare class HeatmapTracker {
229
229
  private sendBatch;
230
230
  init(): void;
231
231
  stop(): void;
232
+ /**
233
+ * Enable visualization mode
234
+ */
235
+ /**
236
+ * Enable visualization mode
237
+ */
238
+ private enableVisualization;
239
+ /**
240
+ * Render heatmap overlay on valid points
241
+ */
242
+ private renderHeatmapOverlay;
243
+ /**
244
+ * Draw points on canvas
245
+ */
246
+ private renderPoints;
247
+ /**
248
+ * Get heatmap color based on intensity
249
+ */
250
+ private getHeatmapColor;
251
+ /**
252
+ * Convert RGB to RGBA
253
+ */
254
+ private rgbToRgba;
232
255
  }
233
256
  /**
234
257
  * Initialize heatmap tracking for user websites
package/dist/index.js CHANGED
@@ -44,25 +44,23 @@ var HeatmapTracker = class {
44
44
  if (!this.config.enabled || !this.config.trackCursor) return;
45
45
  if (typeof window === "undefined") return;
46
46
  const stored = getStoredExperimentInfo();
47
- const proposalId = this.config.proposalId || stored.proposalId;
48
- const variantLabel = this.config.variantLabel || stored.variantLabel;
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
49
  const now2 = Date.now();
50
50
  const cursorThrottle = this.config.cursorThrottle ?? 100;
51
51
  if (now2 - this.lastCursorTime < cursorThrottle) {
52
52
  return;
53
53
  }
54
54
  this.lastCursorTime = now2;
55
- if (this.movements.length < 3) {
56
- console.log("[PROBAT Heatmap] Cursor movement captured:", {
57
- x: event.clientX,
58
- y: event.clientY,
59
- movementsInBatch: this.movements.length + 1
60
- });
61
- }
55
+ console.log("[PROBAT Heatmap] Cursor movement captured:", {
56
+ x: event.pageX,
57
+ y: event.pageY,
58
+ movementsInBatch: this.movements.length + 1
59
+ });
62
60
  const pageUrl = window.location.href;
63
61
  const siteUrl = window.location.origin;
64
- const x = event.clientX;
65
- const y = event.clientY;
62
+ const x = event.pageX;
63
+ const y = event.pageY;
66
64
  const viewportWidth = window.innerWidth;
67
65
  const viewportHeight = window.innerHeight;
68
66
  const movementEvent = {
@@ -73,8 +71,8 @@ var HeatmapTracker = class {
73
71
  viewport_width: viewportWidth,
74
72
  viewport_height: viewportHeight,
75
73
  session_id: this.sessionId,
76
- proposal_id: proposalId,
77
- variant_label: variantLabel
74
+ proposal_id: activeProposalId,
75
+ variant_label: activeVariantLabel
78
76
  };
79
77
  this.movements.push(movementEvent);
80
78
  const cursorBatchSize = this.config.cursorBatchSize ?? 50;
@@ -88,8 +86,8 @@ var HeatmapTracker = class {
88
86
  if (!this.config.enabled) return;
89
87
  if (typeof window === "undefined") return;
90
88
  const stored = getStoredExperimentInfo();
91
- const proposalId = this.config.proposalId || stored.proposalId;
92
- const variantLabel = this.config.variantLabel || stored.variantLabel;
89
+ const activeProposalId = this.config.proposalId || stored.proposalId;
90
+ const activeVariantLabel = stored.proposalId === activeProposalId ? stored.variantLabel || this.config.variantLabel : this.config.variantLabel || stored.variantLabel;
93
91
  const target = event.target;
94
92
  if (!target) return;
95
93
  if (this.shouldExcludeElement(target)) {
@@ -97,8 +95,8 @@ var HeatmapTracker = class {
97
95
  }
98
96
  const pageUrl = window.location.href;
99
97
  const siteUrl = window.location.origin;
100
- const x = event.clientX;
101
- const y = event.clientY;
98
+ const x = event.pageX;
99
+ const y = event.pageY;
102
100
  const viewportWidth = window.innerWidth;
103
101
  const viewportHeight = window.innerHeight;
104
102
  const elementInfo = this.extractElementInfo(target);
@@ -113,8 +111,8 @@ var HeatmapTracker = class {
113
111
  element_class: elementInfo.class,
114
112
  element_id: elementInfo.id,
115
113
  session_id: this.sessionId,
116
- proposal_id: proposalId,
117
- variant_label: variantLabel
114
+ proposal_id: activeProposalId,
115
+ variant_label: activeVariantLabel
118
116
  };
119
117
  this.clicks.push(clickEvent);
120
118
  const batchSize = this.config.batchSize ?? 10;
@@ -264,6 +262,17 @@ var HeatmapTracker = class {
264
262
  if (typeof window === "undefined") {
265
263
  return;
266
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
+ }
267
276
  document.addEventListener("click", this.handleClick, true);
268
277
  if (this.config.trackCursor) {
269
278
  document.addEventListener("mousemove", this.handleMouseMove, { passive: true });
@@ -272,12 +281,7 @@ var HeatmapTracker = class {
272
281
  console.log("[PROBAT Heatmap] Cursor tracking disabled");
273
282
  }
274
283
  this.isInitialized = true;
275
- console.log("[PROBAT Heatmap] Tracker initialized", {
276
- enabled: this.config.enabled,
277
- trackCursor: this.config.trackCursor,
278
- cursorThrottle: this.config.cursorThrottle,
279
- cursorBatchSize: this.config.cursorBatchSize
280
- });
284
+ console.log("[PROBAT Heatmap] Tracker initialized");
281
285
  window.addEventListener("beforeunload", () => {
282
286
  if (this.clicks.length > 0) {
283
287
  const siteUrl = this.clicks[0]?.site_url || window.location.origin;
@@ -331,6 +335,141 @@ var HeatmapTracker = class {
331
335
  }
332
336
  this.isInitialized = false;
333
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
+ }
334
473
  };
335
474
  var trackerInstance = null;
336
475
  function initHeatmapTracking(config) {
@@ -368,13 +507,24 @@ function ProbatProvider({
368
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";
369
508
  const environment = explicitEnvironment || detectEnvironment();
370
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);
371
521
  return {
372
522
  apiBaseUrl: resolvedApiBaseUrl,
373
523
  environment,
374
524
  clientKey,
375
525
  repoFullName: resolvedRepoFullName,
376
- proposalId: proposalId || storedProposalId,
377
- variantLabel: variantLabel || storedVariantLabel
526
+ proposalId: finalProposalId,
527
+ variantLabel: finalVariantLabel
378
528
  };
379
529
  }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName, proposalId, variantLabel, storedProposalId, storedVariantLabel]);
380
530
  React4.useEffect(() => {
@@ -403,7 +553,7 @@ function ProbatProvider({
403
553
  return () => {
404
554
  stopHeatmapTracking();
405
555
  };
406
- }, [contextValue.apiBaseUrl]);
556
+ }, [contextValue.apiBaseUrl, contextValue.proposalId, contextValue.variantLabel]);
407
557
  return /* @__PURE__ */ React4__default.default.createElement(ProbatContext.Provider, { value: contextValue }, children);
408
558
  }
409
559
  function useProbatContext() {
@@ -985,14 +1135,25 @@ function markTrackedVisit(proposalId, label) {
985
1135
 
986
1136
  // src/hooks/useExperiment.ts
987
1137
  function useExperiment(proposalId, options) {
988
- const { apiBaseUrl } = useProbatContext();
1138
+ const context = useProbatContext();
1139
+ const { apiBaseUrl } = context;
989
1140
  const [choice, setChoice] = React4.useState(null);
990
1141
  const [isLoading, setIsLoading] = React4.useState(true);
991
1142
  const [error, setError] = React4.useState(null);
992
1143
  const autoTrackImpression = options?.autoTrackImpression !== false;
993
1144
  React4.useEffect(() => {
994
1145
  let alive = true;
995
- const cached = readChoice(proposalId);
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);
996
1157
  if (cached) {
997
1158
  setChoice({ experiment_id: cached.experiment_id, label: cached.label });
998
1159
  setIsLoading(false);
@@ -1005,7 +1166,9 @@ function useExperiment(proposalId, options) {
1005
1166
  proposalId
1006
1167
  );
1007
1168
  if (!alive) return;
1008
- writeChoice(proposalId, experiment_id, label);
1169
+ if (!isHeatmapMode) {
1170
+ writeChoice(proposalId, experiment_id, label);
1171
+ }
1009
1172
  setChoice({ experiment_id, label });
1010
1173
  setError(null);
1011
1174
  } catch (e) {
@@ -1026,7 +1189,7 @@ function useExperiment(proposalId, options) {
1026
1189
  return () => {
1027
1190
  alive = false;
1028
1191
  };
1029
- }, [proposalId, apiBaseUrl]);
1192
+ }, [proposalId, apiBaseUrl, context.proposalId, context.variantLabel]);
1030
1193
  React4.useEffect(() => {
1031
1194
  if (!autoTrackImpression || !choice) return;
1032
1195
  const exp = choice.experiment_id;
@@ -1162,14 +1325,23 @@ function withExperiment(Control, options) {
1162
1325
  if (!proposalId) return;
1163
1326
  if (useNewAPI && configLoading) return;
1164
1327
  let alive = true;
1165
- const cached = readChoice(proposalId);
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}`);
1331
+ setChoice({
1332
+ experiment_id: `forced_${proposalId}`,
1333
+ label: context.variantLabel
1334
+ });
1335
+ return;
1336
+ }
1337
+ const cached = isHeatmapMode ? null : readChoice(proposalId);
1166
1338
  if (cached) {
1167
1339
  const choiceData = {
1168
1340
  experiment_id: cached.experiment_id,
1169
1341
  label: cached.label
1170
1342
  };
1171
1343
  setChoice(choiceData);
1172
- if (typeof window !== "undefined") {
1344
+ if (typeof window !== "undefined" && !isHeatmapMode) {
1173
1345
  try {
1174
1346
  window.localStorage.setItem("probat_active_proposal_id", proposalId);
1175
1347
  window.localStorage.setItem("probat_active_variant_label", cached.label);
@@ -1182,17 +1354,19 @@ function withExperiment(Control, options) {
1182
1354
  try {
1183
1355
  const { experiment_id, label: label2 } = await fetchDecision(apiBaseUrl, proposalId);
1184
1356
  if (!alive) return;
1185
- writeChoice(proposalId, experiment_id, label2);
1186
- const choiceData = { experiment_id, label: label2 };
1187
- setChoice(choiceData);
1188
- if (typeof window !== "undefined") {
1189
- try {
1190
- window.localStorage.setItem("probat_active_proposal_id", proposalId);
1191
- window.localStorage.setItem("probat_active_variant_label", label2);
1192
- } catch (e) {
1193
- console.warn("[PROBAT] Failed to set proposal/variant in localStorage:", e);
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
+ }
1194
1366
  }
1195
1367
  }
1368
+ const choiceData = { experiment_id, label: label2 };
1369
+ setChoice(choiceData);
1196
1370
  } catch (e) {
1197
1371
  if (!alive) return;
1198
1372
  const choiceData = {
@@ -1200,7 +1374,7 @@ function withExperiment(Control, options) {
1200
1374
  label: "control"
1201
1375
  };
1202
1376
  setChoice(choiceData);
1203
- if (typeof window !== "undefined") {
1377
+ if (typeof window !== "undefined" && !isHeatmapMode) {
1204
1378
  try {
1205
1379
  window.localStorage.setItem("probat_active_proposal_id", proposalId);
1206
1380
  window.localStorage.setItem("probat_active_variant_label", "control");
@@ -1214,7 +1388,7 @@ function withExperiment(Control, options) {
1214
1388
  return () => {
1215
1389
  alive = false;
1216
1390
  };
1217
- }, [proposalId, apiBaseUrl, useNewAPI, configLoading]);
1391
+ }, [proposalId, apiBaseUrl, useNewAPI, configLoading, context.proposalId, context.variantLabel]);
1218
1392
  React4.useEffect(() => {
1219
1393
  if (!proposalId) return;
1220
1394
  const lbl = choice?.label ?? "control";