@probat/react 0.2.1 → 0.3.0

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,39 @@
1
1
  "use client";
2
2
  "use client";
3
- import React4, { createContext, useMemo, useEffect, useContext, useCallback, useState } from 'react';
3
+ import React3, { createContext, useRef, useState, useEffect, useMemo, useCallback, useContext } from 'react';
4
+
5
+ var ProbatContext = createContext(null);
6
+ var DEFAULT_HOST = "https://gushi.onrender.com";
7
+ function ProbatProvider({
8
+ userId,
9
+ host = DEFAULT_HOST,
10
+ bootstrap,
11
+ children
12
+ }) {
13
+ const value = useMemo(
14
+ () => ({
15
+ host: host.replace(/\/$/, ""),
16
+ userId,
17
+ bootstrap: bootstrap ?? {}
18
+ }),
19
+ [userId, host, bootstrap]
20
+ );
21
+ return /* @__PURE__ */ React3.createElement(ProbatContext.Provider, { value }, children);
22
+ }
23
+ function useProbatContext() {
24
+ const ctx = useContext(ProbatContext);
25
+ if (!ctx) {
26
+ throw new Error(
27
+ "useProbatContext must be used within <ProbatProviderClient>. Wrap your app with <ProbatProviderClient userId={...}>."
28
+ );
29
+ }
30
+ return ctx;
31
+ }
32
+
33
+ // src/components/ProbatProviderClient.tsx
34
+ function ProbatProviderClient(props) {
35
+ return React3.createElement(ProbatProvider, props);
36
+ }
4
37
 
5
38
  // src/utils/environment.ts
6
39
  function detectEnvironment() {
@@ -14,1424 +47,442 @@ function detectEnvironment() {
14
47
  return "prod";
15
48
  }
16
49
 
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 {};
50
+ // src/utils/eventContext.ts
51
+ var DISTINCT_ID_KEY = "probat:distinct_id";
52
+ var SESSION_ID_KEY = "probat:session_id";
53
+ var cachedDistinctId = null;
54
+ var cachedSessionId = null;
55
+ function generateId() {
56
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
57
+ return crypto.randomUUID();
58
+ }
59
+ const bytes = new Uint8Array(16);
60
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
61
+ crypto.getRandomValues(bytes);
62
+ } else {
63
+ for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256);
26
64
  }
