@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.
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import React, { createContext, useContext, useMemo } from "react";
3
+ import React, { createContext, useContext, useMemo, useEffect } from "react";
4
4
  import { detectEnvironment } from "../utils/environment";
5
+ import { initHeatmapTracking, stopHeatmapTracking } from "../utils/heatmapTracker";
5
6
 
6
7
  declare global {
7
8
  interface Window {
@@ -14,6 +15,8 @@ export interface ProbatContextValue {
14
15
  environment: "dev" | "prod";
15
16
  clientKey?: string;
16
17
  repoFullName?: string; // Repository full name (e.g., "owner/repo") for component-based experiments
18
+ proposalId?: string;
19
+ variantLabel?: string;
17
20
  }
18
21
 
19
22
  const ProbatContext = createContext<ProbatContextValue | null>(null);
@@ -28,6 +31,14 @@ export interface ProbatProviderProps {
28
31
  * - Default: "https://gushi.onrender.com"
29
32
  */
30
33
  apiBaseUrl?: string;
34
+ /**
35
+ * Optional: proposal/experiment id for heatmap segregation
36
+ */
37
+ proposalId?: string;
38
+ /**
39
+ * Optional: variant label for heatmap segregation
40
+ */
41
+ variantLabel?: string;
31
42
  /**
32
43
  * Client key for identification (optional)
33
44
  */
@@ -53,8 +64,20 @@ export function ProbatProvider({
53
64
  clientKey,
54
65
  environment: explicitEnvironment,
55
66
  repoFullName: explicitRepoFullName,
67
+ proposalId,
68
+ variantLabel,
56
69
  children,
57
70
  }: ProbatProviderProps) {
71
+ // Fallback to localStorage for experiment info if props are not provided
72
+ const storedProposalId =
73
+ typeof window !== "undefined"
74
+ ? window.localStorage.getItem("probat_active_proposal_id") || undefined
75
+ : undefined;
76
+ const storedVariantLabel =
77
+ typeof window !== "undefined"
78
+ ? window.localStorage.getItem("probat_active_variant_label") || undefined
79
+ : undefined;
80
+
58
81
  const contextValue = useMemo<ProbatContextValue>(() => {
59
82
  // Determine API base URL
60
83
  const resolvedApiBaseUrl =
@@ -79,13 +102,65 @@ export function ProbatProvider({
79
102
  (typeof window !== "undefined" && (window as any).__PROBAT_REPO) ||
80
103
  undefined;
81
104
 
105
+ // Check for URL overrides (used for Live Heatmap visualization)
106
+ const params = (typeof window !== "undefined") ? new URLSearchParams(window.location.search) : null;
107
+ const isHeatmapMode = params?.get('heatmap') === 'true';
108
+
109
+ let urlProposalId: string | undefined;
110
+ let urlVariantLabel: string | undefined;
111
+
112
+ if (isHeatmapMode && params) {
113
+ urlProposalId = params.get('proposal_id') || undefined;
114
+ urlVariantLabel = params.get('variant_label') || undefined;
115
+ console.log('[PROBAT] Heatmap mode: Overriding variant from URL', { urlProposalId, urlVariantLabel });
116
+ }
117
+
118
+ // Priority Logic:
119
+ // 1. URL params (if in heatmap mode)
120
+ // 2. Explicit props passed to Provider
121
+ // 3. Stored values in localStorage (ONLY if NOT in heatmap mode)
122
+ const finalProposalId = urlProposalId || proposalId || (!isHeatmapMode ? storedProposalId : undefined);
123
+ const finalVariantLabel = urlVariantLabel || variantLabel || (!isHeatmapMode ? storedVariantLabel : undefined);
124
+
82
125
  return {
83
126
  apiBaseUrl: resolvedApiBaseUrl,
84
127
  environment,
85
128
  clientKey,
86
129
  repoFullName: resolvedRepoFullName,
130
+ proposalId: finalProposalId,
131
+ variantLabel: finalVariantLabel,
132
+ };
133
+ }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName, proposalId, variantLabel, storedProposalId, storedVariantLabel]);
134
+
135
+ // Initialize heatmap tracking when provider mounts
136
+ useEffect(() => {
137
+ // Only initialize on client-side
138
+ if (typeof window !== 'undefined') {
139
+ initHeatmapTracking({
140
+ apiBaseUrl: contextValue.apiBaseUrl,
141
+ batchSize: 10,
142
+ batchInterval: 5000,
143
+ enabled: true,
144
+ excludeSelectors: [
145
+ '[data-heatmap-exclude]',
146
+ 'input[type="password"]',
147
+ 'input[type="email"]',
148
+ 'textarea',
149
+ ],
150
+ // Explicitly enable cursor tracking with sensible defaults
151
+ trackCursor: true,
152
+ cursorThrottle: 100, // capture every 100ms
153
+ cursorBatchSize: 50, // send every 50 movements (or after batchInterval)
154
+ proposalId: contextValue.proposalId,
155
+ variantLabel: contextValue.variantLabel,
156
+ });
157
+ }
158
+
159
+ // Cleanup on unmount
160
+ return () => {
161
+ stopHeatmapTracking();
87
162
  };
88
- }, [apiBaseUrl, clientKey, explicitEnvironment, explicitRepoFullName]);
163
+ }, [contextValue.apiBaseUrl, contextValue.proposalId, contextValue.variantLabel]);
89
164
 
90
165
  return (
91
166
  <ProbatContext.Provider value={contextValue}>
@@ -1,38 +1,10 @@
1
1
  {
2
- "folders": [
3
- {
4
- "path": "../../../../../itrt-frontend"
5
- },
6
- {
7
- "path": "../../../.."
8
- },
9
- {
10
- "path": "../../../../../../onelab_projects/gushi_test_repo"
11
- },
12
- {
13
- "path": "../../../../../../react-jsx-test-repo"
14
- },
15
- {
16
- "path": "../../../../../../testforprobat"
17
- },
18
- {
19
- "path": "../../../../../portfolio"
20
- },
21
- {
22
- "path": "../../../../../portfolio-nextjs"
23
- },
24
- {
25
- "path": "../../../../../react-native-test"
26
- },
27
- {
28
- "path": "../../../../../../tbg_test"
29
- },
30
- {
31
- "path": "../../../../../../probattestforecho"
32
- },
33
- {
34
- "path": "../../../../../zingexample"
35
- }
36
- ],
37
- "settings": {}
2
+ "folders": [
3
+ {
4
+ "path": "../../../.."
5
+ },
6
+ {
7
+ "path": "../../../../../itrt-frontend"
8
+ }
9
+ ]
38
10
  }
@@ -160,32 +160,83 @@ export function withExperiment<P = any>(
160
160
  if (useNewAPI && configLoading) return;
161
161
 
162
162
  let alive = true;
163
- const cached = readChoice(proposalId);
164
163
 
165
- if (cached) {
164
+ // Detect if we are in heatmap mode
165
+ const isHeatmapMode = typeof window !== 'undefined' &&
166
+ new URLSearchParams(window.location.search).get('heatmap') === 'true';
167
+
168
+ // HIGH PRIORITY: Check if context is already forcing a specific variant for this proposal
169
+ // (This happens during Live Heatmap visualization)
170
+ if (context.proposalId === proposalId && context.variantLabel) {
171
+ console.log(`[PROBAT HOC] Forced variant from context: ${context.variantLabel}`);
166
172
  setChoice({
173
+ experiment_id: `forced_${proposalId}`,
174
+ label: context.variantLabel
175
+ });
176
+ return;
177
+ }
178
+
179
+ // If we are in heatmap mode, bypass the cache to avoid showing stale variants
180
+ const cached = isHeatmapMode ? null : readChoice(proposalId);
181
+
182
+ if (cached) {
183
+ const choiceData = {
167
184
  experiment_id: cached.experiment_id,
168
185
  label: cached.label,
169
- });
186
+ };
187
+ setChoice(choiceData);
188
+ // Set localStorage for heatmap tracking
189
+ if (typeof window !== 'undefined' && !isHeatmapMode) {
190
+ try {
191
+ window.localStorage.setItem('probat_active_proposal_id', proposalId);
192
+ window.localStorage.setItem('probat_active_variant_label', cached.label);
193
+ } catch (e) {
194
+ console.warn('[PROBAT] Failed to set proposal/variant in localStorage:', e);
195
+ }
196
+ }
170
197
  } else {
171
198
  (async () => {
172
199
  try {
173
200
  const { experiment_id, label } = await fetchDecision(apiBaseUrl, proposalId);
174
201
  if (!alive) return;
175
- writeChoice(proposalId, experiment_id, label);
176
- setChoice({ experiment_id, label });
202
+
203
+ // Only write to choice cache and localStorage if NOT in heatmap mode
204
+ if (!isHeatmapMode) {
205
+ writeChoice(proposalId, experiment_id, label);
206
+ if (typeof window !== 'undefined') {
207
+ try {
208
+ window.localStorage.setItem('probat_active_proposal_id', proposalId);
209
+ window.localStorage.setItem('probat_active_variant_label', label);
210
+ } catch (e) {
211
+ console.warn('[PROBAT] Failed to set proposal/variant in localStorage:', e);
212
+ }
213
+ }
214
+ }
215
+
216
+ const choiceData = { experiment_id, label };
217
+ setChoice(choiceData);
177
218
  } catch (e) {
178
219
  if (!alive) return;
179
- setChoice({
220
+ const choiceData = {
180
221
  experiment_id: `exp_${proposalId}`,
181
222
  label: "control",
182
- });
223
+ };
224
+ setChoice(choiceData);
225
+
226
+ if (typeof window !== 'undefined' && !isHeatmapMode) {
227
+ try {
228
+ window.localStorage.setItem('probat_active_proposal_id', proposalId);
229
+ window.localStorage.setItem('probat_active_variant_label', 'control');
230
+ } catch (err) {
231
+ console.warn('[PROBAT] Failed to set proposal/variant in localStorage:', err);
232
+ }
233
+ }
183
234
  }
184
235
  })();
185
236
  }
186
237
 
187
238
  return () => { alive = false; };
188
- }, [proposalId, apiBaseUrl, useNewAPI, configLoading]);
239
+ }, [proposalId, apiBaseUrl, useNewAPI, configLoading, context.proposalId, context.variantLabel]);
189
240
 
