@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/README.md +88 -0
- package/dist/index.d.mts +83 -2
- package/dist/index.d.ts +83 -2
- package/dist/index.js +596 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +595 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/context/ProbatContext.tsx +77 -2
- package/src/hoc/itrt-frontend.code-workspace +8 -36
- package/src/hoc/withExperiment.tsx +59 -8
- package/src/hooks/useExperiment.ts +28 -4
- package/src/index.ts +2 -0
- package/src/utils/api.ts +22 -0
- package/src/utils/heatmapTracker.ts +665 -0
|
@@ -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,
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}`);
|