@probat/react 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +99 -7
- package/dist/index.d.ts +99 -7
- package/dist/index.js +173 -108
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +170 -109
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/Track.test.tsx +125 -0
- package/src/__tests__/createExperimentContext.test.tsx +62 -0
- package/src/__tests__/useExperiment.test.tsx +169 -0
- package/src/__tests__/useTrack.test.tsx +431 -0
- package/src/components/Experiment.tsx +22 -222
- package/src/components/Track.tsx +34 -0
- package/src/hooks/useExperiment.ts +118 -0
- package/src/hooks/useTrack.ts +173 -0
- package/src/index.ts +9 -0
- package/src/utils/createExperimentContext.ts +47 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React$1, { ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
interface ProbatProviderProps {
|
|
4
4
|
/** Your end-user's ID. When provided, used as the distinct_id for variant
|
|
@@ -12,7 +12,7 @@ interface ProbatProviderProps {
|
|
|
12
12
|
* e.g. { "cta-copy-test": "ai_v1" }
|
|
13
13
|
*/
|
|
14
14
|
bootstrap?: Record<string, string>;
|
|
15
|
-
children: React.ReactNode;
|
|
15
|
+
children: React$1.ReactNode;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -34,7 +34,7 @@ interface ProbatProviderProps {
|
|
|
34
34
|
* }
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
declare function ProbatProviderClient(props: ProbatProviderProps): React.FunctionComponentElement<ProbatProviderProps>;
|
|
37
|
+
declare function ProbatProviderClient(props: ProbatProviderProps): React$1.FunctionComponentElement<ProbatProviderProps>;
|
|
38
38
|
|
|
39
39
|
interface ExperimentTrackOptions {
|
|
40
40
|
/** Auto-track impressions (default true) */
|
|
@@ -50,9 +50,9 @@ interface ExperimentProps {
|
|
|
50
50
|
/** Experiment key / identifier */
|
|
51
51
|
id: string;
|
|
52
52
|
/** Control variant ReactNode */
|
|
53
|
-
control: React.ReactNode;
|
|
53
|
+
control: React$1.ReactNode;
|
|
54
54
|
/** Named variant ReactNodes, keyed by variant key (e.g. { ai_v1: <MyVariant /> }) */
|
|
55
|
-
variants: Record<string, React.ReactNode>;
|
|
55
|
+
variants: Record<string, React$1.ReactNode>;
|
|
56
56
|
/** Tracking configuration */
|
|
57
57
|
track?: ExperimentTrackOptions;
|
|
58
58
|
/** Stable instance id when multiple instances of the same experiment exist on a page */
|
|
@@ -62,7 +62,73 @@ interface ExperimentProps {
|
|
|
62
62
|
/** Log decisions + events to console */
|
|
63
63
|
debug?: boolean;
|
|
64
64
|
}
|
|
65
|
-
declare function Experiment({ id, control, variants, track, componentInstanceId, fallback, debug, }: ExperimentProps): React.JSX.Element;
|
|
65
|
+
declare function Experiment({ id, control, variants, track, componentInstanceId, fallback, debug, }: ExperimentProps): React$1.JSX.Element;
|
|
66
|
+
|
|
67
|
+
interface UseTrackOptions {
|
|
68
|
+
/** Experiment identifier */
|
|
69
|
+
experimentId: string;
|
|
70
|
+
/** The variant key to attach to events */
|
|
71
|
+
variantKey: string;
|
|
72
|
+
/** Stable instance id when multiple instances of the same experiment exist on a page */
|
|
73
|
+
componentInstanceId?: string;
|
|
74
|
+
/** Auto-track impressions (default true) */
|
|
75
|
+
impression?: boolean;
|
|
76
|
+
/** Auto-track clicks (default true) */
|
|
77
|
+
click?: boolean;
|
|
78
|
+
/** Custom impression event name (default "$experiment_exposure") */
|
|
79
|
+
impressionEventName?: string;
|
|
80
|
+
/** Custom click event name (default "$experiment_click") */
|
|
81
|
+
clickEventName?: string;
|
|
82
|
+
/** Log events to console */
|
|
83
|
+
debug?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Attaches impression and click tracking to a DOM element via a ref.
|
|
87
|
+
* Completely independent of variant assignment — pass the variantKey explicitly.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* const trackRef = useTrack({ experimentId: "pricing", variantKey: "ai_v1" });
|
|
92
|
+
* return <div ref={trackRef}>...</div>;
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function useTrack(options: UseTrackOptions): React.RefObject<HTMLElement | null>;
|
|
96
|
+
|
|
97
|
+
interface TrackProps extends UseTrackOptions {
|
|
98
|
+
children: React$1.ReactNode;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Wrapper component that attaches impression and click tracking to its children.
|
|
102
|
+
* Alternative to the `useTrack` hook when you prefer a component over a ref.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* <Track experimentId="pricing" variantKey="ai_v1">
|
|
107
|
+
* <PricingCard />
|
|
108
|
+
* </Track>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function Track({ children, ...trackOptions }: TrackProps): React$1.JSX.Element;
|
|
112
|
+
|
|
113
|
+
interface UseExperimentOptions {
|
|
114
|
+
/** Behavior when assignment fetch fails: "control" (default) renders control, "suspend" throws */
|
|
115
|
+
fallback?: "control" | "suspend";
|
|
116
|
+
/** Log decisions to console */
|
|
117
|
+
debug?: boolean;
|
|
118
|
+
}
|
|
119
|
+
interface UseExperimentReturn {
|
|
120
|
+
/** The resolved variant key (e.g. "control", "ai_v1") */
|
|
121
|
+
variantKey: string;
|
|
122
|
+
/** Whether the assignment has been resolved (bootstrap, cache, or fetch) */
|
|
123
|
+
resolved: boolean;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolves the variant assignment for an experiment.
|
|
127
|
+
* No tracking — use `useTrack` or `<Track>` to observe impressions/clicks.
|
|
128
|
+
*
|
|
129
|
+
* Priority: bootstrap > localStorage cache > fetchDecision.
|
|
130
|
+
*/
|
|
131
|
+
declare function useExperiment(id: string, options?: UseExperimentOptions): UseExperimentReturn;
|
|
66
132
|
|
|
67
133
|
interface UseProbatMetricsReturn {
|
|
68
134
|
/**
|
|
@@ -102,4 +168,30 @@ declare function fetchDecision(host: string, experimentId: string, distinctId: s
|
|
|
102
168
|
*/
|
|
103
169
|
declare function sendMetric(host: string, event: string, properties: Record<string, unknown>): void;
|
|
104
170
|
|
|
105
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Factory that creates a typed context + hook pair for passing a variantKey
|
|
173
|
+
* between components in different files without prop-drilling.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* // experiment-context.ts
|
|
178
|
+
* export const { ExperimentProvider, useVariantKey } = createExperimentContext("pricing-test");
|
|
179
|
+
*
|
|
180
|
+
* // Parent.tsx
|
|
181
|
+
* const { variantKey } = useExperiment("pricing-test");
|
|
182
|
+
* <ExperimentProvider value={variantKey}><Child /></ExperimentProvider>
|
|
183
|
+
*
|
|
184
|
+
* // Child.tsx (different file)
|
|
185
|
+
* const variantKey = useVariantKey();
|
|
186
|
+
* const trackRef = useTrack({ experimentId: "pricing-test", variantKey });
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
declare function createExperimentContext(experimentId: string): {
|
|
190
|
+
ExperimentProvider: ({ value, children, }: {
|
|
191
|
+
value: string;
|
|
192
|
+
children: ReactNode;
|
|
193
|
+
}) => React$1.FunctionComponentElement<React$1.ProviderProps<string | null>>;
|
|
194
|
+
useVariantKey: () => string;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, Track, type TrackProps, type UseExperimentOptions, type UseExperimentReturn, type UseProbatMetricsReturn, type UseTrackOptions, createExperimentContext, fetchDecision, sendMetric, useExperiment, useProbatMetrics, useTrack };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React$1, { ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
interface ProbatProviderProps {
|
|
4
4
|
/** Your end-user's ID. When provided, used as the distinct_id for variant
|
|
@@ -12,7 +12,7 @@ interface ProbatProviderProps {
|
|
|
12
12
|
* e.g. { "cta-copy-test": "ai_v1" }
|
|
13
13
|
*/
|
|
14
14
|
bootstrap?: Record<string, string>;
|
|
15
|
-
children: React.ReactNode;
|
|
15
|
+
children: React$1.ReactNode;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -34,7 +34,7 @@ interface ProbatProviderProps {
|
|
|
34
34
|
* }
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
declare function ProbatProviderClient(props: ProbatProviderProps): React.FunctionComponentElement<ProbatProviderProps>;
|
|
37
|
+
declare function ProbatProviderClient(props: ProbatProviderProps): React$1.FunctionComponentElement<ProbatProviderProps>;
|
|
38
38
|
|
|
39
39
|
interface ExperimentTrackOptions {
|
|
40
40
|
/** Auto-track impressions (default true) */
|
|
@@ -50,9 +50,9 @@ interface ExperimentProps {
|
|
|
50
50
|
/** Experiment key / identifier */
|
|
51
51
|
id: string;
|
|
52
52
|
/** Control variant ReactNode */
|
|
53
|
-
control: React.ReactNode;
|
|
53
|
+
control: React$1.ReactNode;
|
|
54
54
|
/** Named variant ReactNodes, keyed by variant key (e.g. { ai_v1: <MyVariant /> }) */
|
|
55
|
-
variants: Record<string, React.ReactNode>;
|
|
55
|
+
variants: Record<string, React$1.ReactNode>;
|
|
56
56
|
/** Tracking configuration */
|
|
57
57
|
track?: ExperimentTrackOptions;
|
|
58
58
|
/** Stable instance id when multiple instances of the same experiment exist on a page */
|
|
@@ -62,7 +62,73 @@ interface ExperimentProps {
|
|
|
62
62
|
/** Log decisions + events to console */
|
|
63
63
|
debug?: boolean;
|
|
64
64
|
}
|
|
65
|
-
declare function Experiment({ id, control, variants, track, componentInstanceId, fallback, debug, }: ExperimentProps): React.JSX.Element;
|
|
65
|
+
declare function Experiment({ id, control, variants, track, componentInstanceId, fallback, debug, }: ExperimentProps): React$1.JSX.Element;
|
|
66
|
+
|
|
67
|
+
interface UseTrackOptions {
|
|
68
|
+
/** Experiment identifier */
|
|
69
|
+
experimentId: string;
|
|
70
|
+
/** The variant key to attach to events */
|
|
71
|
+
variantKey: string;
|
|
72
|
+
/** Stable instance id when multiple instances of the same experiment exist on a page */
|
|
73
|
+
componentInstanceId?: string;
|
|
74
|
+
/** Auto-track impressions (default true) */
|
|
75
|
+
impression?: boolean;
|
|
76
|
+
/** Auto-track clicks (default true) */
|
|
77
|
+
click?: boolean;
|
|
78
|
+
/** Custom impression event name (default "$experiment_exposure") */
|
|
79
|
+
impressionEventName?: string;
|
|
80
|
+
/** Custom click event name (default "$experiment_click") */
|
|
81
|
+
clickEventName?: string;
|
|
82
|
+
/** Log events to console */
|
|
83
|
+
debug?: boolean;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Attaches impression and click tracking to a DOM element via a ref.
|
|
87
|
+
* Completely independent of variant assignment — pass the variantKey explicitly.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```tsx
|
|
91
|
+
* const trackRef = useTrack({ experimentId: "pricing", variantKey: "ai_v1" });
|
|
92
|
+
* return <div ref={trackRef}>...</div>;
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function useTrack(options: UseTrackOptions): React.RefObject<HTMLElement | null>;
|
|
96
|
+
|
|
97
|
+
interface TrackProps extends UseTrackOptions {
|
|
98
|
+
children: React$1.ReactNode;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Wrapper component that attaches impression and click tracking to its children.
|
|
102
|
+
* Alternative to the `useTrack` hook when you prefer a component over a ref.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* <Track experimentId="pricing" variantKey="ai_v1">
|
|
107
|
+
* <PricingCard />
|
|
108
|
+
* </Track>
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
declare function Track({ children, ...trackOptions }: TrackProps): React$1.JSX.Element;
|
|
112
|
+
|
|
113
|
+
interface UseExperimentOptions {
|
|
114
|
+
/** Behavior when assignment fetch fails: "control" (default) renders control, "suspend" throws */
|
|
115
|
+
fallback?: "control" | "suspend";
|
|
116
|
+
/** Log decisions to console */
|
|
117
|
+
debug?: boolean;
|
|
118
|
+
}
|
|
119
|
+
interface UseExperimentReturn {
|
|
120
|
+
/** The resolved variant key (e.g. "control", "ai_v1") */
|
|
121
|
+
variantKey: string;
|
|
122
|
+
/** Whether the assignment has been resolved (bootstrap, cache, or fetch) */
|
|
123
|
+
resolved: boolean;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolves the variant assignment for an experiment.
|
|
127
|
+
* No tracking — use `useTrack` or `<Track>` to observe impressions/clicks.
|
|
128
|
+
*
|
|
129
|
+
* Priority: bootstrap > localStorage cache > fetchDecision.
|
|
130
|
+
*/
|
|
131
|
+
declare function useExperiment(id: string, options?: UseExperimentOptions): UseExperimentReturn;
|
|
66
132
|
|
|
67
133
|
interface UseProbatMetricsReturn {
|
|
68
134
|
/**
|
|
@@ -102,4 +168,30 @@ declare function fetchDecision(host: string, experimentId: string, distinctId: s
|
|
|
102
168
|
*/
|
|
103
169
|
declare function sendMetric(host: string, event: string, properties: Record<string, unknown>): void;
|
|
104
170
|
|
|
105
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Factory that creates a typed context + hook pair for passing a variantKey
|
|
173
|
+
* between components in different files without prop-drilling.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```tsx
|
|
177
|
+
* // experiment-context.ts
|
|
178
|
+
* export const { ExperimentProvider, useVariantKey } = createExperimentContext("pricing-test");
|
|
179
|
+
*
|
|
180
|
+
* // Parent.tsx
|
|
181
|
+
* const { variantKey } = useExperiment("pricing-test");
|
|
182
|
+
* <ExperimentProvider value={variantKey}><Child /></ExperimentProvider>
|
|
183
|
+
*
|
|
184
|
+
* // Child.tsx (different file)
|
|
185
|
+
* const variantKey = useVariantKey();
|
|
186
|
+
* const trackRef = useTrack({ experimentId: "pricing-test", variantKey });
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
declare function createExperimentContext(experimentId: string): {
|
|
190
|
+
ExperimentProvider: ({ value, children, }: {
|
|
191
|
+
value: string;
|
|
192
|
+
children: ReactNode;
|
|
193
|
+
}) => React$1.FunctionComponentElement<React$1.ProviderProps<string | null>>;
|
|
194
|
+
useVariantKey: () => string;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export { type DecisionResponse, Experiment, type ExperimentProps, type ExperimentTrackOptions, type MetricPayload, ProbatProviderClient, type ProbatProviderProps, Track, type TrackProps, type UseExperimentOptions, type UseExperimentReturn, type UseProbatMetricsReturn, type UseTrackOptions, createExperimentContext, fetchDecision, sendMetric, useExperiment, useProbatMetrics, useTrack };
|
package/dist/index.js
CHANGED
|
@@ -204,6 +204,75 @@ function buildMeta(el, isPrimary) {
|
|
|
204
204
|
return meta;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
// src/hooks/useExperiment.ts
|
|
208
|
+
var ASSIGNMENT_PREFIX = "probat:assignment:";
|
|
209
|
+
function readAssignment(id) {
|
|
210
|
+
if (typeof window === "undefined") return null;
|
|
211
|
+
try {
|
|
212
|
+
const raw = localStorage.getItem(ASSIGNMENT_PREFIX + id);
|
|
213
|
+
if (!raw) return null;
|
|
214
|
+
const parsed = JSON.parse(raw);
|
|
215
|
+
return parsed.variantKey ?? null;
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function writeAssignment(id, variantKey) {
|
|
221
|
+
if (typeof window === "undefined") return;
|
|
222
|
+
try {
|
|
223
|
+
const entry = { variantKey, ts: Date.now() };
|
|
224
|
+
localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function useExperiment(id, options = {}) {
|
|
229
|
+
const { fallback = "control", debug = false } = options;
|
|
230
|
+
const { host, bootstrap, customerId } = useProbatContext();
|
|
231
|
+
const [variantKey, setVariantKey] = React3.useState(() => {
|
|
232
|
+
if (bootstrap[id]) return bootstrap[id];
|
|
233
|
+
return "control";
|
|
234
|
+
});
|
|
235
|
+
const [resolved, setResolved] = React3.useState(() => {
|
|
236
|
+
return !!bootstrap[id];
|
|
237
|
+
});
|
|
238
|
+
React3.useEffect(() => {
|
|
239
|
+
if (bootstrap[id] || readAssignment(id)) {
|
|
240
|
+
const key = bootstrap[id] ?? readAssignment(id) ?? "control";
|
|
241
|
+
setVariantKey(key);
|
|
242
|
+
setResolved(true);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
let cancelled = false;
|
|
246
|
+
(async () => {
|
|
247
|
+
try {
|
|
248
|
+
const distinctId = customerId ?? getDistinctId();
|
|
249
|
+
const key = await fetchDecision(host, id, distinctId);
|
|
250
|
+
if (cancelled) return;
|
|
251
|
+
setVariantKey(key);
|
|
252
|
+
writeAssignment(id, key);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
if (cancelled) return;
|
|
255
|
+
if (debug) {
|
|
256
|
+
console.error(`[probat] fetchDecision failed for "${id}":`, err);
|
|
257
|
+
}
|
|
258
|
+
if (fallback === "suspend") throw err;
|
|
259
|
+
setVariantKey("control");
|
|
260
|
+
} finally {
|
|
261
|
+
if (!cancelled) setResolved(true);
|
|
262
|
+
}
|
|
263
|
+
})();
|
|
264
|
+
return () => {
|
|
265
|
+
cancelled = true;
|
|
266
|
+
};
|
|
267
|
+
}, [id, host]);
|
|
268
|
+
React3.useEffect(() => {
|
|
269
|
+
if (debug && resolved) {
|
|
270
|
+
console.log(`[probat] Experiment "${id}" -> variant "${variantKey}"`);
|
|
271
|
+
}
|
|
272
|
+
}, [debug, id, variantKey, resolved]);
|
|
273
|
+
return { variantKey, resolved };
|
|
274
|
+
}
|
|
275
|
+
|
|
207
276
|
// src/utils/dedupeStorage.ts
|
|
208
277
|
var PREFIX = "probat:seen:";
|
|
209
278
|
var memorySet = /* @__PURE__ */ new Set();
|
|
@@ -291,113 +360,37 @@ function useStableInstanceIdFallback(experimentId) {
|
|
|
291
360
|
}
|
|
292
361
|
var useStableInstanceId = typeof React3__default.default.useId === "function" ? useStableInstanceIdV18 : useStableInstanceIdFallback;
|
|
293
362
|
|
|
294
|
-
// src/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (typeof window === "undefined") return;
|
|
309
|
-
try {
|
|
310
|
-
const entry = { variantKey, ts: Date.now() };
|
|
311
|
-
localStorage.setItem(ASSIGNMENT_PREFIX + id, JSON.stringify(entry));
|
|
312
|
-
} catch {
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
function Experiment({
|
|
316
|
-
id,
|
|
317
|
-
control,
|
|
318
|
-
variants,
|
|
319
|
-
track,
|
|
320
|
-
componentInstanceId,
|
|
321
|
-
fallback = "control",
|
|
322
|
-
debug = false
|
|
323
|
-
}) {
|
|
324
|
-
const { host, bootstrap, customerId } = useProbatContext();
|
|
325
|
-
const autoInstanceId = useStableInstanceId(id);
|
|
363
|
+
// src/hooks/useTrack.ts
|
|
364
|
+
function useTrack(options) {
|
|
365
|
+
const {
|
|
366
|
+
experimentId,
|
|
367
|
+
variantKey,
|
|
368
|
+
componentInstanceId,
|
|
369
|
+
impression: trackImpression = true,
|
|
370
|
+
click: trackClick = true,
|
|
371
|
+
impressionEventName = "$experiment_exposure",
|
|
372
|
+
clickEventName = "$experiment_click",
|
|
373
|
+
debug = false
|
|
374
|
+
} = options;
|
|
375
|
+
const { host, customerId } = useProbatContext();
|
|
376
|
+
const autoInstanceId = useStableInstanceId(experimentId);
|
|
326
377
|
const instanceId = componentInstanceId ?? autoInstanceId;
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
const impressionEvent = track?.impressionEventName ?? "$experiment_exposure";
|
|
330
|
-
const clickEvent = track?.clickEventName ?? "$experiment_click";
|
|
331
|
-
const [variantKey, setVariantKey] = React3.useState(() => {
|
|
332
|
-
if (bootstrap[id]) return bootstrap[id];
|
|
333
|
-
return "control";
|
|
334
|
-
});
|
|
335
|
-
const [resolved, setResolved] = React3.useState(() => {
|
|
336
|
-
return !!bootstrap[id];
|
|
337
|
-
});
|
|
338
|
-
React3.useEffect(() => {
|
|
339
|
-
if (bootstrap[id] || readAssignment(id)) {
|
|
340
|
-
const key = bootstrap[id] ?? readAssignment(id) ?? "control";
|
|
341
|
-
setVariantKey(key);
|
|
342
|
-
setResolved(true);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
let cancelled = false;
|
|
346
|
-
(async () => {
|
|
347
|
-
try {
|
|
348
|
-
const distinctId = customerId ?? getDistinctId();
|
|
349
|
-
const key = await fetchDecision(host, id, distinctId);
|
|
350
|
-
if (cancelled) return;
|
|
351
|
-
if (key !== "control" && !(key in variants)) {
|
|
352
|
-
if (debug) {
|
|
353
|
-
console.warn(
|
|
354
|
-
`[probat] Unknown variant "${key}" for experiment "${id}", falling back to control`
|
|
355
|
-
);
|
|
356
|
-
}
|
|
357
|
-
setVariantKey("control");
|
|
358
|
-
} else {
|
|
359
|
-
setVariantKey(key);
|
|
360
|
-
writeAssignment(id, key);
|
|
361
|
-
}
|
|
362
|
-
} catch (err) {
|
|
363
|
-
if (cancelled) return;
|
|
364
|
-
if (debug) {
|
|
365
|
-
console.error(`[probat] fetchDecision failed for "${id}":`, err);
|
|
366
|
-
}
|
|
367
|
-
if (fallback === "suspend") throw err;
|
|
368
|
-
setVariantKey("control");
|
|
369
|
-
} finally {
|
|
370
|
-
if (!cancelled) setResolved(true);
|
|
371
|
-
}
|
|
372
|
-
})();
|
|
373
|
-
return () => {
|
|
374
|
-
cancelled = true;
|
|
375
|
-
};
|
|
376
|
-
}, [id, host]);
|
|
377
|
-
React3.useEffect(() => {
|
|
378
|
-
if (debug && resolved) {
|
|
379
|
-
console.log(`[probat] Experiment "${id}" \u2192 variant "${variantKey}"`, {
|
|
380
|
-
instanceId,
|
|
381
|
-
pageKey: getPageKey()
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}, [debug, id, variantKey, resolved, instanceId]);
|
|
378
|
+
const containerRef = React3.useRef(null);
|
|
379
|
+
const impressionSent = React3.useRef(false);
|
|
385
380
|
const eventProps = React3.useMemo(
|
|
386
381
|
() => ({
|
|
387
|
-
experiment_id:
|
|
382
|
+
experiment_id: experimentId,
|
|
388
383
|
variant_key: variantKey,
|
|
389
384
|
component_instance_id: instanceId,
|
|
390
385
|
...customerId ? { distinct_id: customerId } : {}
|
|
391
386
|
}),
|
|
392
|
-
[
|
|
387
|
+
[experimentId, variantKey, instanceId, customerId]
|
|
393
388
|
);
|
|
394
|
-
const containerRef = React3.useRef(null);
|
|
395
|
-
const impressionSent = React3.useRef(false);
|
|
396
389
|
React3.useEffect(() => {
|
|
397
|
-
if (!trackImpression
|
|
390
|
+
if (!trackImpression) return;
|
|
398
391
|
impressionSent.current = false;
|
|
399
392
|
const pageKey = getPageKey();
|
|
400
|
-
const dedupeKey = makeDedupeKey(
|
|
393
|
+
const dedupeKey = makeDedupeKey(experimentId, variantKey, instanceId, pageKey);
|
|
401
394
|
if (hasSeen(dedupeKey)) {
|
|
402
395
|
impressionSent.current = true;
|
|
403
396
|
return;
|
|
@@ -408,8 +401,8 @@ function Experiment({
|
|
|
408
401
|
if (!impressionSent.current) {
|
|
409
402
|
impressionSent.current = true;
|
|
410
403
|
markSeen(dedupeKey);
|
|
411
|
-
sendMetric(host,
|
|
412
|
-
if (debug) console.log(`[probat] Impression sent (no IO) for "${
|
|
404
|
+
sendMetric(host, impressionEventName, eventProps);
|
|
405
|
+
if (debug) console.log(`[probat] Impression sent (no IO) for "${experimentId}"`);
|
|
413
406
|
}
|
|
414
407
|
return;
|
|
415
408
|
}
|
|
@@ -422,8 +415,8 @@ function Experiment({
|
|
|
422
415
|
if (impressionSent.current) return;
|
|
423
416
|
impressionSent.current = true;
|
|
424
417
|
markSeen(dedupeKey);
|
|
425
|
-
sendMetric(host,
|
|
426
|
-
if (debug) console.log(`[probat] Impression sent for "${
|
|
418
|
+
sendMetric(host, impressionEventName, eventProps);
|
|
419
|
+
if (debug) console.log(`[probat] Impression sent for "${experimentId}"`);
|
|
427
420
|
observer.disconnect();
|
|
428
421
|
}, 250);
|
|
429
422
|
} else if (timer) {
|
|
@@ -440,12 +433,11 @@ function Experiment({
|
|
|
440
433
|
};
|
|
441
434
|
}, [
|
|
442
435
|
trackImpression,
|
|
443
|
-
|
|
444
|
-
id,
|
|
436
|
+
experimentId,
|
|
445
437
|
variantKey,
|
|
446
438
|
instanceId,
|
|
447
439
|
host,
|
|
448
|
-
|
|
440
|
+
impressionEventName,
|
|
449
441
|
eventProps,
|
|
450
442
|
debug
|
|
451
443
|
]);
|
|
@@ -454,22 +446,59 @@ function Experiment({
|
|
|
454
446
|
if (!trackClick) return;
|
|
455
447
|
const meta = extractClickMeta(e.target);
|
|
456
448
|
if (!meta) return;
|
|
457
|
-
sendMetric(host,
|
|
449
|
+
sendMetric(host, clickEventName, {
|
|
458
450
|
...eventProps,
|
|
459
451
|
...meta
|
|
460
452
|
});
|
|
461
453
|
if (debug) {
|
|
462
|
-
console.log(`[probat] Click tracked for "${
|
|
454
|
+
console.log(`[probat] Click tracked for "${experimentId}"`, meta);
|
|
463
455
|
}
|
|
464
456
|
},
|
|
465
|
-
[trackClick, host,
|
|
457
|
+
[trackClick, host, clickEventName, eventProps, experimentId, debug]
|
|
466
458
|
);
|
|
459
|
+
React3.useEffect(() => {
|
|
460
|
+
const el = containerRef.current;
|
|
461
|
+
if (!el || !trackClick) return;
|
|
462
|
+
el.addEventListener("click", handleClick);
|
|
463
|
+
return () => {
|
|
464
|
+
el.removeEventListener("click", handleClick);
|
|
465
|
+
};
|
|
466
|
+
}, [handleClick, trackClick]);
|
|
467
|
+
return containerRef;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/components/Experiment.tsx
|
|
471
|
+
function Experiment({
|
|
472
|
+
id,
|
|
473
|
+
control,
|
|
474
|
+
variants,
|
|
475
|
+
track,
|
|
476
|
+
componentInstanceId,
|
|
477
|
+
fallback = "control",
|
|
478
|
+
debug = false
|
|
479
|
+
}) {
|
|
480
|
+
const { variantKey: rawKey, resolved } = useExperiment(id, { fallback, debug });
|
|
481
|
+
const variantKey = rawKey === "control" || rawKey in variants ? rawKey : "control";
|
|
482
|
+
if (debug && rawKey !== variantKey) {
|
|
483
|
+
console.warn(
|
|
484
|
+
`[probat] Unknown variant "${rawKey}" for experiment "${id}", falling back to control`
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
const trackRef = useTrack({
|
|
488
|
+
experimentId: id,
|
|
489
|
+
variantKey,
|
|
490
|
+
componentInstanceId,
|
|
491
|
+
impression: resolved ? track?.impression !== false : false,
|
|
492
|
+
click: track?.primaryClick !== false,
|
|
493
|
+
impressionEventName: track?.impressionEventName,
|
|
494
|
+
clickEventName: track?.clickEventName,
|
|
495
|
+
debug
|
|
496
|
+
});
|
|
467
497
|
const content = variantKey === "control" || !(variantKey in variants) ? control : variants[variantKey];
|
|
468
498
|
return /* @__PURE__ */ React3__default.default.createElement(
|
|
469
499
|
"div",
|
|
470
500
|
{
|
|
471
|
-
ref:
|
|
472
|
-
onClick: handleClick,
|
|
501
|
+
ref: trackRef,
|
|
473
502
|
"data-probat-experiment": id,
|
|
474
503
|
"data-probat-variant": variantKey,
|
|
475
504
|
style: {
|
|
@@ -483,6 +512,19 @@ function Experiment({
|
|
|
483
512
|
content
|
|
484
513
|
);
|
|
485
514
|
}
|
|
515
|
+
function Track({ children, ...trackOptions }) {
|
|
516
|
+
const trackRef = useTrack(trackOptions);
|
|
517
|
+
return /* @__PURE__ */ React3__default.default.createElement(
|
|
518
|
+
"div",
|
|
519
|
+
{
|
|
520
|
+
ref: trackRef,
|
|
521
|
+
"data-probat-track": trackOptions.experimentId,
|
|
522
|
+
"data-probat-variant": trackOptions.variantKey,
|
|
523
|
+
style: { display: "contents" }
|
|
524
|
+
},
|
|
525
|
+
children
|
|
526
|
+
);
|
|
527
|
+
}
|
|
486
528
|
function useProbatMetrics() {
|
|
487
529
|
const { host, customerId } = useProbatContext();
|
|
488
530
|
const capture = React3.useCallback(
|
|
@@ -496,11 +538,34 @@ function useProbatMetrics() {
|
|
|
496
538
|
);
|
|
497
539
|
return { capture };
|
|
498
540
|
}
|
|
541
|
+
function createExperimentContext(experimentId) {
|
|
542
|
+
const Ctx = React3.createContext(null);
|
|
543
|
+
function ExperimentProvider({
|
|
544
|
+
value,
|
|
545
|
+
children
|
|
546
|
+
}) {
|
|
547
|
+
return React3__default.default.createElement(Ctx.Provider, { value }, children);
|
|
548
|
+
}
|
|
549
|
+
function useVariantKey() {
|
|
550
|
+
const v = React3.useContext(Ctx);
|
|
551
|
+
if (v === null) {
|
|
552
|
+
throw new Error(
|
|
553
|
+
`useVariantKey() must be used inside <ExperimentProvider> for "${experimentId}"`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
return v;
|
|
557
|
+
}
|
|
558
|
+
return { ExperimentProvider, useVariantKey };
|
|
559
|
+
}
|
|
499
560
|
|
|
500
561
|
exports.Experiment = Experiment;
|
|
501
562
|
exports.ProbatProviderClient = ProbatProviderClient;
|
|
563
|
+
exports.Track = Track;
|
|
564
|
+
exports.createExperimentContext = createExperimentContext;
|
|
502
565
|
exports.fetchDecision = fetchDecision;
|
|
503
566
|
exports.sendMetric = sendMetric;
|
|
567
|
+
exports.useExperiment = useExperiment;
|
|
504
568
|
exports.useProbatMetrics = useProbatMetrics;
|
|
569
|
+
exports.useTrack = useTrack;
|
|
505
570
|
//# sourceMappingURL=index.js.map
|
|
506
571
|
//# sourceMappingURL=index.js.map
|