190
241
  // Track visit
191
242
  useEffect(() => {
@@ -59,7 +59,8 @@ export function useExperiment(
59
59
  proposalId: string,
60
60
  options?: { autoTrackImpression?: boolean }
61
61
  ): UseExperimentReturn {
62
- const { apiBaseUrl } = useProbatContext();
62
+ const context = useProbatContext();
63
+ const { apiBaseUrl } = context;
63
64
  const [choice, setChoice] = useState<{
64
65
  experiment_id: string;
65
66
  label: string;
@@ -73,7 +74,25 @@ export function useExperiment(
73
74
  useEffect(() => {
74
75
  let alive = true;
75
76
 
76
- const cached = readChoice(proposalId);
77
+ // Detect if we are in heatmap mode
78
+ const isHeatmapMode = typeof window !== 'undefined' &&
79
+ new URLSearchParams(window.location.search).get('heatmap') === 'true';
80
+
81
+ // HIGH PRIORITY: Check if context is already forcing a specific variant for this proposal
82
+ // (This happens during Live Heatmap visualization)
83
+ if (context.proposalId === proposalId && context.variantLabel) {
84
+ console.log(`[PROBAT] Forced variant from context: ${context.variantLabel}`);
85
+ setChoice({
86
+ experiment_id: `forced_${proposalId}`,
87
+ label: context.variantLabel
88
+ });
89
+ setIsLoading(false);
90
+ return;
91
+ }
92
+
93
+ // If we are in heatmap mode, bypass the cache to avoid showing stale variants
94
+ const cached = isHeatmapMode ? null : readChoice(proposalId);
95
+
77
96
  if (cached) {
78
97
  setChoice({ experiment_id: cached.experiment_id, label: cached.label });
79
98
  setIsLoading(false);
@@ -86,7 +105,12 @@ export function useExperiment(
86
105
  proposalId
87
106
  );
88
107
  if (!alive) return;
89
- writeChoice(proposalId, experiment_id, label);
108
+
109
+ // Only write to choice cache if NOT in heatmap mode
110
+ if (!isHeatmapMode) {
111
+ writeChoice(proposalId, experiment_id, label);
112
+ }
113
+
90
114
  setChoice({ experiment_id, label });
91
115
  setError(null);
92
116
  } catch (e) {
@@ -108,7 +132,7 @@ export function useExperiment(
108
132
  return () => {
109
133
  alive = false;
110
134
  };
111
- }, [proposalId, apiBaseUrl]);
135
+ }, [proposalId, apiBaseUrl, context.proposalId, context.variantLabel]);
112
136
 
113
137
  // Track impression when variant is determined
114
138
  useEffect(() => {
package/src/index.ts CHANGED
@@ -28,6 +28,8 @@ export type { WithExperimentOptions } from "./hoc/withExperiment";
28
28
  export { detectEnvironment } from "./utils/environment";
29
29
  export { fetchDecision, sendMetric, extractClickMeta } from "./utils/api";
30
30
  export type { RetrieveResponse } from "./utils/api";
31
+ // Heatmap tracking (automatically initialized by ProbatProvider)
32
+ export { initHeatmapTracking, stopHeatmapTracking, getHeatmapTracker } from "./utils/heatmapTracker";
31
33
  export {
32
34
  readChoice,
33
35
  writeChoice,
package/src/utils/api.ts CHANGED
@@ -51,6 +51,16 @@ export async function fetchDecision(
51
51
  const experiment_id = (data.experiment_id || `exp_${proposalId}`).toString();
52
52
  const label = data.label && data.label.trim() ? data.label : "control";
53
53
 
54
+ // Auto-set localStorage for heatmap tracking
55
+ if (typeof window !== 'undefined') {
56
+ try {
57
+ window.localStorage.setItem('probat_active_proposal_id', proposalId);
58
+ window.localStorage.setItem('probat_active_variant_label', label);
59
+ } catch (e) {
60
+ console.warn('[PROBAT] Failed to set proposal/variant in localStorage:', e);
61
+ }
62
+ }
63
+
54
64
  return { experiment_id, label };
55
65
  } finally {
56
66
  // Remove from pending cache after completion
@@ -161,6 +171,18 @@ export async function fetchComponentExperimentConfig(
161
171
  }
162
172
 
163
173
  const data = (await res.json()) as ComponentExperimentConfig;
174
+
175
+ // Auto-set localStorage for heatmap tracking when component config is loaded
176
+ // This will be updated when a specific variant is selected via fetchDecision
177
+ if (typeof window !== 'undefined' && data?.proposal_id) {
178
+ try {
179
+ window.localStorage.setItem('probat_active_proposal_id', data.proposal_id);
180
+ // Variant label will be set when fetchDecision is called
181
+ } catch (e) {
182
+ console.warn('[PROBAT] Failed to set proposal_id in localStorage:', e);
183
+ }
184
+ }
185
+
164
186
  return data;
165
187
  } catch (e) {
166
188
  console.warn(`[PROBAT] Failed to fetch component config: ${e}`);