@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/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  "use client";
3
- import React4, { createContext, useMemo, useContext, useCallback, useState, useEffect } from 'react';
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, clientKey, explicitEnvironment, explicitRepoFullName]);
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 { apiBaseUrl } = useProbatContext();
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 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);
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
- writeChoice(proposalId, experiment_id, label);
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 cached = readChoice(proposalId);
781
- if (cached) {
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
- writeChoice(proposalId, experiment_id, label2);
792
- setChoice({ experiment_id, label: label2 });
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
- setChoice({
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