65
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
27
66
  }
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);
67
+ function getDistinctId() {
68
+ if (cachedDistinctId) return cachedDistinctId;
69
+ if (typeof window === "undefined") return "server";
70
+ try {
71
+ const stored = localStorage.getItem(DISTINCT_ID_KEY);
72
+ if (stored) {
73
+ cachedDistinctId = stored;
74
+ return stored;
360
75
  }
76
+ } catch {
361
77
  }
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
- }
78
+ const id = `anon_${generateId()}`;
79
+ cachedDistinctId = id;
80
+ try {
81
+ localStorage.setItem(DISTINCT_ID_KEY, id);
82
+ } catch {
455
83
  }
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})`;
84
+ return id;
85
+ }
86
+ function getSessionId() {
87
+ if (cachedSessionId) return cachedSessionId;
88
+ if (typeof window === "undefined") return "server";
89
+ try {
90
+ const stored = sessionStorage.getItem(SESSION_ID_KEY);
91
+ if (stored) {
92
+ cachedSessionId = stored;
93
+ return stored;
463
94
  }
464
- return rgb;
95
+ } catch {
465
96
  }
466
- };
467
- var trackerInstance = null;
468
- function initHeatmapTracking(config) {
469
- if (trackerInstance) {
470
- trackerInstance.stop();
97
+ const id = `sess_${generateId()}`;
98
+ cachedSessionId = id;
99
+ try {
100
+ sessionStorage.setItem(SESSION_ID_KEY, id);
101
+ } catch {
471
102
  }
472
- trackerInstance = new HeatmapTracker(config);
473
- trackerInstance.init();
474
- return trackerInstance;
103
+ return id;
475
104
  }
476
- function getHeatmapTracker() {
477
- return trackerInstance;
105
+ function getPageKey() {
106
+ if (typeof window === "undefined") return "";
107
+ return window.location.pathname + window.location.search;
478
108
  }
479
- function stopHeatmapTracking() {
480
- if (trackerInstance) {
481
- trackerInstance.stop();
482
- trackerInstance = null;
483
- }
109
+ function getPageUrl() {
110
+ if (typeof window === "undefined") return "";
111
+ return window.location.href;
484
112
  }
485
-
486
- // src/context/ProbatContext.tsx
487
- var ProbatContext = createContext(null);
488
- function ProbatProvider({
489
- apiBaseUrl,
490
- clientKey,
491
- environment: explicitEnvironment,
492
- repoFullName: explicitRepoFullName,
493
- proposalId,
494
- variantLabel,
495
- children
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;
499
- const contextValue = useMemo(() => {
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";
501
- const environment = explicitEnvironment || detectEnvironment();
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);
514
- return {
515
- apiBaseUrl: resolvedApiBaseUrl,
516
- environment,
517
- clientKey,
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();
548
- };
549
- }, [contextValue.apiBaseUrl, contextValue.proposalId, contextValue.variantLabel]);
550
- return /* @__PURE__ */ React4.createElement(ProbatContext.Provider, { value: contextValue }, children);
551
- }
552
- function useProbatContext() {
553
- const context = useContext(ProbatContext);
554
- if (!context) {
555
- throw new Error(
556
- "useProbatContext must be used within a ProbatProvider. Please wrap your app with <ProbatProvider>."
557
- );
558
- }
559
- return context;
113
+ function getReferrer() {
114
+ if (typeof document === "undefined") return "";
115
+ return document.referrer;
560
116
  }
561
- function ProbatProviderClient(props) {
562
- return React4.createElement(ProbatProvider, props);
117
+ function buildEventContext() {
118
+ return {
119
+ distinct_id: getDistinctId(),
120
+ session_id: getSessionId(),
121
+ $page_url: getPageUrl(),
122
+ $pathname: typeof window !== "undefined" ? window.location.pathname : "",
123
+ $referrer: getReferrer()
124
+ };
563
125
  }
564
- var pendingFetches = /* @__PURE__ */ new Map();
565
- async function fetchDecision(baseUrl, proposalId) {
566
- const existingFetch = pendingFetches.get(proposalId);
567
- if (existingFetch) {
568
- return existingFetch;
569
- }
570
- const fetchPromise = (async () => {
126
+
127
+ // src/utils/api.ts
128
+ var pendingDecisions = /* @__PURE__ */ new Map();
129
+ async function fetchDecision(host, experimentId, distinctId) {
130
+ const existing = pendingDecisions.get(experimentId);
131
+ if (existing) return existing;
132
+ const promise = (async () => {
571
133
  try {
572
- const url = `${baseUrl.replace(/\/$/, "")}/retrieve_react_experiment/${encodeURIComponent(proposalId)}`;
134
+ const url = `${host.replace(/\/$/, "")}/experiment/decide`;
573
135
  const res = await fetch(url, {
574
136
  method: "POST",
575
- headers: { Accept: "application/json" },
576
- credentials: "include"
577
- // Include cookies for user identification
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ Accept: "application/json"
140
+ },
141
+ credentials: "include",
142
+ body: JSON.stringify({
143
+ experiment_id: experimentId,
144
+ distinct_id: distinctId
145
+ })
578
146
  });
579
147
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
580
148
  const data = await res.json();
581
- const experiment_id = (data.experiment_id || `exp_${proposalId}`).toString();
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
- }
591
- return { experiment_id, label };
149
+ return data.variant_key || "control";
592
150
  } finally {
593
- pendingFetches.delete(proposalId);
151
+ pendingDecisions.delete(experimentId);
594
152
  }
595
153
  })();
596
- pendingFetches.set(proposalId, fetchPromise);
597
- return fetchPromise;
154
+ pendingDecisions.set(experimentId, promise);
155
+ return promise;
598
156
  }
599
- async function sendMetric(baseUrl, proposalId, metricName, variantLabel = "control", experimentId, dimensions = {}) {
600
- const url = `${baseUrl.replace(/\/$/, "")}/send_metrics/${encodeURIComponent(proposalId)}`;
601
- const body = {
602
- experiment_id: experimentId ?? null,
603
- variant_label: variantLabel,
604
- metric_name: metricName,
605
- metric_value: 1,
606
- metric_unit: "count",
607
- source: "react",
608
- environment: detectEnvironment(),
609
- // Include environment (dev or prod)
610
- dimensions,
611
- captured_at: (/* @__PURE__ */ new Date()).toISOString()
157
+ function sendMetric(host, event, properties) {
158
+ if (typeof window === "undefined") return;
159
+ const ctx = buildEventContext();
160
+ const payload = {
161
+ event,
162
+ properties: {
163
+ ...ctx,
164
+ environment: detectEnvironment(),
165
+ source: "react-sdk",
166
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
167
+ ...properties
168
+ }
612
169
  };
613
170
  try {
614
- await fetch(url, {
171
+ const url = `${host.replace(/\/$/, "")}/experiment/metrics`;
172
+ fetch(url, {
615
173
  method: "POST",
616
- headers: {
617
- Accept: "application/json",
618
- "Content-Type": "application/json"
619
- },
174
+ headers: { "Content-Type": "application/json" },
620
175
  credentials: "include",
621
- // CRITICAL: Include cookies to distinguish different users
622
- body: JSON.stringify(body)
176
+ body: JSON.stringify(payload)
177
+ }).catch(() => {
623
178
  });
624
179
  } catch {
625
180
  }
626
181
  }
627
- function extractClickMeta(event) {
628
- if (!event || !event.target) return void 0;
629
- const rawTarget = event.target;
630
- if (!rawTarget) return void 0;
631
- const actionable = rawTarget.closest(
632
- "[data-probat-conversion='true'], button, a, [role='button']"
633
- );
634
- if (!actionable) return void 0;
182
+ function extractClickMeta(target) {
183
+ if (!target || !(target instanceof HTMLElement)) return null;
184
+ const primary = target.closest('[data-probat-click="primary"]');
185
+ if (primary) return buildMeta(primary, true);
186
+ const interactive = target.closest('button, a, [role="button"]');
187
+ if (interactive) return buildMeta(interactive, false);
188
+ return null;
189
+ }
190
+ function buildMeta(el, isPrimary) {
635
191
  const meta = {
636
- target_tag: actionable.tagName
192
+ click_target_tag: el.tagName,
193
+ click_is_primary: isPrimary
637
194
  };
638
- if (actionable.id) meta.target_id = actionable.id;
639
- const attr = actionable.getAttribute("data-probat-conversion");
640
- if (attr) meta.conversion_attr = attr;
641
- const text = actionable.textContent?.trim();
642
- if (text) meta.target_text = text.slice(0, 120);
195
+ if (el.id) meta.click_target_id = el.id;
196
+ const text = el.textContent?.trim();
197
+ if (text) meta.click_target_text = text.slice(0, 120);
643
198
  return meta;
644
199
  }
645
- var componentConfigCache = /* @__PURE__ */ new Map();
646
- async function fetchComponentExperimentConfig(baseUrl, repoFullName, componentPath) {
647
- const cacheKey = `${repoFullName}:${componentPath}`;
648
- const existingFetch = componentConfigCache.get(cacheKey);
649
- if (existingFetch) {
650
- return existingFetch;
651
- }
652
- const fetchPromise = (async () => {
653
- try {
654
- const url = new URL(`${baseUrl.replace(/\/$/, "")}/get_component_experiment_config`);
655
- url.searchParams.set("repo_full_name", repoFullName);
656
- url.searchParams.set("component_path", componentPath);
657
- const res = await fetch(url.toString(), {
658
- method: "GET",
659
- headers: { Accept: "application/json" },
660
- credentials: "include"
661
- });
662
- if (res.status === 404) {
663
- return null;
664
- }
665
- if (!res.ok) {
666
- throw new Error(`HTTP ${res.status}`);
667
- }
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
- }
676
- return data;
677
- } catch (e) {
678
- console.warn(`[PROBAT] Failed to fetch component config: ${e}`);
679
- return null;
680
- } finally {
681
- componentConfigCache.delete(cacheKey);
682
- }
683
- })();
684
- componentConfigCache.set(cacheKey, fetchPromise);
685
- return fetchPromise;
200
+
201
+ // src/utils/dedupeStorage.ts
202
+ var PREFIX = "probat:seen:";
203
+ var memorySet = /* @__PURE__ */ new Set();
204
+ function makeDedupeKey(experimentId, variantKey, instanceId, pageKey) {
205
+ return `${PREFIX}${experimentId}:${variantKey}:${instanceId}:${pageKey}`;
686
206
  }
687
- var variantComponentCache = /* @__PURE__ */ new Map();
688
- var moduleCache = /* @__PURE__ */ new Map();
689
- if (typeof window !== "undefined") {
690
- window.__probatReact = React4;
691
- window.React = window.React || React4;
207
+ function hasSeen(key) {
208
+ if (memorySet.has(key)) return true;
209
+ if (typeof window === "undefined") return false;
210
+ try {
211
+ return sessionStorage.getItem(key) === "1";
212
+ } catch {
213
+ return false;
214
+ }
692
215
  }
693
- function resolveRelativePath(relativePath, basePath) {
694
- const baseDir = basePath.substring(0, basePath.lastIndexOf("/"));
695
- const parts = baseDir.split("/").filter(Boolean);
696
- const relativeParts = relativePath.split("/").filter(Boolean);
697
- for (const part of relativeParts) {
698
- if (part === "..") {
699
- parts.pop();
700
- } else if (part !== ".") {
701
- parts.push(part);
702
- }
216
+ function markSeen(key) {
217
+ memorySet.add(key);
218
+ if (typeof window === "undefined") return;
219
+ try {
220
+ sessionStorage.setItem(key, "1");
221
+ } catch {
703
222
  }
704
- return "/" + parts.join("/");
705
223
  }
706
- async function loadRelativeModule(absolutePath, baseFilePath, repoFullName, baseRef) {
707
- const cacheKey = `${baseFilePath}:${absolutePath}`;
708
- if (moduleCache.has(cacheKey)) {
709
- return moduleCache.get(cacheKey);
224
+ var INSTANCE_PREFIX = "probat:instance:";
225
+ function shortId() {
226
+ const bytes = new Uint8Array(4);
227
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
228
+ crypto.getRandomValues(bytes);
229
+ } else {
230
+ for (let i = 0; i < 4; i++) bytes[i] = Math.floor(Math.random() * 256);
710
231
  }
711
- const extensions = [".jsx", ".tsx", ".js", ".ts"];
712
- let moduleCode = null;
713
- let modulePath = null;
714
- for (const ext of extensions) {
715
- const testPath = absolutePath + (absolutePath.includes(".") ? "" : ext);
232
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
233
+ }
234
+ function resolveStableId(storageKey) {
235
+ if (typeof window !== "undefined") {
716
236
  try {
717
- const localRes = await fetch(testPath, {
718
- method: "GET",
719
- headers: { Accept: "text/plain" }
720
- });
721
- if (localRes.ok) {
722
- moduleCode = await localRes.text();
723
- modulePath = testPath;
724
- break;
725
- }
237
+ const stored = sessionStorage.getItem(storageKey);
238
+ if (stored) return stored;
726
239
  } catch {
727
240
  }
728
- if (!moduleCode && repoFullName) {
729
- try {
730
- const githubPath = testPath.startsWith("/") ? testPath.substring(1) : testPath;
731
- const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${baseRef || "main"}/${githubPath}`;
732
- const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
733
- if (res.ok) {
734
- moduleCode = await res.text();
735
- modulePath = testPath;
736
- break;
737
- }
738
- } catch {
739
- }
740
- }
741
- }
742
- if (!moduleCode || !modulePath) {
743
- throw new Error(`Could not resolve module: ${absolutePath} from ${baseFilePath}`);
744
241
  }
745
- let Babel;
746
- if (typeof window !== "undefined" && window.Babel) {
747
- Babel = window.Babel;
748
- } else {
242
+ const id = `inst_${shortId()}`;
243
+ if (typeof window !== "undefined") {
749
244
  try {
750
- const babelModule = await import('@babel/standalone');
751
- Babel = babelModule.default || babelModule;
245
+ sessionStorage.setItem(storageKey, id);
752
246
  } catch {
753
- throw new Error("Babel not available for compiling relative import");
754
247
  }
755
248
  }
756
- let processedCode = moduleCode;
757
- processedCode = processedCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
758
- processedCode = processedCode.replace(/^import\s+React\s+from\s+['"]react['"];?\s*$/m, "const React = window.React || globalThis.React;");
759
- processedCode = processedCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
760
- processedCode = processedCode.replace(/\bimport\.meta\b/g, "({})");
761
- const isTSX = modulePath.endsWith(".tsx");
762
- const compiled = Babel.transform(processedCode, {
763
- presets: [
764
- ["react", { runtime: "classic" }],
765
- ["typescript", { allExtensions: true, isTSX }]
766
- ],
767
- plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
768
- sourceType: "module",
769
- filename: modulePath
770
- }).code;
771
- const moduleCodeWrapper = `
772
- var require = function(name) {
773
- if (name === "react" || name === "react/jsx-runtime") {
774
- return window.React || globalThis.React;
775
- }
776
- if (name.startsWith("/@vite") || name.includes("@vite/client")) {
777
- return {};
778
- }
779
- throw new Error("Unsupported module in relative import: " + name);
780
- };
781
- var module = { exports: {} };
782
- var exports = module.exports;
783
- ${compiled}
784
- return module.exports;
785
- `;
786
- const moduleExports = new Function(moduleCodeWrapper)();
787
- moduleCache.set(cacheKey, moduleExports);
788
- return moduleExports;
249
+ return id;
789
250
  }
790
- async function loadVariantComponent(baseUrl, proposalId, experimentId, filePath, repoFullName, baseRef) {
791
- if (!filePath) {
792
- return null;
793
- }
794
- const cacheKey = `${proposalId}:${experimentId}`;
795
- const existingLoad = variantComponentCache.get(cacheKey);
796
- if (existingLoad) {
797
- return existingLoad;
251
+ var slotCounters = /* @__PURE__ */ new Map();
252
+ var resetScheduled = false;
253
+ function claimSlot(groupKey) {
254
+ const idx = slotCounters.get(groupKey) ?? 0;
255
+ slotCounters.set(groupKey, idx + 1);
256
+ if (!resetScheduled) {
257
+ resetScheduled = true;
258
+ Promise.resolve().then(() => {
259
+ slotCounters.clear();
260
+ resetScheduled = false;
261
+ });
798
262
  }
799
- const loadPromise = (async () => {
800
- try {
801
- let code = "";
802
- let rawCode = "";
803
- let rawCodeFetched = false;
804
- const isBrowser = typeof window !== "undefined";
805
- const isNextJS = isBrowser && (window.__NEXT_DATA__ !== void 0 || window.__NEXT_LOADED_PAGES__ !== void 0 || typeof globalThis.__NEXT_DATA__ !== "undefined");
806
- if (isBrowser && !isNextJS) {
807
- try {
808
- const variantUrl = `/probat/${filePath}`;
809
- const dynamicImportFunc = new Function("url", "return import(url)");
810
- const mod = await dynamicImportFunc(variantUrl);
811
- const VariantComponent2 = mod?.default || mod;
812
- if (VariantComponent2 && typeof VariantComponent2 === "function") {
813
- console.log(`[PROBAT] \u2705 Loaded variant via dynamic import (CSR): ${variantUrl}`);
814
- return VariantComponent2;
815
- }
816
- } catch (dynamicImportError) {
817
- console.debug(`[PROBAT] Dynamic import failed, using fetch+babel:`, dynamicImportError);
818
- }
819
- }
820
- const localUrl = `/probat/${filePath}`;
821
- try {
822
- const localRes = await fetch(localUrl, {
823
- method: "GET",
824
- headers: { Accept: "text/plain" }
825
- });
826
- if (localRes.ok) {
827
- rawCode = await localRes.text();
828
- rawCodeFetched = true;
829
- console.log(`[PROBAT] \u2705 Loaded variant from local (user's repo): ${localUrl}`);
830
- }
831
- } catch {
832
- console.debug(`[PROBAT] Local file not available (${localUrl}), trying GitHub...`);
833
- }
834
- if (!rawCodeFetched && repoFullName) {
835
- const githubPath = `probat/${filePath}`;
836
- const gitRef = baseRef || "main";
837
- const githubUrl = `https://raw.githubusercontent.com/${repoFullName}/${gitRef}/${githubPath}`;
838
- const res = await fetch(githubUrl, { method: "GET", headers: { Accept: "text/plain" } });
839
- if (res.ok) {
840
- rawCode = await res.text();
841
- rawCodeFetched = true;
842
- console.log(`[PROBAT] \u26A0\uFE0F Loaded variant from GitHub (fallback): ${githubUrl}`);
843
- } else {
844
- console.warn(`[PROBAT] \u26A0\uFE0F GitHub fetch failed (${res.status}), falling back to server compilation`);
845
- }
846
- }
847
- if (rawCodeFetched && rawCode) {
848
- let Babel;
849
- if (typeof window !== "undefined" && window.Babel) {
850
- Babel = window.Babel;
851
- } else {
852
- try {
853
- const babelModule = await import('@babel/standalone');
854
- Babel = babelModule.default || babelModule;
855
- } catch (importError) {
856
- try {
857
- await new Promise((resolve, reject) => {
858
- if (typeof document === "undefined") {
859
- reject(new Error("Document not available"));
860
- return;
861
- }
862
- if (window.Babel) {
863
- Babel = window.Babel;
864
- resolve();
865
- return;
866
- }
867
- const script = document.createElement("script");
868
- script.src = "https://unpkg.com/@babel/standalone/babel.min.js";
869
- script.async = true;
870
- script.onload = () => {
871
- Babel = window.Babel;
872
- if (!Babel) reject(new Error("Babel not found after script load"));
873
- else resolve();
874
- };
875
- script.onerror = () => reject(new Error("Failed to load Babel from CDN"));
876
- document.head.appendChild(script);
877
- });
878
- } catch (babelError) {
879
- console.error("[PROBAT] Failed to load Babel, falling back to server compilation", babelError);
880
- rawCodeFetched = false;
881
- }
882
- }
883
- }
884
- if (rawCodeFetched && rawCode && Babel) {
885
- const isTSX = filePath.endsWith(".tsx");
886
- rawCode = rawCode.replace(/^import\s+['"].*\.css['"];?\s*$/gm, "");
887
- rawCode = rawCode.replace(/^import\s+.*from\s+['"].*\.css['"];?\s*$/gm, "");
888
- rawCode = rawCode.replace(
889
- /^import\s+React(?:\s*,\s*\{[^}]*\})?\s+from\s+['"]react['"];?\s*$/m,
890
- "const React = window.React || globalThis.React;"
891
- );
892
- rawCode = rawCode.replace(
893
- /^import\s+\*\s+as\s+React\s+from\s+['"]react['"];?\s*$/m,
894
- "const React = window.React || globalThis.React;"
895
- );
896
- rawCode = rawCode.replace(
897
- /^import\s+\{([^}]+)\}\s+from\s+['"]react['"];?\s*$/m,
898
- (match, imports) => `const {${imports}} = window.React || globalThis.React;`
899
- );
900
- rawCode = rawCode.replace(/import\.meta\.env\.[\w$]+/g, "undefined");
901
- rawCode = rawCode.replace(/\bimport\.meta\b/g, "({})");
902
- rawCode = rawCode.replace(/^import\s+.*\/@vite\/client.*$/gm, "");
903
- rawCode = rawCode.replace(/import\.meta\.hot(?:\.[\w$]+)*/g, "undefined");
904
- const relativeImportMap = /* @__PURE__ */ new Map();
905
- rawCode = rawCode.replace(
906
- /^import\s+(\w+)\s+from\s+['"](\.\.?\/[^'"]+)['"];?\s*$/gm,
907
- (match, importName, relativePath) => {
908
- const baseDir = filePath.substring(0, filePath.lastIndexOf("/"));
909
- const resolvedPath = resolveRelativePath(relativePath, baseDir);
910
- const absolutePath = resolvedPath.startsWith("/") ? resolvedPath : "/" + resolvedPath;
911
- relativeImportMap.set(importName, absolutePath);
912
- return `import ${importName} from "${absolutePath}";`;
913
- }
914
- );
915
- const compiled = Babel.transform(rawCode, {
916
- presets: [
917
- ["react", { runtime: "classic" }],
918
- ["typescript", { allExtensions: true, isTSX }]
919
- ],
920
- plugins: [["transform-modules-commonjs", { allowTopLevelThis: true }]],
921
- sourceType: "module",
922
- filename: filePath
923
- }).code;
924
- const relativeModules = {};
925
- if (relativeImportMap.size > 0) {
926
- for (const [importName, absolutePath] of relativeImportMap.entries()) {
927
- try {
928
- const moduleExports = await loadRelativeModule(absolutePath, filePath, repoFullName, baseRef);
929
- relativeModules[absolutePath] = moduleExports.default || moduleExports;
930
- } catch (err) {
931
- console.warn(`[PROBAT] Failed to load relative import ${absolutePath}:`, err);
932
- relativeModules[absolutePath] = null;
933
- }
934
- }
935
- }
936
- const relativeModulesJson = JSON.stringify(relativeModules);
937
- code = `
938
- var __probatVariant = (function() {
939
- var relativeModules = ${relativeModulesJson};
940
- var require = function(name) {
941
- if (name === "react" || name === "react/jsx-runtime") {
942
- return window.React || globalThis.React;
943
- }
944
- if (name.startsWith("/@vite") || name.includes("@vite/client") || name.includes(".vite/deps")) {
945
- return {};
946
- }
947
- if (name === "react/jsx-runtime.js") {
948
- return window.React || globalThis.React;
949
- }
950
- // Handle relative imports (now converted to absolute paths)
951
- if (name.startsWith("/") && relativeModules.hasOwnProperty(name)) {
952
- var mod = relativeModules[name];
953
- if (mod === null) {
954
- throw new Error("Failed to load module: " + name);
955
- }
956
- return mod;
957
- }
958
- throw new Error("Unsupported module: " + name);
959
- };
960
- var module = { exports: {} };
961
- var exports = module.exports;
962
- ${compiled}
963
- return module.exports.default || module.exports;
964
- })();
965
- `;
966
- } else {
967
- rawCodeFetched = false;
968
- code = "";
969
- }
970
- }
971
- if (!rawCodeFetched || code === "") {
972
- const variantUrl = `${baseUrl.replace(/\/$/, "")}/variants/${filePath}`;
973
- const serverRes = await fetch(variantUrl, {
974
- method: "GET",
975
- headers: { Accept: "text/javascript" },
976
- credentials: "include"
977
- });
978
- if (!serverRes.ok) {
979
- throw new Error(`HTTP ${serverRes.status}`);
980
- }
981
- code = await serverRes.text();
982
- }
983
- if (typeof window !== "undefined") {
984
- window.React = window.React || React4;
985
- }
986
- const evalFunc = new Function(`
987
- var __probatVariant;
988
- ${code}
989
- return __probatVariant;
990
- `);
991
- const result = evalFunc();
992
- const VariantComponent = result?.default || result;
993
- if (typeof VariantComponent === "function") {
994
- return VariantComponent;
995
- }
996
- console.warn("[PROBAT] Variant component is not a function", result);
997
- return null;
998
- } catch (e) {
999
- console.warn(`[PROBAT] Failed to load variant component: ${e}`);
1000
- return null;
1001
- } finally {
1002
- variantComponentCache.delete(cacheKey);
1003
- }
1004
- })();
1005
- variantComponentCache.set(cacheKey, loadPromise);
1006
- return loadPromise;
263
+ return idx;
1007
264
  }
1008
-
1009
- // src/hooks/useProbatMetrics.ts
1010
- function useProbatMetrics() {
1011
- const { apiBaseUrl } = useProbatContext();
1012
- const trackClick = useCallback(
1013
- (event, options) => {
1014
- const meta = extractClickMeta(event ?? void 0);
1015
- if (!options?.force && event && !meta) {
1016
- return false;
1017
- }
1018
- const proposalId = options?.proposalId;
1019
- const variantLabel = options?.variantLabel || "control";
1020
- if (!proposalId) {
1021
- console.warn(
1022
- "[Probat] trackClick called without proposalId. Provide it in options or use useExperiment hook."
1023
- );
1024
- return false;
1025
- }
1026
- void sendMetric(
1027
- apiBaseUrl,
1028
- proposalId,
1029
- "click",
1030
- variantLabel,
1031
- void 0,
1032
- { ...meta, ...options?.dimensions }
1033
- );
1034
- return true;
1035
- },
1036
- [apiBaseUrl]
1037
- );
1038
- const trackMetric = useCallback(
1039
- (metricName, proposalId, variantLabel = "control", dimensions = {}) => {
1040
- void sendMetric(
1041
- apiBaseUrl,
1042
- proposalId,
1043
- metricName,
1044
- variantLabel,
1045
- void 0,
1046
- dimensions
1047
- );
1048
- },
1049
- [apiBaseUrl]
1050
- );
1051
- const trackImpression = useCallback(
1052
- (proposalId, variantLabel = "control", experimentId) => {
1053
- void sendMetric(
1054
- apiBaseUrl,
1055
- proposalId,
1056
- "visit",
1057
- variantLabel,
1058
- experimentId
1059
- );
1060
- },
1061
- [apiBaseUrl]
1062
- );
1063
- return {
1064
- trackClick,
1065
- trackMetric,
1066
- trackImpression
1067
- };
265
+ function useStableInstanceIdV18(experimentId) {
266
+ const reactId = React3.useId();
267
+ const ref = useRef("");
268
+ if (!ref.current) {
269
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${reactId}`;
270
+ ref.current = resolveStableId(key);
271
+ }
272
+ return ref.current;
1068
273
  }
1069
-
1070
- // src/utils/storage.ts
1071
- var TTL_MS = 6 * 60 * 60 * 1e3;
1072
- function safeGet(k) {
1073
- try {
1074
- const raw = localStorage.getItem(k);
1075
- return raw ? JSON.parse(raw) : null;
1076
- } catch {
1077
- return null;
274
+ function useStableInstanceIdFallback(experimentId) {
275
+ const slotRef = useRef(-1);
276
+ const ref = useRef("");
277
+ if (slotRef.current === -1) {
278
+ slotRef.current = claimSlot(`${experimentId}:${getPageKey()}`);
1078
279
  }
1079
- }
1080
- function safeSet(k, v) {
1081
- try {
1082
- localStorage.setItem(k, JSON.stringify(v));
1083
- } catch {
280
+ if (!ref.current) {
281
+ const key = `${INSTANCE_PREFIX}${experimentId}:${getPageKey()}:${slotRef.current}`;
282
+ ref.current = resolveStableId(key);
1084
283
  }
284
+ return ref.current;
1085
285
  }
1086
- function now() {
1087
- return Date.now();
1088
- }
1089
- function fresh(ts) {
1090
- return now() - ts <= TTL_MS;
1091
- }
1092
- var KEY = (proposalId) => `probat_choice_v3:${proposalId}`;
1093
- var VISIT_KEY = (proposalId, label) => `probat_visit_v1:${proposalId}:${label}`;
1094
- function readChoice(proposalId) {
1095
- const c = safeGet(KEY(proposalId));
1096
- return c && fresh(c.ts) ? c : null;
1097
- }
1098
- function writeChoice(proposalId, experiment_id, label) {
1099
- safeSet(KEY(proposalId), { experiment_id, label, ts: now() });
1100
- }
1101
- var visitMemo = /* @__PURE__ */ new Set();
1102
- function hasTrackedVisit(proposalId, label) {
1103
- const key = VISIT_KEY(proposalId, label);
1104
- if (visitMemo.has(key)) return true;
286
+ var useStableInstanceId = typeof React3.useId === "function" ? useStableInstanceIdV18 : useStableInstanceIdFallback;
287
+
288
+ // src/components/Experiment.tsx
289
+ var ASSIGNMENT_PREFIX = "probat:assignment:";
290
+ function readAssignment(id) {
291
+ if (typeof window === "undefined") return null;
1105
292
  try {
1106
- const raw = localStorage.getItem(key);
1107
- if (!raw) return false;
1108
- const ts = Number(raw);
1109
- if (!Number.isFinite(ts) || ts <= 0) return false;
1110
- if (now() - ts > TTL_MS) {
1111
- localStorage.removeItem(key);
1112
- return false;
1113
- }
1114
- visitMemo.add(key);
1115
- return true;
293
+ const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);
294
+ if (!raw) return null;
295
+ const parsed = JSON.parse(raw);
296
+ return parsed.variantKey ?? null;
1116
297
  } catch {
1117
- return false;
298
+ return null;
1118
299
  }
1119
300
  }
1120
- function markTrackedVisit(proposalId, label) {
1121
- const key = VISIT_KEY(proposalId, label);
1122
- visitMemo.add(key);
301
+ function writeAssignment(id, variantKey) {
302
+ if (typeof window === "undefined") return;
1123
303
  try {
1124
- localStorage.setItem(key, now().toString());
304
+ const entry = { variantKey, ts: Date.now() };
305
+ localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));
1125
306
  } catch {
1126
307
  }
1127
308
  }
1128
-
1129
- // src/hooks/useExperiment.ts
1130
- function useExperiment(proposalId, options) {
1131
- const context = useProbatContext();
1132
- const { apiBaseUrl } = context;
1133
- const [choice, setChoice] = useState(null);
1134
- const [isLoading, setIsLoading] = useState(true);
1135
- const [error, setError] = useState(null);
1136
- const autoTrackImpression = options?.autoTrackImpression !== false;
309
+ function Experiment({
310
+ id,
311
+ control,
312
+ variants,
313
+ track,
314
+ componentInstanceId,
315
+ fallback = "control",
316
+ debug = false
317
+ }) {
318
+ const { host, bootstrap } = useProbatContext();
319
+ const autoInstanceId = useStableInstanceId(id);
320
+ const instanceId = componentInstanceId ?? autoInstanceId;
321
+ const trackImpression = track?.impression !== false;
322
+ const trackClick = track?.primaryClick !== false;
323
+ const impressionEvent = track?.impressionEventName ?? "$experiment_exposure";
324
+ const clickEvent = track?.clickEventName ?? "$experiment_click";
325
+ const [variantKey, setVariantKey] = useState(() => {
326
+ if (bootstrap[id]) return bootstrap[id];
327
+ const cached = readAssignment(id);
328
+ if (cached) return cached;
329
+ return "control";
330
+ });
331
+ const [resolved, setResolved] = useState(() => {
332
+ return !!(bootstrap[id] || readAssignment(id));
333
+ });
1137
334
  useEffect(() => {
1138
- let alive = true;
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);
335
+ if (bootstrap[id] || readAssignment(id)) {
336
+ const key = bootstrap[id] ?? readAssignment(id) ?? "control";
337
+ setVariantKey(key);
338
+ setResolved(true);
1147
339
  return;
1148
340
  }
1149
- const cached = isHeatmapMode ? null : readChoice(proposalId);
1150
- if (cached) {
1151
- setChoice({ experiment_id: cached.experiment_id, label: cached.label });
1152
- setIsLoading(false);
1153
- } else {
1154
- setIsLoading(true);
1155
- (async () => {
1156
- try {
1157
- const { experiment_id, label } = await fetchDecision(
1158
- apiBaseUrl,
1159
- proposalId
1160
- );
1161
- if (!alive) return;
1162
- if (!isHeatmapMode) {
1163
- writeChoice(proposalId, experiment_id, label);
1164
- }
1165
- setChoice({ experiment_id, label });
1166
- setError(null);
1167
- } catch (e) {
1168
- if (!alive) return;
1169
- const err = e instanceof Error ? e : new Error(String(e));
1170
- setError(err);
1171
- setChoice({
1172
- experiment_id: `exp_${proposalId}`,
1173
- label: "control"
1174
- });
1175
- } finally {
1176
- if (alive) {
1177
- setIsLoading(false);
341
+ let cancelled = false;
342
+ (async () => {
343
+ try {
344
+ const distinctId = getDistinctId();
345
+ const key = await fetchDecision(host, id, distinctId);
346
+ if (cancelled) return;
347
+ if (key !== "control" && !(key in variants)) {
348
+ if (debug) {
349
+ console.warn(
350
+ `[probat] Unknown variant "${key}" for experiment "${id}", falling back to control`
351
+ );
1178
352
  }
353
+ setVariantKey("control");
354
+ } else {
355
+ setVariantKey(key);
356
+ writeAssignment(id, key);
1179
357
  }
1180
- })();
1181
- }
358
+ } catch (err) {
359
+ if (cancelled) return;
360
+ if (debug) {
361
+ console.error(`[probat] fetchDecision failed for "${id}":`, err);
362
+ }
363
+ if (fallback === "suspend") throw err;
364
+ setVariantKey("control");
365
+ } finally {
366
+ if (!cancelled) setResolved(true);
367
+ }
368
+ })();
1182
369
  return () => {
1183
- alive = false;
370
+ cancelled = true;
1184
371
  };
1185
- }, [proposalId, apiBaseUrl, context.proposalId, context.variantLabel]);
372
+ }, [id, host]);
1186
373
  useEffect(() => {
1187
- if (!autoTrackImpression || !choice) return;
1188
- const exp = choice.experiment_id;
1189
- const lbl = choice.label ?? "control";
1190
- if (!lbl) return;
1191
- if (hasTrackedVisit(proposalId, lbl)) return;
1192
- markTrackedVisit(proposalId, lbl);
1193
- void sendMetric(apiBaseUrl, proposalId, "visit", lbl, exp);
1194
- }, [proposalId, choice?.experiment_id, choice?.label, apiBaseUrl, autoTrackImpression]);
1195
- const trackClick = useCallback(
1196
- (event) => {
1197
- choice?.experiment_id;
1198
- const lbl = choice?.label ?? "control";
1199
- const meta = event ? (() => {
1200
- const rawTarget = event.target;
1201
- if (!rawTarget) return void 0;
1202
- const actionable = rawTarget.closest(
1203
- "[data-probat-conversion='true'], button, a, [role='button']"
1204
- );
1205
- if (!actionable) return void 0;
1206
- const meta2 = {
1207
- target_tag: actionable.tagName
1208
- };
1209
- if (actionable.id) meta2.target_id = actionable.id;
1210
- const attr = actionable.getAttribute("data-probat-conversion");
1211
- if (attr) meta2.conversion_attr = attr;
1212
- const text = actionable.textContent?.trim();
1213
- if (text) meta2.target_text = text.slice(0, 120);
1214
- return meta2;
1215
- })() : void 0;
1216
- void sendMetric(apiBaseUrl, proposalId, "click", lbl, void 0, meta);
1217
- },
1218
- [proposalId, choice?.experiment_id, choice?.label, apiBaseUrl]
374
+ if (debug && resolved) {
375
+ console.log(`[probat] Experiment "${id}" \u2192 variant "${variantKey}"`, {
376
+ instanceId,
377
+ pageKey: getPageKey()
378
+ });
379
+ }
380
+ }, [debug, id, variantKey, resolved, instanceId]);
381
+ const eventProps = useMemo(
382
+ () => ({
383
+ experiment_id: id,
384
+ variant_key: variantKey,
385
+ component_instance_id: instanceId
386
+ }),
387
+ [id, variantKey, instanceId]
1219
388
  );
1220
- return {
1221
- variantLabel: choice?.label ?? "control",
1222
- experimentId: choice?.experiment_id ?? null,
1223
- isLoading,
1224
- error,
1225
- trackClick
1226
- };
1227
- }
1228
- function withExperiment(Control, options) {
1229
- if (!Control) {
1230
- console.error("[PROBAT] withExperiment: Control component is required");
1231
- return ((props) => null);
1232
- }
1233
- if (!options || typeof options !== "object") {
1234
- console.error("[PROBAT] withExperiment: options is required");
1235
- return Control;
1236
- }
1237
- const useNewAPI = !!options.componentPath;
1238
- const useOldAPI = !!(options.proposalId && options.registry);
1239
- if (!useNewAPI && !useOldAPI) {
1240
- console.warn("[PROBAT] withExperiment: Invalid config, returning Control");
1241
- return Control;
1242
- }
1243
- const ControlComponent = Control;
1244
- function Wrapped(props) {
1245
- const context = useProbatContext();
1246
- const apiBaseUrl = context?.apiBaseUrl || "https://gushi.onrender.com";
1247
- const contextRepoFullName = context?.repoFullName;
1248
- const [config, setConfig] = useState(null);
1249
- const [configLoading, setConfigLoading] = useState(useNewAPI);
1250
- const [choice, setChoice] = useState(null);
1251
- const repoFullName = options.repoFullName || contextRepoFullName;
1252
- const proposalId = useNewAPI ? config?.proposalId : options.proposalId;
1253
- useEffect(() => {
1254
- if (!useNewAPI) return;
1255
- if (!repoFullName) {
1256
- console.warn("[PROBAT] componentPath provided but repoFullName not found");
1257
- setConfigLoading(false);
1258
- return;
1259
- }
1260
- let alive = true;
1261
- setConfigLoading(true);
1262
- (async () => {
1263
- try {
1264
- const componentConfig = await fetchComponentExperimentConfig(
1265
- apiBaseUrl,
1266
- repoFullName,
1267
- options.componentPath
1268
- );
1269
- if (!alive) return;
1270
- if (!componentConfig) {
1271
- setConfig(null);
1272
- setConfigLoading(false);
1273
- return;
1274
- }
1275
- const variantComponents = {
1276
- control: ControlComponent
1277
- };
1278
- for (const [label2, variantInfo] of Object.entries(componentConfig.variants)) {
1279
- if (label2 === "control") continue;
1280
- if (variantInfo?.file_path) {
1281
- try {
1282
- const VariantComp = await loadVariantComponent(
1283
- apiBaseUrl,
1284
- componentConfig.proposal_id,
1285
- variantInfo.experiment_id,
1286
- variantInfo.file_path,
1287
- componentConfig.repo_full_name,
1288
- componentConfig.base_ref
1289
- );
1290
- if (VariantComp && typeof VariantComp === "function" && alive) {
1291
- variantComponents[label2] = VariantComp;
1292
- }
1293
- } catch (e) {
1294
- console.warn(`[PROBAT] Failed to load variant ${label2}:`, e);
1295
- }
1296
- }
1297
- }
1298
- if (alive) {
1299
- setConfig({
1300
- proposalId: componentConfig.proposal_id,
1301
- variants: variantComponents
1302
- });
1303
- setConfigLoading(false);
1304
- }
1305
- } catch (e) {
1306
- console.warn("[PROBAT] Failed to load component config:", e);
1307
- if (alive) {
1308
- setConfig(null);
1309
- setConfigLoading(false);
1310
- }
1311
- }
1312
- })();
1313
- return () => {
1314
- alive = false;
1315
- };
1316
- }, [useNewAPI, options.componentPath, repoFullName, apiBaseUrl]);
1317
- useEffect(() => {
1318
- if (!proposalId) return;
1319
- if (useNewAPI && configLoading) return;
1320
- let alive = true;
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;
389
+ const containerRef = useRef(null);
390
+ const impressionSent = useRef(false);
391
+ useEffect(() => {
392
+ if (!trackImpression || !resolved) return;
393
+ impressionSent.current = false;
394
+ const pageKey = getPageKey();
395
+ const dedupeKey = makeDedupeKey(id, variantKey, instanceId, pageKey);
396
+ if (hasSeen(dedupeKey)) {
397
+ impressionSent.current = true;
398
+ return;
399
+ }
400
+ const el = containerRef.current;
401
+ if (!el) return;
402
+ if (typeof IntersectionObserver === "undefined") {
403
+ if (!impressionSent.current) {
404
+ impressionSent.current = true;
405
+ markSeen(dedupeKey);
406
+ sendMetric(host, impressionEvent, eventProps);
407
+ if (debug) console.log(`[probat] Impression sent (no IO) for "${id}"`);
1329
408
  }
1330
- const cached = isHeatmapMode ? null : readChoice(proposalId);
1331
- if (cached) {
1332
- const choiceData = {
1333
- experiment_id: cached.experiment_id,
1334
- label: cached.label
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
- }
409
+ return;
410
+ }
411
+ let timer = null;
412
+ const observer = new IntersectionObserver(
413
+ ([entry]) => {
414
+ if (!entry || impressionSent.current) return;
415
+ if (entry.isIntersecting) {
416
+ timer = setTimeout(() => {
417
+ if (impressionSent.current) return;
418
+ impressionSent.current = true;
419
+ markSeen(dedupeKey);
420
+ sendMetric(host, impressionEvent, eventProps);
421
+ if (debug) console.log(`[probat] Impression sent for "${id}"`);
422
+ observer.disconnect();
423
+ }, 250);
424
+ } else if (timer) {
425
+ clearTimeout(timer);
426
+ timer = null;
1344
427
  }
1345
- } else {
1346
- (async () => {
1347
- try {
1348
- const { experiment_id, label: label2 } = await fetchDecision(apiBaseUrl, proposalId);
1349
- if (!alive) return;
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);
1363
- } catch (e) {
1364
- if (!alive) return;
1365
- const choiceData = {
1366
- experiment_id: `exp_${proposalId}`,
1367
- label: "control"
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
- }
1378
- }
1379
- })();
1380
- }
1381
- return () => {
1382
- alive = false;
1383
- };
1384
- }, [proposalId, apiBaseUrl, useNewAPI, configLoading, context.proposalId, context.variantLabel]);
1385
- useEffect(() => {
1386
- if (!proposalId) return;
1387
- const lbl = choice?.label ?? "control";
1388
- if (!lbl || hasTrackedVisit(proposalId, lbl)) return;
1389
- markTrackedVisit(proposalId, lbl);
1390
- void sendMetric(apiBaseUrl, proposalId, "visit", lbl, choice?.experiment_id);
1391
- }, [proposalId, choice?.experiment_id, choice?.label, apiBaseUrl]);
1392
- const trackClick = useCallback(
1393
- (event, opts) => {
1394
- if (!proposalId) return false;
1395
- const lbl = choice?.label ?? "control";
1396
- const meta = extractClickMeta(event ?? void 0);
1397
- if (!opts?.force && event && !meta) return false;
1398
- void sendMetric(apiBaseUrl, proposalId, "click", lbl, void 0, meta);
1399
- return true;
1400
428
  },
1401
- [proposalId, choice?.experiment_id, choice?.label, apiBaseUrl]
429
+ { threshold: 0.5 }
1402
430
  );
1403
- if (useNewAPI && (configLoading || !config || !proposalId)) {
1404
- return React4.createElement(ControlComponent, props);
1405
- }
1406
- if (!proposalId) {
1407
- return React4.createElement(ControlComponent, props);
1408
- }
1409
- const registry = { control: ControlComponent };
1410
- if (useNewAPI && config?.variants) {
1411
- for (const [key, value] of Object.entries(config.variants)) {
1412
- if (key !== "control" && value && typeof value === "function") {
1413
- registry[key] = value;
1414
- }
1415
- }
1416
- } else if (!useNewAPI && options.registry) {
1417
- for (const [key, value] of Object.entries(options.registry)) {
1418
- if (key !== "control" && value && typeof value === "function") {
1419
- registry[key] = value;
1420
- }
431
+ observer.observe(el);
432
+ return () => {
433
+ observer.disconnect();
434
+ if (timer) clearTimeout(timer);
435
+ };
436
+ }, [
437
+ trackImpression,
438
+ resolved,
439
+ id,
440
+ variantKey,
441
+ instanceId,
442
+ host,
443
+ impressionEvent,
444
+ eventProps,
445
+ debug
446
+ ]);
447
+ const handleClick = useCallback(
448
+ (e) => {
449
+ if (!trackClick) return;
450
+ const meta = extractClickMeta(e.target);
451
+ if (!meta) return;
452
+ sendMetric(host, clickEvent, {
453
+ ...eventProps,
454
+ ...meta
455
+ });
456
+ if (debug) {
457
+ console.log(`[probat] Click tracked for "${id}"`, meta);
1421
458
  }
1422
- }
1423
- const label = choice?.label ?? "control";
1424
- const Variant = registry[label] || registry.control || ControlComponent;
1425
- return /* @__PURE__ */ React4.createElement("div", { onClick: (event) => trackClick(event), "data-probat-proposal": proposalId }, React4.createElement(Variant, {
1426
- key: `${proposalId}:${label}`,
1427
- ...props,
1428
- probat: { trackClick: () => trackClick(null, { force: true }) }
1429
- }));
1430
- }
1431
- Wrapped.displayName = `withExperiment(${Control.displayName || Control.name || "Component"})`;
1432
- return Wrapped;
459
+ },
460
+ [trackClick, host, clickEvent, eventProps, id, debug]
461
+ );
462
+ const content = variantKey === "control" || !(variantKey in variants) ? control : variants[variantKey];
463
+ return /* @__PURE__ */ React3.createElement(
464
+ "div",
465
+ {
466
+ ref: containerRef,
467
+ onClick: handleClick,
468
+ "data-probat-experiment": id,
469
+ "data-probat-variant": variantKey,
470
+ style: { display: "contents" }
471
+ },
472
+ content
473
+ );
474
+ }
475
+ function useProbatMetrics() {
476
+ const { host } = useProbatContext();
477
+ const capture = useCallback(
478
+ (event, properties = {}) => {
479
+ sendMetric(host, event, properties);
480
+ },
481
+ [host]
482
+ );
483
+ return { capture };
1433
484
  }
1434
485
 
1435
- export { ProbatProvider, ProbatProviderClient, detectEnvironment, extractClickMeta, fetchDecision, getHeatmapTracker, hasTrackedVisit, initHeatmapTracking, markTrackedVisit, readChoice, sendMetric, stopHeatmapTracking, useExperiment, useProbatContext, useProbatMetrics, withExperiment, writeChoice };
486
+ export { Experiment, ProbatProviderClient, fetchDecision, sendMetric, useProbatMetrics };
1436
487
  //# sourceMappingURL=index.mjs.map
1437
488
  //# sourceMappingURL=index.mjs.map